面试知识

支付交易、资金一致性与订单履约:精通级入口

40-支付资金一致性与项目话术 面试知识整理。

支付交易、资金一致性与订单履约:精通级入口

本页是 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. 推荐学习顺序

  1. 先读 4.1.0,统一事实等级、资金/库存/履约/物流不变量及旧资产口径。
  2. 4.1.1 → 4.1.2 → 4.1.3 → 4.1.4 建立“交易建模—渠道确认—内部账务—退款对账结算”的资金闭环。
  3. 4.1.5 → 4.1.6 建立“订单库存—海外仓—面单轨迹—异常恢复”的履约闭环。
  4. 最后读 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.14.1.24.1.34.1.4 的状态机、演绎和排障如何从支付发起讲到回调、查单、记账、退款、对账与结算
45 分钟4.1.54.1.6 的未知态、补偿与终态保护如何处理库存竞争、下游仓超时、面单失败和轨迹乱序
60 分钟直接练习 4.1.7 项目串讲与综合题库如何按背景、约束、设计、风险、降级、证据、恢复与边界完成项目口述

6. 迁移说明

本次采用“旧内容保留,分册深化与交叉引用”,不是物理迁移。下方原文不删除、不移动、不改写;4.1.0 的账本负责把旧资产分配给唯一责任分册,新分册以更完整的状态、失败分支、表格、数据演绎、排障和项目话术深化主题,其他位置通过链接复用。

旧资产01020304050607复算合计
### 标题564863739
旧题9108121251167
旧 Mermaid(图表语法)图22012119
旧表113121413
旧数据演绎02121006
旧排障段01121106
旧项目话术段00000055
可枚举旧资产17221726241128145

复算依据为 legacy-40-h01—h39legacy-40-q01—q67legacy-40-g01—g09legacy-40-t01—t13legacy-40-d01—d06legacy-40-o01—o06legacy-40-p01—p05,共 145 个唯一稳定标识,无重复;计算式为 39 + 67 + 9 + 13 + 6 + 6 + 5 = 1454.1.0 已同步修正汇总口径,旧内容继续保留,分册承担深化与交叉引用,不伪称物理迁移。收口前原根 SHA-256(安全散列算法)为 113a1eaf3151f776025f3dcdcc95980954641c51d716926b908813c0b53b328e

7. 旧版兼容材料

以下内容从原始标题开始完整保留,继续承担旧链接兼容、迁移反向核对和原始学习材料留档职责。


支付交易、资金一致性与订单履约

1. 简历关联点

本模块基于以下真实项目能力整理,目标是把“做过支付、订单和仓储”提升为能够完整讲清业务边界、状态流转、资金正确性、上下游协同和异常补偿的高级面试表达。

项目业务定位可提炼的核心能力
hop-javahop-java2hop-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. 面试主线

回答支付交易和订单履约问题时,建议始终沿下面七层展开:

  1. 业务层:用户购买什么、向谁付款、谁负责履约、何时确认收入和成本。
  2. 单据层:业务订单、支付单、渠道会话、退款单、余额流水、费用流水、账单、结算单分别解决什么问题。
  3. 状态层:状态只能单向合法流转,重复、乱序和延迟事件不能把终态改坏。
  4. 一致性层:数据库事务保证本地原子性,唯一约束和状态条件更新保证幂等,消息和补偿保证最终一致。
  5. 接入层:通过适配器隔离不同支付渠道、承运商和海外仓的协议差异。
  6. 风控层:验签、防重放、金额币种核验、敏感信息保护、审计日志和权限控制。
  7. 兜底层:主动查询、延迟重试、对账、补偿任务、人工工单和可观测性。

一段适合面试开场的总述:

我理解支付和履约不是两个孤立模块。支付解决“钱是否真实到账”,履约解决“商品是否按约交付”,中间依靠订单状态机连接。设计时我会把业务订单、支付单、渠道交易、账务流水、退款单、履约单和结算单分开建模;同步请求只表达“受理”,最终结果以渠道回调、主动查询和对账为准;所有异步入口都要求幂等,并通过状态机、唯一约束、流水和补偿任务保证资金和履约最终一致。

3. 基础知识

3.1 支付交易领域模型

支付系统不能只建一张“充值表”或在订单上放一个支付状态。不同单据承担不同职责,分离后才能支持重复支付、换渠道、部分退款、审计和对账。

