# 模块架构设计文档 — iam

> 模块：IAM（身份认证与权限）
> 版本：v1.0（最终态，对齐 coord-final-decisions I1-I8 + president §2.15/§2.16/§3.2/§5.5）
> 日期：2026-07-10
> 关联：
>
> - [004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)
> - [仲裁决策](../../../docs/architecture/coord-final-decisions.md)
> - [总裁裁决](../../../docs/architecture/president-final-rulings.md)
> - [iam 对接契约](../../../docs/architecture/issues/contracts/iam_contract.md)

---

## 1. 模块内部分层图

### 1.1 调用链总览

```mermaid
flowchart TB
    subgraph Client["客户端 / Gateway / BFF"]
        REST[HTTP 请求<br/>带 x-user-id / x-user-roles / x-user-data-scope 头]
        GRPC[gRPC 调用<br/>next_edu_cloud.iam.v1]
    end

    subgraph NestJS["iam 服务（NestJS Hybrid App）"]
        direction TB
        MW[AuthMiddleware<br/>已注册：me/logout/viewports/<br/>permissions/roles/audit/children]
        Guard[PermissionGuard<br/>APP_GUARD 全局守卫<br/>DB 驱动 + Redis 缓存]
        Filter[GlobalErrorFilter<br/>全局异常过滤器]

        subgraph Controllers["Controller 层（双入口）"]
            IamCtl[IamController<br/>REST /v1/iam/*]
            RbacCtl[RbacController<br/>REST /v1/iam/roles, /permissions]
            AuditCtl[AuditController<br/>REST /v1/iam/audit]
            JwksCtl[JwksController<br/>GET /v1/iam/.well-known/jwks.json]
            GrpcCtl[IamGrpcController<br/>12 RPC @ GrpcMethod]
        end

        subgraph Services["Application Service 层"]
            IamSvc[IamService<br/>认证 + RBAC + 审计编排<br/>REST 与 gRPC 共用]
            CacheSvc[PermissionCacheService<br/>Redis 权限缓存 TTL 5min]
            BlacklistSvc[TokenBlacklistService<br/>Redis jti 黑名单]
            JwksSvc[JwksService<br/>PEM→JWK 转换]
        end

        subgraph Repo["Repository 层"]
            IamRepo[IamRepository<br/>Drizzle 查询 10 表]
        end

        subgraph Outbox["Outbox 模块（shared-ts）"]
            OutboxPub[OutboxPublisher<br/>事务内写入]
            OutboxRelay[OutboxRelayWorker<br/>后台轮询 + Kafka 投递]
        end

        subgraph Infra["基础设施"]
            Db[(MySQL<br/>iam_db)]
            Redis[(Redis<br/>权限缓存 + token 黑名单)]
            Kafka[(Kafka<br/>edu.iam.* topics)]
        end
    end

    REST --> MW
    MW --> Guard
    GRPC --> Guard
    Guard --> Controllers
    Controllers --> IamSvc
    IamSvc --> IamRepo
    IamSvc --> CacheSvc
    IamSvc --> BlacklistSvc
    IamSvc --> OutboxPub
    IamRepo --> Db
    CacheSvc --> Redis
    BlacklistSvc --> Redis
    OutboxPub --> OutboxRelay
    OutboxRelay --> Kafka
    Controllers -.异常.-> Filter
```

### 1.2 中间件 / Guard / Filter 拦截顺序

```
请求进入
  → AuthMiddleware（注册于 me/logout/viewports/permissions/roles/audit/children）
  → PermissionGuard（APP_GUARD，data_scope=all 直放行 + DB 驱动 + Redis 缓存）
  → Controller Handler（Zod 校验 body / @GrpcMethod）
  → Application Service（业务编排：Repository + Cache + Outbox + 审计）
  → Repository（Drizzle 查询）
  → 异常抛出
  → GlobalErrorFilter（统一兜底，注入 traceId）
  → 响应返回
```

### 1.3 目录结构（最终态）

