04|支付生命周期和参数:一笔 Web3 支付从创建到结算怎么走完
这一篇要解决的问题
上一轮反馈是:“后面继续讲支付的生命周期和参数,不要讲资产,这些我知道。”这一篇按这个方向推进:不展开资产类型,专门拆一笔 Web3 支付从订单创建、支付请求、钱包签名、链上确认、网关监听、订单状态更新、商户履约到结算对账的完整生命周期。
重点放在系统参数上:订单号、金额、币种、网络、收款地址、过期时间、确认数、交易哈希、状态机、回调、幂等键和异常处理。
正文
1. 一笔 Web3 支付不是一笔交易,而是一段生命周期
链上交易只是生命周期中的一个节点。完整支付要从商户创建订单开始,到商户确认可履约、财务可对账、资金可结算结束。
可以先看总流程:
| 阶段 | 发生什么 | 核心产物 |
|---|---|---|
| 订单创建 | 商户系统生成业务订单 | merchant_order_id、金额、商品、用户 |
| 支付会话创建 | 支付网关生成支付请求 | payment_id、币种、网络、地址、过期时间 |
| 用户确认支付 | 钱包展示交易并签名 | signed_transaction 或交易请求 |
| 链上广播 | 交易进入网络等待确认 | tx_hash |
| 交易监听 | 网关从链上发现交易 | observed_transaction |
| 订单匹配 | 链上交易匹配支付会话 | matched_payment |
| 状态更新 | 网关更新支付状态并回调商户 | paid、confirmed、failed 等状态 |
| 商户履约 | 商户发货、开通服务或记录收款 | fulfillment_record |
| 结算对账 | 资金进入商户账户并生成报表 | settlement_id、对账记录 |
生命周期的关键点是:每一阶段都需要明确输入、输出和状态变化。否则支付系统会在异常场景里混乱,比如用户付了但订单过期、链上到账但网关没监听到、商户重复发货、回调重试导致重复入账。
2. 第一阶段:商户订单参数
支付系统最开始接到的不是链上交易,而是商户业务订单。
商户订单通常包含:
| 参数 | 含义 | 设计要点 |
|---|---|---|
| merchant_order_id | 商户侧订单号 | 商户系统内唯一,后续对账要能查到 |
| amount | 订单金额 | 明确小数精度和计价单位 |
| currency | 计价币种 | 表示订单用什么单位计价,不一定等于链上支付资产 |
| goods / description | 商品或服务说明 | 给用户确认,也给客服和风控查证 |
| customer_id | 用户标识 | 用于风控、客服、订单历史,不一定上链 |
| callback_url | 支付结果通知地址 | 网关向商户系统发送状态更新 |
| return_url | 用户支付后跳转地址 | 只影响前端体验,不应作为支付成功依据 |
| metadata | 商户自定义字段 | 用于内部业务关联,注意不要放敏感信息 |
这里要区分两个 ID:
| ID | 谁生成 | 作用 |
|---|---|---|
| merchant_order_id | 商户 | 商户业务系统识别订单 |
| payment_id | 支付网关 | 支付系统识别一次支付会话 |
一个订单可能对应多次支付尝试。比如用户第一次支付失败,第二次重新选择网络或钱包;原订单不变,但会生成新的 payment_id。这样可以避免把业务订单和链上支付过程绑死。
3. 第二阶段:支付会话参数
支付网关收到商户订单后,会创建支付会话。支付会话是 Web3 支付系统的核心对象。
一个支付会话通常包含:
| 参数 | 含义 | 为什么重要 |
|---|---|---|
| payment_id | 支付会话 ID | 后续查询、回调、对账都围绕它展开 |
| merchant_order_id | 商户订单号 | 把支付会话和业务订单连接起来 |
| pay_amount | 用户需要支付的链上数量 | 要精确到代币最小单位或链上 decimals |
| pay_currency | 支付资产符号 | 前端展示用,不能只依赖符号判断合约 |
| chain_id / network | 支付网络 | 防止用户在错误链上付款 |
| token_contract | 代币合约地址 | 原生资产可以为空或用特殊标识 |
| recipient_address | 收款地址或合约地址 | 钱最终进入哪里 |
| expires_at | 过期时间 | 控制汇率、库存、重复支付风险 |
| min_confirmations | 需要确认数 | 控制链上回滚和商户履约风险 |
| status | 当前支付状态 | 驱动订单状态机 |
支付会话创建之后,前端展示二维码、支付链接或钱包连接按钮。用户看到的内容必须来自支付会话,而不是前端临时拼出来的数据。
这一步最重要的工程原则是:支付请求要由后端生成并可查询。前端可以展示,但不要让前端决定最终收款地址、金额、网络和过期时间。
4. 第三阶段:支付请求参数
支付请求是用户真正拿去付款的东西。它可能是二维码内容、深链接、WalletConnect 请求、网页按钮,或者钱包 SDK 调用。
支付请求需要让钱包或用户知道:
| 参数 | 说明 |
|---|---|
| chain_id | 要在哪条链上支付 |
| recipient | 收款地址或支付合约 |
| amount | 支付数量,通常要转换成最小单位 |
| token | 支付资产,包含合约地址和 decimals |
| calldata | 如果调用合约,具体函数和参数 |
| memo / reference | 如果链支持备注,用于订单匹配 |
| gas_policy | Gas 由用户付、商户代付,还是账户抽象代付 |
| expires_at | 支付请求有效期 |
这里有一个常见坑:用户界面显示的是“10 USDC”,链上交易里实际传的是整数最小单位,比如 10000000。系统必须统一处理 decimals,否则会出现金额差错。
另一个坑是币种符号。多个链上都可能有 USDT 或 USDC,合约地址不同。支付系统不能只看 symbol=USDT,必须同时校验 chain_id 和 token_contract。
5. 第四阶段:交易广播和链上确认
用户签名后,交易会被广播到链上。此时支付会话通常进入 pending 或 submitted 状态。
需要记录这些参数:
| 参数 | 含义 | 用途 |
|---|---|---|
| tx_hash | 交易哈希 | 查询链上状态、给用户和客服定位 |
| from_address | 付款地址 | 风控、对账、用户识别 |
| to_address | 收款地址或合约 | 校验是否付给正确对象 |
| block_number | 交易所在区块 | 计算确认数 |
| block_time | 区块时间 | 记录支付发生时间 |
| gas_used | 实际消耗 Gas | 成本分析和异常排查 |
| tx_status | 交易成功或失败 | 区分广播成功和执行成功 |
广播成功不等于支付成功。交易可能长时间 pending,可能失败,可能被替换,可能因为 Gas 不足卡住,可能在合约执行时 revert。
支付网关要区分几类状态:
| 状态 | 含义 |
|---|---|
| created | 支付会话已创建,用户还没提交链上交易 |
| submitted | 已拿到交易哈希,但链上还没确认 |
| detected | 网关已在链上发现交易 |
| confirmed | 达到确认数或最终性条件 |
| failed | 交易失败、金额错误、链错误或超时 |
| expired | 支付会话过期,后续到账要走异常处理 |
状态名可以不同,但含义要稳定。商户系统依赖这些状态决定是否发货。
6. 第五阶段:交易监听和订单匹配
网关监听链上交易时,不能只查一个 tx_hash。很多用户会直接转账,不一定通过前端流程提交交易哈希。支付系统需要从链上事件里反向发现付款。
常见监听方式:
| 方式 | 适用场景 | 注意点 |
|---|---|---|
| 监听收款地址入账 | 一次性地址或固定收款地址 | 要处理多链、多代币、重组和重复扫描 |
| 监听代币 Transfer 事件 | ERC-20、TRC-20 等代币支付 | 要校验 token_contract、from、to、amount |
| 监听支付合约事件 | 合约收款 | 事件里应包含 payment_id 或 order_id |
| 查询用户提交的 tx_hash | 前端已拿到交易哈希 | 不能只信用户提交,仍要链上校验 |
订单匹配需要校验这些条件:
| 条件 | 校验目的 |
|---|---|
| 网络一致 | 防止用户在错误链付款 |
| 代币合约一致 | 防止同名假币或错误资产 |
| 收款地址一致 | 防止地址替换或误转 |
| 金额足够 | 防止少付 |
| 支付会话未过期 | 防止旧价格、旧库存或重复支付 |
| 交易成功执行 | 防止 revert 或失败交易被当成到账 |
| 确认数足够 | 防止重组或未最终确认 |
匹配成功后,支付会话才能进入 confirmed 或 paid 状态。
7. 第六阶段:状态机和幂等
支付系统一定要有状态机。状态机的作用,是限制状态只能按允许路径变化。
一个简化状态机可以这样设计:
created
→ submitted
→ detected
→ confirmed
→ settled
created
→ expired
submitted / detected
→ failed
confirmed
→ refunded
实际系统里会更复杂,但原则相同:每个状态都要定义进入条件、退出条件和是否可逆。
幂等也很重要。链上监听、回调、用户刷新页面、商户查询接口都可能重复触发同一动作。系统必须保证重复请求不会导致重复发货、重复入账或重复结算。
常见幂等键包括:
| 幂等键 | 用途 |
|---|---|
| payment_id | 保证同一个支付会话只确认一次 |
| tx_hash + log_index | 保证同一个链上事件只处理一次 |
| merchant_order_id + status | 保证同一个订单状态更新不重复应用 |
| callback_event_id | 保证商户回调可以安全重试 |
Web3 支付里,链上事件天然可能被重复扫描。没有幂等设计,系统迟早会出重复处理问题。
8. 第七阶段:回调和商户确认
支付网关确认支付后,会通知商户系统。这个通知通常叫 webhook 或 callback。
回调内容通常包含:
| 字段 | 含义 |
|---|---|
| event_id | 本次事件 ID,用于幂等 |
| event_type | payment.confirmed、payment.failed、payment.expired 等 |
| payment_id | 支付会话 ID |
| merchant_order_id | 商户订单号 |
| status | 当前支付状态 |
| tx_hash | 链上交易哈希 |
| amount | 实际支付金额 |
| chain_id | 支付网络 |
| token_contract | 代币合约地址 |
| confirmed_at | 确认时间 |
| signature | 网关对回调内容的签名 |
商户收到回调后,不应该只因为 HTTP 请求到了就发货。还要验证回调签名、检查 event_id 是否处理过、确认订单金额和状态,然后再更新业务订单。
回调必须支持重试。商户服务器可能短暂不可用,网关应该按退避策略重发。商户接口也要支持幂等,重复收到同一个 confirmed 事件时,只返回成功,不重复发货。
9. 第八阶段:异常处理
Web3 支付的大量复杂度来自异常场景。
| 异常 | 表现 | 处理方式 |
|---|---|---|
| 少付 | 用户支付金额小于要求 | 标记 underpaid,提示补差或人工处理 |
| 多付 | 用户支付金额大于要求 | 标记 overpaid,按规则退款或记入余额 |
| 过期后到账 | 订单过期后用户才付款 | 标记 late_payment,按当前价格或人工处理 |
| 转错链 | 用户在不支持网络付款 | 如果商户控制地址,人工找回;否则可能无法处理 |
| 转错币 | 用户转了错误代币 | 看是否可识别和可退回 |
| 重复支付 | 同一订单收到多笔付款 | 第一笔确认订单,后续进入退款或余额处理 |
| 交易失败 | tx_hash 存在但执行失败 | 不确认订单,提示用户重新支付 |
| 链拥堵 | 长时间 pending | 保持等待、提示加 Gas 或重新发起 |
| 回调失败 | 商户没收到通知 | 网关重试,商户可主动查询状态 |
异常处理要提前写进产品规则。用户支付后才临时决定怎么处理,会让客服、财务和风控都很难办。
10. 第九阶段:结算和对账
支付确认后,商户还需要对账。对账不是确认用户是否付款,而是把业务订单、链上交易、手续费、汇率、结算记录放到同一张表里。
对账记录至少要包含:
| 字段 | 用途 |
|---|---|
| merchant_order_id | 对应业务订单 |
| payment_id | 对应支付会话 |
| tx_hash | 对应链上交易 |
| chain_id | 对应网络 |
| token_contract | 对应支付资产 |
| paid_amount | 用户实际支付数量 |
| expected_amount | 应支付数量 |
| fee | 网关费、链上费或换汇费 |
| settlement_amount | 商户最终结算金额 |
| settlement_time | 结算时间 |
| settlement_status | 待结算、已结算、失败、冻结 |
对账系统要支持从两边查:财务拿订单号能查到链上交易;客服拿 tx_hash 能查到订单;风控拿地址能查到相关订单和处理记录。
Web3 支付系统成熟与否,很大程度体现在对账能力上。只让用户完成链上转账,还不能算完整商户支付系统。
11. 一张生命周期检查表
看一个 Web3 支付产品时,可以按这张表检查:
| 检查项 | 要问的问题 |
|---|---|
| 订单创建 | merchant_order_id 是否唯一?金额和计价单位是否清楚? |
| 支付会话 | payment_id、chain_id、token_contract、recipient、expires_at 是否明确? |
| 支付请求 | 用户看到的金额、网络、收款方是否来自后端可信数据? |
| 钱包签名 | 钱包展示的内容是否可理解?授权范围是否过大? |
| 交易监听 | 系统如何发现交易?监听地址、Transfer 事件还是合约事件? |
| 订单匹配 | 网络、代币、地址、金额、确认数、过期时间如何校验? |
| 状态机 | created、submitted、detected、confirmed、failed、expired 等状态是否定义清楚? |
| 幂等 | 重复扫描、重复回调、重复刷新是否会造成重复发货或入账? |
| 回调 | webhook 是否签名?是否支持重试?商户是否验证幂等? |
| 异常处理 | 少付、多付、过期到账、转错链、重复支付如何处理? |
| 对账 | 订单、payment_id、tx_hash、费用、结算记录能否互相追溯? |
这张表可以直接用来读一个支付网关的 API 文档,也可以用来设计自己的支付系统。
小结
- Web3 支付是一段生命周期,不是一笔孤立链上交易。
- 关键对象包括 merchant_order_id、payment_id、支付请求、tx_hash、状态机、回调事件和结算记录。
- 支付会话要明确 chain_id、token_contract、recipient_address、pay_amount、expires_at 和 min_confirmations。
- 订单匹配必须校验网络、代币、收款地址、金额、过期时间、交易状态和确认数。
- 状态机、幂等、回调签名、异常处理和对账,是商户支付系统稳定运行的核心。
下一篇预告
下一篇继续沿着支付生命周期展开,进入“支付网关 API 设计”。我们会拆创建支付、查询状态、接收回调、退款、对账这几类接口应该怎么设计,重点看字段、状态码、幂等键和错误码。