统一还款防重复治理

先考虑做对账,,再观察下重复数据情况

1. 文档目的

统一用户主动卡还款、第三方 Web H5、虚拟账号转账和系统自动代扣的敞口计算与防重复规则。

本文重点回答四个问题:

  1. 每种还款方式从什么时候开始占用敞口。
  2. 敞口金额怎么算、会阻断哪些还款。
  3. 敞口保持多久,什么情况下释放、升级或转人工。
  4. 通道成功但 FCS 尚未入账、结果未知和迟到资金如何持续保护并完成处置。

Monnify 按卡月度失败限额、渐进降频和自动代扣暂停规则由 37-Monnify按卡月度失败限额与自动代扣控频 独立承担。

还款 H5 的页面、文案、30 分钟支付会话和通道接口详见 35-还款H5支付会话时效与代扣在途拦截 —— 暂hold。当前代码分析统一放在文末第 14 节,不穿插在业务规则中。

2. 适用范围与责任

2.1 适用范围

  • 用户主动银行卡还款。
  • Paystack、Monnify 等第三方 Web H5 还款。
  • Monnify 虚拟账号转账,包括无意图到账和迟到到账。
  • 系统日常自动代扣。
  • 通道查询、回调、FCS 入账、补偿、人工挂起和退款/溢缴款。

2.2 责任边界

责任方业务责任
FCS当前应还、贷款余额、还款分配、业务入账、溢缴款和最终还款结果的权威
统一还款编排计算可还金额,维护软保护和硬敞口,决定是否允许新还款、阻断范围及下一处理动作
PMT创建支付尝试、调用通道、保存参考号和流水、接收回调、主动查询、失败分类及通知补偿
H5/App展示服务端状态和可用操作;不得根据倒计时、回跳或按钮点击自行释放敞口
运营/财务处理自动补偿无法解决的未知、资金冲突、超额、退款和人工补账

3. 敞口模型

3.1 三类保护

类型含义是否扣减可还金额默认阻断范围
软保护 SOFT_INTENT用户近期可能通过虚拟账号转账,但尚无通道请求或到账证据否只暂停自动代扣;用户主动换方式时需确认替换
支付风险 PAYMENT_RISK通道请求准备发送、已经发送、处理中或结果未知是阻断与该金额重叠的新还款;未知或无法安全分配时阻断整笔贷款
真实资金 CONFIRMED_FUNDS通道成功或虚拟账号已经到账,等待 FCS 入账或退款是保护实际成功/到账金额;冲突、超额或无法分配时阻断整笔贷款

业务上只把 PAYMENT_RISK 和 CONFIRMED_FUNDS 叫作“硬敞口”。软保护用于调度,不代表已经发生资金。

3.2 敞口金额怎么算

敞口维度至少包含:业务产品 + 客户 + 借据。

同一逻辑还款意图下的执行预占、PMT 订单、通道流水和成功资金只计算一次,不能重复扣减。

硬敞口金额
= 未释放的 PAYMENT_RISK 金额
+ 未完成入账/退款的 CONFIRMED_FUNDS 金额
 
可发起金额
= max(FCS 当前剩余应还 - 硬敞口金额, 0)

阻断范围分为两类:

阻断范围适用条件处理
金额级 AMOUNT_ONLY金额、币种、贷款归属和状态都可信,且业务已支持安全分笔只保护对应金额,用户可以偿还剩余未覆盖金额
贷款级 LOAN_HARD结果未知、金额冲突、多候选、人工挂起,或业务尚未支持安全分笔当前贷款可发起金额直接按 0 处理

首期无法证明金额级并发安全的业务,必须使用 LOAN_HARD,不能自行开放剩余金额还款。

3.3 通用状态

