| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115 |
- // 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
- }
|