Files
Edu/services/iam/docs/02-architecture-design.md
SpecialX 0a71b02e04
Some checks failed
CI / quality-ts (push) Failing after 48s
CI / quality-go (push) Failing after 4s
CI / quality-proto (push) Failing after 2s
CI / deploy (push) Has been skipped
fix: code compliance audit and fix across all services
NestJS (6 services): implement @RequirePermission decorator with
SetMetadata+Reflector, register APP_GUARD globally, fix as assertions
to type guards, add explicit return types, fix import type for express,
fix /metrics implicit any, replace native Error with ApplicationError,
remove typeorm remnants, register LifecycleService.

teacher-bff: add logger, ApplicationError, GlobalErrorFilter, forward
real userId to downstream, log downstream failures, migrate health
controller to shared/health.

Go (2 services): interface to any, doc comments, CORS dev whitelist,
JWT secret fail-fast, push-gateway internal API auth, metrics and
readyz endpoints, remove dead code.

Python (2 services): lifespan return type, dev_mode to bool, data-ana
APIRouter, ai POST body model, ClickHouse async wrapping.
2026-07-09 17:28:27 +08:00

37 KiB
Raw Blame History

模块架构设计文档 — iam

AIai02TS / 身份认证) 阶段:阶段 2 交付物 日期2026-07-09 关联:阶段 1 理解确认书004 架构影响地图pending-features P2 状态:待 coord 交叉审查


1. 模块内部分层图

1.1 调用链总览

flowchart TB
    subgraph Client["客户端 / Gateway / BFF"]
        Req[HTTP 请求<br/>带 x-user-id / x-user-roles 头]
    end

    subgraph NestJS["iam 服务NestJS"]
        direction TB
        MW[AuthMiddleware<br/>❌ 当前未注册P2 仍走 header 直读]
        Guard[PermissionGuard<br/>APP_GUARD 全局守卫]
        Filter[GlobalErrorFilter<br/>全局异常过滤器]

        subgraph Controllers["Controller 层"]
            IamCtl[IamController<br/>/iam/register, login, refresh, me]
            RbacCtl[RbacController<br/>/iam/viewports, permissions, roles]
            UserCtl[UserController<br/>P2 新增: 用户 CRUD]
            RoleCtl[RoleController<br/>P2 新增: 角色 CRUD]
            ViewportCtl[ViewportController<br/>P2 新增: 视口 CRUD]
            JwksCtl[JwksController<br/>P2 新增: RS256 公钥暴露]
        end

        subgraph Services["Application Service 层"]
            IamSvc[IamService<br/>认证编排]
            RbacSvc[RbacService<br/>RBAC 编排]
            UserSvc[UserService<br/>用户领域编排]
            CacheSvc[PermissionCacheService<br/>Redis 权限缓存]
        end

        subgraph Domain["Domain 层P2 轻量)"]
            UserEntity[UserEntity<br/>聚合根]
            RoleEntity[RoleEntity<br/>聚合根]
        end

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

        subgraph Outbox["Outbox 模块"]
            OutboxTbl[(iam_outbox 表)]
            Relay[OutboxRelayWorker<br/>后台轮询 + Kafka 投递]
        end

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

    Req --> MW
    MW --> Guard
    Guard --> Controllers
    Controllers --> Services
    Services --> Domain
    Services --> Repo
    Services --> CacheSvc
    Repo --> Db
    CacheSvc --> Redis
    Services --> OutboxTbl
    OutboxTbl --> Relay
    Relay --> Kafka
    Controllers -.异常.-> Filter

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

请求进入
  → AuthMiddlewareP2 仍不注册Controller 直读 header
  → PermissionGuardAPP_GUARDDEV_MODE 旁路 + DB 驱动权限校验)
  → Controller HandlerZod 校验 body
  → Application Service业务编排
  → RepositoryDrizzle 查询)
  → 异常抛出
  → GlobalErrorFilter统一兜底注入 traceId
  → 响应返回

1.3 目录结构P2 目标态)

