|
- 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)
- }
|