Files
bl/modules/config/service/cdk.go
xinian 90f1447d48
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
refactor: 重构服务器冠名逻辑至独立表
2026-04-10 19:36:59 +08:00

352 lines
8.9 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package service
import (
"blazing/cool"
"blazing/modules/config/model"
"context"
"crypto/rand"
"fmt"
"math/big"
"time"
"github.com/gogf/gf/v2/database/gdb"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/frame/g"
"github.com/gogf/gf/v2/util/gconv"
"github.com/google/uuid"
)
const charsetWithSymbol = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz"
const (
CDKTypeReward uint32 = 0
CDKTypeServerNaming uint32 = 1
cdkCodeLength = 16
maxBatchGenerateCount = 5000
)
var charsetWithSymbolSize = big.NewInt(int64(len(charsetWithSymbol)))
func Generate16CharSecure() string {
code, err := generateSecureCode(cdkCodeLength)
if err == nil {
return code
}
fallback := fmt.Sprintf("%x", uuid.New())
if len(fallback) >= cdkCodeLength {
return fallback[:cdkCodeLength]
}
return fmt.Sprintf("%-16s", fallback)
}
func generateSecureCode(length int) (string, error) {
if length <= 0 {
return "", nil
}
result := make([]byte, length)
for i := 0; i < length; i++ {
index, err := rand.Int(rand.Reader, charsetWithSymbolSize)
if err != nil {
return "", err
}
result[i] = charsetWithSymbol[index.Int64()]
}
return string(result), nil
}
func buildUniqueCodes(count int, generator func() (string, error), exists func([]string) (map[string]struct{}, error)) ([]string, error) {
if count <= 0 {
return nil, nil
}
codes := make([]string, 0, count)
selected := make(map[string]struct{}, count)
for len(codes) < count {
remaining := count - len(codes)
batchSize := remaining * 2
if batchSize < remaining+4 {
batchSize = remaining + 4
}
candidates := make([]string, 0, batchSize)
candidateSet := make(map[string]struct{}, batchSize)
for len(candidates) < batchSize {
code, err := generator()
if err != nil {
return nil, err
}
if _, ok := selected[code]; ok {
continue
}
if _, ok := candidateSet[code]; ok {
continue
}
candidateSet[code] = struct{}{}
candidates = append(candidates, code)
}
existing, err := exists(candidates)
if err != nil {
return nil, err
}
for _, code := range candidates {
if _, ok := existing[code]; ok {
continue
}
selected[code] = struct{}{}
codes = append(codes, code)
if len(codes) == count {
break
}
}
}
return codes, nil
}
type CdkService struct {
*cool.Service
}
func NewCdkService() *CdkService {
return &CdkService{
&cool.Service{
Model: model.NewCDKConfig(),
InsertParam: func(ctx context.Context) g.MapStrAny {
uuid.NewV7()
return g.MapStrAny{
"cdk_code": Generate16CharSecure(),
}
},
},
}
}
func (s *CdkService) Get(id string) *model.CDKConfig {
var item *model.CDKConfig
dbm_notenable(s.Model).Where("cdk_code", id).WhereNot("exchange_remain_count", 0).Scan(&item)
return item
}
func (s *CdkService) GetByID(id uint32) *model.CDKConfig {
if id == 0 {
return nil
}
var item *model.CDKConfig
dbm_notenable(s.Model).Where("id", id).Scan(&item)
return item
}
func (s *CdkService) All() []model.CDKConfig {
var item []model.CDKConfig
dbm_notenable(s.Model).WhereLT("exchange_remain_count", 0).Scan(&item)
return item
}
func (s *CdkService) Set(id string) bool {
res, err := cool.DBM(s.Model).Where("cdk_code", id).WhereNot("exchange_remain_count", 0).Decrement("exchange_remain_count", 1)
if err != nil {
return false
}
rows, _ := res.RowsAffected()
if rows == 0 {
return false
}
return true
}
type ServerNamingCDKResult struct {
ServerID uint32
ServerName string
OwnerID uint32
ServerExpireTime time.Time
}
// UseServerNamingCDK 使用服务器冠名类型CDK并原子化更新服务器归属和到期时间。
func (s *CdkService) UseServerNamingCDK(ctx context.Context, code string, ownerID, serverID uint32, serverName string) (*ServerNamingCDKResult, error) {
if ctx == nil {
ctx = context.TODO()
}
now := time.Now()
serverService := NewServerService()
var updated model.ServerShow
err := g.DB(s.Model.GroupName()).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
var cfg model.CDKConfig
if err := tx.Model(s.Model).Where("cdk_code", code).WhereNot("exchange_remain_count", 0).Scan(&cfg); err != nil {
return err
}
if cfg.ID == 0 {
return gerror.New("cdk不存在")
}
if cfg.CDKType != CDKTypeServerNaming {
return gerror.New("cdk类型不匹配")
}
if cfg.BindUserId != 0 && cfg.BindUserId != ownerID {
return gerror.New("cdk已绑定其他用户")
}
if !cfg.ValidEndTime.IsZero() && cfg.ValidEndTime.Before(now) {
return gerror.New("cdk已过期")
}
var server model.ServerList
if err := tx.Model(model.NewServerList()).With(model.ServerShow{}).Where("online_id", serverID).Scan(&server); err != nil {
return err
}
if server.OnlineID == 0 {
return gerror.New("服务器不存在")
}
if !serverService.CanUseDonationName(server, ownerID, now) {
return gerror.New("服务器不可冠名")
}
res, err := tx.Model(s.Model).Where("cdk_code", code).WhereNot("exchange_remain_count", 0).Decrement("exchange_remain_count", 1)
if err != nil {
return err
}
rows, _ := res.RowsAffected()
if rows == 0 {
return gerror.New("cdk已被使用")
}
var currentShow model.ServerShow
if server.ServerShow != nil {
currentShow = *server.ServerShow
}
updated = currentShow
updated.ServerID = serverID
updated.Name = serverName
updated.Owner = ownerID
if currentShow.ServerID == 0 || !serverService.isActiveServerShow(&currentShow, now) || currentShow.Owner != ownerID {
updated.ExpireTime = now.AddDate(0, 1, 0)
} else {
baseTime := currentShow.ExpireTime
if baseTime.IsZero() {
baseTime = now
}
updated.ExpireTime = baseTime.AddDate(0, 1, 0)
}
if currentShow.ServerID == 0 {
_, err = tx.Model(model.NewServerShow()).Data(g.Map{
"server_id": updated.ServerID,
"name": updated.Name,
"owner": updated.Owner,
"expire_time": updated.ExpireTime,
}).Insert()
return err
}
_, err = tx.Model(model.NewServerShow()).Where("server_id", serverID).Data(g.Map{
"name": updated.Name,
"owner": updated.Owner,
"expire_time": updated.ExpireTime,
}).Update()
return err
})
if err != nil {
return nil, err
}
g.DB(s.Model.GroupName()).GetCore().ClearCache(context.TODO(), s.Model.TableName())
g.DB(model.NewServerList().GroupName()).GetCore().ClearCache(context.TODO(), model.NewServerList().TableName())
g.DB(model.NewServerShow().GroupName()).GetCore().ClearCache(context.TODO(), model.NewServerShow().TableName())
return &ServerNamingCDKResult{
ServerID: updated.ServerID,
ServerName: updated.Name,
OwnerID: updated.Owner,
ServerExpireTime: updated.ExpireTime,
}, nil
}
func (s *CdkService) BatchGenerate(ctx context.Context, count int) (interface{}, error) {
if count <= 0 {
return nil, gerror.New("生成数量必须大于0")
}
if count > maxBatchGenerateCount {
return nil, gerror.Newf("单次最多生成%d个CDK", maxBatchGenerateCount)
}
requestData := g.RequestFromCtx(ctx).GetMap()
delete(requestData, "count")
delete(requestData, "cdk_code")
delete(requestData, "id")
cdkType := gconv.Uint32(requestData["type"])
if cdkType != CDKTypeReward && cdkType != CDKTypeServerNaming {
return nil, gerror.New("CDK类型非法")
}
requestData["type"] = cdkType
if cdkType == CDKTypeServerNaming {
requestData["exchange_remain_count"] = int64(1)
requestData["item_reward_ids"] = []uint32{}
requestData["elf_reward_ids"] = []uint32{}
requestData["title_reward_ids"] = uint32(0)
}
codes, err := s.generateAvailableCodes(count)
if err != nil {
return nil, err
}
insertData := make(g.List, 0, len(codes))
for _, code := range codes {
row := g.Map{}
for key, value := range requestData {
row[key] = value
}
row["cdk_code"] = code
insertData = append(insertData, row)
}
err = g.DB(s.Model.GroupName()).Transaction(ctx, func(ctx context.Context, tx gdb.TX) error {
_, err := tx.Model(s.Model).Data(insertData).Insert()
return err
})
if err != nil {
return nil, err
}
g.DB(s.Model.GroupName()).GetCore().ClearCache(context.TODO(), s.Model.TableName())
return g.Map{
"count": count,
"codes": codes,
}, nil
}
func (s *CdkService) generateAvailableCodes(count int) ([]string, error) {
return buildUniqueCodes(count, func() (string, error) {
return generateSecureCode(cdkCodeLength)
}, func(candidates []string) (map[string]struct{}, error) {
return s.findExistingCodeSet(candidates)
})
}
func (s *CdkService) findExistingCodeSet(candidates []string) (map[string]struct{}, error) {
existingSet := make(map[string]struct{})
if len(candidates) == 0 {
return existingSet, nil
}
var records []struct {
CDKCode string `json:"cdk_code"`
}
if err := dbm_nocache_noenable(s.Model).Fields("cdk_code").WhereIn("cdk_code", candidates).Scan(&records); err != nil {
return nil, err
}
for _, record := range records {
existingSet[record.CDKCode] = struct{}{}
}
return existingSet, nil
}