services/iam/src/
├─ iam/                          # 限界上下文:认证
│  ├─ iam.controller.ts          # 认证端点register/login/refresh/me/logout
│  ├─ iam.service.ts             # 认证编排
│  ├─ iam.repository.ts          # 用户/refresh_token 查询
│  ├─ iam.schema.ts              # users / refresh_tokens 表
│  ├─ iam.dto.ts                 # Zod schema
│  └─ domain/
│     └─ user.entity.ts          # UserEntity 聚合根P2 新增)
├─ rbac/                         # 限界上下文RBACP2 从 iam/ 拆出)
│  ├─ rbac.controller.ts         # 角色/权限/视口查询端点
│  ├─ role.controller.ts         # 角色 CRUDP2 新增)
│  ├─ permission.controller.ts   # 权限点 CRUDP2 新增)
│  ├─ viewport.controller.ts     # 视口配置 CRUDP2 新增)
│  ├─ rbac.service.ts            # RBAC 编排
│  ├─ rbac.repository.ts         # 角色/权限/视口查询
│  ├─ rbac.schema.ts             # roles / permissions / role_permissions / role_viewports 表
│  └─ domain/
│     └─ role.entity.ts          # RoleEntity 聚合根P2 新增)
├─ jwks/                         # 限界上下文JWT 公钥暴露P2 新增)
│  ├─ jwks.controller.ts         # GET /iam/.well-known/jwks.json
│  ├─ jwks.service.ts            # 密钥加载 + JWK Set 生成
│  └─ jwks.repository.ts         # 密钥元数据持久化(可选)
├─ cache/                        # 限界上下文Redis 缓存P2 新增)
│  ├─ permission-cache.service.ts # getEffectivePermissions 缓存
│  └─ token-blacklist.service.ts  # refresh token 黑名单
├─ outbox/                       # Outbox 模式P2 新增)
│  ├─ outbox.schema.ts           # iam_outbox 表
│  ├─ outbox.publisher.ts        # 写入 outbox事务内
│  └─ outbox.relay-worker.ts     # 后台轮询 + Kafka 投递
├─ config/
│  ├─ database.ts                # Drizzle 池(已有)
│  ├─ redis.ts                   # Redis 客户端P2 新增)
│  ├─ jwt.ts                     # RS256 密钥加载P2 新增)
│  └─ env.ts                     # 环境变量P2 扩展)
├─ middleware/
│  ├─ auth.middleware.ts         # 保留P2 仍不注册)
│  └─ permission.guard.ts        # 改造DB 驱动 + 缓存
├─ shared/
│  ├─ errors/                    # 已有
│  ├─ health/                    # 已有
│  ├─ lifecycle/                 # 改造:关闭顺序 HTTP→Kafka→Redis→DB
│  └─ observability/             # 已有
├─ app.module.ts                 # 改造imports 新增 OutboxModule、CacheModule、JwksModule
└─ main.ts                       # 改造:启动 OutboxRelayWorker

2. 领域模型

2.1 聚合根与实体

classDiagram
    class UserEntity {
        -id: string
        -email: string
        -passwordHash: string
        -name: string
        -status: UserStatus
        -dataScope: DataScope
        -createdAt: Date
        -updatedAt: Date
        +create(props) UserEntity$
        +rename(name) UserRenamedEvent
        +disable() UserDisabledEvent
        +changeDataScope(scope) UserDataScopeChangedEvent
        +verifyPassword(plain) bool
    }

    class RoleEntity {
        -id: string
        -name: string
        -description: string?
        -roleType: RoleType
        +create(props) RoleEntity$
        +rename(name) RoleRenamedEvent
    }

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

    class RoleViewport {
        +id: string
        +roleId: string
        +viewportKey: string
        +label: string
        +route: string
        +sortOrder: string
        +requiredPermission: string?
        +componentConfig: string?
    }

    class RefreshToken {
        +id: string
        +userId: string
        +tokenHash: string
        +expiresAt: Date
        +revokedAt: Date?
        +isRevoked() bool
        +isExpired() bool
    }

    UserEntity "1" --> "many" RefreshToken : 拥有
    RoleEntity "1" --> "many" Permission : 通过 role_permissions
    RoleEntity "1" --> "many" RoleViewport : 配置

2.2 值对象(枚举)

enum UserStatus {
  ACTIVE = "active",
  DISABLED = "disabled",
  PENDING = "pending", // P2 新增:注册后待激活
}