领域对象核心职责关键唯一标识典型状态
业务订单表达用户购买或充值意图业务订单号待支付、已支付、取消、履约中、完成
Payment Order(支付单)表达一次应付请求,可聚合一个或多个业务订单支付单号申请、处理中、成功、失败、取消、异常
Payment Session(支付会话)记录一次具体渠道交互渠道会话号待支付、成功、取消、异常、退款
Channel Transaction(渠道交易)记录第三方真实交易结果渠道交易号待处理、成功、失败、撤销
Refund Order(退款单)表达一次退款意图和退款进度退款单号申请、退款中、成功、失败、异常
Balance Account(余额账户)保存可用余额、冻结余额等账户快照账户号正常、冻结、停用
Ledger Entry(账务分录)记录每次资金变化的不可变事实流水号已记账、冲正
Fee Record(费用流水)记录商品费、运费、出库费、手续费等应收应付费用流水号待出账、已出账、待支付、已支付、已退款
Bill(账单)按账期聚合费用流水供核对账单号未核对、核对中、已核对
Settlement(结算)单聚合已核对账单并推进实际结款结算单号未结算、审核中、待结款、已结款、驳回

关键关系:

  • 一个业务订单可以产生多个支付单,例如第一次支付超时后换渠道重试,但只能有一个有效成功结果。
  • 一个支付单可以产生多个支付会话,例如用户关闭收银台后重新发起。
  • 支付成功不等于账务完成,必须生成可审计的账务流水并推进业务订单。
  • 退款单必须关联原支付单和原渠道交易,累计成功退款金额不能超过原成功支付金额。

热门面试题

  1. 问题(基础题):为什么业务订单和 Payment Order(支付单)要分开?
    • 考点:领域建模、重复支付、渠道切换。
    • 回答思路:业务订单表达买卖关系,Payment Order(支付单)表达一次收款尝试。分离后可以支持一个订单多次发起支付、换渠道、关闭旧会话,同时保证订单只能被有效支付一次。
    • 进阶追问:同一业务订单出现两个渠道都成功怎么办?
  2. 问题(原理题):为什么余额不能只保存一个余额字段?
    • 考点:账户快照、账务流水、审计。
    • 回答思路:余额字段只表示当前快照,无法解释为什么变化。必须同时保存不可变流水、业务单号、变动前后金额和操作类型,才能追溯和对账。
    • 进阶追问:流水写成功但余额更新失败如何处理?
  3. 问题(项目题)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(双精度浮点数)计算资金。
  • 明确金额最小单位、精度和舍入规则,例如美元保留两位小数,渠道若使用分则在边界层统一转换。
  • 每个支付单固定币种;金额和币种必须同时校验。
  • 分摊时最后一项吸收舍入差,保证各明细之和严格等于总额。

热门面试题

  1. 问题(基础题):支付状态为什么不能随意覆盖?
    • 考点:状态机、终态保护、乱序回调。
    • 回答思路:支付事件可能重复和乱序,如果直接覆盖,迟到的失败回调可能把成功改成失败。必须定义合法迁移,并用状态条件更新保护终态。
    • 进阶追问:取消回调先到、成功回调后到,最终是什么状态?
  2. 问题(原理题):为什么支付金额要使用 BigDecimal(高精度十进制数)?
    • 考点:二进制浮点误差、精度、舍入。
    • 回答思路:double(双精度浮点数)不能精确表示多数十进制小数,连续计算会产生误差。BigDecimal(高精度十进制数)配合明确精度和舍入规则才能保证账务一致。
    • 进阶追问:BigDecimal(高精度十进制数)用字符串构造和用 double(双精度浮点数)构造有什么区别?
  3. 问题(项目题):如何解决 hop 中多个支付状态口径并存的问题?
    • 考点:统一状态语义、兼容迁移。
    • 回答思路:先定义领域标准状态,再为旧表建立映射;写入统一走领域服务,读取阶段兼容旧值;通过数据校验和灰度迁移逐步收口,避免一次性改表造成风险。
    • 进阶追问:迁移期间新旧服务同时写入怎么办?

3.3 订单履约领域模型与状态机

订单履约关注“订单从承诺到交付”的全过程。在海外仓场景中,至少要区分客户订单、履约订单、下游仓订单、库存冻结、包裹、面单、轨迹、费用和异常任务。