```
services/iam/src/
├─ iam/                          # 限界上下文：认证 + RBAC + 审计
│  ├─ iam.controller.ts          # REST /v1/iam 入口
│  ├─ iam.grpc.controller.ts     # gRPC 12 RPC 入口
│  ├─ rbac.controller.ts         # REST /v1/iam/roles, /permissions
│  ├─ audit.controller.ts        # REST /v1/iam/audit
│  ├─ jwks.controller.ts         # REST /v1/iam/.well-known/jwks.json
│  ├─ jwks.service.ts            # PEM→JWK 转换
│  ├─ iam.service.ts             # Application Service（12 RPC + Outbox + 审计）
│  ├─ iam.repository.ts          # Drizzle 查询（10 表）
│  ├─ iam.schema.ts              # 10 张表 schema + DataScope/RoleType 枚举
│  └─ iam.dto.ts                 # Zod schema
├─ config/
│  ├─ database.ts                # Drizzle 池 + getDbInstance + closeDb
│  ├─ redis.ts                   # Redis 单例
│  ├─ jwt.ts                     # RS256 密钥加载 + ttlToSeconds
│  ├─ kafka.ts                   # Kafka producer 单例 + IAM_KAFKA_TOPICS
│  └─ env.ts                     # Zod 校验环境变量
├─ middleware/
│  ├─ auth.middleware.ts         # 解析 x-user-* 头
│  └─ permission.guard.ts        # DB 驱动 + Redis 缓存权限校验
├─ shared/
│  ├─ cache/
│  │  ├─ permission-cache.service.ts  # Redis 权限缓存
│  │  └─ token-blacklist.service.ts   # Redis jti 黑名单
│  ├─ errors/                    # ApplicationError 层次 + GlobalErrorFilter
│  ├─ health/                    # /healthz + /readyz（5 依赖检查）
│  ├─ lifecycle/                 # 优雅停机（Kafka→Redis→DB）
│  └─ observability/             # logger / metrics / tracer
├─ app.module.ts                 # 根模块（IamModule + HealthModule + OutboxModule + APP_GUARD）
└─ main.ts                       # Hybrid 启动（gRPC + HTTP）
```

## 2. 领域模型

### 2.1 聚合根与实体

```mermaid
classDiagram
    class UserEntity {
        -id: string
        -email: string
        -passwordHash: string
        -name: string
        -status: UserStatus
        -dataScope: DataScope
        -passwordChangedAt: Date
        +verifyPassword(plain) bool
    }

    class RoleEntity {
        -id: string
        -name: string
        -description: string?
        -roleType: RoleType
        -level: int
    }

    class Permission {
        +id: string
        +name: string
        +resource: string
        +action: string
    }

    class RoleViewport {
        +id: string
        +roleId: string
        +viewportKey: string
        +label: string
        +route: string
        +level: ViewportLevel
        +sortOrder: int
        +requiredPermission: string?
    }

    class RefreshToken {
        +id: string
        +userId: string
        +tokenHash: string
        +jti: string
        +expiresAt: Date
        +revokedAt: Date?
    }

    class StudentGuardian {
        +id: string
        +studentId: string
        +guardianId: string
        +relation: string
    }

    class AuditLog {
        +id: string
        +actorUserId: string?
        +action: string
        +resourceType: string
        +resourceId: string
        +beforeState: string?
        +afterState: string?
        +ipAddress: string?
        +userAgent: string?
    }

    UserEntity "1" --> "many" RefreshToken : 拥有
    UserEntity "1" --> "many" StudentGuardian : 作为 guardian
    RoleEntity "1" --> "many" Permission : 通过 role_permissions
    RoleEntity "1" --> "many" RoleViewport : 配置
    UserEntity "1" --> "many" AuditLog : 作为 actor
```

### 2.2 值对象（枚举）

```typescript
enum UserStatus {
  ACTIVE = "active",
  DISABLED = "disabled",
}

// DataScope 6 级（I8 裁决：SUBJECT 替代 DISTRICT）
enum DataScope {
  SELF = "self", // L0：仅自己
  SUBJECT = "subject", // L1：科目（教师所教科目+班级）
  CLASS = "class", // L2：班级（班主任所带班级）
  GRADE = "grade", // L3：年级
  SCHOOL = "school", // L4：全校
  ALL = "all", // L5：跨校（系统管理员）
}

// 三层角色模型（§2.15）
enum RoleType {
  SYSTEM = "system", // 系统预设（admin/teacher/student/parent），level=0
  ORGANIZATION = "organization", // 组织分配（年级组长/班主任/学科组长），level=1
  TEMPORARY = "temporary", // 临时授权（代课教师），level=2
}

enum ViewportLevel {
  ADMIN = "admin",
  TEACHER = "teacher",
  STUDENT = "student",
  PARENT = "parent",
}
```

### 2.3 聚合间通信

