电脑网站支付链接在商城订单关闭后仍可支付,如何解决?
结论
关闭自建商城订单时,也要关闭支付平台中的对应交易。只修改本地订单状态,已经生成的支付链接不会自动失效。
如果使用支付宝电脑网站支付,可以在关闭商城订单时调用 alipay.trade.close。交易成功关闭后,即使原支付页面还能打开,用户也无法继续付款。
创建支付交易时还可以设置 timeout_express,让交易到期后自动失效。不过,这只适合用作异常兜底,不能代替主动关单。
为什么商城订单关闭后仍能支付
商城订单和支付平台交易是两个相互独立的对象:
- 商城订单状态保存在自建系统中;
- 支付链接对应的交易状态保存在支付平台中;
- 把本地订单更新为“已关闭”,不会自动通知支付平台;
- 只要平台侧交易仍处于待支付状态,旧链接通常就能继续付款。
完整的关单流程需要同时处理本地订单和平台交易。隐藏订单或修改数据库字段,都无法关闭平台侧交易。
推荐的处理流程
1. 创建支付交易时保存交易标识
发起支付时,需要保存商城订单与支付平台交易的对应关系。在支付宝场景中,通常至少要保存:
out_trade_no:商户订单号;trade_no:支付宝交易号,获取后可以保存;- 当前支付状态;
- 支付链接生成时间;
- 订单过期时间。
out_trade_no 在商户系统中必须唯一。后续查询交易、关闭交易和处理支付通知时,都应使用同一个订单号。
2. 到期后先关闭平台交易
定时任务或人工关闭订单时,可以按以下顺序处理:
- 先把本地订单改为“关闭处理中”,阻止新的支付请求;
- 查询平台交易状态;
- 如果交易仍待支付,调用关单接口;
- 平台确认交易关闭后,再把本地订单改为“已关闭”;
- 如果交易已经支付,则进入支付成功补偿流程,不能再按未支付订单关闭。
增加“关闭处理中”状态,可以降低关单与客户付款同时发生时的竞态风险。
3. 调用支付宝关单接口
下面的示例使用支付宝 Java SDK。具体类名和调用方式,应以项目使用的 SDK 版本为准:
AlipayTradeCloseRequest request = new AlipayTradeCloseRequest();
AlipayTradeCloseModel model = new AlipayTradeCloseModel();
model.setOutTradeNo(order.getOrderNo());
// 如果已经保存了支付宝交易号,也可以根据接口要求使用 trade_no。
// model.setTradeNo(order.getAlipayTradeNo());
request.setBizModel(model);
AlipayTradeCloseResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
orderService.markClosed(order.getId());
} else {
orderService.markCloseFailed(
order.getId(),
response.getCode(),
response.getSubCode(),
response.getSubMsg()
);
}
调用关单接口时,通常从 out_trade_no 和 trade_no 中选择一个定位交易。具体限制应以当前接口文档和项目所用 SDK 为准。
接口没有抛出异常,并不代表交易已经成功关闭。还要检查业务响应,例如:
response.isSuccess()
response.getCode()
response.getSubCode()
response.getSubMsg()
遇到网络超时或响应结果不明确时,不要立即把订单标记为已关闭。应调用交易查询接口确认平台侧状态,或者安排任务重试。
创建支付时设置自动过期时间
发起电脑网站支付时,可以按照业务规则设置 timeout_express,让平台侧交易在指定时间后失效。例如:
AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
AlipayTradePagePayModel model = new AlipayTradePagePayModel();
model.setOutTradeNo(order.getOrderNo());
model.setTotalAmount(order.getPayAmount().toPlainString());
model.setSubject(order.getSubject());
model.setProductCode("FAST_INSTANT_TRADE_PAY");
model.setTimeoutExpress("30m");
request.setBizModel(model);
timeout_express 支持的格式、最小值和最大值可能受接口规则限制,应以当前支付宝开放平台文档为准。
平台超时时间最好与商城订单的关闭时间保持一致,但它们仍由两个不同的系统管理。定时主动关单能更快地同步状态,timeout_express 则用于处理主动关单未执行或执行失败等异常情况。
必须处理支付与关单同时发生的情况
客户付款和商城定时关单可能恰好同时发生。例如:
- 定时任务读取到订单尚未支付;
- 客户此时完成付款;
- 定时任务继续关闭本地订单;
- 支付成功的异步通知随后到达。
因此,更新订单状态时必须带上条件,不能直接覆盖原状态:
UPDATE orders
SET status = 'CLOSING'
WHERE id = ?
AND status = 'WAIT_PAY';
如果受影响行数为 0,说明订单状态已经改变,需要重新读取订单并根据新状态处理。
可以在关单前后查询平台状态。如果平台返回交易已经支付,应把本地订单转入支付成功或人工核对流程,不能继续标记为已关闭。
支付通知也要校验状态
即使已经发起主动关单,系统仍要正常接收和验证支付成功的异步通知。通知处理至少需要检查:
- 签名是否合法;
app_id是否属于当前应用;out_trade_no是否存在并与订单匹配;total_amount是否等于订单应付金额;seller_id或收款方信息是否符合预期;- 通知中的交易状态能否确认支付成功;
- 当前订单状态是否允许转为已支付。
示例逻辑:
@Transactional
public void handlePaySuccess(PayNotify notify) {
Order order = orderRepository.lockByOrderNo(notify.getOutTradeNo());
verifyAppId(notify);
verifySeller(notify);
verifyAmount(order, notify.getTotalAmount());
if (order.isPaid()) {
return; // 重复通知,幂等返回
}
if (order.isClosing() || order.isClosed()) {
// 不能简单丢弃,因为平台侧可能已经真实扣款。
exceptionService.recordPaidAfterClose(order, notify);
return;
}
order.markPaid(notify.getTradeNo());
orderRepository.save(order);
}
如果本地订单已经关闭,但平台显示交易已支付,应按实际资金状态处理。常见做法是记录异常支付、停止发货,再根据业务规则自动退款或交由人工核对。不能因为商城订单已经关闭,就忽略支付通知。
失败重试与对账
关单请求可能因网络超时、平台繁忙或状态冲突而失败。可以增加这些处理机制:
- 使用幂等的商户订单号发起关单请求;
- 关单失败后记录错误码和错误信息;
- 对状态不明确的交易调用查询接口确认;
- 定时扫描“关闭处理中”和“关单失败”的订单;
- 每日使用支付平台账单进行对账;
- 对本地已关闭但平台侧已付款的记录触发退款或人工告警。
请求已经发送,不等于平台交易已经关闭。最终状态应根据平台查询结果、关单响应和后续对账结果确定。
注意事项
- 删除或隐藏支付链接无法关闭平台交易,用户可能还保留着旧地址、二维码或浏览器页面。
- 前端禁止支付只能改善交互,不能代替服务端关单。
- 已支付成功的交易通常无法再通过关单接口撤销,应根据实际情况调用退款接口。
- 订单关闭后再次购买时,应创建新的
out_trade_no,不能复用旧交易。 - 金额、商户身份和订单号都必须由服务端校验,不能信任支付回跳页面中的参数。
- 同步跳转只用于展示结果。最终支付状态应通过验签后的异步通知和主动查询共同确认。
商城关单时,需要同步关闭支付平台交易。支付与关单并发产生的状态问题,则应通过订单状态机、幂等处理、主动查询和对账机制解决。
备注:内容仅供参考。
