package wechat import ( "context" "crypto/aes" "crypto/cipher" "crypto/rsa" "encoding/base64" "encoding/json" "fmt" "math/rand" "strings" "time" "github.com/wechatpay-apiv3/wechatpay-go/core" "github.com/wechatpay-apiv3/wechatpay-go/core/option" "github.com/wechatpay-apiv3/wechatpay-go/services/payments/app" "github.com/wechatpay-apiv3/wechatpay-go/utils" ) func newSys(options Options) (sys *WeChat, err error) { sys = &WeChat{ options: options, } // 商户私钥:含 "-----BEGIN" 视为直接内嵌的 PEM 内容(推荐,配置全在库),否则当作 .pem 文件路径。 var privateKey *rsa.PrivateKey if strings.Contains(options.PrivateKeyPath, "-----BEGIN") { privateKey, err = utils.LoadPrivateKey(options.PrivateKeyPath) } else { privateKey, err = utils.LoadPrivateKeyWithPath(options.PrivateKeyPath) } if err != nil { // 库里绝不能 Fatal/panic:这里曾经是 log.Fatalf,一个错误的商户私钥配置就能直接杀掉 home 进程, // 而 home 在 5-in-1 容器里一死会连坐 gateway/api/timer/mcp,表现为全站 502。 // 由调用方决定是硬失败还是降级。 return nil, fmt.Errorf("加载商户私钥失败: %w", err) } // 2. 初始化客户端选项(推荐自动证书更新模式) ctx := context.Background() opts := []core.ClientOption{ // 自动处理签名、验签,并定时更新微信支付平台证书 option.WithWechatPayAutoAuthCipher( options.MchID, // 商户号 options.CertSerialNo, // 商户证书序列号 privateKey, // 商户私钥 options.ApiV3Key, // APIv3密钥(用于解密平台证书) ), } // 3. 创建微信支付客户端 if sys.client, err = core.NewClient(ctx, opts...); err != nil { return nil, fmt.Errorf("创建微信支付客户端失败: %w", err) } return sys, nil } type WeChat struct { options Options client *core.Client } // 生成商户订单号(示例:APP2024092515302234567890) func (this *WeChat) GenerateOrderNo(prefix string) string { // 1. 时间戳:年月日时分秒(精确到秒,如 20240925153022) timeStr := time.Now().Format("20060102150405") // 2. 随机数:6位随机数字(避免同一秒内重复) rand.Seed(time.Now().UnixNano()) // 初始化随机数种子 randNum := rand.Intn(900000) + 100000 // 生成 100000-999999 之间的随机数 // 3. 拼接前缀(可选,用于区分业务类型,如 APP/JSAPI/NATIVE) return fmt.Sprintf("%s%s%d", prefix, timeStr, randNum) } // 创建App支付订单 func (this *WeChat) CreateAppOrder(ctx context.Context, outTradeNo string, totalFee int64, description string, notifyurl string) (result *AppPayParams, err error) { var ( resp *app.PrepayWithRequestPaymentResponse ) // 创建JSAPI支付服务 svc := app.AppApiService{Client: this.client} // 构建创建订单请求参数 req := app.PrepayRequest{ Appid: core.String(this.options.AppID), // 传入 AppID Mchid: core.String(this.options.MchID), OutTradeNo: core.String(outTradeNo), Description: core.String(description), Amount: &app.Amount{ Total: core.Int64(totalFee), }, NotifyUrl: core.String(notifyurl), } // 调用创建订单接口 if resp, _, err = svc.PrepayWithRequestPayment(ctx, req); err != nil { err = fmt.Errorf("调用微信支付下单接口失败: %v", err) return } // 关键:JSAPI 支付的响应中有 Appid,无 PartnerId,签名字段是 PaySign result = &AppPayParams{ AppID: this.options.AppID, // 从配置取 AppID(响应中无此字段) PartnerID: *resp.PartnerId, // 从响应取商户号(App 支付有此字段) PrepayID: *resp.PrepayId, Package: *resp.Package, NonceStr: *resp.NonceStr, TimeStamp: *resp.TimeStamp, Sign: *resp.Sign, // App 支付签名字段是 Sign } return } // DecryptWxPayNotify 解密微信支付回调的 ciphertext 字段 // 参数: // - apiV3Key: 你的微信支付 APIv3 密钥(32位字符串) // - associatedData: 回调中的 resource.associated_data // - nonce: 回调中的 resource.nonce // - ciphertext: 回调中的 resource.ciphertext(base64 编码) // 返回:解密后的 WxPayTransaction 结构体 // DecryptWxPayNotify 修复后的解密函数 func (this *WeChat) DecryptWxPayNotify(associatedData, nonce, ciphertext string) (*WxPayTransaction, error) { // 1. 解码 base64 加密数据(ciphertext 解码后是「密文+Tag」) cipherBytes, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, fmt.Errorf("base64解码失败: %w", err) } // 2. 初始化 AES-GCM 解密器(APIv3 密钥作为密钥,此处无问题) block, err := aes.NewCipher([]byte(this.options.ApiV3Key)) if err != nil { return nil, fmt.Errorf("初始化AES解密器失败: %w", err) } gcm, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("初始化GCM解密器失败: %w", err) } // 3. 关键修复:分离「密文」和「Tag」,并拼接成「密文+Tag」 tagLength := 16 if len(cipherBytes) < tagLength { return nil, fmt.Errorf("加密数据长度不足(需包含16字节Tag)") } actualCipherText := cipherBytes[:len(cipherBytes)-tagLength] // 实际密文 tag := cipherBytes[len(cipherBytes)-tagLength:] // 提取Tag combinedData := append(actualCipherText, tag...) // 拼接「密文+Tag」(修复核心) // 4. 解密:传入拼接后的 combinedData(带Tag),而非单独的密文 plaintextBytes, err := gcm.Open( nil, // 输出缓冲区(nil 表示自动分配) []byte(nonce), // 随机串(从回调获取,无问题) combinedData, // 修复:用「密文+Tag」的组合 []byte(associatedData), // 附加数据(从回调获取,无问题) ) if err != nil { return nil, fmt.Errorf("解密失败: %w", err) } // 5. 解析解密后的 JSON(无问题) var transaction WxPayTransaction if err := json.Unmarshal(plaintextBytes, &transaction); err != nil { return nil, fmt.Errorf("解析交易数据失败: %w", err) } return &transaction, nil }