package pay import ( "bindbox-game/internal/service/sysconfig" "context" "crypto/rsa" "crypto/x509" "encoding/base64" "encoding/pem" "errors" "sync" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/option" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/jsapi" refundsvc "github.com/wechatpay-apiv3/wechatpay-go/services/refunddomestic" ) type WechatPayClient struct { client *core.Client } // 单例模式:避免每次请求都重新初始化客户端 var ( clientInstance *WechatPayClient clientOnce sync.Once clientErr error ) // LoadPrivateKeyFromBase64 从 Base64 编码的私钥内容创建 RSA 私钥 func LoadPrivateKeyFromBase64(base64Key string) (*rsa.PrivateKey, error) { // 解码 Base64 keyBytes, err := base64.StdEncoding.DecodeString(base64Key) if err != nil { return nil, errors.New("failed to decode base64 private key: " + err.Error()) } // 解析 PEM block, _ := pem.Decode(keyBytes) if block == nil { return nil, errors.New("invalid private key PEM format") } // 尝试 PKCS8 格式 key, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { // 尝试 PKCS1 格式 rsaKey, err2 := x509.ParsePKCS1PrivateKey(block.Bytes) if err2 != nil { return nil, errors.New("failed to parse private key: " + err.Error()) } return rsaKey, nil } rsaKey, ok := key.(*rsa.PrivateKey) if !ok { return nil, errors.New("private key is not RSA type") } return rsaKey, nil } // LoadPublicKeyFromBase64 从 Base64 编码的公钥内容创建 RSA 公钥 func LoadPublicKeyFromBase64(base64Key string) (*rsa.PublicKey, error) { keyBytes, err := base64.StdEncoding.DecodeString(base64Key) if err != nil { return nil, errors.New("failed to decode base64 public key: " + err.Error()) } block, _ := pem.Decode(keyBytes) if block == nil { return nil, errors.New("invalid public key PEM format") } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { return nil, errors.New("failed to parse public key: " + err.Error()) } rsaPub, ok := pub.(*rsa.PublicKey) if !ok { return nil, errors.New("public key is not RSA type") } return rsaPub, nil } // NewWechatPayClient 获取微信支付客户端(单例模式) // 首次调用会初始化客户端,后续调用直接返回缓存的实例 func NewWechatPayClient(ctx context.Context) (*WechatPayClient, error) { clientOnce.Do(func() { clientInstance, clientErr = initWechatPayClient(ctx) }) if clientErr != nil { return nil, clientErr } return clientInstance, nil } // initWechatPayClient 初始化微信支付客户端(内部实现) // 优先使用动态配置中的 Base64 私钥内容,fallback 到静态配置的文件路径 func initWechatPayClient(ctx context.Context) (*WechatPayClient, error) { // 必须从动态配置获取 var dynamicCfg *sysconfig.WechatPayConfig if dc := sysconfig.GetDynamicConfig(); dc != nil { cfg := dc.GetWechatPay(ctx) dynamicCfg = &cfg } if dynamicCfg == nil { return nil, errors.New("wechat pay dynamic config missing") } mchID := dynamicCfg.MchID serialNo := dynamicCfg.SerialNo apiV3Key := dynamicCfg.ApiV3Key if apiV3Key == "" { return nil, errors.New("wechat pay config incomplete: api_v3_key missing") } if mchID == "" || serialNo == "" { return nil, errors.New("wechat pay config incomplete: mchid or serial_no missing") } // 加载私钥:动态配置 Base64 内容 var mchPrivateKey *rsa.PrivateKey var err error if dynamicCfg.PrivateKey != "" { mchPrivateKey, err = LoadPrivateKeyFromBase64(dynamicCfg.PrivateKey) if err != nil { return nil, errors.New("read private key from dynamic config err:" + err.Error()) } } else { return nil, errors.New("wechat pay private key not configured") } // 构建客户端选项 var opts []core.ClientOption // 检查是否有公钥配置(新版验签方式) publicKeyID := dynamicCfg.PublicKeyID if publicKeyID != "" { // 使用公钥验签模式 var pubKey *rsa.PublicKey if dynamicCfg.PublicKey != "" { pubKey, err = LoadPublicKeyFromBase64(dynamicCfg.PublicKey) if err != nil { return nil, errors.New("read public key from dynamic config err:" + err.Error()) } } else { return nil, errors.New("wechat pay public key not configured") } opts = []core.ClientOption{option.WithWechatPayPublicKeyAuthCipher(mchID, serialNo, mchPrivateKey, publicKeyID, pubKey)} } else { // 使用自动证书模式 opts = []core.ClientOption{option.WithWechatPayAutoAuthCipher(mchID, serialNo, mchPrivateKey, apiV3Key)} } client, err := core.NewClient(ctx, opts...) if err != nil { return nil, err } return &WechatPayClient{client: client}, nil } // JSAPIPrepay 直连商户JSAPI预下单,返回prepay_id // 入参:appid、mchid、描述、商户订单号、总金额(分)、openid、回调URL // 返回:prepay_id 或错误 func (c *WechatPayClient) JSAPIPrepay(ctx context.Context, appid, mchid, description, outTradeNo string, total int64, openid, notifyURL string) (string, error) { svc := jsapi.JsapiApiService{Client: c.client} resp, _, err := svc.Prepay(ctx, jsapi.PrepayRequest{ Appid: core.String(appid), Mchid: core.String(mchid), Description: core.String(description), OutTradeNo: core.String(outTradeNo), NotifyUrl: core.String(notifyURL), Amount: &jsapi.Amount{ Total: core.Int64(total), }, Payer: &jsapi.Payer{ Openid: core.String(openid), }, }) if err != nil { return "", err } if resp == nil || resp.PrepayId == nil { return "", errors.New("missing prepay_id in response") } return *resp.PrepayId, nil } // RefundOrder 直连商户退款 // 入参:outTradeNo(商户订单号)、refundNo(商户退款单号)、amountRefund(退款金额分)、total(原订单金额分)、reason(退款原因,可空) // 返回:微信退款单ID与状态,或错误 func (c *WechatPayClient) RefundOrder(ctx context.Context, outTradeNo, refundNo string, amountRefund, total int64, reason string) (string, string, error) { svc := refundsvc.RefundsApiService{Client: c.client} req := refundsvc.CreateRequest{ OutTradeNo: core.String(outTradeNo), OutRefundNo: core.String(refundNo), Amount: &refundsvc.AmountReq{ Refund: core.Int64(amountRefund), Total: core.Int64(total), Currency: core.String("CNY"), }, } if reason != "" { req.Reason = core.String(reason) } resp, _, err := svc.Create(ctx, req) if err != nil { return "", "", err } var refundID, status string if resp != nil { if resp.RefundId != nil { refundID = *resp.RefundId } if resp.Status != nil { status = string(*resp.Status) } } if refundID == "" { return "", status, errors.New("missing refund_id in response") } return refundID, status, nil }