// Package payment 是充值渠道适配层(设计见 PAYMENT_DESIGN.md §3/§5)。 // P5.1 的兑换码不走这里(无「待支付」态,核销即入账);本包面向真渠道: // 下单出支付凭据 → 回调/查单确认 → 上层 MarkOrderPaid 幂等入账。 package payment import ( "context" "crypto/rsa" "errors" "fmt" "net/http" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/notify" "github.com/wechatpay-apiv3/wechatpay-go/core/option" "github.com/wechatpay-apiv3/wechatpay-go/services/payments" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/native" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) // Wechat 微信支付 Native(扫码)适配器。配置来源见 Config(DB 优先、env 兜底,由 Manager 装配)。 // 本地开发收不到公网回调没关系:前端轮询的 GET /billing/orders/:id 会主动查单确认, // 回调只是生产环境更快的到账通道,两条路都汇入同一个幂等入账闸。 type Wechat struct { mchID string appID string notifyURL string apiv3Key string client *core.Client svc native.NativeApiService pubKey *rsa.PublicKey // 微信支付公钥(验签回调用;构造时必填) pubKeyID string } // 编译期断言:Wechat 实现 Channel 接口。 var _ Channel = (*Wechat)(nil) // Name 渠道名(用于按订单渠道路由查单/回调)。 func (w *Wechat) Name() string { return ChannelWechat } // New 按完整配置装配微信渠道(商户私钥/微信支付公钥都从磁盘路径加载;调用方保证字段齐全)。 // 验签体系钉死「微信支付公钥」模式(WithWechatPayPublicKeyAuthCipher):本项目商户 // 2025-09 开户,只有公钥体系;老商户的平台证书模式(AutoAuthCipher)不支持。 func New(ctx context.Context, c Config) (*Wechat, error) { priv, err := utils.LoadPrivateKeyWithPath(c.PrivateKeyPath) if err != nil { return nil, fmt.Errorf("商户私钥加载失败(%s): %w", c.PrivateKeyPath, err) } pub, err := utils.LoadPublicKeyWithPath(c.PublicKeyPath) if err != nil { return nil, fmt.Errorf("微信支付公钥加载失败(%s): %w", c.PublicKeyPath, err) } client, err := core.NewClient(ctx, option.WithWechatPayPublicKeyAuthCipher(c.MchID, c.CertSerial, priv, c.PublicKeyID, pub)) if err != nil { return nil, fmt.Errorf("客户端初始化失败: %w", err) } return &Wechat{ mchID: c.MchID, appID: c.AppID, notifyURL: c.NotifyURL, apiv3Key: c.APIv3Key, client: client, svc: native.NativeApiService{Client: client}, pubKey: pub, pubKeyID: c.PublicKeyID, }, nil } // CreatePay Native 下单:返回 code_url(前端渲染成二维码)。金额取订单锁定值。 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), Description: core.String(description), OutTradeNo: core.String(orderID), NotifyUrl: core.String(w.notifyURL), Amount: &native.Amount{Total: core.Int64(amountFen), Currency: core.String("CNY")}, }) if err != nil { return PayIntent{}, err } if resp.CodeUrl == nil || *resp.CodeUrl == "" { return PayIntent{}, errors.New("微信未返回 code_url") } return PayIntent{CodeURL: *resp.CodeUrl}, nil } func fromTransaction(t *payments.Transaction) PayResult { r := PayResult{} if t.OutTradeNo != nil { r.OrderID = *t.OutTradeNo } if t.TransactionId != nil { r.ChannelTxn = *t.TransactionId } if t.Amount != nil && t.Amount.PayerTotal != nil { r.AmountFen = *t.Amount.PayerTotal } else if t.Amount != nil && t.Amount.Total != nil { r.AmountFen = *t.Amount.Total } if t.TradeState != nil { switch *t.TradeState { case "SUCCESS": r.Paid = true case "CLOSED", "REVOKED", "PAYERROR": r.Closed = true } } return r } // QueryOrder 主动查单(本地开发确认到账、生产掉单补偿共用)。 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 PayResult{}, err } return fromTransaction(t), nil } // VerifyCallback 验签 + 解密支付回调(微信支付公钥验签 + APIv3 密钥 AES-GCM 解密资源)。 // 验签失败一律拒绝——回调路由是公开的,签名是唯一的门。 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 PayResult{}, fmt.Errorf("回调处理器初始化失败: %w", err) } txn := new(payments.Transaction) if _, err := h.ParseNotifyRequest(req.Context(), req, txn); err != nil { return PayResult{}, err } return fromTransaction(txn), nil }