支付交易、资金一致性与订单履约:精通级入口
本页是
4.1模块的稳定入口。新版分册负责深化机制、故障与项目表达;原有正文完整保留在旧版兼容材料中,用于迁移账本反向追踪和既有学习路径兼容。
1. 精通级分册导航
| 编号 | 分册 | 核心职责 |
|---|---|---|
| 4.1.0 | 业务知识图谱、迁移账本与项目事实映射 | 旧资产基线、唯一去向、证据等级、业务不变量和验收索引 |
| 4.1.1 | 支付交易领域模型、状态机、金额币种与订单分离 | 业务订单、支付单、渠道会话、未知态、金额与币种 |
| 4.1.2 | 渠道适配、验签、防重放、回调与主动查单 | 渠道边界、事件确认、回调查单、幂等和安全 |
| 4.1.3 | 余额账户、冻结、账务分录与资金一致性 | 余额分层、不可变分录、复式守恒、并发与恢复 |
| 4.1.4 | 取消、退款、冲正、对账、出账与结算 | 退款额度、未知态、三方对账、差异处理和结算闭环 |
| 4.1.5 | 订单、库存、海外仓履约、同步与补偿 | 库存不变量、下游未知结果、补偿与人工接管 |
| 4.1.6 | 面单、承运商、轨迹乱序、轮询回调与异常恢复 | 面单完整性、轨迹去重、终态保护和双通道恢复 |
| 4.1.7 | 项目串讲、容量、排障、安全审计与综合题库 | WMS(仓储管理系统)、跨境支付、海外仓、物流、容量与事故表达 |
2. 推荐学习顺序
- 先读
4.1.0,统一事实等级、资金/库存/履约/物流不变量及旧资产口径。 - 按
4.1.1 → 4.1.2 → 4.1.3 → 4.1.4建立“交易建模—渠道确认—内部账务—退款对账结算”的资金闭环。 - 按
4.1.5 → 4.1.6建立“订单库存—海外仓—面单轨迹—异常恢复”的履约闭环。 - 最后读 4.1.7 项目串讲,把机制压缩成容量、排障、安全、审计和跨域追问话术。
3. 项目事实边界
| 等级 | 本模块允许的表达 | 使用边界 |
|---|---|---|
E1 | 源码对象、接口或任务“存在” | 必须能定位精确项目与路径,不外推生产配置和运行量 |
E2 | 源码可读出的调用、保存或排队流程 | 只描述真实调用链,不把设计建议写成现网行为 |
E3 | 旧根文档已经提出的知识资产 | 作为学习基线和迁移来源,不自动升级为项目事实 |
E4 | 明确标注的演练样例或建议方案 | 用于解释机制、失败窗口和验证方法,不冒充线上事故 |
E5 | 待源码或现场核对 | 渠道签名版本、密钥轮换、状态码、阈值、结算周期、生产指标与运营流程均留在此级 |
项目表达必须同时守住三条线:源码能证明的只说到源码边界;演练数字只用于复算,不写入简历成果;支付渠道、仓库和承运商返回超时时保留未知态,先查询权威结果,再决定补记、补偿或人工核验。
4. 图形资产索引
七张 PlantUML(统一建模语言)正式图均已有同名 PNG(便携式网络图形)渲染文件;本次收口复用既有渲染结果,不重复执行耗时渲染。
| 分册 | PlantUML(统一建模语言)源文件 | PNG(便携式网络图形)渲染图 |
|---|---|---|
| 4.1.1 | 支付状态与确认源文件 | 支付状态与确认图 |
| 4.1.2 | 渠道回调查单源文件 | 渠道回调查单图 |
| 4.1.3 | 复式账不变量源文件 | 复式账不变量图 |
| 4.1.4 | 退款对账结算闭环源文件 | 退款对账结算闭环图 |
| 4.1.5 | 订单库存仓库补偿源文件 | 订单库存仓库补偿图 |
| 4.1.6 | 面单承运商轨迹恢复源文件 | 面单承运商轨迹恢复图 |
| 4.1.7 | 项目故障域与可观测性源文件 | 项目故障域与可观测性图 |
5. 快速复习路线
| 可用时间 | 复习动作 | 结束时应能回答 |
|---|---|---|
| 20 分钟 | 读 4.1.0 的四类不变量,再看七张正式图 | 为什么超时不是失败、为什么资金/库存/履约/物流要分开守恒 |
| 45 分钟 | 读 4.1.1、4.1.2、4.1.3、4.1.4 的状态机、演绎和排障 | 如何从支付发起讲到回调、查单、记账、退款、对账与结算 |
| 45 分钟 | 读 4.1.5、4.1.6 的未知态、补偿与终态保护 | 如何处理库存竞争、下游仓超时、面单失败和轨迹乱序 |
| 60 分钟 | 直接练习 4.1.7 项目串讲与综合题库 | 如何按背景、约束、设计、风险、降级、证据、恢复与边界完成项目口述 |
6. 迁移说明
本次采用“旧内容保留,分册深化与交叉引用”,不是物理迁移。下方原文不删除、不移动、不改写;4.1.0 的账本负责把旧资产分配给唯一责任分册,新分册以更完整的状态、失败分支、表格、数据演绎、排障和项目话术深化主题,其他位置通过链接复用。
| 旧资产 | 01 | 02 | 03 | 04 | 05 | 06 | 07 | 复算合计 |
|---|---|---|---|---|---|---|---|---|
旧 ### 标题 | 5 | 6 | 4 | 8 | 6 | 3 | 7 | 39 |
| 旧题 | 9 | 10 | 8 | 12 | 12 | 5 | 11 | 67 |
| 旧 Mermaid(图表语法)图 | 2 | 2 | 0 | 1 | 2 | 1 | 1 | 9 |
| 旧表 | 1 | 1 | 3 | 1 | 2 | 1 | 4 | 13 |
| 旧数据演绎 | 0 | 2 | 1 | 2 | 1 | 0 | 0 | 6 |
| 旧排障段 | 0 | 1 | 1 | 2 | 1 | 1 | 0 | 6 |
| 旧项目话术段 | 0 | 0 | 0 | 0 | 0 | 0 | 5 | 5 |
| 可枚举旧资产 | 17 | 22 | 17 | 26 | 24 | 11 | 28 | 145 |
复算依据为 legacy-40-h01—h39、legacy-40-q01—q67、legacy-40-g01—g09、legacy-40-t01—t13、legacy-40-d01—d06、legacy-40-o01—o06 和 legacy-40-p01—p05,共 145 个唯一稳定标识,无重复;计算式为 39 + 67 + 9 + 13 + 6 + 6 + 5 = 145。4.1.0 已同步修正汇总口径,旧内容继续保留,分册承担深化与交叉引用,不伪称物理迁移。收口前原根 SHA-256(安全散列算法)为 113a1eaf3151f776025f3dcdcc95980954641c51d716926b908813c0b53b328e。
7. 旧版兼容材料
以下内容从原始标题开始完整保留,继续承担旧链接兼容、迁移反向核对和原始学习材料留档职责。
支付交易、资金一致性与订单履约
1. 简历关联点
本模块基于以下真实项目能力整理,目标是把“做过支付、订单和仓储”提升为能够完整讲清业务边界、状态流转、资金正确性、上下游协同和异常补偿的高级面试表达。
| 项目 | 业务定位 | 可提炼的核心能力 |
|---|---|---|
hop-java、hop-java2、hop-java3 | 跨境电商交易、在线充值、余额支付、供应商账单与结算 | 支付单与业务单分离、Airwallex(空中云汇)渠道适配、余额流水、退款、账单、对账、结算、异步任务 |
hop-mall | 商城交易前端 | 结算页、支付方式选择、支付结果展示、订单状态查询 |
nest2 | 客户充值与多渠道支付 | Stripe(国际支付网关)、PayPal(国际支付平台)、PhotonPay(光子易支付)、苏宁支付、线下支付、余额充值、回调重试 |
hall-next | 面单与物流轨迹中台 | 多承运商下单、面单异步拉取、失败重试、轨迹订阅、Webhook(回调通知)、扣费与退款 |
hiwi-unify | 海外仓 OMS(订单管理系统)与 WMS(仓储管理系统) | 订单履约、库存冻结、下游仓 SDK(软件开发工具包)适配、面单来源、状态轮询、出库计费、异常补偿 |
源码中能够确认的设计事实包括:
hop系列存在支付渠道工厂、支付接口抽象、支付单、支付会话、支付日志、在线充值、余额流水、退款任务、供应商账单和结算单。nest2将多种支付渠道统一到支付服务接口,并为 Stripe(国际支付网关)、PayPal(国际支付平台)等渠道实现回调后主动查单和延迟重试。hall-next使用延迟队列和分布式锁推进第三方下单、面单拉取、轨迹同步、扣费和退款任务。hiwi-unify通过订单业务编排、库存业务、下游订单和多仓 SDK(软件开发工具包)实现海外仓履约,并在外部调用失败时进行有限重试、状态轮询和异常标记。
面试时只陈述自己真实负责或深度参与的部分。本文中的“推荐设计”用于补全架构表达,不应虚构为已经上线的功能或数据。
2. 面试主线
回答支付交易和订单履约问题时,建议始终沿下面七层展开:
- 业务层:用户购买什么、向谁付款、谁负责履约、何时确认收入和成本。
- 单据层:业务订单、支付单、渠道会话、退款单、余额流水、费用流水、账单、结算单分别解决什么问题。
- 状态层:状态只能单向合法流转,重复、乱序和延迟事件不能把终态改坏。
- 一致性层:数据库事务保证本地原子性,唯一约束和状态条件更新保证幂等,消息和补偿保证最终一致。
- 接入层:通过适配器隔离不同支付渠道、承运商和海外仓的协议差异。
- 风控层:验签、防重放、金额币种核验、敏感信息保护、审计日志和权限控制。
- 兜底层:主动查询、延迟重试、对账、补偿任务、人工工单和可观测性。
一段适合面试开场的总述:
我理解支付和履约不是两个孤立模块。支付解决“钱是否真实到账”,履约解决“商品是否按约交付”,中间依靠订单状态机连接。设计时我会把业务订单、支付单、渠道交易、账务流水、退款单、履约单和结算单分开建模;同步请求只表达“受理”,最终结果以渠道回调、主动查询和对账为准;所有异步入口都要求幂等,并通过状态机、唯一约束、流水和补偿任务保证资金和履约最终一致。
3. 基础知识
3.1 支付交易领域模型
支付系统不能只建一张“充值表”或在订单上放一个支付状态。不同单据承担不同职责,分离后才能支持重复支付、换渠道、部分退款、审计和对账。
| 领域对象 | 核心职责 | 关键唯一标识 | 典型状态 |
|---|---|---|---|
| 业务订单 | 表达用户购买或充值意图 | 业务订单号 | 待支付、已支付、取消、履约中、完成 |
| Payment Order(支付单) | 表达一次应付请求,可聚合一个或多个业务订单 | 支付单号 | 申请、处理中、成功、失败、取消、异常 |
| Payment Session(支付会话) | 记录一次具体渠道交互 | 渠道会话号 | 待支付、成功、取消、异常、退款 |
| Channel Transaction(渠道交易) | 记录第三方真实交易结果 | 渠道交易号 | 待处理、成功、失败、撤销 |
| Refund Order(退款单) | 表达一次退款意图和退款进度 | 退款单号 | 申请、退款中、成功、失败、异常 |
| Balance Account(余额账户) | 保存可用余额、冻结余额等账户快照 | 账户号 | 正常、冻结、停用 |
| Ledger Entry(账务分录) | 记录每次资金变化的不可变事实 | 流水号 | 已记账、冲正 |
| Fee Record(费用流水) | 记录商品费、运费、出库费、手续费等应收应付 | 费用流水号 | 待出账、已出账、待支付、已支付、已退款 |
| Bill(账单) | 按账期聚合费用流水供核对 | 账单号 | 未核对、核对中、已核对 |
| Settlement(结算)单 | 聚合已核对账单并推进实际结款 | 结算单号 | 未结算、审核中、待结款、已结款、驳回 |
关键关系:
- 一个业务订单可以产生多个支付单,例如第一次支付超时后换渠道重试,但只能有一个有效成功结果。
- 一个支付单可以产生多个支付会话,例如用户关闭收银台后重新发起。
- 支付成功不等于账务完成,必须生成可审计的账务流水并推进业务订单。
- 退款单必须关联原支付单和原渠道交易,累计成功退款金额不能超过原成功支付金额。
热门面试题
- 问题(基础题):为什么业务订单和 Payment Order(支付单)要分开?
- 考点:领域建模、重复支付、渠道切换。
- 回答思路:业务订单表达买卖关系,Payment Order(支付单)表达一次收款尝试。分离后可以支持一个订单多次发起支付、换渠道、关闭旧会话,同时保证订单只能被有效支付一次。
- 进阶追问:同一业务订单出现两个渠道都成功怎么办?
- 问题(原理题):为什么余额不能只保存一个余额字段?
- 考点:账户快照、账务流水、审计。
- 回答思路:余额字段只表示当前快照,无法解释为什么变化。必须同时保存不可变流水、业务单号、变动前后金额和操作类型,才能追溯和对账。
- 进阶追问:流水写成功但余额更新失败如何处理?
- 问题(项目题):
hop支付模型中哪些对象最值得讲?- 考点:支付单、支付会话、支付日志、余额流水、账单结算。
- 回答思路:重点讲支付渠道工厂统一接入、Payment Order(支付单)隔离业务订单、Payment Session(支付会话)记录渠道交互、余额流水审计,以及费用流水到供应商结算的闭环。
- 进阶追问:为什么支付日志不能代替账务流水?
3.2 支付状态机与金额模型
状态机的核心不是列出几个字符串,而是定义“哪些状态可以转到哪些状态、由谁触发、是否可逆、重复触发如何处理”。
stateDiagram-v2
[*] --> 申请
申请 --> 支付中: 创建渠道会话
支付中 --> 成功: 回调或主动查询确认
支付中 --> 失败: 渠道明确失败
支付中 --> 取消: 用户关闭且渠道取消成功
支付中 --> 异常: 超时或结果不确定
异常 --> 成功: 补偿查询确认成功
异常 --> 失败: 补偿查询确认失败
成功 --> 部分退款: 部分退款成功
成功 --> 全额退款: 全额退款成功
部分退款 --> 全额退款: 累计退款达到支付金额状态更新应采用条件更新:
UPDATE payment_order
SET status = 'success', paid_amount = ?, paid_time = ?
WHERE payment_order_no = ?
AND status IN ('apply', 'paying', 'exception');受影响行数为 1 表示本次完成了状态推进;为 0 表示已被其他线程处理或当前状态不允许推进,随后应读取当前状态并按幂等结果返回。
金额模型必须遵守:
- Java(编程语言)使用 BigDecimal(高精度十进制数),数据库使用 DECIMAL(定点小数),禁止用 double(双精度浮点数)计算资金。
- 明确金额最小单位、精度和舍入规则,例如美元保留两位小数,渠道若使用分则在边界层统一转换。
- 每个支付单固定币种;金额和币种必须同时校验。
- 分摊时最后一项吸收舍入差,保证各明细之和严格等于总额。
热门面试题
- 问题(基础题):支付状态为什么不能随意覆盖?
- 考点:状态机、终态保护、乱序回调。
- 回答思路:支付事件可能重复和乱序,如果直接覆盖,迟到的失败回调可能把成功改成失败。必须定义合法迁移,并用状态条件更新保护终态。
- 进阶追问:取消回调先到、成功回调后到,最终是什么状态?
- 问题(原理题):为什么支付金额要使用 BigDecimal(高精度十进制数)?
- 考点:二进制浮点误差、精度、舍入。
- 回答思路:double(双精度浮点数)不能精确表示多数十进制小数,连续计算会产生误差。BigDecimal(高精度十进制数)配合明确精度和舍入规则才能保证账务一致。
- 进阶追问:BigDecimal(高精度十进制数)用字符串构造和用 double(双精度浮点数)构造有什么区别?
- 问题(项目题):如何解决
hop中多个支付状态口径并存的问题?- 考点:统一状态语义、兼容迁移。
- 回答思路:先定义领域标准状态,再为旧表建立映射;写入统一走领域服务,读取阶段兼容旧值;通过数据校验和灰度迁移逐步收口,避免一次性改表造成风险。
- 进阶追问:迁移期间新旧服务同时写入怎么办?
3.3 订单履约领域模型与状态机
订单履约关注“订单从承诺到交付”的全过程。在海外仓场景中,至少要区分客户订单、履约订单、下游仓订单、库存冻结、包裹、面单、轨迹、费用和异常任务。
| 对象 | 作用 | 不能混淆的边界 |
|---|---|---|
| Customer Order(客户订单) | 表达客户交付要求 | 不直接承载某个仓库的技术状态 |
| Fulfillment Order(履约订单) | 表达仓库拣货、打包、出库任务 | 可因拆仓、缺货拆成多个履约单 |
| Downstream Order(下游订单) | 对应第三方海外仓系统中的订单 | 下游单号和本地单号必须双向关联 |
| Inventory Reservation(库存预占) | 防止并发订单使用同一库存 | 预占、扣减、释放必须可追溯 |
| Parcel(包裹) | 承载重量、尺寸、箱数、商品明细 | 一个履约单可能拆多个包裹 |
| Label(面单) | 承运商运输凭证 | 可能来自客户、物流中台或下游仓 |
| Tracking Event(轨迹事件) | 记录物流节点事实 | 事件可能重复、乱序、延迟 |
stateDiagram-v2
[*] --> 待审核
待审核 --> 待履约: 审核通过并冻结库存
待履约 --> 下发中: 创建下游仓订单
下发中 --> 待出库: 下游受理成功
下发中 --> 异常: 超时或明确失败
待出库 --> 已出库: 下游状态同步
待出库 --> 拦截中: 客户取消
拦截中 --> 已取消: 下游取消成功并释放库存
已出库 --> 运输中: 首条运输轨迹
运输中 --> 已签收: 签收轨迹
运输中 --> 物流异常: 退件或派送失败热门面试题
- 问题(基础题):为什么客户订单和履约订单要分开?
- 考点:拆仓、拆包、业务与执行解耦。
- 回答思路:客户订单表达一个商业承诺,履约订单表达仓库执行。同一客户订单可能按仓库、库存和包裹拆成多个履约订单,因此不能用一套状态硬塞在一张表里。
- 进阶追问:多个履约单部分成功时客户订单状态怎么计算?
- 问题(原理题):库存冻结、实际扣减和释放分别在什么时候发生?
- 考点:库存状态、支付与履约边界。
- 回答思路:审核或接单时冻结,确认出库时转为实际扣减,取消或履约失败时释放。每次变化必须有库存流水和业务单号。
- 进阶追问:下游已出库但本地仍是冻结状态怎么办?
- 问题(项目题):
hiwi-unify的履约编排如何讲?- 考点:多仓适配、下游订单、状态轮询、有限重试。
- 回答思路:本地订单审核后生成下游订单任务,通过 SDK(软件开发工具包)适配不同仓库;创建响应不确定时进入查询队列,按下游状态更新本地订单,出库后触发费用处理,失败超过阈值进入异常人工处理。
- 进阶追问:下游创建超时后为什么不能直接重试创建?
3.4 上下游契约与防腐层
支付渠道、承运商和海外仓都属于外部依赖。它们的字段、状态、错误码、认证、限流和幂等能力不同,不能让差异渗透到核心业务。
推荐分层:
- Domain Service(领域服务):只处理本地统一模型和业务规则。
- Port(端口接口):定义支付、退款、查单、创建仓单、取消仓单、拉面单、查轨迹等能力。
- Adapter(适配器):实现 Airwallex(空中云汇)、Stripe(国际支付网关)、PayPal(国际支付平台)、具体仓库和承运商协议。
- Anti-Corruption Layer(防腐层):做字段映射、状态映射、金额单位转换、错误分类和敏感日志脱敏。
统一接口返回值至少包含:
- 本地请求号和外部请求号。
- 是否已受理,而不是只返回成功或失败。
- 外部状态、统一状态和原始错误码。
- 是否建议重试、建议重试时间、是否需要主动查询。
- 可脱敏保存的原始响应摘要。
错误应分为三类:
| 错误类型 | 示例 | 处理方式 |
|---|---|---|
| 业务不可重试 | 余额不足、地址非法、订单已取消 | 直接失败并提示修正 |
| 技术可重试 | 网络超时、限流、临时不可用 | 指数退避和最大次数 |
| 结果不确定 | 请求超时但对方可能已成功 | 禁止直接重做,先按业务号主动查询 |
热门面试题
- 问题(基础题):什么是 Anti-Corruption Layer(防腐层)?
- 考点:外部协议隔离、领域模型稳定。
- 回答思路:Anti-Corruption Layer(防腐层)把外部字段、状态和错误映射为内部统一模型,避免核心业务直接依赖某个渠道协议。
- 进阶追问:状态映射表应该放在哪里维护?
- 问题(原理题):外部调用超时为什么不能一律重试?
- 考点:结果不确定、重复扣款、重复下单。
- 回答思路:超时只表示调用方没收到结果,不代表服务端没执行。支付和创建仓单属于有副作用操作,应携带幂等业务号,超时后先查单再决定是否重试。
- 进阶追问:对方不支持幂等键也不支持查单怎么办?
- 问题(项目题):
hall-next和hiwi-unify如何避免每接一个仓库就改核心流程?- 考点:SDK(软件开发工具包)抽象、模板方法、统一上下文。
- 回答思路:核心业务只调用统一 SDK(软件开发工具包)接口,各仓库实现创建、取消、查询、面单和库存同步;公共校验、日志和状态推进放在基类或业务编排层。
- 进阶追问:某个仓库需要特殊字段时如何扩展而不污染公共模型?
4. 底层原理与关键设计
4.1 多支付渠道接入
hop 和 nest2 都体现了“接口 + 抽象基类 + 工厂”的渠道化设计。统一接口负责支付、取消、退款、查退款、配置校验和令牌刷新;抽象基类负责配置解析、Access Token(访问令牌)缓存、并发刷新锁、请求公共参数;具体渠道只实现差异部分。
classDiagram
class PaymentFactory["PaymentFactory(支付工厂)"]
class PaymentPort["PaymentPort(支付端口)"] {
+checkout()
+cancel()
+refund()
+query()
+verifyConfig()
}
class BasePaymentAdapter["BasePaymentAdapter(支付适配基类)"]
class AirwallexAdapter["Airwallex(空中云汇)适配器"]
class StripeAdapter["Stripe(国际支付网关)适配器"]
class PayPalAdapter["PayPal(国际支付平台)适配器"]
class PhotonPayAdapter["PhotonPay(光子易支付)适配器"]
PaymentFactory --> PaymentPort
PaymentPort <|.. BasePaymentAdapter
BasePaymentAdapter <|-- AirwallexAdapter
BasePaymentAdapter <|-- StripeAdapter
BasePaymentAdapter <|-- PayPalAdapter
BasePaymentAdapter <|-- PhotonPayAdapter设计要点:
- 工厂按渠道编码路由,不能在业务层写大量
if/else。 - 渠道配置保存前先校验必填项和连通性;密钥加密存储,日志只打印脱敏摘要。
- Access Token(访问令牌)提前刷新并设置安全时间窗,避免刚取出就过期。
- 多实例刷新令牌时使用分布式锁,但锁外仍要二次检查缓存,防止重复刷新。
- 每次外部调用记录本地请求号、外部请求号、耗时、结果分类和可重试标识。
热门面试题
- 问题(基础题):为什么支付渠道适合使用工厂和适配器模式?
- 考点:开闭原则、协议隔离。
- 回答思路:业务流程稳定,但渠道协议不断变化。工厂负责按渠道选择实现,适配器负责差异转换,新接渠道只新增实现,不修改核心交易逻辑。
- 进阶追问:渠道特有功能如何暴露?
- 问题(原理题):Access Token(访问令牌)缓存为什么还要加锁?
- 考点:缓存击穿、并发刷新、多实例。
- 回答思路:令牌过期瞬间多个实例可能同时刷新,造成渠道限流或令牌互相覆盖。使用分布式锁并在获得锁后二次检查缓存。
- 进阶追问:持锁实例宕机怎么办?
- 问题(项目题):
nest2多渠道接入最值得讲的设计是什么?- 考点:统一支付服务、渠道状态映射、回调重试。
- 回答思路:将 Stripe(国际支付网关)、PayPal(国际支付平台)、PhotonPay(光子易支付)等统一为支付接口,同时保留渠道会话表;支付结果不仅靠回调,还通过任务主动查询并重试。
- 进阶追问:如果 Stripe(国际支付网关)和 PayPal(国际支付平台)的状态粒度不同,怎么统一?
4.2 支付发起、回调与主动查单
可靠支付链路必须同时拥有“正向发起、异步通知、主动查询、定时对账”四条路径。
sequenceDiagram
participant U as 用户
participant O as 订单服务
participant P as 支付服务
participant C as 第三方渠道
participant T as 补偿任务
U->>O: 提交支付
O->>P: 创建 Payment Order(支付单)
P->>P: 保存支付单和幂等键
P->>C: 创建 Payment Intent(支付意图)
C-->>P: 返回会话与收银台信息
P-->>U: 返回支付页面
C-->>P: Webhook(回调通知)
P->>C: 主动查询真实状态
C-->>P: 返回金额、币种、状态
P->>P: 验签、核对、条件更新、记账
P-->>C: 返回受理成功
alt 长时间未收到回调
T->>C: 按渠道交易号主动查询
T->>P: 补偿推进状态
end回调处理顺序:
- 读取原始请求体和签名头,先验签再反序列化业务字段。
- 校验 Timestamp(时间戳)窗口、Nonce(随机数)或 Event ID(事件标识),防止重放。
- 记录回调事件唯一键;已处理事件直接返回成功。
- 按本地支付单号和渠道交易号查找记录。
- 主动查询渠道,核对商户、金额、币种、订单号和真实状态。
- 在本地事务中推进支付单、业务订单和账务流水。
- 提交事务后发送领域事件;下游消费端再次幂等。
- 无法确认时进入异常状态和延迟查询,不把“不确定”当失败。
热门面试题
- 问题(基础题):支付成功为什么不能只相信前端跳转?
- 考点:前端不可信、异步支付、结果伪造。
- 回答思路:前端跳转可能被篡改或中断,只能用于展示。最终支付结果必须来自验签后的渠道回调、主动查询或对账文件。
- 进阶追问:前端显示失败但实际扣款成功怎么办?
- 问题(原理题):收到 Webhook(回调通知)后为什么还要主动查单?
- 考点:纵深校验、金额核对、伪造防护。
- 回答思路:验签证明消息可能来自渠道,但主动查单能再次确认渠道侧最终状态、金额和币种,降低错误配置、事件乱序和验签实现缺陷风险。
- 进阶追问:查单接口也超时怎么办?
- 问题(项目题):
nest2回调重试任务如何讲?- 考点:延迟队列、重试次数、最大时长、分布式锁。
- 回答思路:支付创建后把渠道标识和充值单号写入延迟任务,消费者加业务锁后主动查询;未到终态则按退避时间重新入队,超过最大时长停止自动重试并记录异常供人工处理。
- 进阶追问:为什么不能无限重试?
4.3 分层幂等与并发控制
支付和履约不能依赖单一分布式锁。可靠幂等通常至少有五层:
| 层级 | 技术手段 | 解决的问题 | 仍需注意 |
|---|---|---|---|
| 请求层 | Idempotency Key(幂等键) | 客户端重复提交 | 键必须和业务主体绑定 |
| 数据层 | 唯一索引 | 防止重复单据和重复事件 | 捕获冲突后读取原结果 |
| 状态层 | 条件更新或版本号 | 防止乱序和并发覆盖 | 终态不可被迟到事件改写 |
| 并发层 | Redis(远程字典服务)分布式锁 | 减少同一业务并发执行 | 不能替代数据库正确性 |
| 账务层 | 唯一流水和借贷守恒 | 防止重复入账、重复扣款 | 流水与余额必须同事务 |
推荐唯一约束:
- 业务订单:
tenant_id + client_request_no。 - 支付单:
merchant_id + payment_order_no。 - 渠道交易:
channel_code + channel_transaction_id。 - 回调事件:
channel_code + event_id。 - 退款:
merchant_id + refund_order_no。 - 账务流水:
account_id + business_type + business_no + entry_type。 - 下游仓订单:
warehouse_id + client_order_no。 - 轨迹事件:
tracking_no + event_time + event_code + location_hash。
一个完整的支付成功事务应满足:
- 锁定或条件更新 Payment Order(支付单)。
- 判断当前是否已经成功,成功则直接幂等返回。
- 插入唯一渠道事件记录。
- 更新 Payment Order(支付单)和业务订单。
- 插入 Ledger Entry(账务分录)。
- 写入 Outbox Event(发件箱事件)。
- 事务提交后异步投递。
热门面试题
- 问题(基础题):接口幂等和防重复点击有什么区别?
- 考点:客户端体验、服务端正确性。
- 回答思路:前端防重复点击只能减少请求,不能处理超时重试、网络重放和回调重复。真正幂等必须在服务端通过唯一键、状态机和流水保证。
- 进阶追问:Idempotency Key(幂等键)应该保存多久?
- 问题(原理题):为什么有 Redis(远程字典服务)锁还需要唯一索引?
- 考点:锁失效、主从切换、最终防线。
- 回答思路:锁可能过期、误删或在故障切换中失效,数据库唯一索引是最终不变量。锁优化并发体验,唯一约束保证结果正确。
- 进阶追问:唯一索引冲突后接口应该返回失败还是原结果?
- 问题(项目题):下游仓创建订单如何做幂等?
- 考点:业务单号、先查后建、结果不确定。
- 回答思路:本地生成稳定的客户订单号并持久化,调用下游始终携带该号码;超时后先查询下游是否已存在,只有确认不存在才重建,并对重复返回做状态同步。
- 进阶追问:下游查询接口有延迟,暂时查不到怎么办?
4.4 余额账户、冻结与账务守恒
余额支付需要区分:
- Available Balance(可用余额):当前可以支付的金额。
- Frozen Balance(冻结余额):已经为订单预留但尚未确认扣减的金额。
- Account Balance(账户余额):可用余额与冻结余额等账户分项的汇总口径。
典型状态变化:
| 操作 | 可用余额 | 冻结余额 | 账务含义 |
|---|---|---|---|
| 充值到账 100 | +100 | 0 | 充值入账 |
| 订单预占 30 | -30 | +30 | 冻结资金 |
| 支付确认 | 0 | -30 | 冻结转消费 |
| 订单取消 | +30 | -30 | 解冻返回 |
| 退款 10 | +10 | 0 | 退款入账 |
并发扣款应使用数据库条件更新或行锁:
UPDATE balance_account
SET available_amount = available_amount - :amount,
frozen_amount = frozen_amount + :amount,
version = version + 1
WHERE id = :accountId
AND available_amount >= :amount
AND version = :version;资金不变量:
- 任意时刻余额不能小于零,除非业务明确支持授信。
- 每次余额变化必须存在唯一账务流水。
- 流水变动前金额 + 本次变动 = 变动后金额。
- 重放同一业务流水不能再次改变余额。
- 对账时“期初余额 + 入账 - 出账 = 期末余额”。
热门面试题
- 问题(基础题):余额支付为什么需要冻结金额?
- 考点:长事务、订单取消、并发占用。
- 回答思路:订单从创建到最终确认存在时间窗口,直接扣减不利于取消,完全不预占又会超用。冻结把资金预留和最终消费拆开。
- 进阶追问:冻结多久后自动释放?
- 问题(原理题):如何防止两个订单同时把余额扣成负数?
- 考点:条件更新、行锁、影响行数。
- 回答思路:使用
available_amount >= amount的原子条件更新,或事务内行锁读取并扣减;受影响行数为零表示余额不足或版本冲突。 - 进阶追问:为什么先查余额再更新不安全?
- 问题(项目题):
hop余额流水如何形成面试亮点?- 考点:业务锁、变动前后余额、审计。
- 回答思路:同一余额账户操作串行化,扣款或充值时记录业务单号、变动前后余额和原因;重复回调先检查业务状态和流水唯一键,避免重复入账。
- 进阶追问:余额更新成功但流水插入失败如何恢复?
4.5 取消、退款与冲正
取消和退款不是同一件事:
- Cancel(取消):交易尚未最终成功,关闭支付会话或撤销订单。
- Refund(退款):原交易已经成功,将部分或全部资金退回。
- Reversal(冲正):对账或系统故障发现错误记账后,用反向分录纠正,不直接删除原流水。
退款核心规则:
- 退款单号全局唯一。
- 退款必须关联原支付单和渠道交易号。
累计成功退款 + 处理中退款 <= 原成功支付金额。- 多个并发退款请求必须按原支付单串行或使用原子额度占用。
- 渠道退款成功后,本地退款单、业务单和账务分录在同一本地事务内推进。
- 退款通知丢失时主动查退款状态;结果不确定时不能重复创建新退款。
- 部分退款要明确手续费、优惠、税费和各订单明细的分摊规则。
hop 中退款状态区分申请、退款中、成功、失败和异常;在线充值退款还会关联余额和退款记录。这种设计的价值在于“退款请求”和“退款最终结果”被分开管理。
热门面试题
- 问题(基础题):取消支付和退款有什么区别?
- 考点:交易终态、资金是否已扣。
- 回答思路:取消针对未成功交易,退款针对已成功交易。取消通常关闭会话,退款会产生独立退款单和反向资金流水。
- 进阶追问:用户取消时渠道恰好支付成功怎么办?
- 问题(原理题):如何防止并发退款超过原支付金额?
- 考点:退款额度冻结、行锁、原子条件更新。
- 回答思路:按原支付单锁定退款额度,原子校验已退款和处理中金额,再创建退款单;退款失败时释放占用额度。
- 进阶追问:部分退款失败后重试是复用原退款单还是新建?
- 问题(项目题):为什么退款任务需要异常状态和人工入口?
- 考点:外部结果不确定、长尾故障。
- 回答思路:退款可能长时间处理中或渠道响应异常,自动重试不能无限进行。异常状态保留上下文,配合主动查询、人工重试和对账处理长尾问题。
- 进阶追问:人工操作如何避免再次退款?
4.6 对账、账单与结算
三个概念必须分清:
- Reconciliation(对账):比较双方记录,发现差异。
- Billing(出账):按账期和规则把费用流水聚合为账单。
- Settlement(结算):对已核对账单完成实际收付款和状态闭环。
hop 供应商财务链路体现了较完整的工程设计:
- 每日任务按供应商和账期筛选可出账费用。
- 只有已出库超过账期的订单费用、已退款费用和其他符合条件的费用进入账单。
- 任务先持久化并写入账单号,失败重入时先查账单是否已经生成。
- 费用流水按主键游标分批更新为已出账,避免条件变化导致分页跳行。
- 通过数据库聚合重新计算账单金额,保证重入时统计口径一致。
- 已核对账单才能申请 Settlement(结算)单,并设置最低结算金额。
- 结款完成后异步分批把费用流水更新为已支付,条件限定只处理待支付记录,实现幂等。
flowchart LR
A["业务事件"] --> B["Fee Record(费用流水)"]
B --> C["账期筛选"]
C --> D["Bill(账单)"]
D --> E["Reconciliation(对账)"]
E -->|一致| F["Settlement(结算单)"]
E -->|差异| G["差异单与复核"]
F --> H["审核"]
H --> I["实际结款"]
I --> J["费用流水标记已支付"]
J --> K["会计与银行对账"]对账差异分类:
| 差异 | 可能原因 | 处理 |
|---|---|---|
| 渠道成功、本地失败 | 回调丢失、事务失败 | 主动查单并补记 |
| 本地成功、渠道失败 | 错误状态推进 | 冻结业务并冲正 |
| 金额不一致 | 手续费、汇率、部分退款、精度 | 明细核对后调账 |
| 重复交易 | 幂等失效、渠道重试 | 保留一笔有效交易,其余退款 |
| 账单缺流水 | 出账任务中断、账期条件错误 | 任务重跑和差异补录 |
热门面试题
- 问题(基础题):对账和结算有什么区别?
- 考点:发现差异与实际付款。
- 回答思路:对账是核对双方记录是否一致,结算是在账单核对完成后执行资金收付。没有完成对账的账单不应进入结算。
- 进阶追问:对账一致是否代表账务一定正确?
- 问题(原理题):为什么账单任务要先保存任务和账单号?
- 考点:失败重入、幂等、长任务。
- 回答思路:长任务可能在更新部分费用后宕机。先持久化任务和确定性账单号,重启后可以识别已生成结果并继续处理,不会生成多个账单。
- 进阶追问:费用已标记出账但账单尚未插入时怎么办?
- 问题(项目题):供应商账单为什么按主键游标分页?
- 考点:扫描条件变化、分页遗漏。
- 回答思路:处理后费用状态会从待出账变为已出账,如果使用页码分页,结果集收缩会跳过数据。按主键递增游标读取可以稳定推进。
- 进阶追问:任务运行期间新费用写入怎么办?
4.7 履约编排与库存一致性
海外仓订单主链路建议采用 SAGA(长事务模式)思路:每一步提交本地结果,失败后执行对应补偿,不用一个数据库事务跨越外部仓库。
flowchart TD
A["创建客户订单"] --> B["校验地址、商品、仓库"]
B --> C["冻结库存并写库存流水"]
C --> D["创建 Fulfillment Order(履约订单)"]
D --> E["写入下游创建任务"]
E --> F["调用仓库 SDK(软件开发工具包)"]
F -->|明确成功| G["保存下游订单号"]
F -->|结果不确定| H["进入主动查询队列"]
F -->|明确失败| I["有限重试或异常单"]
G --> J["轮询下游状态"]
H --> J
J --> K["拣货、打包、出库"]
K --> L["冻结转实际扣减"]
L --> M["生成出库费用和物流任务"]
I --> N["释放库存或人工补偿"]库存变化必须使用三张逻辑视图:
- 库存快照:现有、可用、冻结、在途等数量。
- 库存流水:每次冻结、扣减、释放、入库的业务事实。
- 业务占用:某个订单具体占用了哪些 SKU(库存单位)和数量。
外部仓库同步不能直接覆盖本地库存。应保存下游原始快照,计算差异并生成同步流水;若差异超过阈值,进入告警或人工确认,避免错误数据瞬间污染全部可售库存。
热门面试题
- 问题(基础题):为什么订单履约不适合使用一个跨系统强事务?
- 考点:外部系统、长事务、可用性。
- 回答思路:第三方仓库不参与本地事务,网络调用时间长且可能失败。应通过本地事务、状态机、任务和补偿实现最终一致。
- 进阶追问:冻结库存成功但创建下游订单失败怎么办?
- 问题(原理题):下游创建订单超时后的正确处理是什么?
- 考点:结果不确定、业务幂等、主动查询。
- 回答思路:保存超时上下文并进入查询任务,使用稳定业务单号查询下游;确认不存在后才能重试创建,确认存在则补录下游单号并继续状态同步。
- 进阶追问:查询长期返回不存在但创建可能仍在异步处理中怎么办?
- 问题(项目题):
hiwi-unify多仓履约的难点是什么?- 考点:状态映射、下游差异、重试阈值、人工异常。
- 回答思路:不同仓库对创建、取消、面单和状态的定义不同,因此用统一 SDK(软件开发工具包)和下游订单模型隔离差异;可重试错误延迟重试,超过阈值转异常单,避免无限重试拖垮系统。
- 进阶追问:如何判断一个错误可不可以重试?
4.8 面单生成与物流轨迹
面单链路通常有三种来源:
- 客户上传:系统校验文件、跟踪号和订单归属。
- 物流中台生成:
hall-next调用承运商或聚合商创建订单并异步拉取面单。 - 海外仓返回:
hiwi-unify创建下游订单后从仓库获取面单。
面单属于异步资源,创建订单成功不代表面单立即可用。hall-next 将下单和拉面单拆成队列任务,维护下单中、拉面单中、成功、失败、重试和延迟等待等状态,并使用业务锁防止同一订单被多个任务并发处理。
轨迹同步采用“订阅推送 + 主动拉取 + 定时补偿”:
- 首次拿到跟踪号后推送到轨迹平台订阅。
- Webhook(回调通知)到达后验签、标准化状态并幂等保存事件。
- 长时间无更新的运输中订单进入主动拉取。
- 已签收、退件等终态降低或停止轮询。
- 对事件按发生时间排序,但主状态更新必须遵守状态优先级,避免乱序回退。
轨迹事件唯一键不能只用跟踪号,因为一个跟踪号有多个节点。推荐组合:跟踪号、事件时间、状态码、地点和描述摘要。
热门面试题
- 问题(基础题):为什么面单拉取要从下单主链路拆开?
- 考点:外部异步、响应时间、重试隔离。
- 回答思路:承运商可能先受理订单,稍后才生成面单。拆开后主请求快速返回受理状态,后台任务独立重试,不占用 Web(万维网)请求线程。
- 进阶追问:用户一直看不到面单时如何提示?
- 问题(原理题):物流轨迹乱序怎么处理?
- 考点:事件时间、状态优先级、终态保护。
- 回答思路:明细事件按发生时间去重保存,订单主状态按状态机优先级推进;迟到的早期事件可以补入明细,但不能把已签收回退为运输中。
- 进阶追问:承运商修正错误签收状态怎么办?
- 问题(项目题):
hall-next的队列设计有什么价值?- 考点:延迟队列、重试次数、锁、优先级。
- 回答思路:不同业务编码使用独立队列,任务可以延迟、重排、统计处理次数并加业务锁;成功后原子移除并解锁,失败按错误类型延迟或转人工。
- 进阶追问:队列消息丢失如何发现?
4.9 多源同步与最终一致性
支付和履约都不能只依赖一种同步方式。
| 方式 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 同步 API(应用程序接口) | 实时、交互简单 | 超时结果不确定 | 创建支付意图、提交仓单 |
| Webhook(回调通知) | 渠道主动推送、延迟低 | 重复、乱序、可能丢失 | 支付结果、轨迹更新 |
| 主动查询 | 能确认渠道真实状态 | 有调用成本和限流 | 超时补偿、回调校验 |
| MQ(消息队列) | 解耦、削峰、可重试 | 需要处理重复和积压 | 支付成功事件、履约任务 |
| 定时扫描 | 实现简单、兜底可靠 | 实时性较低 | 长时间中间态、漏单 |
| 对账文件或对账 API(应用程序接口) | 可发现历史差异 | 通常是次日或周期性 | 资金和结算最终兜底 |
中间态必须有超时扫描:
- 支付中超过阈值:查询渠道交易。
- 退款中超过阈值:查询渠道退款。
- 下发中超过阈值:查询下游仓订单。
- 拉面单中超过阈值:查询面单或重新拉取。
- 运输中长时间无轨迹:主动拉取或标记异常。
- 结算任务运行中超过阈值:读取任务进度并重入。
任何重试都要具备最大次数、最大总时长、退避、随机抖动和失败归宿。无限重试不是可靠性,而是制造事故。
热门面试题
- 问题(基础题):既然有 Webhook(回调通知),为什么还要定时任务?
- 考点:消息丢失、长尾故障、最终兜底。
- 回答思路:Webhook(回调通知)可能因网络、配置或服务故障丢失,定时扫描中间态并主动查询能发现漏单。
- 进阶追问:定时扫描如何避免全表扫描?
- 问题(原理题):重试如何避免形成重试风暴?
- 考点:指数退避、抖动、限流、熔断。
- 回答思路:按错误分类,只重试临时故障;采用指数退避和随机抖动,设置最大并发和最大时长,下游异常时熔断并转为低频补偿。
- 进阶追问:积压任务恢复时如何控制放量?
- 问题(项目题):
nest2和hiwi-unify的主动查询分别解决什么?- 考点:支付结果不确定、下游仓结果不确定。
- 回答思路:
nest2在支付回调缺失或状态同步中主动查询渠道;hiwi-unify在仓单创建和状态推进不确定时查询下游订单。二者本质都是用可查询事实消除网络超时的不确定性。 - 进阶追问:主动查询结果与回调冲突时信谁?
4.10 安全、风控与审计
支付安全必须建立纵深防御:
- HTTPS(安全超文本传输协议)保证传输加密,但不能代替应用层验签。
- 使用 HMAC(基于哈希的消息认证码)或渠道公钥验证原始请求体,不要对重新序列化后的对象验签。
- 校验 Timestamp(时间戳)窗口、Nonce(随机数)或 Event ID(事件标识)防重放。
- 主动查询并核对商户号、支付单号、金额、币种和收款账户。
- 密钥加密存储,按环境和商户隔离,支持轮换;日志禁止打印完整密钥、银行卡号和身份证件。
- 回调接口先落审计摘要,再执行业务;无论重复事件还是已处理终态,都应快速返回渠道期望响应。
- 高风险退款、结算和人工调账采用权限隔离、双人复核和操作留痕。
- 不自行保存银行卡敏感数据;涉及卡数据时遵守 PCI DSS(支付卡行业数据安全标准)的边界要求。
仅依赖 IP(互联网协议地址)白名单不够,因为地址可能变化、代理链可能复杂,且无法证明消息内容未被篡改。IP(互联网协议地址)限制可以作为附加防线,不能代替签名。
热门面试题
- 问题(基础题):支付回调验签解决什么问题?
- 考点:来源真实性、内容完整性。
- 回答思路:验签确认消息由持有密钥的一方产生且内容未被篡改,但仍需防重放和业务字段核验。
- 进阶追问:为什么验签成功仍不能直接入账?
- 问题(原理题):如何防止合法回调被重复播放?
- 考点:Timestamp(时间戳)、Nonce(随机数)、Event ID(事件标识)。
- 回答思路:限制 Timestamp(时间戳)有效窗口,保存 Nonce(随机数)或 Event ID(事件标识)唯一记录,并让业务处理本身幂等。
- 进阶追问:渠道没有 Event ID(事件标识)怎么办?
- 问题(项目题):结算和人工调账如何控制风险?
- 考点:权限隔离、审核、审计、冲正。
- 回答思路:申请、审核和付款角色分离;关键操作要求二次确认和凭证;不修改历史流水,错误通过冲正分录修复,并保留操作人、原因和前后数据。
- 进阶追问:紧急生产修数如何保证可追溯?
5. 架构图与流程图
5.1 支付与履约总体架构
flowchart LR
U["用户/客户"] --> G["Gateway(网关)"]
G --> O["订单服务"]
O --> P["支付服务"]
P --> PC["支付渠道适配层"]
PC --> AW["Airwallex(空中云汇)"]
PC --> ST["Stripe(国际支付网关)"]
PC --> PP["PayPal(国际支付平台)"]
O --> I["库存服务"]
O --> F["履约编排服务"]
F --> W["海外仓 SDK(软件开发工具包)"]
F --> H["面单轨迹中台"]
P --> L["Ledger(账务系统)"]
F --> FE["费用流水"]
L --> R["Reconciliation(对账)"]
FE --> B["Billing(账单)"]
B --> S["Settlement(结算)"]
P --> M["MQ(消息队列)"]
F --> M
M --> N["通知、报表、补偿任务"]5.2 支付成功后的业务联动时序
sequenceDiagram
participant C as 支付渠道
participant P as 支付服务
participant D as 数据库
participant M as MQ(消息队列)
participant O as 订单服务
participant F as 履约服务
C->>P: Webhook(回调通知)
P->>C: 主动查询交易
C-->>P: 成功、金额、币种
P->>D: 回调事件去重
P->>D: 支付单条件更新
P->>D: 写账务分录和 Outbox(发件箱)
D-->>P: 本地事务提交
P-->>C: 受理成功
P->>M: 发布支付成功事件
M->>O: 幂等更新订单
O->>F: 创建履约任务
F->>F: 冻结库存并下发仓库5.3 面单与轨迹异步流程
flowchart TD
A["履约订单已受理"] --> B{"面单来源"}
B -->|客户| C["校验并保存客户面单"]
B -->|物流中台| D["提交 hall-next 下单任务"]
B -->|下游仓| E["等待仓库生成面单"]
D --> F["异步拉取面单"]
E --> F
F -->|未生成| G["退避重试"]
F -->|成功| H["保存跟踪号和文件"]
H --> I["订阅轨迹平台"]
I --> J["Webhook(回调通知)更新"]
J --> K["轨迹事件去重与状态映射"]
K --> L["签收/异常通知"]
G --> F6. 表格对比
6.1 关键一致性方案对比
| 方案 | 优点 | 局限 | 支付与履约中的位置 |
|---|---|---|---|
| 数据库本地事务 | 强原子性 | 不能跨第三方 | 支付单、流水、Outbox(发件箱)同事务 |
| 唯一索引 | 最终防重可靠 | 只解决唯一性 | 回调事件、支付单、退款单、下游单 |
| 条件更新 | 防乱序和并发覆盖 | 要设计状态迁移 | 支付、退款、履约状态推进 |
| 分布式锁 | 降低并发冲突 | 可能失效 | 同账户扣款、同订单任务执行 |
| MQ(消息队列) | 解耦和削峰 | 至少一次投递会重复 | 支付成功后订单和履约联动 |
| 主动查询 | 消除结果不确定 | 有限流和成本 | 支付回调补偿、下游订单查询 |
| 对账 | 能发现历史差异 | 非实时 | 资金、账单、结算最终兜底 |
6.2 同步、回调与轮询对比
| 场景 | 首选方式 | 兜底方式 | 幂等键 |
|---|---|---|---|
| 创建支付 | 同步 API(应用程序接口) | 按支付单号查单 | 支付单号 |
| 支付结果 | Webhook(回调通知) | 主动查询与对账 | 渠道事件号、渠道交易号 |
| 创建仓单 | 同步 API(应用程序接口) | 按客户订单号查询 | 仓库 + 客户订单号 |
| 获取面单 | 延迟任务 | 人工重拉 | 履约单号 + 面单版本 |
| 物流轨迹 | Webhook(回调通知) | 定时拉取 | 跟踪号 + 事件特征 |
| 供应商结算 | 任务和人工审核 | 对账差异单 | 供应商 + 账期 + 结算批次 |
6.3 现有项目能力与建议补强
| 项目 | 已体现能力 | 建议进一步补强 |
|---|---|---|
hop | 渠道工厂、在线充值、余额流水、退款、账单和结算 | 统一支付与费用状态、补全回调事件唯一表、强化双分录账务 |
nest2 | 多渠道支付、渠道会话、回调重试、主动查单 | 统一重试框架、完善死信与告警、引入对账文件闭环 |
hall-next | 多承运商、面单队列、轨迹平台、扣费退款 | 强化任务持久化审计、轨迹事件唯一约束、队列积压指标 |
hiwi-unify | 多仓 SDK(软件开发工具包)、下游订单、库存同步、状态轮询 | 统一错误分类、补偿工作台、库存差异自动对账 |
7. 数据演绎
7.1 重复支付回调演绎
假设支付单 P1001 金额为 100.00 USD(美元),渠道事件 E9001 连续到达三次。
| 时刻 | 操作 | 数据结果 |
|---|---|---|
| T1 | 第一次回调验签、查单成功 | 插入事件 E9001,支付单由支付中变成功,写入账务流水 +100 |
| T2 | 第二次回调插入事件 | 唯一索引冲突,读取支付单已成功,直接返回成功 |
| T3 | 第三次回调绕过事件表并进入状态更新 | 条件更新影响 0 行,不再写账务流水 |
正确结果:业务订单只支付一次,账户只入账一次,三次回调都向渠道返回成功,避免渠道继续重试。
7.2 支付接口超时但渠道成功演绎
- 本地创建支付单
P1002并调用渠道。 - 渠道成功创建交易,但响应在网络中丢失。
- 本地将状态记为异常,不记为失败,也不立即创建新支付单。
- 补偿任务按
P1002主动查询,查到渠道交易T8002已成功。 - 本地验证金额币种后补记成功和账务流水。
如果错误地把超时当失败并重新扣款,可能产生两笔成功交易。因此“异常”是支付状态机中的必要状态。
7.3 余额并发扣款演绎
账户可用余额为 80,订单 A 扣 60,订单 B 扣 40。
| 方案 | A 读取 | B 读取 | 最终风险 |
|---|---|---|---|
| 先查再更新 | 80 | 80 | 两者都认为余额足够,可能扣成 -20 |
| 原子条件更新 | 成功,余额变 20 | 条件不满足,更新 0 行 | 只有 A 成功,不超扣 |
7.4 部分退款演绎
原支付金额 100,第一次申请退款 30,第二次并发申请退款 80。
- 第一次在事务中占用退款额度
30。 - 第二次校验
已成功 0 + 处理中 30 + 本次 80 > 100,拒绝创建。 - 第一次渠道退款成功后,写
+30退款分录,支付单标记部分退款。 - 后续最多还能退款
70。
7.5 下游仓创建超时演绎
本地订单 O7001 调用海外仓创建,接口超时:
- 本地保持“下发中”,记录请求摘要和稳定客户订单号。
- 三十秒后按客户订单号查询下游。
- 若查到下游单
W8801,补录映射并进入待出库。 - 若明确不存在且超过下游异步受理窗口,才允许复用同一业务号重试创建。
- 超过最大次数进入异常工作台,不释放库存前必须确认下游确实没有订单。
7.6 账单任务宕机重入演绎
任务要处理费用流水 1..250,每批 100 条:
- 第一批和第二批已更新为已出账,任务在插入账单前宕机。
- 重启读取同一个任务和账单号,只扫描仍为待出账且主键大于游标的记录。
- 全部处理后按账单号从数据库重新聚合金额并插入账单。
- 如果账单已存在,直接把任务标为成功,不重复生成。
8. 线上排查与实战经验
8.1 渠道已扣款但本地未到账
排查顺序:
- 按业务订单号、支付单号、渠道交易号定位支付单和会话。
- 检查回调访问日志、验签结果和事件去重记录。
- 调用渠道查单确认商户、金额、币种和最终状态。
- 检查本地事务是否回滚、账务流水是否存在、Outbox(发件箱)是否投递。
- 若渠道成功且本地失败,走补记流程,不直接修改余额字段。
- 记录事故原因并扩大同时间窗差异扫描。
8.2 重复扣款或重复入账
重点核对:支付单与渠道交易是不是一对多、账务流水唯一键是否缺失、回调和主动查询是否并发执行、状态条件更新是否过宽、人工补单是否绕过领域服务。
止损顺序:冻结相关自动任务、标记重复交易、计算受影响订单、对多余渠道交易发起退款、使用冲正分录修复账务,禁止直接删除流水。
8.3 退款长期处理中
先查渠道退款真实状态,再查本地退款单、原支付单累计退款额、退款任务重试记录和回调日志。渠道成功则补记本地结果;渠道失败则释放处理中额度;渠道仍处理中则降低查询频率并设置人工关注时限。
8.4 下游仓重复订单或状态不推进
按本地订单号、下游订单号和客户幂等号串联日志。确认创建超时后是否直接重试、下游是否支持按客户单号查单、任务锁是否失效。状态不推进时检查查询队列、错误分类、重试次数和状态映射,不能只手工改主订单状态。
8.5 面单拉取或轨迹长期无更新
检查任务是否仍在队列、队列分数是否到期、业务锁是否遗留、承运商响应是否为“尚未生成”还是明确失败、文件存储是否成功。轨迹问题还要检查订阅状态、Webhook(回调通知)验签、状态映射和终态过滤。
8.6 账单或结算金额不一致
按“结算单→账单→费用流水→业务订单”逐层下钻,核对账期、费用类型、退款、手续费、汇率和舍入。对比任务统计数量与实际流水数量,检查是否存在费用已出账但账单缺失,或结款完成但费用仍待支付的中间态。
9. 项目落地话术
9.1 hop 支付交易与资金闭环
背景上,系统既支持卖家余额支付,也支持 Airwallex(空中云汇)在线支付,还涉及充值、订单付款、退款、供应商账单和结算。难点不是调通一个接口,而是让业务单、支付单、渠道会话、余额流水和费用流水在重复回调、超时和任务重入下仍然一致。设计上我把支付渠道抽象成统一接口,由工厂按渠道编码路由;每次支付先落 Payment Order(支付单),再创建渠道会话;回调后主动查单,按金额、币种和状态核验,再通过条件更新和唯一流水实现幂等。供应商费用先沉淀为费用流水,按账期异步生成账单,核对通过后申请结算,结款后分批更新费用状态。风险上重点防重复支付、重复入账、超额退款和账单漏算,兜底是主动查询、任务重入、对账和人工审核。
9.2 nest2 多渠道支付
nest2不只有余额充值,还接入了 Stripe(国际支付网关)、PayPal(国际支付平台)、PhotonPay(光子易支付)等第三方渠道。为了避免业务层感知渠道差异,我用统一支付服务接口和渠道适配器封装下单、令牌、会话和状态映射。支付创建后不把同步响应当最终成功,渠道回调经过校验后推进充值单;如果回调未到或状态仍同步中,就把渠道订单号和充值单号放入延迟任务,使用分布式锁串行主动查单,按退避策略重试并设置最大总时长。这样能够处理回调丢失、网络超时和重复通知,同时避免无限重试。
9.3 hall-next 面单与轨迹中台
面单和轨迹的特点是外部接口慢、状态异步且承运商差异大。
hall-next将订单下发、面单拉取、轨迹订阅和扣费退款拆成独立任务,通过统一 SDK(软件开发工具包)适配不同承运商。任务按业务编码隔离,支持延迟、重试次数和分布式锁;面单未生成时退避重试,明确失败时转异常。拿到跟踪号后订阅轨迹平台,同时保留主动拉取兜底;轨迹明细按事件特征幂等,主状态按优先级推进,避免乱序把已签收回退为运输中。
9.4 hiwi-unify 海外仓订单履约
海外仓履约跨越本地订单、库存和多个第三方仓库,无法用一个强事务解决。我把流程拆成审核、冻结库存、创建下游订单、状态查询、出库确认、库存扣减和费用生成。不同仓库通过 SDK(软件开发工具包)适配,外部创建超时不直接重做,而是按稳定客户订单号主动查单;可重试错误进入延迟队列,超过阈值转异常工作台。下游出库后再把冻结转为实际扣减并生成费用,取消时先确认下游取消结果再释放库存,避免本地释放但仓库继续出库。
9.5 对账与结算亮点
财务链路采用“费用流水、账单、结算单”三级模型。业务事件先形成不可变费用流水,账单任务按供应商账期筛选可结算记录,并使用持久化任务、确定性账单号和主键游标支持失败重入。账单核对通过后才能进入结算,申请、审核和结款分离;结款完成后异步分批更新费用流水,查询条件只选择待支付记录以保证幂等。最后通过期初、发生额、期末余额和渠道账单做差异核对。
10. 高频面试题与追问
10.1 如何设计一个支付系统?
- 问题:如何设计一个支付系统?
- 考点:领域模型、状态机、渠道、账务、对账。
- 回答思路:先拆业务订单、支付单、渠道交易、退款单和账务流水,再设计渠道适配、回调与查单、幂等状态机、消息联动、对账补偿和安全审计。
- 进阶追问:哪些数据必须在同一本地事务中?
10.2 为什么支付单和业务订单必须分离?
- 问题:为什么支付单和业务订单必须分离?
- 考点:多次支付、渠道切换、职责边界。
- 回答思路:一个业务订单可能多次发起支付,但只能有一个有效支付结果;分离能独立管理支付尝试和订单生命周期。
- 进阶追问:如何防止两个支付单同时成功?
10.3 支付回调如何保证幂等?
- 问题:支付回调如何保证幂等?
- 考点:事件唯一键、状态条件更新、账务流水。
- 回答思路:渠道事件号唯一入库,支付单只允许从中间态条件更新到成功,账务流水设置业务唯一键,重复请求读取原结果并返回成功。
- 进阶追问:渠道没有事件号怎么办?
10.4 支付接口超时怎么办?
- 问题:支付接口超时怎么办?
- 考点:结果不确定、查单、补偿。
- 回答思路:不能直接判失败或重新扣款,应把支付单标为异常或处理中,按幂等业务号主动查询渠道,确认不存在后才允许重试。
- 进阶追问:查单也一直超时怎么办?
10.5 如何防止回调伪造和重放?
- 问题:如何防止回调伪造和重放?
- 考点:验签、Timestamp(时间戳)、Nonce(随机数)、唯一事件。
- 回答思路:对原始请求体验签,限制时间窗口,保存唯一事件或随机数,再主动查询核对金额、币种、商户和订单。
- 进阶追问:HTTPS(安全超文本传输协议)是否已经足够?
10.6 余额扣款如何避免超扣?
- 问题:余额扣款如何避免超扣?
- 考点:条件更新、行锁、余额流水。
- 回答思路:使用余额大于等于扣款额的原子更新,受影响行数决定成功;余额和唯一流水在同一事务中完成。
- 进阶追问:高并发下是否还需要分布式锁?
10.7 支付成功事件如何可靠通知订单服务?
- 问题:支付成功事件如何可靠通知订单服务?
- 考点:Outbox Pattern(发件箱模式)、MQ(消息队列)、消费幂等。
- 回答思路:支付状态和 Outbox(发件箱)记录同事务提交,后台可靠投递 MQ(消息队列),订单服务按支付单号幂等消费。
- 进阶追问:消息发送成功但确认丢失怎么办?
10.8 如何设计部分退款?
- 问题:如何设计部分退款?
- 考点:退款额度、分摊、并发。
- 回答思路:退款单关联原支付,原子占用可退额度,累计成功与处理中退款不能超过原支付金额;明细按确定规则分摊并处理舍入差。
- 进阶追问:优惠券和手续费退不退?
10.9 退款回调丢失怎么办?
- 问题:退款回调丢失怎么办?
- 考点:主动查询、异常状态、对账。
- 回答思路:退款单保持处理中,延迟任务按渠道退款号查询,长期不确定则进入异常人工处理,并通过渠道对账兜底。
- 进阶追问:是否可以重新创建一个退款?
10.10 对账系统如何设计?
- 问题:对账系统如何设计?
- 考点:单边账、差异分类、补偿。
- 回答思路:获取渠道账单或查询结果,与本地支付、退款和账务流水按交易号匹配,生成长款、短款、金额差和状态差异,自动补偿可确定问题,其余进入人工复核。
- 进阶追问:本地没有渠道交易号怎么匹配?
10.11 账单任务如何保证失败重入?
- 问题:账单任务如何保证失败重入?
- 考点:持久化任务、确定性编号、游标分页。
- 回答思路:先持久化任务和账单号,处理流水时按主键游标分批推进,重启先检查账单是否存在,最终按数据库结果重新聚合。
- 进阶追问:任务执行期间新增费用如何隔离?
10.12 账单、对账和结算的区别是什么?
- 问题:账单、对账和结算的区别是什么?
- 考点:费用聚合、差异核对、实际付款。
- 回答思路:账单是费用聚合,对账是双方记录核对,结算是对核对通过的账单完成实际收付款。
- 进阶追问:为什么未核对账单不能结算?
10.13 如何设计海外仓订单履约?
- 问题:如何设计海外仓订单履约?
- 考点:订单拆分、库存、下游仓、状态机。
- 回答思路:客户订单拆为履约单,冻结库存后异步下发仓库,保存下游单号并轮询状态,出库后扣减库存和计费,取消或失败走补偿。
- 进阶追问:一个订单跨两个仓库怎么办?
10.14 下游创建订单超时怎么处理?
- 问题:下游创建订单超时怎么处理?
- 考点:业务幂等、主动查询、重复下单。
- 回答思路:保持下发中,使用稳定客户订单号查询下游,确认不存在后再重试;无法确认时进入异常,不盲目重复创建。
- 进阶追问:下游不支持按客户订单号查询怎么办?
10.15 如何保证库存冻结和订单一致?
- 问题:如何保证库存冻结和订单一致?
- 考点:本地事务、库存流水、补偿。
- 回答思路:订单初始状态、库存冻结和库存流水同事务;后续下游失败通过补偿释放,定时对账检查订单占用与冻结快照。
- 进阶追问:补偿释放失败怎么办?
10.16 为什么面单获取要异步化?
- 问题:为什么面单获取要异步化?
- 考点:外部延迟、任务重试、请求线程。
- 回答思路:承运商往往先受理订单再生成面单,异步任务允许独立退避重试并快速释放用户请求线程。
- 进阶追问:如何防止同一订单重复拉取?
10.17 物流轨迹如何处理重复和乱序?
- 问题:物流轨迹如何处理重复和乱序?
- 考点:事件唯一键、事件时间、终态保护。
- 回答思路:轨迹明细按组合键去重保存,主状态按状态机优先级推进,迟到事件只补明细,不回退已签收等终态。
- 进阶追问:承运商撤销签收状态怎么办?
10.18 外部系统错误如何分类?
- 问题:外部系统错误如何分类?
- 考点:不可重试、可重试、结果不确定。
- 回答思路:参数和业务拒绝不可重试,网络和临时限流可退避重试,超时等结果不确定错误必须先查单。
- 进阶追问:HTTP 500(服务器内部错误)一定可重试吗?
10.19 如何避免重试风暴?
- 问题:如何避免重试风暴?
- 考点:退避、抖动、限流、熔断。
- 回答思路:按错误分类,指数退避加随机抖动,限制单渠道并发和总重试时长,下游故障时熔断并转低频补偿。
- 进阶追问:服务恢复后积压任务如何放量?
10.20 支付和履约如何做可观测性?
- 问题:支付和履约如何做可观测性?
- 考点:TraceId(链路标识)、指标、审计日志。
- 回答思路:用业务订单号、支付单号、渠道交易号、履约单号贯穿日志;监控成功率、处理中时长、回调延迟、重试量、队列积压、对账差异和异常单。
- 进阶追问:最重要的三个告警是什么?
10.21 如何处理支付成功但订单取消?
- 问题:如何处理支付成功但订单取消?
- 考点:事件竞态、状态机、自动退款。
- 回答思路:支付成功事实不能抹掉;订单若已不可履约,应进入已支付待退款或退款中,创建唯一退款单并保留完整审计链。
- 进阶追问:库存已经释放又恢复订单是否可行?
10.22 如何处理本地状态与渠道状态冲突?
- 问题:如何处理本地状态与渠道状态冲突?
- 考点:事实源、终态校验、对账。
- 回答思路:渠道支付事实和本地账务事实分别保留,通过主动查询确认渠道结果,再按状态机补记或冲正,不能直接覆盖历史流水。
- 进阶追问:渠道自己也发生状态修正怎么办?
10.23 多币种结算要注意什么?
- 问题:多币种结算要注意什么?
- 考点:币种、汇率、精度、汇兑损益。
- 回答思路:原币金额、结算币金额、汇率和汇率时间必须分别保存;每种币种独立对账,汇兑差额单独记账,不能把不同币种直接相加。
- 进阶追问:退款使用原汇率还是退款日汇率?
10.24 为什么不能直接修改资金流水?
- 问题:为什么不能直接修改资金流水?
- 考点:审计、不可变事实、冲正。
- 回答思路:历史流水是资金事实,直接修改会破坏审计和对账。错误应新增等额反向冲正分录,再按正确业务重新记账。
- 进阶追问:余额快照和流水不一致时以谁为准?
10.25 如何评价支付与履约系统是否健康?
- 问题:如何评价支付与履约系统是否健康?
- 考点:正确性、时效、可恢复性、可追溯性。
- 回答思路:不仅看成功率,还看资金差异、重复率、中间态时长、补偿成功率、队列积压、下游异常、面单时效、出库时效和人工异常量;任何一笔交易都应可追溯和可恢复。
- 进阶追问:如果只能先治理一个指标,你选哪个?
11. 本模块复习清单
- 能解释业务订单、Payment Order(支付单)、Payment Session(支付会话)、渠道交易和 Refund Order(退款单)的边界。
- 能画出支付发起、Webhook(回调通知)、主动查单、入账和消息通知时序图。
- 能用唯一索引、条件更新、分布式锁和账务流水解释分层幂等。
- 能演绎重复回调、支付超时、并发余额扣款和部分退款。
- 能解释 Available Balance(可用余额)、Frozen Balance(冻结余额)和 Ledger Entry(账务分录)。
- 能区分 Cancel(取消)、Refund(退款)和 Reversal(冲正)。
- 能讲清 Reconciliation(对账)、Billing(出账)和 Settlement(结算)的关系。
- 能说明账单任务为什么使用持久化任务、确定性编号和主键游标。
- 能解释海外仓订单、库存冻结、下游订单、面单、轨迹和出库计费的完整链路。
- 能说明外部调用结果不确定时为什么必须先查单。
- 能设计面单异步拉取和轨迹重复、乱序处理。
- 能讲
hop的在线支付、余额、退款和供应商结算设计。 - 能讲
nest2的多渠道支付和回调重试设计。 - 能讲
hall-next的面单轨迹中台设计。 - 能讲
hiwi-unify的多仓履约与异常补偿设计。 - 能给出渠道已扣款本地未到账、重复入账、退款卡住和结算差异的排查路径。
- 能说明验签、防重放、金额币种核验、密钥保护和人工调账审计。
- 能在三分钟内完成一段“支付资金一致性 + 订单履约”的项目串讲。
