id.go 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. // Package id 生成 canonical_user_id(ULID,ADR-013)。
  2. //
  3. // 背景:两套 PHP 旧系统各自用自增 int 作 user 主键,合并后必须一个跨系统、
  4. // 跨实例、可排序、可对外暴露的身份主键。ADR-013 拍板 ULID:
  5. // - 26 字符 Crockford Base32(大写),存 VARCHAR(26);
  6. // - 前 48bit 毫秒时间戳 → 天然时间有序,按创建时间排序/分页无需额外索引;
  7. // - 后 80bit 随机数 → 跨双实例(ADR-015 单机双进程)并发生成不冲突,无需 workerId 协调;
  8. // - 全项目所有域外键(student_id/teacher_id/owner_student_id/creator…)一律引用 ULID,
  9. // **禁止**引用旧 int user_id(旧 int 只存 Legacy Mapping 表做迁移对照)。
  10. //
  11. // 典型使用:
  12. //
  13. // // Service 层创建用户
  14. // uid, err := id.NewCanonicalUserID()
  15. // if err != nil { return nil, errors.WithCause(errors.ErrInternal, err) }
  16. // u := &model.User{CanonicalUserID: uid, ...}
  17. //
  18. // // 生成后必须立刻落库;若事务回滚则不得复用该 ULID 之外的副作用
  19. package id
  20. import (
  21. "crypto/rand"
  22. "errors"
  23. "time"
  24. "github.com/oklog/ulid"
  25. )
  26. // Length canonical_user_id 的固定长度(26 字符),用于校验/建表注释。
  27. // 使用方:DDL 注释、入参校验(validate:"len=26")、前端展示截断判断。
  28. const Length = 26
  29. // ErrInvalid 校验失败:入参不是合法的 26 字符 Crockford ULID。
  30. var ErrInvalid = errors.New("id: 非法的 canonical_user_id(须为 26 字符 Crockford Base32)")
  31. // NewCanonicalUserID 生成一个新的 canonical_user_id(以当前时刻为时间戳)。
  32. //
  33. // 入参:无。
  34. //
  35. // 出参:
  36. // - string:26 字符大写 Crockford Base32 字符串(如 "01GAQF0Z6C1V9Y3E5H7K2M8B4T")。
  37. // - error:理论上恒为 nil(内部用 crypto/rand,仅当系统熵源枯竭时出错——
  38. // 视为系统级故障,调用方按 errors.ErrInternal 上抛,**禁止**回退到 math/rand
  39. // (可预测 = 可被枚举伪造身份,安全红线))。
  40. //
  41. // 样例:
  42. //
  43. // uid, _ := id.NewCanonicalUserID()
  44. // // "01GAQF0Z6C1V9Y3E5H7K2M8B4T"
  45. func NewCanonicalUserID() (string, error) {
  46. return NewCanonicalUserIDAt(time.Now())
  47. }
  48. // NewCanonicalUserIDAt 以指定时刻为时间戳生成 ULID(测试与回放场景:
  49. // 需要"某个历史时刻"的 ID 用于数据修复/割接对账)。
  50. //
  51. // 入参:
  52. // - now time.Time:作为 ULID 时间戳的时刻(毫秒精度,微秒/纳秒部分被截断)。
  53. //
  54. // 出参:
  55. // - string:26 字符 ULID(同一毫秒内生成多个时,随机部分保证唯一)。
  56. // - error:同 NewCanonicalUserID(熵源故障才非 nil)。
  57. //
  58. // 异常场景:
  59. // - 同一毫秒内高并发调用:随机部分(80bit)保证唯一性,无需加锁;
  60. // 若需严格递增(同一毫秒内按调用序),调用方自行加序(本包不提供——
  61. // 跨实例严格递增与 ADR-015 双实例形态冲突,不引入该复杂度)。
  62. func NewCanonicalUserIDAt(now time.Time) (string, error) {
  63. // ulid.Timestamp 把 time.Time 转为毫秒时间戳嵌入 ID 前 48bit
  64. // rand.Reader 提供后 80bit 随机熵(crypto/rand,非 math/rand)
  65. id, err := ulid.New(ulid.Timestamp(now), rand.Reader)
  66. if err != nil {
  67. return "", err
  68. }
  69. return id.String(), nil
  70. }
  71. // Validate 校验字符串是否为合法的 canonical_user_id(入参防御:
  72. // 前端/Agent 回传的 ULID 在进库前统一校验,防脏数据写进外键列)。
  73. //
  74. // 入参:
  75. // - s string:待校验字符串(应为 26 字符;小写视为非法——本项目统一大写存储)。
  76. //
  77. // 出参:
  78. // - bool:true 表示合法(长度 26 且全部为 Crockford Base32 合法字符 0-9/A-Z 去掉 I,L,O,U);
  79. // false 表示长度不符或含非法字符。
  80. //
  81. // 说明:不做"时间戳可解码"校验(ulid.Parse 会接受任意合法编码串,
  82. // 但业务上"乱序/未来时间戳的 ID"也属合法输入,不在此层拦截——
  83. // 时间戳异常属于审计问题,由数据质量报表处理)。
  84. //
  85. // 样例:
  86. //
  87. // id.Validate("01GAQF0Z6C1V9Y3E5H7K2M8B4T") // true
  88. // id.Validate("abc") // false(长度)
  89. // id.Validate("01GAQF0Z6C1V9Y3E5H7K2M8B4I") // false(I 不是 Crockford 字符)
  90. func Validate(s string) bool {
  91. if len(s) != Length {
  92. return false
  93. }
  94. for i := 0; i < Length; i++ {
  95. c := s[i]
  96. switch {
  97. case c >= '0' && c <= '9':
  98. case c >= 'A' && c <= 'Z':
  99. // 剔除 Crockford 中易混淆的 I/L/O/U
  100. if c == 'I' || c == 'L' || c == 'O' || c == 'U' {
  101. return false
  102. }
  103. default:
  104. return false
  105. }
  106. }
  107. return true
  108. }