From f9553918304c3f5940b20c1f249fea51efd2c164 Mon Sep 17 00:00:00 2001 From: wangjia <809946525@qq.com> Date: Fri, 19 Jun 2026 12:14:35 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20license-design=20=E5=A2=9E=E8=A1=A5=20v?= =?UTF-8?q?2=20=E6=97=B6=E9=95=BF=E5=85=91=E6=8D=A2=E5=88=B8=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 顶部新增 v2 节,标注 v1 的 Ed25519 token 方案已废弃,记录兑换券 模型/数据表/Redeem 流程/与 v1 差异;运行时降级 phase 两版共用。 Co-Authored-By: Claude Opus 4.8 --- docs/architecture/license-design.md | 41 ++++++++++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/architecture/license-design.md b/docs/architecture/license-design.md index 291811b..ccf133e 100644 --- a/docs/architecture/license-design.md +++ b/docs/architecture/license-design.md @@ -1,6 +1,45 @@ # 授权信息功能 — 完整方案设计 -> 本文档覆盖「付费授权」功能的产品决策、数据模型、API 设计、安全方案、前端方案及分期实施计划。所有编码工作在本文档评审通过后按计划分批推进。 +> ⚠️ **本文档第 1–11 节为 v1(Ed25519 自包含签名 token 方案),已被 v2「时长兑换券」模型取代。** +> 实际线上实现以下方「## v2」为准;下方 v1 各节仅作历史保留,其中「激活时验签」「license_token 列」「AdminOnly 激活」等已不再适用。 + +--- + +## v2 实际实现(时长兑换券模型,2026-06 起) + +### 为什么改 + +v1 把许可证做成「一个 Ed25519 签名 token = 一张完整授权」,但运行时从不验签(签名形同虚设),且 token 内 `expires_at` 是**绝对日期**、不可叠加,不符合「买一张码续 1 年、可买多张累加」的消费直觉。经产品方确认改为**时长兑换券**。 + +### 模型要点 + +- **时长券**:每张码代表一段**时长**(`duration_days`),兑换 = `新到期 = max(今天, 当前到期) + 时长`,**可叠加**;`duration_days=0` 表示永久(到期置 NULL)。 +- **短码 + 服务端码表**:码形如 `JIUKU-7F3A-9K2D`(`util.GenerateRedeemCode`,crypto/rand + 无歧义字母表),归一化(`util.NormalizeCode`)后存 `license_codes` 表。**一码一次**由服务端码表 `status` 强制(签名无法防重放,故 v2 退役 ed25519)。 +- **档位钩子**:`license_codes.tier` / `licenses.tier`(默认 `standard`),为将来 pro/max-like 分档预留;当前不据此做能力差异,分档消费模式后续设计。 +- **签发归平台方**:码由 CLI `go run ./cmd/gencode -type annual -days 365 -count N` 批量生成落库,用户购买/活动获得后在 App「设置 → 授权」兑换。**不开放 App 内自助签发**(防店铺管理员自签终身授权)。 + +### 数据模型 + +- 新表 `license_codes`(码池):`code`(uniq) / `type` / `tier` / `duration_days` / `max_devices` / `status`(unused/redeemed/void) / `redeemed_shop_id` / `redeemed_at` / `redeemed_device_id` / `batch` / `note`。见 `model.LicenseCode` + `schema.sql` + `testutil`。 +- `licenses` 表新增 `tier` 列(additive)。`license_key` 列降级为「当前权益占位」:trial 用 `TRIAL-`、无既有行兜底用 `REDEEM-`,兑换扩展时不改原值。**v1 的 `license_token` 列未实现,已废弃。** + +### 兑换流程(`LicenseService.Redeem`,路由仍 `POST /license/activate` 以兼容客户端) + +单事务 + `FOR UPDATE`(check-then-act 并发安全):① 锁码校验 unused(否则 `无效激活码/已被使用/已失效`)→ ② 锁本店最新 `is_active` 授权行(无则新建)→ ③ 叠加到期、写 `type/tier/max_devices=max(现值,码值)` → ④ 绑定本设备(幂等 + `max_devices` 上限,超限整笔回滚)→ ⑤ 标记码 `redeemed` → ⑥ 提交后 `InvalidateLicensePhase` 即时生效。 + +### 与 v1 的差异(务必注意) + +| 维度 | v1(已废弃) | v2(现状) | +|------|------|------| +| 码形态 | Ed25519 246 字符 token | 短码 `JIUKU-XXXX-XXXX` + 服务端码表 | +| 到期语义 | token 内绝对日期 | 时长券叠加,DB `expires_at` 为准 | +| 验签 | 计划激活时验签(实际从未生效) | 退役 ed25519,改服务端码表查验 | +| 签发 | `cmd/issue` 打印 token(需手动 INSERT) | `cmd/gencode` 批量生成并落库 | +| 激活权限 | 计划加 `AdminOnly` | 维持 `ReadOnly` 豁免、不加 AdminOnly(兑换是店主自助行为) | +| trial | Ed25519 签发 | 直接建行(无需私钥),`createTrialLicense` 去除 Fatal | +| 运行时降级 | 同 v2(`LicenseGuard` 读 DB `expires_at` 实时算 phase,30s 缓存) | **不变,沿用** | + +> v1 第 7 节「授权状态机与拦截层」(`CalcLicensePhase` / `LicenseGuard` / grace/readonly/locked)**已落地且 v2 沿用**,是两版共同的运行时基础。 ---