对象作用不能混淆的边界
Customer Order(客户订单)表达客户交付要求不直接承载某个仓库的技术状态
Fulfillment Order(履约订单)表达仓库拣货、打包、出库任务可因拆仓、缺货拆成多个履约单
Downstream Order(下游订单)对应第三方海外仓系统中的订单下游单号和本地单号必须双向关联
Inventory Reservation(库存预占)防止并发订单使用同一库存预占、扣减、释放必须可追溯
Parcel(包裹)承载重量、尺寸、箱数、商品明细一个履约单可能拆多个包裹
Label(面单)承运商运输凭证可能来自客户、物流中台或下游仓
Tracking Event(轨迹事件)记录物流节点事实事件可能重复、乱序、延迟
stateDiagram-v2
    [*] --> 待审核
    待审核 --> 待履约: 审核通过并冻结库存
    待履约 --> 下发中: 创建下游仓订单
    下发中 --> 待出库: 下游受理成功
    下发中 --> 异常: 超时或明确失败
    待出库 --> 已出库: 下游状态同步
    待出库 --> 拦截中: 客户取消
    拦截中 --> 已取消: 下游取消成功并释放库存
    已出库 --> 运输中: 首条运输轨迹
    运输中 --> 已签收: 签收轨迹
    运输中 --> 物流异常: 退件或派送失败

热门面试题

  1. 问题(基础题):为什么客户订单和履约订单要分开?
    • 考点:拆仓、拆包、业务与执行解耦。
    • 回答思路:客户订单表达一个商业承诺,履约订单表达仓库执行。同一客户订单可能按仓库、库存和包裹拆成多个履约订单,因此不能用一套状态硬塞在一张表里。
    • 进阶追问:多个履约单部分成功时客户订单状态怎么计算?
  2. 问题(原理题):库存冻结、实际扣减和释放分别在什么时候发生?
    • 考点:库存状态、支付与履约边界。
    • 回答思路:审核或接单时冻结,确认出库时转为实际扣减,取消或履约失败时释放。每次变化必须有库存流水和业务单号。
    • 进阶追问:下游已出库但本地仍是冻结状态怎么办?
  3. 问题(项目题)hiwi-unify 的履约编排如何讲?
    • 考点:多仓适配、下游订单、状态轮询、有限重试。
    • 回答思路:本地订单审核后生成下游订单任务,通过 SDK(软件开发工具包)适配不同仓库;创建响应不确定时进入查询队列,按下游状态更新本地订单,出库后触发费用处理,失败超过阈值进入异常人工处理。
    • 进阶追问:下游创建超时后为什么不能直接重试创建?

3.4 上下游契约与防腐层

支付渠道、承运商和海外仓都属于外部依赖。它们的字段、状态、错误码、认证、限流和幂等能力不同,不能让差异渗透到核心业务。

推荐分层:

  1. Domain Service(领域服务):只处理本地统一模型和业务规则。
  2. Port(端口接口):定义支付、退款、查单、创建仓单、取消仓单、拉面单、查轨迹等能力。
  3. Adapter(适配器):实现 Airwallex(空中云汇)、Stripe(国际支付网关)、PayPal(国际支付平台)、具体仓库和承运商协议。
  4. Anti-Corruption Layer(防腐层):做字段映射、状态映射、金额单位转换、错误分类和敏感日志脱敏。

统一接口返回值至少包含:

  • 本地请求号和外部请求号。
  • 是否已受理,而不是只返回成功或失败。
  • 外部状态、统一状态和原始错误码。
  • 是否建议重试、建议重试时间、是否需要主动查询。
  • 可脱敏保存的原始响应摘要。

错误应分为三类:

错误类型示例处理方式
业务不可重试余额不足、地址非法、订单已取消直接失败并提示修正
技术可重试网络超时、限流、临时不可用指数退避和最大次数
结果不确定请求超时但对方可能已成功禁止直接重做,先按业务号主动查询

热门面试题

  1. 问题(基础题):什么是 Anti-Corruption Layer(防腐层)?
    • 考点:外部协议隔离、领域模型稳定。
    • 回答思路:Anti-Corruption Layer(防腐层)把外部字段、状态和错误映射为内部统一模型,避免核心业务直接依赖某个渠道协议。
    • 进阶追问:状态映射表应该放在哪里维护?
  2. 问题(原理题):外部调用超时为什么不能一律重试?
    • 考点:结果不确定、重复扣款、重复下单。
    • 回答思路:超时只表示调用方没收到结果,不代表服务端没执行。支付和创建仓单属于有副作用操作,应携带幂等业务号,超时后先查单再决定是否重试。
    • 进阶追问:对方不支持幂等键也不支持查单怎么办?
  3. 问题(项目题)hall-nexthiwi-unify 如何避免每接一个仓库就改核心流程?
    • 考点:SDK(软件开发工具包)抽象、模板方法、统一上下文。
    • 回答思路:核心业务只调用统一 SDK(软件开发工具包)接口,各仓库实现创建、取消、查询、面单和库存同步;公共校验、日志和状态推进放在基类或业务编排层。
    • 进阶追问:某个仓库需要特殊字段时如何扩展而不污染公共模型?

