PAYATHON 2026

沙箱支付成功后未调用回调接口,如何正确配置配置文件

支付老李

结论

沙箱支付已经成功,但请求没有进入 Controller,通常与 Mapper 无关,更可能是支付宝无法访问异步通知地址。先检查创建支付请求时是否正确传入 notify_url,该地址能否通过公网 HTTPS/HTTP 访问,以及 Controller 是否支持接收 POST 表单请求。

如果后台连“支付宝异步回调”这条日志都没有,说明请求还没到达 Controller,此时检查 Mapper 没有意义。等确认 Controller 收到通知后,再排查数据库更新逻辑。

正确配置支付参数

异步通知地址必须是完整的公网 URL,不能使用 localhost、127.0.0.1 或局域网地址。

alipay:
  app-id: your_app_id
  private-key: your_private_key
  alipay-public-key: your_alipay_public_key
  gateway-url: https://openapi-sandbox.dl.alipaydev.com/gateway.do
  notify-url: https://example.com/api/alipay/notify
  return-url: https://example.com/pay/result
  charset: UTF-8
  sign-type: RSA2

也可以使用 application.properties:

alipay.app-id=your_app_id
alipay.private-key=your_private_key
alipay.alipay-public-key=your_alipay_public_key
alipay.gateway-url=https://openapi-sandbox.dl.alipaydev.com/gateway.do
alipay.notify-url=https://example.com/api/alipay/notify
alipay.return-url=https://example.com/pay/result
alipay.charset=UTF-8
alipay.sign-type=RSA2

notify_url 和 return_url 的用途不同:

  • notify_url:支付宝服务器主动发送异步 POST 通知,用于更新订单状态。
  • return_url:用户完成支付后,浏览器跳转到的同步地址,不能作为确认支付成功的最终依据。

配置文件中的字段名还要与配置类一致。例如:

@Component
@ConfigurationProperties(prefix = "alipay")
public class AlipayProperties {

    private String appId;
    private String privateKey;
    private String alipayPublicKey;
    private String gatewayUrl;
    private String notifyUrl;
    private String returnUrl;
    private String charset;
    private String signType;

    // getter 和 setter
}

如果项目没有启用配置属性扫描,需要添加 @Component,或者在启动类中使用:

@EnableConfigurationProperties(AlipayProperties.class)

确认支付请求真正传入了 notify_url

只在配置文件中填写通知地址还不够。创建支付请求时,必须把它设置到支付宝请求对象中。

AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();

request.setNotifyUrl(alipayProperties.getNotifyUrl());
request.setReturnUrl(alipayProperties.getReturnUrl());

request.setBizContent(objectMapper.writeValueAsString(bizContent));

AlipayResponse response = alipayClient.pageExecute(request);

发起支付前,可以记录最终使用的通知地址:

log.info("支付宝 notify_url: {}", alipayProperties.getNotifyUrl());

如果日志输出的是 null、空字符串或错误路径,先确认配置文件是否已加载,再检查当前运行环境是否启用了其他 Spring Profile,例如:

spring.profiles.active=dev

启用 dev 后,实际生效的可能是 application-dev.yml,而不是 application.yml。

Controller 应接收 POST 表单请求

支付宝的异步通知通常以 application/x-www-form-urlencoded 格式发送。回调接口应支持 POST 请求,不能只配置 @GetMapping。

@RestController
@RequestMapping("/api/alipay")
public class AlipayNotifyController {

    private static final Logger log =
            LoggerFactory.getLogger(AlipayNotifyController.class);

    @PostMapping("/notify")
    public String notify(HttpServletRequest request) {
        log.info("========= 支付宝异步回调 ========");

        Map<String, String> params = new HashMap<>();

        request.getParameterMap().forEach((name, values) -> {
            params.put(name, String.join(",", values));
        });

        try {
            boolean verified = AlipaySignature.rsaCheckV1(
                    params,
                    alipayProperties.getAlipayPublicKey(),
                    alipayProperties.getCharset(),
                    alipayProperties.getSignType()
            );

            if (!verified) {
                log.warn("支付宝异步通知验签失败");
                return "failure";
            }

            String tradeStatus = params.get("trade_status");
            String outTradeNo = params.get("out_trade_no");
            String tradeNo = params.get("trade_no");

            if ("TRADE_SUCCESS".equals(tradeStatus)
                    || "TRADE_FINISHED".equals(tradeStatus)) {
                orderService.handlePaid(outTradeNo, tradeNo);
            }

            return "success";
        } catch (Exception e) {
            log.error("处理支付宝异步通知失败", e);
            return "failure";
        }
    }
}

处理成功后,接口必须直接返回以下纯文本:

success

不能返回 JSON:

{"status":"success"}

也不能返回 HTML 页面。如果响应内容不符合要求,支付宝可能继续重试通知。

检查接口是否真的能从公网访问

可以先绕过支付宝,直接测试回调地址:

curl -i -X POST \
  -d "out_trade_no=test_order&trade_status=TRADE_SUCCESS" \
  https://example.com/api/alipay/notify

即使这次请求验签失败,Controller 开头的日志也应该出现。如果没有日志,依次检查:

  1. 域名是否解析到当前服务器。
  2. 端口是否开放。
  3. Nginx、网关或反向代理是否转发了 /api/alipay/notify。
  4. 项目是否配置了 server.servlet.context-path。
  5. Controller 的类级路径和方法级路径拼接后,是否与 notify_url 完全一致。
  6. Spring Security、Shiro、Sa-Token 或自定义拦截器是否拦截了回调。
  7. HTTPS 证书是否有效。
  8. 防火墙、WAF 或访问白名单是否阻止了外部请求。

例如,项目配置了:

server:
  servlet:
    context-path: /shop

实际通知地址就要包含 /shop:

https://example.com/shop/api/alipay/notify

本地开发时,可以使用支持公网映射的内网穿透工具,但映射地址必须稳定,并且能被外部服务器访问。具体工具和域名规则取决于实际环境。

放行安全校验和登录拦截

支付回调由支付宝服务器发起,不带用户登录状态,因此不能要求登录,也不应执行普通页面使用的 CSRF 校验。

Spring Security 可以根据实际版本和项目结构放行该路径。下面只展示配置思路:

http.authorizeHttpRequests(auth -> auth
        .requestMatchers("/api/alipay/notify").permitAll()
        .anyRequest().authenticated()
);

如果项目启用了 CSRF,还要针对通知接口配置忽略:

http.csrf(csrf -> csrf
        .ignoringRequestMatchers("/api/alipay/notify")
);

不需要关闭整个项目的安全控制,只放行准确的回调路径即可。

Mapper 应在验签成功后执行

Mapper 不负责接收支付宝通知。正确的调用顺序是:

支付宝通知
→ Controller 收到 POST 请求
→ 验签
→ 校验订单信息
→ Service 处理业务
→ Mapper 更新订单
→ 返回 success

Mapper 可以使用带状态条件的更新语句,防止重复通知引起重复处理:

@Mapper
public interface OrderMapper {

    @Update("""
        UPDATE orders
        SET pay_status = 'PAID',
            alipay_trade_no = #{tradeNo},
            paid_at = CURRENT_TIMESTAMP
        WHERE order_no = #{orderNo}
          AND pay_status = 'UNPAID'
        """)
    int markAsPaid(
            @Param("orderNo") String orderNo,
            @Param("tradeNo") String tradeNo
    );
}

Service 根据更新结果继续处理:

@Transactional
public void handlePaid(String orderNo, String tradeNo) {
    int affectedRows = orderMapper.markAsPaid(orderNo, tradeNo);

    if (affectedRows == 0) {
        log.info("订单未更新,可能已经处理或订单不存在,orderNo={}", orderNo);
        return;
    }

    log.info("订单支付状态更新成功,orderNo={}, tradeNo={}",
            orderNo, tradeNo);
}

支付宝可能重复发送异步通知,因此业务逻辑必须具备幂等性。不能每收到一次通知,就重复发货、增加余额或生成权益。

验签之外还要校验业务参数

验签通过只能说明通知内容没有被篡改,还需要检查:

  • app_id 是否属于当前应用。
  • out_trade_no 是否存在。
  • 订单是否已经支付。
  • total_amount 是否与本地订单金额一致。
  • seller_id 是否为预期收款账号。
  • trade_status 是否为可处理的成功状态。

金额可以使用 BigDecimal 比较:

BigDecimal notifiedAmount =
        new BigDecimal(params.get("total_amount"));

if (order.getTotalAmount().compareTo(notifiedAmount) != 0) {
    log.warn("支付金额不一致,orderNo={}", order.getOrderNo());
    return "failure";
}

验签时必须使用支付宝公钥,不能使用应用公钥。沙箱应用和正式应用的密钥、网关、应用 ID 也不能混用。

根据日志位置判断问题范围

如果 Controller 第一行日志没有出现,检查:

  • notify_url 是否传入支付请求;
  • 通知地址能否通过公网访问;
  • 请求路径是否与 Context Path 一致;
  • 网关、安全组件或防火墙是否拦截请求;
  • Controller 是否支持接收 POST。

如果 Controller 日志已经出现,但 Mapper 前的日志没有出现,检查:

  • 验签是否失败;
  • trade_status 判断是否正确;
  • 参数名称是否正确;
  • Service 是否抛出异常;
  • 代码是否提前 return。

如果 Mapper 已经执行,但订单数据没有变化,检查:

  • SQL 的 WHERE 条件;
  • @Param 名称是否与 SQL 参数一致;
  • 数据库连接是否指向预期环境;
  • 事务是否回滚;
  • 订单号和当前订单状态是否匹配。

就当前现象而言,应该先检查 notify_url 和 Controller 的公网可达性,不必直接修改 Mapper。

备注:内容仅供参考。