You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
167 lines
6.0 KiB
167 lines
6.0 KiB
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
|
|
}
|
|
|