4. 底层原理与关键设计

4.1 多支付渠道接入

hopnest2 都体现了“接口 + 抽象基类 + 工厂”的渠道化设计。统一接口负责支付、取消、退款、查退款、配置校验和令牌刷新;抽象基类负责配置解析、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(访问令牌)提前刷新并设置安全时间窗,避免刚取出就过期。
  • 多实例刷新令牌时使用分布式锁,但锁外仍要二次检查缓存,防止重复刷新。
  • 每次外部调用记录本地请求号、外部请求号、耗时、结果分类和可重试标识。

热门面试题

  1. 问题(基础题):为什么支付渠道适合使用工厂和适配器模式?
    • 考点:开闭原则、协议隔离。
    • 回答思路:业务流程稳定,但渠道协议不断变化。工厂负责按渠道选择实现,适配器负责差异转换,新接渠道只新增实现,不修改核心交易逻辑。
    • 进阶追问:渠道特有功能如何暴露?
  2. 问题(原理题):Access Token(访问令牌)缓存为什么还要加锁?
    • 考点:缓存击穿、并发刷新、多实例。
    • 回答思路:令牌过期瞬间多个实例可能同时刷新,造成渠道限流或令牌互相覆盖。使用分布式锁并在获得锁后二次检查缓存。
    • 进阶追问:持锁实例宕机怎么办?
  3. 问题(项目题)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

回调处理顺序:

  1. 读取原始请求体和签名头,先验签再反序列化业务字段。
  2. 校验 Timestamp(时间戳)窗口、Nonce(随机数)或 Event ID(事件标识),防止重放。
  3. 记录回调事件唯一键;已处理事件直接返回成功。
  4. 按本地支付单号和渠道交易号查找记录。
  5. 主动查询渠道,核对商户、金额、币种、订单号和真实状态。
  6. 在本地事务中推进支付单、业务订单和账务流水。
  7. 提交事务后发送领域事件;下游消费端再次幂等。
  8. 无法确认时进入异常状态和延迟查询,不把“不确定”当失败。

热门面试题

  1. 问题(基础题):支付成功为什么不能只相信前端跳转?
    • 考点:前端不可信、异步支付、结果伪造。
    • 回答思路:前端跳转可能被篡改或中断,只能用于展示。最终支付结果必须来自验签后的渠道回调、主动查询或对账文件。
    • 进阶追问:前端显示失败但实际扣款成功怎么办?
  2. 问题(原理题):收到 Webhook(回调通知)后为什么还要主动查单?
    • 考点:纵深校验、金额核对、伪造防护。
    • 回答思路:验签证明消息可能来自渠道,但主动查单能再次确认渠道侧最终状态、金额和币种,降低错误配置、事件乱序和验签实现缺陷风险。
    • 进阶追问:查单接口也超时怎么办?
  3. 问题(项目题)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

一个完整的支付成功事务应满足:

  1. 锁定或条件更新 Payment Order(支付单)。
  2. 判断当前是否已经成功,成功则直接幂等返回。
  3. 插入唯一渠道事件记录。
  4. 更新 Payment Order(支付单)和业务订单。
  5. 插入 Ledger Entry(账务分录)。
  6. 写入 Outbox Event(发件箱事件)。
  7. 事务提交后异步投递。

热门面试题

  1. 问题(基础题):接口幂等和防重复点击有什么区别?
    • 考点:客户端体验、服务端正确性。
    • 回答思路:前端防重复点击只能减少请求,不能处理超时重试、网络重放和回调重复。真正幂等必须在服务端通过唯一键、状态机和流水保证。
    • 进阶追问:Idempotency Key(幂等键)应该保存多久?
  2. 问题(原理题):为什么有 Redis(远程字典服务)锁还需要唯一索引?
    • 考点:锁失效、主从切换、最终防线。
    • 回答思路:锁可能过期、误删或在故障切换中失效,数据库唯一索引是最终不变量。锁优化并发体验,唯一约束保证结果正确。
    • 进阶追问:唯一索引冲突后接口应该返回失败还是原结果?
  3. 问题(项目题):下游仓创建订单如何做幂等?
    • 考点:业务单号、先查后建、结果不确定。
    • 回答思路:本地生成稳定的客户订单号并持久化,调用下游始终携带该号码;超时后先查询下游是否已存在,只有确认不存在才重建,并对重复返回做状态同步。
    • 进阶追问:下游查询接口有延迟,暂时查不到怎么办?

