Skip to content

支付与订单子系统

支付与网关用量计费是两条不同链路:网关计费结算一次模型请求;支付子系统把外部资金转换为用户余额或订阅权益。

支付设计的核心不是“生成二维码”,而是确保回调可信、订单状态单向推进、重复通知不重复履约、错过回调仍可对账、退款能够追溯。

组件地图

text
用户支付页面
   │ 创建订单

Payment Handler ──▶ PaymentService ──▶ payment_orders

                           ├─ Provider Registry / Load Balancer
                           │       └─ 支付 Provider API

支付平台 Webhook ──────────┤ 验签、金额与订单校验

                           ├─ Balance Fulfillment ──▶ 用户余额
                           ├─ Subscription Fulfillment ──▶ 用户订阅
                           └─ Audit / Refund / Reconcile

主要代码位于:

  • backend/internal/payment/:Provider 接口、类型和具体实现。
  • backend/internal/service/payment_*.go:订单、履约、回调、退款、配置和统计。
  • backend/internal/handler/payment*.go:用户与 Webhook HTTP 适配。
  • backend/internal/handler/admin/payment_handler.go:后台订单和 Provider 管理。
  • backend/internal/server/routes/payment.go:三类路由边界。

三类路由

路由调用者鉴权
/api/v1/payment/*已登录用户JWT + backend mode guard
/api/v1/payment/public/*支付结果恢复页签名 resume token 或兼容查询约束
/api/v1/payment/webhook/*支付平台Provider 签名,不使用用户 JWT
/api/v1/admin/payment/*管理员AdminAuth + compliance guard

公开结果页不应因此拥有任意订单查询能力。新增恢复字段时必须签名或从服务端持久状态解析,不能把用户 ID、金额等客户端参数直接当成事实。

Provider 实例

支付方式不是一个全局静态配置。payment_provider_instances 可以保存多个 Provider 实例,运行时通过 Load Balancer 选择,并解密实例配置创建 Provider 对象。

PaymentService 维护进程内 Provider Registry:

  • 首次使用时 lazy load 已启用实例。
  • 后台增删改 Provider 后调用 RefreshProviders()
  • Webhook 会优先根据订单绑定/快照选择能够验签的 Provider。

多实例环境要特别关注 Registry 刷新传播。一次后台保存直接刷新的是处理该请求的进程;没有通用跨实例事件时,其他副本可能仍持有旧 Registry。高风险 Provider 变更应验证每个副本,或通过滚动重启确保一致。

订单创建

创建订单前会检查:

  • 支付系统与对应方式是否启用。
  • 订单类型是余额充值还是订阅购买。
  • 金额/套餐、币种、费率和最小最大限制。
  • 用户待支付订单数量等限制。
  • Provider 可用性和终端环境,如移动端、微信浏览器。

订单写入 PostgreSQL 后再调用 Provider 创建支付。关键字段包括:

  • 内部订单 ID。
  • out_trade_no 外部订单号。
  • 用户、金额、实付金额和币种。
  • order_type 与 plan ID。
  • payment type/provider instance。
  • Provider 快照和返回的 trade/intent 信息。
  • 过期时间和恢复 token 所需上下文。

订单快照很重要:管理员后续修改 Provider 配置时,历史订单仍要知道由哪个实例创建和验签。

正向状态机

text
pending
  ├─ 用户取消 ─────────────▶ cancelled
  ├─ 超时任务 ─────────────▶ expired
  ├─ 创建/处理失败 ─────────▶ failed
  └─ 支付确认 ──────────────▶ paid
                                  │ 条件更新获得履约权

                             recharging
                              ├─ 成功 ─▶ completed
                              └─ 失败 ─▶ failed ──管理员 retry──┐
                                                               └─▶ recharging

状态转换使用条件更新防止多个回调或查询同时取得履约权。completed 是余额/订阅已经实际交付,不只是支付平台说“成功”。

取消订单前会主动查询 Provider,避免“平台已经支付、Webhook 尚未到达”时错误取消。本地过期也不等于支付平台永远未支付;系统保留有限 grace 处理晚到的成功通知。

Webhook 处理链

不同 Provider 的 Handler 负责读取原始请求并验签,统一通知进入:

text
Provider 验签
  → 标准化 PaymentNotification
  → 按 out_trade_no 查询订单
  → 校验订单绑定的 Provider
  → 校验 Provider metadata
  → 校验 amount 有效且与 pay_amount 匹配
  → 条件更新为 paid
  → 写 ORDER_PAID 审计
  → 执行余额或订阅履约

金额比较按币种 minor unit 使用容差,而不是直接比较未经约束的浮点输入。无效、NaN/Inf、Provider 不匹配和 metadata 不匹配都会拒绝并写审计。

找不到本环境订单的通知属于终止型配置错误。Webhook Handler 应避免让支付平台无限重试,同时在审计/日志中留下 out_trade_no 以排查跨环境回调地址。

幂等与重复通知

支付平台可能重复通知,用户也可能同时点击“查询支付结果”。幂等依赖:

  • 订单唯一外部编号。
  • 允许前态的条件更新。
  • paid/recharging/completed/failed 状态检查。
  • 履约记录和支付审计。
  • 用户余额/订阅写入的事务与重复检查。

completed 或已退款订单再次收到相同通知应直接成功;failed 可以进入受控重试;paid/recharging 表示另一个处理者可能正在履约,不能重复充值。

新增 Provider 时必须用“同一 Webhook 并发提交两次”测试,而不只是顺序调用两次。

两种履约

余额充值

余额订单在取得 recharging 状态后,通过事务更新用户余额、总充值等相关记录,再把订单标记为 completed。如果中途失败,订单进入可重试状态;不能只看 Provider 已支付就手工再加一遍余额。

订阅购买

订阅订单根据创建时的 plan/金额上下文创建或扩展订阅,并处理邀请返利等附属效果。订阅履约也必须幂等,旧成功审计可用于避免历史订单重复工作。

新增履约类型时,不要在 Webhook Handler 直接写用户表;应扩展订单类型、状态机、事务履约、重试、退款和审计。

主动查询与补偿

Webhook 可能因 DNS、WAF、证书或 Provider 重试策略丢失。系统提供订单 verify/reconcile:

  1. 用户或后台提供受约束的订单引用。
  2. 服务端根据订单绑定的 Provider 主动查询。
  3. Provider 返回已支付时,重新进入统一 notification/fulfillment 链。
  4. 仍未支付时保持 pending,或按明确操作取消。

补偿不能绕过金额、Provider 和幂等校验。对账脚本也应调用领域 Service 或受保护 API,而不是直接把数据库状态改成 completed

退款状态机

text
completed
   → refund_requested
   → refunding
   ├─ Provider 已受理、等待结果 ─▶ refund_pending
   ├─ 部分退款 ─────────────────▶ partially_refunded
   ├─ 全额退款 ─────────────────▶ refunded
   └─ 失败 ─────────────────────▶ refund_failed

退款不只是调用 Provider:还要判断订单类型、可退金额、余额/订阅是否已消费、是否需要强制扣回、Provider 查询结果和最终审计。订阅与余额订单的回收方式不同。

管理员重试退款前应先查询 Provider 侧状态,避免上游已成功但本地响应丢失造成二次退款。

支付变更测试

新增 Provider 或修改状态机至少覆盖:

  1. 创建订单与 Provider 返回各种 result type。
  2. 签名失败、Provider 不匹配和金额不匹配。
  3. 相同 Webhook 顺序/并发重复。
  4. Webhook 与主动 verify 同时发生。
  5. pending 取消时 Provider 已支付。
  6. expired 后 grace 内/外收到成功通知。
  7. 余额和订阅履约中途失败后重试。
  8. Provider 配置修改后历史订单仍可验签/查询。
  9. 退款成功、pending、部分成功和查询补偿。
  10. 多实例 Registry 配置一致性。

用户用量计费见用量、计费与配额,生产支付故障应进入事件响应流程

本文档用于帮助你理解、部署和运维 Sub2API。使用前请确认上游服务条款与当地法律要求。