- **同服务内**：`IamService` 直接调用 `PermissionCacheService`、`TokenBlacklistService`、`JwksService`、`IamRepository`（NestJS DI）
- **跨服务**：通过 Kafka 事件（Outbox 发布），不直接访问其他服务 DB

## 3. 数据模型

### 3.1 表清单（10 张表）

| 表名                    | 用途                         | 主键 | 唯一索引                  |
| ----------------------- | ---------------------------- | ---- | ------------------------- |
| `iam_users`             | 用户主表                     | `id` | `email`                   |
| `iam_roles`             | 角色表（role_type + level）  | `id` | `name`                    |
| `iam_user_roles`        | 用户-角色绑定                | `id` | `(userId, roleId)`        |
| `iam_permissions`       | 权限点表                     | `id` | `name`                    |
| `iam_role_permissions`  | 角色-权限映射                | `id` | `(roleId, permissionId)`  |
| `iam_refresh_tokens`    | refresh token（含 jti）      | `id` | `jti`                     |
| `iam_role_viewports`    | 角色-视口配置（含 level）    | `id` | `(roleId, viewportKey)`   |
| `iam_student_guardians` | 学生-家长关系（I6 裁决表名） | `id` | `(studentId, guardianId)` |
| `iam_user_audit_log`    | 审计日志（§5.5）             | `id` | —                         |
| `iam_password_history`  | 密码重用限制                 | `id` | —                         |

### 3.2 索引策略

| 表                      | 索引                                                                         | 用途                             |
| ----------------------- | ---------------------------------------------------------------------------- | -------------------------------- |
| `iam_users`             | PK(`id`)、UNIQUE(`email`)                                                    | 主键查询、登录                   |
| `iam_user_roles`        | UNIQUE(`userId`,`roleId`)、INDEX(`roleId`)                                   | 防重、按角色查用户               |
| `iam_role_permissions`  | UNIQUE(`roleId`,`permissionId`)、INDEX(`permissionId`)                       | 防重、按权限查角色               |
| `iam_refresh_tokens`    | UNIQUE(`jti`)、INDEX(`userId`)、INDEX(`tokenHash`)                           | jti 黑名单、按用户、按 hash 校验 |
| `iam_role_viewports`    | UNIQUE(`roleId`,`viewportKey`)                                               | 防重                             |
| `iam_student_guardians` | UNIQUE(`studentId`,`guardianId`)、INDEX(`guardianId`)                        | 防重、按家长查学生               |
| `iam_user_audit_log`    | INDEX(`actorUserId`)、INDEX(`resourceType`,`resourceId`)、INDEX(`createdAt`) | 按操作者、按资源、按时间查审计   |
| `iam_password_history`  | INDEX(`userId`,`createdAt`)                                                  | 按用户查密码历史                 |

### 3.3 读写分离策略

- **写路径**：所有 Command 走 MySQL 主库（iam 独占库）
- **读路径**：MySQL 直接读（iam 读多写少但数据量小，无需 ClickHouse 读模型）
- **缓存层**：
  - `getEffectivePermissions` 结果走 Redis 缓存（TTL 5min），Key: `iam:perm:{userId}`
  - refresh token jti 黑名单走 Redis，Key: `iam:bl:{jti}`，TTL 与 refresh_token 剩余有效期对齐

## 4. API 设计

### 4.1 REST API 清单

| Method | Path                            | 权限              | 说明                                   |
| ------ | ------------------------------- | ----------------- | -------------------------------------- |
| POST   | `/v1/iam/register`              | 公开              | 注册                                   |
| POST   | `/v1/iam/login`                 | 公开              | 登录                                   |
| POST   | `/v1/iam/refresh`               | 公开              | 刷新令牌（轮换 + 旧 jti 加黑名单）     |
| POST   | `/v1/iam/logout`                | `IAM_USER_READ`   | 登出（refresh token 加黑名单）         |
| GET    | `/v1/iam/me`                    | `IAM_USER_READ`   | 当前用户信息                           |
| GET    | `/v1/iam/viewports`             | `IAM_USER_READ`   | 当前用户视口                           |
| GET    | `/v1/iam/permissions/effective` | `IAM_USER_READ`   | 当前用户有效权限                       |
| GET    | `/v1/iam/children`              | `IAM_USER_READ`   | 家长的孩子列表                         |
| GET    | `/v1/iam/roles`                 | `IAM_ROLE_MANAGE` | 角色列表                               |
| GET    | `/v1/iam/permissions`           | `IAM_ROLE_MANAGE` | 权限点列表                             |
| GET    | `/v1/iam/audit`                 | `IAM_AUDIT_READ`  | 审计日志（支持 actor/resource 查询）   |
| GET    | `/v1/iam/.well-known/jwks.json` | 公开              | RS256 公钥 JWK Set（Gateway 拉取验签） |

