App 调用支付宝支付成功后,后端如何获取支付结果?
明确结论
后端应以支付宝服务器发送到 notify_url 的异步通知为准。完成验签,并核对金额和订单信息后,再通过数据库事务更新订单。
App 客户端返回的”支付成功”只适合用于页面提示,不能作为后端确认收款或发货的依据。客户端结果可能被伪造,也可能因网络中断而与实际支付状态不一致。
如果异步通知未及时送达,后端可以调用支付宝交易查询接口 alipay.trade.query 主动核实,再通过定时任务进行补偿和对账。
推荐处理流程
完整流程如下:
- 后端创建本地订单,并生成唯一的商户订单号
out_trade_no。 - 后端调用
alipay.trade.app.pay生成 App 支付参数,同时设置公网可访问的notify_url。 - App 使用后端返回的参数调用支付宝 SDK。
- 用户完成支付后,App 可以显示客户端返回的结果,但不能直接修改订单状态。
- 支付宝服务器向
notify_url发送异步通知。 - 后端验证支付宝签名,并核对订单号、金额、应用 ID、收款方等字段。
- 校验通过后,在数据库事务中更新订单和支付流水。
- 后端向支付宝返回纯文本
success。 - 如果没有收到通知,调用
alipay.trade.query主动查询支付状态。
为什么不能相信 App 返回结果
App 支付完成后通常会收到同步结果,例如 resultStatus=9000。它可以用来提示用户”支付已完成”,但不能直接作为服务端的业务凭证,原因包括:
- 客户端参数可能被篡改或伪造。
- App 显示成功时,后端不一定已经收到支付通知。
- 支付完成后,App 可能被关闭,结果无法上传。
- 网络超时可能导致客户端状态与支付宝实际交易状态不一致。
- 客户端重复提交结果,可能引起重复入账或重复发货。
因此,后端只能根据支付宝服务端的异步通知或主动查询结果修改订单状态。
创建支付订单时设置异步通知地址
下面以支付宝 Java SDK 为例。创建 App 支付请求时,可以设置 notify_url:
AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();
request.setNotifyUrl("https://api.example.com/payment/alipay/notify");
AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
model.setOutTradeNo(orderNo);
model.setTotalAmount("99.00");
model.setSubject("商品订单");
model.setProductCode("QUICK_MSECURITY_PAY");
request.setBizModel(model);
AlipayTradeAppPayResponse response =
alipayClient.sdkExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"创建支付宝支付订单失败:" + response.getSubMsg()
);
}
// 将 response.getBody() 返回给 App,由 App 调用支付宝支付 SDK。
String orderString = response.getBody();
notify_url 必须是支付宝服务器能够访问的 HTTPS 地址,不能使用 localhost、内网地址,也不能指向需要登录认证的页面。
示例中的类名和配置方式取决于项目实际使用的支付宝 SDK 版本。如果项目使用证书模式,客户端初始化和验签也必须采用证书模式对应的配置,不能与普通公钥模式混用。
接收并验证异步通知
下面是一个基于 Spring Boot 的处理示例:
@RestController
@RequestMapping("/payment/alipay")
public class AlipayNotifyController {
private final OrderService orderService;
public AlipayNotifyController(OrderService orderService) {
this.orderService = orderService;
}
@PostMapping(
value = "/notify",
produces = "text/plain;charset=UTF-8"
)
public String notify(HttpServletRequest request) {
Map<String, String> params = new HashMap<>();
request.getParameterMap().forEach((key, values) -> {
if (values != null && values.length > 0) {
params.put(key, String.join(",", values));
}
});
try {
boolean signVerified = AlipaySignature.rsaCheckV1(
params,
AlipayConfig.ALIPAY_PUBLIC_KEY,
AlipayConfig.CHARSET,
AlipayConfig.SIGN_TYPE
);
if (!signVerified) {
return "failure";
}
orderService.handleAlipayNotification(params);
return "success";
} catch (Exception e) {
// 记录异常,避免输出私钥、完整签名等敏感信息。
return "failure";
}
}
}
普通公钥模式一般使用支付宝公钥验签,不是商户应用公钥,更不能用商户私钥验签。证书模式则应使用与该模式对应的 SDK 验签方法和配置。
验签后还必须校验业务参数
签名通过只能证明通知内容由支付宝签发,不能证明它一定属于当前订单。后端至少要检查以下字段:
app_id是否等于当前应用的支付宝 App ID。out_trade_no是否对应本地已有订单。total_amount是否与本地订单的应付金额一致。seller_id是否属于预期的收款支付宝账号。- 订单当前状态是否允许支付。
trade_status是否为业务认可的成功状态。- 同一个
trade_no是否已经被其他订单使用。
处理逻辑示例:
@Service
public class OrderService {
private final OrderRepository orderRepository;
private final PaymentRepository paymentRepository;
@Transactional
public void handleAlipayNotification(Map<String, String> params) {
String outTradeNo = params.get("out_trade_no");
String alipayTradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String appId = params.get("app_id");
String sellerId = params.get("seller_id");
String totalAmount = params.get("total_amount");
if (!AlipayConfig.APP_ID.equals(appId)) {
throw new IllegalArgumentException("app_id 不匹配");
}
if (!AlipayConfig.SELLER_ID.equals(sellerId)) {
throw new IllegalArgumentException("seller_id 不匹配");
}
Order order = orderRepository.findByOrderNoForUpdate(outTradeNo)
.orElseThrow(() -> new IllegalArgumentException("订单不存在"));
BigDecimal notifiedAmount = new BigDecimal(totalAmount);
if (order.getPayAmount().compareTo(notifiedAmount) != 0) {
throw new IllegalArgumentException("支付金额不匹配");
}
if (!"TRADE_SUCCESS".equals(tradeStatus)
&& !"TRADE_FINISHED".equals(tradeStatus)) {
// WAIT_BUYER_PAY、TRADE_CLOSED 等状态不能记为支付成功。
return;
}
// 支付宝可能重复通知,因此必须保证幂等。
if (order.isPaid()) {
if (!alipayTradeNo.equals(order.getGatewayTradeNo())) {
throw new IllegalStateException("订单对应的支付宝交易号不一致");
}
return;
}
order.markPaid(
alipayTradeNo,
notifiedAmount,
params.get("gmt_payment")
);
orderRepository.save(order);
paymentRepository.save(
PaymentRecord.success(
outTradeNo,
alipayTradeNo,
notifiedAmount,
params.get("buyer_id"),
tradeStatus
)
);
}
}
金额比较不要使用 double 或 float。Java 项目可以使用 BigDecimal,数据库可以使用 DECIMAL 类型。
数据库应该保存什么
业务订单与支付流水建议分开保存。
业务订单表可以包含:
order_no 商户订单号,唯一
status CREATED、PAYING、PAID、CLOSED、REFUNDED
pay_amount 应付金额
paid_amount 实付金额
payment_channel ALIPAY
gateway_trade_no 支付宝交易号 trade_no
paid_at 支付时间
created_at
updated_at
支付流水表可以包含:
payment_no 本地支付流水号
order_no 商户订单号
out_trade_no 提交给支付宝的商户订单号
gateway_trade_no 支付宝交易号
trade_status 支付宝交易状态
amount 支付金额
buyer_id 支付方标识,按业务需要保存
notify_payload 通知原始内容或脱敏后的结构化内容
notify_received_at 通知接收时间
verified 是否验签通过
created_at
updated_at
以下字段建议建立唯一约束:
ALTER TABLE payment_record
ADD CONSTRAINT uk_payment_out_trade_no UNIQUE (out_trade_no);
ALTER TABLE payment_record
ADD CONSTRAINT uk_payment_gateway_trade_no UNIQUE (gateway_trade_no);
唯一约束是幂等处理的最后一道防线。是否允许一个业务订单产生多次支付尝试,需要根据订单模型决定。如果允许,每次支付尝试都应生成不同的 out_trade_no。
异步通知必须支持重复调用
支付宝可能多次发送同一笔交易的通知,因此通知接口必须保证幂等。可以结合以下做法:
- 根据
out_trade_no查询并锁定订单。 - 在事务中判断订单是否已经支付。
- 为
trade_no或支付流水号设置唯一约束。 - 相同通知已经处理过时,直接返回
success。 - 已支付订单收到不同的
trade_no时,应进入异常处理,不能直接覆盖原值。 - 发货、开通会员等后续操作也要单独做好幂等控制。
通知接口不适合执行耗时很长的业务。可以先将支付结果写入数据库,再通过事务消息、事件表或可靠消息队列触发发货等后续操作。
主动查询支付结果
App 提示支付完成,但后端暂时没有收到异步通知时,可以调用 alipay.trade.query 查询:
AlipayTradeQueryRequest request = new AlipayTradeQueryRequest();
AlipayTradeQueryModel model = new AlipayTradeQueryModel();
model.setOutTradeNo(orderNo);
request.setBizModel(model);
AlipayTradeQueryResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
String tradeStatus = response.getTradeStatus();
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
// 仍需核对订单号、金额、收款方等信息,
// 然后通过与异步通知相同的幂等逻辑更新数据库。
}
}
主动查询适用于以下场景:
- App 支付后轮询订单状态。
- 异步通知长时间未到。
- 后端处理通知时发生临时故障。
- 定时扫描长时间处于
PAYING状态的订单。 - 日终对账或核查异常订单。
查询接口只应作为补偿机制,不建议对每笔订单进行高频轮询。App 可以查询自己的业务订单状态,再由后端按需决定是否访问支付宝接口。
返回 success 的时机
通知接口只有完成以下操作后,才能返回纯文本 success:
- 签名验证通过。
- 订单、金额、应用和收款方校验通过。
- 支付状态符合成功条件。
- 数据库事务已经成功提交,或者已经确认该通知被幂等处理。
如果处理失败,应返回 failure 或其他非成功响应,让支付宝后续重试。不要先返回 success,再异步执行没有可靠保障的数据库更新。否则更新失败后,可能失去支付宝自动重试的机会。
其他注意事项
- 商户私钥不能放在 App 中。支付订单的创建和签名都应由后端完成。
- 不要直接根据 App 上传的
resultStatus、金额或订单状态记账。 - 通知参数应按照支付宝 SDK 的要求验签,不要自行拼接待验签字符串。
- 记录必要的通知日志,但不要写入私钥、完整用户信息等敏感数据。
- 支付通知接口需要配置限流、异常监控和告警。
- 支付成功、退款成功和交易关闭是不同的业务事件,应分别处理。
- 数据库写入成功不代表后续业务已经执行成功。发货或权益发放需要具备重试能力。
- 上线前,应结合实际接入方式测试异步通知、重复通知、金额不一致、错误签名和数据库回滚等场景。
备注:内容仅供参考。
