Files
jiu/backend/cmd/gencode/main.go
T
wangjia 23dff69c62 feat(backend): 授权改为时长兑换券体系 + 退役 ed25519/HMAC + 平台生码工具
- 新增 license_codes 码池表 + model.LicenseCode;licenses 加 tier 档位列
- LicenseService.Redeem:单事务 FOR UPDATE 校验码未用 → 时长叠加(可叠加,0=永久)
  → 写 type/tier/max_devices → 绑设备(超限整笔回滚) → 标记已用 → 即时失效 phase 缓存
  路由仍 POST /license/activate,客户端零破坏
- util.GenerateRedeemCode/NormalizeCode:JIUKU-XXXX-XXXX 短码(crypto/rand)
- cmd/gencode:平台批量生成兑换码并落库;删除 cmd/issue、cmd/genkey
- 退役 ed25519 + HMAC:删 util/license_key、GenerateKey、License 全部 config 字段
  及生产启动私钥校验;trial 改直接建行(无需私钥、去 Fatal)
- tier 档位钩子默认 standard,分档消费模式后续设计
- 测试:Redeem 全场景(叠加/过期重置/永久/一码一次/无效/设备上限回滚)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 12:14:27 +08:00

91 lines
2.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// gencode — 平台方批量生成兑换券(激活码)并写入码池 license_codes。
//
// 用法(在 backend/ 目录下执行,读 config/env 取数据库 DSN):
//
// go run ./cmd/gencode -type annual -days 365 -count 100 -batch 2026-summer
// go run ./cmd/gencode -days 30 -count 10 -note "试用补偿"
// go run ./cmd/gencode -type lifetime -days 0 -count 1 # 永久授权码
//
// 生成的码以 JIUKU-XXXX-XXXX 格式打印(status=unused),分发给用户在 App 内兑换。
package main
import (
"flag"
"fmt"
stdlog "log"
"os"
"time"
"gorm.io/driver/mysql"
"gorm.io/gorm"
"gorm.io/gorm/logger"
"github.com/wangjia/jiu/backend/config"
"github.com/wangjia/jiu/backend/internal/model"
"github.com/wangjia/jiu/backend/internal/util"
)
func main() {
licType := flag.String("type", "annual", "计费/时长标签:trial|monthly|annual|lifetime")
tier := flag.String("tier", "standard", "档位(当前仅 standard")
days := flag.Int("days", 365, "授予时长(天);0 = 永久")
devices := flag.Int("devices", 0, "授予设备上限;0 = 兑换时不改变门店现值")
count := flag.Int("count", 1, "生成数量")
batch := flag.String("batch", "", "发放批次/活动名(便于追踪)")
note := flag.String("note", "", "备注")
flag.Parse()
if *count < 1 {
fmt.Fprintln(os.Stderr, "error: -count 必须 >= 1")
os.Exit(1)
}
config.Load()
db, err := gorm.Open(mysql.Open(config.C.Database.DSN), &gorm.Config{
Logger: logger.New(
stdlog.New(os.Stdout, "\r\n", stdlog.LstdFlags),
logger.Config{LogLevel: logger.Warn, IgnoreRecordNotFoundError: true},
),
})
if err != nil {
stdlog.Fatalf("连接数据库失败: %v", err)
}
if err := db.AutoMigrate(&model.LicenseCode{}); err != nil {
stdlog.Fatalf("迁移 license_codes 失败: %v", err)
}
display := make([]string, 0, *count)
for i := 0; i < *count; i++ {
// 重试避免极小概率的唯一冲突
var shown string
for attempt := 0; attempt < 5; attempt++ {
shown = util.GenerateRedeemCode()
lc := model.LicenseCode{
Code: util.NormalizeCode(shown),
Type: *licType,
Tier: *tier,
DurationDays: *days,
MaxDevices: *devices,
Status: "unused",
Batch: *batch,
Note: *note,
}
err := db.Create(&lc).Error
if err == nil {
break
}
if attempt == 4 {
stdlog.Fatalf("写入兑换码失败(连续冲突): %v", err)
}
}
display = append(display, shown)
}
fmt.Printf("\n✅ 已生成 %d 个兑换码(type=%s tier=%s 时长=%d天 设备=%d batch=%q)于 %s\n\n",
*count, *licType, *tier, *days, *devices, *batch, time.Now().Format("2006-01-02 15:04:05"))
for _, c := range display {
fmt.Println(" " + c)
}
fmt.Println()
}