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>
This commit is contained in:
wangjia
2026-06-19 12:14:27 +08:00
parent 914e2fb533
commit 23dff69c62
20 changed files with 541 additions and 642 deletions
+27
View File
@@ -0,0 +1,27 @@
package model
import "time"
// LicenseCode 兑换券(激活码)。由平台方批量生成,用户购买/活动获得后在自己门店兑换。
// 每张码代表一段时长(duration_days),兑换时叠加到门店当前到期时间上;一码一次(status 控制)。
type LicenseCode struct {
ID uint64 `gorm:"primaryKey;autoIncrement" json:"id"`
Code string `gorm:"size:32;uniqueIndex;not null" json:"code"` // 归一化大写无连字符存储
Type string `gorm:"type:enum('trial','monthly','annual','lifetime');default:'annual'" json:"type"` // 计费/时长标签
// Tier 档位钩子(类似 pro/max)。当前只用 'standard' 一种,分档能力/消费模式后续设计。
Tier string `gorm:"size:32;not null;default:'standard'" json:"tier"`
DurationDays int `gorm:"not null;default:0" json:"duration_days"` // 授予时长;0 = 永久(lifetime
MaxDevices int `gorm:"not null;default:0" json:"max_devices"` // 授予设备上限;0 = 不改变现值
Status string `gorm:"type:enum('unused','redeemed','void');default:'unused';index" json:"status"`
RedeemedShopID *uint64 `gorm:"index" json:"redeemed_shop_id,omitempty"`
RedeemedAt *time.Time `json:"redeemed_at,omitempty"`
RedeemedDeviceID string `gorm:"size:255" json:"redeemed_device_id,omitempty"`
Batch string `gorm:"size:64" json:"batch,omitempty"` // 发放批次/活动名
Note string `gorm:"size:255" json:"note,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}