| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113 |
- // 用户显示名批量只读仓储(identity 侧,实现 academic 的 `port.UserNameReader`)。
- //
- // 为什么实现放在这里而不是 academic:`t_user` 的属主是 identity 域;academic 只声明**接口**
- // (`internal/academic/port/user_adapter.go`),实现与 SQL 留在本包,依赖方向保持
- // `academic → port ← identity`(装配层注入,academic 对 identity 零 import)。
- //
- // 契约细节(与 `port.UserNameReader` 逐条对齐):
- // - **批量**:一页最多 200 个学生,逐条查会 200 次往返;
- // - 查不到的 userID **不进 map**(不塞空串)——调用方据此下发 `null` 而不是 `""`,
- // 前端才能区分"没有这个人"与"姓名为空";
- // - 只读:本文件只有 SELECT,没有任何写路径。
- package repository
- import (
- "context"
- "strings"
- "gorm.io/gorm"
- "yuansiwei/unified-backend/internal/identity/model"
- )
- // userNameBatchLimit 单次批量查询的 userID 上限(防一条 IN 列表过长打爆 SQL)。
- //
- // 200 = academic 分页上限(`repository.MaxPageSize`),与调用方一页行数一致;
- // 超过则分批查询(本方法内部循环,调用方无感)。
- const userNameBatchLimit = 200
- // UserNameRepo 用户显示名只读仓储。
- type UserNameRepo struct {
- // db 连接池句柄(只读用途;不持有事务,展示类查询不需要快照一致性)。
- db *gorm.DB
- }
- // NewUserNameRepo 构造显示名仓储。
- //
- // 入参:db *gorm.DB(必填)。
- // 出参:*UserNameRepo —— 结构化满足 `academic/port.UserNameReader`(装配层做编译期断言)。
- func NewUserNameRepo(db *gorm.DB) *UserNameRepo {
- return &UserNameRepo{db: db}
- }
- // DisplayNames 批量取显示名(`t_user.display_name`)。
- //
- // 入参:ctx;userIDs 用户 ULID 列表(可重复/可空)。
- // 出参:map[ULID]显示名(仅含"查得到且非空"的项);error 为 DB 错误。
- //
- // 取数口径(**这是 D4 的唯一口径点**):
- // - 列 = `display_name`(identity 域用户表**唯一**的姓名列;不存在 `real_name` 列);
- // - 值为 NULL 或全空白 ⇒ **不进 map**(让调用方下发 `null`,与"空字符串"区分);
- // - 不做任何格式化/拼接(拼接出来的"姓名"无法反查,属伪造数据)。
- func (r *UserNameRepo) DisplayNames(ctx context.Context, userIDs []string) (map[string]string, error) {
- out := make(map[string]string, len(userIDs))
- ids := dedupNonEmpty(userIDs)
- if len(ids) == 0 {
- return out, nil
- }
- if r == nil || r.db == nil {
- return nil, gorm.ErrInvalidDB
- }
- db := r.db
- if ctx != nil {
- db = db.WithContext(ctx)
- }
- for start := 0; start < len(ids); start += userNameBatchLimit {
- end := start + userNameBatchLimit
- if end > len(ids) {
- end = len(ids)
- }
- rows := make([]nameRow, 0, end-start)
- if err := db.Model(&model.User{}).
- Select("canonical_user_id", "display_name").
- Where("canonical_user_id IN ?", ids[start:end]).
- Find(&rows).Error; err != nil {
- return nil, err
- }
- for i := range rows {
- if rows[i].DisplayName == nil {
- continue // NULL:不放进 map(调用方下发 null)
- }
- name := strings.TrimSpace(*rows[i].DisplayName)
- if name == "" {
- continue // 空白:同样按"没有名字"处理
- }
- out[strings.TrimSpace(rows[i].CanonicalUserID)] = name
- }
- }
- return out, nil
- }
- // nameRow `t_user` 的两列投影(不 SELECT *:用户表可能有凭据类列,越少出库越好)。
- type nameRow struct {
- CanonicalUserID string `gorm:"column:canonical_user_id"`
- DisplayName *string `gorm:"column:display_name"`
- }
- // dedupNonEmpty 去重并剔除空白(IN 列表净化,保持首次出现顺序)。
- func dedupNonEmpty(ids []string) []string {
- out := make([]string, 0, len(ids))
- seen := make(map[string]struct{}, len(ids))
- for _, id := range ids {
- trimmed := strings.TrimSpace(id)
- if trimmed == "" {
- continue
- }
- if _, dup := seen[trimmed]; dup {
- continue
- }
- seen[trimmed] = struct{}{}
- out = append(out, trimmed)
- }
- return out
- }
|