PAYATHON 2026

支付宝小程序支付提示订单不存在或状态异常

支付老李

结论

请求和返回中的 out_trade_no 不一致,说明支付链路使用了两笔不同的商户订单:

  • 发起请求时:out_trade_no = 100012
  • 返回前端时:out_trade_no = 300088

支付宝通常不会随机修改商户订单号。更可能的原因是商户服务端在创建订单、组装支付参数、保存结果或向小程序返回数据时发生了串单、变量覆盖、缓存复用或记录读取错误。

排查时,应以实际传给支付宝接口的完整请求、支付宝原始响应,以及小程序最终传入支付 API 的参数为准,确认订单号在哪一步从 100012 变成了 300088。

为什么会提示”订单不存在或状态异常”

out_trade_no 是商户订单号,用于标识商户侧的一笔交易。同一条支付链路中的订单号应保持一致。

当前日志存在明显矛盾:

请求:out_trade_no=100012
返回:out_trade_no=300088

如果小程序拿到的是订单 300088 的支付凭证,却继续按照订单 100012 的状态支付或查询,就可能出现以下问题:

  • 支付凭证与当前业务订单不匹配;
  • 前端重复使用了上一笔订单的支付参数;
  • 服务端返回了其他订单的 trade_no;
  • 订单已经支付、关闭或撤销,却被再次发起支付;
  • 创建交易与后续查询、支付使用了不同的 appId、商户身份或环境;
  • 并发请求共用可变对象,订单数据被其他请求覆盖;
  • 缓存键只使用用户或固定名称,没有按订单号隔离;
  • 数据库查询条件不完整,读到了其他订单;
  • 组装回调或响应 DTO 时,从错误的对象中取出了 out_trade_no。

支付宝在正常创建交易时,不会把商户提交的 out_trade_no 从 100012 自动替换成 300088。如果 300088 确实来自支付宝原始响应,需要继续确认实际发送的请求与当前日志是否属于同一次调用。

重点检查返回数据的来源

目前给出的返回内容是:

{
  "status": 200,
  "appId": "2021004112613225",
  "trade_no": "2023083022001434891447316984",
  "out_trade_no": "300088"
}

从字段结构判断,这段内容更像商户服务端封装后返回给小程序的 JSON,不一定是支付宝 SDK 的原始响应。因此,应先检查生成这段 JSON 的代码。

尤其要留意类似下面的错误:

PayResult result = new PayResult();
result.setStatus(200);
result.setAppId(appId);
result.setTradeNo(alipayResponse.getTradeNo());

// 错误:从其他订单对象或缓存对象中读取订单号
result.setOutTradeNo(cachedOrder.getOutTradeNo());

这里应使用当前请求对应的订单对象,并在返回数据前校验订单号是否一致:

PayResult result = new PayResult();
result.setStatus(200);
result.setAppId(appId);
result.setTradeNo(alipayResponse.getTradeNo());
result.setOutTradeNo(currentOrder.getOutTradeNo());

if (!requestOutTradeNo.equals(result.getOutTradeNo())) {
    throw new IllegalStateException(
        "支付订单号不一致,request=" + requestOutTradeNo
            + ", response=" + result.getOutTradeNo()
    );
}

如果支付宝 SDK 的响应对象提供了 getOutTradeNo(),还应比较支付宝响应与本地订单:

String requestOutTradeNo = currentOrder.getOutTradeNo();
String responseOutTradeNo = alipayResponse.getOutTradeNo();

if (responseOutTradeNo != null
        && !requestOutTradeNo.equals(responseOutTradeNo)) {
    log.error(
        "支付宝交易订单号不一致,requestOutTradeNo={}, responseOutTradeNo={}, tradeNo={}",
        requestOutTradeNo,
        responseOutTradeNo,
        alipayResponse.getTradeNo()
    );
    throw new IllegalStateException("支付宝交易订单号校验失败");
}

建议的排查步骤

