|
|
3 months ago | |
|---|---|---|
| .. | ||
| README.md | 3 months ago | |
| license.go | 3 months ago | |
| license_test.go | 3 months ago | |
README.md
授权码 v2 改造方案(序号按产品累加 + 独立批次表)
状态:已设计、编解码已落地、业务改造未动工。等待择期开工。
一、问题
现有授权码把序号切成 批次(8位) + 批内序号(20位):
- 批次
probatch是factory级计数器,每次生产++,上限 255,用尽即废。 - 序号按批次重置,导致"序号"没有产品级全局意义。
- 批次作为授权码的一个编码字段存在,而它本该是一条独立的生产记录。
目标:序号按产品累加、占满 uint32;批次独立成表,主键用可读短码,无数量上限。
二、关键发现(决定了迁移代价)
1. batch 和 serial 在比特层面本来就是同一个连续整数
comm/core.go 的 v1 编码:
data2 := (int(batchNumber) << 8) | (int(serialNumber>>12) & 0xFF) // 段2
data3 := (int(serialNumber>>0) & 0xFFF) // 段3
令 gs = batch<<20 | serial(28 位),则 gs>>12 & 0xFFFF ≡ data2、gs & 0xFFF ≡ data3。
逐位恒等(1400 组穷举验证零差异)。所以 v1 的"批次+批内序号"就是一个 28 位序号的高 8 位 / 低 20 位拆分。
2. batch / serial 从未被业务消费
ValidateLicense 的全部 4 个调用点都是 pid, _, _, err:
| 调用点 | 用途 |
|---|---|
modules/user/api_binddevice.go:79 |
取 pid 定位 license_<pid_hex> 分表 |
modules/user/api_unbinddevice.go:40 |
同上 |
modules/api/api_batchresetlicensestatus.go:23 |
同上 |
modules/console/api_device.go:386 |
同上 |
真正的校验是拿 code 主键查表。重定义 batch/serial 的语义,后端零业务影响。
3. productid 在 v1/v2 中位置相同
productid 独占段1(第 5-8 个 hex 字符),不参与版本判别。因此新旧码混存期间,
license.ProductID() 恒可用,binddevice 等路径零风险。
三、v2 设计
码长不变:10 字节 = 20 个 hex 字符 = 5 段 × 4 字符。段0 是随机种子,段1-4 是被种子派生的 混淆序列异或过的数据。
逻辑布局(异或前。密文经混淆后不可肉眼分辨版本):
hex位 1-4 5-8 9 10-12 13-16 17 18-20
v1 seed productid probatch高4位 probatch低4位 随机4位 随机 随机
+序号高8位 +序号低12位
v2 seed productid 0xF(标记) 序号[31:20] 序号[19:4] 序号[3:0] 随机
- 第 9 个 hex 字符是版本判别位:v1 存
probatch>>4,v2 恒为0xF。 - 序号占
3+4+1 = 8个 hex 字符 = 完整 uint32(0 ~ 4,294,967,295)。 - 随机填充从 5 个 hex 字符缩到 3 个,不损失安全性:那些位本就由 seed 派生
(
rng.Intn(65536)),不是独立熵。码的唯一性照旧,code ↔ (seed, pid, serial)仍是双射。
硬前提:历史 probatch 必须全部 < 240(0xF0)
判别位是 probatch >> 4。若历史上存在 probatch ∈ [240, 255] 的批次,那些 v1 码会被
误判为 v2,序号解出垃圾。
- 用户已口头确认"肯定 < 240"(
probatch是每次生产 +1 的计数器,实际值很小)。 - 未经生产库验证。迁移脚本第一道闸必须是
MAX(probatch) >= 240 则中止,fail-closed。 utils/license的TestParse_V1OverLimitIsMisdetected已把这条约束钉进代码:它断言batch >= 240的 v1 码确实会被误判。判别位若挪位置,该测试立刻失败。
为什么不选 24 位方案
曾考虑 gs = 0xF<<24 | serial(24位),空间 1677 万/产品,解码路径不分叉(新老共用一个公式)。
最终选 32 位,因为代价近乎为零(只多一个 if 分支),且彻底消除天花板焦虑。
0xF 标记的另一层价值:它让将来扩容成为安全操作。授权码里仍有 3 个 hex 字符的随机填充,
真不够时可以约定"带标记的码才读那些位",老码不受影响。
四、新序号空间与老码物理隔离
v2 序号从 1 开始,不需要扫描老数据算起点——因为版本由标记位区分,不靠数值区间区分。
由此,两条历史风险被自动消解:
- 老码撞号(
(probatch, number)可能因api_updatefactory旧版把probatch重置为 0 而重复, 见modules/api/model_test.go的Test_RebuildFactoryProbatch注释)——不再要紧,老码不回填serialno。 - pid 解析不依赖判别位——混存期零风险。
五、已完成
apps/services/utils/license/(自包含,只依赖标准库):
| 符号 | 说明 |
|---|---|
GenerateV2(productid uint16, serial uint32) |
生成 v2 码 |
Parse(code) (*Info, error) |
自动识别 v1/v2 |
ProductID(code) (uint16, error) |
不判版本、不校验批次,混存期恒可用 |
V1ProbatchLimit = 0xF0 |
迁移闸门常量 |
6 个测试全绿,含:真实老码样本 0D82F1CCD83D72B78FA9 → pid=45069 batch=1 serial=260;
v2 往返覆盖 0xFFFFFFFF;batch ∈ [1,239] × 6 种 serial 共 1434 组 v1 码零误判;
2 万个连续序号零碰撞。
尚未接入任何业务代码,comm.GenerateLicense/ValidateLicense 原样保留,线上零影响。
六、待做
第一步(线上安全,老数据一行不动)
-
proto + pb 重生成
- 新增
DBFactoryBatch(见下方 DDL) DBFactoryDevics加codever/serialno/batchcodeDBProduct加serialcursor- ⚠ 本仓库无 pb 生成脚本。需
protoc --go_out=pb --go_opt=paths=import -I ../proto ../proto/db.proto, 再复刻源项目deep_server_up/pb.py的@go_tags注入——protoc 会把 json tag 重置成,omitempty并丢掉 gorm tag,不注入回去 DB 层会全线失效
- 新增
-
批次短码:6 位,复用
comm.GeneratePublicCode的字符集(去除易混的0/O/I/1,32 字符集 → 10.7 亿空间)。唯一索引 + 冲突重试。 -
生成逻辑改造(两处:
modules/api/api_createfactorydevics.go、modules/console/api_device.go)- 原子分配序号区间,顺手修掉现在
modelFactory.Probatch++的读-改-写竞态:UPDATE product SET serialcursor = serialcursor + $1 WHERE id = $2 RETURNING serialcursor; -- 区间 = [ret - N + 1, ret] - 建批次行,license 行写
codever=2, serialno, batchcode - ⚠ MAC 侧本期不动,
GenerateCustomMAC仍吃probatch,故factory.probatch仍需继续自增
- 原子分配序号区间,顺手修掉现在
-
调用点切换:4 处
comm.ValidateLicense→license.ProductID;之后删除comm.GenerateLicense/comm.ValidateLicense(comm是共享配置与对象,不放算法实现)
第二步(历史归档,可延后,失败不影响线上)
-
迁移 SQL。第一道闸:
-- 对每张 license_<pid_hex> 分表执行,任一命中即中止整个迁移 SELECT count(*) FROM license_b00d WHERE probatch >= 240; -- 必须为 0加列(Postgres 无 uint32,序号列必须
BIGINT,INT放不下0xFFFFFFFF):ALTER TABLE license_<pid_hex> ADD COLUMN IF NOT EXISTS codever SMALLINT NOT NULL DEFAULT 0; ALTER TABLE license_<pid_hex> ADD COLUMN IF NOT EXISTS serialno BIGINT NOT NULL DEFAULT 0; ALTER TABLE license_<pid_hex> ADD COLUMN IF NOT EXISTS batchcode VARCHAR(8) NOT NULL DEFAULT ''; CREATE INDEX IF NOT EXISTS idx_<pid>_batchcode ON license_<pid_hex>(batchcode); -- 只对 v2 行要求序号唯一(老码 serialno 恒为 0) CREATE UNIQUE INDEX IF NOT EXISTS uq_<pid>_serialno ON license_<pid_hex>(serialno) WHERE codever = 2; ALTER TABLE product ADD COLUMN IF NOT EXISTS serialcursor BIGINT NOT NULL DEFAULT 0;批次表:
CREATE TABLE factory_batch ( batchcode VARCHAR(8) PRIMARY KEY, -- 6 位可读短码 factoryid INT NOT NULL, productid INT NOT NULL, serialstart BIGINT NOT NULL, serialend BIGINT NOT NULL, devicenum INT NOT NULL, codever SMALLINT NOT NULL DEFAULT 2, -- 1=从 deliverynote 迁入的历史批次 legacyprobatch INT NOT NULL DEFAULT 0, -- v1 批次号,v2 为 0 ts BIGINT NOT NULL, remark TEXT ); CREATE INDEX idx_factory_batch_fp ON factory_batch(factoryid, productid); -
历史批次导入(需 Go 工具,短码要程序生成):
factory_deliverynote每行 = 一次生产 = 一个批次。序号区间按 v1 语义还原为[probatch<<20 | 1, probatch<<20 | devicetnum],codever=1、legacyprobatch=probatch。 -
老 license 行回填
batchcode:UPDATE license_<pid_hex> l SET batchcode = b.batchcode FROM factory_batch b WHERE b.codever = 1 AND b.legacyprobatch = l.probatch AND b.factoryid = l.factoryid AND b.productid = <pid>;⚠ 依赖
(factoryid, probatch)唯一。若probatch重置 bug 留过疤,同一厂家会有两次生产复用 同一probatch,此处会归错批次,需改用createtimevsdeliverynote.ts的时间窗辅助拆分。 迁移前应先跑体检:SELECT factoryid, probatch, count(*) FROM factory_deliverynote GROUP BY 1,2 HAVING count(*) > 1;老码
serialno保持 0、codever保持 0,不做回填——v1 序号没有产品级全局意义。 -
前端:
apps/admin/app/pages/factory.vue/device.vue的批次展示改为序号区间 + 批次短码。
七、本期不做
GenerateCustomMAC/ CMEI(用户明确"先别急着动")。MAC 侧同样是batch(8) + serial(24), 255 上限仍在。合并成 32 位全局序号时老 MAC 逐位不变,但modules/user/api_binddevice.go:133的 CMEI 取mac[2]当批次段、mac[4..5]当序号段, 中间的mac[3]不参与——序号连续累加后 CMEI 会在 65536 个之后碰撞。需先定 CMEI 新规则。- 32 位之外的扩容(吃掉剩余 3 个 hex 字符的随机填充)。
0xF标记已把这条路留好。
八、未验证事项
| 事项 | 状态 |
|---|---|
生产库 MAX(probatch) < 240 |
用户口头确认,未查库。迁移脚本以 fail-closed 闸门兜底 |
(factoryid, probatch) 是否唯一 |
未查库。只影响第二步历史归档,不影响新码 |
| 固件/产测工具是否解析 batch/serial | 未确认。即便解析也安全:v2 序号单调递增,老规则解出的 (batch, serial) 组合同样单调、不重复 |