支付交易领域模型、状态机、金额币种与订单分离
范围:本分册只定义支付交易的权威对象、状态、金额与确认边界;渠道验签、复式账务和库存扣减分别由后续分册展开。所有金额、编号和时间均为演练样例,不代表现网数据。
1. 简历关联点与事实边界
支付资金一致性的第一问不是“回调怎么写”,而是先把客户订单、支付单、支付尝试和渠道结果拆开。这样才能回答重复点击、超时、取消竞争和币种舍入发生时,哪个对象能改、哪个对象只能追加事实。
| 事实等级 | 本分册可使用的结论 | 证据或限制 |
|---|---|---|
| E1 源码实体 | PaymentBusiness.createPaymentOrder 会创建支付单、设为待支付并生成支付单号 | /Users/Lever/IdeaProjects/work/hop-java/ruoyi-hop/src/main/kotlin/com/ruoyi/hop/business/PaymentBusiness.kt |
| E2 源码流程 | PaymentBusiness.verifyPay 校验支付单存在、卖家归属与待支付状态 | 同上 |
| E2 源码流程 | SellerRechargeBusiness.createAndCheckout 创建充值订单后创建支付单并发起结账 | /Users/Lever/IdeaProjects/work/hop-java/ruoyi-hop/src/main/kotlin/com/ruoyi/hop/business/SellerRechargeBusiness.kt |
| E4 演练设计 | 条件更新、Outbox(发件箱)与三层确认是建议模型 | 不宣称已在该源码完整落地 |
| E5 待核对 | 渠道真实状态码、生产验签、汇率来源和结算周期 | 待源码或现场核对相应配置、适配器和运营规则 |
sequenceDiagram
participant C as 客户
participant O as 客户订单
participant P as 支付单
participant A as 支付尝试
participant K as 渠道
C->>O: 提交交付诉求
O->>P: 创建一次付款义务
P->>A: 发起本次支付尝试
A->>K: 用稳定请求号受理
K-->>A: 会话或超时
A-->>P: 候选结果,不直接改订单图解:节点依次是客户、商业订单、付款义务、可重试尝试与外部渠道;箭头表达“订单产生付款义务,义务产生尝试”,不是反向绑定。前提是每个对象都有不可变编号;正常路径由渠道返回会话,失败路径是超时只留下候选结果;业务结论是客户订单不能被一次渠道调用直接写成成功。
领域对象与事实边界
业务订单表达买什么、交给谁和是否允许取消;支付单表达本次应收的金额、币种与付款义务;支付尝试表达某次点击或重试;渠道会话保存收银台、授权页或令牌;渠道交易保存渠道最终可核验的流水;退款单是独立的反向义务。任何“已成功”都应是可追溯事实,不应只靠一个可覆盖的状态字段。
演练样例 1:对象分离
订单 CO-100 应收 100.00 USD(美元),先生成支付单 PO-900;客户关闭页面后再点一次,新增尝试 PA-2,而不是再建订单。渠道会话 CS-1 超时,CS-2 成功并关联渠道交易 CT-8。核对式为:一个订单的支付单数可大于一,一个支付单的尝试数可大于一,但生效渠道交易数至多一。
热门面试题
- 问题(基础题):为什么业务订单不能直接承载渠道支付状态?
- 考点:领域边界、可重试性、审计。
- 回答思路:先区分商业承诺和外部付款动作,再说明一单多尝试。
- 详细答案:业务订单面对履约和取消,支付状态面对渠道异步结果;两者生命周期不同。把它们混在一起会让一次超时既像订单失败又像支付失败,无法保留前一次会话。支付单把应收金额固定,尝试记录每次动作,订单只消费已确认的支付事实。
- 进阶追问:一笔订单允许部分支付吗?
- 进阶回答:可以,但要把支付单设计为可累计确认的应收义务,并定义剩余应收与关闭条件,不能以覆盖总状态代替累计事实。
- 问题(原理题):什么叫不可变支付事实?
- 考点:审计、事件、状态快照。
- 回答思路:区分追加事实和可重建快照。
- 详细答案:请求号、渠道交易号、回调原文摘要、确认时间和金额币种属于不可变事实;状态是从这些事实归纳出的快照。修正错误时追加更正或退款事实,不删除旧事实,才能解释为什么某笔钱在某时刻被确认。
- 进阶追问:状态字段还需要吗?
- 进阶回答:需要,它服务查询和并发控制;但它必须能由事实复核,且更新受合法迁移限制。
- 问题(项目题):现有源码能证明哪些支付建模事实?
- 考点:证据边界、源码阅读。
- 回答思路:只报类、方法和可见校验,不推断生产流程。
- 详细答案:
PaymentBusiness.createPaymentOrder可证明支付单创建、待支付初始化和编号生成;verifyPay可证明存在归属及待支付校验。SellerRechargeBusiness.createAndCheckout可证明充值场景把充值订单与支付单串联。完整支付尝试表、渠道交易表和退款上限仍待源码或现场核对。 - 进阶追问:为什么不能把源码注释当生产事实?
- 进阶回答:注释只能帮助定位意图,是否启用、异常分支是否走到和数据是否完整仍需要调用链、表结构或现场证据。
客户订单、支付单与退款单分离
支付单通过订单明细关联多个业务订单,避免把拆单、优惠和付款混成一张表。退款单必须引用原已确认支付单与可退款额度,不能把“用户想退”直接写为支付失败。
| 对象 | 权威问题 | 可变字段 | 不可混淆的编号 |
|---|---|---|---|
| 客户订单 | 用户买了什么、是否履约 | 履约汇总状态 | 客户订单号 |
| 支付单 | 应收多少、付什么币种 | 支付快照状态 | 支付单号 |
| 支付尝试 | 这次发起是否已受理 | 尝试阶段 | 尝试号 |
| 渠道会话 | 客户下一步去哪付 | 会话有效期 | 会话号 |
| 渠道交易 | 渠道最终确认了什么 | 只追加确认事实 | 渠道交易号 |
| 退款单 | 已付资金如何反向退回 | 退款状态 | 退款单号 |
flowchart LR
O[客户订单] --> D[支付单明细]
D --> P[支付单]
P --> A1[支付尝试一]
P --> A2[支付尝试二]
A1 --> S1[渠道会话一]
A2 --> S2[渠道会话二]
S2 --> T[唯一生效的渠道交易]
T --> R[退款单]图解:菱形不存在,因为这里不是分支决策而是对象归属;两条尝试箭头表示同一付款义务可多次发起。前提是支付单金额已锁定;正常时一条交易被确认,失败时旧会话失效但仍可审计;业务结论是退款依据渠道交易和支付单,不依据页面显示。
演练样例 2:一单两次尝试
PO-901 应收 35.00 EUR(欧元)。PA-1 的会话在 10 分钟后失效,PA-2 获得新会话;两条尝试都保留,只有 PA-2 绑定的 CT-201 确认成功。若 PA-1 迟到回调成功,系统先按支付单读取已生效交易,进入冲突核验,不允许把两条都生效。
热门面试题
- 问题(基础题):支付单和支付尝试分别解决什么问题?
- 考点:付款义务、重试、历史。
- 回答思路:支付单固定商业金额,尝试记录一次外部动作。
- 详细答案:支付单代表“这笔钱是否已经付清”的唯一业务口径;支付尝试代表一次具体渠道调用,可能超时、关闭或失败。重试新增尝试能保留原因与会话,支付单只在确认事实到达后推进一次。
- 进阶追问:是否允许多个尝试并行?
- 进阶回答:默认不允许同一支付单并行生效;若产品允许多方式并行,也必须在确认端用唯一生效交易和剩余额度做最终裁决。
- 问题(原理题):退款为什么要独立为退款单?
- 考点:反向资金、部分退款、审计。
- 回答思路:说明退款不是把成功状态改回失败。
- 详细答案:退款有自己的申请、受理、渠道结果与未知态,并可能多次部分退款。覆盖原支付状态会丢掉“曾经成功收款”的事实,也无法计算累计退款不超过原成功支付金额的不变量。
- 进阶追问:取消和退款如何区分?
- 进阶回答:取消发生在尚未形成最终扣款前,退款发生在已确认收款后;两者外部动作、账务含义和可用时间窗不同。
- 问题(项目题):充值重新支付时,源码体现了什么边界?
- 考点:旧会话关闭、新支付单。
- 回答思路:引用
checkout的读取、关闭与重建顺序。 - 详细答案:
SellerRechargeBusiness.checkout会拒绝已成功、已取消或支付中的充值订单;对已有支付单尝试关闭其会话,再创建新的支付单。这证明代码有避免重复支付的意图,但“关闭成功与迟到成功回调的最终裁决”仍需源码或现场核对。 - 进阶追问:关闭失败能否直接创建新单?
- 进阶回答:不能默认创建;旧会话仍可能扣款,应进入未知态并优先查单或等待确认。
支付尝试、渠道会话与渠道交易
三者的时间尺度不同:尝试是本地调用生命周期,会话是渠道引导生命周期,渠道交易是渠道资金事实。会话可被关闭、过期或替换;交易一旦确认只能追加校验事实,不能借由新会话覆盖。
| 记录 | 创建时机 | 关键唯一键 | 终态后的处理 |
|---|---|---|---|
| 支付尝试 | 用户点击或系统重试 | 支付单号加尝试序号 | 保留结果,禁止复用 |
| 渠道会话 | 渠道返回收银台信息 | 渠道加会话标识 | 允许失效,保留失效原因 |
| 渠道交易 | 回调、查单或对账发现 | 渠道加交易流水号 | 只补充核验证据 |
classDiagram
class PaymentOrder {支付单号\n应收金额\n币种\n状态}
class PaymentAttempt {尝试号\n请求号\n阶段}
class ChannelSession {会话号\n失效时间\n跳转地址摘要}
class ChannelTransaction {渠道流水号\n确认金额\n确认时间}
PaymentOrder "1" --> "1..*" PaymentAttempt
PaymentAttempt "1" --> "0..1" ChannelSession
PaymentAttempt "0..1" --> "0..1" ChannelTransaction图解:类框是数据责任,连线基数强调一个支付单可有多次尝试,而一次尝试不保证获得会话或交易。前提是稳定请求号随尝试保存;正常路径会话引导到交易,失败路径只留下无交易的尝试;业务结论是“无会话”不等于“未发送请求”。
演练样例 3:受理超时
PA-3 发送后客户端 3 秒超时,本地只有请求号 REQ-3,没有会话。此时新增尝试会造成双扣风险;正确动作是把尝试标成未知,按 REQ-3 查单。查到渠道交易则补齐 CT-202,查不到且过了约定观察窗口才允许新的尝试。
热门面试题
- 问题(基础题):渠道会话和渠道交易有什么区别?
- 考点:交互凭据、资金事实。
- 回答思路:会话服务支付引导,交易服务资金确认。
- 详细答案:渠道会话可能只是二维码、跳转地址或授权令牌,用户不支付也会存在;渠道交易才携带渠道最终的交易标识和金额。会话失效不说明交易失败,交易确认后也不应因会话过期倒退。
- 进阶追问:会话标识可作为幂等键吗?
- 进阶回答:不能单独使用;会话可能由渠道重建,应以本地稳定请求号和渠道交易流水共同去重。
- 问题(原理题):为什么渠道交易要有唯一约束?
- 考点:重复回调、唯一事实。
- 回答思路:从重复通知和跨实例并发说明。
- 详细答案:同一渠道交易可能被重复通知或被回调和查单同时发现。以渠道代码和渠道交易号建立唯一约束,使第二次写入返回已有事实,再由状态机做幂等响应,而不是再次确认资金。
- 进阶追问:渠道未给稳定交易号怎么办?
- 进阶回答:待源码或现场核对渠道能力;最低要求是保存商户请求号、原始事件标识和金额币种组合,并把无法唯一识别的结果转人工核验。
- 问题(项目题):现有充值代码如何避免支付中重复进入?
- 考点:状态校验、锁、边界。
- 回答思路:描述可见的
payStatus校验与锁,不夸大为最终正确性。 - 详细答案:源码在
checkout中对支付中状态拒绝重复支付,并以支付单号加锁;结账出错时重置为未支付。它能降低并发重复发起,但锁的失效语义、数据库唯一约束和跨实例终态裁决仍待源码或现场核对。 - 进阶追问:锁成功是否就能保证不重复扣款?
- 进阶回答:不能,锁只降低并发;最终仍需稳定请求号、渠道查单、唯一约束和条件更新。
支付状态机与合法迁移
推荐把支付单状态限制为待支付、处理中、成功、失败、取消和未知。成功、失败、取消是终态;未知不是终态,它表示本地尚缺可证明的渠道结果。退款属于退款单状态,不把支付成功回退成“已退款”。
| 当前状态 | 可迁移目标 | 触发证据 | 禁止迁移 |
|---|---|---|---|
| 待支付 | 处理中、取消 | 已受理、用户关闭 | 直接成功且无确认 |
| 处理中 | 成功、失败、取消、未知 | 确认、明确拒绝、关闭、超时 | 被旧事件改回待支付 |
| 未知 | 成功、失败、取消 | 回调、查单、对账 | 直接新扣款 |
| 成功 | 保持成功 | 重复成功事件 | 失败、取消 |
| 失败或取消 | 保持原终态 | 重复事件 | 迟到成功直接覆盖 |
stateDiagram-v2
[*] --> 待支付
待支付 --> 处理中: 渠道受理
待支付 --> 已取消: 取消确认
处理中 --> 成功: 金额币种核验通过
处理中 --> 失败: 渠道明确拒绝
处理中 --> 未知: 超时或证据冲突
未知 --> 成功: 回调/查单/对账确认
未知 --> 失败: 回调/查单/对账确认
成功 --> [*]
失败 --> [*]
已取消 --> [*]图解:箭头仅表示允许迁移,标签说明证据;前提是所有更新携带原状态或版本号。正常路径是处理中到成功,失败路径是未知后查询再裁决;业务结论是超时不能写失败,终态也不能被迟到事件覆盖。
演练样例 4:重复回调与终态保护
首次成功回调以 status in (处理中,未知) 更新 PO-902,影响一行并写入确认事实。第二次相同回调插入唯一渠道交易失败,读取到支付单已成功后返回已受理。一个迟到失败回调命中终态保护,只记录冲突证据,不把成功改失败。
热门面试题
- 问题(基础题):未知态为什么不能等同失败?
- 考点:超时语义、资金风险。
- 回答思路:说明调用方未收到结果和渠道未执行是两件事。
- 详细答案:超时只说明链路缺少响应,渠道可能已扣款或已受理。把它写失败并允许重新支付,会让同一付款义务出现两笔渠道交易;未知态冻结“是否可重试”的判断,先由回调、查单和对账补足证据。
- 进阶追问:未知态多久升级人工?
- 进阶回答:阈值、轮询间隔和人工流程待源码或现场核对;设计上应有最大观察窗口、证据记录和明确责任队列。
- 问题(原理题):终态保护怎样实现?
- 考点:状态条件、影响行数、乱序。
- 回答思路:用数据库原子条件代替先查后写。
- 详细答案:更新语句把允许源状态写进条件,只有一行受影响才推进;影响零行时读取当前状态,若已是同一结果就幂等成功,若冲突则留证据和告警。这样两个实例同时处理也只有一个能写入终态。
- 进阶追问:仅用版本号够吗?
- 进阶回答:版本号能防覆盖,但仍需状态迁移表判断业务是否允许;二者结合更清晰。
- 问题(项目题):充值回调已有怎样的终态处理?
- 考点:源码事实、局限。
- 回答思路:引用
onPaymentSuccess的成功短路和取消失败拦截。 - 详细答案:
SellerRechargeBusiness.onPaymentSuccess遇到支付单已成功会直接返回,遇到已取消或失败会抛错,并在事务里更新支付单与充值订单。这是 E2 源码流程;其更新是否使用状态条件和数据库唯一流水仍待源码或现场核对。 - 进阶追问:抛错后渠道会怎样重试?
- 进阶回答:具体响应协议待源码或现场核对;服务端至少应保留可重放事件并用幂等处理避免重试扩大影响。
条件更新、唯一键与并发竞态
锁是并发减压工具,不是资金正确性的最后防线。最后防线是数据库唯一约束、状态条件更新和不可变流水;分布式锁失效、续期失败或网络分区后,数据库仍必须拒绝第二个生效结果。
| 风险 | 第一层 | 最终裁决 | 失败后的动作 |
|---|---|---|---|
| 客户重复提交 | 请求幂等键 | 支付单唯一索引 | 返回原支付单 |
| 回调重复投递 | 事件去重 | 渠道交易唯一索引 | 返回已受理 |
| 成功与取消并发 | 业务锁 | 状态条件更新 | 读取胜出状态 |
| 超时重试 | 尝试号 | 请求号唯一与查单 | 保持未知 |
sequenceDiagram
participant C as 回调实例甲
participant D as 回调实例乙
participant DB as 本地数据库
C->>DB: 插入渠道交易唯一键
D->>DB: 插入相同唯一键
DB-->>C: 成功
DB-->>D: 唯一冲突
C->>DB: 条件更新处理中到成功
D->>DB: 读取已成功支付单
D-->>D: 幂等返回图解:两名参与者模拟多实例,数据库箭头说明先竞争不可变事实,再竞争状态快照。前提是唯一索引覆盖渠道和交易流水;正常路径甲胜出,失败路径乙不重做副作用;业务结论是锁丢失时仍不会出现两个生效确认。
演练样例 5:取消与成功同到
取消线程和成功回调同时读取 处理中。取消条件更新先把状态改为取消,成功更新影响零行;成功方不能直接重试确认,必须查渠道并标记“取消后渠道成功冲突”。若成功方先胜出,取消方影响零行,转为退款或人工处理,而不是关闭已成功交易。
热门面试题
- 问题(基础题):幂等键应该绑定什么?
- 考点:业务主体、参数一致性。
- 回答思路:键不能脱离订单和金额。
- 详细答案:幂等键至少绑定租户、付款义务和操作类型,并保存请求金额币种摘要。相同键相同参数返回原结果;相同键不同参数应拒绝,因为这通常是客户端错误或重放攻击,而不是合法重试。
- 进阶追问:幂等记录多久删除?
- 进阶回答:保留周期应覆盖渠道回调、对账和审计窗口,具体时长待源码或现场核对;删除前要保证可由归档事实重建。
- 问题(原理题):为什么先查询状态再更新有竞态?
- 考点:读写间隙、原子性。
- 回答思路:两个线程可同时读到处理中。
- 详细答案:先查后写把判断与写入拆成两步,两个线程都可能据此执行外部或内部副作用。条件更新把“当前仍可迁移”放到同一条数据库写入里,影响行数就是胜负证据,避免应用内时间窗口。
- 进阶追问:悲观锁能替代条件更新吗?
- 进阶回答:可以在同一数据库事务内序列化局部竞争,但外部调用不能长期持锁;条件更新仍更适合终态裁决。
- 问题(项目题):源码中的锁应怎样客观表述?
- 考点:事实分级、不过度承诺。
- 回答思路:指出锁键和目的,再交代未证明部分。
- 详细答案:
SellerRechargeBusiness可见按卖家、支付单和回调构造锁键,并在创建与回调流程中使用锁,属于减少重复处理的 E2 事实。锁的实现一致性、失效恢复以及是否存在唯一索引,待源码或现场核对,不能宣称锁已保证资金绝对正确。 - 进阶追问:锁获取失败要返回什么?
- 进阶回答:应返回可重试或查询中的语义,不能把未执行误报为支付失败。
受理、授权、扣款与确认语义
支付受理表示渠道已接收请求;授权表示渠道或发卡方暂时占用额度;扣款表示资金动作已发生;确认表示本地验证金额币种、交易归属后承认该结果。不同渠道可能合并或拆开这些阶段,具体状态映射待源码或现场核对。
| 阶段 | 能证明什么 | 不能推出什么 | 本地动作 |
|---|---|---|---|
| 受理 | 请求被渠道接收 | 已扣款 | 保存尝试与请求号 |
| 授权 | 额度可能被占用 | 已最终结算 | 记录候选状态 |
| 扣款 | 渠道资金动作成功 | 本地已履约 | 核验金额币种 |
| 确认 | 本地接受可验证结果 | 退款已完成 | 推进支付单并投递事件 |
sequenceDiagram
participant U as 用户
participant P as 支付服务
participant K as 渠道
participant DB as 本地数据库
U->>P: 选择付款方式
P->>K: 受理请求
K-->>P: 已受理和会话
U->>K: 完成授权或扣款
K-->>P: 回调候选结果
P->>K: 查单核验
P->>DB: 确认本地成功图解:先受理后交互,再由回调与查单形成确认;前提是请求号全程一致。正常路径的最终箭头只到本地数据库,失败路径的回调缺失会转未知;业务结论是“渠道已受理”绝不是“订单已支付”。
演练样例 6:授权未扣款
演练中渠道返回授权成功 120.00 USD(美元),但查单仍显示待扣款。本地支付单保持处理中,不发货、不记成功;到期后查单显示授权撤销才进入失败。若查单显示已扣款,则进入本地确认,不能因为前端页面已经关闭而忽略。
热门面试题
- 问题(基础题):支付受理成功为什么不能更新业务订单成功?
- 考点:受理与资金事实。
- 回答思路:受理只是渠道接单。
- 详细答案:受理返回往往只证明渠道保存了请求或创建了会话,用户还可能未付款、授权失败或页面关闭。业务订单只能消费本地确认成功事实,否则会出现未扣款先履约的漏洞。
- 进阶追问:同步接口直接返回成功呢?
- 进阶回答:仍需核验返回中的交易号、金额、币种和签名或后续查单;同步不等于可信。
- 问题(原理题):为什么确认应是本地领域动作?
- 考点:防腐、统一口径。
- 回答思路:渠道的词汇不等于内部状态。
- 详细答案:不同渠道对授权、捕获和完成的命名不同。本地确认层只接受满足订单号、商户、金额、币种和状态规则的一组证据,再把结果映射到统一支付单状态,避免渠道词汇渗透到订单和账务。
- 进阶追问:确认失败要回复渠道失败吗?
- 进阶回答:应按渠道协议区分已受理与需重试;协议细节待源码或现场核对,但本地不能因处理异常丢失候选事实。
- 问题(项目题):
createAndCheckout能说明什么?- 考点:服务费重算、后端权威。
- 回答思路:说明它以服务端金额为准。
- 详细答案:源码在创建支付订单前根据模板重新计算服务费,再组合支付金额并发起结账,体现后端不信任前端服务费。具体的渠道授权、扣款和最终确认映射不在该方法可见范围,应标待源码或现场核对。
- 进阶追问:服务费变更期间如何避免前后不一致?
- 进阶回答:支付单应固化计费版本、费率与计算结果,重试沿用该快照,而不是重新读取可能变更的配置。
回调、查单与对账三层确认
回调快但可能重复或延迟,主动查单可核验实时渠道状态但也会超时,对账覆盖长尾遗漏和跨系统差异。三层不是互相替代,而是按证据强度把未知态推进到终态。
| 确认层 | 输入 | 优点 | 典型失败 | 输出 | | --- | --- | --- | --- | | 回调 | 渠道通知 | 延迟低 | 重复、乱序、丢失 | 候选事实 | | 主动查单 | 稳定请求号或交易号 | 可反查 | 限流、超时 | 核验结果 | | 对账 | 渠道账单与内部事实 | 覆盖长尾 | 到达晚 | 差异清单与修复证据 |
sequenceDiagram
participant K as 渠道
participant R as 回调处理器
participant Q as 查单任务
participant D as 对账任务
participant P as 支付单
K->>R: 回调候选成功
R->>Q: 请求核验
Q->>K: 按请求号查单
K-->>Q: 金额币种状态
Q->>P: 条件更新成功或未知
D->>K: 获取对账事实
D->>P: 补齐遗漏或标记差异图解:回调不直接走向成功,而是先交给查单;对账箭头是独立的延迟闭环。前提是本地保留可查询请求号;正常路径查单确认,失败路径查单也未知则继续等待对账;业务结论是“没有回调”不能直接判失败。
演练样例 7:超时后查单
PO-903 处理 15 分钟未收到回调,任务按请求号查单第一次超时,状态改未知并记录下次查询时间;第二次查单返回成功 58.00 GBP(英镑),核对支付单金额和币种一致后更新成功。若次日对账没有该交易,不能自动冲掉成功,而是创建差异工单核对证据。
热门面试题
- 问题(基础题):为什么回调成功后还要查单?
- 考点:纵深校验、数据完整性。
- 回答思路:回调是候选证据,查单补齐最终口径。
- 详细答案:回调可能重复、乱序或因本地处理异常只完成一半;查单能够核验渠道侧最终状态以及金额、币种、商户订单号。收到回调后先记录事件,再根据渠道能力主动查询,是把外部通知变成本地确认的关键步骤。
- 进阶追问:查单也失败怎么办?
- 进阶回答:保留未知态、按退避预算重试并等待对账,达到阈值转人工;不能重发扣款请求。
- 问题(原理题):对账为什么不是事后报表?
- 考点:闭环证明、长尾恢复。
- 回答思路:对账能发现双方已丢失的实时链路。
- 详细答案:实时回调和查单都可能因故障遗漏,对账把内部支付事实与渠道账单逐笔比对,发现本地成功渠道无记录、渠道成功本地未知和金额币种不符。它提供的是可处理差异证据,不是只统计成功率的报表。
- 进阶追问:对账发现差异能自动修吗?
- 进阶回答:只有证据完整且状态机允许时可自动推进;金额冲突、重复交易等风险差异应冻结自动动作并人工复核。
- 问题(项目题):本分册为何不写具体回调验签?
- 考点:职责边界、证据分层。
- 回答思路:本篇定义确认接口,渠道分册定义安全细节。
- 详细答案:支付交易模型只约束回调、查单和对账怎样影响状态机;签名原文、时间窗、重放和渠道协议属于渠道适配边界。现有
PaymentBusiness和SellerRechargeBusiness不能单独证明生产验签规则,因此不能把推测写成项目事实。 - 进阶追问:没有验签的回调能进入确认吗?
- 进阶回答:不能作为强证据;最多记录为待核验线索,必须以可信查单或对账结果裁决。
金额最小单位与金额不变量
资金计算要先定义货币的最小单位、存储精度和边界转换。业务层可用货币金额表达,渠道适配层按规则转成整数最小单位;同一支付单必须同时固定订单金额、手续费、应付总额与币种。
| 金额字段 | 含义 | 演练存储 | 不变量 |
|---|---|---|---|
| 订单金额 | 商品或充值应收 | 100.00 | 明细和等于订单金额 |
| 服务费 | 付款附加费用 | 2.35 | 费率版本可追溯 |
| 应付金额 | 渠道请求金额 | 102.35 | 订单金额加服务费 |
| 最小单位 | 渠道整数金额 | 10235 | 与币种小数位一致 |
flowchart TD
A[订单金额 100.00] --> B[固定服务费 2.35]
B --> C[应付金额 102.35]
C --> D[按币种转最小单位]
D --> E[渠道请求 10235]
E --> F{回传金额币种一致}
F -- 是 --> G[允许确认]
F -- 否 --> H[未知或差异]图解:数字是演练而非项目事实;箭头表现先固化金额再转换,判断节点把渠道回传纳入核验。前提是币种小数位已知且支付单不可改价;正常时可确认,失败时不记成功;业务结论是转换必须只有一个边界层。
演练样例 8:最小单位复算
金额 102.35 USD(美元)按两位最小单位转换为 10235。渠道回传 10235 与 USD(美元),反转为 102.35 后与支付单相等;回传 10234 即使状态成功也进入金额不符,不可确认。演练公式:102.35 × 10^2 = 10235,反向为 10235 ÷ 10^2 = 102.35。
热门面试题
- 问题(基础题):为什么渠道金额常用最小单位整数?
- 考点:精度、协议边界。
- 回答思路:整数避免传输端浮点歧义。
- 详细答案:最小单位整数让请求和签名字段有确定表达,避免小数点格式、尾零和浮点序列化造成的差异。但整数本身没有币种语义,必须与固定币种及小数位规则一起保存和反查。
- 进阶追问:所有币种都是两位小数吗?
- 进阶回答:不是,必须以币种规则表和渠道能力为准;具体支持范围待源码或现场核对。
- 问题(原理题):金额核验要比较哪些字段?
- 考点:总额、币种、商户归属。
- 回答思路:不只比数值。
- 详细答案:至少比较本地支付单号或商户请求号、应付金额、币种、收款主体和渠道交易状态。只比较金额会把同金额的其他订单误认进来;只比较订单号会漏掉币种或手续费配置错误。
- 进阶追问:服务费是否允许渠道侧另算?
- 进阶回答:本地应固化应付总额;渠道附加费若无法提前确定,应另作费用事实,不能悄悄改变本次确认金额。
- 问题(项目题):源码中的金额权威性如何描述?
- 考点:后端重算、事实限制。
- 回答思路:引用服务费计算和格式化,不延伸货币规则。
- 详细答案:
SellerRechargeBusiness.createPaymentOrder会按模板重算服务费并组合payAmount,而不是直接信任前端服务费;代码也使用BigDecimal。货币代码、最小单位转换、精度表和汇率来源在已读片段中未证实,需待源码或现场核对。 - 进阶追问:为什么充值到账额与支付额可不同?
- 进阶回答:服务费会使渠道实付高于到账额;两个字段都应固化并在确认时按各自口径核验。
币种锁定、汇率快照与跨币种边界
支付单创建后锁定交易币种,退款默认使用原交易币种和原金额边界。跨币种报价应保存报价币种、结算币种、汇率来源、报价时间和有效期;汇率是报价事实,不应在回调时用最新汇率重算原订单。
| 场景 | 必存快照 | 禁止做法 | 待核对项 |
|---|---|---|---|
| 单币种收款 | 交易币种、应付金额 | 回调时改币种 | 渠道币种能力 |
| 跨币种报价 | 报价币种、结算币种、汇率 | 用实时汇率覆盖 | 汇率来源和有效期 |
| 退款 | 原交易币种、累计退款 | 按今日汇率退款 | 渠道退款规则 |
sequenceDiagram
participant O as 下单服务
participant R as 报价服务
participant P as 支付单
participant K as 渠道
O->>R: 请求报价
R-->>O: 汇率快照与有效期
O->>P: 固定币种、金额、汇率快照
P->>K: 以结算币种请求
K-->>P: 回传交易币种与金额
P->>P: 与快照核验,不取最新汇率图解:报价服务只在建单前参与,支付确认只读快照;前提是汇率快照版本可追溯。正常路径回传同币种同金额,失败路径是币种不符进入差异;业务结论是汇率波动不应篡改已产生的付款义务。
演练样例 9:汇率快照
客户展示 100.00 EUR(欧元),报价快照为 1 EUR(欧元)= 1.10 USD(美元),支付单结算金额固定为 110.00 USD(美元)。两小时后市场汇率变为 1.12,渠道仍返回 110.00 USD(美元)则确认;不得把订单改成 112.00。若渠道只接受 EUR(欧元),则支付单应固定 EUR(欧元)而不是临时换算。
热门面试题
- 问题(基础题):为什么支付成功后不能改币种?
- 考点:资金事实、退款、对账。
- 回答思路:币种是金额的一部分。
- 详细答案:100 并不构成完整金额,必须同时有币种。支付后改币种会破坏渠道交易核验、退款上限和对账口径,也无法解释汇兑差从哪里产生。因此币种要在支付单创建时锁定,变化应新建报价或新支付单。
- 进阶追问:展示币种可以变吗?
- 进阶回答:可以作为展示层换算,但必须标明参考汇率,不能影响交易与结算币种快照。
- 问题(原理题):汇率快照包含哪些最少字段?
- 考点:可复算、可审计。
- 回答思路:从来源、方向、时间和版本回答。
- 详细答案:至少有基准币种、报价币种、汇率值、报价时间、失效时间、来源标识和计算版本;若有点差或手续费也需固化。这样退款、客诉和对账才能按当时的事实复算,而非依赖今天的数据。
- 进阶追问:汇率过期后用户继续付款怎么办?
- 进阶回答:关闭旧支付单或明确其不可支付,重新报价并创建新付款义务;不能无提示地更改旧单金额。
- 问题(项目题):本项目能否声称支持跨币种结算?
- 考点:证据边界。
- 回答思路:明确未见到相应实体与规则。
- 详细答案:不能。已核对源码片段证明有金额、服务费、支付渠道和支付区域字段,但未证明货币代码、汇率快照或跨币种结算流程。本节是可复用的设计和演练,生产币种清单、汇率服务及结算规则均待源码或现场核对。
- 进阶追问:面试中如何避免夸大?
- 进阶回答:说“我会按该模型设计并优先核对现有字段”,把已经读到的类和方法与待确认项分开陈述。
BigDecimal(高精度十进制数)、DECIMAL(定点小数)与舍入余差
Java(编程语言)计算使用 BigDecimal(高精度十进制数),数据库金额列使用 DECIMAL(定点小数);构造 BigDecimal(高精度十进制数)使用字符串或精确整数,不用 double(双精度浮点数)。金额规则必须显式给出精度和舍入方式,分摊时最后一项吸收余差并记录原因。
| 环节 | 推荐表示 | 原因 | 校验 |
|---|---|---|---|
| 领域计算 | BigDecimal(高精度十进制数) | 十进制精确运算 | 指定精度与舍入 |
| 数据库存储 | DECIMAL(定点小数) | 可审计定点值 | 列精度匹配币种 |
| 渠道传输 | 最小单位整数或规范小数 | 协议确定性 | 双向转换一致 |
| 分摊 | 明细加余差项 | 合计守恒 | 明细和等于总额 |
flowchart LR
T[总额 10.00] --> W[按权重初算]
W --> A[3.33]
W --> B[3.33]
W --> C[3.33]
A --> S[小计 9.99]
B --> S
C --> S
S --> R[余差 0.01 给最后一项]
R --> Z[3.33 + 3.33 + 3.34 = 10.00]图解:三个初算节点显示常见尾差,余差箭头只改变明确的最后一项。前提是固定排序和舍入规则;正常路径总额守恒,失败路径是重复分摊导致两次吸收余差;业务结论是余差归属必须可复算而非随机。
演练样例 10:三项分摊余差
10.00 USD(美元)平均分三项,按两位小数向下得到 3.33、3.33、3.33,小计 9.99,余差 0.01。按“最后一项吸收”规则,结果为 3.33、3.33、3.34,复算 3.33 + 3.33 + 3.34 = 10.00。记录分摊顺序与舍入方式,退款时按同一明细反算。
热门面试题
- 问题(基础题):为什么不能用
double(双精度浮点数)算钱?- 考点:二进制误差、累计误差。
- 回答思路:说明多数十进制小数无法被二进制精确表示。
- 详细答案:
double(双精度浮点数)以二进制浮点存储,0.1 等十进制数常是近似值,乘除、累加和序列化后可能出现尾差。资金系统要求可复算和可对账,必须用 BigDecimal(高精度十进制数)或最小单位整数并明确规则。 - 进阶追问:BigDecimal(高精度十进制数)就永远不会有问题吗?
- 进阶回答:仍会有除不尽、精度和舍入选择问题;必须指定小数位与舍入方式,不能依赖默认行为。
- 问题(原理题):为什么最后一项吸收余差?
- 考点:守恒、确定性。
- 回答思路:先独立舍入,再把总额差一次性归属。
- 详细答案:多项分别舍入会使明细和偏离总额。固定排序后由最后一项或规则指定项吸收
总额减明细初算和,既保证守恒又能重放;随机分配会让同一订单每次重算不同,破坏退款和审计。 - 进阶追问:最后一项是负数怎么办?
- 进阶回答:先限制余差不得超过一最小单位乘可调整项数,并按业务规则选择可调整项;异常则拒绝计算并人工核验。
- 问题(项目题):源码出现 BigDecimal(高精度十进制数)能证明什么?
- 考点:局部事实、完整性边界。
- 回答思路:只说明可见计算类型和格式化。
- 详细答案:
SellerRechargeBusiness可见使用 BigDecimal(高精度十进制数)校验到账金额、计算服务费和格式化金额,这可作为局部实现事实。数据库列类型 DECIMAL(定点小数)、统一舍入策略、分摊余差规则没有在已读片段证实,必须待源码或现场核对。 - 进阶追问:如何验证线上精度问题?
- 进阶回答:抽取支付单、明细和渠道流水做逐笔最小单位复算,按币种分组检查差异并保留原始计算版本。
支付单金额锁定与订单改价
支付单是对某个时点付款义务的快照。订单改价、优惠变化或服务费调整后,不修改已经受理的支付单;应关闭或标记旧单不可支付,再创建带新版本的新支付单,避免回调金额无法对应。
flowchart TD
A[订单价格变化] --> B{支付单是否已受理}
B -- 否 --> C[关闭旧单并新建]
B -- 是 --> D{渠道结果是否已确认}
D -- 否 --> E[未知态,先查单]
D -- 成功 --> F[退款或补收新义务]
D -- 失败 --> C图解:两个判断节点分别隔离外部副作用和最终资金事实;前提是改价有版本号。正常路径未受理可重建,失败路径受理未知必须查单;业务结论是改价不能通过覆盖 payAmount(支付金额)实现。
演练样例 11:改价与迟到成功
PO-904 金额 80.00 USD(美元)已受理后订单优惠使新价为 75.00。系统先把旧单置未知并查单,查到旧单成功则确认 80.00 并按业务规则退款 5.00 或生成补偿,不允许直接把旧单金额改成 75.00;查到未发生则关闭旧单,创建 75.00 的新单。
本地事务、外部副作用与 Outbox(发件箱)
本地数据库事务只能保证本地支付单、确认事实和 Outbox(发件箱)事件原子提交,不能把渠道扣款和消息投递包进同一个本地事务。外部副作用要用稳定请求号、查单和补偿处理;事件在提交后异步投递,消费者也以业务键幂等。
| 边界 | 同一事务内 | 事务外 | 失败恢复 |
|---|---|---|---|
| 建单 | 支付单、尝试、请求号 | 调用渠道 | 超时后查单 |
| 成功确认 | 渠道交易、状态更新、Outbox(发件箱) | 发布事件 | 扫描未投递事件 |
| 下游消费 | 消费去重与本地动作 | 通知其他系统 | 重复消费幂等 |
sequenceDiagram
participant P as 支付确认器
participant DB as 本地数据库
participant O as Outbox
participant M as 消息投递器
participant S as 下游服务
P->>DB: 事务:写交易事实和支付成功
P->>O: 同事务写事件
DB-->>P: 提交
M->>O: 拉取未投递事件
M->>S: 投递成功事实
S->>S: 以业务键幂等消费图解:事务边界只圈住数据库和 Outbox(发件箱),投递器在提交后工作。前提是事件有唯一业务键与投递状态;正常路径最终被消费,失败路径事件保留待重投;业务结论是不能在事务内假定消息一定送达。
演练样例 12:确认后投递失败
PO-905 已确认成功,本地事务同时写入 PAYMENT_CONFIRMED:PO-905 的 Outbox(发件箱)记录;投递进程宕机,业务订单暂未感知,但支付单不会回滚。恢复后扫描未投递事件并投递;下游已消费过同一业务键时返回幂等成功,最终只产生一次订单推进。
热门面试题
- 问题(基础题):为什么不能把渠道调用放在长数据库事务里?
- 考点:锁时长、外部不可回滚。
- 回答思路:渠道响应慢且不能随本地回滚。
- 详细答案:长事务会持有行锁并扩大死锁和连接耗尽风险;即使本地事务回滚,渠道也可能已经扣款。本地先持久化请求意图,外部调用使用稳定请求号,结果再以新事务确认,才有可恢复边界。
- 进阶追问:先调用渠道后落库可以吗?
- 进阶回答:风险更大,进程崩溃会丢失本地关联;至少应先保存请求意图和可查的业务号。
- 问题(原理题):Outbox(发件箱)解决了什么,不解决什么?
- 考点:原子提交、至少一次投递。
- 回答思路:区分本地一致性和端到端恰好一次。
- 详细答案:它保证支付确认与待投递事件同库提交,避免成功状态已写而事件凭空丢失;投递仍可能重复或延迟,因此消费者必须幂等。它不替代渠道查单,也不自动解决跨系统所有副作用。
- 进阶追问:如何避免 Outbox(发件箱)无限堆积?
- 进阶回答:监控未投递年龄、失败次数和积压量,重试预算耗尽转人工;已投递且过审计窗口的数据可归档。
- 问题(项目题):能否宣称现有充值代码使用了 Outbox(发件箱)?
- 考点:不虚构、源码范围。
- 回答思路:明确已读片段没有该实体或调用。
- 详细答案:不能。已读
SellerRechargeBusiness.onPaymentSuccess展示了事务内更新支付单、充值订单、余额和日志,但没有在本次核对范围内找到 Outbox(发件箱)实体或投递调用。本节是建议的边界模型,是否已落地待源码或现场核对。 - 进阶追问:现有事务是否完全可靠?
- 进阶回答:它能说明局部本地更新受事务包裹,外部渠道与消息副作用的一致性仍需继续沿调用链核对。
订单取消与支付成功竞争
取消与支付成功是典型双写竞争:客户取消希望停止付款,渠道成功可能已发生。正确原则是先确认外部事实,再由状态机决定谁能胜出;取消请求不能把未知资金结果抹掉,成功回调也不能绕过已确认取消。
| 竞争时点 | 本地动作 | 外部确认 | 后续业务 |
|---|---|---|---|
| 未受理取消 | 取消支付单 | 无需查单 | 订单可取消 |
| 受理中取消 | 标记取消处理中 | 关闭会话并查单 | 保持未知 |
| 成功先到 | 成功终态胜出 | 核验交易 | 退款或人工处理 |
| 取消先确认 | 取消终态胜出 | 迟到成功查单 | 冲突处置,不覆盖 |
sequenceDiagram
participant U as 客户
participant C as 取消服务
participant K as 渠道
participant S as 成功回调
participant DB as 状态机
U->>C: 请求取消
C->>K: 关闭会话或取消请求
par 渠道异步
K-->>S: 可能已成功
and 本地取消
C->>DB: 条件更新取消处理中
end
S->>K: 查单核验
S->>DB: 仅按合法迁移裁决图解:并行框刻意表达现实中无法靠调用顺序避免竞争;前提是取消和成功都携带同一支付单。正常路径按先被条件更新确认的终态裁决,失败路径是渠道事实与本地取消冲突;业务结论是冲突应进入退款或人工流程,而非覆盖历史。
热门面试题
- 问题(基础题):用户点取消后是否一定不会扣款?
- 考点:异步性、结果未知。
- 回答思路:取消指令和扣款在不同系统并发。
- 详细答案:不一定。用户操作只表示本地发起取消,渠道可能已经受理或在路上完成扣款。系统必须将该支付单限制为取消处理中或未知,关闭会话后查单,再依据渠道事实决定取消、成功后退款或人工核验。
- 进阶追问:为什么不立即释放库存?
- 进阶回答:支付结果仍未知时释放可能让订单既扣款又失去履约资源;库存策略要由订单履约分册按同一确认事实协调。
- 问题(原理题):条件更新如何处理取消成功竞争?
- 考点:单一胜出、冲突证据。
- 回答思路:两个动作都只从允许源状态推进。
- 详细答案:取消与成功更新均带
where status in (...),只有第一个提交者影响一行。失败者读取当前终态:同结果则幂等,异结果则保存渠道证据并进入冲突处置。不能为了“让接口成功”再次覆盖对方终态。 - 进阶追问:取消终态后收到真实成功交易怎么办?
- 进阶回答:记录冲突交易并查实金额;若已扣款,按退款或人工资金处理,不把支付单悄悄改回成功。
- 问题(项目题):源码对关闭支付有什么可见限制?
- 考点:终态校验、会话边界。
- 回答思路:引用
closePayment的终态和支付中检查。 - 详细答案:
SellerRechargeBusiness.closePayment会拒绝成功、失败、取消的支付单,并要求关联充值订单仍处于支付中,然后请求渠道仅关闭支付会话。该流程说明有状态门禁;取消请求与迟到渠道成功的最终竞态处理仍待源码或现场核对。 - 进阶追问:为何只关闭会话?
- 进阶回答:会话是付款媒介,业务订单是否取消必须由订单规则独立裁决,避免支付操作越权改变履约事实。
编号、唯一流水与可追溯性
编号承担定位与幂等,不承担金额真实性。支付单号、尝试号、请求号、会话号、渠道交易号和退款单号要各自有来源与唯一边界;业务日志必须能把它们串成证据链。
支付失败恢复、未知态与人工核验
失败恢复遵循“先确认、再补做”:技术失败、业务明确失败和结果未知必须分流。未知态保留原始请求、响应摘要、最后查询时间和下一步计划;达到自动恢复预算后转人工,而不是无限重试或直接置失败。
sequenceDiagram
participant X as 异常处理器
participant Q as 查单任务
participant K as 渠道
participant H as 人工核验
X->>Q: 记录未知与查询计划
Q->>K: 按稳定请求号查单
alt 查到一致成功
K-->>Q: 成功交易
Q-->>X: 确认成功
else 可重试技术失败
K-->>Q: 暂时无结果
Q->>Q: 退避并保留未知态
else 金额币种冲突或预算耗尽
K-->>Q: 冲突或无结论
Q->>H: 提交证据链
end图解:参与者分别承担异常归档、主动查单、外部证据和人工裁决;分支按证据强度而非异常名称分流。前提是已持久化请求号和查询历史;正常路径有明确证据就进终态,失败路径被留在未知或人工队列;业务结论是异常处理的产物是证据链,不是粗暴的失败状态。
源码映射、接口边界与项目表达
面试表达要把“源码已证明”与“目标设计”分开:前者说准确类和方法,后者用“建议”“演练样例”描述。支付领域对象是跨渠道稳定内核,渠道接口只负责结账、取消和退款等能力,不让渠道差异侵入订单状态机。
| 源码对象 | 可证明结论 | 本分册使用方式 | 不能推断 |
|---|---|---|---|
PaymentBusiness.createPaymentOrder | 建单、待支付、编号 | 支付单起点 | 完整状态图 |
PaymentBusiness.verifyPay | 归属与待支付校验 | 受理前门禁 | 渠道确认 |
SellerRechargeBusiness.createAndCheckout | 充值建单与结账编排 | 订单和支付单分离 | 通用订单模型 |
SellerRechargeBusiness.onPaymentSuccess | 事务内成功更新 | 终态保护线索 | 复式账完整实现 |
erDiagram
CUSTOMER_ORDER ||--o{ PAYMENT_ORDER_DETAIL : 关联
PAYMENT_ORDER ||--o{ PAYMENT_ATTEMPT : 产生
PAYMENT_ATTEMPT ||--o| CHANNEL_SESSION : 获得
PAYMENT_ATTEMPT ||--o| CHANNEL_TRANSACTION : 核验
PAYMENT_ORDER ||--o{ REFUND_ORDER : 限额
PAYMENT_ORDER ||--o{ PAYMENT_FACT : 追加图解:实体关系图展示对象可追溯性而不是表结构事实;前提是表名和列名仅是设计建议。正常路径从订单到确认事实闭环,失败路径保留尝试无交易;业务结论是代码类名不等于完整数据模型,实际表结构仍待源码或现场核对。

正式 PlantUML(开源建模工具)图把状态机和确认时序放在同一图中:状态区域表达合法迁移和终态保护,右侧参与者箭头表达客户订单、支付单、渠道、回调/查单、本地条件更新、Outbox(发件箱)和下游确认。前提是渠道回传经过核验;正常时提交后投递确认事实,失败时停留未知并查询恢复;业务结论是外部结果与本地成功之间必须存在可审计的确认门。
热门面试题
- 问题(基础题):支付领域最重要的设计思想是什么?
- 考点:分治、不可变事实、未知态。
- 回答思路:先拆对象,再用状态机约束快照。
- 详细答案:把订单、支付单、尝试、会话、交易和退款分开,让每个对象只回答一个问题;把请求、回调和查单结果保存为不可变事实;由状态机、唯一键和条件更新产出当前快照。未知态让证据不足时不做资金结论。
- 进阶追问:这套模型最大的成本是什么?
- 进阶回答:对象和查询链路更多,需要索引、关联查询和运维工具,但换来可解释、可恢复和可审计的资金行为。
- 问题(原理题):如何向业务解释“超时不是失败”?
- 考点:分布式不确定性、用户体验。
- 回答思路:用证据缺失而非结果失败说明。
- 详细答案:超时是调用方没有在期限内获得响应,并没有证明渠道没执行。把它标为“确认中”或“结果核验中”,同时展示稳定订单号和查询进度,比错误提示失败后诱导用户重付更安全。
- 进阶追问:用户急着完成订单怎么办?
- 进阶回答:提供查询刷新和人工入口,确认未发生后才开放新支付方式;不能以牺牲资金正确性换取表面即时性。
- 问题(项目题):一段可信的项目话术怎样组织?
- 考点:事实与设计区分、可复述。
- 回答思路:先说可证明代码,再说风险和补强。
- 详细答案:可以说:“我在
PaymentBusiness中看到支付单创建、卖家归属和待支付校验;充值编排中还看到创建新支付单、关闭旧会话以及异常状态。基于这些事实,我会用支付尝试、渠道交易、条件更新和查单把重复与未知结果收口。完整验签、表约束和对账机制需继续核对。” - 进阶追问:为什么这比直接说“我们做了幂等”更好?
- 进阶回答:它给出可定位证据、明确边界和补强路径,面试官能追问具体实现而不会暴露虚构细节。
题库分界
2. 综合题与项目话术
- 问题(综合题):请完整说明为什么支付要把业务订单、支付单、支付尝试、渠道会话和渠道交易分开?
- 口述答案:我会先把它看成五个时间尺度不同的对象,而不是一张订单表上的几个状态。业务订单表达客户买什么、是否履约和取消规则;支付单表达一笔固定金额与固定币种的付款义务;支付尝试记录用户点击、系统重试或换方式后的某一次外部请求;渠道会话是收银台或授权页的短生命周期凭据;渠道交易才是渠道侧可核验的资金事实。这样一笔订单可以有一次付款义务,一笔付款义务可以有多次尝试,但只有一个能生效的确认结果。页面关闭、请求超时或会话过期只影响尝试或会话,不能反向把客户订单写失败。确认时我会用渠道交易唯一键、金额币种核验和支付单条件更新裁决,再把确认事实提交给订单域。这样保留每次尝试的证据,也能解释为何没有盲目重发扣款。实现细节以已读
PaymentBusiness的建单和待支付校验为事实基础,完整表结构仍待源码或现场核对。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:领域对象与事实边界
- 追问:一笔订单能有多个成功支付单吗?直接回答:默认不能;需要余额拆分时也要定义累计应收与唯一生效额度。
- 追问:会话过期等于支付失败吗?直接回答:不等于,只说明交互凭据失效,仍要按请求号查渠道结果。
- 追问:为什么不直接用渠道交易号做订单号?直接回答:渠道交易号产生得晚且格式受外部控制,无法覆盖未返回渠道号的尝试。
- 口述答案:我会先把它看成五个时间尺度不同的对象,而不是一张订单表上的几个状态。业务订单表达客户买什么、是否履约和取消规则;支付单表达一笔固定金额与固定币种的付款义务;支付尝试记录用户点击、系统重试或换方式后的某一次外部请求;渠道会话是收银台或授权页的短生命周期凭据;渠道交易才是渠道侧可核验的资金事实。这样一笔订单可以有一次付款义务,一笔付款义务可以有多次尝试,但只有一个能生效的确认结果。页面关闭、请求超时或会话过期只影响尝试或会话,不能反向把客户订单写失败。确认时我会用渠道交易唯一键、金额币种核验和支付单条件更新裁决,再把确认事实提交给订单域。这样保留每次尝试的证据,也能解释为何没有盲目重发扣款。实现细节以已读
- 问题(综合题):一单多次支付尝试时,怎样保证只有唯一生效结果?
- 口述答案:我的做法是让“可重复发起”和“可重复生效”彻底分离。每次点击创建新的支付尝试,保存稳定请求号、渠道、时间和请求摘要;支付单仍是唯一的付款义务。收到回调或查单结果时,先写渠道交易不可变事实,并以渠道代码加渠道交易号做唯一约束,重复事件会读到已有记录。随后执行带源状态条件的更新,只有处于处理中或未知的支付单能推进到成功;影响行数为零时读取当前状态,同结果幂等返回,异结果记录冲突而不覆盖。对于并行会话,我会默认只允许一个可付款窗口,旧会话先关闭并查单;关闭超时不等于安全重建,必须进入未知态。这个模型还能处理迟到回调:旧尝试即便真的成功,也会发现支付单已有生效结果,进入冲突核验或退款处置,不会让订单消费两次资金事实。锁可以降低同一支付单的并发,但最后正确性来自唯一键和条件更新。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:条件更新、唯一键与并发竞态
- 追问:唯一索引冲突返回失败吗?直接回答:相同关键字段是预期幂等,返回既有结果;字段不同才是冲突。
- 追问:分布式锁失效怎么办?直接回答:数据库唯一约束和条件更新仍阻止第二个生效结果。
- 追问:多方式并行付款如何做?直接回答:必须对支付单总额度做原子裁决,胜出者之外的交易进入退款或人工处置。
- 问题(综合题):支付状态机应怎样设计,为什么未知态不可少?
- 口述答案:状态机不是枚举一串字符串,而是规定状态、证据、触发者和不可逆边界。我会把支付单定义为待支付、处理中、成功、失败、取消和未知:待支付经渠道受理进入处理中;有金额、币种、订单归属均一致的确认事实才能成功;渠道明确拒绝才失败;渠道确认关闭才取消。未知代表超时、回调与本地冲突或查单暂时无结论,它不是失败,因为超时只能证明我没有拿到响应,不能证明渠道没有扣款。成功、失败和取消是终态,迟到事件不能覆盖;退款另建退款单,不把成功改成失败。实现上每个迁移都用条件更新限定来源状态,受影响零行就读取当前快照并按幂等或冲突处理。未知态允许回调、主动查单和对账继续补证据,避免用户因前端报错重复付款。它的代价是需要查询任务和人工队列,但资金领域宁可把“不知道”显式暴露,也不能伪造一个失败结论。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付状态机与合法迁移
- 追问:未知态可以直接重试扣款吗?直接回答:不可以,先查单;只有确认未发生外部副作用才能新建尝试。
- 追问:终态收到相反回调怎么办?直接回答:保留冲突证据并核验渠道事实,不能直接覆盖终态。
- 追问:退款为何不在状态机里?直接回答:退款是独立反向义务,有自己的金额上限和渠道确认生命周期。
- 问题(综合题):如何处理支付超时,避免既漏单又重复扣款?
- 口述答案:我先区分“调用超时”和“渠道失败”。调用超时只意味着本地没有及时得到响应,因此支付尝试必须先持久化稳定请求号、支付单号、金额币种和请求时间。超时后将尝试和支付单置为未知或确认中,不再创建新的扣款请求;补偿任务优先用请求号或商户订单号主动查单。查到成功时核验收款主体、订单号、金额和币种,写入渠道交易事实并条件更新支付单;查到明确失败才进入失败并开放新尝试;查单自身超时则采用退避和恢复预算,最终由对账补齐。回调若先到也只是一条候选事实,仍应与查单或可信证据核对。对用户展示的是“正在确认”,而不是诱导重复支付的失败页。恢复期间所有查询历史和原始响应摘要都要保留,使人工能判断是否扣款。核心原则是先确认外部副作用再补做本地动作,绝不把网络错误当作未执行。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:回调、查单与对账三层确认
- 追问:查单多久一次?直接回答:间隔和上限取决于渠道能力,待源码或现场核对;应有退避与总预算。
- 追问:没有请求号怎么办?直接回答:用可证明的商户订单号查;两者都没有就冻结自动重试并人工核验。
- 追问:对账晚到如何处理?直接回答:将它作为延迟确认或差异证据,按状态机补齐,不能静默修改历史。
- 问题(综合题):支付金额为什么要用最小单位、BigDecimal(高精度十进制数)和 DECIMAL(定点小数)?
- 口述答案:金额正确性要同时解决计算、存储和协议传输三个边界。领域计算我使用 BigDecimal(高精度十进制数),因为十进制小数不能可靠地由
double(双精度浮点数)表示;数据库使用 DECIMAL(定点小数)或明确的整数最小单位,避免二进制浮点累计误差;渠道边界则按币种规则转成整数最小单位或规范小数。支付单创建时固化订单金额、服务费、应付总额和币种,渠道回传时从最小单位反向转换并逐项核验。任何分摊都要指定精度、舍入方式和固定排序,最后一项吸收余差,使明细和严格等于总额。这样退款、对账和人工核验可以复算同一笔钱,而不会出现 0.1 加 0.2 的尾差。现有充值源码可证明使用 BigDecimal(高精度十进制数)并由后端重算服务费,但实际数据库精度、币种表和渠道转换规则仍待源码或现场核对,不能据此虚构生产配置。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:金额计算与舍入
- 追问:为什么金额相同还要验币种?直接回答:数字没有币种不构成完整金额,同为 100 的不同币种价值不同。
- 追问:余差能随机分给明细吗?直接回答:不能,必须固定规则与顺序,才能重放和审计。
- 追问:最小单位总是乘 100 吗?直接回答:不是,应按币种与渠道规则确定。
- 口述答案:金额正确性要同时解决计算、存储和协议传输三个边界。领域计算我使用 BigDecimal(高精度十进制数),因为十进制小数不能可靠地由
- 问题(综合题):跨币种支付如何设计汇率快照,为什么不能在回调时重算?
- 口述答案:跨币种设计的关键是把报价与确认分开。建单前,报价服务给出报价币种、结算币种、汇率、点差、来源、报价时间和失效时间;支付单把这些字段连同应付金额一起固化,形成当时的付款义务。发起渠道时按固化的结算币种和最小单位发送;回调或查单只与这份快照核验,不能读取今天的实时汇率重新计算昨天的订单,否则市场波动会让同一渠道交易在本地变成金额不符。退款通常也以原交易币种和原确认金额为上限,汇兑损益若存在需作为独立业务事实处理。展示层可以给用户显示实时参考价,但必须明确它不改变结算事实。当前已读源码没有证明货币代码、汇率来源或跨币种结算实现,所以我会把这一段作为通用设计,并明确这些生产规则待源码或现场核对。这种表达能说明我理解资金边界,同时不夸大项目经历。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:币种锁定、汇率快照与跨币种边界
- 追问:报价过期后可以继续付款吗?直接回答:应关闭旧付款义务并重新报价建单,不能静默改旧金额。
- 追问:退款按今日汇率吗?直接回答:资金退款按原交易事实,汇兑差另行记录。
- 追问:不同币种精度不同怎么办?直接回答:由币种规则表驱动转换和舍入,不在业务代码中写死两位。
- 问题(综合题):本地事务与外部支付渠道怎样划边界?
- 口述答案:我不会尝试用一个本地数据库事务包住渠道扣款,因为渠道是外部副作用,网络超时后无法通过本地回滚判断它是否执行。建单阶段先在本地事务保存支付单、支付尝试和稳定请求号,然后在事务外调用渠道;调用结果无论成功、超时还是报错都保留证据。确认阶段再开启短事务,写入渠道交易不可变事实、用条件更新推进支付单,并同事务写 Outbox(发件箱)事件。事务提交后独立投递器发送确认事件,下游按业务键幂等消费。这样即使投递进程宕机,本地成功和待投递事件仍一致;即使消息重复,下游不会重复履约。渠道调用失败后走查单与对账,而不是回滚本地意图或重发扣款。现有充值代码可看到事务内更新支付单、充值订单和余额日志,但在本次核对范围没有发现 Outbox(发件箱)实体,所以我会明确它是建议补强,而不是声称源码已经实现。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:事务与发件箱
- 追问:Outbox(发件箱)能保证全链路恰好一次吗?直接回答:不能,它保证本地状态和待投递事件原子,消费端仍需幂等。
- 追问:外部调用前为何先落库?直接回答:崩溃后还能凭请求号恢复和查单,不会丢失关联。
- 追问:事务里可以发消息吗?直接回答:不应把成功依赖在同步发送上,应提交后异步投递。
- 问题(综合题):订单取消和支付成功同时发生时,你会如何裁决?
- 口述答案:我把它当作两个独立系统的并发事实,而不是谁先到接口谁就赢。用户取消先把支付单限制为取消处理中,并向渠道关闭会话或发送取消请求;但这只说明我发起了动作,不能说明渠道没有扣款。与此同时成功回调到达时,处理器必须查单核验真实交易,再通过状态条件更新与取消动作竞争。第一个合法迁移到终态的操作胜出,另一个读取当前状态:同结果幂等,异结果记录冲突。若本地已取消但渠道查实成功,不能偷偷把取消改成功或假装失败,而应保留两类事实并走退款、客诉或人工资金处置;若成功先确认,取消转为退款或订单后续规则。库存与履约也应等待可消费的确认结果,避免释放库存后又出现真实扣款。这样的设计承认分布式系统无法完全消灭竞态,但能把竞态限制在一个可审计、可补偿的状态机边界内。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:订单取消与支付成功竞争
- 追问:取消请求是否可重试?直接回答:可以按稳定取消请求号查询和重试,但不能假设重试前渠道未成功。
- 追问:本地取消后能马上创建新付款吗?直接回答:不能,先确认旧尝试没有资金结果。
- 追问:冲突状态如何展示给用户?直接回答:展示确认中并提供订单号查询,不展示不真实的失败结论。
- 问题(综合题):如何设计支付编号与幂等键,避免它们被混为一谈?
- 口述答案:编号解决定位,幂等键解决重复语义,两者相关但不是同一个概念。支付单号在本地建单时生成,代表付款义务;尝试号区分同一支付单的多次发起;请求号作为发给渠道的稳定商户标识,用于查单和重复请求收敛;渠道交易号由渠道产生,代表外部资金事实;退款单号标识反向付款义务。幂等键应绑定租户、操作类型、付款义务和请求参数摘要,相同键相同参数返回原结果,相同键不同金额或币种必须拒绝。数据层对支付单、渠道交易、回调事件和退款单建立各自唯一边界,状态层再通过条件更新防止乱序覆盖。不能因为支付单号带日期和校验位就认定它具有幂等性,校验位最多发现格式错误;也不能拿渠道交易号做本地初始主键,因为调用失败前它不存在。现有
PaymentBusiness确实生成支付单号,但唯一索引和全局范围需继续核对。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:编号、唯一流水与可追溯性
- 追问:幂等键多久保留?直接回答:至少覆盖回调、查单和审计窗口,具体期限待现场规则核对。
- 追问:相同键不同参数怎么办?直接回答:拒绝并记录冲突,不能按新参数覆盖旧意图。
- 追问:渠道无交易号怎么办?直接回答:依赖请求号与事件标识组合,并把不可唯一识别结果交人工核验。
- 口述答案:编号解决定位,幂等键解决重复语义,两者相关但不是同一个概念。支付单号在本地建单时生成,代表付款义务;尝试号区分同一支付单的多次发起;请求号作为发给渠道的稳定商户标识,用于查单和重复请求收敛;渠道交易号由渠道产生,代表外部资金事实;退款单号标识反向付款义务。幂等键应绑定租户、操作类型、付款义务和请求参数摘要,相同键相同参数返回原结果,相同键不同金额或币种必须拒绝。数据层对支付单、渠道交易、回调事件和退款单建立各自唯一边界,状态层再通过条件更新防止乱序覆盖。不能因为支付单号带日期和校验位就认定它具有幂等性,校验位最多发现格式错误;也不能拿渠道交易号做本地初始主键,因为调用失败前它不存在。现有
- 问题(综合题):为什么锁不能替代数据库约束和状态条件更新?
- 口述答案:锁的价值是降低热点竞争和减少无效外部调用,但它不是资金正确性的最终证明。分布式锁可能因租约到期、实例暂停、网络分区或错误释放而失效;即使锁正常,跨进程重试和渠道回调也可能绕开同一个锁键。因此我会把锁放在入口或同一支付单的短临界区,用它降低并发;真正决定是否产生一条渠道交易事实的是数据库唯一索引,真正决定是否推进成功终态的是包含原状态的原子条件更新。两个实例同时处理同一回调时,一个能插入渠道交易并更新状态,另一个遇到唯一冲突或影响零行后读取既有结果并幂等返回。这样即便锁的可用性出现问题,系统仍不会因第二个处理器重复确认资金。已读充值源码能证明存在按支付单和回调构造的锁,但锁服务的实现语义和数据库表约束尚未核对,所以项目表达必须避免说“锁保证了资金一致”。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:条件更新、唯一键与并发竞态
- 追问:悲观锁是否更安全?直接回答:可序列化本地读写,但不能跨渠道长持有,仍需最终条件裁决。
- 追问:唯一冲突后能再插一条吗?直接回答:不能,应读取既有事实并判断是否同一事件。
- 追问:锁失败返回什么?直接回答:返回处理中或可查询语义,避免错误标为支付失败。
- 问题(综合题):重复回调和乱序回调应怎样处理?
- 口述答案:处理回调时我不会直接按到达顺序覆盖支付状态,而是先把回调视为候选事实。第一步保存事件标识、渠道交易号、原始摘要和接收时间,并用渠道加事件标识或交易号去重;第二步按本地请求号找到支付单并核验金额、币种、商户归属;第三步必要时主动查单,取得渠道当前最终状态;最后在短事务内执行状态条件更新。重复成功回调会命中已有事件或已成功状态,返回幂等受理;迟到失败回调发现支付单已成功时只记录冲突,不可反向覆盖;迟到成功回调发现已取消时也不能直接改成功,应查明渠道是否真实扣款并交由退款或人工补偿处理。回调顺序不可靠的根本原因是网络、重试和多实例投递,而状态机以合法迁移和终态保护将这种不可靠性隔离在确认层。具体渠道的签名和事件格式必须待源码或现场核对,本分册只定义统一处理原则。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付状态机与合法迁移
- 追问:回调已验签是否可跳过查单?直接回答:不建议,验签证明来源,查单补强最终状态和金额核验。
- 追问:无事件标识如何去重?直接回答:以渠道交易号和关键业务字段组合,并降低自动确认范围。
- 追问:回调处理失败如何回复?直接回答:按渠道协议决定重试语义,具体协议待核对,但本地必须先保留事件证据。
- 问题(综合题):支付成功确认的最小核验集是什么?
- 口述答案:我会把成功确认定义成“渠道证据通过本地付款义务校验后的领域决定”,而不是一个回调字段。最小核验集包括:本地支付单或稳定商户请求号能唯一定位;收款主体和渠道身份符合配置;回传交易号未被其他支付单占用;回传金额等于已锁定的应付金额;币种相同且最小单位转换可逆;渠道状态达到该渠道的最终成功语义;支付单当前状态允许从处理中或未知迁移。通过后在同一个本地事务写渠道交易事实、更新时间和支付单快照、写确认事件;任何一个条件缺失或不符就不记成功,转未知或差异队列。成功确认后订单域才能消费事件,库存、履约和账务不应从前端跳转或“已受理”直接启动。这样做看似多了一次查单和多个字段,但它把支付从外部不可靠通知变成内部可审计事实,也为对账和退款留下可复算锚点。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:受理、授权、扣款与确认语义
- 追问:只比较订单号行不行?直接回答:不行,还必须比金额、币种、收款主体和交易唯一性。
- 追问:渠道同步返回成功能否立刻履约?直接回答:先按同一最小核验集确认,不能绕过领域门禁。
- 追问:金额不符如何处理?直接回答:冻结自动确认,保存证据并查单或人工核验。
- 问题(综合题):支付单改价、优惠变化和服务费变化如何保证一致?
- 口述答案:支付单代表一个时点的付款义务,因此金额、服务费、币种和计费版本必须在建单时锁定。订单在尚未受理前发生优惠变化,可以取消旧单并创建新单;一旦已向渠道发起受理,不能直接改
payAmount(支付金额),因为渠道回调的是旧请求金额。此时先查单:若确认未发生资金动作,关闭旧会话并按新价创建新支付单;若已成功,则确认旧金额,再依据业务规则退款差额或生成新的补收义务;若结果未知,则保留未知态,禁止再发起新扣款。服务费同理,应由后端按固化的模板版本计算,前端只提交必要参数,不能让客户端传入的费用成为权威。明细分摊的余差也要随支付单保存规则和排序,才能在退款时重算。已读充值源码证明服务费在后端重算,但具体计费版本、改价流程与退款差额处理均待源码或现场核对。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付单金额锁定与订单改价
- 追问:支付成功后能否直接改订单价格?直接回答:不能,应以退款、补收或优惠补偿等新事实处理。
- 追问:关闭会话成功就可以新建吗?直接回答:还要确认渠道没有产生交易,关闭调用本身可能超时。
- 追问:如何让客户知道价格变化?直接回答:明确展示旧支付已失效或确认中,重新报价后生成新付款入口。
- 问题(综合题):退款单与原支付单该如何关联,才能防止超额退款?
- 口述答案:退款不应该通过修改原支付单状态实现,而应创建独立退款单,并把它关联到原已确认的渠道交易和支付单。原支付单保留成功事实,退款单记录申请金额、退款原因、退款请求号、渠道退款号、状态和确认时间。创建退款时,先聚合原交易下所有已经成功或处理中且占用额度的退款金额,再用原成功支付金额减去累计占用额校验本次上限;通过后生成唯一退款单和稳定请求号。渠道受理、回调、查单和未知态仍按支付同样原则处理,不能因为退款接口超时就重复发起。部分退款多次发生时,每笔退款都可审计,累计达到原金额才形成全额退款事实。若渠道退款成功而本地未更新,则对账或查单补齐;若金额不符或原交易不存在,则冻结自动处理。现有接口定义可见退款能力,但具体退款上限与表约束在本次核对范围外,必须标记待源码或现场核对。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:客户订单、支付单与退款单分离
- 追问:退款成功后原支付单变失败吗?直接回答:不变,原支付成功是历史事实,退款是独立反向事实。
- 追问:退款处理中是否占用额度?直接回答:应占用,避免多个并发退款都通过上限校验。
- 追问:退款金额以什么币种校验?直接回答:默认按原交易币种和原最小单位校验。
- 问题(综合题):如何从源码事实出发讲支付项目,而不虚构生产细节?
- 口述答案:我会把表达拆成“我已验证的代码事实”“由事实引出的风险”“建议的收口设计”三层。已验证部分可以精确说:
PaymentBusiness.createPaymentOrder会创建支付单、设为待支付并生成编号,verifyPay校验支付单存在、卖家归属和待支付状态;SellerRechargeBusiness.createAndCheckout先创建充值订单,再创建支付单和结账请求,代码还可见支付中门禁、关闭旧会话和成功、异常状态处理。风险层我会说明这些流程面对重复回调、超时、取消竞争和金额币种核验时,需要统一的证据链。设计层再提出支付尝试、渠道会话、渠道交易、条件更新、唯一约束、未知态和查单对账闭环。这样我既能展示从代码抽象领域模型的能力,也不会把未读到的签名算法、数据库索引、吞吐数据或结算周期说成项目事实。被问到细节时,我会明确指出待源码或现场核对的路径和证据,而不是用模糊术语掩盖边界。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:源码映射、接口边界与项目表达
- 追问:能说代码已经实现完整状态机吗?直接回答:不能,已读方法有状态判断,不足以证明完整迁移矩阵。
- 追问:如何回答没有指标数据?直接回答:不编造,说明待现场核对并给出应观察的确认延迟和未知态积压。
- 追问:面试官要表结构怎么办?直接回答:说明建议字段,并明确实际字段需通过实体、迁移或数据库继续验证。
- 问题(综合题):你会如何设计支付成功后的订单推进,避免消息丢失或重复履约?
- 口述答案:支付确认与订单推进之间我采用提交后事件驱动,而不是在回调线程里直接跨域修改所有数据。确认器在一个短本地事务内写渠道交易事实、支付单成功快照和 Outbox(发件箱)记录,三者要么一起提交,要么一起回滚;提交后投递器扫描未投递事件,把包含支付单号、订单号、确认时间和事件唯一键的成功事实发送给订单域。订单域消费时再以事件唯一键或支付单号做幂等,只有尚未推进的订单才能进入待履约。这样消息投递失败不会让支付成功消失,因为 Outbox(发件箱)保留待投递记录;消息重复也不会让订单重复履约,因为消费端有自己的条件更新和唯一约束。支付确认本身仍不依赖消息成功,外部渠道状态也不被订单失败反向覆盖。若下游长期失败,应监控事件年龄、重试次数和积压量,达到预算转人工。现有源码是否已有该事件表与投递器待源码或现场核对,因此这是一条建议链路。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:事务与发件箱
- 追问:为什么不在支付事务里更新订单?直接回答:会扩大事务边界和耦合,订单域仍需独立幂等与恢复。
- 追问:消息重复怎么处理?直接回答:按事件键或支付单号做唯一消费,并以订单状态条件更新。
- 追问:下游失败要回滚支付吗?直接回答:不能,支付事实已确认,应走履约补偿或退款规则。
- 问题(综合题):如何给支付异常设计恢复预算和人工接管?
- 口述答案:恢复设计首先要区分明确业务失败、技术可重试和结果未知。明确拒绝如余额不足可以直接失败并允许用户修正;技术查询超时采用退避重试;结果未知则绝不重发扣款,只按原请求号查单。每个未知支付单保存首次异常时间、最后查询时间、查询次数、最近响应摘要和下一次计划,恢复任务按分片和限流预算查询。超过最大观察窗口、连续查询失败、金额币种冲突或关联关系缺失时,停止自动动作并创建人工核验任务,任务携带支付单、尝试、请求号、会话、渠道交易、回调、查单历史和原始摘要。人工操作也不直接改数据库,而是通过受控补偿命令产生新的审计事实并受状态机限制。对外页面展示确认中和查询入口,避免用户重复付款。预算数值、排班和工单流程属于运营事实,当前应写待源码或现场核对;但“先确认再补做、人工也留审计”是支付领域必须坚持的设计原则。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付失败恢复、未知态与人工核验
- 追问:人工能直接把未知改成功吗?直接回答:只能依据可验证渠道或对账事实执行受控确认,不能凭猜测。
- 追问:为什么要限制自动重试?直接回答:避免限流放大、资源堆积和重复副作用。
- 追问:异常多久算超时?直接回答:取决于渠道能力和业务时限,需按现场规则配置。
- 问题(综合题):如何设计金额不符的处理,既防资损又不误伤正常订单?
- 口述答案:金额不符不是一个普通失败分支,而是支付证据冲突。确认器收到渠道成功结果后,会先把渠道回传最小单位按该币种规则转换,再和支付单锁定的应付金额、币种、收款主体和请求号逐项比较。完全一致才允许推进成功;不一致时保存原始回传、转换规则版本和本地快照,支付单进入差异或未知态,禁止自动发货、自动入账和自动重新扣款。接下来主动查单确认是否是渠道展示金额、手续费口径或币种转换问题;若渠道真实扣款金额与本地义务不同,转人工资金核验并按退款、补收或渠道争议规则处理。不能为了让订单走通而把本地金额覆盖成渠道金额,也不能只按前端显示金额判断。分摊余差必须在建单时就固化,允许的误差只能是明确规则产生的最小单位差,而非任意容忍。具体渠道是否会附加税费或手续费待源码或现场核对,需在适配层明确映射。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:金额最小单位与金额不变量
- 追问:差一分能自动放过吗?直接回答:不能默认放过,必须先判断是否符合已固化的费用和舍入规则。
- 追问:金额不符可以重试确认吗?直接回答:可重新查单补证据,但不能重复扣款或覆盖原快照。
- 追问:谁决定退款还是补收?直接回答:由可核验资金事实和业务规则决定,高风险差异需人工复核。
- 问题(综合题):支付受理、授权、扣款和确认这四个词如何避免混用?
- 口述答案:我会把它们对应到不同的证据强度。受理表示渠道接收了请求并可能返回会话,它不能证明用户已付款;授权表示额度可能被预留,也不必然等于最终扣款;扣款表示渠道侧已发生资金动作;确认是本地领域服务核验订单号、金额、币种、主体和交易唯一性后接受该结果。不同渠道可能没有明确授权阶段,或者把授权和扣款合并,因此渠道状态不能直接写进业务订单。适配层负责把渠道词汇映射成“候选受理、候选资金成功、明确失败、未知”等统一结果,确认层再做本地裁决。订单、库存和履约只消费确认成功事件,不消费前端跳转或受理返回。退款也要独立区分受理与确认,不能因退款请求发出就把原成功交易当作已退。这样做不仅避免术语混乱,也使异常路径清晰:受理超时查单,授权未扣款继续观察,扣款成功但本地失败走确认恢复。各渠道的实际阶段与状态码仍待源码或现场核对。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:受理、授权、扣款与确认语义
- 追问:同步成功为何还要确认?直接回答:同步响应也可能缺字段或中断,仍需按统一核验集处理。
- 追问:授权撤销后订单能继续吗?直接回答:不能视为已付,应按失败或重新发起规则处理。
- 追问:确认成功后能否撤回?直接回答:支付成功事实不撤回,后续以退款或冲正等独立事实表达。
- 问题(综合题):如何避免支付模型把渠道差异污染订单和账务?
- 口述答案:我的边界是核心领域只认识支付单、支付尝试、统一状态和确认事实;渠道差异放在端口和适配层。适配层负责把各渠道的请求字段、金额单位、会话形态、错误码和状态映射为统一输入,例如“已受理”“明确失败”“结果未知”“候选成功”,但不会直接更新业务订单。确认层把这些输入与本地支付单快照、唯一渠道交易和查单结果结合,形成内部成功、失败、取消或未知。订单域只订阅支付确认事件,账务域只订阅可审计的资金事实,双方都不依赖某个渠道的专有字段。这样新增渠道时改动集中在适配层,核心状态机保持稳定;渠道文档版本变化也不会把枚举扩散到所有业务表。已核对的
IPaymentService与PaymentFactoryService能证明源码存在统一结账、取消、退款等接口和按渠道选择实现的方式,但具体每个渠道的生产映射、签名和重试策略仍待源码或现场核对。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:源码映射、接口边界与项目表达
- 追问:渠道有特殊能力怎么扩展?直接回答:放在适配层的可选能力或扩展字段,不污染支付单主状态。
- 追问:核心模型需要保存原始响应吗?直接回答:保存脱敏摘要和证据引用,原文按安全规则受控留存。
- 追问:新增渠道要改订单表吗?直接回答:原则上不改,订单只依赖统一确认结果。
- 问题(综合题):支付成功后为什么仍需要对账?
- 口述答案:实时成功只是当前链路的确认结果,对账是证明闭环是否完整的第二道防线。回调可能丢失,查单可能超时,本地事务可能在某个异常点中断,渠道账单也可能迟到或更正;因此我会定期把内部支付单、渠道交易事实与渠道账单按请求号、交易号、金额、币种和时间窗口逐笔比对。常见差异包括渠道成功本地未知、内部成功渠道无记录、金额币种不符和重复交易。对账发现渠道成功本地未知时,仍需按状态机补齐并保留对账来源;发现内部成功而渠道无记录时,不能自动回滚或退款,应冻结后复核渠道证据;金额不符则转人工。对账不是把账单导进来做统计,而是把未知态、遗漏回调和跨系统差异转成可执行恢复任务。这个分册不展开账务复式分录和结算,但明确支付交易必须为后续对账提供稳定编号、不可变事实和金额币种快照。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:回调、查单与对账三层确认
- 追问:对账发现重复交易如何处理?直接回答:先确认哪个已生效,另一个按渠道规则退款或人工处置。
- 追问:对账结果能直接覆盖状态吗?直接回答:只能作为证据走状态机,保留来源和迁移记录。
- 追问:对账频率如何定?直接回答:取决于渠道账单可得性与风险窗口,待现场核对。
- 问题(综合题):怎样设计支付领域的可观测性,特别是未知态?
- 口述答案:可观测性不能只看支付成功率,我会围绕状态机和证据链设计指标。每笔支付至少能用支付单号串起订单号、尝试号、请求号、会话号、渠道交易号、回调事件和查单记录;日志要脱敏保存渠道、金额币种、状态迁移、错误分类和耗时。指标上关注受理到确认的延迟分布、未知态数量和年龄、查单成功率、重复事件率、金额币种差异数、取消成功竞争数、Outbox(发件箱)未投递年龄和对账差异量。告警不应只按单次异常触发,而应按未知态超龄、同渠道错误突增或差异积压触发。排查时先查不可变事实是否齐全,再看状态迁移是否被条件更新拒绝,再核对渠道查单和对账结果,最后判断是否需要人工补偿。这样可以区分“用户未付款”“渠道已扣款本地未知”“本地成功下游未消费”等不同故障域。具体阈值与监控平台属于待源码或现场核对内容,不能编造数值。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付失败恢复、未知态与人工核验
- 追问:为什么未知态年龄比总量更重要?直接回答:短暂未知可正常恢复,长期未知才意味着资金风险和人工积压。
- 追问:日志能记录完整渠道响应吗?直接回答:应脱敏并按最小留存原则保存,避免泄露密钥和个人信息。
- 追问:如何定位重复扣款?直接回答:按支付单聚合渠道交易和请求号,检查是否超过唯一生效结果。
- 问题(综合题):如果渠道不支持幂等键也不支持查单,系统该怎样降级?
- 口述答案:这是高风险能力缺口,不能用本地重试掩盖。首先在接入评估中把“稳定商户请求号、查单、交易唯一标识、回调可靠性”作为能力矩阵;若渠道确实都不支持,发起前要严格限制并发和重复点击,发起后把请求原文摘要、时间、金额币种和网络证据持久化,超时直接进入未知态并禁止自动再次扣款。后续只能依赖渠道对账单、人工客服核验或用户账单证据确认结果,因此需要更短的风险窗口、更明确的人工处理和必要的产品提示。不能因为没有查单就把超时标失败,也不能以随机新订单号重新发起,这会显著提高双扣风险。若业务无法接受这种人工成本,应选择具备可确认能力的渠道或把该支付方式限制在低风险场景。渠道能力、合同约束和对账可得性均待源码或现场核对;这里的核心不是假设所有渠道完善,而是在能力不足时诚实降低自动化承诺。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付失败恢复、未知态与人工核验
- 追问:本地唯一键能解决渠道双扣吗?直接回答:只能防本地重复确认,不能阻止渠道收到两次外部请求。
- 追问:能否允许用户换渠道重付?直接回答:未确认旧渠道结果前不应自动允许,需人工或对账确认。
- 追问:为什么要在接入阶段淘汰这种渠道?直接回答:它把核心资金风险转移给人工和用户,恢复成本高。
- 问题(综合题):支付成功、账务入账和订单履约为什么要分治?
- 口述答案:它们共享同一个支付确认事实,但各自的权威状态和失败模式不同。支付域负责从请求、回调、查单和对账中确认“钱是否已收”;账务域负责把确认事实变成不可变分录和余额快照;订单履约域负责决定是否允许发货、库存如何占用和下游创建如何补偿。把三者塞进一个同步长事务,会把外部渠道、数据库锁、消息投递和下游仓库耦合在一起,任何一个超时都会拖住全部操作。更稳妥的做法是支付域短事务确认并写 Outbox(发件箱),账务和订单以幂等消费者处理同一事件,各自失败各自重试和告警。支付成功不因履约失败回滚,履约失败要走补偿或退款规则;账务入账异常也不能否定渠道已扣款,而应从确认事实恢复。分治的前提是稳定编号、不可变事实和明确的状态边界。本篇只定义支付输出,复式账和库存实现由后续分册完成,不能提前宣称现有系统已完整采用该架构。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:事务与发件箱
- 追问:订单能否直接查支付表决定发货?直接回答:可查询确认快照,但推进动作应通过受控事件和幂等规则。
- 追问:账务失败是否要退款?直接回答:先恢复账务;退款是业务决定,不能把技术失败直接变资金逆转。
- 追问:为什么每个域都要幂等?直接回答:事件至少一次投递和人工重放都会产生重复输入。
- 问题(综合题):支付系统应如何处理部分支付和多笔支付的需求?
- 口述答案:部分支付不能只给订单加一个“已付金额”字段,而要先定义付款义务的应收总额、可接受的支付方式、最小可付单位和关闭规则。每一笔已确认渠道交易都关联到支付单,并以不可变金额事实累计;支付单状态可以是未付、部分确认、已付、未知或关闭,但成功累计不得超过应收总额。发起新尝试时,系统计算剩余应付并锁定该金额,避免两个并行尝试都按全额或同一剩余额度请求;确认时用条件更新或额度占用裁决唯一生效部分。退款也按每笔已确认交易和累计退款额度限制,不能从订单展示金额直接退。若产品不支持部分支付,就在模型上明确一笔支付单只能有一个生效交易,并在第二笔成功时转冲突处置。关键是金额快照、币种一致和事务边界不变,复杂度只在“累计额度”而不是推翻状态机。当前源码未证明部分支付功能,因此面试时应把它作为扩展设计,不冒充现网能力。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:客户订单、支付单与退款单分离
- 追问:部分支付的剩余额度如何并发保护?直接回答:在确认事务中用原子累计或额度占用条件更新裁决。
- 追问:不同币种可以凑一笔支付吗?直接回答:需明确换算快照和结算规则,默认不应混算。
- 追问:多笔支付都成功超额怎么办?直接回答:保留事实,按规则退款超额部分,不能删除交易。
- 问题(综合题):如何看待现有充值代码中的状态校验、锁和事务?
- 口述答案:我会把它们作为很有价值的局部证据,而不夸张为完整支付平台能力。
PaymentBusiness.verifyPay明确校验支付单号、支付区域、支付来源、渠道信息、卖家归属和待支付状态,这说明支付入口有门禁。SellerRechargeBusiness可见创建充值订单、创建支付单、使用锁限制并发、支付中拒绝重复结账、关闭旧会话、以及在事务中处理成功和异常状态;这些都是 E1 实体和 E2 流程事实。但从这些方法不能推断数据库唯一索引、完整状态条件更新、渠道签名、主动查单、对账、Outbox(发件箱)或跨币种能力已经存在。我的补强方案是保留已有门禁和锁,新增或核实支付尝试、渠道交易唯一键、未知态、条件更新与确认事件,并沿调用链检查各渠道适配器。这样回答既认可已有实现的工程价值,也能清晰指出资金一致性仍需要哪些证据和设计闭环。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:源码映射、接口边界与项目表达
- 追问:事务能否证明外部调用一致?直接回答:不能,它只覆盖本地资源,外部结果仍需查单与补偿。
- 追问:锁为何仍有价值?直接回答:减少同一订单并发工作和无效请求,但不是最终正确性。
- 追问:下一步核对什么?直接回答:实体表约束、渠道适配器、回调入口、查单任务和对账任务。
- 问题(综合题):设计支付状态表时,哪些字段应是快照,哪些应是不可变事实?
- 口述答案:我会把查询高频、需要并发裁决的内容放在支付单快照,例如当前状态、应付金额、币种、已生效渠道交易引用、确认时间和版本号;把每次请求、回调、查单、渠道交易和状态迁移放在追加事实记录中,例如请求号、事件标识、原始响应摘要、来源、发生时间、核验结论和操作者。快照可以随着合法迁移更新,但任何更新都必须能回到事实解释;事实不覆盖,发生冲突就追加一条冲突或更正记录。这样支付页面不需要扫全量历史就能读到当前状态,审计和排障又能还原为什么从处理中变成成功或未知。金额、币种、计费版本和汇率快照属于付款义务的一部分,建单后不可随意修改;渠道交易号和回传金额属于外部事实,也不可覆盖。历史归档可以减少在线表压力,但归档前必须保证链接和复核能力。实际表名、字段和索引必须按现有实体和迁移文件核对,本节给的是职责划分而非对源码表结构的断言。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:领域对象与事实边界
- 追问:快照丢失能否重建?直接回答:应能由不可变事实重建,至少可用作对账和修复依据。
- 追问:事实表会不会太大?直接回答:会,需要分区、归档和索引,但不能用删除事实换性能。
- 追问:状态迁移要记录谁触发吗?直接回答:要,记录回调、查单、对账或人工补偿来源。
- 问题(综合题):支付领域如何设计安全边界,避免回调或金额被伪造?
- 口述答案:安全边界首先不是相信任何来自前端或回调的单个字段。前端只能提交支付意图,服务端从支付单读取已锁定金额和币种并重算必要费用;回调在渠道适配层进行签名、时间窗、事件唯一性和来源校验,随后仍以请求号主动查单核验真实交易。确认层比较商户归属、订单号、金额、币种和渠道交易唯一性,任何不一致都不进入成功。原始请求和响应只保存脱敏摘要或受控证据引用,密钥、令牌和完整敏感数据不写普通日志;人工处理也要有权限、双人复核和审计记录。对重放事件,以渠道事件标识或交易号唯一约束拦截,重复合法事件幂等返回,关键字段不同则当作冲突。具体签名算法、请求头名称、密钥轮换和时间窗口是渠道生产细节,当前均待源码或现场核对,不能为了写得像实战而编造。核心目标是让任何外部输入都必须经过可验证事实和状态机门禁,才有资格影响资金。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:回调、查单与对账三层确认
- 追问:验签通过为何还要比金额?直接回答:验签证明来源,不保证本地关联和金额配置无误。
- 追问:前端传金额能否直接用?直接回答:不能,金额必须以服务端支付单快照为准。
- 追问:回调重放会不会影响成功状态?直接回答:唯一事件和终态保护会把它变成幂等处理。
- 问题(综合题):请用一次线上排查的口径说明“支付显示失败但用户说已扣款”怎么处理?
- 口述答案:我会先把这类现象定性为结果未知,而不是立即相信页面或立即退款。第一步按订单号、支付单号、尝试号和用户时间范围查本地支付单快照,确认它是失败、未知还是处理中;第二步查该尝试的请求号、会话、回调原始摘要和是否已有渠道交易事实;第三步用稳定请求号或商户订单号主动查渠道,核验交易状态、金额、币种和收款主体。若渠道查实成功而本地未知,就在受控事务中补写交易事实、条件更新支付单并投递确认事件;若本地已失败或取消但渠道成功,则记录冲突并进入退款或人工资金处置,绝不直接覆盖终态;若渠道暂时无结果,继续按恢复预算查单并等待对账。整个过程保留查询时间线,避免客服、订单和账务各自做一次可能矛盾的修改。最后检查是否存在重复支付单或同一请求号多次发起,评估是否需冻结新支付入口。渠道查询权限和具体工单流程待源码或现场核对。
- 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:支付失败恢复、未知态与人工核验
- 追问:能先给用户补发货吗?直接回答:应基于可确认资金事实和业务风险决定,不能仅凭口头反馈。
- 追问:渠道查不到但用户有银行流水呢?直接回答:保存证据并人工核验,等待渠道对账,不自动确认。
- 追问:如何防止客服重复退款?直接回答:退款单唯一键、额度占用和审批审计共同限制。
- 问题(综合题):请总结支付交易领域的核心设计原则,并给出可直接复述的项目结论。
- 口述答案:我的核心结论是:支付不是一次接口调用,而是一条从付款义务到可证明资金事实的恢复链路。对象上分离客户订单、支付单、支付尝试、渠道会话、渠道交易和退款单;状态上用合法迁移、终态保护和未知态管理不确定性;并发上用幂等键、唯一流水和条件更新做最终裁决,锁只做减压;金额上固定最小单位、币种、计费和汇率快照,用 BigDecimal(高精度十进制数)与 DECIMAL(定点小数)保证可复算;事务上把本地状态与 Outbox(发件箱)原子提交,把渠道扣款和消息投递放在可查询、可重试的边界之外;确认上用回调、主动查单和对账三层证据闭环。结合已核对源码,我可以准确说现有
PaymentBusiness已有支付单创建与待支付校验,SellerRechargeBusiness有充值建单、会话关闭、锁和局部事务处理;而完整渠道验签、表约束、汇率、对账与事件投递需要继续核对或补强。这种表达既能支撑面试追问,也能指导真实系统把“超时不是失败、事实不可篡改、确认先于副作用”落实到代码和运维流程。 - 工程收口:具体落地时,我会把每次判断拆成可核验的证据,而不是只留最终状态:支付单号、尝试号、稳定请求号、渠道交易号、金额币种快照、触发来源、发生时间、状态迁移原因和处理人都可追溯。自动确认、自动重试和人工补偿都先检查当前状态、唯一事实和允许迁移,再写入新的事实;处理后再核对支付快照、事件投递和下游消费是否一致。这样遇到重复、乱序、超时、取消竞争或金额冲突时,排查人员能先还原时间线,再决定查单、等待对账、退款或人工核验。任何缺少唯一标识、金额币种不一致或仍在查询中的结果,都不能为了接口即时返回而绕开未知态、终态保护和审计链路。
- 锚点:源码映射、接口边界与项目表达
- 追问:这套模型最重要的不变量是什么?直接回答:同一付款义务只能有唯一生效结果,未知结果不记成功也不记失败。
- 追问:为什么强调不可变事实?直接回答:它让状态、退款、对账和人工修复都有可复核依据。
- 追问:下一步该读什么?直接回答:渠道验签与查单、账务分录与退款、订单库存与履约的独立分册。
3. 复习清单
- 能画出业务订单、支付单、支付尝试、渠道会话、渠道交易、退款单的关系,并说清“一单多次尝试、唯一生效结果”。
- 能解释成功、失败、取消、未知的合法迁移、终态保护和条件更新。
- 能复算最小单位、分摊余差、金额币种核验与汇率快照。
- 能讲清受理、授权、扣款、确认,以及回调、查单、对账三层确认的边界。
- 能区分本地事务、外部副作用、Outbox(发件箱)与消费幂等。
- 能从
PaymentBusiness、SellerRechargeBusiness说明已证实事实,并把未证实渠道规则明确为待源码或现场核对。
