merge: maestro/tsk_tFMU7-hKzfOf [tsk_tFMU7-hKzfOf] codes 激活码模块
解决与 1A 骨架的冲突:module 统一为 github.com/wangjia/pangolin/server (codes 分支原用 pangolin/server,7 个源文件 import 已改写); go.mod require 并集(redis 取 9.20.1);Makefile 以骨架为基底并入 build-codegen/test-unit/test-integration target。 go build/vet 通过,codes 单测通过。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
// Command codegen is a CLI transition tool for batch-generating activation codes
|
||||
// before the admin panel (task #8) is ready.
|
||||
//
|
||||
// Usage:
|
||||
//
|
||||
// codegen -plan=pro -days=30 -count=100 -channel=manual \
|
||||
// -note="telegram batch june" -dsn="root:pass@tcp(127.0.0.1:3306)/pangolin?parseTime=true" \
|
||||
// [-out=codes.csv]
|
||||
//
|
||||
// The generated plaintext codes are written to stdout (or -out) as a CSV file.
|
||||
// They are NEVER logged or stored in the database.
|
||||
// The database receives only the SHA-256 hashes.
|
||||
//
|
||||
// Run `codegen -help` for full flag documentation.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/wangjia/pangolin/server/internal/codes"
|
||||
"github.com/wangjia/pangolin/server/internal/db"
|
||||
)
|
||||
|
||||
func main() {
|
||||
plan := flag.String("plan", "", "Plan code: free | pro | team (required)")
|
||||
days := flag.Int("days", 0, "Duration in days the code grants (required, >0)")
|
||||
count := flag.Int("count", 0, "Number of codes to generate (required, >0)")
|
||||
channel := flag.String("channel", "manual", "Distribution channel: store | tg | line | manual")
|
||||
note := flag.String("note", "", "Human-readable batch note (optional)")
|
||||
dsn := flag.String("dsn", "", "MySQL DSN, e.g. user:pass@tcp(host:port)/dbname?parseTime=true (required, or set DB_DSN env)")
|
||||
out := flag.String("out", "", "Output CSV file path (default: stdout)")
|
||||
createdBy := flag.String("by", "cli", "Identifier of the operator creating this batch")
|
||||
flag.Parse()
|
||||
|
||||
// Resolve DSN from flag or environment.
|
||||
dsnVal := *dsn
|
||||
if dsnVal == "" {
|
||||
dsnVal = os.Getenv("DB_DSN")
|
||||
}
|
||||
|
||||
// Validation.
|
||||
if dsnVal == "" {
|
||||
log.Fatal("codegen: -dsn or DB_DSN is required")
|
||||
}
|
||||
if *plan == "" {
|
||||
log.Fatal("codegen: -plan is required")
|
||||
}
|
||||
if *days <= 0 {
|
||||
log.Fatal("codegen: -days must be > 0")
|
||||
}
|
||||
if *count <= 0 {
|
||||
log.Fatal("codegen: -count must be > 0")
|
||||
}
|
||||
|
||||
planCode := codes.PlanCode(*plan)
|
||||
switch planCode {
|
||||
case codes.PlanFree, codes.PlanPro, codes.PlanTeam:
|
||||
default:
|
||||
log.Fatalf("codegen: unknown plan %q; must be free | pro | team", *plan)
|
||||
}
|
||||
|
||||
ch := codes.BatchChannel(*channel)
|
||||
switch ch {
|
||||
case codes.ChannelStore, codes.ChannelTG, codes.ChannelLine, codes.ChannelManual:
|
||||
default:
|
||||
log.Fatalf("codegen: unknown channel %q; must be store | tg | line | manual", *channel)
|
||||
}
|
||||
|
||||
// Open database.
|
||||
dbConn, err := db.Open(dsnVal)
|
||||
if err != nil {
|
||||
log.Fatalf("codegen: database: %v", err)
|
||||
}
|
||||
defer dbConn.Close()
|
||||
|
||||
store := codes.NewStore(dbConn)
|
||||
// Service is created without Redis (batch generation doesn't need rate limiting).
|
||||
svc := codes.NewService(store, nil, 5, time.Hour)
|
||||
|
||||
log.Printf("codegen: generating %d %s/%dd codes for channel=%s …", *count, planCode, *days, ch)
|
||||
|
||||
result, err := svc.CreateBatch(context.Background(), codes.BatchRequest{
|
||||
PlanCode: planCode,
|
||||
DurationDays: *days,
|
||||
Count: *count,
|
||||
Channel: ch,
|
||||
Note: *note,
|
||||
CreatedBy: *createdBy,
|
||||
})
|
||||
if err != nil {
|
||||
log.Fatalf("codegen: CreateBatch: %v", err)
|
||||
}
|
||||
|
||||
log.Printf("codegen: batch %d created; writing CSV …", result.BatchID)
|
||||
|
||||
// Determine output writer.
|
||||
output := os.Stdout
|
||||
if *out != "" {
|
||||
f, err := os.Create(*out)
|
||||
if err != nil {
|
||||
log.Fatalf("codegen: open output file: %v", err)
|
||||
}
|
||||
defer f.Close()
|
||||
output = f
|
||||
}
|
||||
|
||||
csvRows := make([]codes.CSVRow, len(result.Codes))
|
||||
now := time.Now().UTC()
|
||||
for i, c := range result.Codes {
|
||||
csvRows[i] = codes.CSVRow{
|
||||
Index: i + 1,
|
||||
Code: c,
|
||||
Plan: planCode,
|
||||
DurationDays: *days,
|
||||
BatchID: result.BatchID,
|
||||
Channel: ch,
|
||||
GeneratedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
if err := codes.ExportCSV(output, csvRows); err != nil {
|
||||
log.Fatalf("codegen: ExportCSV: %v", err)
|
||||
}
|
||||
|
||||
if *out != "" {
|
||||
fmt.Printf("codegen: %d codes written to %s\n", *count, *out)
|
||||
fmt.Printf("codegen: ⚠️ Treat this file as a secret — it contains plaintext activation codes.\n")
|
||||
} else {
|
||||
fmt.Fprintf(os.Stderr, "codegen: %d codes written to stdout\n", *count)
|
||||
fmt.Fprintf(os.Stderr, "codegen: ⚠️ Treat the output as a secret — it contains plaintext activation codes.\n")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user