PAYATHON 2026

App 支付中如何验证服务端返回的“支付订单信息”?

支付阿杰

结论

服务端返回的“支付订单信息”,不能由 App 单独判断是否支付成功。App 只需要检查订单信息是否为空、格式是否符合预期,然后原样传给支付 SDK。可信的支付结果应由商户服务端通过支付平台的异步通知确认,或调用平台的订单查询接口核实。

App 与业务服务端之间必须使用 HTTPS,并结合登录态、订单归属校验和短期有效机制,避免订单信息被窃取、替换或重复使用。

为什么不能只在 App 端验证

App 运行在用户设备上,代码、网络请求和本地状态都可能被篡改。因此,以下信息不能直接作为发货或开通权益的依据:

  • App 收到服务端返回的订单信息后,便认为订单有效;
  • 支付 SDK 返回“支付成功”后,立即发货;
  • 客户端自行修改订单状态;
  • 只验证支付回调中的签名,不核对订单金额、商户身份和订单号。

支付 SDK 返回的同步结果主要用于更新界面。即使结果显示成功,也可能遇到网络异常、结果伪造、订单金额不一致或异步通知尚未到达等情况。

推荐处理流程

1. App 向业务服务端创建订单

App 只提交商品编号、购买数量等业务参数,不直接提交最终支付金额。服务端根据商品配置重新计算金额,并生成唯一的商户订单号。

POST /api/orders

{
  "productId": "vip_monthly",
  "quantity": 1
}

创建订单后,服务端将初始状态保存为 PENDING,再调用支付平台接口,或使用商户私钥生成支付订单信息。

返回结构化数据通常比一段无法识别的字符串更便于处理:

{
  "orderId": "ORD202609140001",
  "payOrderInfo": "支付平台要求的订单字符串",
  "expiresAt": 1789372800
}

2. App 对响应做基础校验

客户端可以检查:

  • HTTP 请求是否成功;
  • orderId 和 payOrderInfo 是否为空;
  • 返回的订单是否属于当前登录用户;
  • 订单是否已经过期;
  • 当前订单状态是否允许发起支付。

这些检查只能用来发现接口异常和明显错误,不能作为最终的安全验证。

if (response.orderId.isBlank() || response.payOrderInfo.isBlank()) {
    showError("支付订单信息无效")
    return
}

if (response.expiresAt <= System.currentTimeMillis() / 1000) {
    showError("订单已过期,请重新下单")
    return
}

paymentSdk.pay(response.payOrderInfo)

如果支付平台要求传入完整的签名订单字符串,应把服务端返回的内容原样交给 SDK。不要在客户端重新拼接,也不要先解码再编码或替换其中的特殊字符。

3. 服务端验证支付结果

服务端收到支付平台的异步通知后,至少需要检查以下内容:

  1. 使用支付平台提供的验签方法验证通知签名;
  2. 核对商户号、应用 ID 或收款方身份;
  3. 确认商户订单号存在;
  4. 核对支付金额、币种与本地订单是否一致;
  5. 确认支付状态属于可认定为成功的状态;
  6. 确认平台交易号没有被其他订单使用;
  7. 通过事务或条件更新处理重复通知,避免重复发货。

下面是一段与具体支付平台无关的 Java 示意代码。verifySignature 必须替换为对应平台官方 SDK 提供的验签方法:

@PostMapping("/payment/notify")
public String handlePaymentNotify(@RequestParam Map<String, String> params) {
    boolean signatureValid = paymentVerifier.verifySignature(params);
    if (!signatureValid) {
        return "failure";
    }

    String orderNo = params.get("out_trade_no");
    String tradeNo = params.get("trade_no");
    String amountText = params.get("total_amount");
    String tradeStatus = params.get("trade_status");

    Order order = orderService.findByOrderNo(orderNo);
    if (order == null) {
        return "failure";
    }

    BigDecimal paidAmount;
    try {
        paidAmount = new BigDecimal(amountText);
    } catch (RuntimeException e) {
        return "failure";
    }

    if (order.getAmount().compareTo(paidAmount) != 0) {
        return "failure";
    }

    if (!paymentVerifier.matchesMerchant(params)) {
        return "failure";
    }

    if (!paymentVerifier.isSuccessfulStatus(tradeStatus)) {
        return "failure";
    }

    orderService.markPaidIdempotently(orderNo, tradeNo);
    return "success";
}

更新数据库时应带上状态条件,例如只允许订单从 PENDING 更新为 PAID:

UPDATE payment_order
SET status = 'PAID',
    platform_trade_no = :tradeNo,
    paid_at = CURRENT_TIMESTAMP
WHERE order_no = :orderNo
  AND status = 'PENDING';

如果受影响的行数为 0,应继续判断该订单是否已经处理,不能再次发货。

4. App 查询业务服务端的订单状态

支付 SDK 返回结果后,App 可以向自己的业务服务端查询订单状态:

GET /api/orders/ORD202609140001/status
{
  "orderId": "ORD202609140001",
  "status": "PAID"
}

异步通知可能会有延迟,此时可以短时间轮询,但要限制轮询次数和间隔。如果订单长时间仍处于处理中,应提示用户稍后查看,不能直接判定支付失败并再次扣款。

是否需要在 App 端验签

这取决于支付平台的协议:

  • 如果平台 SDK 明确支持同步结果验签,可以进行验签,以便改善客户端的状态展示;
  • 如果“支付订单信息”由商户私钥签名,App 通常不应持有商户私钥;
  • 即使客户端验签成功,也不能替代服务端对异步通知的验签和订单字段核对。

私钥只能保存在服务端或受控的密钥系统中,不能写入 App、配置文件、日志或前端代码。

注意事项

  • 不要信任客户端提交的价格,金额必须由服务端计算。
  • 不要只根据 SDK 的同步返回值发货或开通权益。
  • 验签成功后,仍需核对订单号、金额、币种、商户身份和支付状态。
  • 回调接口必须支持幂等处理,因为支付平台可能重复发送通知。
  • 金额应使用 BigDecimal 或最小货币单位的整数表示,避免使用 float、double。
  • 支付订单信息和回调参数可能包含敏感数据,不要将其完整写入普通日志。
  • 如果没有收到异步通知,服务端可以调用支付平台的订单查询接口补查。
  • 不同支付平台使用的签名字段、编码规则和成功状态并不相同,应以当前接入平台及对应 SDK 版本的接口说明为准。

备注:内容仅供参考。