enum DataScope {
  SELF = "self", // L0
  CLASS = "class", // L1
  GRADE = "grade", // L2
  SCHOOL = "school", // L3
  DISTRICT = "district", // L4
  ALL = "all", // L5
}

enum RoleType {
  // P2 新增:三层角色模型
  SYSTEM = "system", // 系统预设admin/teacher/student/parent
  ORGANIZATION = "organization", // 组织分配(年级组长/班主任/学科组长)
  TEMPORARY = "temporary", // 临时授权(代课教师)
}

2.3 聚合间通信

  • 同服务内IamService 直接调用 RbacServicePermissionCacheServiceNestJS DI
  • 跨服务:通过 Kafka 事件Outbox 发布),不直接调用其他服务

3. 数据模型

3.1 表清单P2 目标态)

3.1.1 已有表(保留)

表名 用途 主键 唯一索引
iam_users 用户主表 id (char36) email
iam_roles 角色表 id name
iam_user_roles 用户-角色绑定 (userId, roleId) 复合
iam_permissions 权限点表 id name
iam_role_permissions 角色-权限映射 (roleId, permissionId) 复合
iam_refresh_tokens refresh token 持久化 id
iam_role_viewports 角色-视口配置 id (roleId, viewportKey)

3.1.2 P2 新增表

表名 用途 主键 唯一索引
iam_outbox Outbox 事件表(事务内写入) id
iam_parent_student_relations 家长-学生关系表 id (parentId, studentId)
iam_user_sessions 用户会话记录(审计 + 强制下线) id userId + deviceHash

class_subject_teachers 表归属 core-edu见 §8.1 决策点 8不在 iam。

3.1.3 P2 表结构定义

// iam_outboxOutbox 事件表
export const iamOutbox = mysqlTable(
  "iam_outbox",
  {
    id: char("id", { length: 36 }).notNull().primaryKey(),
    aggregateId: char("aggregate_id", { length: 36 }).notNull(),
    aggregateType: varchar("aggregate_type", { length: 50 }).notNull(), // 'User' | 'Role'
    eventType: varchar("event_type", { length: 100 }).notNull(), // 'UserRegistered' | ...
    payload: text("payload").notNull(), // JSON 序列化
    topic: varchar("topic", { length: 100 }).notNull(), // 'edu.identity.user.created'
    status: mysqlEnum("status", ["pending", "published", "failed"])
      .notNull()
      .default("pending"),
    retryCount: int("retry_count").notNull().default(0),
    occurredAt: timestamp("occurred_at").notNull().defaultNow(),
    publishedAt: timestamp("published_at"),
    createdAt: timestamp("created_at").notNull().defaultNow(),
  },
  (table) => ({
    statusIdx: index("idx_outbox_status").on(table.status), // relay 轮询用
    aggregateIdx: index("idx_outbox_aggregate").on(table.aggregateId),
  }),
);

// iam_parent_student_relations家长-学生关系
export const parentStudentRelations = mysqlTable(
  "iam_parent_student_relations",
  {
    id: char("id", { length: 36 }).notNull().primaryKey(),
    parentId: char("parent_id", { length: 36 }).notNull(),
    studentId: char("student_id", { length: 36 }).notNull(),
    relation: varchar("relation", { length: 20 }).notNull(), // 'father' | 'mother' | 'guardian'
    createdAt: timestamp("created_at").notNull().defaultNow(),
  },
  (table) => ({
    parentStudentUniq: uniqueIndex("uniq_parent_student").on(
      table.parentId,
      table.studentId,
    ),
    studentIdx: index("idx_student").on(table.studentId),
  }),
);

// iam_roles 表扩展:新增 role_type 字段
// 在现有 iam_roles 表 ALTER ADD:
//   role_type ENUM('system','organization','temporary') NOT NULL DEFAULT 'system'
//   level INT NOT NULL DEFAULT 0  -- 三层优先级system=0(最高) / organization=1 / temporary=2

3.2 索引策略

索引 用途
iam_users PK(id)、UNIQUE(email) 主键查询、登录查询
iam_user_roles INDEX(userId)、INDEX(roleId) 按用户查角色、按角色查用户
iam_role_permissions INDEX(roleId)、INDEX(permissionId) 按角色查权限
iam_refresh_tokens INDEX(userId)、INDEX(tokenHash) 按用户查 token、按 hash 校验
iam_role_viewports INDEX(roleId) 按角色查视口
iam_outbox INDEX(status)、INDEX(aggregateId) relay 轮询 pending、按聚合查事件
iam_parent_student_relations UNIQUE(parentId,studentId)、INDEX(studentId) 防重、按学生查家长

