package controller import ( "crypto" "crypto/rand" "crypto/rsa" "crypto/sha256" "crypto/x509" "encoding/base64" "encoding/pem" "fmt" "io" "log" "net/http" "net/url" "sort" "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/QuantumNous/new-api/setting/system_setting" "github.com/gin-gonic/gin" "github.com/shopspring/decimal" ) const ( PaymentMethodAlipay = "alipay" ) var alipayAdaptor = &AlipayAdaptor{} // AlipayPayRequest represents a payment request for Alipay checkout. type AlipayPayRequest struct { Amount int64 `json:"amount"` PaymentMethod string `json:"payment_method"` } type AlipayAdaptor struct{} func getAlipayAppId() string { if setting.AlipaySandbox && setting.AlipaySandboxAppId != "" { return setting.AlipaySandboxAppId } return setting.AlipayAppId } func getAlipayPrivateKey() string { if setting.AlipaySandbox && setting.AlipaySandboxPrivateKey != "" { return setting.AlipaySandboxPrivateKey } return setting.AlipayPrivateKey } func getAlipayPublicKey() string { return setting.AlipayPublicKey } func getAlipayGatewayURL() string { if setting.AlipaySandbox { return "https://openapi-sandbox.dl.alipaydev.com/gateway.do" } return "https://openapi.alipay.com/gateway.do" } func getAlipayMinTopUp() int64 { minTopup := setting.AlipayMinTopUp if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { minTopup = minTopup * int(common.QuotaPerUnit) } return int64(minTopup) } func getAlipayPayMoney(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.AlipayUnitPrice * topupGroupRatio * discount } // alipaySign generates RSA2 signature for Alipay params func alipaySign(params map[string]string, privateKey string) (string, error) { keys := make([]string, 0, len(params)) for k := range params { keys = append(keys, k) } sort.Strings(keys) var buf strings.Builder for i, k := range keys { v := params[k] if v == "" { continue } if i > 0 { buf.WriteString("&") } buf.WriteString(k) buf.WriteString("=") buf.WriteString(v) } signStr := buf.String() block, _ := pem.Decode([]byte(privateKey)) 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(signStr)) 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 } // alipayVerify verifies Alipay notification signature func alipayVerify(params map[string]string, publicKey string) bool { sign, ok := params["sign"] if !ok || sign == "" { return false } keys := make([]string, 0, len(params)) for k := range params { if k == "sign" || k == "sign_type" { continue } keys = append(keys, k) } sort.Strings(keys) var buf strings.Builder for i, k := range keys { v := params[k] if v == "" { continue } if i > 0 { buf.WriteString("&") } buf.WriteString(k) buf.WriteString("=") buf.WriteString(v) } signStr := buf.String() sigBytes, err := base64.StdEncoding.DecodeString(sign) if err != nil { log.Printf("Alipay verify: base64 decode sign failed: %v", err) return false } block, _ := pem.Decode([]byte(publicKey)) if block == nil { log.Printf("Alipay verify: failed to decode public key PEM") return false } pub, err := x509.ParsePKIXPublicKey(block.Bytes) if err != nil { log.Printf("Alipay verify: failed to parse public key: %v", err) return false } rsaPub, ok := pub.(*rsa.PublicKey) if !ok { log.Printf("Alipay verify: public key is not RSA key") return false } hashed := sha256.Sum256([]byte(signStr)) err = rsa.VerifyPKCS1v15(rsaPub, crypto.SHA256, hashed[:], sigBytes) return err == nil } func (a *AlipayAdaptor) RequestPay(c *gin.Context, req *AlipayPayRequest) { if req.PaymentMethod != PaymentMethodAlipay { c.JSON(200, gin.H{"message": "error", "data": "不支持的支付渠道"}) return } minTopup := getAlipayMinTopUp() 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 := getAlipayPayMoney(float64(req.Amount), group) if payMoney <= 0.01 { c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) return } tradeNo := fmt.Sprintf("ALI%d%s%d", id, common.GetRandomString(12), time.Now().Unix()) topUp := &model.TopUp{ UserId: id, Amount: req.Amount, Money: payMoney, TradeNo: tradeNo, PaymentMethod: PaymentMethodAlipay, CreateTime: time.Now().Unix(), Status: common.TopUpStatusPending, } err = topUp.Insert() if err != nil { log.Printf("创建Alipay订单失败: %v", err) c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"}) return } payUrl, err := genAlipayPagePayUrl(tradeNo, payMoney, user.Email) if err != nil { log.Printf("生成Alipay支付链接失败: %v", err) c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"}) return } log.Printf("Alipay订单创建成功 - 用户ID: %d, 订单号: %s, 金额: %.2f", id, tradeNo, payMoney) c.JSON(200, gin.H{ "message": "success", "data": gin.H{ "pay_url": payUrl, "trade_no": tradeNo, }, }) } // genAlipayPagePayUrl generates an Alipay page payment URL func genAlipayPagePayUrl(tradeNo string, payMoney float64, email string) (string, error) { appId := getAlipayAppId() privateKey := getAlipayPrivateKey() if appId == "" || privateKey == "" { return "", fmt.Errorf("Alipay not configured") } callBackAddress := service.GetCallbackAddress() notifyUrl := strings.TrimRight(callBackAddress, "/") + "/api/alipay/notify" if setting.AlipayNotifyUrl != "" { notifyUrl = setting.AlipayNotifyUrl } returnUrl := system_setting.ServerAddress + "/console/log" if setting.AlipayReturnUrl != "" { returnUrl = setting.AlipayReturnUrl } amountStr := fmt.Sprintf("%.2f", payMoney) params := map[string]string{ "app_id": appId, "method": "alipay.trade.page.pay", "charset": "utf-8", "sign_type": "RSA2", "timestamp": time.Now().Format("2006-01-02 15:04:05"), "version": "1.0", "notify_url": notifyUrl, "return_url": returnUrl, "biz_content": fmt.Sprintf(`{"out_trade_no":"%s","product_code":"FAST_INSTANT_TRADE_PAY","total_amount":"%s","subject":"TokenFactory TopUp"}`, tradeNo, amountStr), } sign, err := alipaySign(params, privateKey) if err != nil { return "", err } params["sign"] = sign gatewayUrl := getAlipayGatewayURL() queryParams := url.Values{} for k, v := range params { queryParams.Set(k, v) } return gatewayUrl + "?" + queryParams.Encode(), nil } // AlipayNotify handles the Alipay async notification func AlipayNotify(c *gin.Context) { bodyBytes, err := io.ReadAll(c.Request.Body) if err != nil { log.Printf("读取Alipay通知失败: %v", err) c.String(http.StatusOK, "failure") return } defer c.Request.Body.Close() bodyStr := string(bodyBytes) formValues, err := url.ParseQuery(bodyStr) if err != nil { log.Printf("解析Alipay通知参数失败: %v", err) c.String(http.StatusOK, "failure") return } params := make(map[string]string) for k, v := range formValues { if len(v) > 0 { params[k] = v[0] } } log.Printf("收到Alipay异步通知: trade_no=%s, out_trade_no=%s, trade_status=%s", params["trade_no"], params["out_trade_no"], params["trade_status"]) publicKey := getAlipayPublicKey() if publicKey != "" && !alipayVerify(params, publicKey) { log.Printf("Alipay通知签名验证失败: %v", params) c.String(http.StatusOK, "failure") return } tradeStatus := params["trade_status"] outTradeNo := params["out_trade_no"] if tradeStatus == "TRADE_SUCCESS" || tradeStatus == "TRADE_FINISHED" { LockOrder(outTradeNo) defer UnlockOrder(outTradeNo) topUp := model.GetTopUpByTradeNo(outTradeNo) if topUp == nil { log.Printf("Alipay通知未找到订单: %s", outTradeNo) c.String(http.StatusOK, "failure") return } if topUp.Status != common.TopUpStatusPending { log.Printf("Alipay订单状态错误: %s, 当前状态: %s", outTradeNo, topUp.Status) c.String(http.StatusOK, "success") return } dAmount := decimal.NewFromInt(topUp.Amount) dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart()) if quotaToAdd <= 0 { log.Printf("Alipay无效的充值额度: %s", outTradeNo) c.String(http.StatusOK, "failure") return } topUp.CompleteTime = common.GetTimestamp() topUp.Status = common.TopUpStatusSuccess if err := topUp.Update(); err != nil { log.Printf("Alipay更新订单失败: %v", err) c.String(http.StatusOK, "failure") return } if err := model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true); err != nil { log.Printf("Alipay更新用户额度失败: %v", err) c.String(http.StatusOK, "failure") return } model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用支付宝充值成功,充值金额: %v,支付金额:%.2f", logger.LogQuota(quotaToAdd), topUp.Money)) model.ApplyAffiliateTopupReward(topUp.UserId, quotaToAdd) log.Printf("Alipay充值成功 - 订单: %s, 额度: %d", outTradeNo, quotaToAdd) } c.String(http.StatusOK, "success") } // AlipayReturn handles the Alipay synchronous return (page redirect) func AlipayReturn(c *gin.Context) { params := make(map[string]string) for k, v := range c.Request.URL.Query() { if len(v) > 0 { params[k] = v[0] } } log.Printf("Alipay同步返回: out_trade_no=%s, trade_no=%s", params["out_trade_no"], params["trade_no"]) publicKey := getAlipayPublicKey() if publicKey != "" && !alipayVerify(params, publicKey) { log.Printf("Alipay返回签名验证失败") c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup") return } outTradeNo := params["out_trade_no"] if outTradeNo != "" { topUp := model.GetTopUpByTradeNo(outTradeNo) if topUp != nil && topUp.Status == common.TopUpStatusSuccess { c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/log") return } } c.Redirect(http.StatusFound, system_setting.ServerAddress+"/console/topup") } // RequestAlipayPay handles the Alipay payment request from frontend func RequestAlipayPay(c *gin.Context) { if !setting.AlipayEnabled { c.JSON(200, gin.H{"message": "error", "data": "支付宝支付未启用"}) return } var req AlipayPayRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) return } alipayAdaptor.RequestPay(c, &req) }