沙箱支付成功后未调用回调接口,如何正确配置配置文件
结论
沙箱支付已经成功,但请求没有进入 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 开头的日志也应该出现。如果没有日志,依次检查:
- 域名是否解析到当前服务器。
- 端口是否开放。
- Nginx、网关或反向代理是否转发了
/api/alipay/notify。 - 项目是否配置了
server.servlet.context-path。 - Controller 的类级路径和方法级路径拼接后,是否与
notify_url完全一致。 - Spring Security、Shiro、Sa-Token 或自定义拦截器是否拦截了回调。
- HTTPS 证书是否有效。
- 防火墙、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。
备注:内容仅供参考。
