// Package id 生成 canonical_user_id(ULID,ADR-013)。 // // 背景:两套 PHP 旧系统各自用自增 int 作 user 主键,合并后必须一个跨系统、 // 跨实例、可排序、可对外暴露的身份主键。ADR-013 拍板 ULID: // - 26 字符 Crockford Base32(大写),存 VARCHAR(26); // - 前 48bit 毫秒时间戳 → 天然时间有序,按创建时间排序/分页无需额外索引; // - 后 80bit 随机数 → 跨双实例(ADR-015 单机双进程)并发生成不冲突,无需 workerId 协调; // - 全项目所有域外键(student_id/teacher_id/owner_student_id/creator…)一律引用 ULID, // **禁止**引用旧 int user_id(旧 int 只存 Legacy Mapping 表做迁移对照)。 // // 典型使用: // // // Service 层创建用户 // uid, err := id.NewCanonicalUserID() // if err != nil { return nil, errors.WithCause(errors.ErrInternal, err) } // u := &model.User{CanonicalUserID: uid, ...} // // // 生成后必须立刻落库;若事务回滚则不得复用该 ULID 之外的副作用 package id import ( "crypto/rand" "errors" "time" "github.com/oklog/ulid" ) // Length canonical_user_id 的固定长度(26 字符),用于校验/建表注释。 // 使用方:DDL 注释、入参校验(validate:"len=26")、前端展示截断判断。 const Length = 26 // ErrInvalid 校验失败:入参不是合法的 26 字符 Crockford ULID。 var ErrInvalid = errors.New("id: 非法的 canonical_user_id(须为 26 字符 Crockford Base32)") // NewCanonicalUserID 生成一个新的 canonical_user_id(以当前时刻为时间戳)。 // // 入参:无。 // // 出参: // - string:26 字符大写 Crockford Base32 字符串(如 "01GAQF0Z6C1V9Y3E5H7K2M8B4T")。 // - error:理论上恒为 nil(内部用 crypto/rand,仅当系统熵源枯竭时出错—— // 视为系统级故障,调用方按 errors.ErrInternal 上抛,**禁止**回退到 math/rand // (可预测 = 可被枚举伪造身份,安全红线))。 // // 样例: // // uid, _ := id.NewCanonicalUserID() // // "01GAQF0Z6C1V9Y3E5H7K2M8B4T" func NewCanonicalUserID() (string, error) { return NewCanonicalUserIDAt(time.Now()) } // NewCanonicalUserIDAt 以指定时刻为时间戳生成 ULID(测试与回放场景: // 需要"某个历史时刻"的 ID 用于数据修复/割接对账)。 // // 入参: // - now time.Time:作为 ULID 时间戳的时刻(毫秒精度,微秒/纳秒部分被截断)。 // // 出参: // - string:26 字符 ULID(同一毫秒内生成多个时,随机部分保证唯一)。 // - error:同 NewCanonicalUserID(熵源故障才非 nil)。 // // 异常场景: // - 同一毫秒内高并发调用:随机部分(80bit)保证唯一性,无需加锁; // 若需严格递增(同一毫秒内按调用序),调用方自行加序(本包不提供—— // 跨实例严格递增与 ADR-015 双实例形态冲突,不引入该复杂度)。 func NewCanonicalUserIDAt(now time.Time) (string, error) { // ulid.Timestamp 把 time.Time 转为毫秒时间戳嵌入 ID 前 48bit // rand.Reader 提供后 80bit 随机熵(crypto/rand,非 math/rand) id, err := ulid.New(ulid.Timestamp(now), rand.Reader) if err != nil { return "", err } return id.String(), nil } // Validate 校验字符串是否为合法的 canonical_user_id(入参防御: // 前端/Agent 回传的 ULID 在进库前统一校验,防脏数据写进外键列)。 // // 入参: // - s string:待校验字符串(应为 26 字符;小写视为非法——本项目统一大写存储)。 // // 出参: // - bool:true 表示合法(长度 26 且全部为 Crockford Base32 合法字符 0-9/A-Z 去掉 I,L,O,U); // false 表示长度不符或含非法字符。 // // 说明:不做"时间戳可解码"校验(ulid.Parse 会接受任意合法编码串, // 但业务上"乱序/未来时间戳的 ID"也属合法输入,不在此层拦截—— // 时间戳异常属于审计问题,由数据质量报表处理)。 // // 样例: // // id.Validate("01GAQF0Z6C1V9Y3E5H7K2M8B4T") // true // id.Validate("abc") // false(长度) // id.Validate("01GAQF0Z6C1V9Y3E5H7K2M8B4I") // false(I 不是 Crockford 字符) func Validate(s string) bool { if len(s) != Length { return false } for i := 0; i < Length; i++ { c := s[i] switch { case c >= '0' && c <= '9': case c >= 'A' && c <= 'Z': // 剔除 Crockford 中易混淆的 I/L/O/U if c == 'I' || c == 'L' || c == 'O' || c == 'U' { return false } default: return false } } return true }