状态保护类型产品/技术含义可以退出的条件
SOFT_INTENT_ACTIVE软保护已展示虚拟账号并登记转账意愿2 小时到期无到账,或原子替换为其他主动方式
EXECUTION_RESERVED支付风险已预占金额,准备调用通道证明请求未发送后释放;已发送或无法确认则继续保护
CHANNEL_PROCESSING支付风险通道处理中明确成功、明确失败/失效或转未知核对
UNKNOWN_RECONCILING支付风险、贷款级阻断结果未知,系统正在查原交易查得终态;超过自动核对时限后转人工
CHANNEL_SUCCESS真实资金通道成功,等待 FCS进入 FCS 入账
POSTING真实资金FCS 正在入账入账完成或发现冲突
POSTED无FCS 入账完成读取最新余额决定后续可还金额
CONFIRMED_FAILED无权威终态失败,内部无成功资金完成幂等关单并保存失败事实;通道专项计数按独立需求处理
EXPIRED_NO_PAYMENT无通道明确过期且无资金完成幂等关单
MANUAL_REVIEW_HOLD支付风险或真实资金、贷款级阻断自动处理无法安全得出结论人工确认成功、失败、重复资金或继续挂起

4. 各还款方式怎么计算和处理

4.1 总表

还款方式什么时候开始保护敞口金额时间成功怎么处理失败/到期/未知怎么处理
用户主动卡还款浏览和选卡不占用;服务端接受提交、准备调用通道前创建执行预占服务端确认的本次还款金额未发送预占 2 分钟;通道已发送后无固定自动释放时间转真实资金,保护成功金额至 FCS 入账明确失败无资金则释放;未知自动查单 30 分钟,仍未知则贷款级挂起
Paystack Web H5浏览页面不占用;用户确认付款、创建 Checkout 前预占本次 Checkout 金额我方页面及 Paystack 会话暂定 30 分钟;最迟 45 分钟转人工转真实资金,保护成功金额至 FCS 入账页面到期先查原单;明确失败/过期无资金才释放;仍未知则挂起
Monnify Web H5用户确认付款、创建 Monnify Invoice 前预占本次 Invoice 金额创建时设置 expiryDate=服务端创建时间+30分钟;我方页面与 Invoice 同时到期;最迟 45 分钟转人工以 Invoice/关联交易权威结果确认成功,转真实资金并保护至 FCS 入账30 分钟到期查询原 Invoice;只有 Invoice 明确失效/过期、关联交易无成功资金时才释放;仍待处理或未知则继续保护,45 分钟转人工
系统自动代扣进入候选池不占用;选定订单、创建代扣订单和调用通道前预占本次任务确定的扣款金额未发送预占 2 分钟;已发送后自动核对 30 分钟转真实资金,保护至 FCS 入账明确失败释放;未知 30 分钟后贷款级挂起;通道专项控频按对应独立需求执行
虚拟账号主动转账展示虚拟账号并创建转账意愿后仅建立软保护软保护金额为 0;到账后按实际到账金额形成真实资金软保护 2 小时;重复进入不续期到账后保护实际金额至 FCS 入账/退款2 小时无到账则结束软保护;迟到到账仍接收并按真实资金处理
虚拟账号无意图/迟到到账到账前无法占用;收到有效资金时立即形成真实资金实际到账金额;超过应还部分进入溢缴款/退款到账后无固定自动释放时间唯一匹配后入账;完成后按最新余额重算多候选、与其他成功重叠或超额时贷款级挂起

以上时间为首版可配置值。到达自动处理时限只表示升级处理,不能把可能已经发生的支付自动当成失败。

4.2 主动卡还款、Web H5 和自动代扣流程

flowchart TD
    A[用户提交或任务选中订单] --> B[读取FCS剩余应还和全部保护]
    B --> C{存在贷款级阻断?}
    C -- 是 --> X[拦截 不创建订单 不调用通道]
    C -- 否 --> D{本次金额超过可发起金额?}
    D -- 是 --> X
    D -- 否 --> E[原子创建2分钟执行预占]
    E --> F{准备调用前再次校验}
    F -- 不通过 --> G[确认未发送 释放预占]
    F -- 通过 --> H[标记发送中并调用通道]
    H --> I{通道结果}
    I -- 成功 --> J[升级为真实资金]
    J --> K[FCS入账与补偿]
    K --> L[入账完成 读取最新余额重算]
    I -- 明确失败且无资金 --> M[关单并释放支付风险]
    I -- 处理中或未知 --> N[保留支付风险 查询原交易]
    N --> O{自动核对时限内得到终态?}
    O -- 是 --> I
    O -- 否 --> P[贷款级人工挂起并群播报]