3.3 读写分离策略

  • 写路径:所有 Command 走 MySQL 主库iam 独占库,无读写分离)
  • 读路径P2 暂不引入 ClickHouse 读模型iam 读多写少但数据量小MySQL 足够)
  • 缓存层getEffectivePermissions / getUserViewports 结果走 Redis 缓存TTL 5min

4. API 设计

4.1 REST API 完整清单P2 目标态)

Method Path 权限 请求体 / 参数 响应 说明
POST /iam/register 公开 {email, password, name} {user, tokens} 注册 + 自动分配 teacher 角色
POST /iam/login 公开 {email, password} {user, tokens} 登录
POST /iam/refresh 公开 {refreshToken} {tokens} 刷新令牌(轮换 + 旧 token 加入黑名单)
POST /iam/logout IAM_USER_READ {refreshToken} {success} 登出refresh token 加黑名单)
GET /iam/me IAM_USER_READ {user} 当前用户信息
GET /iam/viewports IAM_USER_READ {viewports[]} 当前用户视口L1 导航)
GET /iam/permissions/effective IAM_USER_READ {permissions[]} 当前用户有效权限
GET /iam/.well-known/jwks.json 公开 {keys[]} RS256 公钥 JWK SetGateway 拉取)
GET /iam/roles IAM_ROLE_MANAGE {roles[]} 角色列表
POST /iam/roles IAM_ROLE_MANAGE {name, description, roleType} {role} 创建角色
PUT /iam/roles/:id IAM_ROLE_MANAGE {name?, description?} {role} 更新角色
DELETE /iam/roles/:id IAM_ROLE_MANAGE {success} 删除角色(系统角色禁止删)
GET /iam/permissions IAM_ROLE_MANAGE {permissions[]} 权限点列表
GET /iam/users/:id/roles IAM_ROLE_MANAGE {roles[]} 用户角色列表
POST /iam/users/:id/roles IAM_ROLE_MANAGE {roleId} {success} 给用户分配角色
DELETE /iam/users/:id/roles/:roleId IAM_ROLE_MANAGE {success} 移除用户角色(触发缓存失效)
GET /iam/roles/:id/permissions IAM_ROLE_MANAGE {permissions[]} 角色权限列表
POST /iam/roles/:id/permissions IAM_ROLE_MANAGE {permissionId} {success} 给角色授予权限
DELETE /iam/roles/:id/permissions/:permissionId IAM_ROLE_MANAGE {success} 移除角色权限(触发缓存失效)
GET /iam/roles/:id/viewports IAM_ROLE_MANAGE {viewports[]} 角色视口列表
POST /iam/roles/:id/viewports IAM_ROLE_MANAGE {viewportKey, label, route, ...} {viewport} 创建视口配置
PUT /iam/roles/:id/viewports/:viewportId IAM_ROLE_MANAGE {label?, route?, ...} {viewport} 更新视口配置
DELETE /iam/roles/:id/viewports/:viewportId IAM_ROLE_MANAGE {success} 删除视口配置
GET /iam/users/:id/parents IAM_USER_READ {parents[]} 学生家长列表(家长-学生关系)
POST /iam/users/:studentId/parents IAM_ROLE_MANAGE {parentId, relation} {success} 绑定家长-学生关系

4.2 请求/响应结构示例

// 注册响应
interface RegisterResponse {
  success: true;
  data: {
    user: {
      id: string;
      email: string;
      name: string;
      roles: string[]; // ['teacher']
      permissions: string[]; // ['IAM_USER_READ', 'CLASSES_READ', ...]
      dataScope: "self" | "class" | "grade" | "school" | "district" | "all";
    };
    tokens: {
      accessToken: string; // RS256 签名15min
      refreshToken: string; // RS256 签名7day
      expiresIn: 900; // 秒
    };
  };
}

// JWK Set 响应RS256 公钥暴露)
interface JwkSet {
  keys: Array<{
    kty: "RSA";
    use: "sig";
    alg: "RS256";
    kid: string; // 密钥 ID支持轮换
    n: string; // modulus base64url
    e: string; // exponent base64url
  }>;
}

