1
Some checks failed
Build / build (push) Has been cancelled
CodeQL / Analyze (go) (push) Has been cancelled
build / Build (push) Has been cancelled
GitHub Actions Mirror / mirror_to_gitee (push) Has been cancelled
GitHub Actions Mirror / mirror_to_gitlab (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled

This commit is contained in:
2025-07-02 18:32:43 +08:00
parent 8ae43bfba9
commit fbdf54603b
29 changed files with 1355 additions and 114 deletions

1
.gitignore vendored
View File

@ -21,3 +21,4 @@ cmd/migrate/migration/version-local/*
# go sum # go sum
go.sum go.sum
config/settings.deva.yml config/settings.deva.yml
/aggregate_translate_server

Binary file not shown.

View File

@ -222,3 +222,29 @@ func (e TmMemberPlatform) GetStatistic(c *gin.Context) {
e.OK(data, "获取数据成功") e.OK(data, "获取数据成功")
} }
// 修改用户-翻译通道字符消耗
func (e TmMemberPlatform) ChangeChars(c *gin.Context) {
s := service.TmMemberPlatform{}
req := dto.TmMemberPlatformChangeCharsReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Error(err)
e.Error(500, err, err.Error())
return
}
req.SetUpdateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.ChangeChars(&req, p)
if err != nil {
e.Error(500, err, fmt.Sprintf("修改用户-翻译通道字符消耗失败,\r\n失败信息 %s", err.Error()))
return
}
e.OK(nil, "修改成功")
}

View File

@ -0,0 +1,131 @@
package apis
import (
"go-admin/app/admin/service"
"go-admin/app/admin/service/dto"
"go-admin/common/actions"
"go-admin/common/statuscode"
"github.com/gin-gonic/gin"
"github.com/gin-gonic/gin/binding"
"github.com/go-admin-team/go-admin-core/sdk/api"
"github.com/go-admin-team/go-admin-core/sdk/pkg/jwtauth/user"
)
type TmRechargeLog struct {
api.Api
}
// GetPage 分页查询
func (e TmRechargeLog) GetPage(c *gin.Context) {
s := service.TmRechargeLog{}
req := dto.TmRechargeLogGetPageReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Error(500, err, "")
return
}
p := actions.GetPermissionFromContext(c)
datas := make([]dto.TmRechargeLogResp, 0)
var count int64
err = s.GetPage(&req, p, &datas, &count)
if err != nil {
e.Error(500, err, "")
return
}
e.PageOK(datas, int(count), req.GetPageIndex(), req.GetPageSize(), "")
}
// 创建充值订单
func (e TmRechargeLog) CreateOrder(c *gin.Context) {
req := dto.TmRechargeCreateOrderReq{}
s := service.TmRechargeLog{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Error(500, nil, statuscode.ErrorMessage[statuscode.ServerError])
return
}
apiKey := c.GetString("apiKey")
code := s.CreateOrder(&req, apiKey)
if code != statuscode.Success {
e.OK(code, statuscode.ErrorMessage[code])
return
}
e.OK(nil, "success")
}
// 后台充值
func (e TmRechargeLog) ManagerRecharge(c *gin.Context) {
req := dto.TmRechargeCreateOrderReq{}
s := service.TmRechargeLog{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req).
MakeService(&s.Service).
Errors
if err != nil {
e.Error(500, err, "")
return
}
if err := req.Validate(); err != nil {
e.Error(500, err, "")
return
}
req.SetCreateBy(user.GetUserId(c))
p := actions.GetPermissionFromContext(c)
err = s.ManagerRecharge(&req, p)
if err != nil {
e.Error(500, err, "")
return
}
e.OK(nil, "充值成功")
}
// 获取即将过期充值记录
func (e TmMember) GetMemberAdvent(c *gin.Context) {
s := service.TmRechargeLog{}
req := dto.TmRechargeLogFrontReq{}
err := e.MakeContext(c).
MakeOrm().
Bind(&req, binding.Query, binding.Form).
MakeService(&s.Service).
Errors
if err != nil {
e.Logger.Errorf("获取即将过期充值记录失败:", err)
e.Error(statuscode.ServerError, nil, statuscode.ErrorMessage[statuscode.ServerError])
}
userId := user.GetUserId(c)
datas := []dto.TmRechargeLogFrontResp{}
code := s.GetMemberAdvent(&req, &datas, userId)
if code != statuscode.Success {
e.Error(code, nil, statuscode.ErrorMessage[code])
return
}
e.OK(datas, "success")
}

View File

@ -42,7 +42,7 @@ func (e *Translate) Translate(c *gin.Context) {
if code != statuscode.Success { if code != statuscode.Success {
e.Logger.Error(err) e.Logger.Error(err)
e.Error(code, nil, statuscode.ErrorMessage[code]) e.OK(code, statuscode.ErrorMessage[code])
return return
} }

View File

@ -10,6 +10,7 @@ type TmMemberPlatform struct {
MemberId int `json:"memberId" gorm:"type:bigint;comment:用户id"` MemberId int `json:"memberId" gorm:"type:bigint;comment:用户id"`
RemainingCharacter int `json:"remainingCharacter" gorm:"type:bigint;comment:剩余字符数"` RemainingCharacter int `json:"remainingCharacter" gorm:"type:bigint;comment:剩余字符数"`
TotalCharacter int `json:"totalCharacter" gorm:"type:bigint;comment:总字符数"` TotalCharacter int `json:"totalCharacter" gorm:"type:bigint;comment:总字符数"`
UseCharacter int `json:"useCharacter" gorm:"type:bigint;comment:已用字符数"`
PlatformId int `json:"platformId" gorm:"type:bigint;comment:平台id"` PlatformId int `json:"platformId" gorm:"type:bigint;comment:平台id"`
PlatformKey string `json:"platformKey" gorm:"type:varchar(50);comment:平台key"` PlatformKey string `json:"platformKey" gorm:"type:varchar(50);comment:平台key"`
Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` Status int `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"`

View File

@ -1,14 +1,28 @@
package models package models
import "go-admin/common/models" import (
"go-admin/common/models"
"time"
"github.com/shopspring/decimal"
)
type TmRechargeLog struct { type TmRechargeLog struct {
models.Model models.Model
UserId int `json:"userId" gorm:"column:user_id;type:bigint;not null;comment:用户id"` Type int `json:"type" gorm:"column:type;type:tinyint;not null;comment:类型 1-充值 2-后台充值 3-赠送"`
MemberId int `json:"memberId" gorm:"column:member_id;type:bigint;not null;comment:翻译用户id"` OrderNo string `json:"orderNo" gorm:"column:order_no;type:varchar(36);not null;comment:订单号"`
Status int `json:"status" gorm:"column:status;type:tinyint;not null;comment:状态 1-正常 2-作废"` UserId int `json:"userId" gorm:"column:user_id;type:bigint;not null;comment:用户id"`
TotalChars int `json:"totalChars" gorm:"column:total_chars;type:bigint;not null;comment:充值字符数"` MemberId int `json:"memberId" gorm:"column:member_id;type:bigint;not null;comment:翻译用户id"`
PlatformId int `json:"platformId" gorm:"column:platform_id;type:bigint;not null;comment:平台id"`
Status int `json:"status" gorm:"column:status;type:tinyint;not null;comment:状态 1-待支付 2-已支付 3-已取消 4-申请退款 5-已退款 6-已过期"`
TotalChars int `json:"totalChars" gorm:"column:total_chars;type:bigint;not null;comment:充值字符数"`
TxHash string `json:"txHash" gorm:"column:tx_hash;type:varchar(64);comment:交易hash"`
Amount decimal.Decimal `json:"amount" gorm:"column:amount;type:decimal(10,6);not null;comment:充值金额U"`
ReceiveChannel string `json:"receiveChannel" gorm:"column:receive_channel;type:varchar(100);not null;comment:充值渠道"`
ReceiveAddress string `json:"receiveAddress" gorm:"column:receive_address;type:varchar(100);not null;comment:充值地址"`
PayTime *time.Time `json:"payTime" gorm:"column:pay_time;type:datetime;comment:支付时间"`
ExpireAt time.Time `json:"expireAt" gorm:"column:expire_at;type:datetime;not null;comment:过期时间"`
models.ModelTime models.ModelTime
models.ControlBy models.ControlBy
} }

View File

@ -16,6 +16,7 @@ func init() {
// registerTmMemberRouter // registerTmMemberRouter
func registerTmMemberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { func registerTmMemberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) {
api := apis.TmMember{} api := apis.TmMember{}
rechargeApi := apis.TmRechargeLog{}
r := v1.Group("/tm-member").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) r := v1.Group("/tm-member").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole())
{ {
r.GET("", actions.PermissionAction(), api.GetPage) r.GET("", actions.PermissionAction(), api.GetPage)
@ -24,7 +25,8 @@ func registerTmMemberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddl
r.PUT("/:id", actions.PermissionAction(), api.Update) r.PUT("/:id", actions.PermissionAction(), api.Update)
r.DELETE("", api.Delete) r.DELETE("", api.Delete)
r.POST("recharge", actions.PermissionAction(), api.Recharge) //字符充值 // r.POST("recharge", actions.PermissionAction(), api.Recharge) //字符充值
r.POST("manager-recharge", actions.PermissionAction(), rechargeApi.ManagerRecharge)
r.PUT("status", actions.PermissionAction(), api.ChangeStatus) //状态变更 r.PUT("status", actions.PermissionAction(), api.ChangeStatus) //状态变更
} }
@ -32,5 +34,6 @@ func registerTmMemberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddl
{ {
r2.GET("/api-key", api.GetMyApiKey) r2.GET("/api-key", api.GetMyApiKey)
r2.GET("platforms", api.GetPlatforms) r2.GET("platforms", api.GetPlatforms)
r2.GET("member-advent", api.GetMemberAdvent) //获取用户即将过期的充值信息
} }
} }

View File

@ -23,6 +23,7 @@ func registerTmMemberPlatformRouter(v1 *gin.RouterGroup, authMiddleware *jwt.Gin
r.POST("", api.Insert) r.POST("", api.Insert)
r.PUT("/:id", actions.PermissionAction(), api.Update) r.PUT("/:id", actions.PermissionAction(), api.Update)
r.DELETE("", api.Delete) r.DELETE("", api.Delete)
r.POST("/change-chars", actions.PermissionAction(), api.ChangeChars)
} }
f2 := v1.Group("/tm-member-platform").Use(authMiddleware.MiddlewareFunc()) f2 := v1.Group("/tm-member-platform").Use(authMiddleware.MiddlewareFunc())

View File

@ -120,21 +120,24 @@ func (s *TmMemberDeleteReq) GetId() interface{} {
} }
type TmMemberResp struct { type TmMemberResp struct {
Id int `json:"id"` Id int `json:"id"`
UserId int `json:"userId"` UserId int `json:"userId"`
NickName string `json:"nickName"` NickName string `json:"nickName"`
UserStatus int `json:"userStatus"` UserStatus int `json:"userStatus"`
Status int `json:"status"` Status int `json:"status"`
ApiKey string `json:"apiKey"` ApiKey string `json:"apiKey"`
TotalChars int `json:"totalChars"` TotalChars int `json:"totalChars"`
RemainChars int `json:"remainChars"` RemainChars int `json:"remainChars"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Platforms []TmMemberPlatformResp `json:"platforms"` Platforms []TmMemberPlatformResp `json:"platforms"`
UsedPlatform []TmMemberPlatformResp `json:"usedPlatform"`
} }
type TmMemberPlatformResp struct { type TmMemberPlatformResp struct {
Name string `json:"name"` PlatformId int `json:"platformId"`
RemainChars int `json:"remainChars"` MemberId int `json:"memberId"`
Name string `json:"name"`
TotalChars int `json:"totalChars"`
} }
type TmMemberRechargeReq struct { type TmMemberRechargeReq struct {
@ -175,9 +178,11 @@ func (e *TmMemberChangeStatusReq) Validate() error {
type TmMemberPlatformFrontedResp struct { type TmMemberPlatformFrontedResp struct {
Id int `json:"id"` Id int `json:"id"`
PlatformId int `json:"platformId"`
Name string `json:"name"` Name string `json:"name"`
RemainChars int `json:"remainChars"` RemainChars int `json:"remainChars"`
TotalChars int `json:"totalChars"` TotalChars int `json:"totalChars"`
UseChars int `json:"useChars"`
Price int `json:"price"` Price int `json:"price"`
ApiKey string `json:"apiKey"` ApiKey string `json:"apiKey"`
} }

View File

@ -99,3 +99,10 @@ type TmMemberDailyUsageDeleteReq struct {
func (s *TmMemberDailyUsageDeleteReq) GetId() interface{} { func (s *TmMemberDailyUsageDeleteReq) GetId() interface{} {
return s.Ids return s.Ids
} }
type TmMemberPlatformGroupData struct {
MemberId int `json:"memberId" comment:"用户id"`
PlatformId int `json:"platformId" comment:"平台id"`
// PlatformKey string `json:"platformKey" comment:"平台key"`
Total int `json:"total" comment:"总使用量"`
}

View File

@ -109,3 +109,11 @@ type TmMemberPlatformStatisticItemResp struct {
PlatformId int `json:"platformId"` PlatformId int `json:"platformId"`
Data []int `json:"data" comment:"数据"` Data []int `json:"data" comment:"数据"`
} }
type TmMemberPlatformChangeCharsReq struct {
Type int `json:"type" comment:"1-可用字符2-已使用字符"`
MemberId int `json:"memberId" comment:"用户id"`
PlatformId int `json:"platformId" comment:"平台id"`
ChangeNum int `json:"changeNum" comment:"变更数量"`
common.ControlBy
}

View File

@ -0,0 +1,165 @@
package dto
import (
"errors"
"go-admin/app/admin/models"
"go-admin/common/dto"
common "go-admin/common/models"
"time"
"github.com/shopspring/decimal"
)
type TmRechargeLogGetPageReq struct {
dto.Pagination `search:"-"`
Type int `form:"type" search:"type:exact;column:type;table:tm_recharge_log"`
Status int `form:"status" search:"type:exact;column:status;table:tm_recharge_log"`
PlatformId int `form:"platformId" search:"type:exact;column:platform_id;table:tm_recharge_log"`
TmRechargeLogOrder
}
type TmRechargeLogOrder struct {
Id string `form:"idOrder" search:"type:order;column:id;table:tm_platform"`
PayTime string `form:"payTimeOrder" search:"type:order;column:pay_time;table:tm_platform"`
}
func (m *TmRechargeLogGetPageReq) GetNeedSearch() interface{} {
return *m
}
type TmRechargeLogInsertReq struct {
Id int `json:"-" comment:"平台id"`
MemberId int `json:"memberId" comment:"会员id"`
PlatformId int `json:"platformId" comment:"平台id"`
Amount decimal.Decimal `json:"amount" comment:"充值金额"`
TotalChars int `json:"totalChars" comment:"总字数"`
ReceiveChannel string `json:"receiveChannel" comment:"充值渠道"`
Status int `json:"status" comment:"充值状态"`
common.ControlBy
}
func (s *TmRechargeLogInsertReq) Generate(model *models.TmPlatform) {
if s.Id == 0 {
model.Model = common.Model{Id: s.Id}
}
model.CreateBy = s.CreateBy // 添加这而,需要记录是被谁创建的
}
func (s *TmRechargeLogInsertReq) GetId() interface{} {
return s.Id
}
type TmRechargeLogListResp struct {
Id int `json:"id"`
ShowName string `json:"showName"`
Code string `json:"code"`
Price decimal.Decimal `json:"price"`
}
// TmPlatformGetReq 功能获取请求参数
type TmRechargeLogGetReq struct {
Id int `uri:"id"`
}
func (s *TmRechargeLogGetReq) GetId() interface{} {
return s.Id
}
// TmPlatformDeleteReq 功能删除请求参数
type TmRechargeLogDeleteReq struct {
Ids []int `json:"ids"`
}
func (s *TmRechargeLogDeleteReq) GetId() interface{} {
return s.Ids
}
type TmRechargeLogResp struct {
Id int `json:"id"`
OrderNo string `json:"orderNo"`
Type int `json:"type"`
MemberId int `json:"memberId"`
UserId int `json:"userId"`
PlatformId int `json:"platformId"`
Amount decimal.Decimal `json:"amount"`
TotalChars int `json:"totalChars"`
ReceiveChannel string `json:"receiveChannel"`
Status int `json:"status"`
TxHash string `json:"txHash"`
PayTime string `json:"payTime"`
CreatedAt string `json:"createdAt"`
}
// 用户充值订单创建请求参数
type TmRechargeCreateOrderReq struct {
Type int `json:"type" comment:"充值类型 1-用户充值 2-平台充值"`
MemberId int `json:"memberId" comment:"会员id"`
UserId int `json:"userId" comment:"用户id"`
PlatformId int `json:"platformId" comment:"平台id"`
TotalChars int `json:"totalChars" comment:"总字数"`
ReceiveChannel string `json:"receiveChannel" comment:"充值渠道"`
ReceiveAddress string `json:"receiveAddress" comment:"充值地址"`
TxHash string `json:"txHash" comment:"交易hash"`
Amount decimal.Decimal `json:"amount" comment:"充值金额"`
ExpireDays int `json:"expireDays" comment:"过期天数"`
common.ControlBy
}
func (e *TmRechargeCreateOrderReq) Validate() error {
if e.ExpireDays <= 0 {
return errors.New("过期天数必须大于0")
}
if e.MemberId <= 0 {
return errors.New("会员不存在")
}
if e.PlatformId <= 0 {
return errors.New("平台不存在")
}
return nil
}
func (e *TmRechargeCreateOrderReq) Generate(model *models.TmRechargeLog) {
if model == nil {
model = &models.TmRechargeLog{}
}
model.PlatformId = e.PlatformId
model.MemberId = e.MemberId
model.UserId = e.UserId
model.Type = e.Type
model.TotalChars = e.TotalChars * 10000
model.Amount = e.Amount
model.Status = 1
model.ReceiveChannel = e.ReceiveChannel
model.ReceiveAddress = e.ReceiveAddress
model.TxHash = e.TxHash
model.CreateBy = e.CreateBy
model.CreatedAt = time.Now()
}
// 用户充值订单创建参数
type TmRechargeLogInsertOrUpdateReq struct {
MemberId int `json:"memberId" comment:"会员id"`
PlatformId int `json:"platformId" comment:"平台id"`
PlatformKey string `json:"platformKey" comment:"平台key"`
RemainCharater int `json:"remainChars" comment:"剩余字数"`
}
type TmRechargeLogFrontReq struct {
PlatformId int `json:"platformId" query:"platformId" form:"platformId" comment:"平台id"`
}
type TmRechargeLogFrontResp struct {
Id int `json:"id"`
OrderNo string `json:"orderNo"`
PlatformName string `json:"platformName"`
ExpireUnix int64 `json:"expireUnix"`
TotalCharater int `json:"totalCharater"`
RemainCharater int `json:"remainCharater"`
}

View File

@ -0,0 +1,57 @@
-- KEYS[1] = tm_member_remain_count:{key}:{platformCode}
-- ARGV[1] = 当前时间戳(秒)
-- ARGV[2] = 扣减数量
local zset_key = KEYS[1]
local now = tonumber(ARGV[1])
local deduct = tonumber(ARGV[2])
local remain = deduct
local result = {}
-- 获取所有过期的 quota_id
local expired_quota_ids = redis.call("ZRANGEBYSCORE", zset_key, "-inf", now)
for _, quota_id in ipairs(expired_quota_ids) do
redis.call("DEL", "quota:" .. quota_id)
end
-- 清理过期额度
redis.call("ZREMRANGEBYSCORE", zset_key, "-inf", now)
local quota_ids = redis.call("ZRANGEBYSCORE", zset_key, now, "+inf")
for _, quota_id in ipairs(quota_ids) do
local quota_key = "quota:" .. quota_id
local amount = tonumber(redis.call("HGET", quota_key, "amount") or "0")
if amount > 0 then
local used = 0
if amount >= remain then
used = remain
redis.call("HINCRBY", quota_key, "amount", -remain)
remain = 0
else
used = amount
redis.call("HINCRBY", quota_key, "amount", -amount)
remain = remain - amount
end
table.insert(result, quota_id .. ":" .. used)
local new_amount = tonumber(redis.call("HGET", quota_key, "amount") or "0")
if new_amount <= 0 then
redis.call("DEL", quota_key)
redis.call("ZREM", zset_key, quota_id)
end
if remain == 0 then
break
end
end
end
if remain > 0 then
return {}
end
return result

View File

@ -0,0 +1,21 @@
-- KEYS[1] = tm_member_remain_count:{key}:{platformCode}
-- ARGV = {quota_id_1, amount_1, expire_1, quota_id_2, amount_2, expire_2, ...}
local zset_key = KEYS[1]
local len = table.getn(ARGV)
for i = 1, len, 3 do
local quota_id = ARGV[i]
local amount = tonumber(ARGV[i+1])
local expire = tonumber(ARGV[i+2])
local quota_key = "quota:" .. quota_id
if redis.call("EXISTS", quota_key) == 0 then
redis.call("HSET", quota_key, "amount", amount)
redis.call("ZADD", zset_key, expire, quota_id)
else
redis.call("HINCRBY", quota_key, "amount", amount)
end
end
return 1

View File

@ -0,0 +1,229 @@
package quota_manager
import (
"context"
"fmt"
rediskey "go-admin/common/redis_key"
"strconv"
"strings"
"time"
"github.com/go-redis/redis/v8"
)
type QuotaUsage struct {
QuotaID string
Used int64
Expire int64
}
type QuotaRecharge struct {
QuotaID string
Amount int64
ExpireAt int64
}
type QuotaManager struct {
rdb *redis.Client
deductLua *redis.Script
refundLua *redis.Script
zsetKeyPrefix string // "tm_member_remain_count"
}
func NewQuotaManager(rdb *redis.Client) *QuotaManager {
deductScript := `
-- Lua 脚本内容复制 deduct.lua
local zset_key = KEYS[1]
local now = tonumber(ARGV[1])
local deduct = tonumber(ARGV[2])
local remain = deduct
local result = {}
local expired_quota_ids = redis.call("ZRANGEBYSCORE", zset_key, "-inf", now)
for _, quota_id in ipairs(expired_quota_ids) do
redis.call("DEL", "quota:" .. quota_id)
end
redis.call("ZREMRANGEBYSCORE", zset_key, "-inf", now)
local quota_ids = redis.call("ZRANGEBYSCORE", zset_key, now, "+inf")
for _, quota_id in ipairs(quota_ids) do
local quota_key = "quota:" .. quota_id
local amount = tonumber(redis.call("HGET", quota_key, "amount") or "0")
if amount > 0 then
local used = 0
if amount >= remain then
used = remain
redis.call("HINCRBY", quota_key, "amount", -remain)
remain = 0
else
used = amount
redis.call("HINCRBY", quota_key, "amount", -amount)
remain = remain - amount
end
table.insert(result, quota_id .. ":" .. used)
local new_amount = tonumber(redis.call("HGET", quota_key, "amount") or "0")
if new_amount <= 0 then
redis.call("DEL", quota_key)
redis.call("ZREM", zset_key, quota_id)
end
if remain == 0 then
break
end
end
end
if remain > 0 then
return {}
end
return result
`
refundScript := `
-- Lua 脚本内容复制 refund.lua
local zset_key = KEYS[1]
local len = table.getn(ARGV)
for i = 1, len, 3 do
local quota_id = ARGV[i]
local amount = tonumber(ARGV[i+1])
local expire = tonumber(ARGV[i+2])
local quota_key = "quota:" .. quota_id
if redis.call("EXISTS", quota_key) == 0 then
redis.call("HSET", quota_key, "amount", amount)
redis.call("ZADD", zset_key, expire, quota_id)
else
redis.call("HINCRBY", quota_key, "amount", amount)
end
end
return 1
`
return &QuotaManager{
rdb: rdb,
deductLua: redis.NewScript(deductScript),
refundLua: redis.NewScript(refundScript),
zsetKeyPrefix: rediskey.TM_MEMBER_REMAIN_COUNT_PURE,
}
}
func (q *QuotaManager) zsetKey(userKey, platformCode string) string {
return fmt.Sprintf("%s:%s:%s", q.zsetKeyPrefix, userKey, platformCode)
}
// 充值写入
func (q *QuotaManager) AddQuota(ctx context.Context, userKey, platformCode string, recharge QuotaRecharge) error {
zsetKey := q.zsetKey(userKey, platformCode)
quotaKey := fmt.Sprintf("quota:%s", recharge.QuotaID)
err := q.rdb.HSet(ctx, quotaKey, map[string]interface{}{
"amount": recharge.Amount,
}).Err()
if err != nil {
return err
}
err = q.rdb.ZAdd(ctx, zsetKey, &redis.Z{
Score: float64(recharge.ExpireAt),
Member: recharge.QuotaID,
}).Err()
return err
}
// 扣减
func (q *QuotaManager) Deduct(ctx context.Context, userKey, platformCode string, deductCount int64) ([]QuotaUsage, error) {
zsetKey := q.zsetKey(userKey, platformCode)
now := time.Now().Unix()
res, err := q.deductLua.Run(ctx, q.rdb, []string{zsetKey}, now, deductCount).Result()
if err != nil {
return nil, err
}
arr, ok := res.([]interface{})
if !ok || len(arr) == 0 {
return nil, fmt.Errorf("insufficient quota or invalid response")
}
var usages []QuotaUsage
for _, v := range arr {
s, ok := v.(string)
if !ok {
continue
}
parts := strings.Split(s, ":")
if len(parts) != 2 {
continue
}
used, _ := strconv.ParseInt(parts[1], 10, 64)
usages = append(usages, QuotaUsage{
QuotaID: parts[0],
Used: used,
})
}
for i := range usages {
score, err := q.rdb.ZScore(ctx, zsetKey, usages[i].QuotaID).Result()
if err == nil {
usages[i].Expire = int64(score)
}
}
return usages, nil
}
// 回滚
func (q *QuotaManager) Refund(ctx context.Context, userKey, platformCode string, usages []QuotaUsage) error {
zsetKey := q.zsetKey(userKey, platformCode)
var args []interface{}
for _, u := range usages {
args = append(args, u.QuotaID, u.Used, u.Expire)
}
_, err := q.refundLua.Run(ctx, q.rdb, []string{zsetKey}, args...).Result()
return err
}
func (q *QuotaManager) GetTotalRemainingQuota(ctx context.Context, apiKey, platformCode string) (int, error) {
zsetKey := q.zsetKey(apiKey, platformCode)
now := time.Now().Unix()
total := 0
// 获取全部 quotaID 和过期时间
zsetEntries, err := q.rdb.ZRangeWithScores(ctx, zsetKey, 0, -1).Result()
if err != nil {
return 0, err
}
for _, entry := range zsetEntries {
expireAt := int64(entry.Score)
if expireAt <= now {
continue // 跳过已过期的 quota
}
quotaID := fmt.Sprintf("%v", entry.Member)
quotaKey := fmt.Sprintf("quota:%s", quotaID)
amountStr, err := q.rdb.HGet(ctx, quotaKey, "amount").Result()
if err != nil || amountStr == "" {
continue // 不存在或异常
}
amount, err := strconv.Atoi(amountStr)
if err != nil {
continue
}
total += amount
}
return total, nil
}

View File

@ -0,0 +1,80 @@
package quota_manager
import (
"context"
"go-admin/utils/redishelper"
"os"
"strconv"
"testing"
"time"
)
var (
redisAddr = "localhost:6379"
redisPwd = ""
redisDB = 1
)
func setupRedis() {
redishelper.InitDefaultRedis(redisAddr, redisPwd, redisDB)
redishelper.InitLockRedisConn(redisAddr, redisPwd, strconv.Itoa(redisDB))
}
func TestQuotaManager_AddDeductRefund(t *testing.T) {
ctx := context.Background()
setupRedis()
qmgr := NewQuotaManager(redishelper.DefaultRedis.GetClient())
userKey := "Bw4iSj9Y90ix0e05GrMFp6EuFFTIbE9j"
platformCode := "deepl_free"
quotaID := "testquota1"
expireAt := time.Now().Add(1 * time.Hour).Unix()
amount := int64(1000)
// 充值写入
err := qmgr.AddQuota(ctx, userKey, platformCode, QuotaRecharge{
QuotaID: quotaID,
Amount: amount,
ExpireAt: expireAt,
})
if err != nil {
t.Fatalf("AddQuota failed: %v", err)
}
// 扣减500
usages, err := qmgr.Deduct(ctx, userKey, platformCode, 500)
if err != nil {
t.Fatalf("Deduct failed: %v", err)
}
if len(usages) == 0 {
t.Fatal("Deduct returned empty usage")
}
if usages[0].Used != 500 {
t.Errorf("Expected Used=500, got %d", usages[0].Used)
}
// 回滚500
err = qmgr.Refund(ctx, userKey, platformCode, usages)
if err != nil {
t.Fatalf("Refund failed: %v", err)
}
// 再次扣减1000确认余额充足
usages, err = qmgr.Deduct(ctx, userKey, platformCode, 1000)
if err != nil {
t.Fatalf("Second Deduct failed: %v", err)
}
}
// 设置过期时间
func TestQuotaManager_Deduct_Refund_WithExpire(t *testing.T) {
setupRedis()
if err := redishelper.DefaultRedis.ZUpdateScore("tm_member_remain_count:MiyJrgfh3gYhwyDO43fdhHNswm4CeAfn:deepl", 1751436958, "573610499514565142"); err != nil {
t.Fatalf("ZUpdateScore failed: %v", err)
}
}
func TestMain(m *testing.M) {
os.Exit(m.Run())
}

View File

@ -82,7 +82,7 @@ func (e *SysUser) Insert(c *dto.SysUserInsertReq) error {
e.Orm.Model(role).Where("role_id = ?", c.RoleId).Find(&role) e.Orm.Model(role).Where("role_id = ?", c.RoleId).Find(&role)
if role.RoleId == 0 { if role.RoleId == 0 {
err = errors.New("角色不存在") return errors.New("角色不存在")
} }
err = e.Orm.Model(&data).Where("username = ?", c.Username).Count(&i).Error err = e.Orm.Model(&data).Where("username = ?", c.Username).Count(&i).Error

View File

@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"errors" "errors"
"fmt" "fmt"
"strconv" "strconv"
@ -9,12 +10,12 @@ import (
"github.com/bytedance/sonic" "github.com/bytedance/sonic"
"github.com/go-admin-team/go-admin-core/sdk/service" "github.com/go-admin-team/go-admin-core/sdk/service"
"github.com/go-redis/redis/v8"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"gorm.io/gorm" "gorm.io/gorm"
"go-admin/app/admin/models" "go-admin/app/admin/models"
"go-admin/app/admin/service/dto" "go-admin/app/admin/service/dto"
"go-admin/app/admin/service/quota_manager"
"go-admin/common/actions" "go-admin/common/actions"
cDto "go-admin/common/dto" cDto "go-admin/common/dto"
rediskey "go-admin/common/redis_key" rediskey "go-admin/common/redis_key"
@ -31,6 +32,7 @@ func (e TmMember) GetUserPlatforms(userId int, resp *[]dto.TmMemberPlatformFront
var memberAccount models.TmMember var memberAccount models.TmMember
if err := e.Orm.Model(&memberAccount). if err := e.Orm.Model(&memberAccount).
Where("user_id=?", userId).
Find(&memberAccount).Error; err != nil { Find(&memberAccount).Error; err != nil {
return err return err
} }
@ -51,8 +53,10 @@ func (e TmMember) GetUserPlatforms(userId int, resp *[]dto.TmMemberPlatformFront
dataItem.ApiKey = memberAccount.ApiKey dataItem.ApiKey = memberAccount.ApiKey
dataItem.Name = platform.ShowName dataItem.Name = platform.ShowName
dataItem.PlatformId = platform.Id
dataItem.Price = int(platform.Price.IntPart()) dataItem.Price = int(platform.Price.IntPart())
dataItem.RemainChars, _ = e.GetRemainCount(item.PlatformKey, dataItem.ApiKey) dataItem.RemainChars = item.RemainingCharacter
dataItem.UseChars = item.UseCharacter
*resp = append(*resp, dataItem) *resp = append(*resp, dataItem)
} }
@ -82,25 +86,30 @@ func (e *TmMember) GetPage(c *dto.TmMemberGetPageReq, p *actions.DataPermission,
} }
userIds := []int{} userIds := []int{}
memberIds := []int{}
for _, item := range datas { for _, item := range datas {
if !utility.ContainsInt(userIds, item.UserId) { if !utility.ContainsInt(userIds, item.UserId) {
userIds = append(userIds, item.UserId) userIds = append(userIds, item.UserId)
} }
if !utility.ContainsInt(memberIds, item.Id) {
memberIds = append(memberIds, item.Id)
}
} }
memberPlatformService := TmMemberPlatform{Service: e.Service}
platformService := TmPlatform{Service: e.Service} platformService := TmPlatform{Service: e.Service}
userService := SysUser{Service: e.Service} userService := SysUser{Service: e.Service}
users, _ := userService.GetByIds(userIds) users, _ := userService.GetByIds(userIds)
activeList, _ := platformService.GetActiveList() activeList, _ := platformService.GetActiveList()
memberPlatforms, _ := memberPlatformService.GetMemberList(memberIds)
for _, item := range datas { for _, item := range datas {
dataItem := dto.TmMemberResp{} dataItem := dto.TmMemberResp{}
copier.Copy(&dataItem, &item) copier.Copy(&dataItem, &item)
// count, _ := e.GetRemainCount(,dataItem.ApiKey)
dataItem.ApiKey = utility.DesensitizeGeneric(dataItem.ApiKey, 2, 2, '*') dataItem.ApiKey = utility.DesensitizeGeneric(dataItem.ApiKey, 2, 2, '*')
// dataItem.RemainChars = count
for _, user := range users { for _, user := range users {
if user.UserId == item.UserId { if user.UserId == item.UserId {
@ -108,14 +117,33 @@ func (e *TmMember) GetPage(c *dto.TmMemberGetPageReq, p *actions.DataPermission,
} }
} }
//剩余字符
for _, platform := range activeList { for _, platform := range activeList {
platformItem := dto.TmMemberPlatformResp{} platformItem := dto.TmMemberPlatformResp{}
platformItem.PlatformId = platform.Id
platformItem.MemberId = item.Id
platformItem.Name = platform.Name platformItem.Name = platform.Name
platformItem.RemainChars, _ = e.GetRemainCount(platform.Code, item.ApiKey) platformItem.TotalChars, _ = e.GetRemainCount(platform.Code, item.ApiKey)
dataItem.Platforms = append(dataItem.Platforms, platformItem) dataItem.Platforms = append(dataItem.Platforms, platformItem)
} }
//已用字符
for _, platform := range activeList {
platformItem := dto.TmMemberPlatformResp{}
platformItem.PlatformId = platform.Id
platformItem.MemberId = item.Id
platformItem.Name = platform.Name
for _, memberPlatform := range memberPlatforms {
if memberPlatform.PlatformId == platform.Id && memberPlatform.MemberId == item.Id {
platformItem.TotalChars = memberPlatform.UseCharacter
}
}
dataItem.UsedPlatform = append(dataItem.UsedPlatform, platformItem)
}
*list = append(*list, dataItem) *list = append(*list, dataItem)
} }
@ -190,13 +218,11 @@ func (e *TmMember) SaveAllCache() error {
} }
for _, item := range list { for _, item := range list {
key := fmt.Sprintf(rediskey.TM_MEMBER_BY_KEY, item.ApiKey) key := fmt.Sprintf(rediskey.TM_MEMBER_BY_KEY, item.ApiKey)
// remainKey := fmt.Sprintf(rediskey.TM_MEMBER_REMAIN_COUNT, item.ApiKey)
val, err := sonic.MarshalString(item) val, err := sonic.MarshalString(item)
if err != nil { if err != nil {
return err return err
} }
redishelper.DefaultRedis.SetString(key, val) redishelper.DefaultRedis.SetString(key, val)
// redishelper.DefaultRedis.SetString(remainKey, strconv.Itoa(item.RemainChars))
} }
return nil return nil
} }
@ -262,64 +288,101 @@ func (e *TmMember) RemoveByUserIds(userIds []int) error {
return nil return nil
} }
// SyncMemberRemain 同步剩余字符 // SyncMemberRemain 同步剩余字符ZSET 多笔充值版)
func (e *TmMember) SyncMemberRemain() error { func (e *TmMember) SyncMemberRemain() error {
scanKeys, err := redishelper.DefaultRedis.ScanKeys(fmt.Sprintf("%s*", rediskey.TM_MEMBER_REMAIN_COUNT_PURE)) scanKeys, err := redishelper.DefaultRedis.ScanKeys(fmt.Sprintf("%s:*:*", rediskey.TM_MEMBER_REMAIN_COUNT_PURE))
if err != nil { if err != nil {
return err return err
} }
members := e.getCacheMembers() members := e.getCacheMembers()
dailyUsageService := TmMemberDailyUsage{Service: e.Service}
datas := make([]models.TmMemberPlatform, 0) datas := make([]models.TmMemberPlatform, 0)
now := time.Now().Unix()
for _, key := range scanKeys { for _, key := range scanKeys {
items := strings.Split(key, ":") items := strings.Split(key, ":")
apiKey := items[len(items)-2] if len(items) < 3 {
platform := items[len(items)-1]
val, err := redishelper.DefaultRedis.GetString(key)
remainCount, err1 := strconv.Atoi(val)
if err != nil || err1 != nil {
e.Log.Errorf("TmMemberService SyncMemberRemain GetString error:%s \r\n err1:%s \r\n", err, err1)
continue continue
} }
apiKey := items[len(items)-2]
platform := items[len(items)-1]
// 清除过期的 quotaIDZSET + Hash
expiredIDs, err := redishelper.DefaultRedis.ZRangeByScore(key, "-inf", strconv.FormatInt(now, 10))
if err == nil && len(expiredIDs) > 0 {
for _, quotaID := range expiredIDs {
redishelper.DefaultRedis.DeleteString(fmt.Sprintf("quota:%s", quotaID))
}
redishelper.DefaultRedis.ZRemValues(key, expiredIDs...)
}
// 从 ZSET 获取所有 quota ID不清理过期数据库中保留的是理论值
zsetEntries, err := redishelper.DefaultRedis.GetRevRangeScoresSortSet(key)
if err != nil {
e.Log.Errorf("TmMemberService SyncMemberRemain ZRANGE failed for key %s: %v", key, err)
continue
}
totalRemain := 0
for _, entry := range zsetEntries {
quotaID, ok := entry.Member.(string)
if !ok {
continue
}
quotaKey := fmt.Sprintf("quota:%s", quotaID)
amountStr, err := redishelper.DefaultRedis.HGetField(quotaKey, "amount")
if err != nil || amountStr == "" {
continue
}
amount, err := strconv.Atoi(amountStr)
if err != nil {
continue
}
totalRemain += amount
}
if member := members[apiKey]; member.Id > 0 { if member := members[apiKey]; member.Id > 0 {
item := models.TmMemberPlatform{} memberPlatform := models.TmMemberPlatform{
item.Id = member.Id PlatformKey: platform,
item.RemainingCharacter = remainCount RemainingCharacter: totalRemain,
item.PlatformKey = platform }
datas = append(datas, item) memberPlatform.Id = member.Id
datas = append(datas, memberPlatform)
} }
} }
// 批量更新数据库(分批,每批最多 1000 条)
arrayData := utility.SplitSlice(datas, 1000) arrayData := utility.SplitSlice(datas, 1000)
for _, dataBatch := range arrayData { for _, dataBatch := range arrayData {
// 遍历当前批次的所有记录,为每条记录单独执行 UPDATE
for _, record := range dataBatch { for _, record := range dataBatch {
stmt := ` stmt := `
UPDATE tm_member_platform UPDATE tm_member_platform
SET SET
remaining_character = ?, remaining_character = ?,
updated_at = NOW() updated_at = NOW()
WHERE platform_key = ? AND member_id = ?; WHERE platform_key = ? AND member_id = ?;
` `
args := []interface{}{ args := []interface{}{
record.RemainingCharacter, record.RemainingCharacter,
record.PlatformKey, record.PlatformKey,
record.Id, record.Id,
} }
// 执行单个 UPDATE 语句
if err := e.Orm.Exec(stmt, args...).Error; err != nil { if err := e.Orm.Exec(stmt, args...).Error; err != nil {
// 记录错误,但继续处理批次中的其他记录 e.Log.Errorf("TmMemberService SyncMemberRemain single Exec for PlatformKey %s, MemberID %d error: %s \r\n",
e.Log.Errorf("TmMemberService SyncMemberRemain single Exec for PlatformKey %s, MemberID %d error: %s \r\n", record.PlatformKey, record.Id, err) record.PlatformKey, record.Id, err)
} }
} }
} }
// 同步每日使用量(不变)
dailyUsageService.SyncTotalUse()
return nil return nil
} }
@ -444,24 +507,6 @@ func (e *TmMember) loadSyncData(keys []string, members *map[string]models.TmMemb
} }
} }
// func (e *TmMember) GetMyApiKey(userId int) (dto.TranslateUserInfoResp, error) {
// var data models.TmMember
// resp := dto.TranslateUserInfoResp{}
// if err := e.Orm.Model(&data).Where("user_id = ?", userId).First(&data).Error; err != nil {
// e.Log.Errorf("TmMemberService GetMyApiKey error:%s \r\n", err)
// return resp, nil
// }
// var err error
// resp.UserApiKey = data.ApiKey
// resp.RemainChars, err = e.GetRemainCount(data.ApiKey)
// if err != nil {
// e.Log.Errorf("转换类型失败,error:%v", err)
// }
// return resp, nil
// }
// GetTranslateStatistic 获取翻译统计 // GetTranslateStatistic 获取翻译统计
func (e *TmMember) GetTranslateStatistic(userId int, list *[]dto.TranslateStatisticResp) error { func (e *TmMember) GetTranslateStatistic(userId int, list *[]dto.TranslateStatisticResp) error {
endDate := time.Now().Format("2006-01-02") endDate := time.Now().Format("2006-01-02")
@ -535,31 +580,24 @@ func (e *TmMember) Recharge(req *dto.TmMemberRechargeReq, p *actions.DataPermiss
// 获取可用字符数 // 获取可用字符数
func (e *TmMember) GetRemainCount(platformKey, apiKey string) (int, error) { func (e *TmMember) GetRemainCount(platformKey, apiKey string) (int, error) {
key := fmt.Sprintf(rediskey.TM_MEMBER_REMAIN_COUNT, apiKey, platformKey) ctx := context.Background()
val, err := redishelper.DefaultRedis.GetString(key) quotaManager := quota_manager.NewQuotaManager(redishelper.DefaultRedis.GetClient())
result := 0 result, err := quotaManager.GetTotalRemainingQuota(ctx, apiKey, platformKey)
if err != nil && !errors.Is(err, redis.Nil) { if err != nil {
return 0, err return 0, err
} }
if val != "" { // if result == 0 {
result, err = strconv.Atoi(val) // var data models.TmMember
if err != nil {
return 0, err
}
}
if result == 0 { // if err := e.Orm.Model(&data).Where("api_key = ?", apiKey).First(&data).Error; err != nil {
var data models.TmMember // return 0, err
// }
if err := e.Orm.Model(&data).Where("api_key = ?", apiKey).First(&data).Error; err != nil { // result = data.RemainChars
return 0, err // // redishelper.DefaultRedis.SetString(key, strconv.Itoa(result))
} // }
result = data.RemainChars
redishelper.DefaultRedis.SetString(key, strconv.Itoa(result))
}
return result, nil return result, nil
} }
@ -578,6 +616,55 @@ func (e *TmMember) DecrBy(platformKey, apiKey string, totalChars int) error {
return redishelper.DefaultRedis.DecrBy(remainCountKey, int64(totalChars)).Err() return redishelper.DefaultRedis.DecrBy(remainCountKey, int64(totalChars)).Err()
} }
// 增加字符
func (e *TmMember) IncyByQuote(ctx context.Context, platformKey, apiKey string, totalChars int, orderNo string, expireAt time.Time) error {
quotaManager := quota_manager.NewQuotaManager(redishelper.DefaultRedis.GetClient())
rechargeData := quota_manager.QuotaRecharge{
QuotaID: orderNo,
Amount: int64(totalChars),
ExpireAt: expireAt.Unix(),
}
if err := quotaManager.AddQuota(ctx, apiKey, platformKey, rechargeData); err != nil {
return err
}
return nil
}
// 扣除字符
func (e *TmMember) DecrByQuote(ctx context.Context, platformKey, apiKey string, totalChars int) ([]quota_manager.QuotaUsage, error) {
quoteManage := quota_manager.NewQuotaManager(redishelper.DefaultRedis.GetClient())
datas, err := quoteManage.Deduct(ctx, apiKey, platformKey, int64(totalChars))
if err != nil {
return nil, err
}
return datas, nil
}
// 回滚扣除字符
// apiKey 用户翻译密钥
// platformKey 平台标识
// datas 扣除的字符数据
func (e *TmMember) RefundQuote(ctx context.Context, apiKey, platformKey string, datas *[]quota_manager.QuotaUsage) error {
quoteManage := quota_manager.NewQuotaManager(redishelper.DefaultRedis.GetClient())
if err := quoteManage.Refund(ctx, apiKey, platformKey, *datas); err != nil {
}
return nil
}
// 设置可用字符数
func (e *TmMember) SetChars(platformKey, apiKey string, totalChars int) error {
remainCountKey := fmt.Sprintf(rediskey.TM_MEMBER_REMAIN_COUNT, apiKey, platformKey)
return redishelper.DefaultRedis.SetString(remainCountKey, strconv.Itoa(totalChars))
}
// 根据id获取数据 // 根据id获取数据
func (e *TmMember) GetById(id int, data *models.TmMember) error { func (e *TmMember) GetById(id int, data *models.TmMember) error {
if err := e.Orm.Model(data).Where("id = ?", id).First(data).Error; err != nil { if err := e.Orm.Model(data).Where("id = ?", id).First(data).Error; err != nil {
@ -586,6 +673,15 @@ func (e *TmMember) GetById(id int, data *models.TmMember) error {
return nil return nil
} }
// 根据userId 获取数据
func (e *TmMember) GetByUserId(userId int, data *models.TmMember) error {
if err := e.Orm.Model(data).Where("user_id = ?", userId).First(data).Error; err != nil {
return err
}
return nil
}
// 修改翻译用户状态 // 修改翻译用户状态
func (e TmMember) ChangeStatus(req *dto.TmMemberChangeStatusReq, p *actions.DataPermission) error { func (e TmMember) ChangeStatus(req *dto.TmMemberChangeStatusReq, p *actions.DataPermission) error {
var err error var err error
@ -638,7 +734,6 @@ func (e *TmMember) SyncInsert(req *dto.TmMemberSyncInsertReq, entity *models.TmM
platformService := TmPlatform{Service: e.Service} platformService := TmPlatform{Service: e.Service}
activePlatforms, _ := platformService.GetActiveList() activePlatforms, _ := platformService.GetActiveList()
TmMemberPlatforms := make([]models.TmMemberPlatform, 0) TmMemberPlatforms := make([]models.TmMemberPlatform, 0)
copier.Copy(entity, req) copier.Copy(entity, req)
apiKey, err := utility.GenerateBase62Key(32) apiKey, err := utility.GenerateBase62Key(32)
@ -661,8 +756,8 @@ func (e *TmMember) SyncInsert(req *dto.TmMemberSyncInsertReq, entity *models.TmM
PlatformId: platform.Id, PlatformId: platform.Id,
PlatformKey: platform.Code, PlatformKey: platform.Code,
Status: 1, Status: 1,
TotalCharacter: 10000, TotalCharacter: 0,
RemainingCharacter: 10000, RemainingCharacter: 0,
} }
TmMemberPlatforms = append(TmMemberPlatforms, item) TmMemberPlatforms = append(TmMemberPlatforms, item)
@ -672,9 +767,10 @@ func (e *TmMember) SyncInsert(req *dto.TmMemberSyncInsertReq, entity *models.TmM
return err return err
} }
for _, platform := range TmMemberPlatforms { // for _, platform := range TmMemberPlatforms {
e.IncrBy(platform.PlatformKey, entity.ApiKey, platform.RemainingCharacter) // // e.IncrBy(platform.PlatformKey, entity.ApiKey, platform.RemainingCharacter)
} // e.IncyByQuote(ctx,platform.PlatformKey,entity.ApiKey,platform.TotalCharacter,,)
// }
return nil return nil
} }

View File

@ -128,7 +128,7 @@ func (e *TmMemberDailyUsage) GetStatistic(userId int, resp *dto.TmMemberPlatform
if err := e.Orm.Model(models.TmMemberDailyUsage{}). if err := e.Orm.Model(models.TmMemberDailyUsage{}).
Joins("JOIN tm_member on tm_member.id=tm_member_daily_usage.member_id"). Joins("JOIN tm_member on tm_member.id=tm_member_daily_usage.member_id").
Where("tm_member_daily_usage.date >= ? and tm_member_daily_usage.date <= ?", startTime, endTime).Find(&datas).Error; err != nil { Where("tm_member.user_id =? and tm_member_daily_usage.date >= ? and tm_member_daily_usage.date <= ?", userId, startTime, endTime).Find(&datas).Error; err != nil {
e.Log.Error("获取折线图数据失败", err) e.Log.Error("获取折线图数据失败", err)
return nil return nil
} }
@ -178,3 +178,24 @@ func (e *TmMemberDailyUsage) GetStatistic(userId int, resp *dto.TmMemberPlatform
resp.Data = respDatas resp.Data = respDatas
return nil return nil
} }
// 同步总使用量
func (e *TmMemberDailyUsage) SyncTotalUse() error {
var datas []dto.TmMemberPlatformGroupData
var data models.TmMemberPlatform
if err := e.Orm.Model(models.TmMemberDailyUsage{}).
Group("member_id, platform_id").
Select("member_id, platform_id, sum(use_chars) as total").
Find(&datas).Error; err != nil {
return err
}
for _, item := range datas {
if err := e.Orm.Model(data).Where("member_id = ? and platform_id = ? ", item.MemberId, item.PlatformId).Update("use_character", item.Total).Error; err != nil {
continue
}
}
return nil
}

View File

@ -16,6 +16,42 @@ type TmMemberPlatform struct {
service.Service service.Service
} }
// 修改用户字符
func (e TmMemberPlatform) ChangeChars(req *dto.TmMemberPlatformChangeCharsReq, p *actions.DataPermission) error {
var data models.TmMemberPlatform
switch req.Type {
case 1:
member := models.TmMember{}
memberService := TmMember{Service: e.Service}
memberService.GetById(req.MemberId, &member)
if member.Id == 0 {
return errors.New("用户不存在")
}
platformService := TmPlatform{Service: e.Service}
platform, err := platformService.GetById(req.PlatformId)
if err != nil {
return errors.New("平台不存在")
}
if err := memberService.SetChars(platform.Code, member.ApiKey, req.ChangeNum); err != nil {
return errors.New("设置剩余字符失败")
}
if err := e.Orm.Model(data).Where("member_id =? AND platform_id =?", req.MemberId, req.PlatformId).Update("remaining_character", req.ChangeNum).Error; err != nil {
e.Log.Errorf("TmMemberPlatformService ChangeChars error:%s \r\n", err)
}
case 2:
default:
return errors.New("修改类型错误")
}
return nil
}
// GetPage 获取TmMemberPlatform列表 // GetPage 获取TmMemberPlatform列表
func (e *TmMemberPlatform) GetPage(c *dto.TmMemberPlatformGetPageReq, p *actions.DataPermission, list *[]models.TmMemberPlatform, count *int64) error { func (e *TmMemberPlatform) GetPage(c *dto.TmMemberPlatformGetPageReq, p *actions.DataPermission, list *[]models.TmMemberPlatform, count *int64) error {
var err error var err error
@ -107,3 +143,40 @@ func (e *TmMemberPlatform) Remove(d *dto.TmMemberPlatformDeleteReq, p *actions.D
} }
return nil return nil
} }
// GetMemberList 获取用户列表
func (e *TmMemberPlatform) GetMemberList(memberIds []int) ([]models.TmMemberPlatform, error) {
result := make([]models.TmMemberPlatform, 0)
err := e.Orm.Model(&models.TmMemberPlatform{}).
Where("member_id IN (?)", memberIds).
Find(&result).Error
if err != nil {
e.Log.Errorf("Service GetMemberList error:%s \r\n", err)
return nil, err
}
return result, nil
}
// InsertOrUpdateRemainChars 新增或更新用户剩余字符
func (e *TmMemberPlatform) GetOrInsert(req *dto.TmRechargeLogInsertOrUpdateReq) (models.TmMemberPlatform, error) {
result := models.TmMemberPlatform{}
e.Orm.Model(result).Where("member_id =? AND platform_id =?", req.MemberId, req.PlatformId).First(&result)
if result.Id == 0 {
result.MemberId = req.MemberId
result.PlatformId = req.PlatformId
result.PlatformKey = req.PlatformKey
result.RemainingCharacter = 0
result.TotalCharacter = 0
result.Status = 1
if err := e.Orm.Save(&result).Error; err != nil {
e.Log.Errorf("Service InsertOrUpdateRemainChars error:%s \r\n", err)
return result, err
}
}
return result, nil
}

View File

@ -225,6 +225,17 @@ func (e *TmPlatform) GetByKey(code string) (*models.TmPlatform, error) {
return &result, nil return &result, nil
} }
// 根据id 获取翻译平台信息
func (e *TmPlatform) GetById(id int) (*models.TmPlatform, error) {
var result models.TmPlatform
err := e.Orm.Model(&result).Where("id = ?", id).Find(&result).Error
if err != nil {
e.Log.Errorf("db error:%s", err)
return nil, err
}
return &result, nil
}
func (e *TmPlatform) GetActiveList() ([]models.TmPlatform, error) { func (e *TmPlatform) GetActiveList() ([]models.TmPlatform, error) {
var list []models.TmPlatform var list []models.TmPlatform
err := e.Orm.Model(&models.TmPlatform{}).Find(&list).Error err := e.Orm.Model(&models.TmPlatform{}).Find(&list).Error

View File

@ -0,0 +1,205 @@
package service
import (
"context"
"errors"
"fmt"
"go-admin/app/admin/models"
"go-admin/app/admin/service/dto"
"go-admin/app/admin/service/quota_manager"
"go-admin/common/actions"
cDto "go-admin/common/dto"
"go-admin/common/statuscode"
"go-admin/utils/redishelper"
"go-admin/utils/utility"
"sort"
"strconv"
"time"
"github.com/go-admin-team/go-admin-core/sdk/service"
"gorm.io/gorm"
)
type TmRechargeLog struct {
service.Service
}
// 首页获取即将过期的数据
func (e *TmRechargeLog) GetMemberAdvent(req *dto.TmRechargeLogFrontReq, resp *[]dto.TmRechargeLogFrontResp, userId int) int {
var datas []models.TmRechargeLog
if err := e.Orm.Model(&models.TmRechargeLog{}).
Where("user_id =? and platform_id =? and expire_at > now() and status =2", userId, req.PlatformId).
Find(&datas).Error; err != nil {
e.Log.Errorf("TmRechargeLogService GetMemberAdvent error:%s \r\n", err)
return statuscode.ServerError
}
for _, item := range datas {
respItem := dto.TmRechargeLogFrontResp{}
respItem.Id = item.Id
respItem.OrderNo = item.OrderNo
// respItem.PlatformName=item.PlatformName
respItem.TotalCharater = item.TotalChars
respItem.ExpireUnix = item.ExpireAt.Unix()
count, err := e.GetRemainByOrderNo(item.OrderNo)
if err != nil {
e.Log.Errorf("TmRechargeLogService GetRemainByOrderNo error:%s \r\n", err)
continue
}
respItem.RemainCharater = count
*resp = append(*resp, respItem)
}
sort.Slice(*resp, func(i, j int) bool {
return (*resp)[i].ExpireUnix < (*resp)[j].ExpireUnix
})
return statuscode.Success
}
// 后台充值
func (e *TmRechargeLog) ManagerRecharge(req *dto.TmRechargeCreateOrderReq, p *actions.DataPermission) error {
ctx := context.Background()
var data models.TmRechargeLog
member, platform, memberPlatform, code := e.CreateOrderJudge(req)
if code != statuscode.Success {
return errors.New(statuscode.ErrorMessage[code])
}
req.Generate(&data)
now := time.Now()
data.OrderNo = utility.GenerateTraceID()
data.Type = 2
data.Status = 2
data.UserId = member.UserId
data.ExpireAt = time.Now().AddDate(0, 0, req.ExpireDays)
data.PayTime = &now
rechargeData := quota_manager.QuotaRecharge{
QuotaID: data.OrderNo,
Amount: int64(data.TotalChars),
ExpireAt: data.ExpireAt.Unix(),
}
qmgr := quota_manager.NewQuotaManager(redishelper.DefaultRedis.GetClient())
// 事务处理
err := e.Orm.Transaction(func(tx *gorm.DB) error {
//写入充值记录
if err1 := e.Orm.Create(&data).Error; err1 != nil {
return err1
}
//更新用户翻译可用字符
if err1 := tx.Model(&models.TmMemberPlatform{}).Where("id =?", memberPlatform.Id).Update("remaining_character", data.TotalChars).Error; err1 != nil {
return err1
}
//写入可用字符
if err1 := qmgr.AddQuota(ctx, member.ApiKey, platform.Code, rechargeData); err1 != nil {
return err1
}
return nil
})
return err
}
// 新增充值校验
func (e *TmRechargeLog) CreateOrderJudge(req *dto.TmRechargeCreateOrderReq) (models.TmMember, *models.TmPlatform, models.TmMemberPlatform, int) {
memberService := TmMember{Service: e.Service}
member := models.TmMember{}
if err := memberService.GetById(req.MemberId, &member); err != nil {
return models.TmMember{}, nil, models.TmMemberPlatform{}, statuscode.NotFindMember
}
if member.ApiKey == "" {
return models.TmMember{}, nil, models.TmMemberPlatform{}, statuscode.NotFindApiKey
}
platformService := TmPlatform{Service: e.Service}
platform, err := platformService.GetById(req.PlatformId)
if err != nil {
e.Log.Errorf("获取平台信息失败:%s \r\n", err.Error())
return models.TmMember{}, nil, models.TmMemberPlatform{}, statuscode.PlatformNotSupport
}
if platform == nil || platform.Id == 0 {
return models.TmMember{}, nil, models.TmMemberPlatform{}, statuscode.PlatformNotSupport
}
memberPlatformService := TmMemberPlatform{Service: e.Service}
memberPlatform, err := memberPlatformService.GetOrInsert(&dto.TmRechargeLogInsertOrUpdateReq{
MemberId: req.MemberId,
PlatformId: req.PlatformId,
PlatformKey: platform.Code,
})
if err != nil {
return models.TmMember{}, nil, models.TmMemberPlatform{}, statuscode.ServerError
}
if memberPlatform.Status == 2 {
return models.TmMember{}, nil, models.TmMemberPlatform{}, statuscode.MemberPlatformNotSupport
}
return member, platform, memberPlatform, statuscode.Success
}
// 用户发起充值
// return code
func (e TmRechargeLog) CreateOrder(req *dto.TmRechargeCreateOrderReq, apiKey string) int {
// ctx := context.Background()
// var data models.TmRechargeLog
// member, platform, memberPlatform, code := e.CreateOrderJudge(req)
// if code != statuscode.Success {
// return code
// }
// req.Generate(&data)
// now := time.Now()
// data.OrderNo = utility.GenerateTraceID()
// data.Type = 1
// data.Status = 1
// data.UserId = member.UserId
// data.ExpireAt = time.Now().AddDate(0, 0, 30)
// data.PaymentTime = &now
return statuscode.Success
}
// 分页查询
func (e *TmRechargeLog) GetPage(req *dto.TmRechargeLogGetPageReq, p *actions.DataPermission, datas *[]dto.TmRechargeLogResp, count *int64) error {
var err error
var data models.TmRechargeLog
var list []models.TmRechargeLog
err = e.Orm.Model(&data).
Scopes(
cDto.MakeCondition(req.GetNeedSearch()),
cDto.Paginate(req.GetPageSize(), req.GetPageIndex()),
actions.Permission(data.TableName(), p),
).
Find(list).Limit(-1).Offset(-1).
Count(count).Error
if err != nil {
e.Log.Errorf("TmPlatformService GetPage error:%s \r\n", err)
return err
}
return nil
}
// 根据充值订单号 获取剩余可用字符
func (e *TmRechargeLog) GetRemainByOrderNo(orderNo string) (int, error) {
key := fmt.Sprintf("quota:%s", orderNo)
val, err := redishelper.DefaultRedis.HGetField(key, "amount")
if err != nil {
return 0, err
}
return strconv.Atoi(val)
}

View File

@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"go-admin/app/admin/models" "go-admin/app/admin/models"
@ -95,6 +96,7 @@ func (s *TranslatorService) RegisterProvider(name string, provider Translator) {
// 翻译校验 // 翻译校验
// return statusCode // return statusCode
func (s *TranslatorService) TranslateJudge(req *dto.TranslateReq, apiKey string) (result *dto.TranslateResult, respCode int) { func (s *TranslatorService) TranslateJudge(req *dto.TranslateReq, apiKey string) (result *dto.TranslateResult, respCode int) {
ctx := context.Background()
tmMemberService := TmMember{Service: s.Service} tmMemberService := TmMember{Service: s.Service}
tmPlatformAccount := TmPlatformAccount{Service: s.Service} tmPlatformAccount := TmPlatformAccount{Service: s.Service}
memberInfo, err1 := tmMemberService.GetByKey(apiKey) memberInfo, err1 := tmMemberService.GetByKey(apiKey)
@ -126,16 +128,23 @@ func (s *TranslatorService) TranslateJudge(req *dto.TranslateReq, apiKey string)
return return
} }
result, err := Translator.providers[req.Platform].Translate(req.Text, req.SourceLang, req.TargetLang) decyDatas, err := tmMemberService.DecrByQuote(ctx, req.Platform, apiKey, count)
if err != nil {
s.Log.Errorf("翻译计数失败:%v", err)
return nil, statuscode.ServerError
}
result, err = Translator.providers[req.Platform].Translate(req.Text, req.SourceLang, req.TargetLang)
if err == nil { if err == nil {
err2 := tmMemberService.DecrBy(req.Platform, apiKey, count) // err2 := tmMemberService.DecrBy(req.Platform, apiKey, count)
if err2 != nil { // if err2 != nil {
s.Log.Errorf("翻译计数失败:%v", err2) // s.Log.Errorf("翻译计数失败:%v", err2)
respCode = statuscode.ServerError // respCode = statuscode.ServerError
return // return
} // }
platformConfigInterface := Translator.config.ProviderConfigs[req.Platform] platformConfigInterface := Translator.config.ProviderConfigs[req.Platform]
@ -149,6 +158,8 @@ func (s *TranslatorService) TranslateJudge(req *dto.TranslateReq, apiKey string)
//每日统计保留三天 //每日统计保留三天
redishelper.DefaultRedis.Expire(fmt.Sprintf(rediskey.TM_MEMBER_DAILY_COUNT, date, apiKey, req.Platform), 3*24*time.Hour) redishelper.DefaultRedis.Expire(fmt.Sprintf(rediskey.TM_MEMBER_DAILY_COUNT, date, apiKey, req.Platform), 3*24*time.Hour)
} else { } else {
tmMemberService.RefundQuote(ctx, apiKey, req.Platform, &decyDatas)
code = statuscode.ServerError code = statuscode.ServerError
} }

View File

@ -35,6 +35,7 @@ import (
"go-admin/common/storage" "go-admin/common/storage"
ext "go-admin/config" ext "go-admin/config"
"go-admin/utils/redishelper" "go-admin/utils/redishelper"
"go-admin/utils/utility"
) )
var ( var (
@ -177,6 +178,9 @@ func run() error {
} }
func initCommon() { func initCommon() {
//初始化雪花算法
utility.InitSnowflake()
redishelper.InitDefaultRedis(config.CacheConfig.Redis.Addr, config.CacheConfig.Redis.Password, config.CacheConfig.Redis.DB) redishelper.InitDefaultRedis(config.CacheConfig.Redis.Addr, config.CacheConfig.Redis.Password, config.CacheConfig.Redis.DB)
if err := redishelper.DefaultRedis.Ping(); err != nil { if err := redishelper.DefaultRedis.Ping(); err != nil {
@ -195,6 +199,7 @@ func initCommon() {
os.Exit(-1) os.Exit(-1)
} }
} }
var Router runtime.Router var Router runtime.Router

View File

@ -10,16 +10,19 @@ type Response struct {
} }
var ErrorMessage = map[int]string{ var ErrorMessage = map[int]string{
Success: "success", Success: "success",
Unauthorized: "unauthorized", Unauthorized: "unauthorized",
ServerError: "server error", ServerError: "server error",
NotFound: "not found", NotFound: "not found",
Forbidden: "forbidden", Forbidden: "forbidden",
InvalidParams: "invalid params", InvalidParams: "invalid params",
InSufficRemainChar: "insufficent remain char", InSufficRemainChar: "insufficent remain char",
PlatformNotSupport: "platform not support", PlatformNotSupport: "platform not support",
TransactionNotAvailable: "transaction not available", TransactionNotAvailable: "transaction not available",
ApiUnauthorized: "api unauthorized", ApiUnauthorized: "api unauthorized",
NotFindMember: "not find member",
NotFindApiKey: "not find api key",
MemberPlatformNotSupport: "member platform not support",
} }
const ( const (
@ -37,4 +40,8 @@ const (
PlatformNotSupport = 20002 //平台不支持 PlatformNotSupport = 20002 //平台不支持
TransactionNotAvailable = 20003 //翻译服务不可用 TransactionNotAvailable = 20003 //翻译服务不可用
ApiUnauthorized = 20004 //api禁止访问 ApiUnauthorized = 20004 //api禁止访问
NotFindMember = 30001 //未找到用户
NotFindApiKey = 30002 //未找到api key
MemberPlatformNotSupport = 30003 //用户平台不支持
) )

1
go.mod
View File

@ -126,6 +126,7 @@ require (
github.com/shamsher31/goimgext v1.0.0 // indirect github.com/shamsher31/goimgext v1.0.0 // indirect
github.com/shopspring/decimal v1.4.0 // indirect github.com/shopspring/decimal v1.4.0 // indirect
github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
github.com/sony/sonyflake v1.2.1 // indirect
github.com/spf13/cast v1.3.1 // indirect github.com/spf13/cast v1.3.1 // indirect
github.com/spf13/pflag v1.0.3 // indirect github.com/spf13/pflag v1.0.3 // indirect
github.com/tklauser/go-sysconf v0.3.9 // indirect github.com/tklauser/go-sysconf v0.3.9 // indirect

View File

@ -51,6 +51,14 @@ func NewRedisHelper(addr, password string, db int) *RedisHelper {
} }
} }
func (r *RedisHelper) GetClient() *redis.Client {
return r.client
}
func (r *RedisHelper) GetCtx() context.Context {
return r.ctx
}
// 测试连接 // 测试连接
func (r *RedisHelper) Ping() error { func (r *RedisHelper) Ping() error {
return r.client.Ping(r.ctx).Err() return r.client.Ping(r.ctx).Err()
@ -100,6 +108,19 @@ func (r *RedisHelper) SetAdd(key, value string, expireTime time.Duration) error
} }
} }
// 更新zset score
func (r *RedisHelper) ZUpdateScore(key string, score float64, value string) error {
return r.client.ZAddArgs(r.ctx, key, redis.ZAddArgs{
XX: true, // 只更新已存在
Members: []redis.Z{
{
Score: float64(score),
Member: value,
},
},
}).Err()
}
// 设置对象 // 设置对象
func SetObjString[T any](r *RedisHelper, key string, value T) error { func SetObjString[T any](r *RedisHelper, key string, value T) error {
keyValue, err := sonic.Marshal(value) keyValue, err := sonic.Marshal(value)
@ -736,6 +757,28 @@ func (e *RedisHelper) GetRevRangeScoresSortSet(key string) ([]redis.Z, error) {
return e.client.ZRevRangeWithScores(e.ctx, key, 0, -1).Result() return e.client.ZRevRangeWithScores(e.ctx, key, 0, -1).Result()
} }
// ZSET 中按 score 范围取出成员
func (r *RedisHelper) ZRangeByScore(key string, min, max string) ([]string, error) {
ctx := context.Background()
return r.client.ZRangeByScore(ctx, key, &redis.ZRangeBy{
Min: min,
Max: max,
}).Result()
}
// ZSET 中移除指定成员:
func (r *RedisHelper) ZRemValues(key string, members ...string) error {
ctx := context.Background()
// 转换为 interface{} 类型参数
vals := make([]interface{}, len(members))
for i, m := range members {
vals[i] = m
}
return r.client.ZRem(ctx, key, vals...).Err()
}
// 获取最后一条数据 // 获取最后一条数据
func (e *RedisHelper) GetLastSortSet(key string) ([]redis.Z, error) { func (e *RedisHelper) GetLastSortSet(key string) ([]redis.Z, error) {
// 获取最后一个元素及其分数 // 获取最后一个元素及其分数

View File

@ -1,6 +1,7 @@
package utility package utility
import ( import (
"strconv"
"strings" "strings"
"github.com/rs/xid" "github.com/rs/xid"
@ -14,6 +15,7 @@ import (
"time" "time"
log "github.com/go-admin-team/go-admin-core/logger" log "github.com/go-admin-team/go-admin-core/logger"
"github.com/sony/sonyflake"
) )
const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
@ -79,3 +81,20 @@ func GenerateBase62Key(length int) (string, error) {
return b.String(), nil return b.String(), nil
} }
var sf *sonyflake.Sonyflake
func InitSnowflake() {
sf = sonyflake.NewSonyflake(sonyflake.Settings{})
if sf == nil {
log.Fatalf("Failed to initialize sonyflake")
}
}
func GenerateTraceID() string {
id, err := sf.NextID()
if err != nil {
log.Fatalf("Failed to generate ID: %v", err)
}
return strconv.FormatUint(id, 10)
}