执行预占的释放必须同时满足:

  • 状态明确为“请求未发送”。
  • 不存在 PMT 支付订单、通道参考号和通道流水。
  • 不存在成功回调、查询成功或到账事实。

只要处于发送中、已发送或无法确认是否发送,就不能按 2 分钟自动释放。

4.3 Web H5 特殊时间线

flowchart LR
    A[T0 创建Paystack会话或Monnify Invoice并占用金额] --> B[T+30分钟 页面及支付会话到期并查原单]
    B --> C{通道}
    C -- 成功 --> D[真实资金 等待FCS入账]
    C -- 明确失败或过期无资金 --> E[释放支付风险]
    C -- 处理中或未知 --> F[继续保护并补偿查询]
    F --> G[T+45分钟仍未知 转人工挂起]
  • Paystack 专用 Integration 可将会话超时设为 30 分钟;共享 Integration 修改前必须完成影响评估。
  • Monnify 使用 Invoice Create 获取 checkoutUrl,创建时将必填 expiryDate 设置为服务端创建时间加 30 分钟,格式为 YYYY-MM-DD HH:MM:SS。
  • Monnify 到期查询使用原 invoiceReference,不能通过重新创建 Invoice 判断旧单失败。
  • 页面倒计时、用户关闭页面和回跳结果都不能直接释放敞口。
  • 详细页面行为和通道接口见 35-还款H5支付会话时效与代扣在途拦截 —— 暂hold。

4.4 虚拟账号流程

flowchart TD
    A[展示虚拟账号] --> B[建立2小时软保护 暂停自动代扣]
    B --> C{2小时内到账?}
    C -- 是 --> D[按实际到账金额建立真实资金]
    D --> E{能唯一匹配贷款?}
    E -- 是 --> F[FCS入账]
    F --> G[读取最新余额重算]
    E -- 否 --> H[贷款级人工挂起]
    C -- 否 --> I[结束软保护 自动代扣可重新评估]
    I --> J{之后发生迟到到账?}
    J -- 否 --> K[结束]
    J -- 是 --> D
    B --> L{用户主动改用卡或H5?}
    L -- 是 --> M[确认替换软意图]
    M --> N{原子创建新执行预占成功?}
    N -- 是 --> O[结束软保护 进入新支付流程]
    N -- 否 --> B

补充规则:

  1. 软保护不扣减可发起金额,但保护期内自动代扣必须跳过。
  2. 重复进入、复制账号或刷新不重新计算 2 小时。
  3. 同一贷款新的软保护窗口至少间隔 30 分钟,同一自然日最多 3 次;超过后仍可查看稳定虚拟账号,但不再新增自动代扣保护。
  4. 用户点击“已转账”不能升级为真实资金,只能触发页面查询提示。
  5. 用户改用其他主动方式时,服务端必须原子完成“关闭软保护 + 创建新执行预占”;新预占失败时保留原软保护。
  6. 软保护结束或被替换后,虚拟账号迟到到账仍必须接收。若与其他成功资金重叠,进入贷款级挂起和退款/溢缴款处置。

5. 时间与处理时限

场景首版时间到时动作是否自动释放
执行预占,且确认请求未发送2 分钟关闭预占,重新计算可还金额是
执行预占,但发送状态不明2 分钟转 UNKNOWN_RECONCILING,查询原交易否
主动卡/自动代扣结果未知T+1、5、15、30 分钟查询30 分钟仍未知则人工挂起否
Paystack/Monnify Web H5页面 30 分钟关闭页面继续支付并查原交易否
Monnify InvoiceexpiryDate=创建时间+30分钟页面到期并查询原 Invoice否;只有 Invoice 明确失效/过期、关联交易无成功资金才释放
Web H5 到期后仍未知45 分钟人工挂起并群播报否
虚拟账号软保护2 小时无到账则结束软保护并允许任务重新评估是,仅结束软保护
通道成功/虚拟账号到账待 FCS立即、T+1、3、10 分钟补偿15 分钟告警;30 分钟人工挂起否
P0 人工事件未认领10 分钟群提醒并升级负责人否
P1 人工事件未认领30 分钟群提醒并升级负责人否

