跳到内容

Web3 支付自动结算设计

受众:开发者 摘要:Web3 支付结算专题:交易确认 / 风控审计 / 自动结算 / 资金归集。

本文档说明 Web3 支付从“只读确认”进入“自动结算”必须满足的设计边界。当前项目已经提供链上收款上下文、交易确认、ERC-20 Transfer 匹配、结算 dry-run 计划、复核指纹、复核日志、显式结算执行器、超时自动通过执行、失败通知、自动重试、已结算链上审计、订单状态条件更新和 Transfer 事件唯一性指纹;后续继续增强的是更细的风控策略、链重组工单化处理和真实生产联调。

自动结算属于支付核心链路。实现和扩展时,必须始终明确状态机、幂等、链重组、人工复核、审计和异常兜底。

当前边界

已完成能力:

  • Payment web3 Provider 创建链上收款上下文。
  • Payment web3 Provider 查询交易确认状态。
  • Money Web3 后台单笔和批量只读确认接口。
  • Money Web3 后台已结算订单链上二次审计接口。
  • ERC-20 Transfer 事件匹配。
  • 结算 dry-run 计划。
  • 计划检查项和 reviewFingerprint
  • 人工复核日志留痕。
  • 基于复核日志的显式结算执行入口。
  • 超时自动处理、自动重试和失败通知。
  • 已结算订单链上审计、风险日志和只读补偿建议。

当前不会做的事情:

  • 支付结算流程不托管私钥。
  • 支付结算流程不直接签名交易。
  • 支付结算流程不直接广播交易。
  • 不在 dry-run 或复核日志记录阶段写成功订单。
  • 超时自动处理只有在 auto-approveexecute-approved=true 时,才会复用显式结算执行器写订单和流水。
  • 不自动退款。
  • 不处理跨链桥或非 EVM 链。

结算目标

自动结算只解决一件事:当本地订单仍未支付,且链上交易和订单期望完全匹配时,在事务内把订单和流水更新为已支付。

它不负责:

  • 发起链上交易。
  • 替用户完成钱包授权。
  • 处理私钥托管。
  • 改写链上状态。
  • 自动处理复杂退款和争议。

推荐状态机

本地支付订单继续使用 Payment 订单状态。Web3 结算过程可以在 Service 内部使用以下结算候选状态,不一定需要单独建表:

状态含义
pending本地订单未支付,等待链上交易
confirming交易已发现,但确认数不足
review_required链上确认满足基础条件,但缺少事件匹配上下文或需要人工复核
ready链上交易、金额、收款地址、Token 合约和本地订单状态都匹配
settling正在事务内写入订单和流水
settled已完成本地订单和流水更新
blocked存在阻断原因,不允许结算
failed查询或结算过程失败,需要人工处理

核心规则:

  • 不能只凭交易 Hash 标记订单成功。
  • 不能把终态订单回退到未支付。
  • 本地订单已支付、已退款、已关闭时,不能再次自动结算。
  • ready 只代表可以进入结算事务,不代表已经结算成功。

结算前置检查

进入结算事务前必须全部通过:

检查项说明
订单存在outTradeNo 能找到本地支付订单
支付形态正确订单 provider 是 web3
本地未支付本地状态仍是待支付或待确认
链配置有效chainName 存在且启用
链 ID 匹配RPC 返回链 ID 与配置一致
交易存在交易 Hash 格式合法,且 RPC 能查询到交易或回执
交易成功回执状态成功
确认数满足当前区块高度达到 requiredConfirmations
收款地址匹配ERC-20 Transfer.to 与订单收款地址一致
Token 合约匹配事件合约地址与订单期望 Token 合约一致
金额匹配Transfer.value 与订单 amountRaw 一致
事件唯一chainName + transactionHash + logIndex 未被消费
复核一致提交时的 reviewFingerprint 与服务端重新计算结果一致

任意检查不通过,都只能返回阻断原因,不能写订单状态。

幂等模型

Web3 结算需要三层幂等:

层级推荐 key用途
链上事件chainName:transactionHash:logIndex防止同一转账事件重复结算
业务订单outTradeNo防止同一订单重复变更状态
人工复核reviewFingerprint防止操作员提交过期页面结果;指纹包含 Transfer blockNumberlogIndex 和匹配原因码

结算事务建议流程:

  1. 重新生成单笔结算计划。
  2. 校验提交的 reviewFingerprint
  3. 再次判断订单状态是否仍允许结算。
  4. 使用数据库无关的条件更新执行 pending -> success
  5. 条件更新成功后,用事件唯一 key 写入或更新支付流水。
  6. 条件更新失败时重新读取订单状态,返回已支付或并发冲突原因。
  7. 写入人工复核日志和操作日志。
  8. 分发 Payment 成功处理器,供余额充值、会员权益、业务订单等场景按 scene 做幂等后处理。
  9. 发送通知或事件,供后续业务处理。

如果事件唯一 key 已存在,应返回已处理结果,而不是重复写流水。当前实现不依赖 SELECT ... FOR UPDATE 这类数据库方言锁,优先使用 MyBatis-Plus 条件更新保证 MySQL、PostgreSQL 和 H2 下语义一致。

链重组处理

链重组是 Web3 支付必须考虑的风险。

推荐规则:

  • 每条链配置独立的 requiredConfirmations
  • 自动结算前必须重新查询当前区块高度和交易回执。
  • 自动结算前必须重新生成计划并校验 reviewFingerprint;指纹会纳入 Transfer.blockNumberTransfer.logIndex 和不匹配原因,避免同一交易 Hash 内多条转账事件或事件位置变化时继续使用旧复核结果。
  • 如果事件日志标记 removed=true,必须阻断结算。
  • 如果当前确认数低于要求,必须回到 confirming
  • 已结算后如果后续发现链重组,不要直接自动回滚订单,应进入异常复核和补偿流程。

生产环境建议:

  • BSC、Polygon 等高吞吐链设置更保守的确认数。
  • 大额订单强制人工复核。
  • 公共 RPC 只用于开发调试,生产使用稳定节点服务商或自建节点。

人工和自动模式

建议分三阶段推进:

阶段模式说明
阶段 1人工确认dry-run 计划、复核指纹、复核日志,不自动改订单
阶段 2半自动小额、固定 Token、固定收款地址、事件完全匹配后可自动结算
阶段 3规则化按链、Token、金额、商户、风险等级配置自动结算规则

当前项目已经完成阶段 1,并具备阶段 2 的执行器雏形:后台可通过已保存的复核日志触发单笔结算;超时 auto-approve 和自动重试都可以按配置复用同一执行器;订单状态使用 pending -> success 条件更新保护并发幂等;结算成功后会进入 Payment 成功处理链,余额充值等业务场景按 scene 幂等完成后续入账。规则化批量结算、商户级风控阈值和链重组异常工单仍需继续完善。

当前阶段已经支持人工复核超时自动处理。默认配置下,超时后记录 auto-approve 审计日志,并立即复用结算执行器;如果希望只留痕不改资金状态,可以关闭 execute-approved

配置默认值说明
spring.open.money.web3.settlement.review.auto.enabledtrue是否启用超时自动处理
spring.open.money.web3.settlement.review.auto.auto-startuptrue是否启动时注册 Scheduler 任务
spring.open.money.web3.settlement.review.auto.task-namemoney.web3.payment-settlement-review.auto-handleScheduler 任务名称
spring.open.money.web3.settlement.review.auto.fixed-delay5m自动处理扫描间隔
spring.open.money.web3.settlement.review.auto.timeout30m人工复核超时时间
spring.open.money.web3.settlement.review.auto.actionauto-approve超时动作,可选 auto-approveauto-reject
spring.open.money.web3.settlement.review.auto.execute-approvedtrueauto-approve 后是否立即调用结算执行器
spring.open.money.web3.settlement.review.auto.limit50单次最多处理数量
spring.open.money.web3.settlement.review.auto.failure-notification.enabledtrue自动执行结算失败或跳过时是否发送提醒
spring.open.money.web3.settlement.review.auto.failure-notification.channelslog失败通知通道,支持 Notification 已启用通道
spring.open.money.web3.settlement.review.auto.failure-notification.subjectWeb3 payment settlement failed失败通知标题
spring.open.money.web3.settlement.review.auto.failure-notification.recipient-type可选接收人类型
spring.open.money.web3.settlement.review.auto.failure-notification.recipient-value可选接收人标识