1. 用唯一请求标识串联整条链路

日志中的 trace_id 是:

0b26dfba16934081056993319e9661

通过该 trace_id 查找同一次请求的完整日志,确认以下数据是否来自同一线程和同一次接口调用:

  1. 小程序提交给商户服务端的业务订单号;
  2. 服务端从数据库查到的订单;
  3. 传给支付宝 SDK 的 biz_content;
  4. 支付宝 SDK 返回的原始响应;
  5. 服务端最终返回给小程序的 JSON;
  6. 小程序实际传给支付 API 的参数。

不要仅凭日志位置相邻,就认定它们属于同一次请求。存在并发时,不同请求的日志可能交错输出。

2. 打印支付宝原始响应

在业务代码封装结果之前,记录 SDK 的原始返回内容:

AlipayTradeCreateResponse response =
    alipayClient.execute(request);

log.info(
    "支付宝创建交易结果:traceId={}, requestOutTradeNo={}, code={}, subCode={}, "
        + "msg={}, subMsg={}, tradeNo={}, responseOutTradeNo={}, body={}",
    traceId,
    outTradeNo,
    response.getCode(),
    response.getSubCode(),
    response.getMsg(),
    response.getSubMsg(),
    response.getTradeNo(),
    response.getOutTradeNo(),
    response.getBody()
);

可以根据原始响应判断问题所在:

  • 如果原始响应中的订单号是 100012,返回小程序时却成了 300088,问题在商户服务端的响应组装、缓存或数据库读取环节。
  • 如果原始响应已经是 300088,需要核对实际发送给支付宝的完整请求,并确认请求日志和响应日志是否正确对应。
  • 如果原始响应没有 out_trade_no,封装后的结果却出现了 300088,该字段一定来自商户自己的代码或存储数据。

记录日志时,需要对密钥、签名、用户身份信息和其他敏感字段进行脱敏。

3. 检查请求或响应对象是否被复用

支付对象不应作为 Spring 单例 Bean 的成员变量,也不应使用静态变量保存当前订单数据。例如:

@Service
public class PayService {

    // 错误:多个请求会同时修改该字段
    private String currentOutTradeNo;

    public PayResult createPay(String outTradeNo) {
        this.currentOutTradeNo = outTradeNo;
        // 并发时可能已被另一请求覆盖
        return buildResult(this.currentOutTradeNo);
    }
}

订单号应只保存在当前方法的局部变量中:

@Service
public class PayService {

    public PayResult createPay(String outTradeNo) {
        String currentOutTradeNo = outTradeNo;
        return buildResult(currentOutTradeNo);
    }
}

还要检查以下情况:

  • 静态 Map 是否使用固定键保存支付结果;
  • Redis 是否按用户 ID 保存”当前订单”,导致新旧订单互相覆盖;
  • 异步任务是否引用了可变对象;
  • DTO、Builder 或支付宝请求对象是否被多个线程共用。

4. 核对数据库查询条件

下面的查询可能取到该用户最近创建的另一笔订单:

SELECT *
FROM payment_order
WHERE user_id = ?
ORDER BY create_time DESC
LIMIT 1;

支付时应优先通过明确的商户订单号查询:

SELECT *
FROM payment_order
WHERE out_trade_no = ?
LIMIT 1;

同时可以为 out_trade_no 建立唯一约束,避免一个商户订单号对应多条记录:

ALTER TABLE payment_order
ADD UNIQUE KEY uk_out_trade_no (out_trade_no);

如果系统支持多商户或多应用,唯一条件可能还要包含商户、应用或租户标识,具体取决于现有的订单号生成规则。

5. 检查缓存和前端状态

小程序端也可能保留了上一笔订单 300088 的支付结果。需要检查:

  • 支付参数是否保存在全局变量中;
  • 页面返回后是否继续使用旧的 trade_no;
  • 连续点击支付时,后一个请求是否覆盖前一个请求;
  • 网络重试后是否采用了先返回请求的结果;
  • 本地缓存是否按订单号隔离;
  • 支付按钮是否做了防重复点击处理。

