tokenFactory/controller/topup_alipay.go

464 lines
12 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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