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 }