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