4.4 余额账户、冻结与账务守恒

余额支付需要区分:

  • Available Balance(可用余额):当前可以支付的金额。
  • Frozen Balance(冻结余额):已经为订单预留但尚未确认扣减的金额。
  • Account Balance(账户余额):可用余额与冻结余额等账户分项的汇总口径。

典型状态变化:

操作可用余额冻结余额账务含义
充值到账 100+1000充值入账
订单预占 30-30+30冻结资金
支付确认0-30冻结转消费
订单取消+30-30解冻返回
退款 10+100退款入账

并发扣款应使用数据库条件更新或行锁:

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;

资金不变量:

  • 任意时刻余额不能小于零,除非业务明确支持授信。
  • 每次余额变化必须存在唯一账务流水。
  • 流水变动前金额 + 本次变动 = 变动后金额。
  • 重放同一业务流水不能再次改变余额。
  • 对账时“期初余额 + 入账 - 出账 = 期末余额”。

热门面试题

  1. 问题(基础题):余额支付为什么需要冻结金额?
    • 考点:长事务、订单取消、并发占用。
    • 回答思路:订单从创建到最终确认存在时间窗口,直接扣减不利于取消,完全不预占又会超用。冻结把资金预留和最终消费拆开。
    • 进阶追问:冻结多久后自动释放?
  2. 问题(原理题):如何防止两个订单同时把余额扣成负数?
    • 考点:条件更新、行锁、影响行数。
    • 回答思路:使用 available_amount >= amount 的原子条件更新,或事务内行锁读取并扣减;受影响行数为零表示余额不足或版本冲突。
    • 进阶追问:为什么先查余额再更新不安全?
  3. 问题(项目题)hop 余额流水如何形成面试亮点?
    • 考点:业务锁、变动前后余额、审计。
    • 回答思路:同一余额账户操作串行化,扣款或充值时记录业务单号、变动前后余额和原因;重复回调先检查业务状态和流水唯一键,避免重复入账。
    • 进阶追问:余额更新成功但流水插入失败如何恢复?

4.5 取消、退款与冲正

取消和退款不是同一件事:

  • Cancel(取消):交易尚未最终成功,关闭支付会话或撤销订单。
  • Refund(退款):原交易已经成功,将部分或全部资金退回。
  • Reversal(冲正):对账或系统故障发现错误记账后,用反向分录纠正,不直接删除原流水。

退款核心规则:

  1. 退款单号全局唯一。
  2. 退款必须关联原支付单和渠道交易号。
  3. 累计成功退款 + 处理中退款 <= 原成功支付金额
  4. 多个并发退款请求必须按原支付单串行或使用原子额度占用。
  5. 渠道退款成功后,本地退款单、业务单和账务分录在同一本地事务内推进。
  6. 退款通知丢失时主动查退款状态;结果不确定时不能重复创建新退款。
  7. 部分退款要明确手续费、优惠、税费和各订单明细的分摊规则。

hop 中退款状态区分申请、退款中、成功、失败和异常;在线充值退款还会关联余额和退款记录。这种设计的价值在于“退款请求”和“退款最终结果”被分开管理。

热门面试题

  1. 问题(基础题):取消支付和退款有什么区别?
    • 考点:交易终态、资金是否已扣。
    • 回答思路:取消针对未成功交易,退款针对已成功交易。取消通常关闭会话,退款会产生独立退款单和反向资金流水。
    • 进阶追问:用户取消时渠道恰好支付成功怎么办?
  2. 问题(原理题):如何防止并发退款超过原支付金额?
    • 考点:退款额度冻结、行锁、原子条件更新。
    • 回答思路:按原支付单锁定退款额度,原子校验已退款和处理中金额,再创建退款单;退款失败时释放占用额度。
    • 进阶追问:部分退款失败后重试是复用原退款单还是新建?
  3. 问题(项目题):为什么退款任务需要异常状态和人工入口?
    • 考点:外部结果不确定、长尾故障。
    • 回答思路:退款可能长时间处理中或渠道响应异常,自动重试不能无限进行。异常状态保留上下文,配合主动查询、人工重试和对账处理长尾问题。
    • 进阶追问:人工操作如何避免再次退款?

4.6 对账、账单与结算

三个概念必须分清:

  • Reconciliation(对账):比较双方记录,发现差异。
  • Billing(出账):按账期和规则把费用流水聚合为账单。
  • Settlement(结算):对已核对账单完成实际收付款和状态闭环。

