| package controller |
|
|
| import ( |
| "fmt" |
| "log" |
| "net/url" |
| "strconv" |
| "sync" |
| "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/Calcium-Ion/go-epay/epay" |
| "github.com/gin-gonic/gin" |
| "github.com/samber/lo" |
| "github.com/shopspring/decimal" |
| ) |
|
|
| func GetTopUpInfo(c *gin.Context) { |
| |
| payMethods := operation_setting.PayMethods |
|
|
| |
| if setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "" { |
| |
| hasStripe := false |
| for _, method := range payMethods { |
| if method["type"] == "stripe" { |
| hasStripe = true |
| break |
| } |
| } |
|
|
| if !hasStripe { |
| stripeMethod := map[string]string{ |
| "name": "Stripe", |
| "type": "stripe", |
| "color": "rgba(var(--semi-purple-5), 1)", |
| "min_topup": strconv.Itoa(setting.StripeMinTopUp), |
| } |
| payMethods = append(payMethods, stripeMethod) |
| } |
| } |
|
|
| data := gin.H{ |
| "enable_online_topup": operation_setting.PayAddress != "" && operation_setting.EpayId != "" && operation_setting.EpayKey != "", |
| "enable_stripe_topup": setting.StripeApiSecret != "" && setting.StripeWebhookSecret != "" && setting.StripePriceId != "", |
| "enable_creem_topup": setting.CreemApiKey != "" && setting.CreemProducts != "[]", |
| "creem_products": setting.CreemProducts, |
| "pay_methods": payMethods, |
| "min_topup": operation_setting.MinTopUp, |
| "stripe_min_topup": setting.StripeMinTopUp, |
| "amount_options": operation_setting.GetPaymentSetting().AmountOptions, |
| "discount": operation_setting.GetPaymentSetting().AmountDiscount, |
| } |
| common.ApiSuccess(c, data) |
| } |
|
|
| type EpayRequest struct { |
| Amount int64 `json:"amount"` |
| PaymentMethod string `json:"payment_method"` |
| TopUpCode string `json:"top_up_code"` |
| } |
|
|
| type AmountRequest struct { |
| Amount int64 `json:"amount"` |
| TopUpCode string `json:"top_up_code"` |
| } |
|
|
| func GetEpayClient() *epay.Client { |
| if operation_setting.PayAddress == "" || operation_setting.EpayId == "" || operation_setting.EpayKey == "" { |
| return nil |
| } |
| withUrl, err := epay.NewClient(&epay.Config{ |
| PartnerID: operation_setting.EpayId, |
| Key: operation_setting.EpayKey, |
| }, operation_setting.PayAddress) |
| if err != nil { |
| return nil |
| } |
| return withUrl |
| } |
|
|
| func getPayMoney(amount int64, group string) float64 { |
| dAmount := decimal.NewFromInt(amount) |
| |
| |
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { |
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) |
| dAmount = dAmount.Div(dQuotaPerUnit) |
| } |
|
|
| topupGroupRatio := common.GetTopupGroupRatio(group) |
| if topupGroupRatio == 0 { |
| topupGroupRatio = 1 |
| } |
|
|
| dTopupGroupRatio := decimal.NewFromFloat(topupGroupRatio) |
| dPrice := decimal.NewFromFloat(operation_setting.Price) |
| |
| discount := 1.0 |
| if ds, ok := operation_setting.GetPaymentSetting().AmountDiscount[int(amount)]; ok { |
| if ds > 0 { |
| discount = ds |
| } |
| } |
| dDiscount := decimal.NewFromFloat(discount) |
|
|
| payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio).Mul(dDiscount) |
|
|
| return payMoney.InexactFloat64() |
| } |
|
|
| func getMinTopup() int64 { |
| minTopup := operation_setting.MinTopUp |
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { |
| dMinTopup := decimal.NewFromInt(int64(minTopup)) |
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) |
| minTopup = int(dMinTopup.Mul(dQuotaPerUnit).IntPart()) |
| } |
| return int64(minTopup) |
| } |
|
|
| func RequestEpay(c *gin.Context) { |
| var req EpayRequest |
| err := c.ShouldBindJSON(&req) |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) |
| return |
| } |
| if req.Amount < getMinTopup() { |
| c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())}) |
| return |
| } |
|
|
| id := c.GetInt("id") |
| group, err := model.GetUserGroup(id, true) |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) |
| return |
| } |
| payMoney := getPayMoney(req.Amount, group) |
| if payMoney < 0.01 { |
| c.JSON(200, gin.H{"message": "error", "data": "充值金额过低"}) |
| return |
| } |
|
|
| if !operation_setting.ContainsPayMethod(req.PaymentMethod) { |
| c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"}) |
| return |
| } |
|
|
| callBackAddress := service.GetCallbackAddress() |
| returnUrl, _ := url.Parse(system_setting.ServerAddress + "/console/log") |
| notifyUrl, _ := url.Parse(callBackAddress + "/api/user/epay/notify") |
| tradeNo := fmt.Sprintf("%s%d", common.GetRandomString(6), time.Now().Unix()) |
| tradeNo = fmt.Sprintf("USR%dNO%s", id, tradeNo) |
| client := GetEpayClient() |
| if client == nil { |
| c.JSON(200, gin.H{"message": "error", "data": "当前管理员未配置支付信息"}) |
| return |
| } |
| uri, params, err := client.Purchase(&epay.PurchaseArgs{ |
| Type: req.PaymentMethod, |
| ServiceTradeNo: tradeNo, |
| Name: fmt.Sprintf("TUC%d", req.Amount), |
| Money: strconv.FormatFloat(payMoney, 'f', 2, 64), |
| Device: epay.PC, |
| NotifyUrl: notifyUrl, |
| ReturnUrl: returnUrl, |
| }) |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "拉起支付失败"}) |
| return |
| } |
| amount := req.Amount |
| if operation_setting.GetQuotaDisplayType() == operation_setting.QuotaDisplayTypeTokens { |
| dAmount := decimal.NewFromInt(int64(amount)) |
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) |
| amount = dAmount.Div(dQuotaPerUnit).IntPart() |
| } |
| topUp := &model.TopUp{ |
| UserId: id, |
| Amount: amount, |
| Money: payMoney, |
| TradeNo: tradeNo, |
| PaymentMethod: req.PaymentMethod, |
| CreateTime: time.Now().Unix(), |
| Status: "pending", |
| } |
| err = topUp.Insert() |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "创建订单失败"}) |
| return |
| } |
| c.JSON(200, gin.H{"message": "success", "data": params, "url": uri}) |
| } |
|
|
| |
| var orderLocks sync.Map |
| var createLock sync.Mutex |
|
|
| |
| func LockOrder(tradeNo string) { |
| lock, ok := orderLocks.Load(tradeNo) |
| if !ok { |
| createLock.Lock() |
| defer createLock.Unlock() |
| lock, ok = orderLocks.Load(tradeNo) |
| if !ok { |
| lock = new(sync.Mutex) |
| orderLocks.Store(tradeNo, lock) |
| } |
| } |
| lock.(*sync.Mutex).Lock() |
| } |
|
|
| |
| func UnlockOrder(tradeNo string) { |
| lock, ok := orderLocks.Load(tradeNo) |
| if ok { |
| lock.(*sync.Mutex).Unlock() |
| } |
| } |
|
|
| func EpayNotify(c *gin.Context) { |
| params := lo.Reduce(lo.Keys(c.Request.URL.Query()), func(r map[string]string, t string, i int) map[string]string { |
| r[t] = c.Request.URL.Query().Get(t) |
| return r |
| }, map[string]string{}) |
| client := GetEpayClient() |
| if client == nil { |
| log.Println("易支付回调失败 未找到配置信息") |
| _, err := c.Writer.Write([]byte("fail")) |
| if err != nil { |
| log.Println("易支付回调写入失败") |
| } |
| return |
| } |
| verifyInfo, err := client.Verify(params) |
| if err == nil && verifyInfo.VerifyStatus { |
| _, err := c.Writer.Write([]byte("success")) |
| if err != nil { |
| log.Println("易支付回调写入失败") |
| } |
| } else { |
| _, err := c.Writer.Write([]byte("fail")) |
| if err != nil { |
| log.Println("易支付回调写入失败") |
| } |
| log.Println("易支付回调签名验证失败") |
| return |
| } |
|
|
| if verifyInfo.TradeStatus == epay.StatusTradeSuccess { |
| log.Println(verifyInfo) |
| LockOrder(verifyInfo.ServiceTradeNo) |
| defer UnlockOrder(verifyInfo.ServiceTradeNo) |
| topUp := model.GetTopUpByTradeNo(verifyInfo.ServiceTradeNo) |
| if topUp == nil { |
| log.Printf("易支付回调未找到订单: %v", verifyInfo) |
| return |
| } |
| if topUp.Status == "pending" { |
| topUp.Status = "success" |
| err := topUp.Update() |
| if err != nil { |
| log.Printf("易支付回调更新订单失败: %v", topUp) |
| return |
| } |
| |
| |
| dAmount := decimal.NewFromInt(int64(topUp.Amount)) |
| dQuotaPerUnit := decimal.NewFromFloat(common.QuotaPerUnit) |
| quotaToAdd := int(dAmount.Mul(dQuotaPerUnit).IntPart()) |
| err = model.IncreaseUserQuota(topUp.UserId, quotaToAdd, true) |
| if err != nil { |
| log.Printf("易支付回调更新用户失败: %v", topUp) |
| return |
| } |
| log.Printf("易支付回调更新用户成功 %v", topUp) |
| model.RecordLog(topUp.UserId, model.LogTypeTopup, fmt.Sprintf("使用在线充值成功,充值金额: %v,支付金额:%f", logger.LogQuota(quotaToAdd), topUp.Money)) |
| } |
| } else { |
| log.Printf("易支付异常回调: %v", verifyInfo) |
| } |
| } |
|
|
| func RequestAmount(c *gin.Context) { |
| var req AmountRequest |
| err := c.ShouldBindJSON(&req) |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "参数错误"}) |
| return |
| } |
|
|
| if req.Amount < getMinTopup() { |
| c.JSON(200, gin.H{"message": "error", "data": fmt.Sprintf("充值数量不能小于 %d", getMinTopup())}) |
| return |
| } |
| id := c.GetInt("id") |
| group, err := model.GetUserGroup(id, true) |
| if err != nil { |
| c.JSON(200, gin.H{"message": "error", "data": "获取用户分组失败"}) |
| return |
| } |
| payMoney := getPayMoney(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)}) |
| } |
|
|
| func GetUserTopUps(c *gin.Context) { |
| userId := c.GetInt("id") |
| pageInfo := common.GetPageQuery(c) |
| keyword := c.Query("keyword") |
|
|
| var ( |
| topups []*model.TopUp |
| total int64 |
| err error |
| ) |
| if keyword != "" { |
| topups, total, err = model.SearchUserTopUps(userId, keyword, pageInfo) |
| } else { |
| topups, total, err = model.GetUserTopUps(userId, pageInfo) |
| } |
| if err != nil { |
| common.ApiError(c, err) |
| return |
| } |
|
|
| pageInfo.SetTotal(int(total)) |
| pageInfo.SetItems(topups) |
| common.ApiSuccess(c, pageInfo) |
| } |
|
|
| |
| func GetAllTopUps(c *gin.Context) { |
| pageInfo := common.GetPageQuery(c) |
| keyword := c.Query("keyword") |
|
|
| var ( |
| topups []*model.TopUp |
| total int64 |
| err error |
| ) |
| if keyword != "" { |
| topups, total, err = model.SearchAllTopUps(keyword, pageInfo) |
| } else { |
| topups, total, err = model.GetAllTopUps(pageInfo) |
| } |
| if err != nil { |
| common.ApiError(c, err) |
| return |
| } |
|
|
| pageInfo.SetTotal(int(total)) |
| pageInfo.SetItems(topups) |
| common.ApiSuccess(c, pageInfo) |
| } |
|
|
| type AdminCompleteTopupRequest struct { |
| TradeNo string `json:"trade_no"` |
| } |
|
|
| |
| func AdminCompleteTopUp(c *gin.Context) { |
| var req AdminCompleteTopupRequest |
| if err := c.ShouldBindJSON(&req); err != nil || req.TradeNo == "" { |
| common.ApiErrorMsg(c, "参数错误") |
| return |
| } |
|
|
| |
| LockOrder(req.TradeNo) |
| defer UnlockOrder(req.TradeNo) |
|
|
| if err := model.ManualCompleteTopUp(req.TradeNo); err != nil { |
| common.ApiError(c, err) |
| return |
| } |
| common.ApiSuccess(c, nil) |
| } |
|
|