4.3 JWT PayloadRS256 签发)

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用于黑名单
}

5. 事件设计

5.1 我发布的领域事件

事件 触发时机 Topic 消费者动作
UserRegistered 注册成功 edu.identity.user.created core-edu 初始化默认班级关联msg 发欢迎通知
UserUpdated 用户信息变更name/dataScope edu.identity.user.updated core-edu 同步用户快照msg 通知
UserDisabled 用户禁用/注销 edu.identity.user.deleted core-edu 解除关联msg 通知push-gateway 强制下线
UserRoleChanged 用户角色绑定变更 edu.identity.user.role_changed 自身 Redis 缓存失效msg 审计日志
RoleCreated 角色创建 edu.identity.role.created msg 审计(仅管理端关注)
RoleUpdated 角色权限变更 edu.identity.role.updated 所有该角色用户的缓存失效msg 审计

5.2 事件 Schema建议 coord 在 shared-proto 中统一定义)

// 建议在 packages/shared-proto/proto/events.proto 新增:
message UserEvent {
  string event_id = 1;            // UUID幂等去重
  string aggregate_id = 2;        // userId
  string event_type = 3;          // 'UserRegistered' | 'UserUpdated' | ...
  int64 occurred_at = 4;          // 发生时间戳ms
  string user_id = 5;
  string email = 6;
  string name = 7;
  repeated string roles = 8;
  string data_scope = 9;
  string action = 10;             // 'created' | 'updated' | 'disabled' | 'role_changed'
  map<string, string> metadata = 11;  // trace_id 等
}

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

需 coord 在 shared-proto/events.proto 中统一定义iam 只负责填充字段并写入 outbox。

5.3 我消费的事件

  • 当前:无
  • 未来不主动消费业务事件iam 是权限中枢,单向发布)

5.4 Outbox 实现策略

sequenceDiagram
    participant Ctl as Controller
    participant Svc as IamService
    participant DB as MySQL
    participant Outbox as iam_outbox 表
    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->>Outbox: INSERT event (status=pending)
    Svc->>DB: COMMIT TX
    Svc-->>Ctl: {user, tokens}

    loop 每 100ms 轮询
        Relay->>Outbox: SELECT * WHERE status='pending' LIMIT 100
        Outbox-->>Relay: events[]
        Relay->>Kafka: produce(topic, payload)
        Kafka-->>Relay: ack
        Relay->>Outbox: UPDATE status='published', published_at=NOW()
    end

Relay Worker 实现

  • 独立 @Injectable() 服务,OnModuleInit 启动轮询
  • 每 100ms 查询 status='pending' 的事件,批量投递 Kafka
  • 投递失败重试 3 次,超过后标记 status='failed',记录日志
  • Kafka 未启动时不阻塞主服务try/catch + 日志警告)

6. 横切关注点对齐清单

6.1 权限装饰器(所有端点及对应权限常量)

端点 权限常量
POST /iam/register 公开(无装饰器)
POST /iam/login 公开
POST /iam/refresh 公开
GET /iam/.well-known/jwks.json 公开
POST /iam/logout IAM_USER_READ
GET /iam/me IAM_USER_READ
GET /iam/viewports IAM_USER_READ
GET /iam/permissions/effective IAM_USER_READ
GET /iam/users/:id/parents IAM_USER_READ
GET /iam/roles IAM_ROLE_MANAGE
POST /iam/roles IAM_ROLE_MANAGE
PUT /iam/roles/:id IAM_ROLE_MANAGE
DELETE /iam/roles/:id IAM_ROLE_MANAGE
GET /iam/permissions IAM_ROLE_MANAGE
GET /iam/users/:id/roles IAM_ROLE_MANAGE
POST /iam/users/:id/roles IAM_ROLE_MANAGE
DELETE /iam/users/:id/roles/:roleId IAM_ROLE_MANAGE
GET /iam/roles/:id/permissions IAM_ROLE_MANAGE
POST /iam/roles/:id/permissions IAM_ROLE_MANAGE
DELETE /iam/roles/:id/permissions/:permissionId IAM_ROLE_MANAGE
GET /iam/roles/:id/viewports IAM_ROLE_MANAGE
POST /iam/roles/:id/viewports IAM_ROLE_MANAGE
PUT /iam/roles/:id/viewports/:viewportId IAM_ROLE_MANAGE
DELETE /iam/roles/:id/viewports/:viewportId IAM_ROLE_MANAGE
POST /iam/users/:studentId/parents IAM_ROLE_MANAGE

