package wechat import ( "bytes" "context" "errors" "fmt" "io" "net/http" "sync" "git.lzcwy.cn/zhengchiliu/zt-closet-backend/pkg/setting" "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/jsapi" "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) // payClient 微信支付客户端,基于官方 SDK(自动平台证书轮换 + 请求签名 + 应答/回调验签)。 type payClient struct { appID string mchID string notifyURL string refundNotify string client *core.Client notifyHandler *notify.Handler } var ( defaultPayClient *payClient payClientMu sync.RWMutex ) // InitPayClient 初始化全局微信支付客户端。 // // 使用官方 SDK 的 WithWechatPayAutoAuthCipher:加载商户私钥后自动注册平台证书下载器 // (定期轮换),并具备请求签名、应答自动验签能力;同时构建回调处理器用于回调验签 + 解密。 func InitPayClient(s *setting.WechatPaySettingS) error { if s == nil { return errors.New("wechat pay setting is nil") } if s.AppID == "" || s.MchID == "" || s.SerialNo == "" || s.APIv3Key == "" || s.PrivateKeyPath == "" { return errors.New("wechat pay setting incomplete") } if len(s.APIv3Key) != 32 { return fmt.Errorf("wechat pay APIv3Key must be 32 bytes, got %d", len(s.APIv3Key)) } privateKey, err := utils.LoadPrivateKeyWithPath(s.PrivateKeyPath) if err != nil { return fmt.Errorf("load merchant private key: %w", err) } ctx := context.Background() // 创建客户端:内部注册平台证书自动下载器(轮换)、请求签名器与应答验签器。 client, err := core.NewClient( ctx, option.WithWechatPayAutoAuthCipher(s.MchID, s.SerialNo, privateKey, s.APIv3Key), ) if err != nil { return fmt.Errorf("new wechat pay client: %w", err) } // 回调处理器:使用平台证书访问器进行回调验签,APIv3Key 进行解密。 certVisitor := downloader.MgrInstance().GetCertificateVisitor(s.MchID) notifyHandler, err := notify.NewRSANotifyHandler( s.APIv3Key, verifiers.NewSHA256WithRSAVerifier(certVisitor), ) if err != nil { return fmt.Errorf("new wechat notify handler: %w", err) } c := &payClient{ appID: s.AppID, mchID: s.MchID, notifyURL: s.NotifyURL, refundNotify: s.RefundNotifyURL, client: client, notifyHandler: notifyHandler, } payClientMu.Lock() defaultPayClient = c payClientMu.Unlock() return nil } // getPayClient 获取已初始化的支付客户端。 func getPayClient() (*payClient, error) { payClientMu.RLock() defer payClientMu.RUnlock() if defaultPayClient == nil { return nil, errors.New("wechat pay client not initialized") } return defaultPayClient, nil } // MiniPayParams 小程序调起支付所需参数(对应 dto.WechatPayParams)。 type MiniPayParams struct { TimeStamp string NonceStr string Package string SignType string PaySign string } // JsapiPrepay 调用微信 JSAPI 下单,一步返回小程序调起支付所需的签名参数。 // - openid:支付用户在小程序 AppID 下的 openid // - orderNo:商户订单号(out_trade_no) // - description:商品描述 // - amount:应付金额(分) func JsapiPrepay(ctx context.Context, openid, orderNo, description string, amount int64) (*MiniPayParams, error) { c, err := getPayClient() if err != nil { return nil, err } if openid == "" { return nil, errors.New("empty openid") } svc := jsapi.JsapiApiService{Client: c.client} resp, _, err := svc.PrepayWithRequestPayment(ctx, jsapi.PrepayRequest{ Appid: core.String(c.appID), Mchid: core.String(c.mchID), Description: core.String(description), OutTradeNo: core.String(orderNo), NotifyUrl: core.String(c.notifyURL), Amount: &jsapi.Amount{ Total: core.Int64(amount), Currency: core.String("CNY"), }, Payer: &jsapi.Payer{ Openid: core.String(openid), }, }) if err != nil { return nil, fmt.Errorf("jsapi prepay: %w", err) } return &MiniPayParams{ TimeStamp: strDeref(resp.TimeStamp), NonceStr: strDeref(resp.NonceStr), Package: strDeref(resp.Package), SignType: strDeref(resp.SignType), PaySign: strDeref(resp.PaySign), }, nil } // CloseOrder 关闭微信侧待支付订单,防止本地取消后用户仍完成支付。 // - orderNo:商户订单号(out_trade_no) func CloseOrder(ctx context.Context, orderNo string) error { c, err := getPayClient() if err != nil { return err } if orderNo == "" { return errors.New("empty order no") } svc := jsapi.JsapiApiService{Client: c.client} if _, err := svc.CloseOrder(ctx, jsapi.CloseOrderRequest{ OutTradeNo: core.String(orderNo), Mchid: core.String(c.mchID), }); err != nil { return fmt.Errorf("jsapi close order: %w", err) } return nil } // RefundResult 退款受理结果。 type RefundResult struct { RefundID string // 微信退款单号 Status string // SUCCESS/CLOSED/PROCESSING/ABNORMAL } // Refund 申请退款。 // - orderNo:原商户订单号 // - refundNo:商户退款单号 // - total:原订单总金额(分) // - refund:本次退款金额(分) func Refund(ctx context.Context, orderNo, refundNo string, total, refund int64, reason string) (*RefundResult, error) { c, err := getPayClient() if err != nil { return nil, err } req := refunddomestic.CreateRequest{ OutTradeNo: core.String(orderNo), OutRefundNo: core.String(refundNo), Amount: &refunddomestic.AmountReq{ Refund: core.Int64(refund), Total: core.Int64(total), Currency: core.String("CNY"), }, } if c.refundNotify != "" { req.NotifyUrl = core.String(c.refundNotify) } if reason != "" { req.Reason = core.String(reason) } svc := refunddomestic.RefundsApiService{Client: c.client} resp, _, err := svc.Create(ctx, req) if err != nil { return nil, fmt.Errorf("refund create: %w", err) } res := &RefundResult{RefundID: strDeref(resp.RefundId)} if resp.Status != nil { res.Status = string(*resp.Status) } return res, nil } // ---- 回调验签 + 解密 ---- // PayNotifyData 支付成功回调解密后的关键字段。 type PayNotifyData struct { Raw string // 回调原始报文(验签前捕获,用于审计持久化) OutTradeNo string TransactionID string TradeState string // SUCCESS 表示支付成功 Amount struct { Total int64 PayerTotal int64 Currency string } SuccessTime string } // RefundNotifyData 退款回调解密后的关键字段。 type RefundNotifyData struct { Raw string // 回调原始报文(验签前捕获,用于审计持久化) OutTradeNo string OutRefundNo string RefundID string RefundStatus string // SUCCESS/CLOSED/ABNORMAL SuccessTime string Amount struct { Total int64 Refund int64 } } // refundNotifyContent 退款回调解密后的原始结构(退款通知使用 refund_status 字段)。 type refundNotifyContent struct { OutTradeNo string `json:"out_trade_no"` OutRefundNo string `json:"out_refund_no"` RefundID string `json:"refund_id"` TransactionID string `json:"transaction_id"` RefundStatus string `json:"refund_status"` SuccessTime string `json:"success_time"` Amount struct { Total int64 `json:"total"` Refund int64 `json:"refund"` PayerTotal int64 `json:"payer_total"` PayerRefund int64 `json:"payer_refund"` } `json:"amount"` } // VerifyAndDecryptPayNotify 校验并解密支付回调请求,返回订单数据。 // // 由官方 SDK 完成:校验签名头 → 平台证书验签 → APIv3Key 解密 → 反序列化,杜绝伪造/篡改回调。 func VerifyAndDecryptPayNotify(ctx context.Context, request *http.Request) (*PayNotifyData, error) { c, err := getPayClient() if err != nil { return nil, err } // 验签前捕获原始报文用于审计持久化;ParseNotifyRequest 会消费 request.Body。 raw, err := io.ReadAll(request.Body) if err != nil { return nil, fmt.Errorf("read pay notify body: %w", err) } request.Body = io.NopCloser(bytes.NewReader(raw)) tx := new(payments.Transaction) if _, err := c.notifyHandler.ParseNotifyRequest(ctx, request, tx); err != nil { return nil, fmt.Errorf("parse pay notify: %w", err) } d := &PayNotifyData{ Raw: string(raw), OutTradeNo: strDeref(tx.OutTradeNo), TransactionID: strDeref(tx.TransactionId), TradeState: strDeref(tx.TradeState), SuccessTime: strDeref(tx.SuccessTime), } if tx.Amount != nil { d.Amount.Total = int64Deref(tx.Amount.Total) d.Amount.PayerTotal = int64Deref(tx.Amount.PayerTotal) d.Amount.Currency = strDeref(tx.Amount.Currency) } return d, nil } // VerifyAndDecryptRefundNotify 校验并解密退款回调请求。 func VerifyAndDecryptRefundNotify(ctx context.Context, request *http.Request) (*RefundNotifyData, error) { c, err := getPayClient() if err != nil { return nil, err } // 验签前捕获原始报文用于审计持久化;ParseNotifyRequest 会消费 request.Body。 raw, err := io.ReadAll(request.Body) if err != nil { return nil, fmt.Errorf("read refund notify body: %w", err) } request.Body = io.NopCloser(bytes.NewReader(raw)) content := new(refundNotifyContent) if _, err := c.notifyHandler.ParseNotifyRequest(ctx, request, content); err != nil { return nil, fmt.Errorf("parse refund notify: %w", err) } d := &RefundNotifyData{ Raw: string(raw), OutTradeNo: content.OutTradeNo, OutRefundNo: content.OutRefundNo, RefundID: content.RefundID, RefundStatus: content.RefundStatus, SuccessTime: content.SuccessTime, } d.Amount.Total = content.Amount.Total d.Amount.Refund = content.Amount.Refund return d, nil } // ---- 内部辅助 ---- func strDeref(s *string) string { if s == nil { return "" } return *s } func int64Deref(i *int64) int64 { if i == nil { return 0 } return *i }