hop 供应商财务链路体现了较完整的工程设计:

  1. 每日任务按供应商和账期筛选可出账费用。
  2. 只有已出库超过账期的订单费用、已退款费用和其他符合条件的费用进入账单。
  3. 任务先持久化并写入账单号,失败重入时先查账单是否已经生成。
  4. 费用流水按主键游标分批更新为已出账,避免条件变化导致分页跳行。
  5. 通过数据库聚合重新计算账单金额,保证重入时统计口径一致。
  6. 已核对账单才能申请 Settlement(结算)单,并设置最低结算金额。
  7. 结款完成后异步分批把费用流水更新为已支付,条件限定只处理待支付记录,实现幂等。
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["会计与银行对账"]

对账差异分类:

差异可能原因处理
渠道成功、本地失败回调丢失、事务失败主动查单并补记
本地成功、渠道失败错误状态推进冻结业务并冲正
金额不一致手续费、汇率、部分退款、精度明细核对后调账
重复交易幂等失效、渠道重试保留一笔有效交易,其余退款
账单缺流水出账任务中断、账期条件错误任务重跑和差异补录

热门面试题

  1. 问题(基础题):对账和结算有什么区别?
    • 考点:发现差异与实际付款。
    • 回答思路:对账是核对双方记录是否一致,结算是在账单核对完成后执行资金收付。没有完成对账的账单不应进入结算。
    • 进阶追问:对账一致是否代表账务一定正确?
  2. 问题(原理题):为什么账单任务要先保存任务和账单号?
    • 考点:失败重入、幂等、长任务。
    • 回答思路:长任务可能在更新部分费用后宕机。先持久化任务和确定性账单号,重启后可以识别已生成结果并继续处理,不会生成多个账单。
    • 进阶追问:费用已标记出账但账单尚未插入时怎么办?
  3. 问题(项目题):供应商账单为什么按主键游标分页?
    • 考点:扫描条件变化、分页遗漏。
    • 回答思路:处理后费用状态会从待出账变为已出账,如果使用页码分页,结果集收缩会跳过数据。按主键递增游标读取可以稳定推进。
    • 进阶追问:任务运行期间新费用写入怎么办?

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(库存单位)和数量。

外部仓库同步不能直接覆盖本地库存。应保存下游原始快照,计算差异并生成同步流水;若差异超过阈值,进入告警或人工确认,避免错误数据瞬间污染全部可售库存。

热门面试题

  1. 问题(基础题):为什么订单履约不适合使用一个跨系统强事务?
    • 考点:外部系统、长事务、可用性。
    • 回答思路:第三方仓库不参与本地事务,网络调用时间长且可能失败。应通过本地事务、状态机、任务和补偿实现最终一致。
    • 进阶追问:冻结库存成功但创建下游订单失败怎么办?
  2. 问题(原理题):下游创建订单超时后的正确处理是什么?
    • 考点:结果不确定、业务幂等、主动查询。
    • 回答思路:保存超时上下文并进入查询任务,使用稳定业务单号查询下游;确认不存在后才能重试创建,确认存在则补录下游单号并继续状态同步。
    • 进阶追问:查询长期返回不存在但创建可能仍在异步处理中怎么办?
  3. 问题(项目题)hiwi-unify 多仓履约的难点是什么?
    • 考点:状态映射、下游差异、重试阈值、人工异常。
    • 回答思路:不同仓库对创建、取消、面单和状态的定义不同,因此用统一 SDK(软件开发工具包)和下游订单模型隔离差异;可重试错误延迟重试,超过阈值转异常单,避免无限重试拖垮系统。
    • 进阶追问:如何判断一个错误可不可以重试?

4.8 面单生成与物流轨迹

面单链路通常有三种来源:

  1. 客户上传:系统校验文件、跟踪号和订单归属。
  2. 物流中台生成:hall-next 调用承运商或聚合商创建订单并异步拉取面单。
  3. 海外仓返回:hiwi-unify 创建下游订单后从仓库获取面单。

面单属于异步资源,创建订单成功不代表面单立即可用。hall-next 将下单和拉面单拆成队列任务,维护下单中、拉面单中、成功、失败、重试和延迟等待等状态,并使用业务锁防止同一订单被多个任务并发处理。

轨迹同步采用“订阅推送 + 主动拉取 + 定时补偿”:

  • 首次拿到跟踪号后推送到轨迹平台订阅。
  • Webhook(回调通知)到达后验签、标准化状态并幂等保存事件。
  • 长时间无更新的运输中订单进入主动拉取。
  • 已签收、退件等终态降低或停止轮询。
  • 对事件按发生时间排序,但主状态更新必须遵守状态优先级,避免乱序回退。

