refactor(payment): 渠道抽象成 Channel 接口 —— 接新渠道=加 adapter 不动骨架
PAYMENT_DESIGN §3 承诺的 internal/payment/channel.go 适配器接口此前不存在,微信硬编码在 manager/handler 里。补齐抽象: - channel.go:Channel 接口(Name/CreatePay/QueryOrder/VerifyCallback)+ 统一 PayIntent/PayResult + 渠道名常量。入参用基本类型不吃 *store.PaymentOrder,payment 包不反依赖 store。 - Wechat 实现 Channel(编译期断言 var _ Channel);QueryResult 归一为 PayResult;CreatePay 返回 PayIntent。 - Manager 从「持一个 *Wechat」改为渠道注册表:Get(name)/Available()/Status(name)/ReloadWechat; 按渠道名持有已装配实例,热重载不变。 - 回调路由收敛 /billing/callback/wechat → /billing/callback/:channel 按名路由(旧 notify URL 仍匹配); 查单/掉单补偿据 order.Channel 路由,不再写死微信。下单支持可选 channel(缺省 wechat)。 - 支付宝/Stripe 现在真·只差一个 adapter+注册。唯一未泛化:回调 ack 应答格式(现微信态,注释标明)。 - payment 包首个测试:Manager 注册表 4 用例(空/注册摘除/空配置/配置不全)。build/vet/test/lint 全绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -33,6 +33,12 @@ type Wechat struct {
|
||||
pubKeyID string
|
||||
}
|
||||
|
||||
// 编译期断言:Wechat 实现 Channel 接口。
|
||||
var _ Channel = (*Wechat)(nil)
|
||||
|
||||
// Name 渠道名(用于按订单渠道路由查单/回调)。
|
||||
func (w *Wechat) Name() string { return ChannelWechat }
|
||||
|
||||
// New 按完整配置装配微信渠道(商户私钥/微信支付公钥都从磁盘路径加载;调用方保证字段齐全)。
|
||||
// 验签体系钉死「微信支付公钥」模式(WithWechatPayPublicKeyAuthCipher):本项目商户
|
||||
// 2025-09 开户,只有公钥体系;老商户的平台证书模式(AutoAuthCipher)不支持。
|
||||
@@ -58,7 +64,7 @@ func New(ctx context.Context, c Config) (*Wechat, error) {
|
||||
}
|
||||
|
||||
// CreatePay Native 下单:返回 code_url(前端渲染成二维码)。金额取订单锁定值。
|
||||
func (w *Wechat) CreatePay(ctx context.Context, orderID, description string, amountFen int64) (string, error) {
|
||||
func (w *Wechat) CreatePay(ctx context.Context, orderID, description string, amountFen int64) (PayIntent, error) {
|
||||
resp, _, err := w.svc.Prepay(ctx, native.PrepayRequest{
|
||||
Appid: core.String(w.appID),
|
||||
Mchid: core.String(w.mchID),
|
||||
@@ -68,25 +74,16 @@ func (w *Wechat) CreatePay(ctx context.Context, orderID, description string, amo
|
||||
Amount: &native.Amount{Total: core.Int64(amountFen), Currency: core.String("CNY")},
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
return PayIntent{}, err
|
||||
}
|
||||
if resp.CodeUrl == nil || *resp.CodeUrl == "" {
|
||||
return "", errors.New("微信未返回 code_url")
|
||||
return PayIntent{}, errors.New("微信未返回 code_url")
|
||||
}
|
||||
return *resp.CodeUrl, nil
|
||||
return PayIntent{CodeURL: *resp.CodeUrl}, nil
|
||||
}
|
||||
|
||||
// QueryResult 查单/回调解析后的统一结果。
|
||||
type QueryResult struct {
|
||||
OrderID string // out_trade_no
|
||||
ChannelTxn string // transaction_id
|
||||
Paid bool // TradeState == SUCCESS
|
||||
Closed bool // CLOSED/REVOKED/PAYERROR 等终态失败
|
||||
AmountFen int64 // 用户实付(分);回调/查单都带,供金额核对
|
||||
}
|
||||
|
||||
func fromTransaction(t *payments.Transaction) QueryResult {
|
||||
r := QueryResult{}
|
||||
func fromTransaction(t *payments.Transaction) PayResult {
|
||||
r := PayResult{}
|
||||
if t.OutTradeNo != nil {
|
||||
r.OrderID = *t.OutTradeNo
|
||||
}
|
||||
@@ -110,28 +107,28 @@ func fromTransaction(t *payments.Transaction) QueryResult {
|
||||
}
|
||||
|
||||
// QueryOrder 主动查单(本地开发确认到账、生产掉单补偿共用)。
|
||||
func (w *Wechat) QueryOrder(ctx context.Context, orderID string) (QueryResult, error) {
|
||||
func (w *Wechat) QueryOrder(ctx context.Context, orderID string) (PayResult, error) {
|
||||
t, _, err := w.svc.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{
|
||||
OutTradeNo: core.String(orderID),
|
||||
Mchid: core.String(w.mchID),
|
||||
})
|
||||
if err != nil {
|
||||
return QueryResult{}, err
|
||||
return PayResult{}, err
|
||||
}
|
||||
return fromTransaction(t), nil
|
||||
}
|
||||
|
||||
// VerifyCallback 验签 + 解密支付回调(微信支付公钥验签 + APIv3 密钥 AES-GCM 解密资源)。
|
||||
// 验签失败一律拒绝——回调路由是公开的,签名是唯一的门。
|
||||
func (w *Wechat) VerifyCallback(req *http.Request) (QueryResult, error) {
|
||||
func (w *Wechat) VerifyCallback(req *http.Request) (PayResult, error) {
|
||||
h, err := notify.NewRSANotifyHandler(w.apiv3Key,
|
||||
verifiers.NewSHA256WithRSAPubkeyVerifier(w.pubKeyID, *w.pubKey))
|
||||
if err != nil {
|
||||
return QueryResult{}, fmt.Errorf("回调处理器初始化失败: %w", err)
|
||||
return PayResult{}, fmt.Errorf("回调处理器初始化失败: %w", err)
|
||||
}
|
||||
txn := new(payments.Transaction)
|
||||
if _, err := h.ParseNotifyRequest(req.Context(), req, txn); err != nil {
|
||||
return QueryResult{}, err
|
||||
return PayResult{}, err
|
||||
}
|
||||
return fromTransaction(txn), nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user