权限常量清单P2 完整化):

export const Permissions = {
  // 用户管理
  IAM_USER_CREATE: "IAM_USER_CREATE",
  IAM_USER_READ: "IAM_USER_READ",
  IAM_USER_UPDATE: "IAM_USER_UPDATE",
  IAM_USER_DELETE: "IAM_USER_DELETE",
  // 角色管理
  IAM_ROLE_MANAGE: "IAM_ROLE_MANAGE",
  // 视口管理
  IAM_VIEWPORT_MANAGE: "IAM_VIEWPORT_MANAGE",
  // 家长-学生关系管理
  IAM_RELATION_MANAGE: "IAM_RELATION_MANAGE",
} 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_RATE_LIMITED 429 登录失败次数过多P2 可选,限流在 Gateway
IAM_DATABASE_ERROR 500 Drizzle 操作失败
IAM_INTERNAL_ERROR 500 未预期异常
IAM_OUTBOX_ERROR 500 Outbox 写入或投递失败

6.3 Logger 初始化位置与配置

  • 位置src/shared/observability/logger.ts(已有)
  • 配置pinobase: { service: 'iam', version: '0.1.0' }level: env.LOG_LEVEL
  • P2 新增:日志中注入 traceId(从 x-request-id 头读取OTel auto-instrumentation 已覆盖)

6.4 Metrics 指标清单

指标名 类型 标签 描述
iam_requests_total Counter method, endpoint, status 请求总数(已有)
iam_request_duration_seconds Histogram method, endpoint 请求延迟(已有)
iam_login_attempts_total Counter result(success/failure) 登录尝试次数P2 新增)
iam_login_duration_seconds Histogram 登录耗时P2 新增)
iam_jwt_issued_total Counter type(access/refresh) JWT 签发次数P2 新增)
iam_permission_cache_hits_total Counter 权限缓存命中P2 新增)
iam_permission_cache_misses_total Counter 权限缓存未命中P2 新增)
iam_outbox_pending Gauge Outbox 待投递事件数P2 新增)
iam_outbox_publish_duration_seconds Histogram Outbox 投递耗时P2 新增)

6.5 Tracer 初始化位置

  • 位置src/shared/observability/tracer.ts(已有)
  • 配置NodeSDK + OTLP HTTP exporterserviceName: 'iam'
  • P2 保持auto-instrumentations 覆盖 HTTP/Express/Drizzlemysql2

6.6 /healthz 检查逻辑

  • 端点GET /healthz
  • 逻辑:仅返回进程存活,不检查依赖
  • 响应{ status: 'ok', service: 'iam', timestamp: ISO }

6.7 /readyz 检查逻辑P2 改造)

  • 端点GET /readyz
  • 逻辑P2 新增 Redis + Kafka 检查):
    async readiness() {
      const checks = await Promise.allSettled([
        this.checkDb(),      // db.execute(sql`SELECT 1`)
        this.checkRedis(),   // redis.ping()
        this.checkKafka(),   // kafka.admin().listTopics()(轻量探活)
      ]);
      const allOk = checks.every(r => r.status === 'fulfilled');
      if (!allOk) throw 503;
      return { status: 'ok', service: 'iam', timestamp, checks };
    }
    
  • 失败HTTP 503响应体含失败项详情

6.8 优雅关闭顺序P2 改造)

async onApplicationShutdown(signal?: string) {
  // 1. 停止接收新请求NestJS 自动)
  // 2. 停止 OutboxRelayWorker停止轮询
  await this.relayWorker.stop();
  // 3. 关闭 Kafka producer
  await this.kafkaProducer.disconnect();
  // 4. 关闭 Redis 连接
  await this.redisClient.quit();
  // 5. 关闭 MySQL 连接池
  await closeDb();
  // 6. 关闭 Tracer
  await shutdownTracer();
}

7. 与其他模块的交互点(契约清单)

