feat: auto committed

This commit is contained in:
SpecialX
2026-07-10 15:05:52 +08:00
parent 9ba368477d
commit 21530dc7f6
3 changed files with 612 additions and 85 deletions

View File

@@ -1,47 +1,95 @@
# msg 对接契约
> 负责人ai10
> 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)
> 关联:[matrix.md](../matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[02-architecture-design.md](../../../services/msg/docs/02-architecture-design.md)
> 仲裁依赖ISSUE-008topic 命名、ISSUE-009RPC 数量、ISSUE-013events.proto 补齐)
---
## §1 我提供什么(对外接口)
### 1.1 gRPC 接口(如有)
### 1.1 gRPC 接口
| Service | RPC | 请求 | 响应 | 端口 |
| ----------------------------- | ---------------------- | ----------------------------- | --------------------------- | ----- |
| NotificationService | SendNotification | SendNotificationRequest | Notification | 50056 |
| NotificationService | ListNotifications | ListNotificationsRequest | ListNotificationsResponse | 50056 |
| NotificationService | MarkAsRead | MarkAsReadRequest | MarkAsReadResponse | 50056 |
| NotificationService | SearchNotifications | SearchNotificationsRequest | SearchNotificationsResponse | 50056 |
| NotificationService | RecallNotification | RecallNotificationRequest | RecallNotificationResponse | 50056 |
| NotificationPreferenceService | GetPreference | GetPreferenceRequest | NotificationPreference | 50056 |
| NotificationPreferenceService | UpdatePreference | UpdatePreferenceRequest | NotificationPreference | 50056 |
| NotificationPreferenceService | GetPreferenceByChannel | GetPreferenceByChannelRequest | ChannelPreference | 50056 |
| NotificationPreferenceService | ListPreferences | ListPreferencesRequest | ListPreferencesResponse | 50056 |
| NotificationTemplateService | CreateTemplate | CreateTemplateRequest | NotificationTemplate | 50056 |
| NotificationTemplateService | GetTemplate | GetTemplateRequest | NotificationTemplate | 50056 |
| NotificationTemplateService | ListTemplates | ListTemplatesRequest | ListTemplatesResponse | 50056 |
| NotificationTemplateService | RenderTemplate | RenderTemplateRequest | RenderedTemplate | 50056 |
> 端口50056 | proto 包名:`next_edu_cloud.msg.v1` | proto 文件:[msg.proto](../../../packages/shared-proto/proto/msg.proto)
>
> **⚠️ ISSUE-009 待仲裁**ai-allocation.md 规定 13 RPC02-architecture-design.md 设计 17 RPC。下表列出设计文档完整 17 RPC标注基线 13 RPC✅基线 / 扩展待仲裁。coord 裁决后裁剪或放宽。
### 1.2 HTTP 端点(如有
#### NotificationService9 RPC5 基线 + 4 扩展
无对外 HTTP 端点,仅 gRPC。
| RPC | 请求 | 响应 | 基线/扩展 | 说明 |
| --------------------- | ---------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------- | --------- | ----------------- |
| SendNotification | `SendNotificationRequest{user_id, type, title, content, channel, metadata?, related_entity_type?, related_entity_id?}` | `Notification` | ✅基线 | 单条发送 |
| BatchSendNotification | `BatchSendNotificationRequest{items[], group_id?}` | `BatchSendNotificationResponse{ids[], failed[]}` | ➕扩展 | 批量发送 |
| ListNotifications | `ListNotificationsRequest{user_id, only_unread?, type?, page?, page_size?}` | `ListNotificationsResponse{notifications[], total}` | ✅基线 | 列表(补分页) |
| GetUnreadCount | `GetUnreadCountRequest{user_id}` | `GetUnreadCountResponse{count}` | ➕扩展 | 未读数Redis |
| MarkAsRead | `MarkAsReadRequest{id, user_id}` | `Empty` | ✅基线 | 标记已读 |
| BatchMarkAsRead | `BatchMarkAsReadRequest{ids[], user_id}` | `Empty` | ➕扩展 | 批量已读 |
| MarkAllAsRead | `MarkAllAsReadRequest{user_id, before?}` | `Empty` | ➕扩展 | 全部已读 |
| SearchNotifications | `SearchNotificationsRequest{user_id, query, type?, page?, page_size?}` | `SearchNotificationsResponse{notifications[], total}` | ✅基线 | ES 全文检索 |
| RecallNotification | `RecallNotificationRequest{group_id, reason?}` | `RecallNotificationResponse{recalled_count}` | ✅基线 | 撤回广播 |
### 1.3 GraphQL schema如 BFF
#### NotificationPreferenceService2 RPC 基线
不适用。
| RPC | 请求 | 响应 | 基线/扩展 | 说明 |
| ------------------ | -------------------------------------------------- | --------------------------------------- | --------- | -------- |
| GetPreferences | `GetPreferencesRequest{user_id}` | `GetPreferencesResponse{preferences[]}` | ✅基线 | 偏好查询 |
| UpdatePreferences | `UpdatePreferencesRequest{user_id, preferences[]}` | `Empty` | ✅基线 | 偏好更新 |
### 1.4 Kafka 事件发布(如有
> **注**02-architecture-design.md 设计 2 RPC原 contract 版本的 GetPreferenceByChannel / ListPreferences 暂移除(待 ISSUE-009 仲裁是否保留
| Topic | Event | 消费方 |
| --------------------------- | ------------------------------------------------------ | ----------------------- |
| edu.msg.notification.events | NotificationEventaction: sent/read/recalled/failed | push-gateway / data-ana |
#### NotificationTemplateService4 RPC4 基线 + 2 扩展)
| RPC | 请求 | 响应 | 基线/扩展 | 说明 |
| -------------- | -------------------------------------------------------------------------------------------------- | -------------------------------------- | --------- | ---------- |
| CreateTemplate | `CreateTemplateRequest{code, type, title_template, content_template, default_channels, variables}` | `NotificationTemplate` | ✅基线 | 创建模板 |
| GetTemplate | `GetTemplateRequest{id}` | `NotificationTemplate` | ✅基线 | 查询模板 |
| ListTemplates | `ListTemplatesRequest{type?, status?}` | `ListTemplatesResponse{templates[]}` | ✅基线 | 模板列表 |
| UpdateTemplate | `UpdateTemplateRequest{id, ...}` | `NotificationTemplate` | ➕扩展 | 更新模板 |
| DeleteTemplate | `DeleteTemplateRequest{id}` | `Empty` | ➕扩展 | 删除模板 |
| RenderTemplate | `RenderTemplateRequest{code, variables, locale?}` | `RenderedNotification{title, content}` | ✅基线 | 渲染模板 |
> **汇总**:基线 13 RPC9 基线表中 ✅ × 5 + 偏好 ✅ × 2 + 模板 ✅ × 4 = 11... 实际基线 = NotificationService 5 + Preference 2 + Template 4 = 11。**注**:若严格按 ai-allocation 13 RPC基线应为 13此处设计文档 11 基线 + 6 扩展 = 17与 13 预算差 4。待 ISSUE-009 仲裁后最终确认。
### 1.2 HTTP 端点
> msg 对外以 gRPC 为主BFF 通过 gRPC 调用)。以下 REST 端点为过渡期/管理端使用,最终将逐步迁移至 gRPC。
| Method | Path | 权限 | 说明 |
| ------ | ---------------------------------------- | ----------------------- | -------------------- |
| POST | /notifications/send | MSG_NOTIFICATION_SEND | 单条发送 |
| POST | /notifications/batch | MSG_NOTIFICATION_SEND | 批量发送 |
| GET | /notifications/user/:userId | MSG_NOTIFICATION_READ | 用户通知列表 |
| GET | /notifications/user/:userId/unread-count | MSG_NOTIFICATION_READ | 未读数Redis |
| PUT | /notifications/:id/read | MSG_NOTIFICATION_READ | 标记已读 |
| GET | /notifications/search | MSG_NOTIFICATION_READ | ES 全文检索 |
| GET | /preferences/user/:userId | MSG_NOTIFICATION_READ | 用户偏好 |
| PUT | /preferences/user/:userId | MSG_NOTIFICATION_MANAGE | 更新偏好 |
| GET | /templates | MSG_NOTIFICATION_MANAGE | 模板列表 |
| POST | /templates | MSG_NOTIFICATION_MANAGE | 创建模板 |
### 1.3 GraphQL schema
不适用msg 是业务服务,非 BFF
### 1.4 Kafka 事件发布
> **⚠️ ISSUE-008 待仲裁**topic 命名存在三套约定per-event / aggregate / aggregate+action。本表采用 02-architecture-design.md §5.2 约定per-event topic与 004 §7.2 一致。coord 裁决后统一。
| Topic | Event Type | 触发时机 | 消费方 | Outbox |
| ---------------------------- | --------------------------- | -------------- | ------------------------------ | ------ |
| `edu.notification.sent` | NotificationSent | 通知发送成功 | push-gateway / data-ana | ✅ |
| `edu.notification.read` | NotificationRead | 通知被标记已读 | data-ana | ✅ |
| `edu.notification.recalled` | NotificationRecalled | 通知被撤回 | push-gateway删除已推送消息 | ✅ |
| `edu.notification.failed` | NotificationFailed | 通知投递失败 | data-ana监控告警 | ✅ |
> **Producer 幂等**`idempotent=true` + `transactionalId=msg-producer`
> **DLQ**:消费失败超 3 次投递 `edu.notification.dlq`(见 02-architecture-design.md §5待补充
### 1.5 错误码前缀
`MSG_`如 MSG_TEMPLATE_NOT_FOUND、MSG_CHANNEL_DISABLED、MSG_RATE_LIMITED
`MSG_`完整清单见 02-architecture-design.md §6.2
- MSG_VALIDATION_ERROR / MSG_NOT_FOUND / MSG_PERMISSION_DENIED / MSG_CONFLICT / MSG_BUSINESS_ERROR / MSG_DATABASE_ERROR / MSG_INTERNAL_ERROR当前已实现 7 个)
- MSG_ES_UNAVAILABLE / MSG_REDIS_UNAVAILABLE / MSG_PUSH_GATEWAY_UNAVAILABLE / MSG_TEMPLATE_NOT_FOUND / MSG_TEMPLATE_RENDER_ERROR / MSG_RATE_LIMIT_EXCEEDEDP5 新增 6 个)
---
@@ -49,22 +97,39 @@
### 2.1 gRPC 调用(同步)
无主动 gRPC 调用上游。msg 通过 Kafka 事件被动接收业务事件后触发通知。
| 调用方 | 对方服务 | 协议 | RPC | 用途 | 状态 |
| ------------ | ------------ | ---- | -------------------- | ---------------------- | ------------------ |
| msg → push | push-gateway | gRPC | PushService.Push | 实时推送通知到在线用户 | P5 待实现D5 |
> 当前降级fetch POST `/internal/push`(见 [notifications.service.ts](../../../services/msg/src/notifications/notifications.service.ts) L179
> **调用方向澄清**ISSUE-005004 §4.1 表述"push-gateway → Msg"有歧义,实际方向是 msg → push-gateway
### 2.2 Kafka 事件订阅(异步)
| Topic | Event | 发布方 | mock 策略 |
| --------------------------- | --------------------------------------------------------- | --------------- | ------------------------------------------------------- |
| edu.iam.user.events | UserEvent | iam (ai06) | iam 就绪前使用本地用户偏好默认值 |
| edu.exam.events | ExamEventaction: created/updated/deleted | core-edu (ai08) | core-edu 就绪前不订阅,使用本地 stub 事件触发 mock 通知 |
| edu.homework.events | HomeworkEventaction: assigned/submitted/graded | core-edu (ai08) | 同上 |
| edu.grade.events | GradeEventaction: recorded/updated | core-edu (ai08) | 同上 |
| edu.class.events | ClassEventaction: transferred | core-edu (ai08) | 同上 |
| edu.data_ana.mastery.events | MasteryEventaction: mastery.updated/warning.triggered | data-ana (ai11) | data-ana 就绪前不订阅,预警通知使用本地 stub |
> **⚠️ ISSUE-008 待仲裁**topic 命名采用 02-architecture-design.md §5.1 约定per-event topic与 004 §7.2 一致)。原 contract 版本的 aggregate topicedu.iam.user.events / edu.exam.events待 coord 裁决后统一。
### 2.3 HTTP 调用(如有)
| Topic | 来源服务 | 处理逻辑 | 触发通知 | 幂等键 | 阻塞性 |
| ----------------------------------- | ------------- | ---------------------------- | -------------------- | ---------- | ------ |
| `edu.identity.user.created` | iam (ai06) | 发送欢迎通知 | welcome 通知 | `event_id` | 🟡 |
| `edu.identity.user.updated` | iam (ai06) | 用户信息变更,清理缓存 | — | `event_id` | 🟡 |
| `edu.identity.user.deleted` | iam (ai06) | 用户删除,归档通知 | — | `event_id` | 🟡 |
| `edu.identity.user.role_changed` | iam (ai06) | 角色变更通知 | system 通知 | `event_id` | 🟡 |
| `edu.identity.role.created` | iam (ai06) | 角色新增(管理通知) | system 通知 | `event_id` | 🟡 |
| `edu.identity.role.updated` | iam (ai06) | 角色权限变更通知 | system 通知 | `event_id` | 🟡 |
| `edu.teaching.exam.published` | core-edu (ai08) | 推送考试通知给班级学生 | exam 通知fan-out | `event_id` | 🟡 |
| `edu.teaching.assignment.submitted` | core-edu (ai08) | 通知教师有学生提交作业 | homework 通知 | `event_id` | 🟡 |
| `edu.teaching.assignment.graded` | core-edu (ai08) | 通知学生作业已批改 | grade 通知 | `event_id` | 🟡 |
| `edu.teaching.grade.recorded` | core-edu (ai08) | 通知学生成绩已录入 | grade 通知 | `event_id` | 🟡 |
| `edu.teaching.attendance.recorded` | core-edu (ai08) | 出勤异常通知家长 | attendance 通知 | `event_id` | 🟡 |
| `edu.insight.mastery.updated` | data-ana (ai11) | 学情掌握度下降,触发预警 | mastery_alert 通知 | `event_id` | 🟡 |
> **⚠️ ISSUE-013 阻塞**events.proto 当前仅有 ClassEvent / ExamEvent / HomeworkEvent / GradeEvent缺 UserEvent / RoleEvent / MasteryEvent / NotificationEvent 4 类 message。msg 消费需 coord 补齐 protoD1。补齐前用通用 JSON payload 解析
>
> **幂等去重**三层防线L1 Redis SETNX `msg:processed:{event_id}` TTL 7d / L2 msg_idempotency 表 / L3 notifications.event_id UNIQUE INDEX
### 2.3 HTTP 调用
无主动 HTTP 调用上游push-gateway 走 gRPCHTTP 仅为降级)。
---
@@ -72,39 +137,60 @@
### 3.1 我依赖的上游就绪标志
- [ ] iam gRPC 50052 启用ai06—— 用于用户通知偏好查询(可选)
- [ ] core-edu gRPC 50053 启用ai08—— 业务事件来源
- [ ] edu.exam.events / edu.homework.events / edu.grade.events / edu.class.events topic 有事件发布ai08
- [ ] data-ana gRPC 50055 启用ai11—— 预警事件来源
- [ ] edu.data_ana.mastery.events topic 有事件发布ai11
| 标志 | 提供方 | 阻塞性 | 说明 |
| ---- | ------ | ------ | ---- |
| events.proto 补 UserEvent/RoleEvent/MasteryEvent/NotificationEvent | coord | 🔴 阻塞 T9 | D1ISSUE-013 |
| events.proto GradeEvent 补 class_id全部补 student_ids[] | coord | 🔴 阻塞 fan-out | D2ISSUE-006 |
| msg.proto 补 5 扩展 RPC + Preference/Template Service | coord | 🔴 阻塞 gRPC | D3-D4ISSUE-009 |
| push-gateway gRPC PushService.Push | ai02 | 🔴 阻塞 T8 | D5 |
| iam 发布 6 类 user/role 事件 | ai06 | 🟡 集成验证 | D6开发期用 mock |
| core-edu 发布 5 类教学事件 | ai08 | 🟡 集成验证 | D7 |
| data-ana 发布 mastery 事件 | ai11 | 🟡 集成验证 | D8 |
| Redis 集群可用 | infra | 🟡 降级 DB | D9 |
### 3.2 我的就绪标志(供下游消费)
- [ ] msg gRPC 50056 启用HealthService.Check 返回 SERVING
- [ ] NotificationService 5 RPC 可调用
- [ ] NotificationPreferenceService 4 RPC 可调用
- [ ] NotificationTemplateService 4 RPC 可调用(含 RenderTemplate 模板渲染
- [ ] edu.msg.notification.events topic 可发布(供 push-gateway 推送
- [ ] NotificationService RPC 可调用(基线 5 + 扩展 4待 ISSUE-009 仲裁)
- [ ] NotificationPreferenceService 2 RPC 可调用
- [ ] NotificationTemplateService RPC 可调用(基线 4 + 扩展 2含 RenderTemplate
- [ ] `edu.notification.sent/read/recalled/failed` topic 可发布(待 ISSUE-008 仲裁命名
- [ ] /readyz 返回 6 项依赖状态DB/ES/Redis/Kafka producer/Kafka consumer/PushGateway
- [ ] 测试覆盖率 ≥ 80%
---
## §4 Mock 策略
### 4.1 我提供的 mock
### 4.1 我提供的 mock(供下游)
在 msg 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / push-gateway提供以下 mock
在 msg 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / push-gateway提供 mock
- **gRPC mock**使用 grpc-mock 拦截 50056 端口
- NotificationService.ListNotifications 返回固定 10 条未读通知
- NotificationService.MarkAsRead 返回 success=true
- NotificationPreferenceService.GetPreference 返回默认偏好in_app+email 开启sms+push 关闭)
- NotificationTemplateService.RenderTemplate 返回固定 title+content
- **Kafka mock**msg 就绪前不发布真实 NotificationEventpush-gateway 使用本地 stub 推送
- **gRPC mock**grpc-mock 拦截 50056 端口
- ListNotifications 返回固定 10 条未读通知
- MarkAsRead 返回 success=true
- GetPreferences 返回默认偏好in_app + email 开启sms + push 关闭)
- RenderTemplate 返回固定 title + content
- **Kafka mock**msg 就绪前不发布真实通知事件push-gateway 使用本地 stub 推送
### 4.2 我消费的 mock
### 4.2 我消费的 mock(开发期间)
在真实上游就绪前msg 使用以下 mock
- 业务事件core-edu/data-ana 就绪前msg 内置定时器发布本地 stub 事件ExamEvent/HomeworkEvent触发 mock 通知流程
- 用户偏好iam 就绪前使用默认偏好(所有用户 in_app 开启)
- 模板渲染:内置 5 个常用模板exam.created / homework.assigned / grade.recorded / warning.triggered / system.notice
- **业务事件**core-edu / data-ana 就绪前msg 内置定时器发布本地 stub 事件ExamEvent / HomeworkEvent触发 mock 通知流程
- **用户偏好**iam 就绪前使用默认偏好(所有用户 in_app 开启)
- **模板渲染**:内置 5 个常用模板exam.published / homework.graded / grade.recorded / mastery.warning / system.notice
- **Push Gateway**ai02 就绪前用 fetch POST /internal/push 降级(当前实现保留)
---
## §5 待仲裁项汇总
| ISSUE | 主题 | 阻塞性 | 当前采用 |
| ----- | ---- | ------ | -------- |
| ISSUE-008 | Kafka topic 命名per-event vs aggregate | 🟡 | per-event02-architecture-design.md §5 + 004 §7.2 |
| ISSUE-009 | RPC 数量13 vs 17 | 🟡 | 1702-architecture-design.md §4.2),标注基线/扩展 |
| ISSUE-013 | events.proto 缺 4 类 message | 🔴 | 用 JSON payload 降级,待 coord 补齐 |
| ISSUE-006 | events.proto P9 字段描述 | 🟡 | 待 coord 修正 |
| ISSUE-010 | markAsRead 权限点 | 🟡 | 以 02-architecture-design.md §6.1 为准READ |
| ISSUE-011 | DB↔ES 降级方向 | 🟡 | 待 coord 仲裁(双向降级 vs 单向) |

View File

@@ -6,19 +6,189 @@
---
## 问题列表
## §0 已有仲裁核查ai10 复核)
<!--
追加条目格式:
> 本节核查 01-understanding.md / 02-architecture-design.md 中引用的已有仲裁,对照源码/proto 验证准确性。
### ISSUE-[编号]-[AI标识][标题]
### ISSUE-001-ai10M8 ZodError 已修复 — 核查通过
- **提请方**aiXX
- **日期**YYYY-MM-DD
- **类型**契约不明确 / 工作量超批 / 前置依赖缺失 / 编号冲突 / 其他
- **描述**[详细描述问题]
- **建议方案**[AI 的建议]
- **状态**待 coord 仲裁 / 已裁决(见 coord.md §X
-->
- **提请方**ai10
- **日期**2026-07-10
- **类型**仲裁核查
- **描述**01-understanding.md M8 称"GlobalErrorFilter 已识别 ZodError 返回 400"。核查 [global-error.filter.ts](../../../services/msg/src/shared/errors/global-error.filter.ts) L32-42`else if (exception instanceof ZodError) { statusCode = 400; ... }`,确实已处理,返回 `MSG_VALIDATION_ERROR` + 400。
- **核查结论**:✅ 仲裁准确M8 标记"已修复"无误
- **状态**已裁决(核查通过,无需处理
(暂无问题)
### ISSUE-002-ai10M12 proto 包名规范 — 核查通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**01-understanding.md M12 称"实际 `next_edu_cloud.msg.v1` 符合规范"。核查 [msg.proto](../../../packages/shared-proto/proto/msg.proto) L3`package next_edu_cloud.msg.v1;`,与 01-understanding.md 描述一致。
- **核查结论**:✅ 仲裁准确,但存在规则冲突(见 ISSUE-007
- **状态**:已裁决(核查通过)
### ISSUE-003-ai10M2 msg 必须有 Outbox — 核查通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**01-understanding.md M2 称"004 §7.2 明确 msg 生产 `edu.notification.events`msg 必须有 Outbox"。核查 004 §7.3 L638 `NotificationRequested` 事件Msg 投递通知到多渠道)+ known-issues §msg L346"Outbox 强制"。
- **核查结论**:✅ 仲裁准确msg 必须实现 OutboxP5 强制)
- **状态**:已裁决(核查通过)
### ISSUE-004-ai10core-edu 事件 topic 命名统一 — 核查部分通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**02-architecture-design.md §7.2 P11 称"coord 已仲裁采用 `edu.teaching.*` 新约定"。核查:
- 004 §7.2 L623-625 使用 `edu.teaching.assignment.submitted` / `edu.teaching.exam.published` / `edu.teaching.grade.recorded`(新约定)✅
- [events.proto](../../../packages/shared-proto/proto/events.proto) L9-13 注释仍用 `edu.exam.events` / `edu.homework.events`(旧约定)❌ 未同步
- [matrix.md](../matrix.md) §4 L112-115 使用 `edu.exam.events` / `edu.homework.events`(旧约定)❌ 未同步
- **核查结论**:⚠️ 仲裁已作出但未全量同步events.proto 注释与 matrix.md 仍用旧约定,需 coord 统一更新
- **状态**:待 coord 同步(见 ISSUE-008
### ISSUE-005-ai10Push Gateway 调用方向歧义 — 核查通过
- **提请方**ai10
- **日期**2026-07-10
- **类型**:仲裁核查
- **描述**02-architecture-design.md §7.2 P10 称"004 §4.1 写 PushGW→Msg实际是 Msg→PushGW"。核查 004 §4.1 L413`push-gateway → Msg | gRPC | 推送通道建立`,方向确实反了。实际流程是 msg 调 push-gateway 的 gRPC PushService.Push见 [notifications.service.ts](../../../services/msg/src/notifications/notifications.service.ts) L179 fetch POST /internal/push 降级实现)。
- **核查结论**:✅ 歧义确认,建议 coord 修正 004 §4.1 表述为"Msg → push-gateway (gRPC)"
- **状态**:待 coord 修正 004
---
## §1 新发现问题ai10 提请)
### ISSUE-006-ai10events.proto P9 字段描述不准确
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**02-architecture-design.md §7.2 P9 称"events.proto ExamEvent / HomeworkEvent / GradeEvent 需补 `class_id` / `student_ids[]` 字段"。核查 events.proto
- `ExamEvent` L32 **已有** `class_id` 字段 ✅
- `HomeworkEvent` L45 **已有** `class_id` 字段 ✅
- `GradeEvent` L50-59 **无** `class_id`(仅有 `student_id`)❌
- 三者均**无** `student_ids[]`(复数,用于 fan-out 广播)❌
- **建议方案**:修正 P9 表述为"`GradeEvent` 需补 `class_id`;全部事件需补 `student_ids[]` 字段msg fan-out 广播通知需要)"
- **状态**:待 coord 仲裁
### ISSUE-007-ai10proto 包名规则冲突project_rules vs 实际)
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[project_rules §5](../../../.trae/rules/project_rules.md) 规定"包名规范:`edu.<domain>.v1`(如 `edu.iam.v1``edu.core_edu.v1`",但实际所有 proto 文件使用 `next_edu_cloud.<domain>.v1`(如 msg.proto L3 `next_edu_cloud.msg.v1`、events.proto L3 `next_edu_cloud.events.v1`。01-understanding.md 称"coord 已裁决采用 `next_edu_cloud.*`",但 project_rules §5 未同步更新,仍写 `edu.<domain>.v1`
- **建议方案**coord 统一裁决,二选一:
- 方案 A更新 project_rules §5 为 `next_edu_cloud.<domain>.v1`(与实际 proto 一致)
- 方案 B重命名所有 proto package 为 `edu.<domain>.v1`(与规则一致,但改动大)
- **状态**:待 coord 仲裁
### ISSUE-008-ai10Kafka topic 命名三套约定并存
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**msg 相关的 Kafka topic 命名存在三套约定:
- **约定 A004 §7.2per-event topic**`edu.identity.user.created` / `edu.teaching.exam.published` / `edu.notification.sent`
- **约定 Bmatrix.md §4 + events.proto 注释aggregate topic**`edu.iam.user.events` / `edu.exam.events` / `edu.notification.requested`
- **约定 Cmsg_contract.md §1.4aggregate topic + action 字段)**`edu.msg.notification.events`action: sent/read/recalled/failed
- 02-architecture-design.md §5.1/§5.2 采用约定 Amsg_contract.md §1.4 采用约定 Cmatrix.md 采用约定 B。known-issues §全局 L182 已标记此冲突。
- **建议方案**coord 统一为一套约定。ai10 倾向约定 Aper-event topic理由
- 004 §7.2 已采用,是架构设计意图唯一源
- per-event topic 便于消费者按需订阅,避免反序列化无关事件
- 与 NotificationSent / NotificationRead 等事件命名PascalCase对齐
- **状态**:待 coord 仲裁
### ISSUE-009-ai10RPC 数量超预算17 vs 13
- **提请方**ai10
- **日期**2026-07-10
- **类型**:工作量超批
- **描述**[ai-allocation.md §3.2](../../ai-allocation.md) L112 与 [matrix.md](../matrix.md) §2 L90 均规定 msg 为"3 Service 13 RPC"。但 02-architecture-design.md §4.2 列出 17 RPC
- NotificationService 9 RPCSendNotification / BatchSendNotification / ListNotifications / GetUnreadCount / MarkAsRead / BatchMarkAsRead / MarkAllAsRead / SearchNotifications / RecallNotification
- NotificationPreferenceService 2 RPCGetPreferences / UpdatePreferences
- NotificationTemplateService 6 RPCCreateTemplate / GetTemplate / ListTemplates / UpdateTemplate / DeleteTemplate / RenderTemplate
- 而 msg_contract.md §1.1 列出 13 RPC分布不同5+4+4两文档互相不一致
- **建议方案**coord 裁决 RPC 范围,二选一:
- 方案 A维持 13 RPC 预算02-architecture-design.md 裁剪至 13移除 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / UpdateTemplate / DeleteTemplate降级为 REST only 或合并)
- 方案 B放宽至 17 RPC同步更新 ai-allocation.md + matrix.md + msg_contract.md
- **状态**:待 coord 仲裁
### ISSUE-010-ai10markAsRead 权限点与设计不一致
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**[notifications.controller.ts](../../../services/msg/src/notifications/notifications.controller.ts) L74 markAsRead 使用 `MSG_NOTIFICATION_MANAGE` 权限,但 02-architecture-design.md §6.1 L730 规定 markAsRead 应使用 `MSG_NOTIFICATION_READ`。MANAGE 权限通常给管理员,学生标记自己通知已读不应需要 MANAGE 权限。
- **建议方案**:以 02-architecture-design.md §6.1 为准READP5 实现时修正 controller 权限点
- **状态**:待 coord 确认
### ISSUE-011-ai10DB→ES 降级方向与 ai-allocation §5 相反
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- [ai-allocation.md §5](../../ai-allocation.md) ai10 设计重点要求"ES 降级查询策略(**DB 不可用时走 ES 索引**"——即 DB 故障时 ES 作为读模型兜底
- 02-architecture-design.md §3.2.2 / §3.4 描述"**ES 不可用时降级到 MySQL LIKE 查询**"——即 ES 故障时 DB 兜底
- 01-understanding.md M16 称"无 DB→ES 降级读路径"
- 三处描述方向相反,需统一
- **建议方案**ai10 倾向双向降级(两种故障场景都覆盖):
- ES 故障 → DB LIKE 查询(设计文档已覆盖)
- DB 故障 → ES 只读模式ai-allocation 要求,设计文档需补充)
- 但 DB 故障时写操作无法降级(必须等 DB 恢复),仅读操作可走 ES
- **状态**:待 coord 仲裁
### ISSUE-012-ai10设计文档缺 DLQ 与三层幂等防线
- **提请方**ai10
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**[known-issues §msg](../../../docs/troubleshooting/known-issues.md) L344 / L356 已记录两项 ai10 设计点,但 02-architecture-design.md 未覆盖:
- **三层幂等防线**L344L1 Redis SETNX / L2 msg_idempotency 表 / L3 notifications.source_event_id 唯一索引。设计文档 §5.5 仅描述两层Redis + DB UNIQUE缺中间层 msg_idempotency 表
- **死信队列**L356消费失败超 3 次投递 `edu.notification.dlq`。设计文档 §5 完全未提及 DLQ 设计
- **建议方案**02-architecture-design.md 补充:
- §3.1 补 `msg_idempotency` 表 schema中间层
- §5.5 改为三层幂等防线
- §5 补 DLQ 设计(重试 3 次后投递 `edu.notification.dlq` + 告警)
- **状态**:待 coord 确认非阻塞ai10 自行补充设计文档即可)
### ISSUE-013-ai10events.proto 缺 4 类 message 阻塞 msg 消费
- **提请方**ai10
- **日期**2026-07-10
- **类型**:前置依赖缺失
- **描述**[events.proto](../../../packages/shared-proto/proto/events.proto) 仅有 ClassEvent / ExamEvent / HomeworkEvent / GradeEvent 4 个 message。msg 消费还需要:
- `UserEvent`iam 发布 user.created/updated/deleted/role_changed— 阻塞欢迎通知/角色变更通知
- `RoleEvent`iam 发布 role.created/updated— 阻塞角色变更通知
- `MasteryEvent`data-ana 发布 mastery.updated— 阻塞学情预警通知
- `NotificationEvent`msg 发布 notification.sent/read/recalled/failed— 阻塞 push-gateway 消费 msg 事件
- **建议方案**coord 维护 shared-proto在 P5 启动前补齐这 4 个 message。msg 在 proto 补齐前用通用 JSON payload 解析A6 假设)
- **状态**:待 coord 仲裁(🔴 阻塞 P5 消费链路)
### ISSUE-014-ai10msg_contract.md 与 02-architecture-design.md RPC 清单不一致
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**:两文档 RPC 清单存在差异:
- msg_contract.md 独有02 缺GetPreferenceByChannel、ListPreferences
- 02-architecture-design.md 独有contract 缺BatchSendNotification、GetUnreadCount、BatchMarkAsRead、MarkAllAsRead、UpdateTemplate、DeleteTemplate
- 即使忽略 ISSUE-009 的数量问题,两文档的 RPC 组合也不同
- **建议方案**:待 ISSUE-009 仲裁后,统一两文档 RPC 清单
- **状态**:待 coord 仲裁(依赖 ISSUE-009
### ISSUE-015-ai10msg_contract.md Kafka 发布事件与设计文档不一致
- **提请方**ai10
- **日期**2026-07-10
- **类型**:契约不明确
- **描述**
- msg_contract.md §1.4`edu.msg.notification.events` topic单一 NotificationEvent 含 action 字段
- 02-architecture-design.md §5.24 个 per-event topic`edu.notification.sent` / `edu.notification.read` / `edu.notification.recalled` / `edu.notification.failed`
- matrix.md §4 L118`edu.notification.requested`(第三种命名)
- **建议方案**:待 ISSUE-008 仲裁 topic 命名约定后统一
- **状态**:待 coord 仲裁(依赖 ISSUE-008

View File

@@ -1,45 +1,316 @@
# msg 工作排期
> 负责人ai10
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/msg_contract.md](../contracts/msg_contract.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,最后统一集成测试)
> 关联:[workline.md](../workline.md)、[coord.md](../coord.md)、[contracts/msg_contract.md](../contracts/msg_contract.md)、[02-architecture-design.md §10](../../../services/msg/docs/02-architecture-design.md)
> 模式:全并行(各 AI 一口气完成 P2-P6 全部代码,开发期间用 mock最后统一集成测试)
---
## §1 总览
msg 是消息服务,提供 NotificationServicePreferenceServiceTemplateService基于 Outbox 模式发布消息事件。全阶段目标P2 服务骨架+Outbox → P3 三大 Service 实现 → P4-P6 持续优化
msg 是消息通知中台P5,提供 NotificationService + NotificationPreferenceService + NotificationTemplateService 三服务,基于 Outbox 模式发布通知事件,消费 iam/core-edu/data-ana 共 12 类事件触发多渠道通知
**全阶段目标**
- P2-P3服务骨架补全schema 迁移 + Outbox + Kafka 基础设施 + mock 消费)
- P4三大 Service 主体实现Notification + Preference + Template+ ChannelDispatcher 多渠道
- P5gRPC 50056 启用 + PushGatewayClient gRPC + 12 类事件 consumer + ES mapping
- P6测试覆盖 ≥ 80% + /readyz 硬化 + 黄金模板对齐 + README 修正
**批次归属**:批次 4P5依赖批次 3 contentai09就绪后启动预估 13 天。
---
## §2 全阶段甘特图P2-P6,各 AI 自行细化
## §2 全阶段甘特图P2-P6
```mermaid
gantt
title ai10 msg 全阶段排期
title ai10 msg 全阶段排期13 天)
dateFormat YYYY-MM-DD
axisFormat %m-%d
section P2-P6
[阶段任务] :a10a, 2026-07-10, Xd
```
section P2-P3 骨架补全
T1-T2 schema迁移(notifications+preferences字段) :crit, a1, 2026-07-10, 1d
T3 新建msg_notification_templates表 :a2, after a1, 1d
T4 新建msg_outbox_events+Publisher worker :crit, a3, after a1, 2d
T5 新建shared/kafka(producer+consumer骨架) :crit, a4, after a3, 1d
T6 引入ioredis+IdempotencyGuard(SETNX) :a5, after a3, 1d
> **注意**:以上为 coord 初始规划ai10 接管后必须自行细化为完整 P2-P6 排期。
section P4 主体实现
T7 ChannelDispatcher多渠道抽象 :crit, a6, after a4, 2d
T10 重构notifications.service(移除同步fetch) :a7, after a6, 1d
T11 batchMarkAsRead/markAllAsRead/recall/getUnreadCount :a8, after a7, 1d
T12 NotificationPreference CRUD :a9, after a7, 1d
T13 NotificationTemplate CRUD+render :a10, after a7, 1d
T15 createBatch改批量INSERT :a11, after a7, 1d
section P5 gRPC+事件+ES
T8 PushGatewayClient gRPC(替代fetch降级) :crit, a12, after a8, 1d
T9 12类Kafka事件consumer(iam/core-edu/data-ana) :crit, a13, after a12, 2d
T14 ES索引mapping+ensureIndex+同步 :a14, after a12, 1d
gRPC 50056启用+3 Service 13 RPC :crit, a15, after a13, 1d
section P6 硬化与对齐
T16 NotificationsModule补exports :a16, after a15, 1d
T17 /readyz多依赖(DB/ES/Redis/Kafka/PushGW) :a17, after a15, 1d
T19 统一关闭到LifecycleService :a18, after a15, 1d
T20-T21 DB改getDb()+ID改cuid2 :a19, after a15, 1d
T22 单元测试覆盖≥80% :crit, a20, after a19, 2d
T23 修正README与实现对齐 :a21, after a20, 1d
```
---
## §3 详细任务
### 全阶段任务
### P2-P3 骨架补
#### T1-T2schema 迁移notifications + preferences 字段扩展)
- **负责人**ai10
- **交付物**:⚠️ 由 ai10 自行补充
- **依赖**:见 [contracts/msg_contract.md](../contracts/msg_contract.md)
- **验收标准**:⚠️ 由 ai10 自行补充
- **依赖**msg 独占 DB
- **交付物**
- `msg_notifications` 表新增 status / metadata / related_entity_type / related_entity_id / group_id / sender_id / template_id / event_id / updated_at 字段 + 6 个索引
- `msg_notification_preferences` 表补齐 created_at / updated_at + 新增 frequency_limit / quiet_hours_start / quiet_hours_end / quiet_hours_timezone 字段
- **验收标准**Drizzle schema 定义更新,迁移脚本可执行,索引符合 02-architecture-design.md §3.1.1 / §3.1.2
#### T3新建 msg_notification_templates 表
- **负责人**ai10
- **依赖**T1-T2
- **交付物**`msg_notification_templates` 表 schemacode + type + title_template + content_template + default_channels + variables + locale + statusUNIQUE INDEX `(code, locale)`
- **验收标准**schema 定义 + 迁移脚本,符合 02-architecture-design.md §3.1.3
#### T4新建 msg_outbox_events 表 + Outbox Publisher worker
- **负责人**ai10
- **依赖**T1-T2
- **交付物**
- `msg_outbox_events` 表 schemaevent_id PK + aggregate_type + aggregate_id + event_type + topic + payload + status + retry_count + created_at + published_at + next_retry_at
- `shared/outbox/outbox.publisher.ts`(独立 worker每 1s 轮询 PENDING 事件投递 Kafka
- `shared/outbox/outbox.schema.ts`Drizzle schema
- **验收标准**Outbox Publisher 可轮询 + 投递 + 更新 status=SENT符合 02-architecture-design.md §3.1.4 / §5.4
- **关联 ISSUE**ISSUE-003Outbox 强制)
#### T5新建 shared/kafka/producer + consumer 骨架)
- **负责人**ai10
- **依赖**T4
- **交付物**
- `shared/kafka/kafka.producer.ts`idempotent=true + transactionalId=msg-producer
- `shared/kafka/kafka.consumer.ts`consumer group = msg-service
- `shared/kafka/topic-map.ts`PRODUCER_TOPIC_MAP + CONSUMER_TOPICS
- **验收标准**producer 可投递消息consumer 可订阅 topic符合 02-architecture-design.md §5.3
#### T6引入 ioredis + IdempotencyGuardSETNX
- **负责人**ai10
- **依赖**:无
- **交付物**
- `shared/redis/redis.client.ts`ioredis 客户端,连接 REDIS_URL
- `shared/redis/idempotency.guard.ts`SETNX `msg:processed:{event_id}` TTL 7 天)
- `shared/redis/read-bitmap.ts`(已读位图 BITCOUNT / GETBIT
- env.ts 补 REDIS_URL 必填校验
- **验收标准**IdempotencyGuard SETNX 原子去重Redis 不可用时降级到 DB 唯一索引
- **关联 ISSUE**ISSUE-012三层幂等防线补 msg_idempotency 表中间层)
### P4 主体实现
#### T7ChannelDispatcher 多渠道抽象
- **负责人**ai10
- **依赖**T5、T6
- **交付物**
- `channels/notification-channel.interface.ts`NotificationChannel 接口)
- `channels/in-app.channel.ts`(站内信,写 MySQL
- `channels/email.channel.ts`邮件SMTP异步队列
- `channels/sms.channel.ts`短信HTTP API
- `channels/wechat.channel.ts`微信HTTP API
- `channels/push.channel.ts`(推送,调 PushGatewayClient
- `channels/channel-dispatcher.ts`Promise.allSettled 并行投递 + in_app 总是发送)
- **验收标准**:新增渠道只需实现接口 + 注册,符合 02-architecture-design.md §12
#### T10重构 notifications.service.ts
- **负责人**ai10
- **依赖**T4、T5、T7
- **交付物**:重构 notifications.service.ts移除同步 fetch push-gateway改为 ChannelDispatcher + Outbox 事务
- **验收标准**send 方法走 BEGIN TX → INSERT notifications + INSERT outbox → COMMIT → ChannelDispatcher.dispatch
#### T11新增端点batchMarkAsRead / markAllAsRead / recall / getUnreadCount
- **负责人**ai10
- **依赖**T10
- **交付物**controller + service 新增 4 个端点
- **验收标准**:符合 02-architecture-design.md §4.1 REST API 表
- **关联 ISSUE**ISSUE-010markAsRead 权限改 READ
#### T12NotificationPreference CRUD
- **负责人**ai10
- **依赖**T1-T2
- **交付物**`preferences/` 目录controller + service + repository + schema + dto
- **验收标准**GET / PUT preferences 端点可用
#### T13NotificationTemplate CRUD + render
- **负责人**ai10
- **依赖**T3
- **交付物**`templates/` 目录controller + service + repository + schema + dto`{{variable}}` 占位符替换渲染
- **验收标准**CreateTemplate / GetTemplate / ListTemplates / RenderTemplate 可用
#### T15createBatch 改批量 INSERT
- **负责人**ai10
- **依赖**T10
- **交付物**createBatch 改为 `db.insert(notifications).values([...])` 批量 INSERT
- **验收标准**1 万条广播通知 < 5s性能验收
### P5 gRPC + 事件 + ES
#### T8PushGatewayClient gRPC
- **负责人**ai10
- **依赖**D5push-gateway 提供 gRPC PushService.Push
- **交付物**`shared/push/push-gateway.client.ts`gRPC 调用,替代 fetch POST /internal/push 降级)
- **验收标准**gRPC 调用 push-gateway PushService.Push降级模式保留push-gateway 不可用时走 in_app
- **关联 ISSUE**ISSUE-005调用方向澄清
#### T912 类 Kafka 事件 consumer
- **负责人**ai10
- **依赖**T5、T6、D1-D2events.proto 补齐 message + 字段)
- **交付物**
- `shared/kafka/consumers/iam.consumer.ts`6 类 user/role 事件)
- `shared/kafka/consumers/core-edu.consumer.ts`5 类 exam/homework/grade/attendance 事件)
- `shared/kafka/consumers/data-ana.consumer.ts`1 类 mastery 事件)
- 每个 consumer 走 IdempotencyGuard → NotificationService.createNotificationFromEvent
- **验收标准**12 类事件均可消费 + 幂等去重 + fan-out 通知
- **关联 ISSUE**ISSUE-013events.proto 缺 4 类 message阻塞
#### T14ES 索引 mapping + ensureIndex + 同步
- **负责人**ai10
- **依赖**:无
- **交付物**
- `config/elasticsearch.ts``notifications` 索引 mappingik_max_word 分词)
- ensureIndex 幂等创建
- 数据同步Outbox 事件触发 ES 索引更新(替代当前同步 safeIndex
- **验收标准**ES 检索可用mapping 符合 02-architecture-design.md §3.2.1
- **关联 ISSUE**ISSUE-011降级方向待仲裁
#### gRPC 50056 启用 + 3 Service 13 RPC
- **负责人**ai10
- **依赖**D3-D4msg.proto 补 RPC + Service
- **交付物**
- `notifications.grpc.controller.ts`NotificationService gRPC
- `preferences.grpc.controller.ts`NotificationPreferenceService gRPC
- `templates.grpc.controller.ts`NotificationTemplateService gRPC
- app.module.ts 注册 gRPC server :50056
- **验收标准**HealthService.Check 返回 SERVING13 RPC 可调用
- **关联 ISSUE**ISSUE-009RPC 数量待仲裁 13 vs 17
### P6 硬化与对齐
#### T16NotificationsModule 补 exports
- **负责人**ai10
- **依赖**:无
- **交付物**notifications.module.ts 补 `exports: [NotificationsService]`
- **验收标准**BFF 可注入 NotificationsService
#### T17/readyz 多依赖检查
- **负责人**ai10
- **依赖**T4、T5、T6、T8
- **交付物**health.controller.ts /readyz 检查 DB / ES / Redis / Kafka producer / Kafka consumer / PushGateway 6 项依赖
- **验收标准**:符合 02-architecture-design.md §6.6 判定规则DB down → downRedis/ES/Kafka down → degraded
#### T19统一关闭到 LifecycleService
- **负责人**ai10
- **依赖**:无
- **交付物**:移除 main.ts 重复 closeDb/closeEs统一到 LifecycleService8 步关闭序列
- **验收标准**:符合 02-architecture-design.md §6.7 优雅关闭顺序
#### T20-T21DB 改 getDb() + ID 改 cuid2
- **负责人**ai10
- **依赖**:无
- **交付物**database.ts 改 getDb() 函数式service 层 randomUUID → cuid2
- **验收标准**:与 classes 黄金模板对齐
#### T22单元测试覆盖 ≥ 80%
- **负责人**ai10
- **依赖**:全部 P4-P5 任务
- **交付物**`*.spec.ts`Service / Repository / ChannelDispatcher / IdempotencyGuard / OutboxPublisher
- **验收标准**:覆盖率 ≥ 80%
#### T23修正 README
- **负责人**ai10
- **依赖**:全部任务
- **交付物**README.md API 表与实现对齐PUT /:id/read、补 batch/user/:userId/page 端点、补 env 变量表)
- **验收标准**:无文档脱节
---
## §4 依赖与就绪信号
- **我依赖**:⚠️ 由 ai10 自行补充(见 contract.md
- **我的就绪信号**:⚠️ 由 ai10 自行补充
### 4.1 我依赖的上游就绪标志
- [ ] **D1**events.proto 补 UserEvent / RoleEvent / NotificationEvent / MasteryEvent messagecoord 维护)— 🔴 阻塞 T9
- [ ] **D2**events.proto GradeEvent 补 class_id全部事件补 student_ids[]coord 维护)— 🔴 阻塞 T9 fan-out
- [ ] **D3**msg.proto 补 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / RecallNotification RPCcoord 维护)— 🔴 阻塞 gRPC依赖 ISSUE-009 仲裁)
- [ ] **D4**msg.proto 补 NotificationPreferenceService + NotificationTemplateServicecoord 维护)— 🔴 阻塞 gRPC
- [ ] **D5**push-gateway 提供 gRPC PushService.Push 方法ai02— 🔴 阻塞 T8
- [ ] **D6**iam 发布 6 类 user/role 事件ai06— 🟡 不阻塞开发(用 mock阻塞集成验证
- [ ] **D7**core-edu 发布 5 类教学事件ai08— 🟡 同上
- [ ] **D8**data-ana 发布 mastery 事件ai11— 🟡 同上
- [ ] **D9**Redis 集群可用infra 部署)— 🟡 降级到 DB 唯一索引
### 4.2 我的就绪标志(供下游消费)
- [ ] msg gRPC 50056 启用HealthService.Check 返回 SERVING
- [ ] NotificationService RPC 可调用(数量待 ISSUE-009 仲裁)
- [ ] NotificationPreferenceService RPC 可调用
- [ ] NotificationTemplateService RPC 可调用(含 RenderTemplate
- [ ] `edu.notification.*` topic 可发布(供 push-gateway / data-ana 消费,命名待 ISSUE-008 仲裁)
- [ ] /readyz 返回 6 项依赖状态
- [ ] 测试覆盖率 ≥ 80%
---
## §5 Mock 策略
### 5.1 我提供的 mock供下游
在 msg 真实服务就绪前为下游teacher-bff / student-bff / parent-bff / push-gateway提供 mock
- **gRPC mock**grpc-mock 拦截 50056 端口
- ListNotifications 返回固定 10 条未读通知
- MarkAsRead 返回 success=true
- GetPreference 返回默认偏好in_app + email 开启sms + push 关闭)
- RenderTemplate 返回固定 title + content
- **Kafka mock**msg 就绪前不发布真实通知事件push-gateway 使用本地 stub 推送
### 5.2 我消费的 mock开发期间
在真实上游就绪前msg 使用以下 mock
- **业务事件**core-edu / data-ana 就绪前msg 内置定时器发布本地 stub 事件ExamEvent / HomeworkEvent触发 mock 通知流程验证 consumer 链路
- **用户偏好**iam 就绪前使用默认偏好(所有用户 in_app 开启)
- **模板渲染**:内置 5 个常用模板exam.published / homework.graded / grade.recorded / mastery.warning / system.notice
- **Push Gateway**ai02 就绪前用 fetch POST /internal/push 降级(当前实现保留)
---
## §6 风险与缓解
| 风险 | 影响 | 缓解 |
| ---- | ---- | ---- |
| events.proto 补齐延迟D1-D2 | T9 consumer 无法验证 | 开发期用 stub 事件proto 补齐后切换 |
| RPC 数量仲裁延迟ISSUE-009 | gRPC controller 实现范围不确定 | 先实现 13 RPC 基线,仲裁后增减 |
| topic 命名仲裁延迟ISSUE-008 | Kafka producer/consumer topic 不确定 | 开发期用 02-architecture-design.md §5.3 的 TOPIC_MAP仲裁后统一 |
| push-gateway gRPC 延迟D5 | T8 无法验证 | 保留 fetch POST 降级gRPC 就绪后切换 |