### 4.2 gRPC API 清单（12 RPC，package `next_edu_cloud.iam.v1`）

| RPC                       | 请求                      | 响应                   | 说明                       |
| ------------------------- | ------------------------- | ---------------------- | -------------------------- |
| `Register`                | `{email, password, name}` | `{user, tokens}`       | 注册                       |
| `Login`                   | `{email, password}`       | `{user, tokens}`       | 登录                       |
| `RefreshToken`            | `{refreshToken}`          | `{tokens}`             | 刷新令牌                   |
| `Logout`                  | `{refreshToken, userId}`  | `{success}`            | 登出                       |
| `GetUserInfo`             | `{userId}`                | `UserInfo`             | 查询用户信息               |
| `BatchGetUsers`           | `{userIds[]}`             | `{users[]}`            | 批量查询用户（BFF 聚合用） |
| `GetEffectivePermissions` | `{userId}`                | `{permissions[]}`      | 用户有效权限               |
| `GetEffectiveAccess`      | `{userId, permission}`    | `{allowed, dataScope}` | 用户对某权限的有效访问判定 |
| `GetEffectiveDataScope`   | `{userId}`                | `{dataScope}`          | 用户数据范围               |
| `GetViewports`            | `{userId}`                | `{viewports[]}`        | 用户视口                   |
| `GetPublicKey`            | —                         | `{kid, alg, pem}`      | 公钥拉取（备用 JWKS）      |
| `GetChildrenByParent`     | `{parentId}`              | `{children[]}`         | 家长的孩子列表             |

### 4.3 JWT Payload（RS256 签发）

```typescript
interface JwtPayload {
  sub: string; // userId
  email: string;
  roles: string[]; // ['teacher', 'grade_leader']
  dataScope: DataScope; // 'class' | 'grade' | ...
  type: "access" | "refresh";
  iat: number; // 签发时间
  exp: number; // 过期时间
  iss: "next-edu-cloud";
  aud: "next-edu-cloud";
  jti: string; // JWT ID（仅 refresh token，用于黑名单）
  kid: string; // 密钥 ID（header，支持 JWKS 轮换）
}
```

- **access_token**：TTL 15min（`ACCESS_TOKEN_TTL`），RS256 签名
- **refresh_token**：TTL 7day（`REFRESH_TOKEN_TTL_DAYS`），RS256 签名，含 jti
- **密钥**：本地文件（`IAM_PRIVATE_KEY_PATH` / `IAM_PUBLIC_KEY_PATH`），启动时一次性加载

## 5. 事件设计

### 5.1 我发布的领域事件

| 事件           | 触发时机                             | Topic                   | 消费者         |
| -------------- | ------------------------------------ | ----------------------- | -------------- |
| `UserCreated`  | 注册成功                             | `edu.iam.user.events`   | core-edu / msg |
| `AuditCreated` | 关键操作（登录/角色变更/密码修改等） | `edu.iam.audit.created` | data-ana       |
| `RoleChanged`  | 角色绑定变更                         | `edu.iam.role.events`   | msg / 缓存失效 |

### 5.2 事件 Schema（events.proto）

```protobuf
message UserEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string user_id = 5;
  string email = 6;
  string name = 7;
  repeated string roles = 8;
  string data_scope = 9;
  string action = 10;
  map<string, string> metadata = 11;
}

message RoleEvent {
  string event_id = 1;
  string aggregate_id = 2;
  string event_type = 3;
  int64 occurred_at = 4;
  string role_id = 5;
  string role_name = 6;
  string action = 7;
  map<string, string> metadata = 8;
}

message AuditEvent {
  string event_id = 1;
  string actor_user_id = 2;
  string action = 3;
  string resource_type = 4;
  string resource_id = 5;
  int64 occurred_at = 6;
  string ip_address = 7;
  map<string, string> metadata = 8;
}
```

### 5.3 我消费的事件

无。iam 是权限中枢，单向发布，不消费业务事件。

### 5.4 Outbox 实现策略