所有时间按服务端时间计算并支持按业务、国家和通道配置。调整只能改变查询和升级节奏,不能把“已发送/未知/已成功”的硬敞口配置成自动释放。

6. 结果处理

6.1 统一决策表

权威结果内部资金事实处理结果敞口
成功金额、币种、参考关系一致进入 FCS 入账转 CONFIRMED_FUNDS,保护成功金额
明确失败/过期无成功回调、无到账、无 FCS 入账幂等关单释放当前 PAYMENT_RISK
处理中无冲突查询原交易保持 PAYMENT_RISK
未知/超时无法确认是否发送或受理自动核对;到时限转人工LOAN_HARD,不释放
失败结果与成功事实冲突存在成功回调、到账或 FCS 入账人工核对CONFIRMED_FUNDS + LOAN_HARD
虚拟账号多候选无法唯一匹配人工核对CONFIRMED_FUNDS + LOAN_HARD
实际资金超过剩余应还存在超额入账可还部分,剩余进入溢缴款/退款保护至入账和退款完成

6.2 幂等规则

  • 同一通道交易参考号永久只消费一次。
  • 同一资金事实最多匹配一个逻辑还款意图。
  • 同一逻辑意图中的预占、支付订单、流水和资金不能重复计入敞口。
  • 回调、主动查询和补偿重复到达只更新同一资金事实。
  • FCS 已入账后再次通知返回原结果,不重复更新贷款。
  • 明确失败只释放当前失败尝试,不释放其他软保护或真实资金。

7. 自动补偿、人工挂起与群播报

7.1 自动处理队列

队列处理对象自动动作转人工条件
预占回收EXECUTION_RESERVED 未推进核对是否存在订单、参考号、流水或发送证据无法证明未发送,或发现无主通道请求
通道核对UNKNOWN_RECONCILING只查询原参考号到自动核对时限仍未知,或状态冲突
FCS 入账补偿CONFIRMED_FUNDS 未入账重试通知并读取 FCS 结果30 分钟仍未入账、金额冲突或多候选
虚拟账号匹配已到账未入账按客户、贷款、币种和余额唯一匹配多候选、无候选、超额或与其他成功重叠

7.2 人工挂起

进入 MANUAL_REVIEW_HOLD 后:

  1. 保持支付风险或真实资金,并把阻断范围升级为 LOAN_HARD。
  2. 停止该贷款的新主动还款、自动代扣、自动重试和自动结算。
  3. 创建唯一异常事件,记录 owner、级别、首次发现时间、处理时限和下一步动作。
  4. 群消息发送失败不能解除挂起。
  5. 人工结论只能是:确认成功并入账、确认失败无资金并释放、重复资金进入退款/溢缴款、继续未知保持挂起。

7.3 群播报

  • P0/P1 创建人工挂起后立即发送到按业务配置的支付、FCS、运营和财务群。
  • 消息包含:级别、业务、脱敏事件号、通道、金额与币种、冲突状态、owner、处理时限和受限详情链接。
  • 禁止包含姓名、手机号、BVN、完整卡号、账号、Token、原始鉴权数据和完整通道报文。
  • 同一异常只发送一次首次消息;状态升级、超时和关闭时更新,不因轮询重复刷屏。

8. 页面状态与埋点

H5 详细交互、英文文案和事件字典由 35-还款H5支付会话时效与代扣在途拦截 —— 暂hold 统一维护。本文只定义产品需要识别的场景。

