user_name_repo.go 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. // 用户显示名批量只读仓储(identity 侧,实现 academic 的 `port.UserNameReader`)。
  2. //
  3. // 为什么实现放在这里而不是 academic:`t_user` 的属主是 identity 域;academic 只声明**接口**
  4. // (`internal/academic/port/user_adapter.go`),实现与 SQL 留在本包,依赖方向保持
  5. // `academic → port ← identity`(装配层注入,academic 对 identity 零 import)。
  6. //
  7. // 契约细节(与 `port.UserNameReader` 逐条对齐):
  8. // - **批量**:一页最多 200 个学生,逐条查会 200 次往返;
  9. // - 查不到的 userID **不进 map**(不塞空串)——调用方据此下发 `null` 而不是 `""`,
  10. // 前端才能区分"没有这个人"与"姓名为空";
  11. // - 只读:本文件只有 SELECT,没有任何写路径。
  12. package repository
  13. import (
  14. "context"
  15. "strings"
  16. "gorm.io/gorm"
  17. "yuansiwei/unified-backend/internal/identity/model"
  18. )
  19. // userNameBatchLimit 单次批量查询的 userID 上限(防一条 IN 列表过长打爆 SQL)。
  20. //
  21. // 200 = academic 分页上限(`repository.MaxPageSize`),与调用方一页行数一致;
  22. // 超过则分批查询(本方法内部循环,调用方无感)。
  23. const userNameBatchLimit = 200
  24. // UserNameRepo 用户显示名只读仓储。
  25. type UserNameRepo struct {
  26. // db 连接池句柄(只读用途;不持有事务,展示类查询不需要快照一致性)。
  27. db *gorm.DB
  28. }
  29. // NewUserNameRepo 构造显示名仓储。
  30. //
  31. // 入参:db *gorm.DB(必填)。
  32. // 出参:*UserNameRepo —— 结构化满足 `academic/port.UserNameReader`(装配层做编译期断言)。
  33. func NewUserNameRepo(db *gorm.DB) *UserNameRepo {
  34. return &UserNameRepo{db: db}
  35. }
  36. // DisplayNames 批量取显示名(`t_user.display_name`)。
  37. //
  38. // 入参:ctx;userIDs 用户 ULID 列表(可重复/可空)。
  39. // 出参:map[ULID]显示名(仅含"查得到且非空"的项);error 为 DB 错误。
  40. //
  41. // 取数口径(**这是 D4 的唯一口径点**):
  42. // - 列 = `display_name`(identity 域用户表**唯一**的姓名列;不存在 `real_name` 列);
  43. // - 值为 NULL 或全空白 ⇒ **不进 map**(让调用方下发 `null`,与"空字符串"区分);
  44. // - 不做任何格式化/拼接(拼接出来的"姓名"无法反查,属伪造数据)。
  45. func (r *UserNameRepo) DisplayNames(ctx context.Context, userIDs []string) (map[string]string, error) {
  46. out := make(map[string]string, len(userIDs))
  47. ids := dedupNonEmpty(userIDs)
  48. if len(ids) == 0 {
  49. return out, nil
  50. }
  51. if r == nil || r.db == nil {
  52. return nil, gorm.ErrInvalidDB
  53. }
  54. db := r.db
  55. if ctx != nil {
  56. db = db.WithContext(ctx)
  57. }
  58. for start := 0; start < len(ids); start += userNameBatchLimit {
  59. end := start + userNameBatchLimit
  60. if end > len(ids) {
  61. end = len(ids)
  62. }
  63. rows := make([]nameRow, 0, end-start)
  64. if err := db.Model(&model.User{}).
  65. Select("canonical_user_id", "display_name").
  66. Where("canonical_user_id IN ?", ids[start:end]).
  67. Find(&rows).Error; err != nil {
  68. return nil, err
  69. }
  70. for i := range rows {
  71. if rows[i].DisplayName == nil {
  72. continue // NULL:不放进 map(调用方下发 null)
  73. }
  74. name := strings.TrimSpace(*rows[i].DisplayName)
  75. if name == "" {
  76. continue // 空白:同样按"没有名字"处理
  77. }
  78. out[strings.TrimSpace(rows[i].CanonicalUserID)] = name
  79. }
  80. }
  81. return out, nil
  82. }
  83. // nameRow `t_user` 的两列投影(不 SELECT *:用户表可能有凭据类列,越少出库越好)。
  84. type nameRow struct {
  85. CanonicalUserID string `gorm:"column:canonical_user_id"`
  86. DisplayName *string `gorm:"column:display_name"`
  87. }
  88. // dedupNonEmpty 去重并剔除空白(IN 列表净化,保持首次出现顺序)。
  89. func dedupNonEmpty(ids []string) []string {
  90. out := make([]string, 0, len(ids))
  91. seen := make(map[string]struct{}, len(ids))
  92. for _, id := range ids {
  93. trimmed := strings.TrimSpace(id)
  94. if trimmed == "" {
  95. continue
  96. }
  97. if _, dup := seen[trimmed]; dup {
  98. continue
  99. }
  100. seen[trimmed] = struct{}{}
  101. out = append(out, trimmed)
  102. }
  103. return out
  104. }