← 返回文档索引

pay v2 · P2 收款管线 + Provider 抽象 + webhook v2 (阅读版)

执行真相源 docs/superpowers/plans/2026-07-10-pay-v2-p2-pipeline.md(含 checkbox + 每步完整代码/测试)。设计见 pay v2 设计,DAG 见 依赖图

P2 目标:在 P1 数据地基上搭起一次性收款的完整管线 —— Provider 渠道抽象(6 render_type)+ 建单/查单/重试/取消 + 统一入账/开通(webhook 与 query 都归一成 PaidEvent,复用 P1 的幂等标付)+ pay→业务方 webhook v2(带 event_type)。用一个 fake provider 端到端验证管线,真渠道在 P3。全程复用 P1、免 docker。

7 个 TDD 任务

Task 1 · Provider 抽象接口 + render_type + PaidEvent + 注册表

文件 internal/provider/provider.go。核心契约,P3 所有渠道 adapter 依赖它:

type Provider interface {
  Method() string
  Capabilities() Capabilities
  Create(ctx, CreateRequest) (*Session, error)
  VerifyCallback(ctx, CallbackInput) (*PaidEvent, error)
  Query(ctx, providerRef string) (*PaidEvent, error)
}
// 可选子接口:RefundingProvider(P4)、RecurringProvider(P8)
type Session   struct { ProviderRef string; RenderType string; Payload map[string]any; ExpiresAt *time.Time }
type PaidEvent struct { ProviderRef string; Status PaidStatus; PaidAmountMinor int64; PaidCurrency, Raw string }

6 个 render_type 常量 + PaidPending/Succeeded/Failed;Registry(Register/Get/Methods + ErrUnknownMethod)。

Task 2 · fake provider(测试用)

internal/provider/fake/fake.go —— 实现 Provider,Method()=="fake"、render=crypto_address;带 SetQueryResult 测试 seam。P2 用它端到端验管线,不接真渠道。

Task 3 · OrderStore 扩展(查询)

internal/store/order_query.go:GetOrder / AttemptByProviderRef / ListAttemptsByStatus / ExpirePendingAttempts + 哨兵错误。为管线定位订单/尝试用。

Task 4 · 一次性收款管线 gateway

internal/gateway/gateway.go:CreateOrder(选 provider→create→落 Order+Attempt→返回 {order_no, session:{render_type,payload}})/ GetOrder / RetryOrder / CancelOrder。接口 ProductResolver.Resolve(sku)(取权威价/币种/subject/bizCode)、WebhookEnqueuer.Enqueue(...)

Task 5 · 统一入账/开通管线 settle

internal/gateway/settle.go:Settle(ctx, *PaidEvent) = 归一 → 定位(provider_ref→attempt→order)→ 币种/金额核对 → 复用 P1 MarkAttemptPaid 幂等 → 仅翻转时入队 payment.succeededHandleCallback(webhook 入口)、SyncPendingAttempts(query 兜底)都汇入这条。SettleResult:ignored/not_found/amount_mismatch/duplicate/processed。

Task 6 · webhook v2 outbox + Notifier

model.WebhookDelivery(uniqueIndex(out_trade_no,event_type) 幂等)+ store.WebhookStore(Enqueue/ListUndelivered/MarkDelivered/MarkFailed)+ webhook.Notifier(DeliverPending/Start,HMAC 头 X-Pay-System/Event/Timestamp/Nonce/Sign,失败重试)。event_type 从 payment.succeeded

Task 7 · HTTP /v1 接线

router.SetupV2POST /v1/ordersGET /v1/orders/:no.../retry.../cancelPOST /v1/callback/:method;main.go 装配 provider 注册表 + gateway + notifier,AutoMigrate 加 WebhookDelivery。

关键设计取舍(计划 Self-Review 已记)

取舍说明
fake provider_ref需纳秒唯一化——P1 Attempt 有 uniqueIndex(channel,provider_ref),retry 同 method 会撞键(真渠道天然不同 ref)
webhook 无 biz_codeP1 OrderV2 无 biz_code 列,不改已落地 schema;业务方暂用 biz_ref 映射,P3 补
路由取首个账户P2 取"首个 enabled 账户",完整路由策略在 P5
币种走部署默认多币种在 P3+
/v1 与 /api/v1 并存v2 新端点与 v1 旧端点并存,收口在 pay 定稿后
审阅重点:① Provider 接口是否够覆盖后续渠道(Task1);② settle 的归一/幂等/金额核对链路(Task5,资金命脉);③ webhook outbox 幂等键 (out_trade_no,event_type) 是否合理(Task6);④ 上面 5 条取舍是否认可(尤其"路由取首个账户""币种默认"延后)。

相关:pay v2 设计 · 真相源 docs/superpowers/plans/2026-07-10-pay-v2-p2-pipeline.md