轨迹事件唯一键不能只用跟踪号,因为一个跟踪号有多个节点。推荐组合:跟踪号、事件时间、状态码、地点和描述摘要。

热门面试题

  1. 问题(基础题):为什么面单拉取要从下单主链路拆开?
    • 考点:外部异步、响应时间、重试隔离。
    • 回答思路:承运商可能先受理订单,稍后才生成面单。拆开后主请求快速返回受理状态,后台任务独立重试,不占用 Web(万维网)请求线程。
    • 进阶追问:用户一直看不到面单时如何提示?
  2. 问题(原理题):物流轨迹乱序怎么处理?
    • 考点:事件时间、状态优先级、终态保护。
    • 回答思路:明细事件按发生时间去重保存,订单主状态按状态机优先级推进;迟到的早期事件可以补入明细,但不能把已签收回退为运输中。
    • 进阶追问:承运商修正错误签收状态怎么办?
  3. 问题(项目题)hall-next 的队列设计有什么价值?
    • 考点:延迟队列、重试次数、锁、优先级。
    • 回答思路:不同业务编码使用独立队列,任务可以延迟、重排、统计处理次数并加业务锁;成功后原子移除并解锁,失败按错误类型延迟或转人工。
    • 进阶追问:队列消息丢失如何发现?

4.9 多源同步与最终一致性

支付和履约都不能只依赖一种同步方式。

方式优点缺点适用场景
同步 API(应用程序接口)实时、交互简单超时结果不确定创建支付意图、提交仓单
Webhook(回调通知)渠道主动推送、延迟低重复、乱序、可能丢失支付结果、轨迹更新
主动查询能确认渠道真实状态有调用成本和限流超时补偿、回调校验
MQ(消息队列)解耦、削峰、可重试需要处理重复和积压支付成功事件、履约任务
定时扫描实现简单、兜底可靠实时性较低长时间中间态、漏单
对账文件或对账 API(应用程序接口)可发现历史差异通常是次日或周期性资金和结算最终兜底

中间态必须有超时扫描:

  • 支付中超过阈值:查询渠道交易。
  • 退款中超过阈值:查询渠道退款。
  • 下发中超过阈值:查询下游仓订单。
  • 拉面单中超过阈值:查询面单或重新拉取。
  • 运输中长时间无轨迹:主动拉取或标记异常。
  • 结算任务运行中超过阈值:读取任务进度并重入。

任何重试都要具备最大次数、最大总时长、退避、随机抖动和失败归宿。无限重试不是可靠性,而是制造事故。

热门面试题

  1. 问题(基础题):既然有 Webhook(回调通知),为什么还要定时任务?
    • 考点:消息丢失、长尾故障、最终兜底。
    • 回答思路:Webhook(回调通知)可能因网络、配置或服务故障丢失,定时扫描中间态并主动查询能发现漏单。
    • 进阶追问:定时扫描如何避免全表扫描?
  2. 问题(原理题):重试如何避免形成重试风暴?
    • 考点:指数退避、抖动、限流、熔断。
    • 回答思路:按错误分类,只重试临时故障;采用指数退避和随机抖动,设置最大并发和最大时长,下游异常时熔断并转为低频补偿。
    • 进阶追问:积压任务恢复时如何控制放量?
  3. 问题(项目题)nest2hiwi-unify 的主动查询分别解决什么?
    • 考点:支付结果不确定、下游仓结果不确定。
    • 回答思路nest2 在支付回调缺失或状态同步中主动查询渠道;hiwi-unify 在仓单创建和状态推进不确定时查询下游订单。二者本质都是用可查询事实消除网络超时的不确定性。
    • 进阶追问:主动查询结果与回调冲突时信谁?

4.10 安全、风控与审计

支付安全必须建立纵深防御:

  • HTTPS(安全超文本传输协议)保证传输加密,但不能代替应用层验签。
  • 使用 HMAC(基于哈希的消息认证码)或渠道公钥验证原始请求体,不要对重新序列化后的对象验签。
  • 校验 Timestamp(时间戳)窗口、Nonce(随机数)或 Event ID(事件标识)防重放。
  • 主动查询并核对商户号、支付单号、金额、币种和收款账户。
  • 密钥加密存储,按环境和商户隔离,支持轮换;日志禁止打印完整密钥、银行卡号和身份证件。
  • 回调接口先落审计摘要,再执行业务;无论重复事件还是已处理终态,都应快速返回渠道期望响应。
  • 高风险退款、结算和人工调账采用权限隔离、双人复核和操作留痕。
  • 不自行保存银行卡敏感数据;涉及卡数据时遵守 PCI DSS(支付卡行业数据安全标准)的边界要求。

