支付并签约未成功,再次拉取为何跳过签约进入支付页?
结论
再次拉起时直接进入支付页,通常表示平台认为签约已经完成,或者存在可以继续执行的签约流程,所以不会再次展示签约页面。
首次打开过签约页面,但用户认为没有签约成功,并不代表平台仍将其视为未签约。签约可能已经异步完成,只是客户端没有收到结果;也可能是系统重复使用了原支付单号、签约流水号或请求令牌,恢复了上一次流程。
常见原因
签约已成功,但客户端没有正确处理结果
签约结果可能通过页面回跳、服务端异步通知或主动查询返回。如果客户端中途关闭页面、遇到网络异常,或者服务端没有正确处理通知,业务系统可能仍记录为”未签约”,而支付平台上已经存在有效协议。
再次发起支付并签约时,平台查到有效协议,便会跳过签约,直接进入支付页。
重复使用了上一次的业务请求标识
支付并签约请求通常会包含支付单号、签约流水号、业务请求号或预下单令牌。这些字段一般用于保证幂等性。
如果第二次请求沿用第一次的标识,平台可能不会新建签约流程,而是继续原来的流程。只要原流程已经进入支付阶段,再次拉起时就可能直接显示支付页面。
不同支付产品和 API 使用的字段名称不同,仅凭页面现象无法判断具体是哪个参数造成的。
页面展示状态与后台协议状态不一致
客户端是否看到”签约成功”页面,不能作为判断签约结果的唯一依据。例如:
- 用户完成确认后,在结果页加载前关闭了页面;
- 页面回跳失败,但服务端已经完成签约;
- 异步通知延迟或处理失败;
- 业务系统更新签约状态时发生异常;
- 平台缓存并继续使用了首次请求生成的流程状态。
签约结果应以状态查询接口或验签通过的服务端通知为准。
排查和解决步骤
1. 查询平台侧的实际签约状态
使用支付平台提供的协议查询接口,按照用户标识、协议号或签约请求号查询当前状态。
需要重点确认:
- 是否已经生成协议号;
- 协议是否有效;
- 签约完成时间;
- 当前用户是否还存在其他有效协议;
- 首次请求对应的签约流水是否已经成功。
如果查询结果显示协议有效,直接进入支付页就是正常流程。此时应修正业务系统记录的签约状态,不要重复发起签约。
2. 核对两次请求的关键参数
对比首次请求和第二次请求的日志,检查以下数据是否重复使用:
- 商户支付订单号;
- 商户签约请求号;
- 幂等键;
- 预下单令牌;
- 协议模板或产品标识;
- 商户用户标识;
- 支付平台用户标识。
同一业务的重试请求可以复用幂等标识。如果需要重新创建一个全新的流程,则应按照对应 API 的规则生成新的请求号。在确认平台状态之前,不要直接更换所有标识,否则可能产生重复订单或重复协议。
3. 检查异步通知和页面回跳
服务端应保存完整的通知日志并完成签名验证,不能只依赖客户端回跳参数更新签约状态。
检查内容包括:
- 通知地址能否从公网访问;
- 是否返回了平台要求的成功响应;
- 验签是否成功;
- 通知是否被重复消费;
- 数据库事务是否提交;
- 是否存在超时、异常或消息积压。
4. 根据查询结果决定下一步
处理逻辑如下:
查询签约状态
├── 已签约:直接发起或继续支付
├── 签约处理中:等待通知或稍后再次查询
├── 未签约:创建新的支付并签约请求
└── 已失效/已解约:按平台规则重新签约
不能因为客户端没有看到成功页,就直接认定为未签约。
代码示例
下面是一个通用处理示例。接口名和状态值需要替换为实际支付平台提供的定义:
public PaymentResult startPaymentAndSign(
String merchantUserId,
String merchantOrderNo) {
AgreementStatus status =
agreementClient.queryAgreementStatus(merchantUserId);
if (status == AgreementStatus.ACTIVE) {
// 平台侧已有有效协议,无须重复签约
return paymentClient.createPayment(merchantOrderNo);
}
if (status == AgreementStatus.PROCESSING) {
throw new IllegalStateException(
"签约结果仍在处理中,请等待异步通知或稍后查询");
}
String signRequestNo = generateUniqueSignRequestNo();
return paymentClient.createPaymentAndSign(
merchantOrderNo,
signRequestNo,
merchantUserId
);
}
异步通知处理需要支持验签和幂等:
public String handleAgreementNotify(Map<String, String> parameters) {
if (!signatureVerifier.verify(parameters)) {
throw new SecurityException("签约通知验签失败");
}
String notifyId = parameters.get("notify_id");
if (notifyRepository.hasProcessed(notifyId)) {
return "success";
}
String agreementNo = parameters.get("agreement_no");
String status = parameters.get("status");
transactionTemplate.executeWithoutResult(transaction -> {
agreementRepository.updateStatus(agreementNo, status);
notifyRepository.markProcessed(notifyId);
});
return "success";
}
注意事项
- 页面成功拉起只表示流程入口创建成功,不能说明签约成功或失败。
- 客户端回跳可能丢失,最终状态应以服务端查询结果和验签通过的异步通知为准。
- 支付订单号与签约请求号需要分别管理,避免错误复用。
- 遇到处理中状态时,不要立即创建大量新请求,应先查询状态或等待通知。
- 保存请求参数、平台响应、协议号、订单号和通知记录,以便串联整个流程。
- 如果平台查询结果明确为”未签约”,但页面仍持续跳过签约环节,应携带两次请求号、订单号、协议查询结果和发生时间联系支付平台技术支持。仅凭页面现象,无法进一步判断平台内部的状态处理逻辑。
备注:内容仅供参考。
