228 lines
6.6 KiB
Go
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
}