已结算链上审计任务配置:

配置默认值说明
spring.open.money.web3.settlement.audit.enabledtrue是否启用已结算订单链上审计任务
spring.open.money.web3.settlement.audit.auto-startuptrue是否启动时注册 Scheduler 任务
spring.open.money.web3.settlement.audit.task-namemoney.web3.payment-settlement.auditScheduler 任务名称
spring.open.money.web3.settlement.audit.fixed-delay30m审计扫描间隔
spring.open.money.web3.settlement.audit.providerweb3支付 Provider
spring.open.money.web3.settlement.audit.limit50单次最多审计订单数
spring.open.money.web3.settlement.audit.older-than-minutes60只审计早于指定分钟数的已结算订单
spring.open.money.web3.settlement.audit.chain-name可选链名称过滤
spring.open.money.web3.settlement.audit.required-confirmations可选确认数;为空时使用订单或链配置
spring.open.money.web3.settlement.audit.only-riskytrue是否只保留风险项
spring.open.money.web3.settlement.audit.risk-notification.enabledtrue发现风险时是否发送提醒
spring.open.money.web3.settlement.audit.risk-notification.channelslog风险通知通道,支持 Notification 已启用通道
spring.open.money.web3.settlement.audit.risk-notification.subjectWeb3 payment settlement audit risk风险通知标题

后台也可以手动触发:

http
POST /api/v1/admin/web3/payment-settlement-review-logs/auto-handle-expired
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/settle

后续新增人工审核场景时,应沿用同类配置策略:可关闭、可调整超时时间、可选择自动处理动作,并保留审计日志。

当前 API 与扩展 API

当前已实现单笔显式执行入口:

http
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/settle

执行入口会基于复核日志重新生成单笔计划并校验 reviewFingerprint,通过后在同一事务内把 pending 订单条件更新为 success,写入类型为 web3-settlement 的支付流水,并分发 CashierPaymentOrderSuccessHandler。余额充值等业务场景不在 Web3 模块里直接改账,而是通过 Payment 成功处理器按订单 scene 做幂等后处理。若订单已支付、订单状态不允许结算、并发更新冲突、复核指纹变化、计划已不再是可结算候选,或同一 outTradeNo + transactionHash + web3-settlement 流水已存在,则返回跳过原因,不重复写资金状态。超时自动通过时也复用这一入口,不维护第二套结算规则。自动执行失败或跳过会走 Notification 通道提醒,默认记录到 log。

复核日志记录、超时自动通过 / 驳回、自动重试和手动执行结算都会先执行审批工作流 money.web3.payment-settlement.review。该工作流只负责动作、来源状态和权限快照前置校验:复核日志创建要求 money:web3:payment-settlement-review-log:create 权限快照,手动结算要求 money:web3:payment-settlement:execute 权限快照;复核指纹、链上确认、订单条件更新、支付流水幂等、Outbox 和补偿仍以 Web3 Service 为事实来源。

如果后续需要独立的批量结算工作台,可以在现有复核日志执行链路之上增加以下聚合接口;它们不应绕过当前 reviewFingerprint、订单状态条件更新和支付流水幂等规则:

http
POST /api/v1/money/web3/payment-settlements/batch
GET  /api/v1/money/web3/payment-settlements/{id}

单笔提交 DTO 建议字段:

字段说明
outTradeNo商户订单号
reviewFingerprint页面复核指纹
traceId操作追踪号
remark人工备注

