PAYATHON 2026

商家App支付宝支付成功后如何将订单状态改为已支付

支付老李

明确结论

不要根据 App 端返回的”支付成功”直接修改订单状态。App 返回的结果只适合用于页面提示。后端应以支付宝异步通知为主要依据,必要时再调用支付宝交易查询接口核验。

要准确匹配订单,需要在创建支付请求时为每笔本地订单生成全局唯一的商户订单号 out_trade_no,同时将它保存到订单表并传给支付宝。支付宝异步通知会原样返回这个字段。后端通过它找到对应订单,完成验签、金额校验和幂等处理后,再把订单状态更新为”已支付”。

多个用户同时支付也不会串单,因为每次支付都有各自的 out_trade_no。订单匹配不依赖当前登录用户、请求顺序或 App 回调顺序。

正确的支付流程

支付流程如下:

  1. 用户提交订单。
  2. 后端生成唯一的 out_trade_no,保存订单,并将状态设为 WAIT_PAY。
  3. 后端使用这个 out_trade_no 创建支付宝 App 支付参数。
  4. App 调起支付宝完成支付。
  5. 支付宝向后端配置的 notify_url 发送异步通知。
  6. 后端验证通知签名,并校验商户、金额、订单状态等信息。
  7. 根据通知中的 out_trade_no 查询本地订单。
  8. 在数据库事务中将订单幂等更新为 PAID。
  9. 业务处理成功后,向支付宝返回 success。

三类编号的对应关系如下:

本地订单 ID
    ↓
唯一商户订单号 out_trade_no
    ↓
支付宝交易号 trade_no

out_trade_no 用于关联本地订单,trade_no 是支付宝生成的交易标识。支付成功后,建议将两者都保存到订单表或支付流水表。

订单号必须全局唯一

不要直接使用用户 ID、秒级时间戳或数据库当前最大 ID 作为商户订单号,这些值在并发场景下可能重复。

可以使用雪花算法、UUID,也可以通过数据库唯一键保证编号不重复。例如:

String outTradeNo = "APP" + LocalDateTime.now()
        .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmssSSS"))
        + UUID.randomUUID().toString().replace("-", "").substring(0, 12);

数据库还应添加唯一约束,不能只依赖代码中的生成规则:

CREATE UNIQUE INDEX uk_order_out_trade_no
ON orders(out_trade_no);

如果一笔业务订单可以多次重新发起支付,建议单独建立支付流水表,并为每次支付尝试生成新的 out_trade_no,以免不同支付请求互相覆盖。

创建订单和支付参数

订单金额必须由后端根据商品、优惠和运费重新计算,不能直接采用 App 提交的金额。

下面是一个简化示例:

@Transactional
public String createOrder(CreateOrderRequest request, Long userId) {
    BigDecimal amount = calculateAmountOnServer(request);

    Order order = new Order();
    order.setUserId(userId);
    order.setOutTradeNo(generateOutTradeNo());
    order.setAmount(amount);
    order.setStatus("WAIT_PAY");
    orderRepository.insert(order);

    AlipayTradeAppPayRequest payRequest = new AlipayTradeAppPayRequest();
    payRequest.setNotifyUrl("https://api.example.com/pay/alipay/notify");

    AlipayTradeAppPayModel model = new AlipayTradeAppPayModel();
    model.setOutTradeNo(order.getOutTradeNo());
    model.setTotalAmount(amount.toPlainString());
    model.setSubject("订单支付");
    model.setProductCode("QUICK_MSECURITY_PAY");

    payRequest.setBizModel(model);

    return alipayClient.sdkExecute(payRequest).getBody();
}

具体的 SDK 类名和调用方式取决于项目所用的支付宝 SDK 版本,请以对应版本的官方文档为准。

接收支付宝异步通知

异步通知接口必须能被支付宝服务器通过公网访问。处理通知时,不能只检查 trade_status,还要验证签名和关键业务字段。

下面是 Spring 风格的示意代码:

@PostMapping("/pay/alipay/notify")
public String alipayNotify(HttpServletRequest request) {
    Map<String, String> params = extractAlipayParams(request);

    boolean verified;
    try {
        verified = AlipaySignature.rsaCheckV1(
                params,
                alipayPublicKey,
                "UTF-8",
                "RSA2"
        );
    } catch (AlipayApiException e) {
        return "failure";
    }

    if (!verified) {
        return "failure";
    }

    String outTradeNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String tradeStatus = params.get("trade_status");
    String totalAmount = params.get("total_amount");
    String appId = params.get("app_id");
    String sellerId = params.get("seller_id");

    if (!expectedAppId.equals(appId) || !expectedSellerId.equals(sellerId)) {
        return "failure";
    }

    if (!"TRADE_SUCCESS".equals(tradeStatus)
            && !"TRADE_FINISHED".equals(tradeStatus)) {
        return "success";
    }

    boolean handled = paymentService.confirmPaid(
            outTradeNo,
            tradeNo,
            new BigDecimal(totalAmount)
    );

    return handled ? "success" : "failure";
}

提取参数时,应保留支付宝传来的原始字符串值。验签前不要自行拼接、转义或修改编码。

核心业务处理可以这样写:

@Transactional
public boolean confirmPaid(
        String outTradeNo,
        String alipayTradeNo,
        BigDecimal paidAmount
) {
    Order order = orderRepository.findByOutTradeNoForUpdate(outTradeNo);

    if (order == null) {
        return false;
    }

    if (order.getAmount().compareTo(paidAmount) != 0) {
        recordAbnormalPayment(outTradeNo, alipayTradeNo, paidAmount);
        return false;
    }

    if ("PAID".equals(order.getStatus())) {
        return alipayTradeNo.equals(order.getAlipayTradeNo());
    }

    if (!"WAIT_PAY".equals(order.getStatus())) {
        recordAbnormalPayment(outTradeNo, alipayTradeNo, paidAmount);
        return false;
    }

    int affectedRows = orderRepository.markAsPaid(
            order.getId(),
            alipayTradeNo,
            LocalDateTime.now()
    );

    if (affectedRows != 1) {
        throw new IllegalStateException("订单支付状态更新失败");
    }

    return true;
}

对应的条件更新可以写成:

UPDATE orders
SET status = 'PAID',
    alipay_trade_no = :alipayTradeNo,
    paid_at = :paidAt
WHERE id = :orderId
  AND status = 'WAIT_PAY';

WHERE status = 'WAIT_PAY' 这个条件不能省略。即使支付宝重复发送通知,或多个服务实例同时处理同一条通知,也只有第一次更新会成功。

为什么不能相信 App 的支付结果

App 端返回的支付结果并不可靠,原因包括:

  • 客户端数据可能被篡改。
  • App 可能显示支付成功,但后端还没有收到通知。
  • 支付成功后,App 可能被关闭,客户端请求无法发送到后端。
  • 网络超时可能使 App 无法判断最终支付状态。
  • 客户端可能重复提交”支付成功”请求。

因此,App 收到成功结果后,可以通知后端查询订单状态,但不能要求后端直接把订单改为已支付。

例如:

GET /api/orders/{orderId}/payment-status
Authorization: Bearer ...

后端应先确认该订单属于当前用户,再返回数据库中记录的支付状态。如果异步通知还未到达,页面可以暂时显示”支付结果确认中”。

异步通知延迟或丢失时怎么办

异步通知通常是确认支付结果的主要方式,但它不一定会立即到达。系统可以增加主动查询机制作为补偿:

  1. App 支付结束后,请求后端查询订单状态。
  2. 如果本地订单仍为 WAIT_PAY,后端使用 out_trade_no 调用支付宝交易查询接口。
  3. 查询结果确认支付成功后,复用同一个 confirmPaid 方法完成幂等更新。
  4. 定时扫描长时间处于 WAIT_PAY 状态的订单,主动查询或关闭超时订单。

主动查询的结果也要经过校验:

  • out_trade_no 是否一致;
  • 支付金额是否与本地订单一致;
  • app_id、商户身份等是否属于当前系统;
  • 支付状态是否确实成功。

查询接口返回交易记录,并不代表可以直接更新订单。

并发场景如何保证不串单

多个客户同时创建订单或支付时,应通过数据库约束和支付标识保证数据一致:

  • 每笔支付都使用唯一的 out_trade_no。
  • 在订单表中为 out_trade_no 建立唯一索引。
  • 回调按照 out_trade_no 精确查询订单,不查找所谓的”最后一笔订单”。
  • 更新操作放在数据库事务中执行。
  • 使用条件更新或行锁处理并发。
  • 为 trade_no 建立唯一约束,防止同一个支付宝交易号关联多笔订单。
  • 支付成功后的发货、积分发放、优惠券核销等操作也必须支持幂等处理。
  • 只有异步通知处理成功后,才能返回 success。

例如:

CREATE UNIQUE INDEX uk_order_alipay_trade_no
ON orders(alipay_trade_no);

如果数据库允许唯一索引中存在多个 NULL,这个索引可以直接用于尚未支付的订单。不同数据库对唯一索引和 NULL 的处理方式可能不同,需要根据实际使用的数据库确认。

还需要注意的安全问题

  • notify_url 应使用 HTTPS,同时避免被登录拦截器或 CSRF 校验错误阻断。
  • 支付宝公钥、应用私钥和网关配置应从安全配置中读取,不能写入客户端或提交到代码仓库。
  • 必须使用支付宝 SDK 提供的方法验签,不能仅根据参数值判断通知来源。
  • 回调中的用户信息不能作为判断订单归属的依据,订单归属应以本地 out_trade_no 对应的数据为准。
  • 金额应使用 BigDecimal 或数据库定点数类型,不能使用 float、double 比较。
  • 支付回调应尽快处理完成。发货、短信、积分等耗时业务,可以在支付状态写入数据库后投递到消息队列。
  • 回调日志应记录 out_trade_no、trade_no、通知状态和处理结果,但不能记录私钥、完整签名密钥或其他敏感信息。
  • 已取消、已退款或已关闭的订单收到支付成功通知时,不能直接覆盖原状态。系统应记录异常并进入补偿处理。

整个流程可以归纳为:使用唯一的 out_trade_no 关联订单,以支付宝服务端结果确认支付,通过验签和金额校验判断通知是否可信,再用事务和幂等更新处理并发与重复通知。

备注:内容仅供参考。