228 lines
6.6 KiB
Go
228 lines
6.6 KiB
Go
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
|
||
}
|