PAYATHON 2026

中小学教育缴费中支付宝返回的trade_no不一致

支付老李

结论

order_no、trade_no 和 out_trade_no 是三种不同的编号,不能通过比较字符串是否相等来判断它们是否对应同一笔缴费。

  • order_no:发送教育缴费账单后,支付宝返回的账单编号,用于查询账单状态。
  • out_trade_no:商户侧的业务订单号,通常由接入方生成,支付回调会返回该编号。
  • trade_no:学生或家长完成付款后,支付宝为这笔支付生成的交易号。

发送账单时获得的 order_no 与支付回调中的 trade_no 不一致,一般属于正常情况。本地应以商户业务订单号为主要关联依据,同时保存支付宝返回的 order_no 和付款后产生的 trade_no。

为什么会出现不一致

发送账单与完成支付分属两个阶段:

  1. 商户向支付宝发送教育缴费账单。
  2. 支付宝受理账单并返回 order_no。
  3. 家长进入支付宝完成付款。
  4. 支付宝为这笔付款生成 trade_no,再通过异步通知返回 trade_no 和 out_trade_no。

order_no 标识教育缴费账单,trade_no 标识实际发生的资金交易。两者用途不同,因此不需要使用相同的值。

例如:

商户业务订单号 out_trade_no
        │
        ├── 支付宝教育账单号 order_no
        │
        └── 支付宝支付交易号 trade_no

如果使用发送账单时返回的 order_no 查询到状态为”已支付”,只能说明该账单已经完成付款,不能据此认为 order_no 应当等于回调中的 trade_no。

正确的关联方式

建议在本地订单表中保存以下字段:

merchant_order_id  商户内部订单主键
out_trade_no       提交给支付宝的商户业务订单号
alipay_order_no    发送账单返回的 order_no
alipay_trade_no    支付回调返回的 trade_no

处理流程可以按以下方式设计:

  1. 生成全局唯一的 out_trade_no。
  2. 调用发送账单接口时,保存该 out_trade_no。
  3. 接口调用成功后,把返回的 order_no 写入同一条本地订单记录。
  4. 收到支付回调后,先完成验签。
  5. 根据回调中的 out_trade_no 查找本地订单。
  6. 核对金额、收款方和订单状态等数据。
  7. 把回调中的 trade_no 保存到对应订单。
  8. 如需主动确认账单状态,使用发送账单时返回的 order_no 调用相应的账单查询接口。

不要采用下面这种判断方式:

if (orderNo.equals(tradeNo)) {
    // 认为支付成功
}

这两个字段的业务含义不同,直接比较没有可靠依据。

回调处理示例

下面的代码只用于说明关联逻辑。实际的回调参数名、验签方法和金额字段,应以所用支付宝接口的文档为准。

public String handleAlipayNotify(Map<String, String> params) {
    // 1. 必须先完成支付宝异步通知验签
    boolean verified = verifyAlipaySignature(params);
    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 sellerId = params.get("seller_id");

    if (outTradeNo == null || tradeNo == null) {
        return "failure";
    }

    // 2. 使用商户业务订单号关联本地订单
    EducationBill bill = billService.findByOutTradeNo(outTradeNo);
    if (bill == null) {
        return "failure";
    }

    // 3. 校验金额和收款方,不能只依赖 trade_status
    if (!bill.getAmount().toPlainString().equals(totalAmount)) {
        return "failure";
    }

    if (!bill.getSellerId().equals(sellerId)) {
        return "failure";
    }

    // 4. 根据实际接口约定判断支付状态
    if ("TRADE_SUCCESS".equals(tradeStatus)
            || "TRADE_FINISHED".equals(tradeStatus)) {
        // 幂等处理:重复通知不能重复入账
        billService.markPaidIfUnpaid(
            bill.getId(),
            tradeNo
        );
    }

    return "success";
}

发送账单成功后,可以这样保存编号:

String outTradeNo = generateUniqueOutTradeNo();

EducationBill bill = new EducationBill();
bill.setOutTradeNo(outTradeNo);
bill.setStatus("PENDING");
billService.create(bill);

// 调用实际的教育缴费账单发送接口
BillingSendResponse response = sendBill(outTradeNo);

if (response.isSuccess()) {
    billService.saveAlipayOrderNo(
        bill.getId(),
        response.getOrderNo()
    );
}

同一条业务记录最终可能如下:

{
  "out_trade_no": "EDU202307150001",
  "order_no": "2023071511003005007591670193",
  "trade_no": "实际支付回调返回的支付宝交易号",
  "status": "PAID"
}

如果 out_trade_no 也无法关联

这时应检查发送账单时实际提交的商户订单字段,不能假定回调中的 out_trade_no 一定等于 order_no。

需要重点核对以下内容:

  • 发送账单请求中的商户业务订单号是否唯一。
  • 回调中的 out_trade_no 是否对应发送账单时提交的业务编号。
  • 是否发生过账单重新发送、关闭后重建或订单号复用。
  • 服务商、学校或其他中间系统是否重新生成了业务订单号。
  • 当前支付回调是否来自该教育缴费接口,而非其他支付产品。
  • 数据库是否完整保存了请求参数和发送账单响应。

如果教育缴费接口没有明确说明会在支付通知中原样返回某个业务字段,就不能自行推断字段之间的关系。应以该接口的请求参数、响应字段和异步通知文档为准,并使用商户自己的业务主键建立映射。

注意事项

  • order_no、trade_no 和 out_trade_no 都应使用字符串类型保存,以免整数类型引起溢出或格式变化。
  • out_trade_no 必须全局唯一,不能用于不同学生或不同缴费项目。
  • 处理支付回调时必须验签,并核对金额、收款方和订单状态。
  • 异步通知可能重复发送,因此更新支付状态时必须保证幂等。
  • 不能仅凭账单查询结果直接入账,还要确认查询结果是否对应预期的 order_no 和业务订单。
  • 退款、撤销等后续操作通常需要使用支付交易号 trade_no,不能用账单号替代。
  • 如果同一账单支持拆分支付、合并支付或重新生成支付订单,order_no 与 trade_no 可能并非简单的一对一关系。是否支持这些模式,应以当前教育缴费产品的接口约定为准。

备注:内容仅供参考。