手机网页支付的支付交易号用完后如何处理?
结论
不能靠清除支付宝的历史支付记录来重复使用已经用过的订单号,也不应把自增 ID 清零后直接作为新的支付交易号。
如果传给支付宝的是商户订单号 out_trade_no,那么每笔新订单都要使用一个在商户侧长期唯一的值。原编号无论对应的订单已经支付、关闭还是退款,都不应再分配给其他订单。可以扩大编号容量,或在自增 ID 前加上业务标识、日期、规则版本等信息,以生成新的唯一订单号。
还要分清以下两个字段:
out_trade_no:由商户生成并传给支付宝的订单号。trade_no:由支付宝生成的交易号,商户不能自行指定。
为什么不能重新使用
支付订单号不只用于发起支付,还会用于支付查询、异步通知、退款、对账和争议处理。支付宝通过订单号识别同一笔业务请求,并避免重复支付。
如果自增 ID 清零后再次生成旧的 out_trade_no,支付宝无法确认这是旧订单重试,还是一笔全新的订单。出于幂等性和资金安全考虑,系统通常会把请求识别为原交易,或者直接提示订单号已经使用。
关闭订单、完成退款或删除商户本地记录,都不会释放订单号。支付宝侧的交易记录属于支付和对账数据,商户通常无法自行清除,因此不能通过删除历史记录来循环使用订单号。
推荐处理方式
1. 不要重置当前自增序列
保留已有 ID,让序列继续递增。如果数据库字段容量不足,可以将 INT 升级为 BIGINT。同时检查应用代码中的数据类型,避免数据库已经支持大整数,应用却仍在使用 32 位 int。
例如:
ALTER TABLE payment_order
MODIFY COLUMN id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT;
具体 SQL 要根据数据库类型、表结构和外键关系调整。生产环境修改大表前,还要评估锁表、索引和回滚方案。
2. 将内部主键与支付订单号分开
数据库自增 ID 可以继续作为内部主键,但不要直接将它作为完整的 out_trade_no。建议为支付订单号单独设置字段,并添加唯一约束:
CREATE TABLE payment_order (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
out_trade_no VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
created_at DATETIME NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_out_trade_no (out_trade_no)
);
VARCHAR 的长度应以当前使用的支付宝接口对 out_trade_no 的实际限制为准,不能只参照示例确定。
3. 为存量系统启用新的编号前缀
如果旧系统的自增 ID 已接近上限,不要直接归零,可以启用新的编号命名空间。例如:
WAP20260909V2-000000012345
完整编号可以由以下内容组成:
- 支付渠道或业务标识,如
WAP - 日期或系统启用时间
- 编号规则版本,如
V2 - 新序列号或分布式唯一 ID
这样即使末尾的序列重新从 1 开始,完整的 out_trade_no 也不会与旧订单重复。
Java 生成示例
下面的代码使用业务前缀、日期和数据库序列生成商户订单号:
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public final class PaymentOrderNoGenerator {
private static final DateTimeFormatter DATE_FORMATTER =
DateTimeFormatter.ofPattern("yyyyMMdd");
private PaymentOrderNoGenerator() {
}
public static String generate(long sequence) {
String date = LocalDate.now().format(DATE_FORMATTER);
return String.format("WAP%sV2-%012d", date, sequence);
}
}
调用方式:
long sequence = paymentOrderRepository.nextSequence();
String outTradeNo = PaymentOrderNoGenerator.generate(sequence);
生成编号后,先将它写入数据库,并通过唯一索引完成最终校验:
PaymentOrder order = new PaymentOrder();
order.setOutTradeNo(outTradeNo);
order.setStatus("CREATED");
paymentOrderRepository.insert(order);
只使用当前毫秒时间戳并不可靠,并发请求可能在同一毫秒内生成相同编号。多实例部署时,可以采用数据库序列、号段服务、Snowflake 类算法或 UUID,但数据库中仍建议保留唯一约束。
重试支付时如何处理
处理支付重试时,需要先判断是同一订单重试,还是创建了新订单:
- 同一业务订单因网络超时而重新提交时,可以继续使用原来的
out_trade_no,但应先查询支付宝侧的交易状态。 - 用户重新下单,或者原订单关闭后又创建了一笔业务订单时,应生成新的
out_trade_no。 - 收到异步通知后,应按
out_trade_no做幂等处理,防止重复更新订单或重复发货。 - 本地请求超时后不要立即更换订单号再次支付,否则可能产生多笔支付交易。
建议按以下顺序处理:
- 根据
out_trade_no查询本地订单。 - 查询或核实支付宝侧的交易状态。
- 如果已经支付,按幂等逻辑处理成功结果。
- 如果原订单仍可支付,按接口规则重试。
- 如果需要重新下单,创建新的业务订单和新的
out_trade_no。
注意事项
- 不要通过删除历史订单来释放编号。
- 退款、撤销或关闭订单后,订单号仍不能重新使用。
- 商户订单号应长期关联对应的业务订单、支付宝交易号和退款记录。
- 编号规则发生变化后,应保留规则版本,方便查询和对账。
- 订单号中不要包含手机号、身份证号等敏感信息。
- 编号允许使用的字符和最大长度,应以当前接入产品及对应支付宝接口文档为准。
- 如果报错字段是
trade_no,而不是out_trade_no,需要重新检查参数映射。trade_no应保存支付宝返回的值,不能由商户通过自增方式生成。
备注:内容仅供参考。
