执行真相源 docs/superpowers/plans/2026-07-10-pay-v2-p2-pipeline.md(含 checkbox + 每步完整代码/测试)。设计见 pay v2 设计,DAG 见 依赖图。
event_type)。用一个 fake provider 端到端验证管线,真渠道在 P3。全程复用 P1、免 docker。
文件 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, QueryRequest) (*PaidEvent, error) // D4-A2:带尝试上下文快照,非裸 ref } // 可选子接口: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; PaidAt *time.Time } type QueryRequest struct { ProviderRef, OutTradeNo, AccountID string; AmountMinor int64; Currency string; CreatedAt time.Time; ExpiresAt *time.Time }
6 个 render_type 常量 + PaidPending/Succeeded/Failed;Registry(Register/Get/Methods + ErrUnknownMethod)。
internal/provider/fake/fake.go —— 实现 Provider,Method()=="fake"、render=crypto_address;带 SetQueryResult 测试 seam。P2 用它端到端验管线,不接真渠道。
internal/store/order_query.go:GetOrder / AttemptByProviderRef / ListAttemptsByStatus / ExpirePendingAttempts + 哨兵错误。为管线定位订单/尝试用。
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(...)。
internal/gateway/settle.go:Settle(ctx, *PaidEvent) = 归一 → 定位(provider_ref→attempt→order)→ 币种/金额核对 → 先幂等入队 payment.succeeded,再复用 P1 MarkAttemptPaid 幂等翻转(D4-A1 崩溃安全顺序:不变量「paid ⇒ outbox 行存在」,入队失败返回 SettleFailed 不翻转、渠道重投自愈)。HandleCallback(webhook 入口)、SyncPendingAttempts(query 兜底)都汇入这条。SettleResult:ignored/not_found/amount_mismatch/duplicate/processed/failed。paid_at 优先用渠道报的 PaidAt(D4-A3)。
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 起。投递门禁(D4-A1 另一半):发前确认订单已付,防把"未付单"通知出去。
router.SetupV2 挂 POST /api/v2/orders、GET /api/v2/orders/:no、.../retry、.../cancel、POST /api/v2/callback/:method(D3:与旧版统一 /api 前缀;POST /api/v1/orders 被旧契约占用无法共存,P3 渠道迁入后删除整组 /api/v1 收口为一套);main.go 装配 provider 注册表 + gateway + notifier(含投递门禁闭包),AutoMigrate 加 WebhookDelivery。
| 取舍 | 说明 |
|---|---|
| fake provider_ref | 需纳秒唯一化——P1 Attempt 有 uniqueIndex(channel,provider_ref),retry 同 method 会撞键(真渠道天然不同 ref) |
| webhook 无 biz_code | P1 OrderV2 无 biz_code 列,不改已落地 schema;业务方暂用 biz_ref 映射,P3 补 |
| 路由取首个账户 | P2 取"首个 enabled 账户",完整路由策略在 P5 |
| 币种走部署默认 | 多币种在 P3+ |
| 路由 /api/v2 | D3 已拍:v2 挂 /api/v2(统一 /api 前缀);旧 /api/v1 仅为存量当面付部署保留,P3 渠道迁入后整组删除,最终只剩一套 |
| 修订 | 内容与理由 |
|---|---|
| A1 崩溃安全顺序 | Settle 改为先幂等入队 webhook、再幂等翻转订单 + Notifier「订单已付」投递门禁 + SettleFailed。原顺序(先翻转后入队)两步间崩溃 = 已收钱但业务方永不知情且无人重试;新顺序两个方向的窗口都由幂等 + 门禁 + 渠道重投/查单兜底自愈 |
| A2 Query 带上下文 | Query(ctx, providerRef) → Query(ctx, QueryRequest)(尝试完整快照)。crypto 自托管的"查单"是按地址+金额+时间窗扫链核对,裸 ref 会逼 adapter 自建 ref→上下文映射表,重复 pay 已有数据 |
| A3 渠道支付时间 | PaidEvent 增 PaidAt;settle/webhook payload 优先用渠道报的支付时间,否则收到时间。不加则本地 paid_at 全是"回调到达时间",P6 对账与渠道流水对不上产生假差异 |
| 有意不调(记录) | 回调原始报文落表留痕(P3 callback_logs);Notifier 退避/死信上限+告警(P6);多账户回调验签由 P3 装配注入账户注册表(接口不变);crypto 分笔凑单由 adapter 聚合(P3) |
相关:pay v2 设计 · 真相源 docs/superpowers/plans/2026-07-10-pay-v2-p2-pipeline.md