先考虑做对账,,再观察下重复数据情况
还款 H5 支付会话时效与代扣在途拦截
1. 需求背景
现有还款 H5 允许用户通过银行卡或第三方 Web Checkout 主动还款。若通道支付页面长期有效、页面倒计时与通道真实状态不一致,或系统自动代扣已经处理中时仍允许用户发起主动还款,可能形成两笔并行支付并造成重复还款。
本需求作为独立迭代新增,不修改 13-还款链接生成 和 32-还款H5页面改造 的既有正文。跨主动还款、虚拟账号、自动代扣和入账补偿的总体互斥规则继续以 34-统一还款防重复治理 —— 暂hold 为准。
2. 目标
- 为 Paystack、Monnify Web H5 单次支付会话建立明确时效,产品口径暂定 30 分钟。
- 会话到期后先核验原交易,不以页面倒计时替代通道终态,不允许直接创建第二笔交易。
- 自动代扣处理中时,在页面进入和主动还款提交两个节点拦截用户,不生成主动还款意图。
- 对临近过期、到期、查单、重新获取会话和代扣拦截建立可衡量、无敏感信息的前端埋点。
3. 适用范围
3.1 本期包含
- 还款 H5 中 Paystack、Monnify Web Checkout 的支付会话创建、倒计时、到期和恢复。
- 自动代扣处理中时的页面禁用、弹窗提示、查看状态和重新开放规则。
- 用户已获取虚拟账号、仍处于软意图保护时改用卡或 Web Checkout 的确认替换流程。
- 服务端统一状态校验、原支付会话复用、防并发创建和通道终态查询。
- Paystack Integration 级支付会话超时配置评估。
- Monnify 使用 Invoice Create 返回 H5
checkoutUrl,并通过expiryDate将 Invoice 有效期设置为 30 分钟。 - 对应前端埋点、业务监控、灰度和验收。
3.2 本期不包含
- 不修改银行卡 OTP、手机号后 4 位补充验证、协议展示和切卡交互。
- 不重写还款链接和短链的触达有效期规则。
- 不调整虚拟账号 2 小时保护期;Monnify 按卡每月 22 次及自动代扣控频由 37-Monnify按卡月度失败限额与自动代扣控频 独立定义。
- 不以本需求替代 FCS 入账补偿、退款或溢缴款流程。
4. 当前实现结论
| 系统 | 当前事实 | 本期影响 |
|---|---|---|
还款 H5 wp_h5 | 收到通道 URL 后直接跳转,页面未形成统一的服务端支付会话倒计时和到期恢复状态 | 需新增倒计时、到期查单和代扣在途阻断体验 |
| PMT Paystack WebPay | 初始化交易时未传单笔过期时间;现有 abandonedTimeoutMin 用于后台查询分类,不是 Checkout 页面有效期 | 不能用现有后台参数代替 30 分钟页面时效 |
| PMT Monnify WebPay | 当前使用交易初始化获取 Checkout,未设置单笔过期时间 | 改用 Invoice Create 获取 checkoutUrl,创建时传入服务端计算的 30 分钟 expiryDate |
| FCS / PMT | 不同入口依赖各自订单状态,用户刷新或重新进入可能形成新的参考号 | 创建新会话前必须查询并复用原还款敞口 |
分析基线:FCS fund-control dev@97f2312e49278f5698c6b2040cbcafe6c9d7a378、PMT payment-middle origin/master@6aeda9b1d8c16e8a1cdd0be0e62ef81ec23d9b4e、H5 wp_h5 dev@f76dec622f70e928a8bdf292e2341a12ded318a7。
5. 角色与责任边界
| 角色/系统 | 责任 |
|---|---|
| 用户 | 选择主动还款方式、确认付款、查看原交易状态;不得通过刷新获得并行会话 |
| 还款 H5 | 展示服务端状态和倒计时;在受限状态禁用操作;触发查单,不自行判定支付终态 |
| FCS | 提供最新贷款、应还、软意图、支付风险、真实资金和阻断范围;按已确认金额计算剩余可还,无法安全计算时返回贷款级阻断 |
| PMT | 复用或创建唯一支付尝试;保存通道参考;查询、接收回调并输出权威支付状态 |
| Paystack / Monnify | 创建 Paystack Checkout 或 Monnify Invoice H5、处理资金并返回查询或回调结果 |
| 运营/支付 | 审核 Paystack 共享配置影响,处理长时间未知、状态冲突和人工挂起 |
6. 关键定义
| 名称 | 定义 |
|---|---|
| 还款入口有效期 | 短信、客服或催收链接可以进入我方还款 H5 的时间,继续按触达场景配置 |
| 支付会话有效期 | 从通道初始化成功并返回 Checkout URL 开始计时的单次支付页面时效,本期暂定 30 分钟 |
| 通道终态 | 通道明确返回成功、失败或过期,并经服务端核验金额、币种和参考关系后的状态 |
| 软意图保护 | 用户已获取虚拟账号但尚无通道请求或到账证据;不扣减可还金额,但保护期内暂停自动代扣 |
| 支付风险/真实资金 | 通道可能已受理,或资金已经成功/到账形成的硬敞口;按服务端返回金额和阻断范围限制新还款 |
| 可安全重建 | 原交易明确未支付、无成功资金事实、无未知结果且统一敞口已释放后,允许创建新会话 |
入口有效期与支付会话有效期必须分离。长期触达链接不得直接绑定一个提前生成的 Paystack authorization_url 或 Monnify checkoutUrl。
7. 主流程
用户进入还款 H5
-> 服务端查询最新贷款、应还、保护层级和阻断范围
-> 若自动代扣处理中:禁用方式并展示阻断弹窗,不生成主动还款意图
-> 若只有虚拟账号软意图:保留虚拟账号方式;选择其他方式时展示替换确认
-> 若已有主动支付会话:返回原会话或原状态,不生成新参考号
-> 若可发起:用户选择方式并确认付款
-> 提交前服务端再次检查硬敞口;替换软意图时原子关闭软意图并创建执行预占
-> 创建唯一支付会话并返回服务端 expiresAt
-> H5 展示倒计时并进入通道 Checkout
-> 回跳、回调或主动刷新均查询原交易
-> 成功:持续保护已确认资金金额至 FCS 入账;无法安全计算剩余金额时整笔贷款阻断
-> 明确失败/过期且无资金:释放敞口,允许用户重新选择
-> 处理中/未知/冲突:继续保护,必要时人工挂起8. 支付会话时效规则
8.1 通用规则
- 只有用户确认付款且提交前校验通过后,才初始化通道支付会话。
- 通道初始化成功时由服务端记录
startedAt和expiresAt;暂定expiresAt = startedAt + 30 分钟。 - 页面倒计时以服务端时间为准。刷新、退出重进、更换设备、返回上一步均不得重置原会话。
- 相同还款意图重复提交时,服务端返回原有效会话或原处理状态,不创建新参考号。
- 剩余 5 分钟时,同一会话只展示一次即将过期提示,不自动刷新或生成订单。
- 到达 30 分钟时,H5 关闭继续付款和再次提交入口,展示“支付会话已过期,正在确认原支付结果”,并查询原交易。
- 页面到期不是通道失败。只有通道已给出可释放终态、内部无成功或未知资金事实且统一敞口已释放,才展示“重新获取支付会话”。
- 回跳只用于进入结果查询,不作为成功或失败依据。
8.2 Paystack
- Paystack 的支付会话超时为 Integration 级配置,不是单笔初始化交易参数。
- 若还款使用独立 Paystack Integration,可将支付会话超时设为
1800秒。 - 若 Integration 同时承载其他产品,修改会影响该 Integration 下全部 Checkout。上线前必须盘点使用范围、留存原值并完成回归;无法接受共享影响时,应使用独立 Integration。
- H5 到达 30 分钟后停止继续付款并核验原
reference。原交易为处理中、未知或已成功时不得创建新会话。 - 现有
abandonedTimeoutMin仅用于后台查询后的本地分类,不得作为页面倒计时或敞口释放依据;其值需与新策略一并评审。
8.3 Monnify
- Monnify H5 改用 Invoice API 创建:
POST /api/v1/invoice/create。 - 服务端在创建 Invoice 时设置唯一
invoiceReference,并将必填expiryDate设置为“服务端创建时间 + 30 分钟”;格式按官方要求使用YYYY-MM-DD HH:MM:SS。 expiryDate由服务端按 Monnify 商户约定时区生成,本期按Africa/Lagos执行;由于字段格式本身不携带时区,上线前必须在 Sandbox 和生产商户分别验证解析结果。- 创建成功后只使用响应中的
checkoutUrl跳转 H5,同时保存invoiceReference、transactionReference、invoiceStatus和实际返回的expiryDate。 - Invoice Create 返回缺少
checkoutUrl、invoiceReference或expiryDate,或者返回的失效时间与预期 30 分钟不一致时,不跳转 H5;保留原逻辑意图并按原invoiceReference查询/处置,不能直接重复创建。 - H5 倒计时以服务端保存的 Invoice
expiryDate为准,页面和 Monnify Invoice 使用同一 30 分钟截止时间;刷新、退出重进和更换设备不重置。 - 到达 30 分钟后关闭继续付款入口,并通过
GET /api/v1/invoice/{invoiceReference}/details查询原 Invoice;不得通过创建新 Invoice 判断旧 Invoice 已失败。Invoice 已关联transactionReference或显示已支付时,再通过交易查询核验支付状态、实付金额和币种。 - 只有 Invoice 明确失效/过期、关联交易不存在成功资金时才释放敞口。Invoice 仍为待处理、查询失败或状态未知时继续保护,并在创建后 45 分钟仍无法确认时转人工挂起。
- Invoice 到期后收到的有效成功回调或查单结果仍必须正常核验并入账,不能因页面已过期而拒绝真实资金。
9. 在途还款拦截与软意图替换
9.1 触发节点
- 页面进入:H5 首次加载时由服务端返回是否存在自动代扣处理中或成功待入账。
- 提交前:用户点击任一主动还款提交按钮时,服务端再次原子校验,防止页面加载后状态发生变化。
- 状态刷新:用户点击查看状态时只查询原代扣,不创建主动还款意图。
9.2 页面行为
- 存在自动代扣处理中时,银行卡、Paystack、Monnify、虚拟账号等全部主动还款方式置为不可提交。
- 首次识别该状态时展示阻断弹窗;关闭弹窗后页面继续保持禁用状态。
- 弹窗标题:
Repayment in progress - 弹窗正文:
An automatic repayment is being processed. Please wait for the result before trying another repayment method. - 操作:
Check Status、Close;不提供Continue、Pay anyway等继续支付入口。 Check Status查询原代扣:- 明确失败且敞口已释放:关闭阻断状态,重新加载最新应还和可用方式;
- 通道成功但 FCS 未入账:继续阻断并提示正在更新贷款;
- 处理中、未知或人工挂起:继续阻断;
- 已入账:展示最新贷款状态,不再按旧金额发起还款。
9.3 服务端硬规则
- 前端禁用只负责体验,是否允许还款必须以提交时服务端结果为准。
- 被拦截的请求不得创建主动还款意图、FCS 还款订单、PMT 订单或通道参考号。
- 两个并发请求同时通过普通查询时,仍须由统一还款敞口的原子占用保证最多一个请求成功。
- 自动代扣状态未知、成功待入账或人工挂起时,与处理中同等拦截。
- 本期自动代扣处理中默认由服务端返回
block_scope=loan_hard,页面按全方式阻断实现;只有完成金额级并发专项验收后,才可另行按服务端可还余额开放部分还款,前端不得自行放宽。
9.4 虚拟账号软意图替换
- 服务端返回
protectionLevel=SOFT_INTENT时,页面继续展示虚拟账号保护状态;该状态不等于资金处理中。 - 用户继续使用虚拟账号不产生新意图、不重置两小时保护期。
- 用户选择银行卡、Paystack 或 Monnify 时,先展示替换确认,说明系统将结束自动代扣保护,但之前发起的银行转账仍可能迟到。
- 用户确认后,服务端必须在同一原子动作中重新检查硬敞口、关闭软意图并创建新方式的执行预占;成功后才能继续支付。
- 若硬敞口已出现、软意图已变化或新执行预占失败,保留原软意图和页面状态,不得只关闭保护后让用户重新提交。
- 用户选择保留虚拟账号时关闭弹窗并保持原软意图;不调用支付通道。
10. 页面状态与文案
| 状态 | 页面主提示 | 可用操作 |
|---|---|---|
| 会话有效 | 显示剩余支付时间 | 继续原会话、查看状态 |
| 剩余 5 分钟 | Payment session expires soon | 继续原会话 |
| 页面 30 分钟到期 | Payment session expired. We are confirming the payment status. | 查看状态 |
| 原交易处理中/未知 | Payment is being confirmed. Please do not pay again. | 查看状态、关闭 |
| 原交易明确未支付且已释放 | This payment session has ended. You can start a new one. | 重新获取支付会话 |
| 虚拟账号软意图有效 | Bank transfer details are active. Automatic repayment is paused temporarily. | 继续查看虚拟账号;选择其他方式时确认替换 |
| 自动代扣处理中 | Repayment in progress | Check Status、Close |
| 通道成功待入账 | Payment received. We are updating your loan. | 查看状态 |
| 人工核对中 | Your repayment is being reviewed. Please do not pay again. | 查看状态、联系客服 |
文案不展示通道原始错误、卡号、账号、支付参考号或内部订单号。
11. 异常与边界场景
| 场景 | 处理规则 |
|---|---|
| 通道初始化超时,结果未知 | 保留原意图并按原参考号查单;不得重复初始化 |
| 页面已到期但成功回调后到 | 接受并核验真实资金,保持敞口至 FCS 入账 |
| 页面显示失败但查单成功 | 以通道成功和 FCS 入账事实纠正页面,不允许再支付 |
| 查询失败或通道不可用 | 展示确认中,继续保护;达到阈值后人工挂起和群播报 |
| 用户同时在两台设备提交 | 统一敞口原子占用,最多创建一个新支付会话 |
| 用户切换 Paystack/Monnify/银行卡 | 原会话未到可释放终态前不允许切换后创建新交易 |
| 用户在虚拟账号软意图期间切换方式 | 展示替换确认;服务端原子完成“关闭软意图 + 创建执行预占”,失败时保持原软意图 |
| 替换软意图后虚拟账号迟到入账 | 继续接受真实资金;与新支付重叠时按 34 号需求升级贷款级阻断和人工处理 |
| 30 分钟后 Monnify Invoice 仍为待处理 | 继续保留原 Invoice 敞口并查询;创建后 45 分钟仍无法确认时转人工挂起,不能提前释放 |
| Paystack 共享 Integration 无法改为 30 分钟 | 不直接改共享配置;我方 H5 控制 30 分钟并保持终态查询,或拆分专用 Integration |
| 通道状态与内部资金事实冲突 | 进入 MANUAL_REVIEW_HOLD,继续占用敞口并按 34 号需求群播报 |
12. 前端埋点要求
12.1 事件表
event_name | 类型 | 页面/场景与 owner | 唯一触发时机 | 关键属性及来源 | 指标/漏斗 |
|---|---|---|---|---|---|
repayment_session_expiring_view | view | 支付会话倒计时,wp_h5 | 同一会话首次进入剩余 5 分钟时 | payment_method 取服务端会话;expiry_bucket=within_5_minutes | 临近过期会话数、临近过期完成率 |
repayment_session_expired_view | view | 会话过期状态,wp_h5 | 服务端 30 分钟到期状态首次展示时 | payment_method、provider_state 稳定枚举、can_restart 取服务端 | 会话到期率、到期后未知率 |
repayment_session_restart_click | click | 会话过期状态,wp_h5 | 用户有效点击重新获取支付会话时 | payment_method、source_status 取服务端 | 安全重建意愿 |
repayment_session_restart_result | result | 会话过期状态,wp_h5 | 服务端返回复用、创建或拦截结果时 | result_status、blocked_reason 稳定分类、payment_method | 安全重建成功率、未知拦截率 |
repayment_duplicate_block_view | popup_view | 自动代扣在途弹窗,wp_h5 | 页面首查或提交前校验首次实际展示弹窗时 | active_stage、protection_level、block_scope、block_source=auto_debit_processing 取服务端;未选择方式时不传 payment_method | 自动代扣处理中拦截量 |
repayment_duplicate_block_click | popup_click | 自动代扣在途弹窗,wp_h5 | 用户点击 Check Status 或 Close 时 | action_type=check_status/close、active_stage 取服务端 | 拦截后查单率、关闭率 |
repayment_soft_intent_replace_view | popup_view | 虚拟账号软意图替换弹窗,wp_h5 | 有效软意图下选择其他主动方式并实际展示弹窗时 | protection_level=soft_intent 取服务端;target_method 取 UI | 软意图后切换方式需求量 |
repayment_soft_intent_replace_click | popup_click | 虚拟账号软意图替换弹窗,wp_h5 | 点击确认替换或保留虚拟账号时 | action_type=confirm_replace/keep_virtual_account、target_method 取 UI | 替换确认率 |
repayment_soft_intent_replace_result | result | 虚拟账号软意图替换弹窗,wp_h5 | 服务端返回替换成功、软意图已到期或硬敞口拦截时 | result_status、blocked_reason、protection_level 取服务端 | 原子替换成功率、并发拦截率 |
Check Status 的查询结果复用既有 repayment_status_refresh_result,不新增同义结果事件。
12.2 埋点约束
- 所有事件使用稳定
snake_case名称,event_page沿用还款页既有稳定标识,不拼接通道、金额、ID 或错误文本。 - 页面曝光只取 UI 实际展示;支付成功、失败、处理中和能否重建必须取服务端业务结果。
protection_level、block_scope、是否允许替换以及替换结果均取服务端,不根据页面倒计时或本地缓存推导。- 同一 H5 页面会话内,同一状态的提示曝光只上报一次;刷新后服务端状态未变化不得重复计为新业务曝光。
- Native 只记录容器进入/退出,H5 记录页面和弹窗,不由 Native 代报 H5 结果。
- 禁止采集卡号、账号、BVN、手机号、Token、Mandate、通道参考号、内部订单号和原始错误文本。
13. 业务监控
- Paystack、Monnify 会话创建数、复用数、30 分钟到期数及到期率。
- 到期后查询结果分布:成功、明确失败/过期、处理中、未知、人工挂起。
- Monnify Invoice 创建成功率、
checkoutUrl返回率、30 分钟到期率及到期后查询结果分布。 - 会话重新获取请求数、成功数、被处理中/未知/成功待入账拦截数。
- 虚拟账号软意图替换弹窗曝光、确认、保留、替换成功和硬敞口拦截数。
- 页面进入拦截和提交前二次拦截量;被拦截请求创建订单或通道参考号的数量目标为 0。
- Paystack 当前 Integration 超时配置、共享消费者范围和配置变更记录。
14. 上线与降级
- 影子阶段:只记录服务端支付会话、30 分钟到期判断和代扣在途命中,不改变用户操作;核对误拦截和漏拦截。
- 灰度阶段:先启用提交前服务端硬拦截和原会话复用,再灰度页面倒计时、弹窗和重新获取入口。
- Paystack 配置:先查询并留存生产 Integration 原值;确认专用或完成共享影响评估后,才允许设置
1800秒。 - Monnify:切换到 Invoice Create,按服务端时间生成 30 分钟
expiryDate,补齐原 Invoice 查询、回调接收和敞口保护。 - 降级:页面状态服务不可用时,从严禁止创建新主动支付;原交易查询、回调、FCS 补偿和入账继续运行。
- 回滚页面能力时,不删除已有支付会话和敞口记录,不把未知订单批量置为失败。
15. 验收标准
- 用户确认付款前不创建通道 Checkout;通道初始化成功后形成唯一会话并返回服务端 30 分钟到期时间。
- 刷新、退出重进、更换设备不会延长原会话,也不会生成新参考号。
- 剩余 5 分钟提示同一会话只展示和上报一次。
- 30 分钟到期后页面不能继续付款,系统先查询原交易;处理中、未知、成功或成功待入账均不能创建新会话。
- Paystack 专用还款 Integration 的支付会话超时为
1800秒;共享 Integration 未经影响评估不得直接修改。 - Monnify Invoice 的
expiryDate等于服务端创建时间加 30 分钟,H5 使用返回的checkoutUrl,页面倒计时与 Invoice 截止时间一致。 - Monnify
expiryDate按Africa/Lagos服务端时间生成,并在 Sandbox、生产商户分别验证实际失效时间;误差超出双方确认容差时阻断上线。 - Monnify Invoice Create 缺少
checkoutUrl、invoiceReference或expiryDate,或者返回失效时间不符合 30 分钟规则时,不跳转页面、不直接重复创建,并能查询原 Invoice。 - 原交易明确未支付且敞口释放后,用户才能看到并使用“重新获取支付会话”。
- 自动代扣处理中时,页面进入即禁用主动还款并展示弹窗;关闭弹窗后方式仍不可提交。
- 页面加载后才进入代扣处理的场景,在主动还款提交前被服务端二次校验拦截。
- 被自动代扣在途拦截的请求不创建主动还款意图、FCS/PMT 订单或通道参考号。
Check Status只有在原代扣明确失败且敞口释放后才重新开放方式;其他非终态继续拦截。- 两台设备并发提交时最多创建一个支付会话,另一请求返回原会话或拦截状态。
- 通道与内部资金事实冲突时自动挂起并群播报,不允许用户再次还款。
- 虚拟账号软意图期间改用其他方式时展示替换确认;服务端原子替换成功后才继续支付,失败时保留原软意图。
- 替换软意图后发生迟到入账仍正常接收;与新支付重叠时进入贷款级阻断和人工处置。
- 第 12 节事件按唯一时机上报,结果取服务端,不含卡、账号、手机号、参考号或原始错误信息。
16. 通道接口附录
本节仅作为本迭代的通道能力与研发评审依据,不改变正文中的业务责任边界。
16.1 Paystack
| 用途 | 方法与地址 | 本需求使用方式 |
|---|---|---|
| 查询支付会话超时 | GET https://api.paystack.co/integration/payment_session_timeout | 上线前查询当前 Integration 配置并留存原值 |
| 更新支付会话超时 | PUT https://api.paystack.co/integration/payment_session_timeout | 请求 timeout=1800;仅在完成 Integration 影响评估后执行 |
| 初始化支付 | POST https://api.paystack.co/transaction/initialize | 使用唯一 reference 初始化;该接口无单笔有效期字段 |
| 核验支付 | GET https://api.paystack.co/transaction/verify/{reference} | 服务端核验状态、金额和币种,不以回跳判断 |
16.2 Monnify
| 用途 | 方法与地址 | 本需求使用方式 |
|---|---|---|
| 获取访问令牌 | POST https://api.monnify.com/api/v1/auth/login | 仅服务端使用商户凭证;密钥不得进入 H5 |
| 创建 Invoice H5 | POST https://api.monnify.com/api/v1/invoice/create | 使用唯一 invoiceReference;必填 expiryDate=服务端创建时间+30分钟;从响应 checkoutUrl 获取 H5 页面 |
| 查询 Invoice | GET https://api.monnify.com/api/v1/invoice/{invoiceReference}/details | 查询原 Invoice 的 invoiceStatus、金额、expiryDate 和 transactionReference,不创建新 Invoice 代替查单 |
| 核验支付交易 | GET https://api.monnify.com/api/v2/merchant/transactions/query?transactionReference={transactionReference} | Invoice 已产生交易后,核验通道支付状态、实付金额和币种;参数需 URL 编码 |