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
				
			
		
		
	
	
				
					
				
			
		
			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:
		
							
								
								
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @ -21,3 +21,4 @@ cmd/migrate/migration/version-local/* | ||||
| # go sum | ||||
| go.sum | ||||
| config/settings.deva.yml | ||||
| /aggregate_translate_server | ||||
|  | ||||
										
											Binary file not shown.
										
									
								
							| @ -222,3 +222,29 @@ func (e TmMemberPlatform) GetStatistic(c *gin.Context) { | ||||
|  | ||||
| 	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, "修改成功") | ||||
| } | ||||
|  | ||||
							
								
								
									
										131
									
								
								app/admin/apis/tm_recharge_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										131
									
								
								app/admin/apis/tm_recharge_log.go
									
									
									
									
									
										Normal 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") | ||||
| } | ||||
| @ -42,7 +42,7 @@ func (e *Translate) Translate(c *gin.Context) { | ||||
|  | ||||
| 	if code != statuscode.Success { | ||||
| 		e.Logger.Error(err) | ||||
| 		e.Error(code, nil, statuscode.ErrorMessage[code]) | ||||
| 		e.OK(code, statuscode.ErrorMessage[code]) | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
|  | ||||
| @ -10,6 +10,7 @@ type TmMemberPlatform struct { | ||||
| 	MemberId           int    `json:"memberId" gorm:"type:bigint;comment:用户id"` | ||||
| 	RemainingCharacter int    `json:"remainingCharacter" 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"` | ||||
| 	PlatformKey        string `json:"platformKey" gorm:"type:varchar(50);comment:平台key"` | ||||
| 	Status             int    `json:"status" gorm:"type:tinyint;comment:状态 1-启用 2-禁用"` | ||||
|  | ||||
| @ -1,14 +1,28 @@ | ||||
| package models | ||||
|  | ||||
| import "go-admin/common/models" | ||||
| import ( | ||||
| 	"go-admin/common/models" | ||||
| 	"time" | ||||
|  | ||||
| 	"github.com/shopspring/decimal" | ||||
| ) | ||||
|  | ||||
| type TmRechargeLog struct { | ||||
| 	models.Model | ||||
|  | ||||
| 	Type           int             `json:"type" gorm:"column:type;type:tinyint;not null;comment:类型 1-充值 2-后台充值 3-赠送"` | ||||
| 	OrderNo        string          `json:"orderNo" gorm:"column:order_no;type:varchar(36);not null;comment:订单号"` | ||||
| 	UserId         int             `json:"userId" gorm:"column:user_id;type:bigint;not null;comment:用户id"` | ||||
| 	MemberId       int             `json:"memberId" gorm:"column:member_id;type:bigint;not null;comment:翻译用户id"` | ||||
| 	Status     int `json:"status" gorm:"column:status;type:tinyint;not null;comment:状态 1-正常 2-作废"` | ||||
| 	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.ControlBy | ||||
| } | ||||
|  | ||||
| @ -16,6 +16,7 @@ func init() { | ||||
| // registerTmMemberRouter | ||||
| func registerTmMemberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddleware) { | ||||
| 	api := apis.TmMember{} | ||||
| 	rechargeApi := apis.TmRechargeLog{} | ||||
| 	r := v1.Group("/tm-member").Use(authMiddleware.MiddlewareFunc()).Use(middleware.AuthCheckRole()) | ||||
| 	{ | ||||
| 		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.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) //状态变更 | ||||
| 	} | ||||
|  | ||||
| @ -32,5 +34,6 @@ func registerTmMemberRouter(v1 *gin.RouterGroup, authMiddleware *jwt.GinJWTMiddl | ||||
| 	{ | ||||
| 		r2.GET("/api-key", api.GetMyApiKey) | ||||
| 		r2.GET("platforms", api.GetPlatforms) | ||||
| 		r2.GET("member-advent", api.GetMemberAdvent) //获取用户即将过期的充值信息 | ||||
| 	} | ||||
| } | ||||
|  | ||||
| @ -23,6 +23,7 @@ func registerTmMemberPlatformRouter(v1 *gin.RouterGroup, authMiddleware *jwt.Gin | ||||
| 		r.POST("", api.Insert) | ||||
| 		r.PUT("/:id", actions.PermissionAction(), api.Update) | ||||
| 		r.DELETE("", api.Delete) | ||||
| 		r.POST("/change-chars", actions.PermissionAction(), api.ChangeChars) | ||||
| 	} | ||||
|  | ||||
| 	f2 := v1.Group("/tm-member-platform").Use(authMiddleware.MiddlewareFunc()) | ||||
|  | ||||
| @ -130,11 +130,14 @@ type TmMemberResp struct { | ||||
| 	RemainChars  int                    `json:"remainChars"` | ||||
| 	CreatedAt    time.Time              `json:"createdAt"` | ||||
| 	Platforms    []TmMemberPlatformResp `json:"platforms"` | ||||
| 	UsedPlatform []TmMemberPlatformResp `json:"usedPlatform"` | ||||
| } | ||||
|  | ||||
| type TmMemberPlatformResp struct { | ||||
| 	PlatformId int    `json:"platformId"` | ||||
| 	MemberId   int    `json:"memberId"` | ||||
| 	Name       string `json:"name"` | ||||
| 	RemainChars int    `json:"remainChars"` | ||||
| 	TotalChars int    `json:"totalChars"` | ||||
| } | ||||
|  | ||||
| type TmMemberRechargeReq struct { | ||||
| @ -175,9 +178,11 @@ func (e *TmMemberChangeStatusReq) Validate() error { | ||||
|  | ||||
| type TmMemberPlatformFrontedResp struct { | ||||
| 	Id          int    `json:"id"` | ||||
| 	PlatformId  int    `json:"platformId"` | ||||
| 	Name        string `json:"name"` | ||||
| 	RemainChars int    `json:"remainChars"` | ||||
| 	TotalChars  int    `json:"totalChars"` | ||||
| 	UseChars    int    `json:"useChars"` | ||||
| 	Price       int    `json:"price"` | ||||
| 	ApiKey      string `json:"apiKey"` | ||||
| } | ||||
|  | ||||
| @ -99,3 +99,10 @@ type TmMemberDailyUsageDeleteReq struct { | ||||
| func (s *TmMemberDailyUsageDeleteReq) GetId() interface{} { | ||||
| 	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:"总使用量"` | ||||
| } | ||||
|  | ||||
| @ -109,3 +109,11 @@ type TmMemberPlatformStatisticItemResp struct { | ||||
| 	PlatformId   int    `json:"platformId"` | ||||
| 	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 | ||||
| } | ||||
|  | ||||
							
								
								
									
										165
									
								
								app/admin/service/dto/tm_recharge_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								app/admin/service/dto/tm_recharge_log.go
									
									
									
									
									
										Normal 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"` | ||||
| } | ||||
							
								
								
									
										57
									
								
								app/admin/service/quota_manager/lua/deduct.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										57
									
								
								app/admin/service/quota_manager/lua/deduct.lua
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										21
									
								
								app/admin/service/quota_manager/lua/refund.lua
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								app/admin/service/quota_manager/lua/refund.lua
									
									
									
									
									
										Normal 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 | ||||
							
								
								
									
										229
									
								
								app/admin/service/quota_manager/quota_manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										229
									
								
								app/admin/service/quota_manager/quota_manager.go
									
									
									
									
									
										Normal 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 | ||||
| } | ||||
							
								
								
									
										80
									
								
								app/admin/service/quota_manager/quota_manager_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								app/admin/service/quota_manager/quota_manager_test.go
									
									
									
									
									
										Normal 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()) | ||||
| } | ||||
| @ -82,7 +82,7 @@ func (e *SysUser) Insert(c *dto.SysUserInsertReq) error { | ||||
| 	e.Orm.Model(role).Where("role_id = ?", c.RoleId).Find(&role) | ||||
|  | ||||
| 	if role.RoleId == 0 { | ||||
| 		err = errors.New("角色不存在") | ||||
| 		return errors.New("角色不存在") | ||||
| 	} | ||||
|  | ||||
| 	err = e.Orm.Model(&data).Where("username = ?", c.Username).Count(&i).Error | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"errors" | ||||
| 	"fmt" | ||||
| 	"strconv" | ||||
| @ -9,12 +10,12 @@ import ( | ||||
|  | ||||
| 	"github.com/bytedance/sonic" | ||||
| 	"github.com/go-admin-team/go-admin-core/sdk/service" | ||||
| 	"github.com/go-redis/redis/v8" | ||||
| 	"github.com/jinzhu/copier" | ||||
| 	"gorm.io/gorm" | ||||
|  | ||||
| 	"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" | ||||
| 	rediskey "go-admin/common/redis_key" | ||||
| @ -31,6 +32,7 @@ func (e TmMember) GetUserPlatforms(userId int, resp *[]dto.TmMemberPlatformFront | ||||
| 	var memberAccount models.TmMember | ||||
|  | ||||
| 	if err := e.Orm.Model(&memberAccount). | ||||
| 		Where("user_id=?", userId). | ||||
| 		Find(&memberAccount).Error; err != nil { | ||||
| 		return err | ||||
| 	} | ||||
| @ -51,8 +53,10 @@ func (e TmMember) GetUserPlatforms(userId int, resp *[]dto.TmMemberPlatformFront | ||||
|  | ||||
| 		dataItem.ApiKey = memberAccount.ApiKey | ||||
| 		dataItem.Name = platform.ShowName | ||||
| 		dataItem.PlatformId = platform.Id | ||||
| 		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) | ||||
| 	} | ||||
| @ -82,25 +86,30 @@ func (e *TmMember) GetPage(c *dto.TmMemberGetPageReq, p *actions.DataPermission, | ||||
| 	} | ||||
|  | ||||
| 	userIds := []int{} | ||||
| 	memberIds := []int{} | ||||
|  | ||||
| 	for _, item := range datas { | ||||
| 		if !utility.ContainsInt(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} | ||||
| 	userService := SysUser{Service: e.Service} | ||||
| 	users, _ := userService.GetByIds(userIds) | ||||
| 	activeList, _ := platformService.GetActiveList() | ||||
| 	memberPlatforms, _ := memberPlatformService.GetMemberList(memberIds) | ||||
|  | ||||
| 	for _, item := range datas { | ||||
| 		dataItem := dto.TmMemberResp{} | ||||
| 		copier.Copy(&dataItem, &item) | ||||
|  | ||||
| 		// count, _ := e.GetRemainCount(,dataItem.ApiKey) | ||||
| 		dataItem.ApiKey = utility.DesensitizeGeneric(dataItem.ApiKey, 2, 2, '*') | ||||
| 		// dataItem.RemainChars = count | ||||
|  | ||||
| 		for _, user := range users { | ||||
| 			if user.UserId == item.UserId { | ||||
| @ -108,14 +117,33 @@ func (e *TmMember) GetPage(c *dto.TmMemberGetPageReq, p *actions.DataPermission, | ||||
| 			} | ||||
| 		} | ||||
|  | ||||
| 		//剩余字符 | ||||
| 		for _, platform := range activeList { | ||||
| 			platformItem := dto.TmMemberPlatformResp{} | ||||
| 			platformItem.PlatformId = platform.Id | ||||
| 			platformItem.MemberId = item.Id | ||||
| 			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) | ||||
| 		} | ||||
|  | ||||
| 		//已用字符 | ||||
| 		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) | ||||
| 	} | ||||
|  | ||||
| @ -190,13 +218,11 @@ func (e *TmMember) SaveAllCache() error { | ||||
| 	} | ||||
| 	for _, item := range list { | ||||
| 		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) | ||||
| 		if err != nil { | ||||
| 			return err | ||||
| 		} | ||||
| 		redishelper.DefaultRedis.SetString(key, val) | ||||
| 		// redishelper.DefaultRedis.SetString(remainKey, strconv.Itoa(item.RemainChars)) | ||||
| 	} | ||||
| 	return nil | ||||
| } | ||||
| @ -262,42 +288,77 @@ func (e *TmMember) RemoveByUserIds(userIds []int) error { | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| // SyncMemberRemain 同步剩余字符 | ||||
| // SyncMemberRemain 同步剩余字符(ZSET 多笔充值版) | ||||
| 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 { | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	members := e.getCacheMembers() | ||||
| 	dailyUsageService := TmMemberDailyUsage{Service: e.Service} | ||||
| 	datas := make([]models.TmMemberPlatform, 0) | ||||
| 	now := time.Now().Unix() | ||||
|  | ||||
| 	for _, key := range scanKeys { | ||||
| 		items := strings.Split(key, ":") | ||||
| 		apiKey := items[len(items)-2] | ||||
| 		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) | ||||
| 		if len(items) < 3 { | ||||
| 			continue | ||||
| 		} | ||||
|  | ||||
| 		apiKey := items[len(items)-2] | ||||
| 		platform := items[len(items)-1] | ||||
|  | ||||
| 		// 清除过期的 quotaID(ZSET + 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 { | ||||
| 			item := models.TmMemberPlatform{} | ||||
| 			item.Id = member.Id | ||||
| 			item.RemainingCharacter = remainCount | ||||
| 			item.PlatformKey = platform | ||||
| 			datas = append(datas, item) | ||||
| 			memberPlatform := models.TmMemberPlatform{ | ||||
| 				PlatformKey:        platform, | ||||
| 				RemainingCharacter: totalRemain, | ||||
| 			} | ||||
| 			memberPlatform.Id = member.Id | ||||
| 			datas = append(datas, memberPlatform) | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 批量更新数据库(分批,每批最多 1000 条) | ||||
| 	arrayData := utility.SplitSlice(datas, 1000) | ||||
| 	for _, dataBatch := range arrayData { | ||||
|  | ||||
| 		// 遍历当前批次的所有记录,为每条记录单独执行 UPDATE | ||||
| 		for _, record := range dataBatch { | ||||
| 			stmt := ` | ||||
| 				UPDATE tm_member_platform | ||||
| @ -312,14 +373,16 @@ func (e *TmMember) SyncMemberRemain() error { | ||||
| 				record.Id, | ||||
| 			} | ||||
|  | ||||
| 			// 执行单个 UPDATE 语句 | ||||
| 			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", record.PlatformKey, record.Id, err) | ||||
| 				e.Log.Errorf("TmMemberService SyncMemberRemain single Exec for PlatformKey %s, MemberID %d error: %s \r\n", | ||||
| 					record.PlatformKey, record.Id, err) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	// 同步每日使用量(不变) | ||||
| 	dailyUsageService.SyncTotalUse() | ||||
|  | ||||
| 	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 获取翻译统计 | ||||
| func (e *TmMember) GetTranslateStatistic(userId int, list *[]dto.TranslateStatisticResp) error { | ||||
| 	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) { | ||||
| 	key := fmt.Sprintf(rediskey.TM_MEMBER_REMAIN_COUNT, apiKey, platformKey) | ||||
| 	val, err := redishelper.DefaultRedis.GetString(key) | ||||
| 	result := 0 | ||||
| 	ctx := context.Background() | ||||
| 	quotaManager := quota_manager.NewQuotaManager(redishelper.DefaultRedis.GetClient()) | ||||
| 	result, err := quotaManager.GetTotalRemainingQuota(ctx, apiKey, platformKey) | ||||
|  | ||||
| 	if err != nil && !errors.Is(err, redis.Nil) { | ||||
| 		return 0, err | ||||
| 	} | ||||
|  | ||||
| 	if val != "" { | ||||
| 		result, err = strconv.Atoi(val) | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| 	} | ||||
| 	} | ||||
|  | ||||
| 	if result == 0 { | ||||
| 		var data models.TmMember | ||||
| 	// if result == 0 { | ||||
| 	// 	var data models.TmMember | ||||
|  | ||||
| 		if err := e.Orm.Model(&data).Where("api_key = ?", apiKey).First(&data).Error; err != nil { | ||||
| 			return 0, err | ||||
| 		} | ||||
| 	// 	if err := e.Orm.Model(&data).Where("api_key = ?", apiKey).First(&data).Error; err != nil { | ||||
| 	// 		return 0, err | ||||
| 	// 	} | ||||
|  | ||||
| 		result = data.RemainChars | ||||
| 		redishelper.DefaultRedis.SetString(key, strconv.Itoa(result)) | ||||
| 	} | ||||
| 	// 	result = data.RemainChars | ||||
| 	// 	// redishelper.DefaultRedis.SetString(key, strconv.Itoa(result)) | ||||
| 	// } | ||||
|  | ||||
| 	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() | ||||
| } | ||||
|  | ||||
| // 增加字符 | ||||
| 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获取数据 | ||||
| func (e *TmMember) GetById(id int, data *models.TmMember) error { | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| // 根据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 { | ||||
| 	var err error | ||||
| @ -638,7 +734,6 @@ func (e *TmMember) SyncInsert(req *dto.TmMemberSyncInsertReq, entity *models.TmM | ||||
| 	platformService := TmPlatform{Service: e.Service} | ||||
| 	activePlatforms, _ := platformService.GetActiveList() | ||||
| 	TmMemberPlatforms := make([]models.TmMemberPlatform, 0) | ||||
|  | ||||
| 	copier.Copy(entity, req) | ||||
|  | ||||
| 	apiKey, err := utility.GenerateBase62Key(32) | ||||
| @ -661,8 +756,8 @@ func (e *TmMember) SyncInsert(req *dto.TmMemberSyncInsertReq, entity *models.TmM | ||||
| 			PlatformId:         platform.Id, | ||||
| 			PlatformKey:        platform.Code, | ||||
| 			Status:             1, | ||||
| 			TotalCharacter:     10000, | ||||
| 			RemainingCharacter: 10000, | ||||
| 			TotalCharacter:     0, | ||||
| 			RemainingCharacter: 0, | ||||
| 		} | ||||
|  | ||||
| 		TmMemberPlatforms = append(TmMemberPlatforms, item) | ||||
| @ -672,9 +767,10 @@ func (e *TmMember) SyncInsert(req *dto.TmMemberSyncInsertReq, entity *models.TmM | ||||
| 		return err | ||||
| 	} | ||||
|  | ||||
| 	for _, platform := range TmMemberPlatforms { | ||||
| 		e.IncrBy(platform.PlatformKey, entity.ApiKey, platform.RemainingCharacter) | ||||
| 	} | ||||
| 	// for _, platform := range TmMemberPlatforms { | ||||
| 	// 	// e.IncrBy(platform.PlatformKey, entity.ApiKey, platform.RemainingCharacter) | ||||
| 	// 	e.IncyByQuote(ctx,platform.PlatformKey,entity.ApiKey,platform.TotalCharacter,,) | ||||
| 	// } | ||||
|  | ||||
| 	return nil | ||||
| } | ||||
|  | ||||
| @ -128,7 +128,7 @@ func (e *TmMemberDailyUsage) GetStatistic(userId int, resp *dto.TmMemberPlatform | ||||
|  | ||||
| 	if err := e.Orm.Model(models.TmMemberDailyUsage{}). | ||||
| 		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) | ||||
| 		return nil | ||||
| 	} | ||||
| @ -178,3 +178,24 @@ func (e *TmMemberDailyUsage) GetStatistic(userId int, resp *dto.TmMemberPlatform | ||||
| 	resp.Data = respDatas | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| @ -16,6 +16,42 @@ type TmMemberPlatform struct { | ||||
| 	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列表 | ||||
| func (e *TmMemberPlatform) GetPage(c *dto.TmMemberPlatformGetPageReq, p *actions.DataPermission, list *[]models.TmMemberPlatform, count *int64) error { | ||||
| 	var err error | ||||
| @ -107,3 +143,40 @@ func (e *TmMemberPlatform) Remove(d *dto.TmMemberPlatformDeleteReq, p *actions.D | ||||
| 	} | ||||
| 	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 | ||||
| } | ||||
|  | ||||
| @ -225,6 +225,17 @@ func (e *TmPlatform) GetByKey(code string) (*models.TmPlatform, error) { | ||||
| 	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) { | ||||
| 	var list []models.TmPlatform | ||||
| 	err := e.Orm.Model(&models.TmPlatform{}).Find(&list).Error | ||||
|  | ||||
							
								
								
									
										205
									
								
								app/admin/service/tm_recharge_log.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								app/admin/service/tm_recharge_log.go
									
									
									
									
									
										Normal 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) | ||||
| } | ||||
| @ -1,6 +1,7 @@ | ||||
| package service | ||||
|  | ||||
| import ( | ||||
| 	"context" | ||||
| 	"encoding/json" | ||||
| 	"fmt" | ||||
| 	"go-admin/app/admin/models" | ||||
| @ -95,6 +96,7 @@ func (s *TranslatorService) RegisterProvider(name string, provider Translator) { | ||||
| // 翻译校验 | ||||
| // return statusCode | ||||
| func (s *TranslatorService) TranslateJudge(req *dto.TranslateReq, apiKey string) (result *dto.TranslateResult, respCode int) { | ||||
| 	ctx := context.Background() | ||||
| 	tmMemberService := TmMember{Service: s.Service} | ||||
| 	tmPlatformAccount := TmPlatformAccount{Service: s.Service} | ||||
| 	memberInfo, err1 := tmMemberService.GetByKey(apiKey) | ||||
| @ -126,16 +128,23 @@ func (s *TranslatorService) TranslateJudge(req *dto.TranslateReq, apiKey string) | ||||
| 		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 { | ||||
| 		err2 := tmMemberService.DecrBy(req.Platform, apiKey, count) | ||||
| 		// err2 := tmMemberService.DecrBy(req.Platform, apiKey, count) | ||||
|  | ||||
| 		if err2 != nil { | ||||
| 			s.Log.Errorf("翻译计数失败:%v", err2) | ||||
| 			respCode = statuscode.ServerError | ||||
| 			return | ||||
| 		} | ||||
| 		// if err2 != nil { | ||||
| 		// 	s.Log.Errorf("翻译计数失败:%v", err2) | ||||
| 		// 	respCode = statuscode.ServerError | ||||
| 		// 	return | ||||
| 		// } | ||||
|  | ||||
| 		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) | ||||
| 	} else { | ||||
| 		tmMemberService.RefundQuote(ctx, apiKey, req.Platform, &decyDatas) | ||||
|  | ||||
| 		code = statuscode.ServerError | ||||
| 	} | ||||
|  | ||||
|  | ||||
| @ -35,6 +35,7 @@ import ( | ||||
| 	"go-admin/common/storage" | ||||
| 	ext "go-admin/config" | ||||
| 	"go-admin/utils/redishelper" | ||||
| 	"go-admin/utils/utility" | ||||
| ) | ||||
|  | ||||
| var ( | ||||
| @ -177,6 +178,9 @@ func run() error { | ||||
| } | ||||
|  | ||||
| func initCommon() { | ||||
| 	//初始化雪花算法 | ||||
| 	utility.InitSnowflake() | ||||
|  | ||||
| 	redishelper.InitDefaultRedis(config.CacheConfig.Redis.Addr, config.CacheConfig.Redis.Password, config.CacheConfig.Redis.DB) | ||||
|  | ||||
| 	if err := redishelper.DefaultRedis.Ping(); err != nil { | ||||
| @ -195,6 +199,7 @@ func initCommon() { | ||||
|  | ||||
| 		os.Exit(-1) | ||||
| 	} | ||||
|  | ||||
| } | ||||
|  | ||||
| var Router runtime.Router | ||||
|  | ||||
| @ -20,6 +20,9 @@ var ErrorMessage = map[int]string{ | ||||
| 	PlatformNotSupport:       "platform not support", | ||||
| 	TransactionNotAvailable:  "transaction not available", | ||||
| 	ApiUnauthorized:          "api unauthorized", | ||||
| 	NotFindMember:            "not find member", | ||||
| 	NotFindApiKey:            "not find api key", | ||||
| 	MemberPlatformNotSupport: "member platform not support", | ||||
| } | ||||
|  | ||||
| const ( | ||||
| @ -37,4 +40,8 @@ const ( | ||||
| 	PlatformNotSupport      = 20002 //平台不支持 | ||||
| 	TransactionNotAvailable = 20003 //翻译服务不可用 | ||||
| 	ApiUnauthorized         = 20004 //api禁止访问 | ||||
|  | ||||
| 	NotFindMember            = 30001 //未找到用户 | ||||
| 	NotFindApiKey            = 30002 //未找到api key | ||||
| 	MemberPlatformNotSupport = 30003 //用户平台不支持 | ||||
| ) | ||||
|  | ||||
							
								
								
									
										1
									
								
								go.mod
									
									
									
									
									
								
							
							
						
						
									
										1
									
								
								go.mod
									
									
									
									
									
								
							| @ -126,6 +126,7 @@ require ( | ||||
| 	github.com/shamsher31/goimgext v1.0.0 // indirect | ||||
| 	github.com/shopspring/decimal v1.4.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/pflag v1.0.3 // indirect | ||||
| 	github.com/tklauser/go-sysconf v0.3.9 // indirect | ||||
|  | ||||
| @ -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 { | ||||
| 	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 { | ||||
| 	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() | ||||
| } | ||||
|  | ||||
| // 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) { | ||||
| 	// 获取最后一个元素及其分数 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| package utility | ||||
|  | ||||
| import ( | ||||
| 	"strconv" | ||||
| 	"strings" | ||||
|  | ||||
| 	"github.com/rs/xid" | ||||
| @ -14,6 +15,7 @@ import ( | ||||
| 	"time" | ||||
|  | ||||
| 	log "github.com/go-admin-team/go-admin-core/logger" | ||||
| 	"github.com/sony/sonyflake" | ||||
| ) | ||||
|  | ||||
| const base62Chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" | ||||
| @ -79,3 +81,20 @@ func GenerateBase62Key(length int) (string, error) { | ||||
|  | ||||
| 	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) | ||||
| } | ||||
|  | ||||
		Reference in New Issue
	
	Block a user