Web3 支付自动结算设计
受众:开发者 摘要:Web3 支付结算专题:交易确认 / 风控审计 / 自动结算 / 资金归集。
本文档说明 Web3 支付从“只读确认”进入“自动结算”必须满足的设计边界。当前项目已经提供链上收款上下文、交易确认、ERC-20 Transfer 匹配、结算 dry-run 计划、复核指纹、复核日志、显式结算执行器、超时自动通过执行、失败通知、自动重试、已结算链上审计、订单状态条件更新和 Transfer 事件唯一性指纹;后续继续增强的是更细的风控策略、链重组工单化处理和真实生产联调。
自动结算属于支付核心链路。实现和扩展时,必须始终明确状态机、幂等、链重组、人工复核、审计和异常兜底。
当前边界
已完成能力:
- Payment
web3Provider 创建链上收款上下文。 - Payment
web3Provider 查询交易确认状态。 - Money Web3 后台单笔和批量只读确认接口。
- Money Web3 后台已结算订单链上二次审计接口。
- ERC-20
Transfer事件匹配。 - 结算 dry-run 计划。
- 计划检查项和
reviewFingerprint。 - 人工复核日志留痕。
- 基于复核日志的显式结算执行入口。
- 超时自动处理、自动重试和失败通知。
- 已结算订单链上审计、风险日志和只读补偿建议。
当前不会做的事情:
- 支付结算流程不托管私钥。
- 支付结算流程不直接签名交易。
- 支付结算流程不直接广播交易。
- 不在 dry-run 或复核日志记录阶段写成功订单。
- 超时自动处理只有在
auto-approve且execute-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 blockNumber、logIndex 和匹配原因码 |
结算事务建议流程:
- 重新生成单笔结算计划。
- 校验提交的
reviewFingerprint。 - 再次判断订单状态是否仍允许结算。
- 使用数据库无关的条件更新执行
pending -> success。 - 条件更新成功后,用事件唯一 key 写入或更新支付流水。
- 条件更新失败时重新读取订单状态,返回已支付或并发冲突原因。
- 写入人工复核日志和操作日志。
- 分发 Payment 成功处理器,供余额充值、会员权益、业务订单等场景按
scene做幂等后处理。 - 发送通知或事件,供后续业务处理。
如果事件唯一 key 已存在,应返回已处理结果,而不是重复写流水。当前实现不依赖 SELECT ... FOR UPDATE 这类数据库方言锁,优先使用 MyBatis-Plus 条件更新保证 MySQL、PostgreSQL 和 H2 下语义一致。
链重组处理
链重组是 Web3 支付必须考虑的风险。
推荐规则:
- 每条链配置独立的
requiredConfirmations。 - 自动结算前必须重新查询当前区块高度和交易回执。
- 自动结算前必须重新生成计划并校验
reviewFingerprint;指纹会纳入Transfer.blockNumber、Transfer.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.enabled | true | 是否启用超时自动处理 |
spring.open.money.web3.settlement.review.auto.auto-startup | true | 是否启动时注册 Scheduler 任务 |
spring.open.money.web3.settlement.review.auto.task-name | money.web3.payment-settlement-review.auto-handle | Scheduler 任务名称 |
spring.open.money.web3.settlement.review.auto.fixed-delay | 5m | 自动处理扫描间隔 |
spring.open.money.web3.settlement.review.auto.timeout | 30m | 人工复核超时时间 |
spring.open.money.web3.settlement.review.auto.action | auto-approve | 超时动作,可选 auto-approve 或 auto-reject |
spring.open.money.web3.settlement.review.auto.execute-approved | true | auto-approve 后是否立即调用结算执行器 |
spring.open.money.web3.settlement.review.auto.limit | 50 | 单次最多处理数量 |
spring.open.money.web3.settlement.review.auto.failure-notification.enabled | true | 自动执行结算失败或跳过时是否发送提醒 |
spring.open.money.web3.settlement.review.auto.failure-notification.channels | log | 失败通知通道,支持 Notification 已启用通道 |
spring.open.money.web3.settlement.review.auto.failure-notification.subject | Web3 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.enabled | true | 是否启用已结算订单链上审计任务 |
spring.open.money.web3.settlement.audit.auto-startup | true | 是否启动时注册 Scheduler 任务 |
spring.open.money.web3.settlement.audit.task-name | money.web3.payment-settlement.audit | Scheduler 任务名称 |
spring.open.money.web3.settlement.audit.fixed-delay | 30m | 审计扫描间隔 |
spring.open.money.web3.settlement.audit.provider | web3 | 支付 Provider |
spring.open.money.web3.settlement.audit.limit | 50 | 单次最多审计订单数 |
spring.open.money.web3.settlement.audit.older-than-minutes | 60 | 只审计早于指定分钟数的已结算订单 |
spring.open.money.web3.settlement.audit.chain-name | 空 | 可选链名称过滤 |
spring.open.money.web3.settlement.audit.required-confirmations | 空 | 可选确认数;为空时使用订单或链配置 |
spring.open.money.web3.settlement.audit.only-risky | true | 是否只保留风险项 |
spring.open.money.web3.settlement.audit.risk-notification.enabled | true | 发现风险时是否发送提醒 |
spring.open.money.web3.settlement.audit.risk-notification.channels | log | 风险通知通道,支持 Notification 已启用通道 |
spring.open.money.web3.settlement.audit.risk-notification.subject | Web3 payment settlement audit risk | 风险通知标题 |
后台也可以手动触发:
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
当前已实现单笔显式执行入口:
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、订单状态条件更新和支付流水幂等规则:
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。
- 私钥。
- 助记词。
- 服务商密钥。
- 可直接操作资金的敏感凭据。
实现顺序
推荐实现顺序:
- 固化本文档中的状态机和阻断原因。
- 先写自动结算 Service 单元测试。
- 实现单笔结算 command 和 service。
- 接入订单状态条件更新、流水幂等和复核指纹校验。
- 增加单笔后台 API。
- 增加批量结算入口,但默认关闭或需要高权限。
- 补充 Notification 失败提醒。
- 用样板订单和模拟 Web3 manager 做集成测试。
- 完成真实 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、复核指纹匹配通过,且动作为 settle 或 auto-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,并通过后台接口查询、标记 resolved 或 ignored。风险日志只做运维闭环,不代表资金状态已经修正;后续链重组补偿或退款仍必须走独立规则和人工复核。
后台可使用 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-plan 和 POST /api/v1/admin/web3/payment-settlement-audit-logs/compensation-plan-batch 会输出 mark-resolved、wait-confirmations、recheck-later、manual-compensation 或 manual-review 等只读补偿建议,可选先重新核验链上状态;响应中的 riskLevel 和 chainReorgSuspected 可用于后台排序和疑似链重组提醒。人工复核完成后,可以使用 PUT /api/v1/admin/web3/payment-settlement-audit-logs/status-batch 批量把日志标记为 resolved 或 ignored;该入口只更新审计日志处理状态,不代表已经做了资金修正。
阻断原因
建议保持稳定原因码,方便后台统计和前端展示:
| 原因码 | 说明 |
|---|---|
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-mismatch | Token 合约不匹配 |
amount-mismatch | 金额不匹配 |
transaction-already-exists | 同一订单、交易 Hash 和结算类型的支付流水已存在 |
review-fingerprint-mismatched | 复核指纹不一致 |
applier-not-available | 当前运行环境缺少结算执行器 |
transfer-removed | ERC-20 Transfer 事件已被链重组移除 |
rpc-query-failed | RPC 查询失败 |
开放问题
进入代码实现前仍需确认:
- 原生币支付如何匹配金额和收款地址。
- 是否允许一个订单多笔转账合并支付。
- 少付、超付、重复支付如何处理。
- 不同链和 Token 的默认确认数策略。
- 多商户、多租户下的收款地址归属规则。
- 自动结算是否按金额阈值分级。
- 自动结算失败是否进入 Queue 或 Scheduler 重试;当前只做 Notification 提醒,不自动重试。
在这些问题没有明确前,规则化全自动结算仍应保持设计态或人工确认态;当前已落地的超时 auto-approve 只复用显式结算执行器,不绕过复核指纹和流水幂等校验。