权限建议:

权限说明
money:web3:payment-settlement-review-log:view查看待复核结算日志
money:web3:payment-settlement-review-log:create保存复核日志和触发超时自动处理
money:web3:payment-settlement-audit-log:view查看已结算审计日志
money:web3:payment-settlement:execute执行已复核 Web3 支付结算或批量高风险动作

这些接口必须继续返回 Result<T> 语义的 JSON 响应体,不能在 Controller 写业务逻辑。

数据模型建议

第一阶段优先复用已有表:

  • cashier_payment_order:本地支付订单。
  • cashier_payment_transaction:支付流水和链上事件唯一 key。
  • cashier_payment_callback_log:第三方回调日志。
  • money_web3_payment_settlement_review_log:人工复核日志和计划快照。
  • money_web3_payment_settlement_audit_log:已结算链上二次审计风险日志,记录风险原因、确认快照和处理状态。

暂不建议立即新增结算表。只有当复核日志无法承载自动结算执行记录、重试队列和异常处理时,再评估新增 money_web3_payment_settlement_log

通知和审计

自动结算必须接入:

  • 操作日志。
  • 人工复核日志。
  • 支付流水。
  • Notification 通知。
  • 必要的失败告警。

通知内容可以包含:

  • 订单号。
  • 链名称。
  • 交易 Hash。
  • 区块浏览器链接。
  • 阻断原因。

通知内容不能包含:

  • RPC URL。
  • 私钥。
  • 助记词。
  • 服务商密钥。
  • 可直接操作资金的敏感凭据。

实现顺序

推荐实现顺序:

  1. 固化本文档中的状态机和阻断原因。
  2. 先写自动结算 Service 单元测试。
  3. 实现单笔结算 command 和 service。
  4. 接入订单状态条件更新、流水幂等和复核指纹校验。
  5. 增加单笔后台 API。
  6. 增加批量结算入口,但默认关闭或需要高权限。
  7. 补充 Notification 失败提醒。
  8. 用样板订单和模拟 Web3 manager 做集成测试。
  9. 完成真实 RPC 小额联调后,再考虑开放自动规则。

当前已完成第 1 至第 8 步的第一版:MoneyWeb3PaymentSettlementApplier 会重新校验复核指纹、确认计划仍可结算、使用订单状态条件更新完成 pending -> success,按流水幂等写入 web3-settlement,并通过后台复核日志入口暴露执行能力;超时 auto-approve 可配置为复用同一执行器,执行失败或被幂等规则跳过时会通过 Notification 通道提醒。复核指纹已纳入 Transfer 事件位置和不匹配原因码,能识别同一交易 Hash 内匹配到不同转账事件的过期复核计划。

后台同时提供 POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/retry 作为运维重试入口。它与 settle 共享同一套 Service 执行方法,只是语义上用于失败或跳过后的重新触发;重试不会降低复核指纹、当前链上计划、订单状态条件更新和支付流水幂等要求。

复核日志分页可以使用 retryable=true 筛出当前适合重新触发的候选记录:候选状态为 true、复核指纹匹配通过,且动作为 settleauto-approve。后台批量操作使用 POST /api/v1/admin/web3/payment-settlement-review-logs/retry-batch,请求体传入 ids,返回请求数、实际执行数、跳过数和每条执行结果。批量入口不创建新的资金处理链路,只按 ID 顺序复用单条重试逻辑。

自动重试任务使用 spring.open.money.web3.settlement.retry.* 配置。任务会周期扫描上述可重试候选,写入 auto-retry 审计日志,再复用同一套显式结算执行器;max-attempts 限制同一订单和复核指纹的自动重试次数。自动重试仍然不能绕过复核指纹、当前链上计划、订单状态条件更新和流水幂等。

