// Package provider abstracts a payment channel behind a uniform interface: // create → {render_type, payload}, verify_callback / query → normalized PaidEvent. // Channel-specific quirks (RSA/cert/on-chain confirmations/receipt checks) are // sealed inside each Provider's VerifyCallback; the pipeline stays channel-neutral. package provider import ( "context" "errors" "fmt" "sort" "time" "github.com/wangjia/pay/config" ) // RenderType — 客户端只认的 6 种付款意图形态(设计 §4.2),不含任何 UI。 type RenderType string const ( RenderRedirect RenderType = "redirect" RenderQR RenderType = "qr" RenderCryptoAddress RenderType = "crypto_address" RenderNativePay RenderType = "native_pay" RenderSDKHandoff RenderType = "sdk_handoff" RenderIAPReceipt RenderType = "iap_receipt" ) // PaidStatus — verify_callback / query 归一化后的支付状态。 type PaidStatus string const ( PaidPending PaidStatus = "pending" PaidSucceeded PaidStatus = "succeeded" PaidFailed PaidStatus = "failed" ) // Capabilities — 渠道能力自述(设计 §4.1 capabilities())。 type Capabilities struct { RenderTypes []RenderType SupportsRefund bool SupportsRecurring bool RecurringKind string // token_offsession/gateway_scheduled/store_managed/none SettleCurrencies []string Regions []string } // CreateRequest — Provider.Create 入参:pay 已定金额/币种/账户,Provider 拿去渠道建单。 type CreateRequest struct { OutTradeNo string Subject string AmountMinor int64 Currency string Account config.AccountConfig // 选中的收款账户(含 CredentialEnvPrefix) ReturnURL string Metadata map[string]string } // Session — Provider.Create 产出:渠道单号 + 付款意图数据(render_type + payload)。 type Session struct { ProviderRef string RenderType RenderType Payload map[string]any ExpiresAt *time.Time } // CallbackInput — 渠道原始回调(HTTP body/头/query),由各 Provider 自行解析验签。 type CallbackInput struct { Raw []byte Headers map[string]string Query map[string]string } // EventKind — PaidEvent 的语义判别器(P8,设计 §5)。零值 EventPayment 是既有一次性/首期 // 支付语义(P3 三渠道全落此),新增枚举全为 additive,不改变任何既有调用点的行为。 type EventKind string const ( EventPayment EventKind = "" // 默认:一次性/首期支付(向后兼容) EventSubscriptionRenewal EventKind = "subscription_renewal" EventSubscriptionPastDue EventKind = "subscription_past_due" EventSubscriptionCanceled EventKind = "subscription_canceled" EventChargeback EventKind = "chargeback" ) // RecurringKindGatewayScheduled — Capabilities.RecurringKind 取值之一:续费由渠道网关自身 // 调度驱动(如 Stripe invoice.paid),pay 不主动发起 Charge(区别于 token_offsession)。 const RecurringKindGatewayScheduled = "gateway_scheduled" // PaidEvent — verify_callback / query 的统一产出(设计 §4.1 → {order_ref,status,paid_amount})。 type PaidEvent struct { ProviderRef string Status PaidStatus PaidAmountMinor int64 PaidCurrency string Raw string PaidAt *time.Time // 渠道报的支付时间;nil 则 settle 用收到时间,对账时两边时间才对得上 // 以下均可选(P8,零值=旧行为): Kind EventKind // 事件语义判别器,零值=既有一次性支付 SubscriptionRef string // 渠道订阅号(checkout.completed 诞生 / invoice / deleted 反查) InvoiceRef string // 续费期次唯一号(renewal attempt 的 provider_ref) DisputeRef string // 拒付号 ProviderPaymentRef string // 拒付关联的 PaymentIntent id OutTradeNo string // 拒付解析出的原单号(可空) } // QueryRequest — Provider.Query 入参:尝试的完整上下文快照,不是裸 provider_ref。 // crypto 自托管的"查单"= 按地址+期望金额+时间窗扫链核对;裸 ref 会逼渠道 adapter // 自建 ref→(地址/金额/窗口) 映射表,重复 pay 已持有的数据。管线侧(SyncPendingAttempts) // 本就拿着整个 attempt,填这个结构零成本。 type QueryRequest struct { ProviderRef string OutTradeNo string AccountID string AmountMinor int64 Currency string CreatedAt time.Time ExpiresAt *time.Time } var ( ErrUnknownMethod = errors.New("provider: unknown method") ErrNotSupported = errors.New("provider: capability not supported") ) // Provider — 每个支付渠道实现的统一接口(设计 §4.1 PaymentProvider)。 type Provider interface { Method() string Capabilities() Capabilities Create(ctx context.Context, req CreateRequest) (*Session, error) VerifyCallback(ctx context.Context, in CallbackInput) (*PaidEvent, error) Query(ctx context.Context, req QueryRequest) (*PaidEvent, error) } // RefundingProvider — 可选:支持渠道退款的 Provider 额外实现(P4)。refundID 为 pay 侧 // 退款单号,作渠道幂等键(alipay out_request_no / stripe Idempotency-Key)——同单多次 // 部分退款靠它去重,重试不重复退。不支持退款的渠道不实现本接口(capabilities=false)。 type RefundingProvider interface { Provider Refund(ctx context.Context, providerRef, refundID string, amountMinor int64, reason string) (refundRef string, status PaidStatus, err error) } // RecurringProvider — 可选:支持自动续订(P8,设计 §5.1 4 类 kind)。 type RecurringProvider interface { Provider CreateAgreement(ctx context.Context, req CreateRequest) (agreementRef string, err error) Charge(ctx context.Context, agreementRef string, amountMinor int64, currency string) (*PaidEvent, error) CancelAgreement(ctx context.Context, agreementRef string) error } // SubscriptionProvider — 可选:渠道网关自身调度续费(RecurringKindGatewayScheduled,如 // Stripe Checkout mode=subscription)的 Provider 额外实现。与 RecurringProvider(pay 主动 // Charge 的 token_offsession 类)不同:订阅号在用户完成收银台支付后才诞生,续费由渠道 // webhook(invoice.paid)驱动,pay 只负责建单与取消。 type SubscriptionProvider interface { Provider CreateSubscriptionCheckout(ctx context.Context, req CreateRequest) (*Session, error) CancelSubscription(ctx context.Context, providerSubRef string) error } // Registry — 方法名 → Provider(设计 §2 Provider adapter 注册表)。启动期注册,运行期只读。 type Registry struct{ providers map[string]Provider } func NewRegistry() *Registry { return &Registry{providers: map[string]Provider{}} } func (r *Registry) Register(p Provider) { r.providers[p.Method()] = p } func (r *Registry) Get(method string) (Provider, error) { p, ok := r.providers[method] if !ok { return nil, fmt.Errorf("%w: %s", ErrUnknownMethod, method) } return p, nil } // Methods 返回已注册方法名(字典序,供 GET /methods 下发已启用渠道)。 func (r *Registry) Methods() []string { out := make([]string, 0, len(r.providers)) for m := range r.providers { out = append(out, m) } sort.Strings(out) return out }