feat(server): CreateBatch/生成器切换到 codes 库(Mint 单事务全成或全无)(#codes-lib)
Store 挂库 store(NewStore 内部构造 libcodes.Store,签名不变); generator.go 三函数委托 libcodes,ErrDuplicate 别名同一哨兵; Service.CreateBatch 走 libcodes.Mint(签名不变)。openMigratedSQLite 挪到共享 sqlite_helper_test.go,backfill_test.go/service_sqlite_test.go 共用。Store.CreateBatch/CreateCode/CodeExistsByHash 原样保留供 webhook.go(Task 6 改用新原语,Task 7 删除)。
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/redis/go-redis/v9"
|
||||
libcodes "github.com/wangjia/codes"
|
||||
"github.com/wangjia/pangolin/server/internal/apierr"
|
||||
)
|
||||
|
||||
@@ -322,60 +323,31 @@ type BatchResult struct {
|
||||
Channel BatchChannel
|
||||
}
|
||||
|
||||
// CreateBatch generates Count activation codes, writes the batch + code hashes
|
||||
// to MySQL, and returns the plaintext codes. The plaintext codes are the ONLY
|
||||
// time they ever appear; the caller is responsible for delivering them securely
|
||||
// (e.g., streaming to CSV without logging).
|
||||
//
|
||||
// Duplicate-hash retries (birthday collision, astronomically unlikely) are
|
||||
// handled automatically up to maxRetries per slot.
|
||||
// CreateBatch generates Count activation codes via the shared library's Mint
|
||||
// (all-or-nothing: batch + code hashes commit in one tx) and returns the
|
||||
// plaintext codes — the only time they ever appear.
|
||||
func (svc *Service) CreateBatch(ctx context.Context, req BatchRequest) (*BatchResult, error) {
|
||||
const maxRetries = 10
|
||||
|
||||
// Look up plan ID.
|
||||
planID, err := svc.store.GetPlanID(ctx, req.PlanCode)
|
||||
if err != nil {
|
||||
// 未知 plan 先拒(行为等价:旧实现先查 GetPlanID)。
|
||||
if _, err := svc.store.GetPlanID(ctx, req.PlanCode); err != nil {
|
||||
return nil, fmt.Errorf("CreateBatch: unknown plan %s: %w", req.PlanCode, err)
|
||||
}
|
||||
|
||||
// Insert the batch record.
|
||||
batchID, err := svc.store.CreateBatch(ctx, req.Channel, req.CreatedBy, req.Note)
|
||||
ent, err := libcodes.NewDurationEntitlement(string(req.PlanCode), req.DurationDays)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateBatch: %w", err)
|
||||
}
|
||||
|
||||
plaintexts := make([]string, 0, req.Count)
|
||||
|
||||
for i := 0; i < req.Count; i++ {
|
||||
var code string
|
||||
var h string
|
||||
ok := false
|
||||
for attempt := 0; attempt < maxRetries; attempt++ {
|
||||
c, err := GenerateCode()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateBatch: GenerateCode: %w", err)
|
||||
}
|
||||
h = Hash(c)
|
||||
err = svc.store.CreateCode(ctx, h, planID, req.DurationDays, batchID)
|
||||
if err == ErrDuplicate {
|
||||
continue // collision – generate a fresh code
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateBatch: CreateCode: %w", err)
|
||||
}
|
||||
code = c
|
||||
ok = true
|
||||
break
|
||||
}
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("CreateBatch: exceeded %d retry attempts for slot %d", maxRetries, i)
|
||||
}
|
||||
plaintexts = append(plaintexts, code)
|
||||
res, err := libcodes.Mint(ctx, svc.store.Lib(), libcodes.MintRequest{
|
||||
Channel: string(req.Channel),
|
||||
Entitlement: ent,
|
||||
Count: req.Count,
|
||||
CreatedBy: req.CreatedBy,
|
||||
Note: req.Note,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("CreateBatch: %w", err)
|
||||
}
|
||||
|
||||
return &BatchResult{
|
||||
BatchID: batchID,
|
||||
Codes: plaintexts,
|
||||
BatchID: res.BatchID,
|
||||
Codes: res.Codes,
|
||||
PlanCode: req.PlanCode,
|
||||
DurationDays: req.DurationDays,
|
||||
Channel: req.Channel,
|
||||
|
||||
Reference in New Issue
Block a user