支付宝周期扣款达到总期数后是否自动撤销
支付阿杰
明确结论
实际机制是第 1 种:最后一期扣款成功,且累计成功期数达到协议约定的总期数后,支付宝会终止该周期扣款协议,不再发起后续自动扣款。
这里的“撤销”准确来说是协议履行完毕后终止,并非用户主动解约。商户也不需要等到下一个扣款周期再次提交请求,再由支付宝拒绝扣款。
处理原因
总期数是周期扣款协议的终止条件。最后一期扣款成功后,协议约定的扣款计划已经全部完成,协议无需继续保持有效。
处理时需要注意:
- 期数是否完成应以“扣款成功”为准,不能因为扣款请求已经提交就累计期数。
- 如果最后一期扣款失败,通常不能算作已经达到总期数。
- 协议终止和最后一笔支付成功可能由不同的通知反映,通知到达的顺序也未必固定。
- 判断“最后一期”时,应同时核对支付宝侧的协议状态和商户侧的成功记录,不能只看本地定时任务。
建议的处理步骤
- 接收支付宝异步通知并验证签名。
- 按支付宝交易号或商户订单号进行幂等处理。
- 确认交易成功后,再将对应期次标记为成功。
- 成功期数达到总期数时,将本地周期扣款计划标记为“已完成”。
- 查询并同步支付宝侧的协议状态,确认协议已经终止。
- 停止生成后续期次,不再提交新的扣款请求。
- 如果状态尚未同步,不要立即重复解约,可以稍后重新查询。
代码示例
下面的示例是商户系统收到最后一期扣款成功通知时的基本处理逻辑。示例中的字段名称属于业务层定义,并非支付宝接口的固定参数。
@Transactional
public void handleDeductionSuccess(
String agreementNo,
String tradeNo,
int currentPeriod) {
// 幂等判断,防止重复通知导致重复累计
if (paymentRecordRepository.existsByTradeNo(tradeNo)) {
return;
}
DeductionPlan plan = deductionPlanRepository
.findByAgreementNoForUpdate(agreementNo)
.orElseThrow(() -> new IllegalStateException("周期扣款计划不存在"));
paymentRecordRepository.save(
new PaymentRecord(agreementNo, tradeNo, currentPeriod, "SUCCESS")
);
int successPeriods =
paymentRecordRepository.countSuccessPeriods(agreementNo);
if (successPeriods >= plan.getTotalPeriods()) {
plan.setStatus("COMPLETED");
plan.setNextDeductionTime(null);
deductionPlanRepository.save(plan);
// 建议通过当前产品对应的协议查询接口同步支付宝侧状态。
// 不要仅根据本地计数伪造支付宝协议状态。
agreementStatusSyncService.scheduleQuery(agreementNo);
}
}
生成扣款任务时,还应增加状态拦截:
public boolean canCreateNextDeduction(DeductionPlan plan) {
return "ACTIVE".equals(plan.getStatus())
&& plan.getSuccessPeriods() < plan.getTotalPeriods()
&& plan.getNextDeductionTime() != null;
}
注意事项
- 不要将“协议完成”只理解为“下个周期不再扣款”。本地系统也要及时关闭计划,避免继续创建订单或发送扣款提醒。
- 最后一期扣款成功后,不建议无条件再次调用解约接口。此时协议可能已经由支付宝自动终止,重复解约可能返回“协议不存在”“状态不允许”或幂等成功,具体结果取决于接入的产品。
- 支付宝有多种周期扣款、代扣和签约产品,各产品的协议状态名称及查询接口可能不同。请以当前产品的接口文档和实际查询结果为准。
- 异步通知只能证明相应事件已经发生,不能代替完整的协议状态管理。如果通知缺失、超时或双方状态不一致,应通过相应的官方查询接口进行补偿核对。
- 如果合同约定的是“执行期数”,而不是“成功扣款期数”,失败、重试和补扣是否计入总期数可能会有不同规则,需要按所接入产品的计期规则确认。
备注:内容仅供参考。
