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

还款 H5 支付会话时效与代扣在途拦截

1. 需求背景

现有还款 H5 允许用户通过银行卡或第三方 Web Checkout 主动还款。若通道支付页面长期有效、页面倒计时与通道真实状态不一致,或系统自动代扣已经处理中时仍允许用户发起主动还款,可能形成两笔并行支付并造成重复还款。

本需求作为独立迭代新增,不修改 13-还款链接生成 和 32-还款H5页面改造 的既有正文。跨主动还款、虚拟账号、自动代扣和入账补偿的总体互斥规则继续以 34-统一还款防重复治理 —— 暂hold 为准。

2. 目标

  1. 为 Paystack、Monnify Web H5 单次支付会话建立明确时效,产品口径暂定 30 分钟。
  2. 会话到期后先核验原交易,不以页面倒计时替代通道终态,不允许直接创建第二笔交易。
  3. 自动代扣处理中时,在页面进入和主动还款提交两个节点拦截用户,不生成主动还款意图。
  4. 对临近过期、到期、查单、重新获取会话和代扣拦截建立可衡量、无敏感信息的前端埋点。

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 通用规则

  1. 只有用户确认付款且提交前校验通过后,才初始化通道支付会话。
  2. 通道初始化成功时由服务端记录 startedAt 和 expiresAt;暂定 expiresAt = startedAt + 30 分钟。
  3. 页面倒计时以服务端时间为准。刷新、退出重进、更换设备、返回上一步均不得重置原会话。
  4. 相同还款意图重复提交时,服务端返回原有效会话或原处理状态,不创建新参考号。
  5. 剩余 5 分钟时,同一会话只展示一次即将过期提示,不自动刷新或生成订单。
  6. 到达 30 分钟时,H5 关闭继续付款和再次提交入口,展示“支付会话已过期,正在确认原支付结果”,并查询原交易。
  7. 页面到期不是通道失败。只有通道已给出可释放终态、内部无成功或未知资金事实且统一敞口已释放,才展示“重新获取支付会话”。
  8. 回跳只用于进入结果查询,不作为成功或失败依据。

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 触发节点

  1. 页面进入:H5 首次加载时由服务端返回是否存在自动代扣处理中或成功待入账。
  2. 提交前:用户点击任一主动还款提交按钮时,服务端再次原子校验,防止页面加载后状态发生变化。
  3. 状态刷新:用户点击查看状态时只查询原代扣,不创建主动还款意图。

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 progressCheck 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_viewview支付会话倒计时,wp_h5同一会话首次进入剩余 5 分钟时payment_method 取服务端会话;expiry_bucket=within_5_minutes临近过期会话数、临近过期完成率
repayment_session_expired_viewview会话过期状态,wp_h5服务端 30 分钟到期状态首次展示时payment_method、provider_state 稳定枚举、can_restart 取服务端会话到期率、到期后未知率
repayment_session_restart_clickclick会话过期状态,wp_h5用户有效点击重新获取支付会话时payment_method、source_status 取服务端安全重建意愿
repayment_session_restart_resultresult会话过期状态,wp_h5服务端返回复用、创建或拦截结果时result_status、blocked_reason 稳定分类、payment_method安全重建成功率、未知拦截率
repayment_duplicate_block_viewpopup_view自动代扣在途弹窗,wp_h5页面首查或提交前校验首次实际展示弹窗时active_stage、protection_level、block_scope、block_source=auto_debit_processing 取服务端;未选择方式时不传 payment_method自动代扣处理中拦截量
repayment_duplicate_block_clickpopup_click自动代扣在途弹窗,wp_h5用户点击 Check Status 或 Close 时action_type=check_status/close、active_stage 取服务端拦截后查单率、关闭率
repayment_soft_intent_replace_viewpopup_view虚拟账号软意图替换弹窗,wp_h5有效软意图下选择其他主动方式并实际展示弹窗时protection_level=soft_intent 取服务端;target_method 取 UI软意图后切换方式需求量
repayment_soft_intent_replace_clickpopup_click虚拟账号软意图替换弹窗,wp_h5点击确认替换或保留虚拟账号时action_type=confirm_replace/keep_virtual_account、target_method 取 UI替换确认率
repayment_soft_intent_replace_resultresult虚拟账号软意图替换弹窗,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. 上线与降级

  1. 影子阶段:只记录服务端支付会话、30 分钟到期判断和代扣在途命中,不改变用户操作;核对误拦截和漏拦截。
  2. 灰度阶段:先启用提交前服务端硬拦截和原会话复用,再灰度页面倒计时、弹窗和重新获取入口。
  3. Paystack 配置:先查询并留存生产 Integration 原值;确认专用或完成共享影响评估后,才允许设置 1800 秒。
  4. Monnify:切换到 Invoice Create,按服务端时间生成 30 分钟 expiryDate,补齐原 Invoice 查询、回调接收和敞口保护。
  5. 降级:页面状态服务不可用时,从严禁止创建新主动支付;原交易查询、回调、FCS 补偿和入账继续运行。
  6. 回滚页面能力时,不删除已有支付会话和敞口记录,不把未知订单批量置为失败。

15. 验收标准

  1. 用户确认付款前不创建通道 Checkout;通道初始化成功后形成唯一会话并返回服务端 30 分钟到期时间。
  2. 刷新、退出重进、更换设备不会延长原会话,也不会生成新参考号。
  3. 剩余 5 分钟提示同一会话只展示和上报一次。
  4. 30 分钟到期后页面不能继续付款,系统先查询原交易;处理中、未知、成功或成功待入账均不能创建新会话。
  5. Paystack 专用还款 Integration 的支付会话超时为 1800 秒;共享 Integration 未经影响评估不得直接修改。
  6. Monnify Invoice 的 expiryDate 等于服务端创建时间加 30 分钟,H5 使用返回的 checkoutUrl,页面倒计时与 Invoice 截止时间一致。
  7. Monnify expiryDate 按 Africa/Lagos 服务端时间生成,并在 Sandbox、生产商户分别验证实际失效时间;误差超出双方确认容差时阻断上线。
  8. Monnify Invoice Create 缺少 checkoutUrl、invoiceReference 或 expiryDate,或者返回失效时间不符合 30 分钟规则时,不跳转页面、不直接重复创建,并能查询原 Invoice。
  9. 原交易明确未支付且敞口释放后,用户才能看到并使用“重新获取支付会话”。
  10. 自动代扣处理中时,页面进入即禁用主动还款并展示弹窗;关闭弹窗后方式仍不可提交。
  11. 页面加载后才进入代扣处理的场景,在主动还款提交前被服务端二次校验拦截。
  12. 被自动代扣在途拦截的请求不创建主动还款意图、FCS/PMT 订单或通道参考号。
  13. Check Status 只有在原代扣明确失败且敞口释放后才重新开放方式;其他非终态继续拦截。
  14. 两台设备并发提交时最多创建一个支付会话,另一请求返回原会话或拦截状态。
  15. 通道与内部资金事实冲突时自动挂起并群播报,不允许用户再次还款。
  16. 虚拟账号软意图期间改用其他方式时展示替换确认;服务端原子替换成功后才继续支付,失败时保留原软意图。
  17. 替换软意图后发生迟到入账仍正常接收;与新支付重叠时进入贷款级阻断和人工处置。
  18. 第 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 H5POST https://api.monnify.com/api/v1/invoice/create使用唯一 invoiceReference;必填 expiryDate=服务端创建时间+30分钟;从响应 checkoutUrl 获取 H5 页面
查询 InvoiceGET 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 编码

17. 官方参考