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(¤tShow, 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 }