方向 对方服务 协议 接口/事件 用途
被调用 api-gateway HTTP POST /iam/register Gateway 反向代理 /api/v1/iam/*
被调用 teacher-bff HTTP GET /iam/meGET /iam/viewports BFF 聚合用户身份与视口
被调用 student-bff HTTP GET /iam/meGET /iam/viewports 同上P3
被调用 parent-bff HTTP GET /iam/meGET /iam/users/:id/parents 同上 + 家长-学生关系P4
被调用 api-gateway HTTP GET /iam/.well-known/jwks.json Gateway 拉取 RS256 公钥校验 JWT
发布 Kafka edu.identity.user.created core-edu / msg 消费
发布 Kafka edu.identity.user.updated core-edu / msg 消费
发布 Kafka edu.identity.user.deleted core-edu / msg 消费
发布 Kafka edu.identity.user.role_changed 自身缓存失效 / msg 审计
发布 Kafka edu.identity.role.created msg 审计
发布 Kafka edu.identity.role.updated 自身缓存失效 / msg 审计
消费 iam 不消费外部事件

7.1 端口分配

服务 端口 备注
iam HTTP 3002 已有REST 入口
iam gRPC 50052 P3 引入(与 core-edu 50053 等区分,需 coord 全局端口表确认)

7.2 Topic 命名(遵循 004 §7.2

  • edu.identity.user.created
  • edu.identity.user.updated
  • edu.identity.user.deleted
  • edu.identity.user.role_changed
  • edu.identity.role.created
  • edu.identity.role.updated

需 coord 在全局 Topic 表中登记,避免与 core-edu 的 edu.identity.user.created 冲突004 §7.2 已记录 iam 为生产者)。

8. 风险与假设

8.1 架构决策点(需 coord 仲裁)

# 决策点 我的建议 风险
1 RS256 密钥管理 P2 本地文件 IAM_PRIVATE_KEY_PATH / IAM_PUBLIC_KEY_PATHP6 迁 Vault 密钥文件权限管理;容器挂载
2 公钥暴露端点 GET /iam/.well-known/jwks.jsonJWK Set 标准) Gateway 需实现 JWK 解析
3 权限缓存失效 角色变更时本地主动 DEL iam:perms:{userId} + 发事件 缓存与 DB 短暂不一致(< 5min TTL
4 Outbox 实现 iam 自建轻量 OutboxP2 先于 core-edu 落地) 与 core-edu P3 Outbox 模式需对齐(建议 coord 在 shared-ts 提供通用工具)
5 gRPC 引入时机 P2 仅 RESTP3 随 core-edu 引入 gRPC server teacher-bff P2 仍走 HTTPP3 改 gRPC 需协调 ai03
6 DataScope 枚举 schema 用字符串枚举proto 用数值枚举 L0-L5 映射 跨层映射需文档化
7 parent_student_relations 归属 归 iam身份关系优先 core-edu 查询家长需走 iam API
8 class_subject_teachers 归属 归 core-edu教学组织数据 iam 只存 userId + 角色,不存任教关系
9 PermissionGuard 改造 DB 驱动 + Redis 缓存,废弃本地 ROLE_PERMISSIONS map 性能:每次请求多一次缓存查询
10 AuthMiddleware 注册 P2 仍不注册Controller 直读 header与 Gateway 透传策略一致) 多 Controller 重复读 header 代码

8.2 技术风险

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

8.3 假设

  • 假设 coord 在 shared-proto/events.proto 中统一定义 UserEvent / RoleEvent messageiam 只填充字段)
  • 假设 api-gateway 实现 JWK Set 拉取与 RS256 公钥校验ai01 负责)
  • 假设 teacher-bff 仍走 HTTP 调用 iamai03 负责P3 改 gRPC 时协调)
  • 假设 shared-ts 在 P2 不提供通用 Outbox 工具iam 自建P3 core-edu 落地后回写到 shared-ts

8.4 未决问题(需 coord 回复)

  1. shared-ts 是否在 P2 提供通用 Outbox 工具?还是 iam 自建?
  2. events.proto 中 UserEvent / RoleEvent 由 coord 统一定义,还是 iam 提交 PR
  3. iam gRPC 端口 50052 是否与全局端口表冲突?
  4. class_subject_teachers 表归属确认(我建议归 core-edupending-features §P2 写在 iam

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