已结算后的链上二次确认使用 POST /api/v1/admin/web3/payment-confirmations/settled-audits。该接口只读复查本地 success 订单,不回滚订单、不修改支付流水、不自动退款。它用于识别链重组、RPC 查询失败、确认数不足、交易失败或 ERC-20 Transfer 不匹配等风险,并把这些订单交给人工复核、补偿流程或后续规则化任务处理。

定时审计任务发现风险后,会写入 money_web3_payment_settlement_audit_log,并通过后台接口查询、标记 resolvedignored。风险日志只做运维闭环,不代表资金状态已经修正;后续链重组补偿或退款仍必须走独立规则和人工复核。

后台可使用 GET /api/v1/admin/web3/payment-settlement-audit-logs/summary 查看风险日志汇总,也可以使用 POST /api/v1/admin/web3/payment-settlement-audit-logs/{id}/recheck 对单条风险日志重新核验,或使用 POST /api/v1/admin/web3/payment-settlement-audit-logs/recheck-batch 通过请求体 ids 批量核验。重新核验只按日志中的订单号、链名称和交易 Hash 重新查询链上状态,返回最新审计条目和 stillRisky,不自动修改订单、流水或日志状态。GET /api/v1/admin/web3/payment-settlement-audit-logs/compensation-metadata 会返回后台页面需要的建议动作、风险等级、风险原因和处理状态选项;GET /api/v1/admin/web3/payment-settlement-audit-logs/compensation-candidates 会返回 open + risky 风险日志的只读补偿候选队列,并支持按风险等级、是否可重试、是否需要人工复核和是否需要资金复核过滤。POST /api/v1/admin/web3/payment-settlement-audit-logs/{id}/compensation-planPOST /api/v1/admin/web3/payment-settlement-audit-logs/compensation-plan-batch 会输出 mark-resolvedwait-confirmationsrecheck-latermanual-compensationmanual-review 等只读补偿建议,可选先重新核验链上状态;响应中的 riskLevelchainReorgSuspected 可用于后台排序和疑似链重组提醒。人工复核完成后,可以使用 PUT /api/v1/admin/web3/payment-settlement-audit-logs/status-batch 批量把日志标记为 resolvedignored;该入口只更新审计日志处理状态,不代表已经做了资金修正。

阻断原因

建议保持稳定原因码,方便后台统计和前端展示:

原因码说明
order-not-found本地订单不存在
order-not-settleable执行结算前本地订单状态不是 pending
order-update-conflict执行结算时订单条件更新失败,通常表示并发修改或状态已变化
provider-not-web3订单不是 Web3 支付
local-order-already-paid本地订单已支付
local-order-not-settleable本地订单状态不允许结算
chain-not-configured链配置不存在或未启用
chain-id-mismatch链 ID 不匹配
transaction-hash-missing缺少交易 Hash
chain-transaction-not-mined交易尚未出块
chain-transaction-failed链上交易失败
chain-confirmations-insufficient确认数不足
transfer-missing未找到匹配转账事件
receiver-address-mismatch收款地址不匹配
token-contract-mismatchToken 合约不匹配
amount-mismatch金额不匹配
transaction-already-exists同一订单、交易 Hash 和结算类型的支付流水已存在
review-fingerprint-mismatched复核指纹不一致
applier-not-available当前运行环境缺少结算执行器
transfer-removedERC-20 Transfer 事件已被链重组移除
rpc-query-failedRPC 查询失败

开放问题

进入代码实现前仍需确认:

  • 原生币支付如何匹配金额和收款地址。
  • 是否允许一个订单多笔转账合并支付。
  • 少付、超付、重复支付如何处理。
  • 不同链和 Token 的默认确认数策略。
  • 多商户、多租户下的收款地址归属规则。
  • 自动结算是否按金额阈值分级。
  • 自动结算失败是否进入 Queue 或 Scheduler 重试;当前只做 Notification 提醒,不自动重试。

在这些问题没有明确前,规则化全自动结算仍应保持设计态或人工确认态;当前已落地的超时 auto-approve 只复用显式结算执行器,不绕过复核指纹和流水幂等校验。

Released under the Apache License 2.0.