# 模块理解确认书 — iam

> AI：ai02（TS / 身份认证）
> 阶段：阶段 1 交付物
> 日期：2026-07-09
> 关联：[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[AI 分配方案](../../../docs/architecture/ai-allocation.md)、[pending-features P2](../../../docs/architecture/roadmap/pending-features.md)

---

## 1. 我在架构中的位置

- **层级**：L5 业务微服务层（004 §3.1 六层架构）
- **业务领域**：**D1 身份认证领域**（004 §1.1b），独立限界上下文，不归属其他领域
- **上游（谁调用我）**：
  - `api-gateway`（Go/Gin）：HTTP 反向代理 `/api/v1/iam/*` → `http://iam:3002/iam/*`（已注册，见 [api-gateway/main.go](../../api-gateway/main.go) 第 76-83 行）
  - `teacher-bff`（NestJS）：HTTP 调用 `/iam/me`、`/iam/viewports` 聚合教师身份与视口（见 [teacher-bff/teacher.service.ts](../../teacher-bff/src/teacher/teacher.service.ts) 第 30/78 行）
  - 未来：`student-bff`、`parent-bff`（同样走 HTTP 聚合）
- **下游（我调用谁）**：
  - `MySQL`（独占库 `iam_db`，连接串 `DATABASE_URL`）
  - `Redis`（P2 待启用：权限缓存 + refresh token 黑名单，env 已留 `REDIS_URL` 占位）
  - `Kafka`（P2 待启用：发布 `edu.identity.user.*` 事件，由 core-edu/msg 消费）
- **通信方式**：
  - 入口：**HTTP/REST**（当前实现，端口 3002）
  - 出口：MySQL（mysql2 连接池 + Drizzle ORM）、Redis（待接）、Kafka（待接）
  - 演进：P3 起对外暴露 gRPC（`iam.proto` 已定义 `IamService`，REST 与 gRPC 并存过渡期）
- **不持有跨服务状态**：会话/token 黑名单放 Redis，不落本地内存

## 2. 我的限界上下文

### 2.1 我负责的聚合 / 实体

| 聚合根                              | 实体 / 值对象         | 当前表                 | 职责                                 |
| ----------------------------------- | --------------------- | ---------------------- | ------------------------------------ |
| **User**（用户）                    | UserStatus、DataScope | `iam_users`            | 注册、登录、密码校验、用户信息查询   |
| **Role**（角色）                    | —                     | `iam_roles`            | 三层角色模型（系统/组织/临时）的载体 |
| **Permission**（权限点）            | resource + action     | `iam_permissions`      | 权限点常量集中表                     |
| **RolePermission**（角色-权限映射） | —                     | `iam_role_permissions` | 多对多，权限并集来源                 |
| **UserRole**（用户-角色绑定）       | —                     | `iam_user_roles`       | 用户拥有的多层角色                   |
| **RoleViewport**（角色-视口配置）   | componentConfig(JSON) | `iam_role_viewports`   | 4 层视口模型的 L1 导航 + L3 组件配置 |
| **RefreshToken**（刷新令牌）        | tokenHash + revokedAt | `iam_refresh_tokens`   | refresh token 持久化 + 轮换/撤销     |

### 2.2 业务领域归属

- **D1 身份认证领域**（004 §1.1b）：iam 独占此领域
- 上游依赖：无（身份认证是整个系统的权限中枢，不依赖其他业务领域）
- 下游被依赖：core-edu（消费 `UserRegistered` 初始化默认班级关联）、msg（消费 `UserRegistered` 发欢迎通知）

### 2.3 我不负责什么（边界外）

- ❌ 班级/学科/年级数据 → `core-edu`（D2 教学组织）
- ❌ 考试/作业/成绩 → `core-edu`（D3 教学核心）
- ❌ 教材/知识点/题库 → `content`（D4 内容资源）
- ❌ 站内信/通知投递 → `msg`（D5 沟通通知）
- ❌ 学情分析/掌握度计算 → `data-ana`（D6 智能洞察）
- ❌ WebSocket 长连接管理 → `push-gateway`
- ❌ JWT 公钥校验 → `api-gateway`（iam 只签发，不校验）
- ❌ 前端权限 Hook → `packages/ui-components/hooks/use-permission.ts`（前端通过 BFF 拉取，不直连 iam）

## 3. 我与外部的契约

### 3.1 我消费的 proto message（从 shared-proto）

- 当前**不消费**任何外部 proto（iam 是身份认证源头，不反向依赖业务服务）
- 未来 P3 转 gRPC 时，消费 `google/protobuf/timestamp.proto`（时间字段标准化）

### 3.2 我暴露的契约

#### 当前 REST API（已实现，见 [iam.controller.ts](../src/iam/iam.controller.ts)、[rbac.controller.ts](../src/iam/rbac.controller.ts)）

| Method | Path                         | 权限              | 说明                                  |
| ------ | ---------------------------- | ----------------- | ------------------------------------- |
| POST   | `/iam/register`              | 公开              | 注册（自动分配 teacher 角色）         |
| POST   | `/iam/login`                 | 公开              | 登录，返回 accessToken + refreshToken |
| POST   | `/iam/refresh`               | 公开              | 刷新令牌                              |
| GET    | `/iam/me`                    | `IAM_USER_READ`   | 当前用户信息（读 `x-user-id` 头）     |
| GET    | `/iam/viewports`             | `IAM_USER_READ`   | 当前用户视口（L1 导航，按权限过滤）   |
| GET    | `/iam/permissions/effective` | `IAM_USER_READ`   | 当前用户有效权限（多角色去重）        |
| GET    | `/iam/roles`                 | `IAM_ROLE_MANAGE` | 所有角色列表（管理端）                |
| GET    | `/iam/permissions`           | `IAM_ROLE_MANAGE` | 所有权限点列表（管理端）              |

#### 健康检查（[health.controller.ts](../src/shared/health/health.controller.ts)）

| Method | Path       | 鉴权 | 用途                                                             |
| ------ | ---------- | ---- | ---------------------------------------------------------------- |
| GET    | `/healthz` | 无   | liveness，进程存活                                               |
| GET    | `/readyz`  | 无   | readiness，校验 DB `SELECT 1`，失败 503                          |
| GET    | `/metrics` | 无   | Prometheus 指标抓取（[main.ts](../src/main.ts) 第 23-26 行注册） |

#### Proto 契约（[iam.proto](../../../packages/shared-proto/proto/iam.proto)）

```protobuf
package next_edu_cloud.iam.v1;
service IamService {
  rpc Register(RegisterRequest) returns (AuthResponse);
  rpc Login(LoginRequest) returns (AuthResponse);
  rpc RefreshToken(RefreshTokenRequest) returns (TokenPair);
  rpc GetUserInfo(GetUserInfoRequest) returns (UserInfo);
}
```

> **缺口**：proto 仅定义 4 个 RPC，REST 已实现的 `viewports`、`permissions/effective`、`roles`、`permissions` 未在 proto 中体现。P2 阶段 2 设计文档需补齐 `GetViewports`、`GetEffectivePermissions`、`ListRoles`、`ListPermissions`、`Logout`、`GetPublicKey`（RS256 公钥暴露端点）等 RPC。

#### 我发布的领域事件（P2 待实现 Outbox）

| 事件                           | 触发时机      | Topic（遵循 004 §7.2）           | 消费者                                            |
| ------------------------------ | ------------- | -------------------------------- | ------------------------------------------------- |
| `UserRegistered`               | 注册成功      | `edu.identity.user.created`      | core-edu（初始化默认班级关联）、msg（发欢迎通知） |
| `UserUpdated`                  | 用户信息变更  | `edu.identity.user.updated`      | core-edu、msg                                     |
| `UserDeleted` / `UserDisabled` | 用户注销/禁用 | `edu.identity.user.deleted`      | core-edu、msg（清理关联）                         |
| `UserRoleChanged`              | 角色绑定变更  | `edu.identity.user.role_changed` | 自身缓存失效、msg（审计）                         |

> **Topic 命名遵循 004 §7.2**：`edu.identity.user.created` / `edu.identity.user.updated`。

#### 我消费的事件

- 当前：**无**
- 未来：不主动消费业务事件（iam 是权限中枢，不订阅其他领域事件）

### 3.3 错误码前缀

- **前缀**：`IAM_`（见 [application-error.ts](../src/shared/errors/application-error.ts)）
- 已定义错误码：

| 错误码                  | HTTP | 触发条件                     |
| ----------------------- | ---- | ---------------------------- |
| `IAM_VALIDATION_ERROR`  | 400  | Zod 校验失败 / 字段非法      |
| `IAM_UNAUTHORIZED`      | 401  | 未登录、密码错误、token 失效 |
| `IAM_PERMISSION_DENIED` | 403  | 缺少所需权限点               |
| `IAM_NOT_FOUND`         | 404  | 用户/角色/权限不存在         |
| `IAM_CONFLICT`          | 409  | 邮箱已注册、角色名重复       |
| `IAM_BUSINESS_ERROR`    | 422  | 业务规则违反（如账号禁用）   |
| `IAM_DATABASE_ERROR`    | 500  | Drizzle 操作失败             |
| `IAM_INTERNAL_ERROR`    | 500  | 未预期异常                   |

> **全局错误格式**：`{ success: false, error: { code, message, details?, traceId } }`，由 [GlobalErrorFilter](../src/shared/errors/global-error.filter.ts) 统一兜底，traceId 从 `x-request-id` 头注入。

## 4. 我的技术栈

| 维度     | 选型                  | 版本           | 备注                                                                            |
| -------- | --------------------- | -------------- | ------------------------------------------------------------------------------- |
| 语言     | TypeScript（ESM）     | 5.6+           | `tsconfig.json` 用 `NodeNext` + `incremental: false`（避免 nest watch 不 emit） |
| 框架     | NestJS                | 10.4+          | 装饰器 + DI，`@nestjs/platform-express`                                         |
| ORM      | Drizzle ORM（mysql2） | 0.31+          | `getDb()` 单例池，无 typeorm DataSource 依赖                                    |
| 数据库   | MySQL 8               | —              | 独占库 `iam_db`，连接池 connectionLimit=10                                      |
| 缓存     | Redis（P2 待接）      | 7              | 权限缓存 TTL 5min + refresh token 黑名单                                        |
| 密码哈希 | bcrypt                | 5.1+           | cost ≥ 12（已对齐 project_rules §4）                                            |
| JWT      | jsonwebtoken          | 9.0+           | **当前 HS256，P2 必须切 RS256**                                                 |
| 日志     | pino                  | 9.4+           | 结构化 JSON，`service: iam` base 字段                                           |
| 指标     | prom-client           | 15.1+          | `iam_requests_total` + `iam_request_duration_seconds`                           |
| 链路     | OpenTelemetry SDK     | 0.53+          | auto-instrumentations + OTLP HTTP exporter                                      |
| 输入校验 | Zod                   | 3.23+          | Controller 层 `schema.parse(body)`                                              |
| 测试     | Vitest                | 2.1+           | **当前缺失，P2 必须补齐**                                                       |
| 容器     | Dockerfile 多阶段     | node:20-alpine | builder + runtime，EXPOSE 3002                                                  |

## 5. 我的阶段归属

- **阶段**：**P2 身份**（M4-M6）
- **当前阶段目标**（pending-features §P2）：
  1. ✅ 已实现骨架：注册/登录/刷新/me/viewports/effective permissions/roles/permissions 列表
  2. ❌ **RS256 非对称签名**（当前仍是 HS256，env.JWT_SECRET 单密钥）
  3. ❌ **refresh_token 轮换 + Redis 黑名单失效**（当前只存 hash，未实现撤销检查）
  4. ❌ **权限缓存**（`getEffectivePermissions` 结果 Redis 缓存 TTL 5min，角色变更主动失效）
  5. ❌ **Outbox 事件发布**（`UserRegistered` / `UserUpdated` / `UserRoleChanged`）
  6. ❌ **2FA**（pending-features §P2 提及但优先级低）
  7. ❌ **补全数据表**：`parent_student_relations`、`class_subject_teachers`（pending-features §P2 schema 清单）
  8. ❌ **gRPC 化**（iam.proto 已定义 4 RPC，但服务尚未实现 gRPC server）
  9. ❌ **测试覆盖**（classes 有 `test/unit/classes.service.test.ts`，iam 无任何测试）
  10. ❌ **RBAC CRUD 完整化**（当前只有读，缺角色/权限/视口的增删改）

- **依赖的上游阶段产出**：
  - P1 ✅ api-gateway（已注册 `/iam/*` 路由，已透传 `x-user-id`/`x-user-roles` 头）
  - P1 ✅ classes 黄金模板（横切关注点模板：错误处理/可观测/健康检查/优雅关闭）
  - P1 ✅ shared-proto（iam.proto 已定义基础 4 RPC）

- **P2 退出标准**（pending-features §P2 + 004 §14.2）：
  - 教师登录 → 获取 JWT（RS256）→ 访问 teacher-portal → 侧边栏按 `viewports.L1` 渲染 → 看到空白 Dashboard
  - 打 tag `v0.2.0-p2`

## 6. 我需要对齐的黄金模板项（对照 classes 服务）

> 参照 [classes 黄金模板](../../classes/src/) 全部源码与 [known-issues §2.2 classes](../../../docs/troubleshooting/known-issues.md) 经验。

| 对齐项                                  | classes 状态                                         | iam 当前状态                                                                                                 | iam 待补齐                                                                                                             |
| --------------------------------------- | ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------- |
| **权限装饰器 `@RequirePermission`**     | ✅ 每个 Handler 都有                                 | ⚠️ Handler 有装饰器，但 `PermissionGuard` 用本地硬编码 `ROLE_PERMISSIONS` map（admin/teacher），未走 DB 查询 | P2 改为 DB 驱动：Guard 调用 `IamService.getEffectivePermissions(userId)`（从 Redis 缓存读取）                          |
| **错误码前缀统一**                      | ✅ `CLASSES_*`                                       | ✅ `IAM_*`                                                                                                   | 无                                                                                                                     |
| **logger（pino）**                      | ✅ `shared/observability/logger.ts`                  | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **metrics（prom-client）**              | ✅ `*_requests_total` + `*_request_duration_seconds` | ✅ `iam_requests_total` + `iam_request_duration_seconds`                                                     | 无                                                                                                                     |
| **tracer（OTel）**                      | ✅ auto-instrumentations + OTLP                      | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **`/healthz` + `/readyz`**              | ✅ Drizzle `SELECT 1`                                | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **优雅关闭（SIGTERM）**                 | ✅ `LifecycleService` 关闭 DB 池                     | ✅ 同模板                                                                                                    | P2 接入 Redis/Kafka 后需补关闭顺序：HTTP → Kafka → Redis → DB                                                          |
| **测试覆盖率 ≥ 80%**                    | ✅ `test/unit/classes.service.test.ts`               | ❌ **完全缺失**                                                                                              | P2 必须补齐：`test/unit/iam.service.test.ts`、`test/unit/iam.repository.test.ts`、`test/unit/permission.guard.test.ts` |
| **Dockerfile 多阶段构建**               | ✅ builder + runtime                                 | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **Zod 输入验证**                        | ✅ Controller 层 `schema.parse(body)`                | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **GlobalErrorFilter**                   | ✅ 注册到 `main.ts`                                  | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **ESM `.js` 后缀**                      | ✅ 相对 import 带 `.js`                              | ✅ 同模板                                                                                                    | 无                                                                                                                     |
| **`tsconfig.json` incremental: false**  | ✅                                                   | ✅                                                                                                           | 无                                                                                                                     |
| **AppModule 显式 imports HealthModule** | ✅                                                   | ✅                                                                                                           | 无                                                                                                                     |
| **Drizzle `getDb()` 单例**              | ✅                                                   | ✅                                                                                                           | 无                                                                                                                     |
| **Controller 读 `x-user-id` 头**        | ✅                                                   | ✅                                                                                                           | 无                                                                                                                     |

### 6.1 当前 iam 与 classes 黄金模板的差异点（需在阶段 2 设计文档中明确处理）

1. **PermissionGuard 数据源**：classes 用本地 `ROLE_PERMISSIONS` map（够用，因为 classes 权限点固定），iam **必须改为 DB 驱动**——因为 iam 自身就是权限中枢，权限点/RBAC 是动态的，硬编码会导致角色变更不生效。
2. **AuthMiddleware 未注册**：[app.module.ts](../src/app.module.ts) 只注册了 `PermissionGuard` 作为 `APP_GUARD`，`AuthMiddleware` 未在 `AppModule.configure()` 中消费。Controller 直接从 `req.headers['x-user-id']` 读取，这是 known-issues §2.3 记录的决策。P2 设计文档需明确：要么注册 AuthMiddleware，要么继续走 header 直读（当前选择后者，与 Gateway 透传策略一致）。
3. **JWT 签名算法**：classes 不签发 JWT（只校验），iam 是**唯一签发方**，必须切 RS256 并暴露公钥端点 `/iam/.well-known/jwks.json` 或 `/iam/public-key`。
4. **Outbox 模式**：classes 无 Outbox（P3 才引入），iam P2 需要引入 Outbox 发布用户事件——需要 coord 在 shared-ts 中提供 Outbox 工具或 iam 自建（参照 core-edu P3 设计）。

## 7. 服务审计表（ai02 自检）

> 依照 ai-allocation §10 模板，对当前 iam 已实现代码进行审计。

| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
| ---- | ---------- | ---------- | ------ | ------- | ------ | -------- | ------- | -------- | ---------- | ---------- |
| iam  | ⚠️         | ✅         | ✅     | ✅      | ✅     | ✅       | ✅      | ✅       | 0% ❌      | ✅         |

**说明**：

- 权限装饰器 ⚠️：装饰器已挂载到每个 Handler，但 Guard 用本地硬编码 `ROLE_PERMISSIONS`，未走 DB（与 iam 作为权限中枢的职责矛盾）
- 测试覆盖率 0%：[services/iam/](../) 下无 `test/` 目录，Vitest 配置缺失（classes 有 `vitest.config.ts` + `test/unit/`）

---

## 8. 待 coord 交叉审查的关键决策点

以下决策需在阶段 2 设计文档中明确，并在 coord 交叉审查时确认：

1. **RS256 密钥管理**：P2 用本地文件 `/opt/edu/keys/iam-private.pem` + `/opt/edu/keys/iam-public.pem`，还是引入 Vault（P6 才落地）？建议 P2 用本地文件 + 环境变量 `IAM_PRIVATE_KEY_PATH`，P6 迁 Vault。
2. **公钥暴露端点**：用 `GET /iam/.well-known/jwks.json`（JWK Set 标准，支持密钥轮换）还是 `GET /iam/public-key`（简单 PEM）？建议前者，为未来密钥轮换留余地。
3. **权限缓存失效策略**：角色变更时主动 `DEL iam:perms:{userId}`，还是发 `UserRoleChanged` 事件让消费者自行失效？建议两者都做：本地服务内主动 DEL（同步），事件供其他服务感知。
4. **Outbox 实现位置**：iam 自建 `iam_outbox` 表 + relay worker，还是复用 shared-ts 提供的通用 Outbox 工具？**需 coord 确认 shared-ts 是否在 P2 提供 Outbox 工具**。若未提供，iam 自建轻量 Outbox（参照 core-edu P3 设计，但要先于 core-edu 落地）。
5. **gRPC 与 REST 并存策略**：P2 是否同步实现 gRPC server？还是 P2 仅 REST，P3 再补 gRPC？建议 P2 仅 REST（保证退出标准达成），gRPC server 在 P3 随 core-edu 一起引入（此时 shared-ts 也有 gRPC 工具沉淀）。
6. **DataScope 枚举对齐**：当前 schema 用 `self/class/grade/school/district/all`，004 §5.3 用 `L0-SELF` ~ `L5-ALL`。建议保持 schema 字符串枚举（DB 友好），在 API/proto 层用 `L0`~`L5` 数值枚举映射，业务代码用语义常量。
7. **parent_student_relations 表归属**：pending-features §P2 schema 清单把此表放在 iam，但 004 §5.4 提到家长场景域由 parent-bff 聚合。需确认：此表是 iam 管理（家长-学生关系是身份关系），还是 core-edu 管理（教学组织关系）？**建议归 iam**（身份关系优先于教学组织）。
8. **`class_subject_teachers` 表归属**：同上，此表横跨 iam（教师身份）与 core-edu（班级/学科）。pending-features §P2 放 iam，但 core-edu 的 classes 模块也需要。**建议归 core-edu**（班级-学科-教师是教学组织数据），iam 只存 `userId` 与角色，不存具体任教关系。

---

**AI Agent**: ai02 (iam-module)
**Branch**: main（单仓库并行模式，见 ai-allocation §9.1）
**Coordinator**: coord-ai
