157 lines
4.4 KiB
Go
157 lines
4.4 KiB
Go
package pay
|
||
|
||
import (
|
||
"context"
|
||
"crypto"
|
||
crand "crypto/rand"
|
||
"crypto/rsa"
|
||
"crypto/sha256"
|
||
"crypto/x509"
|
||
"encoding/base64"
|
||
"encoding/pem"
|
||
"errors"
|
||
"fmt"
|
||
mrand "math/rand"
|
||
"os"
|
||
"sync"
|
||
"time"
|
||
|
||
"bindbox-game/configs"
|
||
"bindbox-game/internal/service/sysconfig"
|
||
)
|
||
|
||
// 私钥缓存 - 避免每次请求都重新加载
|
||
var (
|
||
cachedRSAKey *rsa.PrivateKey
|
||
rsaKeyOnce sync.Once
|
||
rsaKeyLoadErr error
|
||
rsaKeyLoadFrom string // "dynamic" 或 "file"
|
||
)
|
||
|
||
// getCachedRSAKeyForSign 获取缓存的RSA私钥用于签名
|
||
// 优先使用动态配置中的 Base64 私钥内容,fallback 到静态文件路径
|
||
func getCachedRSAKeyForSign(ctx context.Context) (*rsa.PrivateKey, error) {
|
||
rsaKeyOnce.Do(func() {
|
||
staticCfg := configs.Get()
|
||
|
||
// 尝试从动态配置获取
|
||
var dynamicCfg *sysconfig.WechatPayConfig
|
||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
||
cfg := dc.GetWechatPay(ctx)
|
||
dynamicCfg = &cfg
|
||
}
|
||
|
||
// 优先动态配置的 Base64 内容
|
||
if dynamicCfg != nil && dynamicCfg.PrivateKey != "" {
|
||
cachedRSAKey, rsaKeyLoadErr = LoadPrivateKeyFromBase64(dynamicCfg.PrivateKey)
|
||
if rsaKeyLoadErr == nil {
|
||
rsaKeyLoadFrom = "dynamic"
|
||
return
|
||
}
|
||
}
|
||
|
||
// fallback 到静态文件路径
|
||
if staticCfg.WechatPay.PrivateKeyPath != "" {
|
||
cachedRSAKey, rsaKeyLoadErr = loadRSAPrivateKeyFromFile(staticCfg.WechatPay.PrivateKeyPath)
|
||
if rsaKeyLoadErr == nil {
|
||
rsaKeyLoadFrom = "file"
|
||
}
|
||
} else if rsaKeyLoadErr == nil {
|
||
rsaKeyLoadErr = errors.New("wechat pay private key not configured")
|
||
}
|
||
})
|
||
return cachedRSAKey, rsaKeyLoadErr
|
||
}
|
||
|
||
// loadRSAPrivateKeyFromFile 从磁盘加载私钥(内部函数)
|
||
func loadRSAPrivateKeyFromFile(keyPath string) (*rsa.PrivateKey, error) {
|
||
b, err := os.ReadFile(keyPath)
|
||
if err != nil {
|
||
return nil, err
|
||
}
|
||
block, _ := pem.Decode(b)
|
||
if block == nil {
|
||
return nil, errors.New("invalid merchant private key pem")
|
||
}
|
||
key, err := x509.ParsePKCS8PrivateKey(block.Bytes)
|
||
if err != nil {
|
||
// 兼容PKCS1
|
||
k1, e1 := x509.ParsePKCS1PrivateKey(block.Bytes)
|
||
if e1 != nil {
|
||
return nil, err
|
||
}
|
||
key = k1
|
||
}
|
||
rsaKey, ok := key.(*rsa.PrivateKey)
|
||
if !ok {
|
||
return nil, errors.New("merchant private key is not RSA")
|
||
}
|
||
return rsaKey, nil
|
||
}
|
||
|
||
// BuildJSAPIParams 为小程序支付构造客户端参数
|
||
// 入参:ctx(上下文)、appid(微信小程序AppID)、prepayID(统一下单返回的prepay_id)
|
||
// 返回:timeStamp、nonceStr、package(格式为"prepay_id=***")、signType(固定"RSA")、paySign(RSA-SHA256签名)
|
||
// 错误:当私钥读取或签名失败时返回错误
|
||
func BuildJSAPIParams(ctx context.Context, appid string, prepayID string) (timeStamp string, nonceStr string, pkg string, signType string, paySign string, err error) {
|
||
// 使用缓存的私钥,优先动态配置
|
||
rsaKey, err := getCachedRSAKeyForSign(ctx)
|
||
if err != nil {
|
||
return "", "", "", "", "", err
|
||
}
|
||
|
||
ts := fmt.Sprintf("%d", time.Now().Unix())
|
||
// 随机串32位
|
||
const letters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||
rb := make([]byte, 32)
|
||
for i := range rb {
|
||
rb[i] = letters[mrand.Intn(len(letters))]
|
||
}
|
||
nonce := string(rb)
|
||
pkg = "prepay_id=" + prepayID
|
||
signType = "RSA"
|
||
|
||
message := fmt.Sprintf("%s\n%s\n%s\n%s\n", appid, ts, nonce, pkg)
|
||
h := sha256.Sum256([]byte(message))
|
||
sig, err := rsa.SignPKCS1v15(crand.Reader, rsaKey, crypto.SHA256, h[:])
|
||
if err != nil {
|
||
return "", "", "", "", "", err
|
||
}
|
||
paySign = base64.StdEncoding.EncodeToString(sig)
|
||
return ts, nonce, pkg, signType, paySign, nil
|
||
}
|
||
|
||
// ValidateConfig 校验微信支付必要配置
|
||
// 入参:ctx(上下文)
|
||
// 返回:true表示配置齐全;false表示缺失,并附带错误信息
|
||
func ValidateConfig(ctx context.Context) (bool, error) {
|
||
// 检查动态配置
|
||
var dynamicCfg *sysconfig.WechatPayConfig
|
||
var wxCfg *sysconfig.WechatConfig
|
||
|
||
if dc := sysconfig.GetDynamicConfig(); dc != nil {
|
||
pCfg := dc.GetWechatPay(ctx)
|
||
dynamicCfg = &pCfg
|
||
wCfg := dc.GetWechat(ctx)
|
||
wxCfg = &wCfg
|
||
}
|
||
|
||
if wxCfg == nil || wxCfg.AppID == "" {
|
||
return false, errors.New("wechat app_id missing")
|
||
}
|
||
|
||
if dynamicCfg == nil {
|
||
return false, errors.New("wechat pay config incomplete")
|
||
}
|
||
|
||
mchID := dynamicCfg.MchID
|
||
serialNo := dynamicCfg.SerialNo
|
||
apiV3Key := dynamicCfg.ApiV3Key
|
||
hasPrivateKey := dynamicCfg.PrivateKey != ""
|
||
|
||
if mchID == "" || serialNo == "" || !hasPrivateKey || apiV3Key == "" {
|
||
return false, errors.New("wechat pay config incomplete")
|
||
}
|
||
return true, nil
|
||
}
|