可以在前端调用支付 API 前增加订单号校验:

const expectedOutTradeNo = currentOrder.outTradeNo;
const payResult = await createPay(currentOrder);

if (payResult.out_trade_no !== expectedOutTradeNo) {
  throw new Error(
    `支付订单号不一致:expected=${expectedOutTradeNo}, actual=${payResult.out_trade_no}`
  );
}

// 确认一致后再调用支付 API

前端校验不能代替服务端校验,但可以阻止错误的支付凭证继续进入支付流程。

6. 查询两笔订单的实际状态

分别通过商户系统和支付宝交易查询接口核对以下编号:

100012
300088
2023083022001434891447316984

需要确认:

  • trade_no 实际对应哪个 out_trade_no;
  • 100012 是否成功创建过支付宝交易;
  • 300088 是否已经支付、关闭或撤销;
  • 两笔订单是否属于相同的应用、商户和买家;
  • 当前支付凭证是否来自历史请求。

查询时必须使用创建交易时相同的支付宝应用和商户配置。使用不同的应用或商户身份查询同一订单,也可能返回”交易不存在”一类结果。

增加服务端防串单校验

服务端返回支付结果前,至少要校验业务订单号、应用 ID 和交易状态:

public PayResult buildPayResult(
        String expectedOutTradeNo,
        String expectedAppId,
        AlipayTradeCreateResponse response) {

    if (!response.isSuccess()) {
        throw new IllegalStateException(
            "支付宝创建交易失败,code=" + response.getCode()
                + ", subCode=" + response.getSubCode()
                + ", msg=" + response.getMsg()
                + ", subMsg=" + response.getSubMsg()
        );
    }

    String actualOutTradeNo = response.getOutTradeNo();

    if (actualOutTradeNo != null
            && !expectedOutTradeNo.equals(actualOutTradeNo)) {
        throw new IllegalStateException(
            "订单号不一致,expected=" + expectedOutTradeNo
                + ", actual=" + actualOutTradeNo
        );
    }

    PayResult result = new PayResult();
    result.setStatus(200);
    result.setAppId(expectedAppId);
    result.setTradeNo(response.getTradeNo());
    result.setOutTradeNo(expectedOutTradeNo);
    return result;
}

订单表中还应保存这些关联信息:

业务订单主键
out_trade_no
trade_no
app_id
buyer_id
支付状态
请求 trace_id
创建时间
更新时间

处理异步通知时,同样要校验通知中的 app_id、out_trade_no、金额和订单归属,不能只根据 trade_no 更新订单状态。

其他注意事项

  • out_trade_no 应在商户范围内保持唯一,避免使用容易重复的短序号。
  • 同一业务订单需要重试支付时,应先查询原交易状态,不要直接生成互相冲突的交易。
  • HTTP status: 200 通常只表示商户接口调用成功,不能据此判断支付成功。实际结果应以支付宝返回码、交易状态和异步通知为准。
  • 支付请求、查询请求和回调处理必须使用相同的应用及商户配置。
  • biz_content 中出现了 appid,而支付宝公共请求参数通常使用 app_id。应根据实际接口和 SDK 请求结构确认该字段是否有效,不能用自定义的 appid 代替公共参数 app_id。
  • notify_url 通常是接口的公共请求参数。需要确认代码是否通过 SDK 对应的方法设置,不能只把它放入 biz_content 并假定一定会生效。

根据现有信息,应优先检查商户服务端生成返回 JSON 的位置,以及缓存、数据库查询和并发对象复用情况。先确认 trade_no=2023083022001434891447316984 实际关联的商户订单号,就能判断错配发生在调用支付宝之前、收到支付宝响应之后,还是服务端向小程序返回数据时。

备注:内容仅供参考。