package controller import ( "crypto" "crypto/aes" "crypto/cipher" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "io" "log" "net/http" "strings" "time" "github.com/QuantumNous/new-api/common" "github.com/QuantumNous/new-api/logger" "github.com/QuantumNous/new-api/model" "github.com/QuantumNous/new-api/service" "github.com/QuantumNous/new-api/setting" "github.com/QuantumNous/new-api/setting/operation_setting" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" ) const ( PaymentMethodWechatPay = "wxpay" ) var wechatPayAdaptor = &WechatPayAdaptor{} // WechatPayRequest represents a payment request for WeChat Pay. type WechatPayRequest struct { Amount int64 `json:"amount"` PaymentMethod string `json:"payment_method"` } type WechatPayAdaptor struct{} func getWechatPayAppId() string { return setting.WechatPayAppId } func getWechatPayMchId() string { if setting.WechatPaySandbox && setting.WechatPaySandboxMchId != "" { return setting.WechatPaySandboxMchId } return setting.WechatPayMchId } func getWechatPayApiV3Key() string { if setting.WechatPaySandbox && setting.WechatPaySandboxApiV3Key != "" { return setting.WechatPaySandboxApiV3Key } return setting.WechatPayApiV3Key } func getWechatPayPrivateKey() string { if setting.WechatPaySandbox && setting.WechatPaySandboxPrivateKey != "" { return setting.WechatPaySandboxPrivateKey } return setting.WechatPayPrivateKey } func getWechatPayAPIBaseURL() string { if setting.WechatPaySandbox { return "https://apihk.mch.weixin.qq.com/sandboxnew" } return "https://api.mch.weixin.qq.com" } func getWechatPayMinTopUp() int64 { minTopup := setting.WechatPayMinTopUp if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { minTopup = minTopup * int(common.QuotaPerUnit) } return int64(minTopup) } func getWechatPayPayMoney(amount float64, group string) float64 { originalAmount := amount if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { amount = amount / common.QuotaPerUnit } topupGroupRatio := common.GetTopupGroupRatio(group) if topupGroupRatio == 0 { topupGroupRatio = 1 } discount := 1.0 if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(originalAmount)]; ok { if ds > 0 { discount = ds } } return amount * setting.WechatPayUnitPrice * topupGroupRatio * discount } // wechatPaySign generates the WeChat Pay v3 signature func wechatPaySign(method string, urlPath string, body string, timestamp string, nonce string, privateKeyStr string) (string, error) { message := fmt.Sprintf("%s\n%s\n%s\n%s\n", method, urlPath, timestamp, nonce) if body != "" && body != "{}" { message = fmt.Sprintf("%s\n%s\n%s\n%s\n", method, urlPath, timestamp, nonce) + body } else { message = fmt.Sprintf("%s\n%s\n%s\n%s\n", method, urlPath, timestamp, nonce) } block, _ := pem.Decode([]byte(privateKeyStr)) if block == nil { return "", fmt.Errorf("failed to decode private key PEM") } priv, err := x509.ParsePKCS8PrivateKey(block.Bytes) if err != nil { priv, err = x509.ParsePKCS1PrivateKey(block.Bytes) if err != nil { return "", fmt.Errorf("failed to parse private key: %v", err) } } rsaKey, ok := priv.(*rsa.PrivateKey) if !ok { return "", fmt.Errorf("private key is not RSA key") } hashed := sha256.Sum256([]byte(message)) signature, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hashed[:]) if err != nil { return "", fmt.Errorf("sign failed: %v", err) } return base64.StdEncoding.EncodeToString(signature), nil } // wechatPayBuildAuthHeader builds the WeChat Pay v3 Authorization header func wechatPayBuildAuthHeader(method string, urlPath string, body string) (string, error) { mchId := getWechatPayMchId() privateKey := getWechatPayPrivateKey() certSerial := setting.WechatPayCertSerial if mchId == "" || privateKey == "" || certSerial == "" { return "", fmt.Errorf("WeChat Pay not fully configured") } timestamp := fmt.Sprintf("%d", time.Now().Unix()) nonce := common.GetRandomString(32) sign, err := wechatPaySign(method, urlPath, body, timestamp, nonce, privateKey) if err != nil { return "", err } auth := fmt.Sprintf(`WECHATPAY2-SHA256-RSA2048 mchid="%s",nonce_str="%s",timestamp="%s",serial_no="%s",signature="%s"`, mchId, nonce, timestamp, certSerial, sign) return auth, nil } // wechatPayPost sends a POST request to WeChat Pay API v3 func wechatPayPost(path string, requestBody interface{}) (map[string]interface{}, error) { jsonBytes, err := common.Marshal(requestBody) if err != nil { return nil, fmt.Errorf("marshal request body failed: %v", err) } bodyStr := string(jsonBytes) authHeader, err := wechatPayBuildAuthHeader("POST", path, bodyStr) if err != nil { return nil, err } apiBaseURL := getWechatPayAPIBaseURL() req, err := http.NewRequest("POST", apiBaseURL+path, strings.NewReader(bodyStr)) if err != nil { return nil, fmt.Errorf("create request failed: %v", err) } req.Header.Set("Content-Type", "application/json") req.Header.Set("Authorization", authHeader) req.Header.Set("Accept", "application/json") req.Header.Set("User-Agent", "TokenFactory") client := &http.Client{Timeout: 30 * time.Second} resp, err := client.Do(req) if err != nil { return nil, fmt.Errorf("request failed: %v", err) } defer resp.Body.Close() respBody, err := io.ReadAll(resp.Body) if err != nil { return nil, fmt.Errorf("read response failed: %v", err) } if resp.StatusCode != 200 && resp.StatusCode != 201 && resp.StatusCode != 204 { return nil, fmt.Errorf("WeChat Pay API error: HTTP %d, body: %s", resp.StatusCode, string(respBody)) } var result map[string]interface{} if len(respBody) > 0 { if err := common.Unmarshal(respBody, &result); err != nil { return nil, fmt.Errorf("parse response failed: %v", err) } } return result, nil } // wechatPayDecryptResource decrypts WeChat Pay notification resource func wechatPayDecryptResource(ciphertext, associatedData, nonce, apiV3Key string) ([]byte, error) { aesKey, err := base64.StdEncoding.DecodeString(apiV3Key) if err != nil { return nil, fmt.Errorf("decode api v3 key failed: %v", err) } cipherData, err := base64.StdEncoding.DecodeString(ciphertext) if err != nil { return nil, fmt.Errorf("decode ciphertext failed: %v", err) } nonceBytes := []byte(nonce) adBytes := []byte(associatedData) block, err := aes.NewCipher(aesKey) if err != nil { return nil, fmt.Errorf("create cipher failed: %v", err) } aesGCM, err := cipher.NewGCM(block) if err != nil { return nil, fmt.Errorf("create GCM failed: %v", err) } plaintext, err := aesGCM.Open(nil, nonceBytes, cipherData, adBytes) if err != nil { return nil, fmt.Errorf("decrypt failed: %v", err) } return plaintext, nil } func (a *WechatPayAdaptor) RequestPay(c *gin.Context, req *WechatPayRequest) { if req.PaymentMethod != PaymentMethodWechatPay { c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"}) return } minTopup := getWechatPayMinTopUp() if req.Amount < minTopup { c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", minTopup)}) return } if req.Amount > 100000 { c.JSON(200, gin.H{"message": "error", "data": "充值数量不能大于 100000"}) return } id := c.GetInt("id") user, err := model.GetUserById(id, false) if err != nil || user == nil { c.JSON(200, gin.H{"message": "error", "data": "用户不存在"}) return } group, err := model.GetUserGroup(id, true) if err != nil { c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) return } payMoney := getWechatPayPayMoney(float64(req.Amount), group) if payMoney <= 0.01 { c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) return } tradeNo := fmt.Sprintf("WX%d%s%d", id, common.GetRandomString(12), time.Now().Unix()) topUp := &model.TopUp{ UserId: id, Amount: req.Amount, Money: payMoney, TradeNo: tradeNo, PaymentMethod: PaymentMethodWechatPay, CreateTime: time.Now().Unix(), Status: common.TopUpStatusPending, } err = topUp.Insert() if err != nil { log.Printf("创建WeChat Pay订单失败: %v", err) c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"}) return } // Create WeChat Pay order via API v3 callBackAddress := service.GetCallbackAddress() notifyUrl := strings.TrimRight(callBackAddress, "/") + "/api/wechatpay/notify" if setting.WechatPayNotifyUrl != "" { notifyUrl = setting.WechatPayNotifyUrl } amountFen := int64(payMoney * 100) // Convert yuan to fen log.Printf("WeChat Pay创建订单 - 用户: %d, 订单: %s, 金额: %.2f元 (%d分)", id, tradeNo, payMoney, amountFen) payParams := map[string]interface{}{ "mchid": getWechatPayMchId(), "out_trade_no": tradeNo, "appid": getWechatPayAppId(), "description": "TokenFactory TopUp", "notify_url": notifyUrl, "amount": map[string]interface{}{ "total": amountFen, "currency": "CNY", }, "payer": map[string]interface{}{ "openid": "", // Will be obtained from WeChat OAuth }, "scene_info": map[string]interface{}{ "payer_client_ip": c.ClientIP(), }, } // For H5 payment payParams["scene_info"].(map[string]interface{})["h5_info"] = map[string]interface{}{ "type": "Wap", } result, err := wechatPayPost("/v3/pay/transactions/h5", payParams) if err != nil { log.Printf("WeChat Pay创建订单失败: %v", err) c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"}) return } h5Url := "" if result != nil { if urlStr, ok := result["h5_url"].(string); ok { h5Url = urlStr } } log.Printf("WeChat Pay订单创建成功 - 用户ID: %d, 订单号: %s, 金额: %.2f", id, tradeNo, payMoney) c.JSON(200, gin.H{ "message": "success", "data": gin.H{ "pay_url": h5Url, "trade_no": tradeNo, }, }) } // WechatPayNotify handles the WeChat Pay payment notification func WechatPayNotify(c *gin.Context) { bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { log.Printf("读取WeChat Pay通知失败: %v", err) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "read body failed"}) return } defer c.Request.Body.Close() log.Printf("收到WeChat Pay通知: %s", string(bodyBytes)) var notifyData struct { Id string `json:"id"` CreateTime string `json:"create_time"` EventType string `json:"event_type"` ResourceType string `json:"resource_type"` Resource struct { Algorithm string `json:"algorithm"` Ciphertext string `json:"ciphertext"` AssociatedData string `json:"associated_data"` Nonce string `json:"nonce"` OriginalType string `json:"original_type"` } `json:"resource"` Summary string `json:"summary"` } if err := common.Unmarshal(bodyBytes, ¬ifyData); err != nil { log.Printf("解析WeChat Pay通知失败: %v", err) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "parse failed"}) return } apiV3Key := getWechatPayApiV3Key() if apiV3Key == "" { log.Printf("WeChat Pay API v3 key not configured") c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "not configured"}) return } // Decrypt the resource plaintext, err := wechatPayDecryptResource( notifyData.Resource.Ciphertext, notifyData.Resource.AssociatedData, notifyData.Resource.Nonce, apiV3Key, ) if err != nil { log.Printf("WeChat Pay通知解密失败: %v", err) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "decrypt failed"}) return } log.Printf("WeChat Pay通知解密成功: %s", string(plaintext)) var paymentResult struct { AppId string `json:"appid"` MchId string `json:"mchid"` OutTradeNo string `json:"out_trade_no"` TransactionId string `json:"transaction_id"` TradeType string `json:"trade_type"` TradeState string `json:"trade_state"` TradeStateDesc string `json:"trade_state_desc"` BankType string `json:"bank_type"` Attach string `json:"attach"` SuccessTime string `json:"success_time"` Payer struct { OpenId string `json:"openid"` } `json:"payer"` Amount struct { Total int `json:"total"` PayerTotal int `json:"payer_total"` Currency string `json:"currency"` PayerCurrency string `json:"payer_currency"` } `json:"amount"` } if err := common.Unmarshal(plaintext, &paymentResult); err != nil { log.Printf("解析WeChat Pay支付结果失败: %v", err) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "parse result failed"}) return } tradeNo := paymentResult.OutTradeNo log.Printf("WeChat Pay支付结果: out_trade_no=%s, transaction_id=%s, trade_state=%s", tradeNo, paymentResult.TransactionId, paymentResult.TradeState) if paymentResult.TradeState != "SUCCESS" { log.Printf("WeChat Pay支付未成功: %s, trade_state=%s", tradeNo, paymentResult.TradeState) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "ok"}) return } LockOrder(tradeNo) defer UnlockOrder(tradeNo) topUp := model.GetTopUpByTradeNo(tradeNo) if topUp == nil { log.Printf("WeChat Pay通知未找到订单: %s", tradeNo) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "order not found"}) return } if topUp.Status != common.TopUpStatusPending { log.Printf("WeChat Pay订单状态错误: %s, 当前状态: %s", tradeNo, topUp.Status) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "ok"}) return } dAmount := decimal.NewFromInt(topUp.Amount) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart()) if quotaToAdd <= 0 { log.Printf("WeChat Pay无效的充值额度: %s", tradeNo) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "invalid quota"}) return } topUp.CompleteTime = common.GetTimestamp() topUp.Status = common.TopUpStatusSuccess if err := topUp.Update(); err != nil { log.Printf("WeChat Pay更新订单失败: %v", err) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "update failed"}) return } if err := model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true); err != nil { log.Printf("WeChat Pay更新用户额度失败: %v", err) c.JSON(http.StatusOK, gin.H{"code": "FAIL", "message": "quota update failed"}) return } model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用微信支付充值成功,充值金额: %v,支付金额:%.2f", logger.LogQuota(quotaToAdd), topUp.Money)) model.ApplyAffiliateTopupReward(topUp.UserId, quotaToAdd) log.Printf("WeChat Pay充值成功 - 订单: %s, 额度: %d", tradeNo, quotaToAdd) c.JSON(http.StatusOK, gin.H{"code": "SUCCESS", "message": "ok"}) } // RequestWechatPay handles the WeChat Pay payment request from frontend func RequestWechatPay(c *gin.Context) { if !setting.WechatPayEnabled { c.JSON(200, gin.H{"message": "error", "data": "微信支付未启用"}) return } var req WechatPayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } wechatPayAdaptor.RequestPay(c, &req) }