// 用户姓名只读端口(academic → identity 单向只读,D4 裁决)——**接口在此,实现在 identity 侧**。 // // --------------------------------------------------------------------------- // 为什么需要它 // --------------------------------------------------------------------------- // // 端点 53(报课学生列表)契约要求 `realName`。姓名列在 `t_user.display_name`(identity 域), // academic 域**不持有** `t_user`,而"报课学生列表没有姓名"等于页面不可用 ⇒ 必须跨域只读。 // // --------------------------------------------------------------------------- // 依赖方向(设计 §1.3 铁律) // --------------------------------------------------------------------------- // // academic/handler → academic/port(**接口**) ← identity/repository(**实现**) // ↑ // 装配层注入(cmd/api/wiring_academic.go) // // 三条硬约束(评审逐条检查): // // 1. **academic 不 import identity**(本文件只定义接口与文档,零业务 import); // 2. 实现放 identity 侧(那里才是 `t_user` 的属主),本包不写任何 `t_user` SQL; // 3. 端口**只读**(`SELECT`),绝不跨域写。 // // 与 `MembershipChecker` / `CampusReader` 的区别:那两个是**越权判定**依赖(缺失会放宽安全), // 本端口只是**展示**依赖 —— 未注入时页面少一列姓名,不影响任何判据(因此可以是 nil)。 package port import "context" // UserNameReader 用户显示名只读端口(端点 53 的 `realName`)。 // // 契约(实现方必须遵守): // - **批量**取(一页最多 200 个学生 ⇒ 禁止逐条查询); // - 返回 `map[userID]displayName`;**查不到的 userID 不要放进 map**(不要塞空串), // 调用方据此区分"没有这个人 / 姓名为空"并下发 `null`; // - 任一 DB 错误如实返回,调用方决定是降级(下发 null)还是整体失败。 type UserNameReader interface { // DisplayNames 批量取显示名。 // // 入参:ctx 请求上下文;userIDs 用户 ULID 列表(可重复、可空)。 // 出参:map[ULID]显示名(仅含查到的);error 非 nil 表示查询失败。 DisplayNames(ctx context.Context, userIDs []string) (map[string]string, error) }