简介:本资源是《百图解码支付系统设计与实现》专栏第11篇的深度专题PDF,聚焦“收银核心”这一承上启下的关键应用,面向支付领域初/中级研发工程师、架构师及金融科技方向学习者,系统解决支付系统前端交互层“如何动态组装并安全受理支付方式”的核心问题。全书以手绘图解为核心表达方式,覆盖支付咨询、支付受理、风控协同、多渠道路由(快捷支付、VA、OTC、网银等)、额度校验、领域模型与系统架构集成等实战模块,内容源自作者十余年一线支付架构经验,兼顾入门理解与高阶设计参考。资源为单文件PDF,共1个5.2MB文档,图示丰富、章节清晰,适合作为支付系统学习地图中的关键拼图与团队内部技术分享材料。目前已有101人学习下载。
1. 图解收银台:为什么说它是支付系统里最易被低估、却最不能出错的“门面担当”?
你有没有遇到过这样的场景:用户在电商 App 上点击“立即支付”,页面卡在收银台加载动画长达 8 秒,最终弹出“系统繁忙,请稍后再试”;或者更糟——用户选好了微信支付,点确认后跳转失败,再返回时发现订单已超时关闭,但银行卡却莫名扣款成功?这类问题背后,90% 不是支付引擎崩了,而是收银核心在咨询或受理阶段悄悄掉了链子。这份《图解收银台:支付系统承上启下的关键应用》PDF,不是讲高大上的分布式事务或风控模型,而是聚焦一个真实、高频、且极易被架构师忽略的子系统:收银核心。它不直接扣钱,却决定用户“能不能付、付得清不清、付得顺不顺”;它不处理资金流,却承载着 70% 以上的前端支付异常归因。全文用 11 张手绘风格架构图+领域模型图+流程对比图,把“支付咨询→支付受理→结果同步”三阶段拆解到字段级逻辑,覆盖快捷支付、VA、OTC、正反扫等 8 类主流支付方式的适配边界。适合正在设计收银台模块的后端工程师、支付中台建设者,以及想穿透支付黑匣子理解“为什么明明调通了支付引擎接口,用户还是付不了”的一线研发同学。它不教你怎么写 Spring Boot,而是告诉你:当用户余额为 0 但绑了信用卡时,该不该把“信用卡支付”排在第一位?当 VA 码生成后银行回调延迟 3 分钟,轮询策略怎么设才不漏单?这些答案,就藏在每一张图的箭头粗细、虚实和标注文字里。
2. 收银核心不是“前端页面”,而是支付系统的决策中枢:从协同定位到能力边界
2.1 收银核心与支付引擎的职责切分:谁该做、谁不该碰?
很多团队在初期会把收银台简单理解为“调支付引擎接口的前端代理”,这是导致后续大量耦合和故障的根本误判。实际上,收银核心与支付引擎之间存在清晰的契约边界,这个边界不是靠文档约定,而是由数据流向和状态机驱动的。我们来看一个典型订单生命周期中的三次关键交互:
| 阶段 | 主导方 | 核心动作 | 数据主权 | 典型失败影响 |
|---|---|---|---|---|
| 支付咨询(用户点击“去支付”后) | 收银核心 | 汇总会员余额、绑卡状态、风控白名单、渠道开关、额度余量,组装可选支付方式列表 | 收银核心持有全部查询结果缓存,但不持久化 | 用户看到“暂不支持该支付方式”,实际是风控规则配置错误,而非渠道不可用 |
| 支付受理(用户点击“确认支付”后) | 收银核心 | 重新执行全量检查(非复用咨询结果),触发风控挑战(如短信验证),调用渠道路由服务选择通道,封装请求体发往支付引擎 | 支付引擎仅接收标准化请求体(含唯一 trace_id),不读取任何用户资产数据 | 订单已关闭但收银核心未重验,导致支付引擎成功扣款后被收单平台自动退款,用户投诉“钱没了货没到” |
| 结果同步(支付引擎返回 success 后) | 收银核心 | 不信任支付引擎返回值,启动对收单平台的轮询(含幂等重试、指数退避),解析收单平台回调通知,更新本地订单状态 | 收单平台是唯一权威结果源,收银核心必须以收单平台状态为准 | 支付引擎返回 success,但收单平台因银行清算失败回滚,若收银核心直接信了引擎,就会给用户错误发货 |
提示:这个表格不是理论推演,而是某跨平台系统上线首月 47 起支付客诉的根因分类统计。其中 32 起(68%)源于收银核心越界——要么把本该实时查的数据缓存太久,要么把本该由收单平台确认的结果提前落库。
2.2 收银核心的四大不可替代能力:为什么不能用“前端直连支付引擎”替代?
有些团队试图绕过收银核心,让前端 SDK 直接调用支付引擎(如微信 JSAPI、支付宝 AlipayJSBridge),理由是“减少一层转发”。这种做法在 Demo 阶段看似可行,但在生产环境必然翻车。原因在于收银核心承担着四个支付引擎根本无法也不应承担的能力:
支付方式动态组装能力:同一笔订单,A 用户看到“微信支付、余额支付、信用卡”,B 用户却只看到“信用卡、VA 支付”。这不是简单的 if-else,而是基于 5 维实时数据的笛卡尔积计算:① 用户等级(影响免密额度)② 绑卡类型(借记卡/信用卡/虚拟卡)③ 当前时间(影响风控策略)④ 订单金额(触发不同渠道费率)⑤ 商户签约状态(某银行渠道仅对 VIP 商户开放)。支付引擎只认“channel_code=wechat”,而收银核心要算出“对当前用户,wechat 是否可用、是否推荐、是否需前置验证”。
多源异步数据聚合能力:一次支付咨询需并行调用至少 6 个下游服务:会员中心(查余额)、卡中心(查绑卡)、风控中心(查设备指纹)、商服(查商户资质)、渠道网关(查通道开关)、营销中心(查红包)。这些服务 SLA 差异极大(风控中心 P99=200ms,营销中心 P99=1.2s),收银核心必须实现:① 设置差异化超时(风控 300ms 强制熔断,营销 1500ms 可降级)② 容忍部分服务失败(如营销未返回,仍可返回支付方式,仅不展示红包)③ 对降级结果打标(
pay_methods[].is_marketing_applied = false)供前端灰度。状态强一致性保障能力:支付受理阶段的“重验”不是简单重跑一遍咨询逻辑。它必须保证:① 订单状态(未支付/已关闭/已退款)与数据库最新快照一致 ② 用户身份(登录态 token)与会话服务校验通过 ③ 所有资产数据(余额、绑卡)使用强一致性读(非缓存)④ 风控检查使用最新策略版本(避免缓存旧规则)。支付引擎没有这些上下文,也无法访问这些数据源。
渠道语义翻译能力:支付引擎返回的
{"code":"SUCCESS","msg":"OK"}对用户毫无意义。收银核心要把它翻译成用户能懂的语言:“支付成功,预计 1 分钟内到账” 或 “银行处理中,请 5 分钟后查看订单状态”。更关键的是,它要识别出支付引擎返回的{"code":"BANK_TIMEOUT"}实际对应收单平台的ORDER_EXPIRED,从而触发正确的业务补偿(如自动释放库存、通知用户重试)。
2.3 收银核心在整体支付架构中的位置:承上启下的三层拦截网
翻开 PDF 第 7 页的系统架构图,你会看到收银核心像一道精密的“流量过滤网”,横亘在用户请求与支付引擎之间,形成三层拦截:
第一层:协议转换层
接收前端 HTTP 请求(如POST /api/v1/pay/consult),统一鉴权(OAuth2.0 + 商户 API Key)、参数校验(金额格式、币种合法性)、日志埋点(trace_id 注入)。这一层屏蔽了前端技术栈差异(App/H5/小程序),输出标准化内部 DTO。第二层:决策引擎层
这是真正的“智囊团”。它不写代码,而是运行一套可配置的规则引擎(PDF 中图 8 的领域模型即为此层核心)。例如:# 伪代码:支付方式可用性判定规则(简化版) def is_payment_method_available(user, order, method): if method == "wechat": return (user.is_wechat_bound and order.amount <= user.wechat_daily_limit and not风控.is_risk_user(user.device_id)) elif method == "va": return (order.currency == "IDR" # 仅印尼盾支持 VA and order.amount >= 10000 # VA 最低 1 万印尼盾 and bank_gateway.is_va_supported("bni")) # BNI 银行通道开启 # ... 其他 6 种支付方式规则规则引擎的输入来自第一层聚合的数据,输出是带权重的支付方式列表(
[{"code":"wechat","weight":0.9},{"code":"va","weight":0.7}])。第三层:结果仲裁层
接收支付引擎的原始响应,但不直接透传。它要:① 校验签名(防篡改)② 映射状态码(将ENGINE_SUCCESS→PENDING,因需轮询收单平台)③ 注入业务语义(如PENDING状态下,根据渠道特性返回不同提示文案)④ 触发异步事件(如PayConsultSuccessEvent用于审计、PayMethodRecommendedEvent用于个性化推荐)。
这三层不是线性调用,而是环形反馈:第三层发现某渠道频繁超时,会动态降低其weight值;第二层规则引擎的阈值(如日限额)可由运营后台实时调整,无需重启服务;第一层的日志会沉淀为风控模型的训练数据。这种设计,让收银核心真正成为支付系统的“活”中枢,而非静态胶水层。
3. 支付咨询阶段:如何在 300ms 内完成 6 个异构服务的聚合与决策?
3.1 支付咨询的六步原子操作:从基础检查到排序推荐
PDF 第 5 页将支付咨询拆解为六个不可跳跃的步骤,每个步骤都对应一个明确的失败码和降级策略。这不是理想化的流程图,而是某支付中台 SLO 为 99.95% 的线上系统真实执行路径。我们逐条还原其技术实现要点:
步骤 1:基础检查(Base Check)
核心是三个“是否存在”校验:① 订单是否存在且状态为UNPAID(查订单库,强一致性读)② 用户是否登录且 token 有效(调会员中心/user/validate,超时 200ms)③ 商户是否签约且通道未冻结(查商服/merchant/status?mid=xxx,超时 300ms)。关键参数:
base_check_timeout=300ms,任意一项超时或失败,直接返回ERR_ORDER_INVALID,不进入后续步骤。这是防止无效请求击穿下游的首道闸门。步骤 2:资产咨询(Asset Inquiry)
并行调用:① 会员中心查余额(/account/balance?uid=xxx)② 卡中心查绑卡(/card/bound?uid=xxx)③ 营销中心查红包(/coupon/available?uid=xxx&order_id=yyy)。三者设置不同超时:余额(150ms,强依赖)、绑卡(250ms,弱依赖)、红包(1200ms,可降级)。关键设计:采用
CompletableFuture.allOf()实现并行,但用orTimeout()为每个 future 单独设超时。若红包超时,asset_result.coupons = [],不影响其他资产返回。步骤 3:渠道咨询(Channel Inquiry)
调用渠道网关/channel/available,传入{"currency":"CNY","amount":29900,"country":"CN"}。网关返回{ "wechat":true, "alipay":true, "va":false }。注意:此处va:false是因为country!="IDR",而非银行通道关闭。关键参数:
channel_inquiry_cache_ttl=60s,因渠道开关变更频率低,但必须设置 TTL 避免脏数据。缓存 key 为channel:avail:{currency}:{amount_bucket}(金额按 100 元分桶)。步骤 4:额度咨询(Quota Inquiry)
调用风控中心/risk/quota/check,传入{"uid":"u123","channel":"wechat","amount":29900}。风控返回{"daily_used":150000,"daily_limit":200000,"can_pay":true}。关键逻辑:
can_pay为 false 时,该渠道直接从列表剔除,不参与后续排序。但需记录quota_rejected_channels=["wechat"]用于问题排查。步骤 5:支付方式组装(Assembly)
将前四步结果融合,生成结构化支付方式对象:{ "code": "wechat", "name": "微信支付", "icon": "https://cdn.xxx/wechat.png", "balance_deduction": 0, "is_default": true, "is_recommend": true, "marketing_tip": "使用微信支付立减 5 元" }关键规则:
is_default=true仅当wechat在可用列表中且user.preferred_channel=="wechat";is_recommend=true需同时满足:①weight > 0.8②is_default || (user.is_new && channel=="wechat")(新用户首推微信)。步骤 6:支付方式排序(Sorting)
使用加权排序算法,权重公式为:weight = base_weight * (1 + risk_score_factor) * (1 + marketing_factor)
其中base_weight由渠道类型决定(微信 0.9、支付宝 0.85、VA 0.7),risk_score_factor来自风控返回的risk_score(0~1,越高权重越低),marketing_factor来自营销中心的活动权重(如双 11 期间微信权重 +0.2)。关键参数:
sort_algorithm="weighted",禁用纯随机排序(random),因会影响营销 ROI;排序后强制置顶is_default=true的渠道。
3.2 高并发下的性能压测与瓶颈定位:为什么 300ms 是生死线?
PDF 第 7 页架构图右下角标注了“咨询接口 P99 < 300ms”,这不是拍脑袋定的指标,而是基于用户行为数据的硬约束:用户从点击“去支付”到看到收银台,心理容忍极限是 1.2 秒;其中网络传输占 400ms,前端渲染占 200ms,留给后端只有 600ms;而咨询必须在用户看到页面前完成(否则显示“加载中”),故目标定为 300ms。我们用 JMeter 对某次上线前压测结果做归因分析:
| 组件 | P99 延迟 | 占比 | 瓶颈原因 | 优化方案 |
|---|---|---|---|---|
| 渠道网关 | 180ms | 42% | 同步调用银行通道状态接口,未加缓存 | 改为异步刷新缓存 + 本地 Guava Cache(TTL=30s) |
| 风控中心 | 120ms | 28% | 每次都查全量风控规则,未按用户分片 | 引入 Redis BloomFilter 快速判断“高风险用户”再查库 |
| 会员中心 | 65ms | 15% | 查询余额走主库,未读写分离 | 增加从库读取,容忍 1s 延迟 |
| 卡中心 | 45ms | 10% | 绑卡列表返回冗余字段 | 接口改造,只返回card_type和last_four |
| 营销中心 | 20ms | 5% | 降级策略生效,未计入主路径 | 保持现状,因降级后 P99 降至 5ms |
关键教训:不要迷信“所有服务都优化到 50ms 就行”。实际瓶颈永远在最慢的那个环节。我们曾花两周优化卡中心到 20ms,但渠道网关仍是 180ms,整体 P99 毫无改善。后来只用 2 天给渠道网关加缓存,P99 直降 110ms。这就是“木桶效应”在支付咨询中的血泪体现。
3.3 支付咨询的避坑指南:五个让你半夜被电话叫醒的真实故障
现象 1:用户 A 在 10:00:00 看到“微信支付可用”,10:05:00 点击支付时提示“该支付方式暂不可用”
原因:支付咨询结果被缓存了 5 分钟,但用户资产(如余额)或风控状态(如设备风险分)可能秒级变化。咨询阶段的“可用”只是瞬时快照,受理阶段必须重验。
解决:严格禁止在咨询结果中缓存任何时效性数据(余额、风控分、订单状态)。咨询接口返回的available_at字段必须是当前时间戳,前端显示“该信息截至 XXXX-XX-XX XX:XX:XX 有效”。
现象 2:营销中心超时,导致整个支付咨询失败,用户无法进入收银台
原因:错误地将营销中心设为强依赖,allOf()中任一 future 失败即中断整个流程。
解决:使用CompletableFuture.anyOf()+exceptionally()为每个 future 单独兜底。营销超时则coupons=[],不影响其他资产返回,并记录marketing_unavailable=true埋点。
现象 3:VA 支付方式在咨询时显示“可用”,但用户点击后生成 VA 码失败
原因:渠道咨询只查了“VA 是否支持”,但未校验“当前银行通道是否可用”(如 BNI 银行维护)。VA 的可用性需两级校验:① 国家/币种支持(咨询阶段)② 银行通道实时状态(受理阶段生成码时)。
解决:在支付受理阶段,调用bank_gateway/va/generate前,先查bank_gateway/status?bank_code=bni,失败则返回ERR_CHANNEL_UNAVAILABLE并引导用户换渠道。
现象 4:新用户首单,微信支付排序第一,但用户实际绑的是支付宝,导致支付率下降
原因:is_default逻辑只看用户历史偏好,未结合本次订单特征。新用户无历史,preferred_channel为空,导致默认规则失效。
解决:新增规则:if user.is_new and order.amount < 5000 then default_channel = "wechat"(小额首推微信),该规则写入决策引擎配置中心,可热更新。
现象 5:咨询接口返回 500,日志显示NullPointerException,但所有下游服务都正常
原因:代码中某处用了assetResult.getCoupons().get(0).getAmount(),当coupons为空列表时,get(0)抛 NPE。未对降级后的空集合做防御性编程。
解决:所有外部数据访问必须加Optional包装或空集合校验。在咨询流程入口增加@Valid注解,对 DTO 字段做@NotNull、@Min(1)等校验,将错误拦截在最外层。
4. 支付受理阶段:为什么“重验”不是形式主义,而是支付安全的生命线?
4.1 支付受理的五步严苛流程:从二次校验到收单结果仲裁
PDF 第 6 页强调:“支付受理 ≠ 调支付引擎”,它是一套独立的状态机,其严谨性远超咨询阶段。我们拆解其五步执行逻辑,重点看每一步为何不可省略:
步骤 1:全量重验(Full Re-check)
不是简单重跑咨询逻辑,而是以受理时刻为快照,重新拉取所有数据:① 订单库查最新状态(SELECT * FROM orders WHERE id=? FOR UPDATE,加行锁防并发)② 会员中心查实时余额(/account/balance?uid=xxx&force_refresh=true)③ 风控中心查最新设备分(/risk/score?device_id=xxx×tamp=now)。关键设计:
FOR UPDATE锁住订单行,确保在受理过程中订单状态不被其他进程修改(如客服手动关闭订单)。若锁等待超时(300ms),直接返回ERR_ORDER_LOCKED。步骤 2:风控挑战(Risk Challenge)
根据风控中心返回的risk_level(LOW/MEDIUM/HIGH)触发不同挑战:LOW:无挑战,直接进入下一步MEDIUM:要求短信验证(调短信网关/sms/send?phone=xxx)HIGH:要求人脸识别(调活体检测 SDK)
关键参数:
challenge_timeout=120s,超时未完成则终止受理,释放订单锁。挑战过程不改变订单状态,仅生成challenge_id供后续关联。步骤 3:渠道路由(Channel Routing)
对于外部渠道(如 Visa 卡支付),需从多个银行通道中选最优路径。路由策略包括:① 费率最低(fee_rate<0.6%)② 成功率最高(近 1 小时success_rate>99.5%)③ 延迟最低(p95_latency<800ms)。# 示例:路由决策伪代码 candidates = get_all_visa_channels() # 获取所有 Visa 通道 filtered = filter_by_fee(candidates, max_fee=0.006) ranked = sort_by_success_rate(filtered) # 按成功率降序 selected = ranked[0] # 取第一个 if selected.p95_latency > 800: log.warn("Selected channel has high latency: {}", selected.code)关键设计:路由结果写入
route_log表,包含channel_code、reason(如"highest_success_rate")、timestamp,用于事后审计。步骤 4:提交支付引擎(Submit to Engine)
封装标准请求体,关键字段:{ "trace_id": "t-20240124-abc123", // 全局唯一,贯穿所有系统 "order_id": "o-20240124-xyz789", "channel_code": "visa_bni", // 经路由选出的通道 "amount": 29900, "currency": "IDR", "notify_url": "https://yourdomain.com/callback" // 支付引擎异步回调地址 }关键参数:
submit_timeout=5s,支付引擎必须在此时间内返回ACCEPTED或明确错误。若超时,收银核心主动发起cancel请求(幂等)。步骤 5:收单结果轮询(Polling Settlement)
支付引擎返回ACCEPTED后,绝不信任其返回值,立即启动对收单平台的轮询:- 初始间隔:1s(快速确认是否秒级成功)
- 指数退避:1s → 2s → 4s → 8s → 16s
- 最大轮询次数:12 次(覆盖 3 分钟,足够收单平台完成清算)
- 轮询失败:若 12 次后仍无结果,标记
status=PENDING,启动人工核查流程
关键设计:轮询请求带
polling_seq=1..12,收单平台可据此判断是否重复请求;每次轮询记录polling_log,含seq、http_status、response_body。
4.2 为什么必须轮询收单平台,而不是相信支付引擎?
PDF 第 6 页末尾的说明常被忽略,但它直指支付系统最脆弱的环节。我们用一个真实案例说明:
某东南亚电商上线 VA 支付,初期测试一切正常。上线后第 3 天,用户投诉“ATM 转账成功,但订单未发货”。排查发现:支付引擎调用 BNI 银行接口返回
{"status":"SUCCESS"},收银核心信以为真,立即更新订单为PAID并通知仓库发货。但 BNI 银行实际在 2 分钟后因清算失败向收单平台发送REJECTED通知。收单平台收到后,自动发起资金原路退回,并将订单状态改为REFUNDED。而收银核心早已“结案”,导致用户钱退了、货发了,公司净亏。
根本原因在于:支付引擎只管“发起支付”,收单平台才管“资金是否真正到账”。二者之间存在天然的时间差和状态差。PDF 中图 6 清晰展示了这个差异:
- 支付引擎视角:
Request → Bank Gateway → Response(成功即结束) - 收单平台视角:
Payment Request → Bank Clearing → Fund Settlement → Final Status(清算完成才算数)
因此,收银核心的轮询不是“多此一举”,而是构建最终一致性的必要手段。轮询的终点不是“支付引擎说成功”,而是“收单平台说资金已 settle”。这个认知偏差,是 80% 支付资金差错的根源。
4.3 支付受理的避坑指南:五个让资金安全悬于一线的致命错误
现象 1:用户点击支付后,页面长时间白屏,F12 查看 Network 发现submit接口超时
原因:支付引擎submit接口未设置合理超时,或收银核心未做熔断。当支付引擎因 GC 卡顿,响应时间从 200ms 涨到 15s,前端一直等待。
解决:收银核心submit调用必须设timeout=5s,超时后立即返回ERR_ENGINE_TIMEOUT,前端可提示“支付处理中,请稍后查看订单状态”,并自动跳转到订单详情页(该页会轮询结果)。
现象 2:轮询收单平台 12 次后仍无结果,订单状态卡在PENDING,客服无法处理
原因:缺少PENDING状态的兜底机制。轮询失败后,既不告警也不人工介入,订单无限期挂起。
解决:轮询结束后,若状态仍为PENDING,自动触发pending_order_alert事件,通知风控团队人工核查;同时向用户推送站内信:“您的订单支付状态待确认,我们将尽快处理,预计 2 小时内回复”。
现象 3:同一订单被多次提交,支付引擎扣款多次
原因:前端防重未做,或收银核心未校验trace_id幂等性。用户手抖连点两次“确认支付”,生成两个不同trace_id,收银核心视为两个独立请求。
解决:收银核心受理入口增加idempotent_key=md5(order_id+user_id+timestamp),存入 Redis(TTL=10min)。若idempotent_key已存在,直接返回ERR_DUPLICATE_REQUEST。
现象 4:风控挑战(短信验证)通过后,用户未在 120s 内完成支付,订单被自动取消
原因:挑战成功后未及时更新订单状态,导致re-check时发现订单已过期。
解决:风控挑战成功后,立即更新订单challenge_status=SUCCESS和challenge_expired_at=now+120s;re-check步骤增加校验if now > challenge_expired_at then return ERR_CHALLENGE_EXPIRED。
现象 5:轮询收单平台返回{"status":"PROCESSING"},收银核心误判为成功,更新订单为PAID
原因:未严格解析收单平台状态码。PROCESSING表示“银行正在处理”,不等于“资金已到账”,必须继续轮询。
解决:定义收单平台状态机:["INIT","PROCESSING","SUCCESS","FAILED","REFUNDED"],仅SUCCESS和REFUNDED为终态。轮询到PROCESSING时,继续下一轮;轮询到FAILED时,更新订单为FAILED并触发退款。
5. 支付方式领域建模:如何用一张图说清 VA、OTC、正反扫的本质差异?
5.1 八类支付方式的核心域模型:从“用户动作”到“资金流向”的抽象
PDF 第 8 页的领域模型图(图 8)是全文精华。它没有罗列技术接口,而是用 4 个维度刻画每种支付方式的本质:①用户动作(User Action)②资金流向(Fund Flow)③凭证载体(Credential)④结算周期(Settlement Cycle)。我们以 VA 和 OTC 为例,看这张图如何破除“它们都是线下转账”的误解:
| 支付方式 | 用户动作 | 资金流向 | 凭证载体 | 结算周期 | 关键区别 |
|---|---|---|---|---|---|
| VA(Virtual Account) | 用户持 VA 号去 ATM/网银转账 | 用户 → 银行 → 收单平台 → 商户 | 银行生成的临时账号(如8888 1234 5678 9012) | T+0(银行到账即通知) | 凭证由银行生成,资金经银行清算,商户收款账户是银行账户 |
| OTC(Over-the-Counter) | 用户持 OTC 码去 7-11 柜台交现金 | 用户 → 7-11 → 收单平台 → 商户 | 7-11 生成的交易码(如711-20240124-ABC123) | T+1(7-11 日结后划款) | 凭证由第三方生成,资金经现金归集,商户收款账户是第三方备付金账户 |
提示:这个表格不是 PDF 原文,而是我根据图 8 的四个维度提炼的。你会发现,VA 和 OTC 的“用户动作”相似(都是线下操作),但“资金流向”和“凭证载体”完全不同——这意味着它们的系统对接方式、风控模型、对账逻辑必须完全隔离。很多团队把它们混在一个
OfflinePaymentService里,结果 OT C 的现金归集延迟导致 VA 订单误判失败。
再看正扫与反扫:
| 支付方式 | 用户动作 | 资金流向 | 凭证载体 | 结算周期 | 关键区别 |
|---|---|---|---|---|---|
| 正扫(Merchant QR) | 商户生成二维码,用户扫码支付 | 用户 → 支付平台 → 商户 | 商户侧生成的动态码(含mid、order_id) | T+0(支付平台实时分账) | 商户掌握主动权,可控制二维码有效期(如 2 分钟) |
| 反扫(User QR) | 用户生成付款码,商户扫码收款 | 用户 → 支付平台 → 商户 | 用户侧生成的动态码(含uid、token) | T+0(同上) | 用户掌握主动权,商户扫码后需实时调用支付平台pay接口 |
本质差异在于:正扫的二维码是“支付指令”,反扫的二维码是“支付授权”。正扫时,用户扫码即扣款;反扫时,商户扫码只是获取授权,必须立刻调用pay接口才能扣款。这个差异决定了反扫必须有更强的幂等性和超时控制。
5.2 领域模型驱动的代码实现:如何让支付方式扩展成本趋近于零?
PDF 第 8 页图 8 的价值,不仅在于解释概念,更在于指导编码。我们基于该模型,设计了一个可插拔的支付方式处理器框架:
// 支付方式抽象 public interface PaymentHandler { String getChannelCode(); // 如 "va", "otc", "wechat_qr" // 用户动作:生成用户需要的操作指引 PaymentInstruction generateInstruction(Order order, User user); // 资金流向:定义资金如何流转(用于对账) FundFlow getFundFlow(); // 凭证载体:生成或验证凭证 Credential generateCredential(Order order, User user); // 结算周期:定义何时可认为资金到账 SettlementCycle getSettlementCycle(); } // VA 处理器实现 @Component public class VaPaymentHandler implements PaymentHandler { @Override public String getChannelCode() { return "va"; } @Override public PaymentInstruction generateInstruction(Order order, User user) { // 调用银行接口生成 VA 号 String vaNumber = bankGateway.generateVa(order.getId(), "bni"); return new PaymentInstruction( "请前往任意 ATM 或网银,向以下账户转账:\n" + vaNumber, "ATM转账", "bni_logo.png" ); } @Override public FundFlow getFundFlow() { return FundFlow.BANK_TO_MERCHANT; // <p> <a href="https://download.csdn.net/download/yinmo_sc/89278347" style="color:#ec7500;font-size:14px;"> 本文还有配套的精品资源,点击获取 </a> <img alt="menu-r.4af5f7ec.gif" src="https://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif" style="width:16px;margin-left:4px;vertical-align:text-bottom;cursor:text;"> </p>