- **shared-ts OutboxModule**（I4 裁决）：`OutboxModule.forRoot({ config, db, kafkaProducer })`
- **表名**：`iam_outbox`
- **topic**：`edu.iam.user.events`（默认）/ `edu.iam.role.events` / `edu.iam.audit.created`
- **轮询间隔**：1000ms，批量 20 条，最大重试 5 次，退避 1000ms
- **幂等性**：producer `idempotent=true` + `transactionalId: "iam-tx"`，consumer 基于 `event_id` 去重

```mermaid
sequenceDiagram
    participant Ctl as Controller
    participant Svc as IamService
    participant DB as MySQL
    participant Pub as OutboxPublisher
    participant Relay as OutboxRelayWorker
    participant Kafka as Kafka

    Ctl->>Svc: register(dto)
    Svc->>DB: BEGIN TX
    Svc->>DB: INSERT iam_users
    Svc->>DB: INSERT iam_user_roles
    Svc->>DB: INSERT iam_user_audit_log
    Svc->>Pub: publish(UserCreated + AuditCreated)
    Pub->>DB: INSERT iam_outbox
    Svc->>DB: COMMIT TX
    Svc-->>Ctl: {user, tokens}

    loop 每 1000ms 轮询
        Relay->>DB: SELECT pending LIMIT 20
        Relay->>Kafka: produce(topic, payload)
        Kafka-->>Relay: ack
        Relay->>DB: UPDATE status='published'
    end
```

## 6. 横切关注点对齐清单

### 6.1 权限装饰器

每个 Controller 方法用 `@RequirePermission()` 装饰器声明权限点。权限点常量：

```typescript
export const Permissions = {
  IAM_USER_READ: "iam:user:read",
  IAM_USER_MANAGE: "iam:user:manage",
  IAM_ROLE_MANAGE: "iam:role:manage",
  IAM_AUDIT_READ: "iam:audit:read",
  IAM_VIEWPORT_READ: "iam:viewport:read",
} as const;
```

### 6.2 错误码清单

| 错误码                  | HTTP | 触发条件                                             |
| ----------------------- | ---- | ---------------------------------------------------- |
| `IAM_VALIDATION_ERROR`  | 400  | Zod 校验失败                                         |
| `IAM_UNAUTHORIZED`      | 401  | 未登录、密码错误、token 失效、refresh token 在黑名单 |
| `IAM_PERMISSION_DENIED` | 403  | 缺少所需权限点                                       |
| `IAM_NOT_FOUND`         | 404  | 用户/角色/权限/视口不存在                            |
| `IAM_CONFLICT`          | 409  | 邮箱已注册、角色名重复、家长-学生关系已存在          |
| `IAM_BUSINESS_ERROR`    | 422  | 账号禁用、系统角色禁止删除、refresh token 已撤销     |
| `IAM_DATABASE_ERROR`    | 500  | Drizzle 操作失败                                     |
| `IAM_INTERNAL_ERROR`    | 500  | 未预期异常                                           |
| `IAM_OUTBOX_ERROR`      | 500  | Outbox 写入或投递失败                                |

### 6.3 可观测性

- **日志**：pino，`base: { service: 'iam', version: '0.2.0' }`，`level: env.LOG_LEVEL`，注入 traceId
- **指标**：prom-client，`/metrics` 端点
  - `iam_login_attempts_total`、`iam_login_duration_seconds`
  - `iam_jwt_issued_total`（type=access/refresh）
  - `iam_permission_cache_hits_total` / `iam_permission_cache_misses_total`
  - `iam_outbox_pending`、`iam_outbox_publish_duration_seconds`
- **链路**：OpenTelemetry SDK + OTLP exporter，serviceName: `iam`

### 6.4 健康检查

- **`/healthz`**：liveness，仅返回进程存活
  - 响应：`{ status: 'ok', service: 'iam', timestamp: ISO }`
- **`/readyz`**：readiness，5 依赖检查
  - DB：`SELECT 1`
  - Redis：`ping`
  - Kafka：producer 实例存在且已连接
  - JWKS：密钥文件可读
  - gRPC：进程内 microservice 存在
  - 失败：HTTP 503，响应体含失败项详情

### 6.5 优雅关闭顺序

```typescript
async onApplicationShutdown(signal?: string) {
  // 1. 停止接收新请求（NestJS 自动）
  // 2. 停止 OutboxRelayWorker（shared-ts 自动）
  // 3. 关闭 Kafka producer
  await closeKafkaProducer();
  // 4. 关闭 Redis 连接
  await closeRedis();
  // 5. 关闭 MySQL 连接池
  await closeDb();
}
```

