feat: 添加批量生成CDK功能
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
All checks were successful
ci/woodpecker/push/my-first-workflow Pipeline was successful
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
76
modules/config/service/cdk_test.go
Normal file
76
modules/config/service/cdk_test.go
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user