仅依赖 IP(互联网协议地址)白名单不够,因为地址可能变化、代理链可能复杂,且无法证明消息内容未被篡改。IP(互联网协议地址)限制可以作为附加防线,不能代替签名。

热门面试题

  1. 问题(基础题):支付回调验签解决什么问题?
    • 考点:来源真实性、内容完整性。
    • 回答思路:验签确认消息由持有密钥的一方产生且内容未被篡改,但仍需防重放和业务字段核验。
    • 进阶追问:为什么验签成功仍不能直接入账?
  2. 问题(原理题):如何防止合法回调被重复播放?
    • 考点:Timestamp(时间戳)、Nonce(随机数)、Event ID(事件标识)。
    • 回答思路:限制 Timestamp(时间戳)有效窗口,保存 Nonce(随机数)或 Event ID(事件标识)唯一记录,并让业务处理本身幂等。
    • 进阶追问:渠道没有 Event ID(事件标识)怎么办?
  3. 问题(项目题):结算和人工调账如何控制风险?
    • 考点:权限隔离、审核、审计、冲正。
    • 回答思路:申请、审核和付款角色分离;关键操作要求二次确认和凭证;不修改历史流水,错误通过冲正分录修复,并保留操作人、原因和前后数据。
    • 进阶追问:紧急生产修数如何保证可追溯?

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 --> F

6. 表格对比

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 支付接口超时但渠道成功演绎

  1. 本地创建支付单 P1002 并调用渠道。
  2. 渠道成功创建交易,但响应在网络中丢失。
  3. 本地将状态记为异常,不记为失败,也不立即创建新支付单。
  4. 补偿任务按 P1002 主动查询,查到渠道交易 T8002 已成功。
  5. 本地验证金额币种后补记成功和账务流水。

如果错误地把超时当失败并重新扣款,可能产生两笔成功交易。因此“异常”是支付状态机中的必要状态。

7.3 余额并发扣款演绎

账户可用余额为 80,订单 A 扣 60,订单 B 扣 40

方案A 读取B 读取最终风险
先查再更新8080两者都认为余额足够,可能扣成 -20
原子条件更新成功,余额变 20条件不满足,更新 0只有 A 成功,不超扣

7.4 部分退款演绎

原支付金额 100,第一次申请退款 30,第二次并发申请退款 80

  • 第一次在事务中占用退款额度 30
  • 第二次校验 已成功 0 + 处理中 30 + 本次 80 > 100,拒绝创建。
  • 第一次渠道退款成功后,写 +30 退款分录,支付单标记部分退款。
  • 后续最多还能退款 70

7.5 下游仓创建超时演绎

本地订单 O7001 调用海外仓创建,接口超时:

  1. 本地保持“下发中”,记录请求摘要和稳定客户订单号。
  2. 三十秒后按客户订单号查询下游。
  3. 若查到下游单 W8801,补录映射并进入待出库。
  4. 若明确不存在且超过下游异步受理窗口,才允许复用同一业务号重试创建。
  5. 超过最大次数进入异常工作台,不释放库存前必须确认下游确实没有订单。

7.6 账单任务宕机重入演绎

任务要处理费用流水 1..250,每批 100 条:

  • 第一批和第二批已更新为已出账,任务在插入账单前宕机。
  • 重启读取同一个任务和账单号,只扫描仍为待出账且主键大于游标的记录。
  • 全部处理后按账单号从数据库重新聚合金额并插入账单。
  • 如果账单已存在,直接把任务标为成功,不重复生成。

8. 线上排查与实战经验

8.1 渠道已扣款但本地未到账

排查顺序:

  1. 按业务订单号、支付单号、渠道交易号定位支付单和会话。
  2. 检查回调访问日志、验签结果和事件去重记录。
  3. 调用渠道查单确认商户、金额、币种和最终状态。
  4. 检查本地事务是否回滚、账务流水是否存在、Outbox(发件箱)是否投递。
  5. 若渠道成功且本地失败,走补记流程,不直接修改余额字段。
  6. 记录事故原因并扩大同时间窗差异扫描。

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 的多仓履约与异常补偿设计。
  • 能给出渠道已扣款本地未到账、重复入账、退款卡住和结算差异的排查路径。
  • 能说明验签、防重放、金额币种核验、密钥保护和人工调账审计。
  • 能在三分钟内完成一段“支付资金一致性 + 订单履约”的项目串讲。