Browse Source

添加 授权吗 的方案

main
liwei1dao 3 months ago
parent
commit
d7445984cb
  1. 1
      apps/services/comm/log
  2. 208
      apps/services/utils/license/README.md
  3. 143
      apps/services/utils/license/license.go
  4. 172
      apps/services/utils/license/license_test.go

1
apps/services/comm/log

@ -1 +0,0 @@
2026/07/10 10:56:05.452 ERROR comm/moduleconfig.go:202 [ModuleConfig] 密钥字段解密失败,已跳过该字段(保留默认值) module=wechatpay key=PrivateKeyPath err=GCM 解密失败: cipher: message authentication failed 本服务key指纹=sha256:19731e34cd8c [不足 32 字节,已右侧补零(原长 30)] 【诊断】密文是用「空 key(FIELD_ENCRYPT_KEY 未注入时补零出的全零密钥)」(指纹 sha256:17a63bfb3b5b [!!未配置 FIELD_ENCRYPT_KEY,正在使用全零密钥])加密的存量数据,与本服务当前 key 不是同一把 —— 到 console 后台把该字段重新填一遍保存,即可用当前 key 重新加密落库

208
apps/services/utils/license/README.md

@ -0,0 +1,208 @@
# 授权码 v2 改造方案(序号按产品累加 + 独立批次表)
状态:**已设计、编解码已落地、业务改造未动工**。等待择期开工。
## 一、问题
现有授权码把序号切成 `批次(8位) + 批内序号(20位)`:
- 批次 `probatch` 是 `factory` 级计数器,每次生产 `++`,**上限 255**,用尽即废。
- 序号按批次重置,导致"序号"没有产品级全局意义。
- 批次作为授权码的一个**编码字段**存在,而它本该是一条独立的生产记录。
目标:序号按产品累加、占满 `uint32`;批次独立成表,主键用可读短码,无数量上限。
## 二、关键发现(决定了迁移代价)
### 1. `batch` 和 `serial` 在比特层面本来就是同一个连续整数
`comm/core.go` 的 v1 编码:
```go
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 开始**,不需要扫描老数据算起点——因为版本由标记位区分,不靠数值区间区分。
由此,两条历史风险被自动消解:
1. **老码撞号**(`(probatch, number)` 可能因 `api_updatefactory` 旧版把 `probatch` 重置为 0 而重复,
见 `modules/api/model_test.go` 的 `Test_RebuildFactoryProbatch` 注释)——不再要紧,老码不回填 `serialno`。
2. **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` 原样保留,线上零影响。
## 六、待做
### 第一步(线上安全,老数据一行不动)
1. **proto + pb 重生成**
- 新增 `DBFactoryBatch`(见下方 DDL)
- `DBFactoryDevics` 加 `codever` / `serialno` / `batchcode`
- `DBProduct` 加 `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 层会全线失效
2. **批次短码**:6 位,复用 `comm.GeneratePublicCode` 的字符集(去除易混的 `0/O/I/1`,32 字符集
→ 10.7 亿空间)。唯一索引 + 冲突重试。
3. **生成逻辑改造**(两处:`modules/api/api_createfactorydevics.go`、`modules/console/api_device.go`)
- 原子分配序号区间,顺手修掉现在 `modelFactory.Probatch++` 的读-改-写竞态:
```sql
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. **调用点切换**:4 处 `comm.ValidateLicense` → `license.ProductID`;之后删除
`comm.GenerateLicense` / `comm.ValidateLicense`(`comm` 是共享配置与对象,不放算法实现)
### 第二步(历史归档,可延后,失败不影响线上)
5. **迁移 SQL**。第一道闸:
```sql
-- 对每张 license_<pid_hex> 分表执行,任一命中即中止整个迁移
SELECT count(*) FROM license_b00d WHERE probatch >= 240; -- 必须为 0
```
加列(Postgres 无 uint32,序号列必须 `BIGINT`,`INT` 放不下 `0xFFFFFFFF`):
```sql
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;
```
批次表:
```sql
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);
```
6. **历史批次导入**(需 Go 工具,短码要程序生成):
`factory_deliverynote` 每行 = 一次生产 = 一个批次。序号区间按 v1 语义还原为
`[probatch<<20 | 1, probatch<<20 | devicetnum]`,`codever=1`、`legacyprobatch=probatch`。
7. **老 license 行回填 `batchcode`**:
```sql
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`,此处会归错批次,需改用 `createtime` vs `deliverynote.ts` 的时间窗辅助拆分。
**迁移前应先跑体检**:
```sql
SELECT factoryid, probatch, count(*) FROM factory_deliverynote GROUP BY 1,2 HAVING count(*) > 1;
```
老码 `serialno` 保持 0、`codever` 保持 0,不做回填——v1 序号没有产品级全局意义。
8. **前端**:`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)` 组合同样单调、不重复 |

