This commit introduces a major architectural refactoring to improve quota management, centralize logging, and streamline the relay handling logic. Key changes: - **Pre-consume Quota:** Implements a new mechanism to check and reserve user quota *before* making the request to the upstream provider. This ensures more accurate quota deduction and prevents users from exceeding their limits due to concurrent requests. - **Unified Relay Handlers:** Refactors the relay logic to use generic handlers (e.g., `ChatHandler`, `ImageHandler`) instead of provider-specific implementations. This significantly reduces code duplication and simplifies adding new channels. - **Centralized Logger:** A new dedicated `logger` package is introduced, and all system logging calls are migrated to use it, moving this responsibility out of the `common` package. - **Code Reorganization:** DTOs are generalized (e.g., `dalle.go` -> `openai_image.go`) and utility code is moved to more appropriate packages (e.g., `common/http.go` -> `service/http.go`) for better code structure.
267 lines
7.2 KiB
Go
267 lines
7.2 KiB
Go
package controller
|
||
|
||
import (
|
||
"fmt"
|
||
"log"
|
||
"net/url"
|
||
"one-api/common"
|
||
"one-api/logger"
|
||
"one-api/model"
|
||
"one-api/service"
|
||
"one-api/setting"
|
||
"strconv"
|
||
"sync"
|
||
"time"
|
||
|
||
"github.com/Calcium-Ion/go-epay/epay"
|
||
"github.com/gin-gonic/gin"
|
||
"github.com/samber/lo"
|
||
"github.com/shopspring/decimal"
|
||
)
|
||
|
||
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 setting.PayAddress == "" || setting.EpayId == "" || setting.EpayKey == "" {
|
||
return nil
|
||
}
|
||
withUrl, err := epay.NewClient(&epay.Config{
|
||
PartnerID: setting.EpayId,
|
||
Key: setting.EpayKey,
|
||
}, setting.PayAddress)
|
||
if err != nil {
|
||
return nil
|
||
}
|
||
return withUrl
|
||
}
|
||
|
||
func getPayMoney(amount int64, group string) float64 {
|
||
dAmount := decimal.NewFromInt(amount)
|
||
|
||
if !common.DisplayInCurrencyEnabled {
|
||
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(setting.Price)
|
||
|
||
payMoney := dAmount.Mul(dPrice).Mul(dTopupGroupRatio)
|
||
|
||
return payMoney.InexactFloat64()
|
||
}
|
||
|
||
func getMinTopup() int64 {
|
||
minTopup := setting.MinTopUp
|
||
if !common.DisplayInCurrencyEnabled {
|
||
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 !setting.ContainsPayMethod(req.PaymentMethod) {
|
||
c.JSON(200, gin.H{"message": "error", "data": "支付方式不存在"})
|
||
return
|
||
}
|
||
|
||
callBackAddress := service.GetCallbackAddress()
|
||
returnUrl, _ := url.Parse(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 !common.DisplayInCurrencyEnabled {
|
||
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,
|
||
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})
|
||
}
|
||
|
||
// tradeNo lock
|
||
var orderLocks sync.Map
|
||
var createLock sync.Mutex
|
||
|
||
// LockOrder 尝试对给定订单号加锁
|
||
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()
|
||
}
|
||
|
||
// UnlockOrder 释放给定订单号的锁
|
||
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
|
||
}
|
||
//user, _ := model.GetUserById(topUp.UserId, false)
|
||
//user.Quota += topUp.Amount * 500000
|
||
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)})
|
||
}
|