产品场景页面/owner关键事件服务端属性衡量指标
页面进入与可还状态还款首页 / wp_h5repayment_page_viewpage_state、protection_level、block_scope、has_active_intent各保护层级用户占比
提交还款提交区 / wp_h5repayment_submit_click、repayment_submit_resultpayment_method、result_status、blocked_reason、intent_reused提交成功率、复用率、硬敞口拦截率
虚拟账号软保护替换替换弹窗 / wp_h5repayment_soft_intent_replace_view;repayment_soft_intent_replace_click;repayment_soft_intent_replace_resultaction_type、target_method、protection_level替换确认率、原子替换成功率
自动代扣/硬敞口阻断阻断弹窗 / wp_h5repayment_duplicate_block_view;repayment_duplicate_block_clickactive_stage、protection_level、block_scope阻断量、查看状态率
状态查询结果页 / wp_h5repayment_status_refresh_click;repayment_status_refresh_resultbefore_status、after_status、result_status未知转终态率、等待时间

埋点结果必须来自服务端业务状态。禁止采集金额、卡号、银行账号、BVN、手机号、Token、通道参考号、内部订单号或原始错误文本。Native 与 H5 各自上报自己负责的页面生命周期。

9. 监控指标

  • 三类保护的创建数、金额、平均存续时间和状态转换。
  • AMOUNT_ONLY/LOAN_HARD 数量、金额和拦截转化损失。
  • 软保护创建、复用、无到账到期、主动替换、保护期内到账和迟到到账。
  • 执行预占确认未发送释放数、僵尸预占数和无主通道风险数。
  • 未知订单数量、金额、年龄、查询次数和最终成功/失败/人工比例。
  • 通道成功待 FCS 入账数量、金额、最长时长和补偿成功率。
  • 虚拟账号多候选、超额、重复资金和退款积压。
  • 敞口服务可用率、原子预占耗时、从严关闭请求数和恢复时间。
  • 人工挂起存量、超时、认领时间、关闭时间和群播报成功率。

10. 上线方案

10.1 数据盘点

  • 回填或隔离 FCS、PMT、WebPay、自动代扣和虚拟账号现有非终态订单。
  • 确认每类历史状态对应软保护、支付风险、真实资金或无需保护。

10.2 影子运行

  • 先记录敞口计算结果,不拦截现网。
  • 对比现网实际调用和新规则本应拦截,计算误拦截、漏拦截及回收影响。
  • 重点核对软保护无到账、执行预占僵尸、未知订单和成功待入账样本。

10.3 分阶段启用

  1. 先启用执行预占、跨入口硬敞口和原交易查询。
  2. 再启用虚拟账号软保护、替换流程和迟到到账处置。
  3. 最后启用金额级并发;未完成专项验收的业务继续使用 LOAN_HARD。

10.4 降级

  • 敞口控制不可用时,禁止新卡扣、Checkout 创建和自动代扣。
  • 原交易查询、回调、虚拟账号到账、FCS 入账和补偿必须继续运行。
  • 硬敞口读取失败不能默认无敞口。
  • 回滚不删除已有意图、敞口、资金事实和审计记录。

11. 验收标准

11.1 通用敞口

  1. 同一逻辑意图的预占、订单、流水和资金只计一次。
  2. 可发起金额严格等于 FCS 剩余应还减去未释放硬敞口。
  3. LOAN_HARD 存在时任何新增还款均在创建订单和调用通道前被拦截。
  4. 只有确认未发送的 2 分钟执行预占可以自动释放。
  5. 已发送、未知和成功待入账不会因本地超时自动释放。
  6. FCS 入账后读取最新余额重算,不直接恢复原额度。
  7. 敞口服务不可用时新支付从严关闭,资金接收和原交易处理不受影响。

11.2 各方式

  1. 主动卡连续提交两次只产生一个有效执行预占和最多一次通道调用。
  2. Web H5 页面与通道支付会话均按 30 分钟到期;Monnify Invoice 的 expiryDate 等于服务端创建时间加 30 分钟,到期后先查询原 Invoice。
  3. 自动代扣进入候选池不占敞口,调用前必须完成原子预占。
  4. 虚拟账号页面只建立 2 小时软保护,不扣减可还金额。
  5. 软保护重复进入不续期,超过每日次数不再暂停自动代扣。
  6. 软保护替换和新执行预占原子完成;失败时保留原软保护。
  7. 点击“已转账”不形成真实资金;实际到账才形成 CONFIRMED_FUNDS。
  8. 无意图或迟到到账全部接收;冲突和超额进入贷款级人工处置。