143
apps/services/utils/license/license.go

@ -0,0 +1,143 @@
// Package license 授权码编解码。
//
// 授权码固定 10 字节(20 个 hex 字符),分 5 段:段0 是随机种子,段1-4 是被种子
// 派生的混淆序列异或过的数据。码长与混淆结构在 v1/v2 之间保持不变。
//
// v1 把序号切成 批次(8位) + 批内序号(20位),批次每次生产 +1、上限 255,用尽即废。
// v2 取消批次字段,序号按产品累加、占满 32 位;批次改由独立的批次表承载。
//
// 逻辑布局(异或前。密文经混淆后不可肉眼分辨版本):
//
// 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,历史 probatch 恒小于
// V1ProbatchLimit,故不可能等于 v2 标记。productid 位于第 5-8 位,两版一致,
// 因此提取 productid 无需先判版本(见 ProductID)。
package license
import (
cryptorand "crypto/rand"
"errors"
"fmt"
"math/rand"
"strconv"
)
const (
totalSegments = 5 // 5段,每段4字符 = 20字符 = 10字节
segmentLength = 4
// v2Tag v2 格式标记,占第 9 个 hex 字符
v2Tag = 0xF
// V1ProbatchLimit v1 批次号上界(不含)。历史 probatch 必须全部小于该值,
// 否则越界的 v1 码会在判别位上撞上 v2 标记而被误判为 v2。
// 迁移脚本必须先校验 MAX(probatch) < V1ProbatchLimit 再继续。
V1ProbatchLimit = 0xF0
)
// Version 授权码格式版本
type Version uint8
const (
V1 Version = 1 // 批次(8位) + 批内序号(20位)
V2 Version = 2 // 产品级全局序号(32位)
)
// Info 授权码解析结果
type Info struct {
ProductID uint16 // 产品ID
Version Version // 格式版本
Serial uint32 // v2:产品级全局序号(32位) v1:批内序号(20位)
Batch byte // 批次号,仅 v1 有效
}
// decode 拆出 5 个数据段,并用种子重建混淆序列。v1/v2 共用,不涉及版本判别。
func decode(code string) (segments, confusion []int, err error) {
expectedLength := totalSegments * segmentLength
if len(code) != expectedLength {
return nil, nil, fmt.Errorf("授权码长度错误,期望%d字符,实际%d字符", expectedLength, len(code))
}
segments = make([]int, totalSegments)
for i := 0; i < totalSegments; i++ {
value, perr := strconv.ParseUint(code[i*segmentLength:(i+1)*segmentLength], 16, 32)
if perr != nil {
return nil, nil, fmt.Errorf("第%d段包含无效字符", i+1)
}
segments[i] = int(value)
}
rng := rand.New(rand.NewSource(int64(segments[0])))
confusion = make([]int, totalSegments-1)
for i := range confusion {
confusion[i] = rng.Intn(65536)
}
return segments, confusion, nil
}
// GenerateV2 生成 v2 授权码。
// serial 为产品级全局序号,占满 uint32,按产品累加,不随批次重置。
func GenerateV2(productid uint16, serial uint32) (string, error) {
seedBytes := make([]byte, 2)
if _, err := cryptorand.Read(seedBytes); err != nil {
return "", fmt.Errorf("生成随机种子失败: %v", err)
}
seed := int(seedBytes[0])<<8 | int(seedBytes[1])
rng := rand.New(rand.NewSource(int64(seed)))
confusion := make([]int, totalSegments-1)
for i := range confusion {
confusion[i] = rng.Intn(65536)
}
confusion[0] ^= int(productid)
confusion[1] ^= v2Tag<<12 | int(serial>>20) // 标记 + 序号[31:20]
confusion[2] ^= int(serial>>4) & 0xFFFF // 序号[19:4]
confusion[3] ^= int(serial&0xF)<<12 | rng.Intn(1<<12) // 序号[3:0] + 12位随机填充
code := fmt.Sprintf("%04X", seed)
for _, c := range confusion {
code += fmt.Sprintf("%04X", c)
}
return code, nil
}
// Parse 解析授权码,自动识别 v1/v2 格式
func Parse(code string) (*Info, error) {
segments, confusion, err := decode(code)
if err != nil {
return nil, err
}
info := &Info{ProductID: uint16((segments[1] ^ confusion[0]) & 0xFFFF)}
data2 := segments[2] ^ confusion[1]
data3 := segments[3] ^ confusion[2]
if (data2>>12)&0xF == v2Tag {
data4 := segments[4] ^ confusion[3]
info.Version = V2
info.Serial = uint32(data2&0xFFF)<<20 | uint32(data3&0xFFFF)<<4 | uint32((data4>>12)&0xF)
return info, nil
}
info.Version = V1
info.Batch = byte((data2 >> 8) & 0xFF)
info.Serial = uint32(data2&0xFF)<<12 | uint32(data3&0xFFF)
if info.Batch < 1 {
return nil, errors.New("批次数据异常")
}
return info, nil
}
// ProductID 提取授权码中的产品ID。
// productid 在 v1/v2 中位置相同,故不做版本判别、也不校验批次,
// 新旧码混存期间恒可用(调用方通常只需要它来定位 license 分表)。
func ProductID(code string) (uint16, error) {
segments, confusion, err := decode(code)
if err != nil {
return 0, err
}
return uint16((segments[1] ^ confusion[0]) & 0xFFFF), nil
}