## 7. 与其他模块的交互点（契约清单）

| 方向   | 对方服务    | 协议  | 接口/事件                                         | 用途                            |
| ------ | ----------- | ----- | ------------------------------------------------- | ------------------------------- |
| 被调用 | api-gateway | HTTP  | `POST /v1/iam/register` 等                        | Gateway 反向代理 `/iam/v1/*`    |
| 被调用 | api-gateway | HTTP  | `GET /v1/iam/.well-known/jwks.json`               | Gateway 拉取 RS256 公钥校验 JWT |
| 被调用 | teacher-bff | gRPC  | `GetUserInfo`、`BatchGetUsers`、`GetViewports` 等 | BFF 聚合用户身份与视口          |
| 被调用 | student-bff | gRPC  | 同上                                              | 同上                            |
| 被调用 | parent-bff  | gRPC  | `GetChildrenByParent`                             | 家长视角聚合孩子信息            |
| 发布   | —           | Kafka | `edu.iam.user.events`                             | core-edu / msg 消费             |
| 发布   | —           | Kafka | `edu.iam.role.events`                             | msg / 缓存失效                  |
| 发布   | —           | Kafka | `edu.iam.audit.created`                           | data-ana 消费                   |

### 7.1 端口分配

| 服务     | 端口  | 备注                 |
| -------- | ----- | -------------------- |
| iam HTTP | 3002  | REST 入口            |
| iam gRPC | 50052 | gRPC 入口（I1 裁决） |

### 7.2 Topic 命名

- `edu.iam.user.events`
- `edu.iam.role.events`
- `edu.iam.audit.created`

## 8. 仲裁裁决对齐

### 8.1 I1-I8 裁决

| 裁决 | 内容                                 | 实现位置                                          |
| ---- | ------------------------------------ | ------------------------------------------------- |
| I1   | 双入口（REST + gRPC），gRPC 50052    | main.ts Hybrid app + iam.grpc.controller.ts       |
| I2   | JWT RS256 本地文件密钥               | config/jwt.ts + env IAM_PRIVATE/PUBLIC_KEY_PATH   |
| I3   | DB 驱动 + Redis 缓存 PermissionGuard | permission.guard.ts + permission-cache.service.ts |
| I4   | shared-ts OutboxModule 接入          | app.module.ts OutboxModule.forRoot                |
| I5   | /iam/v1/* 前缀                       | iam.controller.ts @Controller("v1/iam")           |
| I6   | iam_student_guardians 表名           | iam.schema.ts + iam-init.sql                      |
| I7   | jti 黑名单                           | token-blacklist.service.ts                        |
| I8   | DataScope SUBJECT 替代 DISTRICT      | iam.schema.ts DataScope enum                      |

### 8.2 总裁裁决

| 裁决  | 内容                                          | 实现位置                                     |
| ----- | --------------------------------------------- | -------------------------------------------- |
| §2.15 | 三层角色模型（system/organization/temporary） | iam.schema.ts RoleType + iam_roles.level     |
| §2.16 | 双入口策略（同一 IamService）                 | iam.service.ts REST 与 gRPC 共用             |
| §3.2  | AuthMiddleware 注册                           | app.module.ts configure()                    |
| §5.5  | 审计日志（iam_user_audit_log + AuditEvent）   | iam.schema.ts + iam.service.ts writeAuditLog |

## 9. 技术风险

| 风险                 | 影响                               | 缓解措施                                                 |
| -------------------- | ---------------------------------- | -------------------------------------------------------- |
| Redis 不可用         | 权限校验回退到 DB 查询，性能下降   | `getEffectivePermissions` catch 后走 DB，不抛错          |
| Kafka 不可用         | Outbox 事件积压，下游服务感知延迟  | Relay Worker 重试 + `status='failed'` 记录，不阻塞主流程 |
| RS256 密钥泄露       | 任何人可伪造 JWT                   | 密钥文件权限 600 + 容器 Secret 挂载                      |
| 权限缓存与 DB 不一致 | 用户角色变更后 5min 内旧权限仍生效 | 角色变更主动 DEL + 短 TTL                                |
| Outbox 表膨胀        | 磁盘占用增长                       | 已发布事件定期清理（保留 7 天）                          |

---

**模块**: iam-service（@edu/iam-service v0.2.0）
**分支**: feat-implement-iam-module
**入口**: HTTP 3002 + gRPC 50052