11.3 人工与数据

  1. 自动补偿达到时限才转人工;状态冲突和无主支付风险可以立即转人工。
  2. 人工挂起先升级贷款级阻断,再群播报;群消息失败不解锁。
  3. 页面和埋点状态来自服务端,不以点击、倒计时或回跳代替业务结果。
  4. 埋点和群消息均不包含客户、银行卡或通道敏感信息。
  5. 看板可以分开展示软保护、支付风险、真实资金和金额级/贷款级阻断。

12. 待确认参数

参数本文暂定值待确认方
执行预占未发送自动释放2 分钟支付/FCS研发
主动卡、自动代扣自动核对时限30 分钟支付/运营
Web H5 未知转人工45 分钟支付/运营
FCS 入账告警/转人工15/30 分钟FCS/财务
虚拟账号软保护2 小时支付/催收
新软保护最小间隔30 分钟产品/催收
同贷款每日软保护上限3 次产品/催收
金额级并发开放范围未完成专项验收前关闭FCS/支付
Monnify Invoice expiryDate 时区本期按 Africa/Lagos;Sandbox、生产分别验证支付/通道

13. 外部规则参考

14. 当前代码分析参考

本节只说明需求形成时的代码依据,不代表目标业务规则。代码版本变化后需重新核对。

14.1 分析版本

系统分析版本说明
FCS fund-controldev / 97f2312e49278f5698c6b2040cbcafe6c9d7a378本地与远端 dev 一致
PMT payment-middleorigin/master / 6aeda9b1d8c16e8a1cdd0be0e62ef81ec23d9b4e结论以只读获取的远端代码为准
H5 wp_h5dev / f76dec622f70e928a8bdf292e2341a12ded318a7本地与远端 dev 一致

未读取生产 Apollo 和生产数据库。生产状态及通道参数需上线前核对。

14.2 当前记录方式与缺口

方式当前记录方式主要缺口
主动卡/Web H5FCS 保存 WebPay 初始化订单;PMT 保存支付订单和流水不同参考号之间缺少客户/贷款级统一敞口
自动代扣FCS 生成 NC/OC 等订单,PMT 选择卡/Token/Mandate在途检查未完整覆盖其他主动方式,检查和创建存在竞争窗口
虚拟账号PMT 保存账号、绑定和入账流水,通知 FCS 入账外部资金不可阻止;多候选时存在一笔资金结算多订单风险
查询与回调PMT 按支付订单/流水更新并通知 FCS主要按订单号锁定,未知与明确失败未统一治理

14.3 关键代码证据

系统代码位置/方法分析结论
FCSOrderFacility.isExistOnTheWayOrder通用在途未完整覆盖 PHP、VP 等入口
FCSOrderFacility.checkSelfOrderOnWay只检查同交易类型,不是跨方式防重
FCSPlutusBusiness.plutusWebPayInit初始化使用短时客户锁,但不同初始化可产生新参考号
FCSPlutusBusiness.plutusWebPayCallBackEventsWebPay 回调后才创建 PHP 业务订单
FCSFcsJobBusiness.generateCommonChargeOrder自动代扣检查与订单创建不是统一原子占用
PMTInnerPaymentService.repayOrderForH5主要按调用方 orderId 幂等
PMTcheckPaymentOrderPending部分处理中订单可能被本地置失败后放行新尝试
PMTVaOrderSettleBusiness.trySettleRepayOnTransaction异常多绑定时存在一笔入账结算多订单风险
PMTMonnifyRepayCallbackBusiness主要消费成功回调,失败和处理中依赖同步结果或主动查询
PMTPaystack/Monnify WebPay 初始化当前 Monnify 仍使用交易初始化获取 Checkout,需改为 Invoice Create,并通过必填 expiryDate 设置 30 分钟有效期