Web3 Payment Starter
受众:开发者 摘要:Web3 Payment Provider(创建收款上下文 + 查询确认)+ Money Web3 支付链上确认 + 结算复核审计。
何时使用
| 场景 | 建议 |
|---|---|
| 创建链上收款上下文 | Payment.create("web3", ...) |
| 查询交易确认状态 | Payment.query("web3", ...) |
| 单笔 / 批量只读确认 + 结算建议 | POST /api/v1/admin/web3/payment-confirmations |
| 已结算订单链上二次审计 | POST .../settled-audits |
| dry-run 结算计划 + 复核指纹 | POST .../settlement-plans |
| 显式 / 自动结算执行器 + 审计 + 重试 | POST /api/v1/admin/web3/payment-settlement-review-logs/... |
Web3 Payment Provider
Web3 starter 会在 Payment starter 存在时注册 web3 支付 Provider。这个 Provider 面向业务仍然是 Payment 抽象,底层能力留在 Web3 starter 内,避免 web3j、RPC、合约交互依赖进入通用支付模块。
通过 Cashier 发起支付时,Web3 Provider 只消费 Cashier 已确认的 quote 快照:定价金额由业务订单提供,实际链上应收金额来自 quote 的 payAmount / paySymbol / payDecimals。业务模块和 Web3 Provider 都以 quote 为支付金额来源,链上确认也会按 quote 的支付资产数量校验 ERC-20 Transfer 或原生币转账。
创建支付时返回链上收款上下文:
PaymentResult result = Payment.create("web3", PaymentCreateRequest.builder()
.outTradeNo("ORDER_202605210001")
.provider("bsc")
.amount(new BigDecimal("1.23"))
.currency("BNB")
.build());返回 payload 会包含:
| 字段 | 说明 |
|---|---|
chainName | 收款链名称 |
receiverAddress | checksum 格式收款地址 |
amount | 支付金额 |
amountRaw | 按 Token 精度换算后的原始最小单位金额,后续只读事件匹配会优先使用 |
currency | 币种;未传时使用链原生代币符号 |
scene | 支付场景 |
notifyUrl | 异步通知地址,仅作为上下文返回,当前 Web3 Provider 不主动回调 |
returnUrl | 同步返回地址,仅作为上下文返回 |
tokenContract | Token 合约地址,空表示原生代币 |
tokenSymbol | Token 符号 |
tokenDecimals | Token 精度 |
requiredConfirmations | 要求确认数 |
查询支付时传入交易 Hash:
PaymentResult result = Payment.query("web3", PaymentQueryRequest.builder()
.outTradeNo("ORDER_202605210001")
.provider("bsc")
.transactionId("0x...")
.attribute("requiredConfirmations", 12)
.build());状态规则:
- 链上交易失败:返回
failed。 - 链上交易成功且达到确认数:返回
success。 - 未上链、未达到确认数或仍在等待回执:返回
pending。
安全边界:
- 不托管私钥。
- 不签名交易。
- 不广播交易。
- 不自动修改
cashier_payment_order或写支付流水。 - 不凭交易 Hash 直接认为订单可结算。
配置示例:
spring:
open:
web3:
payment:
enabled: true
chain-name: bsc
receiver-address: "0x0000000000000000000000000000000000000001"
token-contract: ""
token-symbol: ""
token-decimals: 18
required-confirmations: 12配置优先级与 Web3 其他配置一致:数据库配置中的 spring.open.web3.payment.* 优先于本地 application.yml。其中 receiver-address 是创建收款上下文的必要配置,也可以在 PaymentCreateRequest.attributes.receiverAddress 中按订单覆盖。
Web3 支付链上确认
Money Web3 模块提供一个后台只读确认入口,用于把本地 cashier_payment_order 和链上交易确认状态放在同一份结果里对比:
POST /api/v1/admin/web3/payment-confirmations请求示例:
{
"outTradeNo": "ORDER_202605210001",
"chainName": "bsc",
"transactionHash": "0x0000000000000000000000000000000000000000000000000000000000000000",
"requiredConfirmations": 12,
"traceId": "trace-001"
}接口权限:
money:web3:payment-confirmation:execute行为规则:
- 接口只读,不修改支付订单、支付流水或链上状态。
transactionHash可从请求直接传入,也可放在支付订单attributes中的web3.transactionHash、web3TransactionHash、transactionHash、txHash或hash。chainName可从请求直接传入,也可放在attributes.web3.chainName、web3ChainName、web3Chain、chainName或chain。requiredConfirmations可从请求直接传入,也可放在attributes.web3.requiredConfirmations、web3RequiredConfirmations、requiredConfirmations或confirmations;未提供时默认12。- 如果订单
transaction_id本身是 EVM 交易 Hash,也会作为兜底来源。 - 返回中的
settlementSuggested=true表示链上已成功并达到确认数,但本地订单还不是成功状态,后续可由明确的补偿服务在完成金额、收款地址、Token 合约和事件匹配后再幂等更新订单。 - 返回中的
settlementReady=true表示当前只读检查没有发现结算阻断原因。它仍然不是自动结算指令,只是给后续补偿链路做更明确的前置判断。 settlementBlockReasons使用稳定原因码,例如transaction-hash-missing、chain-transaction-not-mined、chain-confirmations-insufficient、local-order-already-paid,方便后台页面和运维任务做分类展示。- 当订单
attributes中存在receiverAddress、tokenContract和tokenDecimals或amountRaw时,接口会在交易所在区块内只读查询 ERC-20Transfer事件,并返回transferMatch。这个字段只做事件匹配分析,不会更新订单或流水。
响应重点字段:
| 字段 | 说明 |
|---|---|
localPaid | 本地订单是否已经是支付成功 |
chainConfirmed | 链上交易是否成功且达到要求确认数 |
transactionUrl | 基于 block-explorer-url 生成的交易浏览器链接,未配置时为空 |
blockUrl | 交易所在区块浏览器链接,未配置或交易未上链时为空 |
currentBlockUrl | 当前链头区块浏览器链接,未配置时为空 |
matched | 本地状态与链上确认状态是否一致 |
settlementSuggested | 是否建议后续补偿链路继续处理 |
settlementReady | 当前只读检查是否没有结算阻断原因 |
settlementBlockReasons | 结算前阻断原因码列表 |
transferMatch.checked | 是否执行了 ERC-20 转账事件匹配 |
transferMatch.matched | 事件链名、交易 Hash、收款地址、Token 合约和金额是否匹配 |
transferMatch.transactionUrl | 转账事件所属交易浏览器链接,未配置浏览器地址时为空 |
transferMatch.receiverAddressUrl | 预期收款地址浏览器链接 |
transferMatch.actualReceiverAddressUrl | 实际转账事件收款地址浏览器链接 |
transferMatch.tokenContractUrl | 预期 Token 合约浏览器链接 |
transferMatch.actualTokenContractUrl | 实际转账事件 Token 合约浏览器链接 |
transferMatch.blockUrl | 转账事件所在区块浏览器链接 |
transferMatch.mismatchReasons | 转账事件不匹配原因码列表 |
querySuccess | 本次链上查询是否成功 |
也可以批量确认待处理订单:
POST /api/v1/admin/web3/payment-confirmations/batch批量入口默认查询 provider=web3 且 status=pending 的支付订单,并逐笔执行只读链上确认:
{
"provider": "web3",
"status": "pending",
"limit": 20,
"olderThanMinutes": 5,
"chainName": "bsc",
"requiredConfirmations": 12,
"onlySettlementSuggested": true,
"onlyTransferMatched": false,
"traceId": "trace-batch-001"
}批量结果会返回:
| 字段 | 说明 |
|---|---|
scannedCount | 本次扫描到的订单数量 |
returnedCount | 最终返回的确认结果数量 |
querySuccessCount | 链上查询成功数量 |
matchedCount | 本地状态与链上状态一致数量 |
settlementSuggestedCount | 建议后续补偿处理数量 |
settlementReadyCount | 当前只读检查无阻断原因的数量 |
transferCheckedCount | 已执行 ERC-20 转账事件匹配的返回结果数量 |
transferMatchedCount | ERC-20 转账事件匹配通过的返回结果数量 |
transferMismatchedCount | 已执行转账事件匹配但不匹配的返回结果数量 |
failedCount | 链上查询失败或参数缺失数量 |
onlySettlementSuggested=true 时只返回需要进入复核、结算或补偿处理的候选项。onlyTransferMatched=true 时只返回 ERC-20 转账事件匹配通过的候选项;如果两个开关同时启用,会同时满足两个条件才返回。这个批量入口仍然只读,不更新订单、不写流水,适合作为复核结算或补偿任务前的候选发现能力。
已结算订单也可以做链上二次审计:
POST /api/v1/admin/web3/payment-confirmations/settled-audits该入口默认扫描 provider=web3 且本地状态为 success 的订单,并重新查询链上交易确认状态和可选 ERC-20 Transfer 匹配结果:
{
"provider": "web3",
"limit": 20,
"olderThanMinutes": 60,
"chainName": "bsc",
"requiredConfirmations": 12,
"onlyRisky": true,
"traceId": "trace-audit-001"
}审计入口只读,不回滚订单、不修改流水、不触发退款。它用于发现已结算后的链重组、RPC 异常、交易失败、确认数不足或转账事件不匹配风险。返回结果会包含 scannedCount、returnedCount、healthyCount、riskyCount、transferCheckedCount、transferMatchedCount、transferMismatchedCount 和逐条 riskReasons。常见风险原因码包括 query-failed、transaction-not-mined、transaction-failed、confirmations-insufficient、transfer-removed 和 transfer-mismatched。其中 transfer-removed 表示 ERC-20 Transfer 事件已被链重组移除,后台应按疑似链重组优先复核。
onlyRisky=true 时只返回风险项,适合后台定时巡检或人工运维页展示;不开启时会返回本轮扫描到的所有已结算订单复查结果,方便做全量抽样。
Money Web3 模块还内置已结算审计 Scheduler 任务,默认每 30 分钟扫描一页已结算 Web3 订单,只读发现风险并通过 Notification 发送提醒:
spring:
open:
money:
web3:
settlement:
audit:
enabled: true
auto-startup: true
task-name: money.web3.payment-settlement.audit
instance-id: default
fixed-delay: 30m
provider: web3
limit: 50
older-than-minutes: 60
chain-name:
required-confirmations:
only-risky: true
risk-notification:
enabled: true
channels:
- log
subject: Web3 payment settlement audit risk
recipient-type:
recipient-value:该任务和手动审计接口共用同一套只读逻辑。risk-notification 复用 Notification starter,默认 log 通道不需要额外接收人;生产环境可切换到 database、mail、sms 或 webhook 等通道。任务发现风险只提醒,不自动修改资金状态。
发现风险后,任务会把风险项写入后台审计日志,避免告警只停留在日志或通知里:
GET /api/v1/admin/web3/payment-settlement-audit-logs
GET /api/v1/admin/web3/payment-settlement-audit-logs/summary
GET /api/v1/admin/web3/payment-settlement-audit-logs/compensation-metadata
GET /api/v1/admin/web3/payment-settlement-audit-logs/compensation-candidates
GET /api/v1/admin/web3/payment-settlement-audit-logs/{id}
POST /api/v1/admin/web3/payment-settlement-audit-logs/{id}/recheck
POST /api/v1/admin/web3/payment-settlement-audit-logs/{id}/compensation-plan
POST /api/v1/admin/web3/payment-settlement-audit-logs/compensation-plan-batch
POST /api/v1/admin/web3/payment-settlement-audit-logs/recheck-batch
PUT /api/v1/admin/web3/payment-settlement-audit-logs/{id}
PUT /api/v1/admin/web3/payment-settlement-audit-logs/status-batch
DELETE /api/v1/admin/web3/payment-settlement-audit-logs/{id}
DELETE /api/v1/admin/web3/payment-settlement-audit-logs审计日志表为 money_web3_payment_settlement_audit_log,记录订单号、链名称、交易 Hash、风险原因、确认快照和处理状态。状态只保留 open、resolved、ignored 三种:open 表示待处理,resolved 表示已处理,ignored 表示确认后忽略。列表支持 riskReason 过滤,例如 query-failed、confirmations-insufficient 或 transfer-mismatched。summary 入口返回总数、各状态数量、查询失败、链上未结算和 Transfer 不匹配数量,方便后台仪表盘展示风险规模。status-batch 通过请求体 ids 批量标记状态,适合人工批量复核后统一处理运维待办。定时任务对同一商户订单号、交易 Hash 和风险原因的未处理 open 记录做去重;如果历史风险已经 resolved 或 ignored,后续再次发现会生成新的待处理记录。
recheck 入口会按风险日志中的订单号、链名称和交易 Hash 重新执行一次只读链上确认,并返回最新审计条目和 stillRisky。批量入口通过请求体传入 ids,避免大量 ID 放入 URL。它们不会自动把日志改成 resolved,也不会回滚订单、修改流水或触发退款;后台应由人工根据最新结果选择标记处理状态或进入补偿流程。
compensation-plan 入口会基于当前风险日志生成只读补偿建议,也可以通过请求体 {"recheck": true} 先重新核验链上状态。compensation-plan-batch 通过请求体 ids 批量生成同类建议。返回的 recommendedAction 可能是 mark-resolved、wait-confirmations、recheck-later、manual-compensation 或 manual-review;同时返回 riskLevel 和 chainReorgSuspected,用于后台优先级排序和疑似链重组提示。该入口只给后台操作建议,不代表已经执行资金修正。
compensation-metadata 入口返回后台页面需要的稳定选项,包括建议动作、风险等级、风险原因、处理状态、可重试动作、需要人工复核动作和需要资金复核动作。前端不应硬编码这些选项;新增动作或风险原因时,优先由后端元数据支撑页面筛选、按钮和文案。
compensation-candidates 入口返回当前 open + risky 风险日志的只读补偿候选队列,按风险等级优先级排序。它支持 limit、riskLevel、retryable、manualReviewRequired 和 fundsReviewRequired 查询参数,适合后台默认展示“最需要处理”的风险项。该入口不重新查询链上状态;需要最新链上结果时,仍应使用 recheck 或带 recheck=true 的补偿计划入口。
也可以生成结算 dry-run 计划:
POST /api/v1/admin/web3/payment-confirmations/settlement-plans请求参数与批量确认基本一致,额外支持:
outTradeNo:按商户订单号生成单笔结算计划;不传时按批量规则扫描订单。onlyCandidates=true:只返回当前可进入人工确认或后续补偿 Service 的候选项。
计划入口仍然只读,只把确认结果转换为建议动作:
| 动作 | 说明 |
|---|---|
settle | 链上确认成功、本地未支付、无阻断原因,并且 ERC-20 转账事件匹配通过 |
review-transfer-unchecked | 链上确认满足结算前置条件,但订单缺少 Token 转账匹配所需上下文,需要人工复核 |
review-transfer-mismatched | 链上确认满足结算前置条件,但 ERC-20 转账事件金额、地址、合约等不匹配 |
blocked | 交易缺失、未出块、失败、确认数不足、本地已支付或查询失败等原因阻断 |
每个计划条目还会返回 checks,供后台页面做人工复核清单:
| 检查项 | 说明 |
|---|---|
query-success | 链上确认查询是否成功 |
chain-confirmed | 链上交易是否成功且达到确认数 |
local-order-unpaid | 本地订单是否仍未支付 |
settlement-ready | 只读结算前置检查是否无阻断 |
transfer-checked | 是否执行了 ERC-20 转账事件检查 |
transfer-matched | ERC-20 转账事件是否匹配金额、收款地址和 Token 合约 |
计划条目还会返回 reviewFingerprint。它是基于订单号、链名、交易 Hash、本地订单状态、链上确认、转账匹配结果、Transfer 事件区块号、logIndex、动作和检查项生成的 SHA-256 指纹,用于后续人工确认或补偿提交前校验“操作员看到的计划”和“服务端当前计划”是否一致。同一交易 Hash 内如果存在多条 Transfer,或重新查询时匹配到的事件位置、金额、地址、合约、原因码发生变化,指纹都会失配。
计划条目的 confirmation 嵌套对象会沿用只读确认结果字段,因此在配置了 block-explorer-url 时,也会包含 transactionUrl、blockUrl 和 currentBlockUrl,方便后台页面跳转到区块浏览器人工核验。
提交人工确认前可以先调用指纹校验入口:
POST /api/v1/admin/web3/payment-confirmations/settlement-plans/verify请求体传入 outTradeNo、reviewFingerprint、可选 chainName 和 requiredConfirmations。接口会重新生成单笔 dry-run 计划并返回 matched、currentReviewFingerprint 和当前计划内容;matched=false 表示订单、链上确认、转账事件或检查项已经变化,需要页面重新展示最新计划。
后台也提供人工复核日志入口:
GET /api/v1/admin/web3/payment-settlement-review-logs
GET /api/v1/admin/web3/payment-settlement-review-logs/{id}
POST /api/v1/admin/web3/payment-settlement-review-logs
POST /api/v1/admin/web3/payment-settlement-review-logs/auto-handle-expired
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/settle
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/retry
POST /api/v1/admin/web3/payment-settlement-review-logs/retry-batch
DELETE /api/v1/admin/web3/payment-settlement-review-logs/{id}
DELETE /api/v1/admin/web3/payment-settlement-review-logsPOST 请求体与指纹校验入口一致,可额外传 remark。服务端会重新校验指纹并保存 matched、currentReviewFingerprint、建议动作、候选状态和计划快照,用于追踪人工复核过程;记录复核日志本身不会修改订单或流水。
分页查询支持 candidate、matched 和 retryable 筛选。retryable=true 会收敛到 candidate=true、matched=true 且动作为 settle 或 auto-approve 的记录,适合后台先筛出可重新触发结算的候选项。
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/settle 会在事务内基于已保存的复核日志执行结算。执行前会再次生成单笔计划并校验 reviewFingerprint,只有当前计划仍是 settle 候选、链上确认满足要求、ERC-20 转账匹配通过、本地订单状态仍是 pending、并且 outTradeNo + transactionHash + web3-settlement 流水未存在时,才会通过条件更新把订单状态改为 success 并写入 web3-settlement 支付流水。重复执行、订单已经支付、订单状态不允许结算或并发更新冲突都会返回稳定跳过原因,不会重复写流水。
复核日志记录、超时自动通过 / 驳回、自动重试和手动执行结算都会先经过审批工作流 money.web3.payment-settlement.review 校验动作、来源状态和权限快照。审批工作流不保存资金事实,也不替代复核指纹、链上确认、订单条件更新、支付流水幂等或 Outbox / 补偿链路。
POST /api/v1/admin/web3/payment-settlement-review-logs/{id}/retry 是面向运维的重试语义入口,适合 RPC 临时失败、自动处理跳过或人工排查后重新触发。它不会绕过任何资金校验,内部仍复用同一套复核日志执行链路、当前计划重算、复核指纹、订单条件更新和流水幂等。
POST /api/v1/admin/web3/payment-settlement-review-logs/retry-batch 使用请求体传入 ids,用于后台批量重试已筛选的复核日志。批量入口只是顺序调用同一套单条执行链路,并返回请求数、成功执行数、跳过数和每条执行结果;不把大量 ID 放入 URL。
auto-handle-expired 会扫描超时未处理的复核候选项,并按配置写入自动处理审计日志。action=auto-approve 且 execute-approved=true 时,会继续调用同一套结算执行器;执行前仍会重新校验复核指纹、当前链上计划和流水幂等。默认配置如下:
spring:
open:
money:
web3:
settlement:
review:
auto:
enabled: true
auto-startup: true
task-name: money.web3.payment-settlement-review.auto-handle
instance-id: default
fixed-delay: 5m
timeout: 30m
action: auto-approve # auto-approve 或 auto-reject
execute-approved: true
limit: 50
failure-notification:
enabled: true
channels: log
subject: Web3 payment settlement failed
recipient-type: ""
recipient-value: ""
retry:
enabled: true
auto-startup: true
task-name: money.web3.payment-settlement.retry
instance-id: default
fixed-delay: 10m
limit: 50
max-attempts: 3后续新增任何需要人工审核的业务流,都应该沿用同一类 review.auto 思路:支持关闭、超时时间、批量数量、自动处理动作和后台审计,不要把超时策略写死在业务代码里。
自动执行结算失败或被幂等规则跳过时,会按 failure-notification 发送提醒。默认使用 log 通道,生产环境可切到 database、mail、sms、webhook 等 Notification 通道。
settlement.retry 会周期扫描 retryable=true 的复核日志,并写入一条 auto-retry 审计日志后复用同一套结算执行器。它只处理 candidate=true、matched=true、动作为 settle 或 auto-approve 的记录;max-attempts 限制同一订单和复核指纹的自动重试次数,避免持续刷同一问题。自动重试不会绕过复核指纹、当前链上计划、订单状态条件更新和支付流水幂等。
dry-run 计划不会自动更新订单或流水。显式结算执行器已经补齐第一阶段事务闭环,并使用数据库无关的 pending -> success 条件更新保护并发幂等;执行时仍必须重新校验订单状态、金额、收款地址、Token 合约、链 ID、交易确认、复核指纹和支付流水幂等条件。
自动结算专项的状态机、幂等 key、链重组处理和人工复核边界见 Web3 支付自动结算设计。
相关
- Payment Facade / 服务商 Provider / 后台运维:payment.md
- Web3 支付自动结算设计:web3-payment-settlement.md
- 事件扫描 / 检查点:web3-event-scan.md
- Web3 顶层概览:web3.md