# 模块理解确认书 — msg

> AI 标识：ai05（初版）→ ai10（复核修订，按 ai-allocation.md §3.2 重新分配）
> 负责模块：msg（P5）
> 阶段：架构设计外包 · 阶段 1（全局理解）
> 初版日期：2026-07-09
> 复核修订日期：2026-07-09（ai10 接手后修正 3 处失实条目 M2/M8/M12，并补充 ai-allocation §5 缺失项）
> 关联文档：[ai-allocation.md](../../../docs/architecture/ai-allocation.md)、[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[pending-features.md](../../../docs/architecture/roadmap/pending-features.md)、[known-issues.md](../../../docs/troubleshooting/known-issues.md)

---

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

- **层级**：业务微服务层（L5），DDD 限界上下文
- **业务领域**：D5 沟通通知领域（004 §1.1b）
- **上游**（谁调用我）：
  - teacher-bff / student-bff / parent-bff：聚合层调 msg 查询用户通知列表、发送通知
  - api-gateway：REST 转发通知请求
  - Kafka：消费 core-edu / iam 事件触发通知（004 §4.1 `CoreEdu -.事件.-> Msg`、`IAM -.事件.-> Msg`）
- **下游**（我调用谁）：
  - MySQL（写模型主库，独占）
  - Elasticsearch（全文检索，已实现 safeSearch）
  - push-gateway（gRPC 推送通道，004 §4.1 `PushGW → Msg`，当前用 fetch POST /internal/push 降级）
- **通信方式**：
  - 当前：HTTP REST（Controller，无 gRPC controller）
  - 目标态（004 §4.1 / pending-features P5）：gRPC 暴露 `NotificationService`
  - Kafka：消费 core-edu（ExamPublished/HomeworkGraded/GradeRecorded）、iam（UserRegistered）事件（004 §7.3）
- **端口**：3007（见 [msg env.ts](../src/config/env.ts)）

## 2. 我的限界上下文

- **聚合职责**：管理 Notification（通知）聚合 + NotificationPreference（用户通知偏好）
- **我的数据属于**：D5 沟通通知领域
- **我不负责**：
  - 不负责 WebSocket 长连接管理（由 push-gateway 承载）
  - 不负责业务数据变更（仅消费事件触发通知）
  - 不直接访问 core-edu / iam 的数据库
- **现有骨架领域模块**（1 个，见 [msg/src](../src)）：
  - `notifications/`：通知 CRUD + ES 全文检索 + Push Gateway 推送 + 用户偏好（6 端点）

## 3. 我与外部的契约

- **消费的 proto message**（从 shared-proto）：
  - 消费 `events.proto` 的事件 message（ClassEvent/ExamEvent/HomeworkEvent/GradeEvent）
  - **events.proto 当前无 NotificationEvent**，若 msg 发事件需补充
- **暴露的契约**（见 [msg.proto](../../../packages/shared-proto/proto/msg.proto)，包名 `next_edu_cloud.msg.v1`）：
  - `NotificationService`：SendNotification / ListNotifications / MarkAsRead / SearchNotifications
  - **当前实现均为 REST，gRPC controller 未实现**
- **事件契约**：
  - 消费（004 §7.2 / §7.3）：
    - `edu.identity.user.created` / `edu.identity.user.updated`（IAM 发，msg 发欢迎通知）
    - `edu.teaching.exam.published`（core-edu 发，msg 推送考试通知给学生）
    - `edu.teaching.assignment.submitted`（core-edu 发，msg 通知教师）
    - `edu.teaching.grade.recorded`（core-edu 发，msg 通知学生）
    - `edu.insight.mastery.updated`（data-ana 发，msg 触发预警）
  - 发布：无明确（pending-features 未要求 msg 发事件）
- **错误码前缀**：`MSG_*`（MSG_VALIDATION_ERROR / NOT_FOUND / PERMISSION_DENIED / CONFLICT / BUSINESS_ERROR / DATABASE_ERROR / INTERNAL_ERROR，见 [application-error.ts](../src/shared/errors/application-error.ts)）
- **权限点**（3 个，见 [permission.guard.ts](../src/middleware/permission.guard.ts)）：
  - `MSG_NOTIFICATION_SEND`、`MSG_NOTIFICATION_READ`、`MSG_NOTIFICATION_MANAGE`

## 4. 我的技术栈

- 语言：TypeScript 5.6（ESM 模式）
- 框架：NestJS 10
- ORM：Drizzle ORM 0.31 + mysql2 3.11
- 全文检索：@elastic/elasticsearch 8.15（已实现 safeIndex/safeSearch，**无 mapping 定义**，依赖动态 mapping）
- 消息总线：kafkajs 2.2（**已装依赖但无 consumer/producer 代码**，env.ts 有 KAFKA_BROKERS 默认值）
- 推送：fetch POST 到 push-gateway `/internal/push`（降级模式，PUSH_GATEWAY_URL 未配置或失败时跳过）
- 幂等去重：Redis（**env.ts 预留 REDIS_URL 但无 redis 客户端依赖**，pending-features P5 要求 event_id 去重）
- 可观测：pino + prom-client + OpenTelemetry（三支柱已具备）

## 5. 我的阶段归属

- **P5 沟通与 AI 阶段**（pending-features §P5）：
  - 会话/消息 CRUD + 调 Push Gateway 推送 + 通知偏好
  - 多渠道（站内/SMS/邮件/微信），沿用旧项目 dispatcher 模式
  - Elasticsearch 题库全文检索（从 MySQL 同步）
- **退出标准**：教师发广播通知 → 全在线学生实时收到（Push Gateway）→ AI 辅助出题流式返回 → 题库全文检索 < 200ms
- **依赖上游**：P1 黄金模板 classes、P3 core-edu（事件来源）、P5 push-gateway（推送通道，ai01 设计）、P5 ai（无直接依赖）

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

- [x] 权限装饰器 `@RequirePermission`（3 个 MSG_* 权限点，全部 Controller 方法已覆盖）
- [x] 错误码前缀统一（`MSG_*`）
- [x] logger / metrics / tracer 三支柱（已具备）
- [x] `/healthz` 健康检查（HealthModule 已注册）
- [⚠️] `/readyz`（**仅检查 DB `SELECT 1`，未检查 ES 连通性**，ES 故障时仍返回 ok）
- [x] 优雅关闭 SIGTERM（main.ts 已处理：app.close → closeEs → closeDb → shutdownTracer）
- [ ] 测试覆盖率 ≥ 80%（**当前 0%**，无测试文件）
- [x] Dockerfile 多阶段构建（已具备）
- [x] Zod 输入验证（Controller 用 `schema.parse(body)`；**GlobalErrorFilter 已识别 ZodError 返回 400**，见 [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) L32-42，ai10 复核修正）
- [x] GlobalErrorFilter 统一兜底

## 7. 现有骨架差距与待决策（提请 coord 仲裁）

| #   | 差距                                                                                                                                                                                      | 影响                                                                                                                                                                         | 提请决策                                                  |
| --- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------- |
| M1  | **Kafka 消费未实现**：装了 kafkajs 但无 consumer 代码，无 shared/kafka/ 目录                                                                                                              | 004 §7.3 列 msg 消费 5 类事件（ExamPublished/HomeworkGraded/GradeRecorded/UserRegistered/MasteryUpdated）                                                                    | 阶段 2 设计需补 Kafka consumer + 幂等去重                 |
| M2  | **无 Outbox**：骨架无 shared/outbox/                                                                                                                                                      | 004 §7.2 明确 msg 生产 `edu.notification.events`（NotificationRequested，消费者 push-gateway），**ai10 复核：msg 必须有 Outbox**（004 §7.2/§7.3 已定义，原"无明确"判断失实） | 阶段 2 设计补 Outbox（参照 core-edu outbox.publisher.ts） |
| M3  | **ES 无 mapping 定义**：依赖动态 mapping，索引名 "notifications" 硬编码在 service                                                                                                         | 检索质量不稳定，索引管理缺失                                                                                                                                                 | 阶段 2 设计补 mapping + ensureIndex                       |
| M4  | **无独立 repository**：notifications.service.ts 直接用 db，无 repository 抽象                                                                                                             | 与黄金模板分层不一致（iam 有 repository）                                                                                                                                    | 阶段 2 设计补 repository 层                               |
| M5  | **Redis 未引入**：env.ts 预留 REDIS_URL 但无 redis 客户端依赖                                                                                                                             | pending-features P5 要求 event_id 去重（Redis SETNX 或 DB 唯一索引）                                                                                                         | 阶段 2 设计决策：Redis SETNX vs DB 唯一索引               |
| M6  | **createBatch 无事务/无批量优化**：for 循环串行调 send，无批量 INSERT                                                                                                                     | 性能瓶颈（广播场景）                                                                                                                                                         | 阶段 2 设计改为批量 INSERT                                |
| M7  | **NotificationsModule 缺 exports**：notifications.module.ts 无 `exports: [NotificationsService]`                                                                                          | 未来 BFF 注入受阻                                                                                                                                                            | 阶段 2 设计补 exports                                     |
| M8  | **~~ZodError 未特殊处理~~（ai10 复核：已修复，失实条目）**：GlobalErrorFilter 已识别 ZodError 返回 400（见 [global-error.filter.ts](../src/shared/errors/global-error.filter.ts) L32-42） | 无（已修复）                                                                                                                                                                 | ✅ 已修复，无需处理                                       |
| M9  | **gRPC 未实现**：proto 定义了 NotificationService，但无 gRPC controller                                                                                                                   | pending-features P5 未强制 gRPC                                                                                                                                              | 确认 P5 是否启用 gRPC（ai03 提请统一决策）                |
| M10 | **README 与实现脱节**：README API 表标 `POST /notifications/:id/read`，实际是 PUT；漏 batch/user/:userId/user/:userId/page 端点；声称"消费 Kafka 事件"但无代码                            | 文档误导                                                                                                                                                                     | 阶段 2 设计同步修正 README                                |
| M11 | **main.ts 与 LifecycleService 重复关闭资源**：两者都调 closeDb/closeEs                                                                                                                    | 重复关闭可能报错（虽有 try-catch）                                                                                                                                           | 阶段 2 设计统一关闭逻辑到 LifecycleService                |
| M12 | **~~proto 包名不一致~~（ai10 复核：失实条目）**：实际 `next_edu_cloud.msg.v1` 完全符合 project_rules §5 `next_edu_cloud.<domain>.v1` 规范（coord 已裁决采用 `next_edu_cloud.*` 格式）     | 无（命名正确）                                                                                                                                                               | ✅ 命名正确，无需处理                                     |

---

## 服务审计表 — ai10（msg 复核）

> 审计标准对照 [project_rules §3](../../../.trae/rules/project_rules.md) 与 [known-issues §2.2 classes 黄金模板](../../../docs/troubleshooting/known-issues.md)

| 服务 | 权限装饰器      | 错误码前缀 | logger  | metrics        | tracer                          | /healthz | /readyz          | 优雅关闭                                | 测试覆盖率 | Dockerfile |
| ---- | --------------- | ---------- | ------- | -------------- | ------------------------------- | -------- | ---------------- | --------------------------------------- | ---------- | ---------- |
| msg  | ✅ 6 端点全覆盖 | ✅ `MSG_*` | ✅ pino | ✅ prom-client | ✅ OTel + auto-instrumentations | ✅       | ⚠️ 仅 DB 不查 ES | ⚠️ main.ts 与 LifecycleService 重复关闭 | 0%         | ✅         |

## ai10 复核结论（ai-allocation §5 设计重点对照）

ai-allocation.md §5 对 ai10（msg）明确列出 6 项设计重点，对照现有骨架：

| ai-allocation §5 设计重点                           | 现状                           | 阶段 2 是否需设计 |
| --------------------------------------------------- | ------------------------------ | ----------------- |
| 通知渠道抽象（站内信/邮件/短信，策略模式）          | ❌ 无抽象，硬编码              | ✅ 必须           |
| ES 降级查询策略（DB 不可用时走 ES 索引）            | ❌ 仅 ES 降级，无 DB 降级到 ES | ✅ 必须           |
| Kafka 消费幂等设计（event_id 去重 via Redis SETNX） | ❌ 无 Kafka 代码               | ✅ 必须           |
| 与 ai02（push-gateway）的推送通道协议（gRPC）       | ⚠️ 用 fetch POST 降级，无 gRPC | ✅ 需 gRPC 化     |
| 通知模板管理（模板 CRUD + 变量替换）                | ❌ 无模板模块                  | ✅ 必须           |
| 已读/未读状态管理（Redis 位图）                     | ❌ 仅 DB boolean               | ✅ 必须           |

**新增差距（ai10 补充，原 ai05 文档遗漏）**：

| #   | 差距                                                                            | 影响                                                  |
| --- | ------------------------------------------------------------------------------- | ----------------------------------------------------- |
| M13 | **无通知模板模块**：缺 msg_notification_templates 表、模板 CRUD、变量替换、i18n | ai-allocation §5 强制要求                             |
| M14 | **无渠道策略抽象**：send 方法硬编码 channel 判断，无 ChannelStrategy 接口       | 多渠道扩展困难（未来加微信/钉钉需改核心）             |
| M15 | **无 Redis 位图已读/未读**：isRead 仅 DB boolean，无 unread 计数缓存            | 高频未读数查询打 DB，广播场景性能瓶颈                 |
| M16 | **无 DB→ES 降级读路径**：仅有 ES→空降级，无 DB 故障时走 ES 读模型               | ai-allocation §5 要求 DB 不可用走 ES 索引             |
| M17 | **无投放记录表**：缺 msg_notification_deliveries（per-channel 投递状态）        | 无法追踪邮件/短信投递成功失败                         |
| M18 | **无幂等键**：send 无 idempotencyKey 参数，重复请求会重复发通知                 | 客户端重试导致重复通知                                |
| M19 | **无通知优先级/分类**：缺 priority/category 字段                                | 无法区分紧急通知与普通通知，无法按类退订              |
| M20 | **无调度/延迟发送**：缺 scheduledAt 字段与调度器                                | 未来定时通知、每日摘要无法支持                        |
| M21 | **权限模型过简**：仅 3 个权限点，硬编码 ROLE_PERMISSIONS                        | 应对齐 iam 的 RBAC + DataScope（DATA_SCOPE=SELF/ALL） |

---

## 跨模块契约对齐提请（coord 交叉审查）

### 接口一致性检查（msg 相关）

| 本服务声明                                   | 对方服务声明                    | 是否匹配  | 备注                                                                                             |
| -------------------------------------------- | ------------------------------- | --------- | ------------------------------------------------------------------------------------------------ |
| ai05 msg: 消费 `edu.teaching.exam.published` | ai03 core-edu: 发布考试事件     | ⚠️ 待确认 | ai03 提请 topic 命名统一（004 `edu.teaching.exam.published` vs core-edu 代码 `edu.exam.events`） |
| ai05 msg: 消费 `edu.identity.user.created`   | ai02 iam: 发布用户创建事件      | ⚠️ 待确认 | ai02 设计需确认 topic                                                                            |
| ai05 msg: 调 push-gateway `/internal/push`   | ai01 push-gateway: 暴露推送端点 | ✅ 已实现 | msg 当前用 fetch POST，push-gateway 已有 /internal/push 端点                                     |

### 全局冲突检查（msg 相关）

| 检查项               | 检查结果        | 备注                                                                                                     |
| -------------------- | --------------- | -------------------------------------------------------------------------------------------------------- |
| 端口不冲突           | ✅ msg 3007     | 与 iam(3002)/classes(3001)/teacher-bff(3003)/core-edu(3004)/content(3005)/data-ana(3006)/ai(3008) 不冲突 |
| Topic 不重复         | ⚠️ 待汇总       | msg 仅消费不发布                                                                                         |
| 错误码前缀不重叠     | ✅ `MSG_*` 唯一 | 与 iam `IAM_*` / core-edu `CORE_EDU_*` / content `CONTENT_*` 不重叠                                      |
| Proto message 不遗漏 | ⚠️ 待确认       | events.proto 无 NotificationEvent，若 msg 发事件需补充                                                   |
| proto 包名规范       | ⚠️ 不一致       | 实际 `next_edu_cloud.<domain>.v1`，规则要求 `edu.<domain>.v1`，**提请 coord 仲裁**（ai03 已提请）        |

### 待 coord 仲裁的决策项（msg 相关）

1. ~~**proto 包名统一**：`next_edu_cloud.*` vs `edu.*`~~（ai10 复核：coord 已裁决 `next_edu_cloud.*`，本项目全部 proto 已采用，无需再仲裁）
2. **gRPC 启用时机**：P4/P5 是否启用 gRPC controller，还是继续 REST（影响 content/msg/iam/core-edu 全部服务）
3. ~~**msg Outbox**：是否需要~~（ai10 复核：004 §7.2 已明确 msg 生产 `edu.notification.events`，**必须有 Outbox**，无需仲裁）
4. **msg Redis 引入**：env.ts 预留 REDIS_URL 但无依赖，幂等去重用 Redis SETNX 还是 DB 唯一索引（ai10 倾向 Redis SETNX + DB 唯一索引双保险，待 coord 确认 Redis 是否纳入 P5 基础设施）
5. **events.proto 补充**：msg 发 NotificationRequested 事件需追加 NotificationEvent message（coord 维护 proto）

---

## 下一步

1. ~~等待 coord 审核本确认书~~（ai10 复核修订完成，进入阶段 2）
2. 进入**阶段 2：模块架构设计文档**，按 ai-allocation.md §7 模板产出 [02-architecture.md](./02-architecture.md)：
   - msg 模块架构设计文档（含通知渠道抽象策略模式、ES 降级查询、Kafka 消费幂等、push-gateway 推送协议、通知模板、已读/未读状态管理）
3. 阶段 2 设计完成后同步更新 README（修正 §M10 文档脱节问题）

---

**AI Agent**: ai10 (msg)
**Coordinator**: coord-ai
**Branch**: 单仓库并行模式（直接 push main）
