支付对账:流水采集、差异识别与补偿
支付回调负责及时更新业务,对账负责发现遗漏和不一致。对账可以由渠道账单可用事件触发,也可以定期拉取;用户登录不应影响它是否执行。
示例按商户、渠道、币种和业务日期建立对账批次。具体账单生成时间、时区与字段以支付渠道约定为准,不把“每天 00:00”假定为所有渠道账单都已完整可用。
从回调处理推导独立对账
假设用户付款后,支付回调通常能及时到达。回调适合更新当前订单,却不能证明历史上没有漏接事件;反过来,每次用户查看订单都完整拉一遍渠道账单,也不能形成稳定的核对范围。
| 候选方案 | 能提供什么 | 无法单独证明什么 |
|---|---|---|
| 只处理支付回调 | 及时接续业务变化 | 某段历史是否漏了事件 |
| 定期查询未完成订单 | 修复已知待支付对象 | 本地根本没有对应记录的渠道交易 |
| 完整采集窗口后双向匹配 | 对指定范围建立可复查证据 | 仍需界定渠道数据是否完整及允许延迟 |
本篇保留回调的实时路径,再按明确范围做独立对账。原始采集、匹配和补偿分开,是因为“不一致尚待确认”与“已经可以执行修复”具有不同的证据要求。
采集、匹配与复核时序
渠道请求位于数据库事务之外。只有输入完整性满足要求,后续匹配才有依据判断两侧差异。
任意一页失败时,循环暂停在已提交检查点,不能继续把缺失页当成渠道没有对应交易。补偿是另一个可追踪的业务动作。
对账需求与完成标准
示例分为“完整采集渠道事实”“匹配本地流水”“确认差异”“按规则补偿”四个阶段。采集完成不等于差异已解决,对账成功也不表示自动执行了资金操作。
| 决策 | 示例约定 | 开发约束 |
|---|---|---|
| 对账范围 | 商户、渠道、账户、币种和固定窗口 | 不跨账户混用流水身份 |
| 数据完整性 | 账单可用,所有分页成功保存 | 未完成采集不判定单边缺失 |
| 复核等待 | 为渠道延迟保留明确复查窗口 | 未确认与已确认差异分开 |
| 自动补偿范围 | 仅执行明确批准的幂等补记规则 | 资金动作走独立权限与审核 |
| 业务完成 | 差异有可追踪的解决结果或待办 | 不用“任务已运行”代替闭环 |
先把两侧事实保存下来
| 数据 | 用途 |
|---|---|
| 渠道原始流水或账单文件 | 保留渠道报告的原始事实与采集版本 |
| 本地支付、退款与订单记录 | 反映本地处理结果 |
| 对账运行记录 | 保存窗口、页游标、完成范围和重跑版本 |
| 差异记录 | 保存匹配键、差异类型、核对进度 |
金额先统一币种和最小货币单位,分开处理收款、退款、手续费与结算。订单金额、支付金额和到账净额不一定相同,不能只比较两个总数就下结论。
表结构与操作入口
| 记录 | 关键字段与约束 |
|---|---|
reconciliation_runs | 范围、窗口、采集游标、源完整性标记、匹配版本、状态 |
channel_transactions | 渠道账户、交易 ID、类型、币种、金额、原始内容;来源身份唯一 |
reconciliation_items | 运行、匹配键、两侧证据、差异类别和差异版本 |
compensation_actions | 差异版本、动作种类、审批依据、稳定幂等键、执行结果 |
定时器和手动补跑接口都创建运行记录,查询接口返回采集进度与差异分布。复核、批准和重放接口分别校验权限并保存操作者与原因,不提供一个绕过差异核对的通用“强制成功”按钮。
本地支付、退款流水应有不可混淆的业务身份,不能仅用订单号判断一对一匹配:部分支付和多次退款可能让一个订单对应多条资金流水。
1000 条流水如何处理
如果接口一页返回 100 条,1000 条约需拉取 10 页,这是分页示例,真实上限以渠道文档为准。以 Stripe 余额流水接口为例,它支持日期范围和游标分页;余额流水也不能直接等同于所有本地订单。
每页事务保存原始记录与检查点,按渠道流水 ID 建立唯一约束。只有确认所有页完整落地,才将采集阶段标记完成。失败后可以继续游标或安全重拉,不能把半份账单当成完整账单去判定“本地多单”。
持续变化的数据源可能让分页期间出现新流水。需要固定窗口和截止水位,或在后续批次回看重叠区间,通过唯一约束消除重复采集。
分页采集怎样保证检查点正确
取得本轮采集资格,读取固定窗口和已提交游标
事务外调用渠道接口,取得一页与后续游标
BEGIN
校验本轮仍属于当前采集尝试,游标仍等于请求起点
幂等写入这一页原始流水
保存这一页摘要和后续游标
只有渠道明确没有后续页且完整性条件满足,才标记采集完成
COMMIT
匹配 Worker 只处理达到输入完整性要求的范围调用渠道时不持有数据库长事务。返回结果后先落数据再推进游标,放在同一事务内;两者分开提交会产生跳页或不完整重复消费。
这里游标检查保护的是采集进度,来源交易唯一键保护的是数据身份。即使唯一约束消除了重复流水,过早推进游标仍可能跳过没保存的整页,两个机制不能互相替代。
如果源账单能够修订,记录来源版本或校验值并创建新的对账运行,不静默修改已经审核过的差异证据。
匹配和差异分类
| 现象 | 先核对什么 | 可以安排的动作 |
|---|---|---|
| 渠道已收款、本地待支付 | 回调是否丢失,交易是否属于该订单 | 查询确认后幂等补记 |
| 本地已支付、渠道未出现 | 渠道账单是否完整、是否仍有延迟 | 记录待确认,稍后复核 |
| 金额或币种不符 | 退款、部分支付、手续费和映射关系 | 保留原始差异并诊断 |
| 同一订单有多笔收款 | 是否分次支付或重复交易 | 按既定规则审核处理 |
| 本地已关闭、渠道已收款 | 关闭与支付的竞争结果 | 建立差异处理单 |
单次查询没有返回,不等于资金操作失败。对账结果应区分“已确认差异”和“尚未取得完整事实”。
差异任务与补偿权限
1000 条差异不应在扫描事务里直接执行退款、补款等动作。先保存可追踪的差异项,再根据预先确定的业务规则、审批流程和处理权限生成补偿任务。
补偿任务携带原交易、差异版本与稳定业务键。外部操作超时后先核对结果;无幂等保障的动作不能通过无限重试处理。
已经成功补记的差异不因整批重跑再次操作。差异状态可以从待确认、已确认、处理中推进到已解决,并保存解决它的业务流水。
补偿处理器怎样实现
领取 compensation_action,核对规则版本与审批状态
重新读取差异、原支付和渠道当前结果
如果差异已经解决:关联已有结果,幂等完成
如果证据仍不完整:安排复查,不执行资金动作
如果满足允许的本地补记规则:
同一事务写业务流水、解决差异、保存后续事件并确认任务
如果需要外部渠道动作:
使用该动作稳定幂等键提交,保存请求身份
超时进入待核对状态;查询原请求后再决定后续处理审批与执行之间也会发生业务变化,因此批准不代表可以永远使用旧证据执行。补偿开始前要再次检查,批准记录本身不能作为操作已经完成的证明。
怎样报告结果
对账完成时报告采集范围、原始条数、匹配条数、待确认差异和已确认差异。只通知“发现 1000 条差异”却不区分账单缺页、渠道延迟和真实问题,会制造不必要的人工工作。
告警按商户和差异类别聚合,具体条目进入受控的排查入口;通知中避免暴露完整账户信息。
第 7 页超时的一次完整恢复
假设运行 C21 需要采集 10 页,每页样例 100 条,第 6 页已经提交:
| 故障或动作 | 运行记录应怎样变化 | 下一步 |
|---|---|---|
| 第 7 页接口超时 | 游标仍指向第 7 页请求起点 | 重试相同范围,不认定这些交易不存在 |
| 第 7 页落库,但提交响应丢失 | 客户端不能确定结果 | 重读检查点;必要时安全重拉 |
| 第 7 页重复返回 | 唯一来源身份不产生重复流水 | 数据与检查点一起确认 |
| 全部页完整保存 | 本轮输入标记完整 | 才判断两侧缺失与金额差异 |
| 发现渠道已付款、本地未记账 | 创建带证据的差异项 | 复核当前事实后走允许的补记流程 |
如果第 7 页尚未取得就开始判断本地多单,结果会把“未采到”错误解释成“渠道没有”。因此,对账运行阶段需要区分等待渠道、采集中、匹配中等状态;这些业务阶段与底层任务的 pending、running 等执行状态分别记录。
验收与排查依据
| 场景 | 应断言的结果 |
|---|---|
| 第 7 页保存前退出 | 从已提交检查点恢复,前面页不丢失 |
| 写页成功但客户端未收到提交结果 | 安全重拉,不重复创建原始流水 |
| 账单尚未完整生成 | 运行等待,不把未出现的渠道记录立即认作差异 |
| 同一差异被两个补偿 Worker 领取 | 本地业务约束或外部幂等键阻止重复动作 |
| 审批后用户已退款 | 执行前复核,旧批准动作退出或重新审核 |
| 渠道请求超时但实际成功 | 关联查询到的原结果,不更换请求键再次操作 |
监控源数据完整性、采集检查点年龄、未确认差异年龄和补偿待核对量。异常信息应能定位到交易证据,避免在告警中传播完整敏感流水。
美团配送对账体系中的二次核对
美团技术团队在 2018 年的配送资金安全治理文章中,将对账拆成数据准备、核对和差错处理;按漏结、重复结、错结梳理问题,并结合明细核对与总数复核。文章还介绍了周期对账、在线核对,以及差错记录再次核实后进入自动修复或人工处理的设计。
这可以补充本文差异分类的开发方式:除逐条比较交易金额和状态,还按渠道、币种和账期保存笔数与金额摘要。明细用于定位具体记录,汇总用于发现漏采或规则覆盖不足;两边金额恰好相等也不能替代明细核对,因为相反方向的错误可能互相抵消。这是对该实践在本文模型中的应用推导。
差错处理器应保留首次证据、复核结果和处置身份。例如首次发现渠道已收款而本地未确认,复核时支付回调可能已经补齐,本次处理应标记差异已消除。仍有差异时才按授权建立补偿待办。该历史文章提供的是架构与阶段性实践,本文不沿用其当年的吞吐数字,也不将其描述为美团当前全部生产架构。
美团旅行结算:明细接收与账单计算分离
美团的旅行结算平台实践讨论了另一个阶段的问题:单个账单写明细、计算汇总放在同一串行路径时,会限制处理效率。文章采用明细写入与账单计算分离、分散计算时机,并在最终结账时兜底计算。这是对结算平台的历史介绍,其选择依赖于业务接受中间状态最终收敛。
对照本文,账单页面上的暂时汇总、已经采集的交易明细、最终允许结算的金额应分别标记。若一批明细尚未齐备,界面可以展示“更新中”,但不能把临时汇总作为补付依据。跨系统核对还要保存对账口径、输入完整性与复核证据。
例如某商户新增一条退款,明细已经落库、汇总尚未重算,此时差异可能来自计算时差。复核器应等到约定水位或重算完成后再判断;确认是实际错账后才进入补偿。不能把原文特定环节的解耦优化推广成“对账和付款都不需要事务”。
参考资料
美团技术团队:高性能平台设计—美团旅行结算平台实践
甄超、宏伟,美团技术团队:美团配送资金安全治理之对账体系建设
Stripe:Webhook event ordering
PostgreSQL 18:Transaction Isolation