Files
pay/internal/provider/provider.go
T

150 lines
4.9 KiB
Go

// 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
}
// 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 用收到时间,对账时两边时间才对得上
}
// 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;不支持则 capabilities=false)。
type RefundingProvider interface {
Provider
Refund(ctx context.Context, providerRef 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
}
// 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
}