绑卡失败率优化与异常结果页 — 迭代需求
目标:在绑卡验证金额已调整到 Monnify 可受理金额的基础上,补齐失败原因映射、异常结果页英文文案、reference 复制能力,以及“客户余额不足时自动打款后引导重新绑卡”的业务方案。
1. 背景
生产绑卡失败目前重点关注两类问题:
- 绑卡失败提示不清晰。当前结果页只展示
Failed和通用重试文案,客户无法判断应该充值、换卡、重新输入 OTP、稍后查询,还是联系客服。 - 绑卡验证金额已调整为 Monnify 可受理金额后,客户真实卡余额不足会成为新的失败来源。为减少小额余额不足导致的绑卡流失,需要支持按业务线配置自动打款,再提醒客户重新发起绑卡。
说明:绑卡验证金额调整已完成,本需求不再定义相关配置;本需求也不实现指标、监控告警和埋点。
2. 本期范围
| 范围 | 是否纳入 | 说明 |
|---|---|---|
| 失败原因映射 | 是 | 将 provider / 系统错误转换为稳定 failReason |
| 异常结果页英文文案 | 是 | 使用统一结果页样式和一张文案表覆盖各异常场景 |
| reference 页面复制 | 是 | 页面展示 Reference: {reference},支持复制 |
| 余额不足自动打款 | 是 | 仅按业务线配置开启,支持 Paystack 或 Monnify 出款 |
| 自动打款后短信提醒 | 是 | 打款成功后提醒客户重新发起绑卡 |
| 指标、监控告警、埋点 | 否 | 本需求不实现 |
| 验证金额退款/补偿 | 否 | 不承诺绑卡验证金退款或补偿 |
3. 产品原则
AMOUNT_BELOW_MINIMUM不等于客户余额不足。该错误只作为历史和兼容映射保留,不作为本期金额配置需求。- 只有 provider 明确返回余额不足、insufficient funds 等信息时,才展示余额不足文案或触发自动打款判断。
- 客户看到的提示必须包含下一步动作:重试、换卡、重新发起、稍后查询或联系客服。
- 绑卡成功只代表还款授权或 card token 建立成功,不代表还款成功。
- 自动打款只用于帮助客户完成绑卡验证,不等同于验证金额退款/补偿。
- 前端不得展示完整银行卡号、CVV、PIN、OTP、token、authData 等敏感信息。
4. 失败原因映射建议
后端应保存 provider 原始错误码和 message,但 App 只使用标准 failReason 决定页面标题、文案和按钮。以下表格基于 Monnify 官方 Error Codes 页面可检索到的错误条目整理,主要覆盖绑卡、卡 token、自动打款、账户校验和常见配置错误。错误码全集仍需从当前生产库继续拉取更完整 provider code / message 后补齐。
官方来源:Monnify API Error Messages。该页面按 Authentication、Transaction & Payment、Recurring Payment、Direct Debit、Disbursements / Transfer、Account Validation 等分类维护错误信息。
4.1 Monnify 官方错误映射表
| Monnify 官方错误 / message | 官方含义摘要 | 我方 failReason | Title | Message | 按钮建议 | 处理说明 |
|---|---|---|---|---|---|---|
Invalid Card Number | 卡号字段错误 | INVALID_CARD_INFO | Invalid Card Details | Some card details are incorrect. Please check the card number, expiry date, CVV, and PIN, then try again. | Retry / Use Another Card | 返回卡信息输入页;不记录完整卡号 |
Auth Data error | 常见于有效期年份或其他卡字段错误 | INVALID_CARD_INFO | Invalid Card Details | Some card details are incorrect. Please check the card number, expiry date, CVV, and PIN, then try again. | Retry / Use Another Card | 优先提示检查有效期和卡信息 |
Merchant has not been configured for bin | 卡 PAN 前六位不在商户支持 BIN 内 | CARD_NOT_SUPPORTED | Card Not Supported | This card cannot be used for automatic repayment. Please bind another card. | Use Another Card | 客户侧换卡;支付侧确认商户 BIN 配置 |
OTP authorize 接口返回 responseCode=99 / Transaction Failed | 发卡行限制该卡完成 OTP 授权或线上验证 | CARD_NOT_SUPPORTED | Card Not Supported | Your bank does not allow this card for verification. Please use another debit card. | Use Another Card | 不引导反复输 OTP;不触发自动打款;优先换卡 |
No Card Payment found for this transaction. | 当前交易没有先发起 card payment,却调用 OTP / 3DS 授权 | PROVIDER_FLOW_ERROR | Failed | Card verification could not be completed. Please restart card binding. | Restart Binding | 后端流程错误或交易状态不一致;重新创建绑卡交易 |
Card token has expired. | card token 已过期 | CARD_TOKEN_EXPIRED | Card Expired | This saved card can no longer be used. Please bind your card again. | Retry Binding / Use Another Card | 多用于已绑卡后续扣款;需重新绑卡 |
Invalid card token | token 不存在或无效 | CARD_TOKEN_INVALID | Card Not Available | This saved card is no longer available. Please bind your card again. | Retry Binding / Use Another Card | 不继续使用旧 token 发起扣款 |
Invalid amount | 交易金额无效 | PROVIDER_AMOUNT_INVALID | Failed | Card verification could not be started. Please try again later. | Try Again Later / Contact Support | 系统参数或通道规则问题,不提示客户充值 |
Amount must be greater than 20. | invoice 金额必须至少 20 Naira | PROVIDER_AMOUNT_INVALID | Failed | Card verification could not be started. Please try again later. | Try Again Later / Contact Support | 官方 invoice 类错误;绑卡场景仅作金额规则参考 |
Could not find specified contract / Unknown Contract Code provided. / Invalid contract code supplied. | contractCode 无效或不属于商户 | PROVIDER_CONFIG_ERROR | Failed | Card binding is temporarily unavailable. Please try again later. | Try Again Later / Contact Support | 商户配置问题;客户不可自助解决 |
Duplicate payment reference / Supplied reference already exists! | payment reference 已被使用 | DUPLICATE_REFERENCE | Processing | Card verification is already in progress. Please check the result before trying again. | Check Status / Back | 优先查询原交易,不立即重复创建 |
Invalid transaction reference supplied | transaction reference 无效或无法识别 | PROVIDER_REFERENCE_INVALID | Failed | Card verification could not be found. Please restart card binding. | Restart Binding / Contact Support | 查询不到原交易时,按业务状态决定是否重建 |
could not find specified bank | 银行 code 无法识别或不支持 | INVALID_BANK_CODE | Bank Not Supported | The selected bank is not available. Please choose another bank or use another card. | Use Another Card / Contact Support | 多用于账户校验 / 出款,不展示内部 bankCode |
Unable to validate account information | 账户号和银行 code 校验失败 | INVALID_ACCOUNT_INFO | Invalid Account Details | We could not verify your account details. Please check the account number and bank, then try again. | Retry / Contact Support | 自动打款前账户校验失败时使用 |
Account Validation 99 | 账户信息无效 | INVALID_ACCOUNT_INFO | Invalid Account Details | We could not verify your account details. Please check the account number and bank, then try again. | Retry / Contact Support | 官方账户校验失败码 |
Could not find disbursement transaction for given reference | 出款 reference 错误或查不到交易 | PAYOUT_REFERENCE_NOT_FOUND | Processing | Funding status could not be confirmed yet. Please check again later. | Check Status / Contact Support | 先补偿查询和人工排查,不重复打款 |
Access token expired | Monnify access token 超过 1 小时时效 | PROVIDER_AUTH_ERROR | Failed | Card binding is temporarily unavailable. Please try again later. | Try Again Later / Contact Support | 后端应自动刷新 token 后重试一次 |
Cannot convert access token to json | access token 格式异常 | PROVIDER_AUTH_ERROR | Failed | Card binding is temporarily unavailable. Please try again later. | Try Again Later / Contact Support | 认证链路异常,客户不可自助解决 |
4.2 生产补充映射
以下错误不一定出现在官方 Error Codes 页面当前可检索结果中,但已在生产排查或 provider message 中出现,应继续保留映射,并用生产库错误码全集校准。
| provider / 系统信号 | 我方 failReason | 客户文案场景 | 处理说明 |
|---|---|---|---|
AMOUNT_BELOW_MINIMUM | VERIFY_AMOUNT_TOO_LOW | 系统暂不可用 | 兼容历史错误;不提示客户充值 |
INSUFFICIENT_FUNDS / INSUFFICIENT_BALANCE / message 包含 insufficient funds | INSUFFICIENT_BALANCE | 卡余额不足 | 可进入自动打款判断 |
| OTP invalid / OTP expired | OTP_FAILED | OTP 错误或过期 | 重新发起绑卡并输入最新 OTP |
| Too Many Attempts | OTP_TOO_MANY_ATTEMPTS | OTP 尝试次数过多 | 引导稍后重试或换卡 |
Monnify OTP authorize 返回 responseCode=99 / Transaction Failed,且 Monnify 确认为 issuer restriction / issuing bank restriction | CARD_NOT_SUPPORTED | 发卡行限制该卡 | 引导客户使用其他 debit card;不再提示继续输 OTP |
| 3DS cancelled / bank authorization failed | BANK_AUTH_FAILED | 银行验证失败 | 引导重试并完成银行验证 |
| bank declined / issuer declined | BANK_DECLINED | 银行拒绝 | 引导联系银行或换卡 |
| bank registered mobile required | BANK_PHONE_REQUIRED | 需要银行预留手机号验证 | 仅在 provider 明确返回时使用 |
| timeout / query unknown / pending timeout | PROVIDER_TIMEOUT | 第三方处理中或超时 | 优先查询状态,不立即重复绑卡 |
| App 网络失败 | NETWORK_ERROR | 网络异常 | 重试当前请求 |
| 未分类 5xx / 异常 | SYSTEM_ERROR | 系统异常 | 稍后重试或联系客服 |
5. 后端返回模型
后端返回给 App 的失败结果应包含:
| 字段 | 类型 | 是否返回 App | 说明 |
|---|---|---|---|
bindStatus | string | 是 | FAILED / TIMEOUT / PROCESSING |
failReason | string | 是 | 我方标准失败原因枚举 |
message | string | 是 | 可选;如为空,App 使用本地文案表 |
primaryAction | string | 是 | 主按钮动作 |
secondaryAction | string | 是 | 次按钮动作,可为空 |
retryable | boolean | 是 | 是否允许立即重试 |
reference | string | 是 | 可展示和复制的绑卡 reference 或订单号 |
autoFundingSuccess | boolean | 条件返回 | 自动打款是否成功;成功时为 true—— 绑卡信息怎么回显,@技术确定方案 |
providerCode | string | 否 | 仅后端、客服后台、日志使用 |
providerMessage | string | 否 | 仅后端、客服后台、日志使用 |
规则:
title不由后端返回,App 根据failReason使用本地标题文案。providerCode和providerMessage不直接透传到客户前端页面。reference在页面内展示为Reference: {reference},右侧提供Copy操作。- 复制成功后提示
Copied;复制失败时提示Copy failed. Please try again. - Monnify OTP authorize 场景中,
responseCode=99/Transaction Failed本身是通用失败信号,不得无条件映射为发卡行限制;只有同时满足接口为/api/v1/merchant/cards/otp/authorize、对应 transaction reference、Monnify 人工或规则反馈确认为 issuer restriction / issuing bank restriction 时,才映射为CARD_NOT_SUPPORTED。 - 发卡行限制类
CARD_NOT_SUPPORTED不触发自动打款,不引导继续输入 OTP,不优先展示Retry;前端主操作为Use Another Card。
6. UI 基准样式
参考现有失败结果页,所有异常结果页保持同一结构:
页面展示顺序按用户交互路径排列:用户发起绑卡时先判断是否存在同支付通道 active 订单;如果仍在冷却期,进入倒计时页;倒计时结束后进入可查询的处理中页;再根据查询结果进入自动打款处理中、打款成功后的回显绑卡页、可换卡的发卡行限制 / 银行拒绝页,或系统异常页。设计图中的倒计时态以 15s 作为起始示例,即按钮显示 Check Status (00:15)。
6.1 页面主交互流程图
主流程只描述用户从发起绑卡到结果页的路径;active 订单内部判断见 6.2。
flowchart TD A[用户点击绑卡 / Retry / Use Another Card] --> B[前端提交绑卡发起请求] B --> C[后端执行 active 订单检查] C -- 返回可继续创建 --> D[创建新绑卡订单并生成新 reference] C -- 返回原订单处理中 --> E[展示 Processing 或倒计时页] D --> F[进入 Monnify / Paystack 绑卡验证] F --> G{绑卡验证结果} G -- 成功 --> H[绑卡成功,回到业务成功流程] G -- 处理中或超时未确认 --> E E --> I[用户点击 Check Status] I --> J[查询原 reference 状态] J --> G G -- 余额不足且自动打款已受理 --> K[展示 Funding Card 页面] K --> L[用户点击 Check Status] L --> M{自动打款状态} M -- 处理中 --> K M -- 成功 --> N[发送短信并自动跳转至绑卡页] N --> T[回显刚才的卡信息;CVV 显示为 ***] T --> U[用户确认或修改卡信息] U --> V[提交新的绑卡申请并生成新 reference] M -- 失败 --> O[展示 Insufficient Balance 页面] G -- 发卡行限制或卡不支持 --> P[展示 Card Not Supported 页面] G -- 银行拒绝 --> Q[展示 Bank Declined 页面] G -- 系统临时异常 --> R[展示 Failed / Try Again Later 页面] P --> S[用户点击 Use Another Card] Q --> S O --> S S --> B
6.2 Active 订单处理流程图
active 订单检查是创建前主拦截,结果页初始化和 Check Status 只做状态刷新,不创建新绑卡订单。
flowchart TD A[触发点] --> B{触发来源} B -- 发起绑卡 / Retry / Use Another Card --> C[创建前 active 订单检查] B -- 进入结果页 --> D[按当前 reference 刷新状态] B -- 点击 Check Status --> E[查询原 reference 状态] C --> F{是否存在同支付通道 active 订单?} F -- 否 --> G[允许创建新绑卡订单] F -- 是 --> H[查询原 active 订单状态] D --> H E --> H H --> I{原订单状态} I -- 已成功 --> J[更新原订单成功并返回成功状态] I -- 已失败 / 已取消 --> K[按失败原因返回标准 failReason] I -- 仍处理中且在冷却期 --> L[返回原 reference 和 countdownSeconds] I -- 仍处理中但无冷却期 --> M[返回 Processing 和 Check Status] I -- 超过 TTL 且无明确终态 --> N[标记超时或待补偿查询] L --> O[前端展示倒计时页] O --> P{倒计时结束?} P -- 否 --> O P -- 是 --> M M --> Q[前端展示 Processing 页面] N --> R{是否允许超时后重新路由?} R -- 否 --> Q R -- 是 --> S[创建新订单、新 reference、新 provider request]
| 层级 | 区域 | 样式要求 |
|---|---|---|
| 1 | 页面标题 | 顶部居中:Binding Bank Card;左上角保留返回箭头 |
| 2 | 状态图标 | 失败用红色叉号;处理中 / 自动打款中用黄色或橙色时钟 / loading 图标 |
| 3 | 主标题 | 居中,深色加粗;由 App 根据 failReason 本地映射,只表达当前状态 |
| 4 | 关键信息条 | 位于主标题下方,只允许一行加粗短句,例如 Need at least ₦20、Do not retry yet、Try another card;不再做双行 chip,避免信息抢层级 |
| 5 | 说明文案 | 居中灰色,最多 2-3 行;解释原因和下一步,不重复关键信息条 |
| 6 | 操作区 | 主按钮使用绿色圆角按钮;次按钮使用文本按钮,仅在需要联系客服、换卡、返回或稍后查询时展示 |
| 7 | 倒计时 | 仅在处理中 / active 订单冷却期内展示;主按钮置灰并显示 Check Status (mm:ss),倒计时结束后恢复绿色可点击状态 |
| 8 | Reference | 置于底部辅助区,小号灰字,格式 Reference: {reference},右侧支持 Copy |
7. 异常场景 UI 文案
failReason / 状态 | Icon | Title | Key info | Message | Primary button / action | Secondary button / action |
|---|---|---|---|---|---|---|
BIND_FAILED | 红色叉号 | Failed | Check your card details | We could not bind your card. Please check your card information and try again. | Retry / 重新进入绑卡流程 | 无 |
VERIFY_AMOUNT_TOO_LOW | 红色叉号 | Failed | Please try again later | Card verification could not be started. | Try Again Later / 返回上一页 | Contact Support / 打开客服入口 |
INSUFFICIENT_BALANCE | 红色叉号 | Insufficient Balance | Need at least ₦20 | Your card balance is not enough for card verification. Please make sure your card has at least ₦20 and try binding again. | Retry Binding / 重新发起绑卡 | Use Another Card / 进入新卡绑定 |
AUTO_FUNDING_PROCESSING | 黄色/橙色处理中图标 | Funding Card | Need at least ₦20 | Your balance is low for card verification. We are sending money. You will receive an SMS when ready. | Check Status / 查询打款和绑卡状态 | Use Another Card / 进入新卡绑定 |
AUTO_FUNDING_SUCCESS | 绿色成功图标 | Account Funded | Continue binding your card | Your account has been funded for card verification. We are taking you back to card binding. | 自动跳转绑卡页 | 无 |
AUTO_FUNDING_FAILED | 红色叉号 | Insufficient Balance | Need at least ₦20 | We could not add funds to your account. Please make sure your card has at least ₦20 and try binding again. | Retry Binding / 重新发起绑卡 | Use Another Card / 进入新卡绑定 |
OTP_FAILED | 红色叉号 | OTP Failed | Enter the latest OTP | The OTP is incorrect or expired. Please restart card binding and enter the latest OTP from your bank. | Restart Binding / 重新创建绑卡交易 | Use Another Card / 进入新卡绑定 |
OTP_TOO_MANY_ATTEMPTS | 红色叉号 | Too Many Attempts | Please wait a few minutes | You have entered the OTP too many times. Please restart card binding after a few minutes. | Try Again Later / 返回上一页 | Use Another Card / 进入新卡绑定 |
BANK_AUTH_FAILED | 红色叉号 | Bank Verification Failed | Follow your bank instructions | Your bank verification was not completed. Please try again and follow your bank's instructions. | Retry / 重新进入绑卡流程 | Use Another Card / 进入新卡绑定 |
BANK_DECLINED | 红色叉号 | Bank Declined | Try another card | Your bank declined this card verification. Contact your bank or use another debit card. | Use Another Card / 进入新卡绑定 | Contact Support / 打开客服入口 |
CARD_NOT_SUPPORTED | 红色叉号 | Card Not Supported | Try another card | Your bank does not allow this card for verification. Please use another debit card. | Use Another Card / 进入新卡绑定 | 无 |
INVALID_CARD_INFO | 红色叉号 | Invalid Card Details | Check card number and CVV | Some card details are incorrect. Please check the card number, expiry date, CVV, and PIN, then try again. | Retry / 返回卡信息输入页 | Use Another Card / 进入新卡绑定 |
BANK_PHONE_REQUIRED | 红色叉号 | Phone Verification Needed | Use your bank phone number | Your bank needs phone verification for this card. Please use the phone number registered with your bank or try another card. | Retry / 重新进入绑卡流程 | Use Another Card / 进入新卡绑定 |
BIND_CARD_PROCESSING | 黄色/橙色处理中图标 | Processing | Do not retry yet | Card verification is still processing. Please check the result before trying again. | Check Status / 查询原绑卡结果 | Back / 返回上一页 |
PROVIDER_TIMEOUT | 黄色/橙色处理中图标 | Processing | Do not retry yet | Card verification is taking longer than expected. Please check the result before trying again. | Check Status / 查询绑卡结果 | Back / 返回上一页 |
NETWORK_ERROR | 红色叉号 | Connection Failed | Check your internet | Please check your internet connection and try again. | Retry / 重试当前请求 | 无 |
SYSTEM_ERROR | 红色叉号 | Failed | Please try again later | Card binding is temporarily unavailable. | Try Again Later / 返回上一页 | Contact Support / 打开客服入口 |
INSUFFICIENT_BALANCE 前端展示规则:
- 如果后端仅返回
failReason=INSUFFICIENT_BALANCE,且未返回自动打款订单或自动打款状态,前端展示普通余额不足结果页:Title 为Insufficient Balance,Key info 为Need at least ₦20,主按钮为Retry Binding,次按钮为Use Another Card。 - 如果后端已创建自动打款订单并返回处理中状态,前端展示
AUTO_FUNDING_PROCESSING对应页面:Title 为Funding Card,主按钮为Check Status,页面按轮询间隔自动查询,不引导客户立即重复绑卡。 - 如果自动打款成功,前端可短暂展示成功状态后自动跳转绑卡页;如果自动打款失败、开关关闭、频控不通过、每日总量不通过、通道不可用或 provider 拒绝出款,均回落展示普通
INSUFFICIENT_BALANCE页面。 - 自动跳转的绑卡页只回显本次申请已提交的非敏感卡信息;CVV 固定显示为
***,不得回显原始 CVV。用户提交时必须创建新的绑卡订单和新的reference,不得复用上一笔绑卡申请。
8. 余额不足自动打款需求
8.1 业务目标
当客户绑卡时因真实余额不足导致验证失败,系统可按业务线配置自动向客户账户打款,打款成功后通过短信提醒客户重新发起绑卡。该能力用于降低小额余额不足造成的绑卡流失。
8.2 触发条件
同时满足以下条件才允许自动打款:
- 绑卡失败原因为明确的
INSUFFICIENT_BALANCE。 - 当前业务线自动打款开关为开启。
- 同一
bizCode + customerId没有成功或处理中的自动打款记录。 - 未超过客户、
bizCode + customerId、每日总打款单量等配置上限。 - 自动打款通道为可用状态,支持 Paystack 或 Monnify。
不满足任一条件时,按 INSUFFICIENT_BALANCE 展示普通余额不足结果页。
8.3 配置项
| 配置项 | 示例值 | 功能和作用 |
|---|---|---|
bindCard.autoFunding.bizLine.{bizLine}.enabled | Y/N | 业务线级开关。只有对应业务线为 Y 时才允许自动打款;不设置或为 N 时关闭。 |
bindCard.autoFunding.bizLine.{bizLine}.amount | 30 | 自动打款金额,单位 NGN。由运营根据目标净到账金额和通道手续费提前测算并配置;支付中台按该配置值直接发起打款。 |
bindCard.autoFunding.bizLine.{bizLine}.channel | Paystack / Monnify | 自动打款出款通道。可按业务线配置 Paystack 或 Monnify,后端按所选通道创建出款订单和查询终态。 |
bindCard.autoFunding.bizLine.{bizLine}.maxTimesPerCustPerDay | 1 | 单客户每日自动打款上限,防止同一客户重复获取小额打款。 |
bindCard.autoFunding.bizLine.{bizLine}.maxTimesPerBizCodeCustomer | 1 | 同一 bizCode + customerId 的自动打款上限,防止同一业务线客户重复触发。 |
bindCard.autoFunding.bizLine.{bizLine}.dailyTotalLimit | 1000 / -1 | 业务线每日自动打款总单量上限,用于按量灰度;不配置或配置为 -1 表示不限制。 |
bindCard.autoFunding.bizLine.{bizLine}.cooldownMinutes | 30 | 自动打款失败、处理中或客户重新绑卡失败后的冷却时间,避免短时间重复触发。 |
bindCard.autoFunding.bizLine.{bizLine}.smsTemplateCode | BIND_CARD_AUTO_FUND_SUCCESS | 自动打款成功后的短信模板编码,用于提醒客户重新发起绑卡。 |
手续费规则:
- 手续费默认由我司承担,不需要单独配置手续费承担方式。
amount表示用户目标净到账金额;运营需提前根据所选通道手续费测算并配置金额。- 支付中台不再额外计算或叠加手续费,直接按运营配置的金额发起打款。
- 如果 provider 因手续费、账户余额或其他出款原因拒绝打款,按自动打款失败处理,展示余额不足结果页并引导重新绑卡。
8.4 状态流转
服务侧状态流转中文说明:
| 阶段 | 服务侧判断 / 动作 | 前端结果 |
|---|---|---|
| 1. 绑卡失败识别 | 绑卡服务将 provider 返回的余额不足类错误映射为 INSUFFICIENT_BALANCE | 暂不直接决定页面,先进入自动打款判断 |
| 2. 业务线开关判断 | 检查当前业务线是否开启自动打款;未开启则不创建打款订单 | 展示普通余额不足结果页 |
| 3. 资格和频控判断 | 按 bizCode + customerId 校验处理中唯一、次数限制、冷却时间和每日总打款单量 | 不通过时展示普通余额不足结果页 |
| 4. 通道可用性判断 | 按业务线配置选择 Paystack 或 Monnify,并确认通道可发起出款 | 不通过时展示普通余额不足结果页 |
| 5. 创建自动打款订单 | 创建独立自动打款订单,绑定 bizCode、客户、绑卡 reference,保证幂等 | 创建成功后按配置金额发起出款 |
| 6. 调用出款通道 | 调用 Paystack 或 Monnify 出款接口,记录通道 request / response 和出款 reference | accepted / pending 时展示 Funding Card |
| 7. 出款结果确认 | 通过回调或主动查询确认出款终态;Funding Card 页面同时按轮询间隔自动查询,Check Status 支持用户立即查询 | 成功发送短信并自动跳转绑卡页;失败回落普通余额不足结果页 |
flowchart TD A[绑卡失败:余额不足] --> B{业务线自动打款开关是否开启?} B -- 否 --> C[展示余额不足结果页] B -- 是 --> D{资格、频控和每日总量是否通过?} D -- 否 --> C D -- 是 --> E{出款通道是否可用?} E -- 否 --> C E -- 是 --> F[创建自动打款订单] F --> G[调用 Paystack 或 Monnify 出款] G --> H{出款状态} H -- 处理中 --> I[展示 Funding Card;进入页面立即查询并轮询] H -- 成功 --> J[发送短信并自动跳转绑卡页] H -- 失败 --> K[展示余额不足结果页] I --> L[用户 Check Status 或页面自动查询] L -- 处理中 --> I L -- 成功 --> J L -- 失败 --> K J --> M[回显脱敏卡信息;CVV 显示 ***] M --> N[用户确认或修改并提交] N --> O[创建新的绑卡订单和 reference]
8.5 后端实现要求
- 自动打款订单必须独立建模,不能复用绑卡订单号作为出款订单主键。
- 自动打款需与
bizCode、客户、绑卡 reference 绑定,支持幂等查询和客服追踪。 - 同一
bizCode + customerId只允许一笔处理中自动打款。 - 出款 reference 使用自动打款订单号,避免和绑卡 reference 混用。
- 出款请求 accepted / pending 不代表到账成功,必须通过回调或主动查询确认。
- 自动打款通道支持 Paystack 或 Monnify,具体通道由业务线配置决定;支付中台按运营配置金额直接发起出款,不额外计算手续费。
- 自动打款成功后发送短信,提醒用户可以重新发起绑卡;前端收到成功终态后自动跳转绑卡页。
- 自动打款失败不得无限重试;超过配置次数或冷却期未结束时,展示普通余额不足文案。
- 所有日志和客服后台展示必须脱敏,不展示完整银行卡号。
- 自动打款成功后,将原绑卡申请标记为
ENDED_BY_AUTO_FUNDING,不再等待或接受该申请的后续绑卡动作作为当前申请结果;原绑卡reference仅用于追踪。 - 回显绑卡页提交时必须创建新的绑卡订单、新的
reference和新的 provider request;不得复用原绑卡 reference、原 provider transaction 或原 OTP 会话。
8.5.1 打款结果页交互
| 场景 | 页面行为 | 用户操作 | 服务调用与状态处理 |
|---|---|---|---|
| 自动打款处理中 | 展示 Funding Card,说明正在补充绑卡验证所需余额 | 用户可点击 Check Status | 进入页面立即查询一次;之后按客户端统一轮询间隔查询,查询只针对当前自动打款上下文,不创建绑卡订单 |
用户点击 Check Status | 按钮进入 loading,避免重复点击 | 等待查询结果 | 与自动轮询使用同一个查询接口;请求进行中暂停下一次轮询,完成后恢复 |
| 自动打款仍处理中 | 保持 Funding Card 页面 | 可继续等待或返回 | 返回前端所需的自动打款结果;不得显示绑卡 Retry |
| 自动打款成功 | 可短暂展示 Account Funded 成功反馈 | 无需再次点击 | 发送一次成功短信;结束旧绑卡申请,随后自动跳转回显绑卡页 |
| 自动打款失败 / 查询明确失败 | 展示普通 Insufficient Balance | Retry Binding 或 Use Another Card | 明确提示余额不足;不自动再次打款;重新绑卡时重新执行 active 订单检查和自动打款资格判断 |
| 查询超时 / 网络失败 | 保持当前处理中页,提示暂时无法确认 | Check Status 可再次查询 | 不将未知状态当作失败,不重复创建打款订单;按原自动打款 reference 查询 |
轮询要求:
- 轮询从进入
Funding Card页面开始,首次进入立即查询一次,后续按客户端统一轮询间隔执行。 - 轮询与
Check Status必须幂等;同一时刻只能存在一个查询请求,用户点击查询时取消或跳过同一时刻的自动查询。 - 页面退后台、离开页面或自动打款进入终态后停止轮询;重新进入页面时按服务端状态重新开始,不以前端本地状态判断已成功。
- 轮询只查询自动打款订单,不能触发新的绑卡申请、不能触发新的自动打款订单。
- 轮询间隔和最大轮询时长由客户端统一配置执行;本需求不新增监控、告警和埋点。
8.5.2 打款成功后的绑卡页
自动打款成功后,页面直接跳转至绑卡信息页,避免客户再次手动输入。该页面是新的绑卡申请入口:
| 字段 / 操作 | 展示和交互规则 |
|---|---|
| 卡号 | 回显上一次申请的脱敏卡号,例如 **** **** **** 1234;允许修改;不得回显完整卡号 |
| 有效期 | 回显上一次申请的有效期;允许修改;提交前按当前输入校验 |
| CVV | 固定显示为 ***,不得回显、不得通过接口返回原始 CVV;用户如需修改必须重新输入新的 CVV |
| PIN / OTP | 不回显、不缓存、不带入新申请;在新的 provider 验证流程中重新输入 |
| 主按钮 | Continue / Bind Card,提交当前表单后创建新的绑卡申请 |
| 修改卡信息 | 任一卡字段被修改后,视作新的绑卡申请;旧申请立即结束,不能继续作为当前申请更新 |
| 未修改卡信息 | 仍创建新的绑卡订单和新的 reference;回显只是减少重复输入,不复用旧申请 |
| 返回 | 返回业务页;不创建新绑卡订单,不改变已成功的自动打款状态 |
旧申请结束规则:自动打款成功时旧绑卡申请进入 ENDED_BY_AUTO_FUNDING;用户从回显页提交后,旧申请保持结束状态,新申请使用新的订单号、reference、provider transaction 和验证会话。旧申请晚到的回调只能幂等更新旧申请,不得覆盖新申请状态。
8.5.3 模块边界
支付中台尽量完整承接绑卡和自动打款处理,避免 BNS 与支付中台形成两套状态和错误映射逻辑:
| 模块 | 负责内容 |
|---|---|
| BNS | 提供 bizCode、客户标识、手机号等业务上下文;调用或转发支付中台结果 |
| 支付中台 | 负责绑卡订单、active 订单、provider 调用、失败原因映射、自动打款资格和频控、出款、查询、回调、幂等及标准结果返回 |
| App / H5 | 根据标准结果展示页面;打款成功后进入回显绑卡页,打款失败后展示余额不足并引导重新绑卡 |
| 消息网关 | 根据支付中台的成功事件发送短信 |
BNS 不负责 provider 错误映射、active 订单判断、自动打款订单处理、手续费计算或绑卡状态机。现有 BNS 接口可继续作为业务入口,但实际绑卡编排统一由支付中台完成。
8.6 已有同支付通道进行中绑卡订单处理
当前代码中,H5 绑卡入口主要通过客户 + 业务线维度的分布式锁防止瞬时重复请求;API 绑卡路径存在同卡号 pending 检查,但缺少统一的“同客户 / 同卡 / 同业务线 / 同支付通道进行中订单”复用和状态刷新规则。本期需补齐该流程,避免客户重复发起绑卡、重复进入三方验证,或看到不清晰的 pending 错误。
8.6.1 Active 订单识别
active 订单不是只在进入结果页时判断。完整绑卡流程中应分三处处理:
| 时机 | 是否必须 | 处理目标 | 处理方式 |
|---|---|---|---|
| 发起绑卡前 | 是 | 防止重复创建同支付通道绑卡订单、重复进入三方验证、重复扣验证金额 | 后端在创建新的绑卡订单或生成 H5 绑卡链接前,先查询是否存在 active 绑卡订单 |
| 进入结果页时 | 是 | 刷新当前 reference 的最新状态,避免展示过期 pending 或通用失败页 | 前端携带 reference 进入结果页,后端按该 reference 查询订单;如发现同支付通道 active 订单,返回标准 bindStatus / failReason / countdownSeconds |
点击 Check Status 时 | 是 | 主动查询原订单或自动打款订单终态 | 只查询原 reference 或自动打款 reference,不创建新绑卡订单;查询后按终态展示成功、失败、Processing 或自动打款结果页 |
其中“发起绑卡前判断”是主拦截点,“进入结果页判断”是状态刷新和兜底,“Check Status”是用户主动查询。前端不得依赖结果页判断来阻止重复下单。
| 维度 | 规则 |
|---|---|
| 客户 | identityNumber 必须一致;如入口只有 uid,需先转换为当前业务线可识别的客户标识 |
| 业务线 | channel 必须一致 |
| 支付通道 | payChannel 必须一致;如果路由前还未确定支付通道,则先完成路由计算,再按计算后的 payChannel 查询 |
| 卡号 | 已输入卡号的 API 绑卡场景必须校验 bankCardNo;H5 入口未输入卡号时可不校验卡号 |
| 状态 | status 属于 I、pending、send_card、send_pin、send_otp、send_birthday、send_phone、send_address |
| 时间 | 仅查询配置有效期内订单,建议按支付通道配置 active TTL;超过 TTL 的订单先触发状态查询或超时关闭 |
建议新增配置:
| 配置项 | 类型 | 示例 | 说明 |
|---|---|---|---|
bindCard.activeOrder.ttlMinutes.{payChannel} | integer | 30 | 各支付通道 active 订单有效期 |
bindCard.activeOrder.queryBeforeCreate.enabled.{bizLine} | boolean | true | 当前业务线是否在创建前查询 active 订单 |
bindCard.activeOrder.allowReRouteAfterTimeout.{bizLine} | boolean | true | active 订单超时后,我方是否允许新建绑卡订单并重新路由到其他支付通道 |
说明:
- Monnify 或 Paystack 不支持把已创建的绑卡 / 交易订单直接“切换”到另一个支付通道。
- 这里的“切换支付通道”是我方系统行为:原 active 订单超时且完成状态查询 / 超时处理后,新建一笔新的绑卡订单,生成新的
reference,再按路由策略选择 Monnify、Paystack 或其他可用通道。 - 原订单必须保留原
reference、payChannel、provider 原始状态和最终处理结果,不能覆盖成新通道订单。 - 新订单必须使用新的
reference。Monnify 和 Paystack 都要求交易 reference 唯一,复用旧 reference 会触发 duplicate reference 类错误。
8.6.2 处理规则
| 查询结果 | 后端处理 | 前端展示 |
|---|---|---|
| 无 active 订单 | 正常创建新绑卡订单,按路由结果进入三方验证 | 正常进入绑卡流程 |
| active 订单已查到三方成功 | 更新原订单为 success,返回原 reference 和成功状态 | 展示成功页或回到业务成功流程 |
| active 订单已查到三方失败 / 取消 | 更新原订单为终态失败 / 取消;如仍允许绑卡,可创建新订单 | 展示失败原因页,允许 Retry 或 Use Another Card |
| active 订单仍处理中 | 不创建新订单,返回原 reference、payChannel、bindStatus=PROCESSING | 展示 Processing,Key info 为 Do not retry yet,主按钮为 Check Status |
| active 订单超过 TTL 且查不到明确终态 | 先做原订单状态查询;仍无明确终态时,将原订单标记为超时或待人工补偿查询;如配置允许,则新建绑卡订单并重新路由到其他支付通道 | 默认展示 Processing / Check Status;确认可重新路由时,进入新绑卡并使用新 reference |
服务端返回建议:
{
"bindStatus": "PROCESSING",
"failReason": "BIND_CARD_PROCESSING",
"reference": "BC202607210003",
"payChannel": "Monnify",
"message": "Card verification is still processing. Please check the result before trying again.",
"primaryAction": "CHECK_STATUS",
"secondaryAction": "BACK",
"retryable": false,
"countdownSeconds": 15
}实现要求:
- active 订单检查必须在三方请求前执行,避免同支付通道重复扣验证金额或重复创建三方会话。
- 对同一客户、同一业务线、同一支付通道,同一时间只允许一笔 active 绑卡订单。
- 发现 active 订单时,优先查询原订单状态,不直接创建新订单。
- 如果原订单仍在处理中,返回原
reference,前端必须支持复制该 reference。 - 如果原订单已失败且失败原因明确,按失败原因映射返回标准
failReason,而不是继续返回 pending。 - 不再向客户展示
this card have a pending order...please try again tomorrow这类不可操作文案。 - active 订单仍在冷却期内时,后端返回
countdownSeconds,取值为距离下一次允许查询或重试的剩余秒数。 - active 订单超时后的重新路由必须创建新订单、新
reference、新 provider request;不得复用原 provider 交易或原 reference。 - 重新路由前必须确认原订单不会继续被当作成功订单通知业务方;如后续收到原订单成功回调,需按原
reference做幂等处理并进入人工或补偿规则,不得覆盖新订单。 - 建议增加 DB 索引:
identity_number, channel, pay_channel, status, create_time。 - 建议增加 Redis 锁:
bindcard:active:{identityNumber}:{channel}:{payChannel},用于防止并发下重复创建 active 订单。
8.7 短信提醒
自动打款成功后,通过消息网关发送短信。短信只提醒客户重新发起绑卡,不承诺退款或补偿。
推荐英文短信文案:
Your account has been funded for card verification. Please open the app and retry binding your bank card.短信发送规则:
- 仅在自动打款终态成功后发送。
- 同一自动打款订单只发送一次成功短信。
- 短信发送失败不影响自动打款终态,但需要保留发送记录供客服查询。
- 短信不展示完整银行卡号、金额以外的敏感账户信息或 provider reference。
9. 前端行为规则
- App 根据
failReason使用本地 title 文案,不依赖后端返回 title。 - App 优先使用后端返回的
message;如为空,使用本地文案表。 reference必须展示并支持复制,始终展示的是绑卡的reference,不用展示出款的reference。BIND_CARD_PROCESSING、PROVIDER_TIMEOUT、AUTO_FUNDING_PROCESSING优先展示Check Status,不直接引导重复绑卡;Funding Card页面进入后立即查询一次,并按客户端统一轮询间隔自动轮询。- 如后端返回
countdownSeconds,前端展示倒计时状态:主按钮置灰,文案为Check Status (mm:ss);倒计时结束后按钮恢复绿色并允许点击查询。 - 倒计时期间不自动重复创建绑卡订单;用户点击返回后再次进入页面时,应按服务器剩余时间继续展示倒计时。
- 点击
Use Another Card时,清空本次卡输入状态,不复用上次敏感输入。 - 点击
Contact Support时,当前跳转至 contact us 页面。 - 返回上一页后,借款申请或还款业务状态不得丢失。
- 自动打款成功后,页面自动跳转至绑卡信息页并回显脱敏卡信息;CVV 始终显示为
***,不回显原始值。 - 回显页允许用户修改卡号、有效期和 CVV;用户提交时无论是否修改,都必须作为新的绑卡申请创建新订单和新
reference。
9.1 绑卡交互动作边界
| 用户动作 | 是否创建新绑卡订单 | 交互要求 |
|---|---|---|
| 首次点击绑卡 | 可能创建 | 后端先做 active 订单判断;无 active 订单时才创建新订单 |
点击 Retry / Retry Binding | 可能创建 | 仅适用于明确可重试的终态失败或自动打款成功;仍需先做 active 订单判断 |
| 自动打款成功自动跳转回显页 | 否 | 仅展示脱敏卡信息;不在跳转时创建绑卡订单,等待用户确认并提交新申请 |
| 在回显页修改并提交卡信息 | 是 | 视作新的绑卡申请;旧绑卡申请保持结束,后端生成新的订单、reference 和 provider request |
点击 Use Another Card | 可能创建 | 前端清空本次卡输入状态,用户重新输入新卡;后端仍需按新卡和支付通道判断 active 订单 |
点击 Check Status | 否 | 只查询原 reference、绑卡订单或自动打款订单状态,不创建新绑卡订单 |
| 倒计时结束 | 否 | 仅恢复 Check Status 可点击,不自动查询、不自动创建新订单 |
点击返回 Back | 否 | 返回上一页或业务页;再次进入结果页时按服务器剩余 countdownSeconds 展示 |
| 结果页初始化 | 否 | 只做当前 reference 的状态刷新和展示;不得把“进入结果页”作为创建新订单的触发点 |
10. 验收标准
10.1 后端验收
- 后端不再返回
title字段作为页面标题来源。 - 后端能返回稳定
failReason、可展示message、按钮动作和可复制reference。 providerCode、providerMessage只在日志、订单明细或客服后台使用,不直接展示给客户。- 明确余额不足返回映射为
INSUFFICIENT_BALANCE。 - Monnify OTP authorize
responseCode=99/Transaction Failed在确认属于发卡行限制时,映射为CARD_NOT_SUPPORTED;未确认发卡行限制时,不得默认使用该原因。 - 自动打款只按业务线开关控制。
- 自动打款开启时,只有明确
INSUFFICIENT_BALANCE且通过资格、频控、每日总量和通道可用性校验后才发起。 - 自动打款支持按业务线选择 Paystack 或 Monnify。
amount由运营提前根据目标净到账金额和通道手续费测算并配置,支付中台按配置金额直接发起出款,不额外计算手续费。- 本需求不新增商户余额前置校验、余额预警、监控告警或埋点;provider 出款失败时按自动打款失败处理。
- 自动打款成功后只发送一次短信提醒。
- 自动打款处理中支持进入页面立即查询、按间隔轮询和用户点击
Check Status查询;查询只针对原自动打款 reference。 - 自动打款成功后将旧绑卡申请置为
ENDED_BY_AUTO_FUNDING,并自动跳转至回显绑卡页;旧申请不得被新申请或晚到回调覆盖。 - 回显绑卡页只能返回脱敏卡号、有效期和
cvvMasked=***;不得返回原始 CVV、PIN、OTP 或 provider token。 - 回显绑卡页提交时必须创建新的绑卡订单、reference、provider transaction 和验证会话;修改任一字段均按新绑卡申请处理。
- 同一
bizCode + customerId不能存在多笔处理中的自动打款。 - 创建绑卡订单前能识别同客户、同业务线、同支付通道的 active 绑卡订单;active 订单仍处理中时返回原
reference,不重复创建新订单。 - active 订单判断主拦截点在发起绑卡前;结果页初始化不得创建新绑卡订单,只允许刷新当前
reference状态。 - active 订单已终态时,先更新原订单状态,再按成功、失败、取消或超时结果返回标准
failReason。 - active 订单仍在冷却期内时,后端返回
countdownSeconds,前端据此展示倒计时。
10.2 前端验收
- 所有异常场景均展示
Binding Bank Card结果页,页面结构与现有失败页一致。 - 每个
failReason能展示对应英文 title、message、主按钮和次按钮。 - 页面展示
Reference: {reference},并支持复制。 - 文案不展示内部错误码、完整银行卡号、CVV、PIN、OTP、token 等敏感信息。
Use Another Card会进入新卡绑定流程,并清空本次卡输入状态。- 发卡行限制类
CARD_NOT_SUPPORTED展示Card Not Supported,Key info 为Try another card,主按钮为Use Another Card,不展示Retry作为主操作。 Check Status会触发结果查询,不直接重复绑卡。Check Status和倒计时结束均不得创建新绑卡订单;只有首次绑卡、Retry/Retry Binding、Use Another Card这类重新发起动作才允许进入创建前 active 订单判断。- 已有同支付通道 active 绑卡订单时,前端展示
Processing,Key info 为Do not retry yet,主按钮为Check Status,并展示原Reference。 - 后端返回
countdownSeconds时,前端主按钮显示Check Status (mm:ss)且不可点击;倒计时结束后恢复为可点击的Check Status。 - 自动打款处理中展示
Funding Card,不展示普通失败终态。 - 自动打款处理中页面支持自动轮询和
Check Status主动查询;请求进行中不得重复发起查询。 - 自动打款成功后自动跳转绑卡信息页,回显脱敏卡号和有效期,CVV 固定显示为
***;用户可修改后提交。 - 自动打款成功后的回显页提交会创建新的绑卡申请,不能复用旧绑卡 reference;旧申请状态为已结束。
- 自动打款失败、开关关闭或不满足频控/通道条件时,展示普通余额不足文案。
11. 上线TODO
| 事项 | 影响 |
|---|---|
| 各业务线差异配置 | 决定哪些业务线开启自动打款、各自金额、通道、频控和短信模板 |
| 每日总打款单量配置 | 配置后按量进行灰度;不配置或配置为 -1 时不限制 |
| 当前生产库错误码全集 | 需要从生产库拉取更完整 provider code / message,用于补齐 failReason 映射 |