package channel import ( "context" "errors" "fmt" "net/http" "time" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/auth/verifiers" "github.com/wechatpay-apiv3/wechatpay-go/core/downloader" "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" "github.com/wangjia/pay/internal/model" "github.com/wangjia/pay/internal/util" ) // wechatChannel 微信支付 V3 · Native(扫码)实现。 // PC 网站场景微信只提供 Native 扫码(无支付宝那种网页跳转收银台),所以: // - PreCreate → Native 下单,返回 code_url(前端经 /qrcode 渲染成二维码) // - PagePay → 不适用,返回错误引导改用扫码 // - VerifyNotify / Query → 回调解密验签 / 主动查单 type wechatChannel struct { mchID string appID string client *core.Client native *native.NativeApiService handler *notify.Handler } func newWechat(m *model.Merchant) (Channel, error) { if m.MchID == "" || m.WxAppID == "" || m.WxPrivateKey == "" || m.CertSerial == "" || m.APIv3Key == "" { return nil, fmt.Errorf("商户 %s 的微信凭证不完整(mch_id/wx_app_id/cert_serial/wx_private_key/apiv3_key)", m.Code) } mchPrivateKey, err := utils.LoadPrivateKey(m.WxPrivateKey) if err != nil { return nil, fmt.Errorf("加载微信商户私钥失败: %w", err) } // 初始化时自动下载并周期性更新微信支付平台证书(需公网可达微信 API)。 ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() client, err := core.NewClient(ctx, option.WithWechatPayAutoAuthCipher( m.MchID, m.CertSerial, mchPrivateKey, m.APIv3Key, )) if err != nil { return nil, fmt.Errorf("初始化微信支付客户端失败: %w", err) } // 回调处理器:用平台证书验签 + APIv3 密钥解密。 certVisitor := downloader.MgrInstance().GetCertificateVisitor(m.MchID) handler, err := notify.NewRSANotifyHandler(m.APIv3Key, verifiers.NewSHA256WithRSAVerifier(certVisitor)) if err != nil { return nil, fmt.Errorf("初始化微信回调处理器失败: %w", err) } return &wechatChannel{ mchID: m.MchID, appID: m.WxAppID, client: client, native: &native.NativeApiService{Client: client}, handler: handler, }, nil } func (w *wechatChannel) Name() string { return "wechat" } // PagePay 微信无网页跳转收银台(PC 网站走 Native 扫码),改用 PreCreate。 func (w *wechatChannel) PagePay(context.Context, CreateReq) (string, error) { return "", errors.New("微信支付无网页跳转收银台,请用扫码下单(CreateQR / PreCreate)") } // PreCreate Native 下单,返回 code_url 供渲染二维码。 func (w *wechatChannel) PreCreate(ctx context.Context, req CreateReq) (string, error) { cents, err := util.AmountToCents(req.Amount) if err != nil { return "", fmt.Errorf("金额换算失败: %w", err) } resp, _, err := w.native.Prepay(ctx, native.PrepayRequest{ Appid: core.String(w.appID), Mchid: core.String(w.mchID), Description: core.String(req.Subject), OutTradeNo: core.String(req.OutTradeNo), NotifyUrl: core.String(req.NotifyURL), Amount: &native.Amount{ Total: core.Int64(cents), Currency: core.String("CNY"), }, }) if err != nil { return "", fmt.Errorf("微信 Native 下单失败: %w", err) } if resp.CodeUrl == nil || *resp.CodeUrl == "" { return "", errors.New("微信 Native 下单未返回 code_url") } return *resp.CodeUrl, nil } // VerifyNotify 解密验签微信异步回调,解析成统一结果。 func (w *wechatChannel) VerifyNotify(ctx context.Context, r *http.Request) (*NotifyResult, error) { tx := new(payments.Transaction) if _, err := w.handler.ParseNotifyRequest(ctx, r, tx); err != nil { return nil, fmt.Errorf("微信回调验签/解密失败: %w", err) } return txToNotifyResult(tx), nil } // Query 主动查单(out_trade_no 维度)。 func (w *wechatChannel) Query(ctx context.Context, outTradeNo string) (*QueryResult, error) { tx, _, err := w.native.QueryOrderByOutTradeNo(ctx, native.QueryOrderByOutTradeNoRequest{ OutTradeNo: core.String(outTradeNo), Mchid: core.String(w.mchID), }) if err != nil { // 交易不存在等:视为未找到,交由上层决定(与支付宝实现一致)。 return &QueryResult{Found: false}, nil } res := txToNotifyResult(tx) return &QueryResult{ Found: true, OutTradeNo: res.OutTradeNo, TradeNo: res.TradeNo, Amount: res.Amount, Paid: res.Paid, }, nil } // txToNotifyResult 把微信交易对象归一化成本服务的统一结构(金额分→元字符串)。 func txToNotifyResult(tx *payments.Transaction) *NotifyResult { res := &NotifyResult{} if tx.OutTradeNo != nil { res.OutTradeNo = *tx.OutTradeNo } if tx.TransactionId != nil { res.TradeNo = *tx.TransactionId } if tx.Amount != nil && tx.Amount.Total != nil { res.Amount = util.CentsToAmount(*tx.Amount.Total) } if tx.Payer != nil && tx.Payer.Openid != nil { res.BuyerLogonID = *tx.Payer.Openid } // 交易状态:SUCCESS 视为已支付(其余 NOTPAY/CLOSED/REFUND 等均非成功)。 res.Paid = tx.TradeState != nil && *tx.TradeState == "SUCCESS" return res }