172
apps/services/utils/license/license_test.go

@ -0,0 +1,172 @@
package license
import (
cryptorand "crypto/rand"
"fmt"
"math/rand"
"testing"
)
// 生产库里的真实 v1 授权码样本
const legacySampleCode = "0D82F1CCD83D72B78FA9"
// generateV1 复刻 comm.GenerateLicense 的 v1 编码,仅供测试构造老码。
// 生产代码不再生成 v1,只解析。
func generateV1(productid uint16, batch byte, serial uint32) (string, error) {
if batch < 1 {
return "", fmt.Errorf("批次必须 >= 1")
}
if serial > 0xFFFFF {
return "", fmt.Errorf("v1 序号上限 0xFFFFF")
}
seedBytes := make([]byte, 2)
if _, err := cryptorand.Read(seedBytes); err != nil {
return "", err
}
seed := int(seedBytes[0])<<8 | int(seedBytes[1])
rng := rand.New(rand.NewSource(int64(seed)))
confusion := make([]int, 4)
for i := range confusion {
confusion[i] = rng.Intn(65536)
}
confusion[0] ^= int(productid)
confusion[1] ^= (int(batch) << 8) | (int(serial>>12) & 0xFF)
confusion[2] ^= int(serial) & 0xFFF
confusion[3] ^= rng.Intn(65536)
code := fmt.Sprintf("%04X", seed)
for _, c := range confusion {
code += fmt.Sprintf("%04X", c)
}
return code, nil
}
func TestParse_LegacySample(t *testing.T) {
info, err := Parse(legacySampleCode)
if err != nil {
t.Fatalf("解析真实老码失败: %v", err)
}
if info.Version != V1 {
t.Errorf("版本应为 v1, 实际 v%d", info.Version)
}
if info.ProductID != 45069 || info.Batch != 1 || info.Serial != 260 {
t.Errorf("解析结果不符: pid=%d batch=%d serial=%d, 期望 45069/1/260",
info.ProductID, info.Batch, info.Serial)
}
}
// v2 往返:序号必须能占满 uint32
func TestGenerateV2_RoundTrip(t *testing.T) {
pids := []uint16{0, 1, 45058, 45069, 0xFFFF}
serials := []uint32{
0, 1, 15, 16, 0xFFF, 0x1000, 0xFFFFF, 0x100000,
0xFFFFFFF, 0x10000000, 0x12345678, 0xFFFFFFFE, 0xFFFFFFFF,
}
for _, pid := range pids {
for _, serial := range serials {
code, err := GenerateV2(pid, serial)
if err != nil {
t.Fatalf("生成失败 pid=%d serial=%d: %v", pid, serial, err)
}
if len(code) != totalSegments*segmentLength {
t.Fatalf("码长变了: %d, 期望 %d", len(code), totalSegments*segmentLength)
}
info, err := Parse(code)
if err != nil {
t.Fatalf("解析失败 %s: %v", code, err)
}
if info.Version != V2 {
t.Errorf("%s 应识别为 v2, 实际 v%d", code, info.Version)
}
if info.ProductID != pid || info.Serial != serial {
t.Errorf("往返不一致: 写入 pid=%d serial=%d, 读出 pid=%d serial=%d",
pid, serial, info.ProductID, info.Serial)
}
}
}
}
// probatch < 240 的 v1 码不能被误判成 v2,且 batch/serial 必须原样解出
func TestParse_V1NotMisdetected(t *testing.T) {
const pid = 45069
serials := []uint32{0, 1, 4095, 4096, 65535, 0xFFFFF}
for batch := 1; batch < V1ProbatchLimit; batch++ {
for _, serial := range serials {
code, err := generateV1(pid, byte(batch), serial)
if err != nil {
t.Fatalf("v1 生成失败 batch=%d serial=%d: %v", batch, serial, err)
}
info, err := Parse(code)
if err != nil {
t.Fatalf("v1 解析失败 batch=%d serial=%d: %v", batch, serial, err)
}
if info.Version != V1 {
t.Fatalf("batch=%d serial=%d 的 v1 码被误判为 v%d", batch, serial, info.Version)
}
if info.ProductID != pid || info.Batch != byte(batch) || info.Serial != serial {
t.Fatalf("v1 解析不一致 batch=%d serial=%d -> pid=%d batch=%d serial=%d",
batch, serial, info.ProductID, info.Batch, info.Serial)
}
}
}
}
// 文档化约束:batch >= 240 的 v1 码必然被误判为 v2(判别位 = batch>>4 == 0xF)。
// 迁移脚本必须先验证 MAX(probatch) < V1ProbatchLimit,否则老码序号会解出垃圾。
// 本测试若开始失败,说明判别位挪了位置,前提校验需要跟着改。
func TestParse_V1OverLimitIsMisdetected(t *testing.T) {
const pid = 45069
for batch := V1ProbatchLimit; batch <= 0xFF; batch++ {
code, err := generateV1(pid, byte(batch), 1)
if err != nil {
t.Fatalf("v1 生成失败 batch=%d: %v", batch, err)
}
info, err := Parse(code)
if err != nil {
t.Fatalf("解析失败 batch=%d: %v", batch, err)
}
if info.Version != V2 {
t.Errorf("batch=%d 的 v1 码应当被误判为 v2(证明前提校验必要),实际 v%d", batch, info.Version)
}
}
}
// productid 的提取与版本无关,也不受 v1 批次异常影响
func TestProductID_VersionAgnostic(t *testing.T) {
const pid = 45069
v1, err := generateV1(pid, 3, 1234)
if err != nil {
t.Fatalf("v1 生成失败: %v", err)
}
v2, err := GenerateV2(pid, 0xDEADBEEF)
if err != nil {
t.Fatalf("v2 生成失败: %v", err)
}
for _, code := range []string{v1, v2, legacySampleCode} {
got, err := ProductID(code)
if err != nil {
t.Fatalf("%s 提取 pid 失败: %v", code, err)
}
if got != pid {
t.Errorf("%s 提取 pid=%d, 期望 %d", code, got, pid)
}
}
}
// 同产品下连续序号生成的码互不相同(code <-> (seed, pid, serial) 双射)
func TestGenerateV2_Uniqueness(t *testing.T) {
const pid = 45069
seen := make(map[string]uint32, 20000)
for serial := uint32(1); serial <= 20000; serial++ {
code, err := GenerateV2(pid, serial)
if err != nil {
t.Fatalf("生成失败 serial=%d: %v", serial, err)
}
if prev, dup := seen[code]; dup {
t.Fatalf("授权码碰撞: serial=%d 与 serial=%d 都生成 %s", serial, prev, code)
}
seen[code] = serial
}
}
Loading…
Cancel
Save