package controller import ( "context" "encoding/base64" "encoding/pem" "errors" "fmt" "log" "math" "net/http" "os" "strconv" "strings" "sync" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/setting" "github.com/gin-gonic/gin" "github.com/go-pay/gopay" "github.com/go-pay/gopay/wechat/v3" "github.com/thanhpk/randstr" ) const ( PaymentMethodWechatPay = "wechat_pay" ) var wechatPayClientMu sync.Mutex var wechatPayClient *wechat.ClientV3 // ResetWechatPayClient 重置微信支付客户端(配置变更时调用) func ResetWechatPayClient() { wechatPayClientMu.Lock() wechatPayClient = nil wechatPayClientMu.Unlock() } func init() { setting.OnWechatPayConfigChanged = ResetWechatPayClient } // getWechatPayClient 获取或创建微信支付 V3 客户端 func getWechatPayClient() (*wechat.ClientV3, error) { wechatPayClientMu.Lock() defer wechatPayClientMu.Unlock() if wechatPayClient != nil { return wechatPayClient, nil } if !setting.IsWechatPayConfigured() { return nil, fmt.Errorf("微信支付未配置") } var privateKey string if setting.WechatPayKeyB64 != "" { keyBytes, err := base64.StdEncoding.DecodeString(setting.WechatPayKeyB64) if err != nil { return nil, fmt.Errorf("解析商户私钥 Base64 失败: %w", err) } privateKey = wrapAsPEM(keyBytes, "PRIVATE KEY") } else { keyBytes, err := os.ReadFile(setting.WechatPayKeyPath) if err != nil { return nil, fmt.Errorf("读取商户私钥文件失败: %w", err) } privateKey = string(keyBytes) } client, err := wechat.NewClientV3(setting.WechatPayMchID, setting.WechatPaySerialNo, setting.WechatPayAPIv3Key, privateKey) if err != nil { return nil, fmt.Errorf("创建微信支付客户端失败: %w", err) } // 设置微信支付公钥(用于回调验签) if setting.WechatPayPubKeyB64 != "" { pubKeyBytes, err := base64.StdEncoding.DecodeString(setting.WechatPayPubKeyB64) if err != nil { return nil, fmt.Errorf("解析微信公钥 Base64 失败: %w", err) } if err := client.AutoVerifySignByPublicKey([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY")), setting.WechatPayPubKeyID); err != nil { return nil, fmt.Errorf("开启公钥验签失败: %w", err) } } else { pubKeyBytes, err := os.ReadFile(setting.WechatPayPubKeyPath) if err != nil { return nil, fmt.Errorf("读取微信公钥文件失败: %w", err) } if err := client.AutoVerifySignByPublicKey([]byte(wrapAsPEM(pubKeyBytes, "PUBLIC KEY")), setting.WechatPayPubKeyID); err != nil { return nil, fmt.Errorf("开启公钥验签失败: %w", err) } } wechatPayClient = client return wechatPayClient, nil } // wrapAsPEM 确保 keyBytes 是 PEM 格式,兼容多种输入: // 1. 已是 PEM(含 -----BEGIN)→ 原样返回 // 2. 是 DER base64 文本(如 PEM 内层内容)→ 解码为 DER 后包装为 PEM // 3. 是原始 DER 字节 → 直接包装为 PEM func wrapAsPEM(keyBytes []byte, keyType string) string { content := string(keyBytes) if strings.Contains(content, "-----BEGIN") { return content } // 尝试 base64 解码:内容可能是 PEM 文件内层的 base64 文本 if derBytes, err := base64.StdEncoding.DecodeString(content); err == nil && len(derBytes) > 0 { return string(pem.EncodeToMemory(&pem.Block{ Type: keyType, Bytes: derBytes, })) } // 当作原始 DER 字节 return string(pem.EncodeToMemory(&pem.Block{ Type: keyType, Bytes: keyBytes, })) } // WechatPayRequest 微信支付请求参数 type WechatPayRequest struct { Amount int64 `json:"amount"` } // createWechatNativeOrder 调用微信 Native 下单 API,返回二维码 URL func createWechatNativeOrder(client *wechat.ClientV3, description, tradeNo string, totalFee int) (string, error) { bm := make(gopay.BodyMap) bm.Set("appid", setting.WechatPayAppID). Set("mchid", setting.WechatPayMchID). Set("description", description). Set("out_trade_no", tradeNo). Set("notify_url", setting.WechatPayNotifyURL). SetBodyMap("amount", func(bm gopay.BodyMap) { bm.Set("total", totalFee). Set("currency", "CNY") }) ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) defer cancel() rx, err := client.V3TransactionNative(ctx, bm) if err != nil { return "", fmt.Errorf("微信统一下单失败: %w", err) } if rx.Code != wechat.Success { return "", fmt.Errorf("微信支付错误: %d - %s", rx.Code, rx.ErrResponse.Message) } return rx.Response.CodeUrl, nil } // RequestWechatPayAmount 计算微信支付应付金额 func RequestWechatPayAmount(c *gin.Context) { var req WechatPayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } minTopup := getWechatMinTopup() if req.Amount < minTopup { c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) return } id := c.GetInt("id") group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) return } payMoney := getWechatPayMoney(float64(req.Amount), group) if payMoney <= 0.01 { c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) return } c.JSON(200, gin.H{"message": "success", "data": strconv.FormatFloat(payMoney, 'f', 2, 64)}) } // RequestWechatPay 创建微信支付订单,返回二维码 URL func RequestWechatPay(c *gin.Context) { var req WechatPayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } minTopup := getWechatMinTopup() if req.Amount < minTopup { c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) return } if req.Amount > 10000 { c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 10000"}) return } if !setting.IsWechatPayConfigured() { c.JSON(200, gin.H{"message": "error", "data": "微信支付未配置"}) return } client, err := getWechatPayClient() if err != nil { log.Println("获取微信支付客户端失败:", err) c.JSON(200, gin.H{"message": "error", "data": "微信支付配置错误"}) return } id := c.GetInt("id") group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) return } topupGroupRatio := common.GetTopupGroupRatio(group) if topupGroupRatio == 0 { topupGroupRatio = 1 } chargedMoney := float64(req.Amount) * topupGroupRatio tradeNo := fmt.Sprintf("wx%d%s", time.Now().UnixMilli(), randstr.String(8)) payMoney := getWechatPayMoney(float64(req.Amount), group) totalFee := int(math.Round(payMoney * 100)) // 元 -> 分 codeUrl, err := createWechatNativeOrder(client, fmt.Sprintf("充值%d", req.Amount), tradeNo, totalFee) if err != nil { log.Println(err) c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"}) return } topUp := &model.TopUp{ UserId: id, Amount: req.Amount, Money: chargedMoney, TradeNo: tradeNo, PaymentMethod: PaymentMethodWechatPay, CreateTime: time.Now().Unix(), Status: common.TopUpStatusPending, } if err := topUp.Insert(); err != nil { c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"}) return } c.JSON(200, gin.H{ "message": "success", "data": gin.H{ "trade_no": tradeNo, "qr_code_url": codeUrl, }, }) } // WechatPayStatus 轮询微信支付订单状态 func WechatPayStatus(c *gin.Context) { tradeNo := c.Query("trade_no") if tradeNo == "" { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } topUp := model.GetTopUpByTradeNo(tradeNo) if topUp == nil { c.JSON(200, gin.H{"message": "error", "data": "订单不存在"}) return } // 验证订单属于当前用户 userId := c.GetInt("id") if topUp.UserId != userId { c.JSON(200, gin.H{"message": "error", "data": "订单不存在"}) return } c.JSON(200, gin.H{ "message": "success", "data": gin.H{ "status": topUp.Status, "amount": topUp.Amount, }, }) } // WechatPayWebhook 处理微信支付回调通知 func WechatPayWebhook(c *gin.Context) { notifyReq, err := wechat.V3ParseNotify(c.Request) if err != nil { log.Printf("解析微信支付回调失败: %v", err) c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "解析回调失败"}) return } client, err := getWechatPayClient() if err != nil { log.Printf("获取微信支付客户端失败: %v", err) c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "服务错误"}) return } if err := notifyReq.VerifySignByPK(client.WxPublicKey()); err != nil { log.Printf("微信支付回调验签失败: %v", err) c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "验签失败"}) return } result, err := notifyReq.DecryptPayCipherText(setting.WechatPayAPIv3Key) if err != nil { log.Printf("解密微信支付回调数据失败: %v", err) c.JSON(http.StatusBadRequest, gin.H{"code": "FAIL", "message": "解密失败"}) return } if result.TradeState != "SUCCESS" { log.Printf("微信支付回调非成功状态: %s, 订单号: %s", result.TradeState, result.OutTradeNo) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) return } tradeNo := result.OutTradeNo LockOrder(tradeNo) defer UnlockOrder(tradeNo) if err := model.CompleteSubscriptionOrder(tradeNo, common.GetJsonString(result)); err == nil { log.Printf("微信支付订阅订单完成: %s", tradeNo) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) return } else if !errors.Is(err, model.ErrSubscriptionOrderNotFound) { log.Printf("微信支付订阅订单处理失败: %s, err: %s", tradeNo, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"}) return } if err := model.RechargeWechat(tradeNo); err != nil { log.Printf("微信支付充值失败: %s, err: %s", tradeNo, err.Error()) c.JSON(http.StatusInternalServerError, gin.H{"code": "FAIL", "message": "处理失败"}) return } log.Printf("微信支付充值成功: %s, 金额: %d", tradeNo, result.Amount.Total) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "成功"}) } func getWechatPayMoney(amount float64, group string) float64 { return calcPayMoney(amount, group, setting.WechatPayUnitPrice) } func getWechatMinTopup() int64 { return calcMinTopup(setting.WechatPayMinTopUp) }