From 9825944efc125d4831377ae481e534b07f2dfea5 Mon Sep 17 00:00:00 2001 From: xinian Date: Wed, 8 Apr 2026 14:17:10 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=89=B9=E9=87=8F?= =?UTF-8?q?=E7=94=9F=E6=88=90CDK=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/config/controller/admin/cdk.go | 16 +++ modules/config/service/cdk.go | 165 ++++++++++++++++++++++++- modules/config/service/cdk_test.go | 76 ++++++++++++ 3 files changed, 252 insertions(+), 5 deletions(-) create mode 100644 modules/config/service/cdk_test.go diff --git a/modules/config/controller/admin/cdk.go b/modules/config/controller/admin/cdk.go index 238d98c23..223e29451 100644 --- a/modules/config/controller/admin/cdk.go +++ b/modules/config/controller/admin/cdk.go @@ -3,6 +3,9 @@ package admin import ( "blazing/cool" "blazing/modules/config/service" + "context" + + "github.com/gogf/gf/v2/frame/g" ) type CdkController struct { @@ -20,3 +23,16 @@ func init() { }, }) } + +type BatchGenerateReq struct { + g.Meta `path:"/batchGenerate" method:"POST"` + Count int `json:"count" v:"required|min:1#请输入正确的生成数量"` +} + +func (c *CdkController) BatchGenerate(ctx context.Context, req *BatchGenerateReq) (res *cool.BaseRes, err error) { + data, err := service.NewCdkService().BatchGenerate(ctx, req.Count) + if err != nil { + return nil, err + } + return cool.Ok(data), nil +} diff --git a/modules/config/service/cdk.go b/modules/config/service/cdk.go index 66465e219..b84d8e793 100644 --- a/modules/config/service/cdk.go +++ b/modules/config/service/cdk.go @@ -4,20 +4,104 @@ import ( "blazing/cool" "blazing/modules/config/model" "context" + "crypto/rand" + "fmt" + "math/big" + "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/grand" "github.com/google/uuid" ) const charsetWithSymbol = "0123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz" +const ( + cdkCodeLength = 16 + maxBatchGenerateCount = 5000 +) + +var charsetWithSymbolSize = big.NewInt(int64(len(charsetWithSymbol))) + func Generate16CharSecure() string { - result := make([]byte, 16) - for i := 0; i < 16; i++ { - result[i] = charsetWithSymbol[grand.N(0, len(charsetWithSymbol)-1)] + code, err := generateSecureCode(cdkCodeLength) + if err == nil { + return code } - return string(result) + + 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 { @@ -71,3 +155,74 @@ func (s *CdkService) Set(id string) bool { } return true } + +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") + + 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 +} diff --git a/modules/config/service/cdk_test.go b/modules/config/service/cdk_test.go new file mode 100644 index 000000000..344ccee47 --- /dev/null +++ b/modules/config/service/cdk_test.go @@ -0,0 +1,76 @@ +package service + +import ( + "errors" + "testing" +) + +func TestGenerate16CharSecure(t *testing.T) { + for i := 0; i < 100; i++ { + code := Generate16CharSecure() + if len(code) != cdkCodeLength { + t.Fatalf("expected code length %d, got %d", cdkCodeLength, len(code)) + } + + for _, ch := range code { + if !containsRune(charsetWithSymbol, ch) { + t.Fatalf("unexpected character %q in code %q", ch, code) + } + } + } +} + +func TestBuildUniqueCodesSkipsDuplicatesAndExisting(t *testing.T) { + sequence := []string{ + "DUPLICATE-000001", + "DUPLICATE-000001", + "EXISTING-000001", + "VALID-000000001", + "VALID-000000002", + "VALID-000000003", + "VALID-000000004", + "VALID-000000005", + "VALID-000000006", + } + index := 0 + + codes, err := buildUniqueCodes(3, func() (string, error) { + if index >= len(sequence) { + return "", errors.New("generator exhausted") + } + code := sequence[index] + index++ + return code, nil + }, func(candidates []string) (map[string]struct{}, error) { + return map[string]struct{}{ + "EXISTING-000001": {}, + }, nil + }) + if err != nil { + t.Fatalf("buildUniqueCodes returned error: %v", err) + } + + if len(codes) != 3 { + t.Fatalf("expected 3 codes, got %d", len(codes)) + } + + want := []string{ + "VALID-000000001", + "VALID-000000002", + "VALID-000000003", + } + for i, code := range want { + if codes[i] != code { + t.Fatalf("expected code %q at index %d, got %q", code, i, codes[i]) + } + } +} + +func containsRune(value string, target rune) bool { + for _, ch := range value { + if ch == target { + return true + } + } + return false +}