支付宝小程序支付提示订单不存在或状态异常
结论
请求和返回中的 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 查找同一次请求的完整日志,确认以下数据是否来自同一线程和同一次接口调用:
- 小程序提交给商户服务端的业务订单号;
- 服务端从数据库查到的订单;
- 传给支付宝 SDK 的
biz_content; - 支付宝 SDK 返回的原始响应;
- 服务端最终返回给小程序的 JSON;
- 小程序实际传给支付 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 实际关联的商户订单号,就能判断错配发生在调用支付宝之前、收到支付宝响应之后,还是服务端向小程序返回数据时。
备注:内容仅供参考。
