Files
Edu/services/msg/docs/02-architecture-design.md
SpecialX faaaf29f67 docs: ai 协作文档体系重构与多 ai 仲裁结果落地
1.AI 协作文档体系重构(objections/worklines/contracts+matrix.md)

2.coord 仲裁文档(final-decisions/cross-review/final-rulings/orchestration)

3.各服务 01/02 文档补全

4.共享包初始化(shared-ts/shared-go/hooks/ui-components/ui-tokens)

5.Proto 契约补全

6.004 架构影响地图更新

7.端口分配表

8.设计规格文档
2026-07-10 12:58:22 +08:00

74 KiB
Raw Permalink Blame History

模块架构设计文档 — msg

AI 标识ai05 负责模块msgP5 阶段:架构设计外包 · 阶段 2模块架构设计 日期2026-07-09 版本v1.0 关联文档:01-understanding.mdai-allocation.md004 架构影响地图pending-features.mdknown-issues.mdproject_rules.md


0. 设计哲学与文档定位

0.1 设计哲学

msg 服务承载沟通通知中台职责,设计哲学遵循五条原则:

  1. 多渠道抽象策略模式:站内信 / 邮件 / 短信 / 微信 / 推送通道抽象为 NotificationChannel 接口,新增渠道只需新增策略实现,无需改动业务代码
  2. 事件驱动 fan-out:消费 core-edu / iam / data-ana 事件触发通知所有事件处理幂等去重at-least-once 语义保证)
  3. CQRS 读写分离:写模型走 MySQL + Outbox读模型走 ES 全文检索 + Redis 位图(已读状态)
  4. Push Gateway 解耦msg 不直接管理 WebSocket 长连接,通过 gRPC 委托 push-gateway 推送实时通知msg 负责通知"业务编排"与"持久化"
  5. Outbox 强制业务事务事件NotificationSent / NotificationRead必须 Outbox 模式投递004 §12.2 强制条款)

0.2 长远演进目标

msg 不止服务 P5 通知 CRUD 阶段,需为以下未来场景预留架构弹性:

时间线 演进方向 当前架构预留点
P5 完成 通知 CRUD + 多渠道分发 + Kafka 消费 + Push Gateway 推送 NotificationChannel 抽象 + Kafka consumer + Redis 幂等去重
P6+ 富媒体消息(图片/视频/文件)、消息撤回与编辑 metadata jsonb 字段预留 + status 状态机预留 recalled 字段
未来 群组消息(班级群/学科群/年级群) group_id 字段预留 + fan-out 算法可扩展
未来 跨端消息已读状态同步(多端登录) Redis 位图 key 设计支持多端维度
未来 AI 触发的智能通知(学情预警自动推送) 接收 data-ana 的 MasteryUpdated 事件触发预警通知
未来 通知疲劳控制(用户接收频率限制) NotificationPreference.frequency_limit 字段预留
未来 消息合规审计(学校/教育主管部门审计要求) notification_audit_log 表预留
未来 推送通道扩展(小程序/企业微信/钉钉/IM NotificationChannel 抽象 + 渠道配置表
未来 消息搜索与归档(按时间归档冷数据) ES 索引 + alias 切换 + 冷热分离

0.3 文档结构说明

本设计文档遵循 ai-allocation.md §7 模板的 8 节结构,并补充第 9 节"演进路线"与第 10 节"风险与假设"。所有跨模块契约点均回标到 004 架构影响地图对应章节。


1. 模块内部分层图

1.1 整体分层架构

flowchart TB
    subgraph Client["客户端"]
        BFF["teacher-bff / student-bff / parent-bff"]
    end

    subgraph Gateway["API Gateway"]
        GW["api-gateway (JWT 校验/限流/熔断)"]
    end

    subgraph Msg["msg 服务 (3007)"]
        direction TB
        CTL[Controller 层<br/>HTTP REST + gRPC]
        GRD[Guard 层<br/>PermissionGuard + AuthMiddleware]
        VAL[Validation 层<br/>Zod Schema Parse]
        SVC[NotificationService<br/>通知编排]
        DISP[ChannelDispatcher<br/>多渠道分发]
        REPO[Repository 层<br/>Drizzle ORM]
        OUT[Outbox Publisher<br/>独立 worker]
        CONS[Kafka Consumer<br/>消费 5+ 类事件]
        IDEM[Idempotency Guard<br/>Redis SETNX 去重]
        PUSH[Push Gateway Client<br/>gRPC 推送通道]
        FILTER[GlobalErrorFilter<br/>统一错误兜底]
        OBS[Observability<br/>pino + prom-client + OTel]
    end

    subgraph Channels["通知渠道实现"]
        INAPP[InAppChannel<br/>站内信]
        EMAIL[EmailChannel<br/>邮件]
        SMS[SMSChannel<br/>短信]
        WECHAT[WeChatChannel<br/>微信]
    end

    subgraph Storage["数据存储"]
        MySQL[MySQL 8<br/>写模型主库]
        ES[Elasticsearch 8<br/>全文检索读模型]
        Redis[Redis 7<br/>幂等去重 + 已读位图]
    end

    subgraph Bus["消息总线"]
        Kafka[Kafka<br/>edu.notification.* + 消费 edu.teaching.* / edu.identity.* / edu.insight.*]
    end

    subgraph External["外部服务"]
        PushGW[push-gateway<br/>WebSocket 推送]
        SMTP[SMTP Server]
        SMSProv[SMS Provider]
    end

    BFF -->|HTTP REST| GW
    GW -->|HTTP REST| CTL
    Kafka -.->|consume| CONS
    CONS --> IDEM
    IDEM --> SVC
    CTL --> GRD
    GRD --> VAL
    VAL --> SVC
    SVC --> REPO
    SVC --> OUT
    SVC --> DISP
    DISP --> INAPP
    DISP --> EMAIL
    DISP --> SMS
    DISP --> WECHAT
    INAPP --> REPO
    REPO --> MySQL
    OUT -->|polling + publish| Kafka
    SVC --> PUSH
    PUSH -.->|gRPC| PushGW
    EMAIL -.->|SMTP| SMTP
    SMS -.->|HTTP API| SMSProv
    WECHAT -.->|HTTP API| WECHAT
    REPO --> ES
    CTL -.-> FILTER
    SVC -.-> OBS
    IDEM -.-> Redis

1.2 请求处理链路

阶段 组件 职责
入口 NestJS ExpressAdapter / gRPC server HTTP 3007 / gRPC 50056
鉴权 AuthMiddleware 信任 Gateway 注入的 x-user-id / x-user-roles
授权 PermissionGuard (APP_GUARD) 校验 3 个 MSG_* 权限点(未来扩展为 6+ 个)
校验 Zod Schema Parse Controller 层解析 body失败抛 ZodError
业务编排 NotificationService 通知发送 / 列表查询 / 标记已读 / 搜索
渠道分发 ChannelDispatcher 按用户偏好与通知类型选择渠道,并行投递
持久化 Repository (Drizzle ORM) 写 msg_notifications + msg_notification_preferences
事件 Outbox Publisher 投递 NotificationSent / NotificationRead 事件
异步消费 Kafka Consumer 消费 core-edu / iam / data-ana 事件触发通知
幂等 IdempotencyGuard Redis SETNX 基于 event_id 去重
推送 PushGatewayClient gRPC 调用 push-gateway 实时投递
错误 GlobalErrorFilter 捕获 ApplicationError + ZodError结构化响应
观测 Logger / Metrics / Tracer 全链路 traceparent 传递

1.3 同步链路 vs 异步链路

同步链路(用户主动操作):

  • 用户调 POST /notifications/send → 同步写 DB + Outbox + 调度 channel dispatcher → 返回响应
  • channel dispatcher 内部并行调多个渠道in_app / push / email / sms慢渠道走异步队列

异步链路(事件驱动触发):

  • core-edu 发布 edu.teaching.exam.published → msg Kafka Consumer 消费 → 创建通知 → 调度 channel dispatcher → 发送给所有学生
  • iam 发布 edu.identity.user.created → msg 消费 → 发欢迎通知

2. 领域模型

2.1 聚合根与实体

classDiagram
    class Notification {
        +id: string
        +userId: string
        +type: NotificationType
        +title: string
        +content: string
        +channel: NotificationChannel
        +status: NotificationStatus
        +metadata: object
        +relatedEntityType: string|null
        +relatedEntityId: string|null
        +groupId: string|null
        +senderId: string|null
        +createdAt: Date
        +readAt: Date|null
        +markAsRead(userId: string) void
        +recall() void
    }
    class NotificationPreference {
        +id: string
        +userId: string
        +type: NotificationType
        +channels: NotificationChannel[]
        +frequencyLimit: number
        +quietHours: QuietHours|null
        +enabled: boolean
        +createdAt: Date
        +updatedAt: Date
        +allowsChannel(channel) boolean
    }
    class NotificationTemplate {
        +id: string
        +code: string
        +type: NotificationType
        +titleTemplate: string
        +contentTemplate: string
        +defaultChannels: NotificationChannel[]
        +variables: string[]
        +locale: string
        +status: TemplateStatus
        +createdAt: Date
        +updatedAt: Date
        +render(variables: object) RenderedNotification
    }
    class NotificationChannel {
        <<interface>>
        +send(notification: Notification) Promise~SendResult~
        +name: string
    }

    class InAppChannel {
        +send() Promise~SendResult~
    }
    class EmailChannel {
        +send() Promise~SendResult~
    }
    class SMSChannel {
        +send() Promise~SendResult~
    }
    class WeChatChannel {
        +send() Promise~SendResult~
    }
    class PushChannel {
        +send() Promise~SendResult~
    }

    Notification --> NotificationChannel : dispatched via
    Notification "1" --> "0..1" NotificationTemplate : rendered from
    Notification "many" --> "1" NotificationPreference : respects
    NotificationChannel <|.. InAppChannel
    NotificationChannel <|.. EmailChannel
    NotificationChannel <|.. SMSChannel
    NotificationChannel <|.. WeChatChannel
    NotificationChannel <|.. PushChannel

2.2 值对象

值对象 字段 用途
NotificationType system / exam / homework / grade / attendance / announcement / mastery_alert / welcome 通知类型枚举(对齐 004 §7.3 事件类型)
NotificationChannel in_app / email / sms / wechat / push 渠道枚举
NotificationStatus pending / sent / delivered / read / failed / recalled 状态机
TemplateStatus draft / active / archived 模板状态机
SendResult { success, channel, messageId?, error? } 发送结果值对象
QuietHours { start: "22:00", end: "07:00", timezone: "Asia/Shanghai" } 免打扰时段(未来扩展)

2.3 状态机设计

Notification 状态机

stateDiagram-v2
    [*] --> pending: create
    pending --> sent: channel dispatcher 投递成功
    pending --> failed: 投递失败(重试耗尽)
    sent --> delivered: 渠道回执确认送达
    delivered --> read: 用户标记已读
    sent --> read: 用户标记已读(跳过 delivered
    read --> recalled: 撤回 (P6+ 预留)
    sent --> recalled: 撤回 (P6+ 预留)
    failed --> pending: 重试

NotificationTemplate 状态机

stateDiagram-v2
    [*] --> draft: create
    draft --> active: publish
    active --> archived: archive
    archived --> draft: edit (复活)

2.4 多渠道分发策略

flowchart LR
    NOTIF[Notification] --> DISP[ChannelDispatcher]
    DISP --> PREF[用户 NotificationPreference]
    PREF --> INAPP[in_app 总是发送]
    PREF --> EMAIL{email 启用?}
    PREF --> SMS{sms 启用?}
    PREF --> WECHAT{wechat 启用?}
    PREF --> PUSH{push 启用?}
    INAPP --> DB[(MySQL)]
    EMAIL --> SMTP[SMTP Server]
    SMS --> SMSAPI[SMS Provider]
    WECHAT --> WXAPI[WeChat API]
    PUSH --> PUSHGW[Push Gateway gRPC]

分发规则

  1. in_app 渠道总是发送(保证站内信可见)
  2. 其他渠道按 NotificationPreference.channels 配置启用/禁用
  3. push 渠道仅对在线用户发送push-gateway 维护在线状态)
  4. quiet_hours 时段内除紧急通知外,其他渠道延迟到时段结束后发送

3. 数据模型

3.1 MySQL Schema

3.1.1 msg_notifications 表(扩展字段)

字段 类型 约束 说明
id varchar(32) PK cuid2
user_id varchar(32) NOT NULL, INDEX 接收者
type varchar(32) NOT NULL, INDEX NotificationType 枚举
title varchar(255) NOT NULL
content text NOT NULL
channel varchar(32) NOT NULL NotificationChannel 枚举
status varchar(32) NOT NULL DEFAULT 'pending' 新增 状态机
metadata json NULL 新增 扩展attachments/links/actions
related_entity_type varchar(64) NULL, INDEX 新增 关联实体类型exam/homework/grade
related_entity_id varchar(32) NULL 新增 关联实体 ID
group_id varchar(32) NULL, INDEX 新增 群组 ID广播通知标识
sender_id varchar(32) NULL 新增 发送者(系统通知为 null
template_id varchar(32) NULL 新增 模板 ID
event_id varchar(64) NULL, UNIQUE 新增 触发事件 ID幂等去重
read_at timestamp NULL 已读时间
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 新增

索引

  • PRIMARY KEY (id)
  • INDEX idx_notif_user_created (user_id, created_at DESC)
  • INDEX idx_notif_user_type (user_id, type)
  • INDEX idx_notif_user_unread (user_id, read_at IS NULL)
  • INDEX idx_notif_group (group_id)
  • INDEX idx_notif_related (related_entity_type, related_entity_id)
  • UNIQUE INDEX idx_notif_event_id (event_id)

3.1.2 msg_notification_preferences 表(扩展字段)

字段 类型 约束 说明
id varchar(32) PK cuid2
user_id varchar(32) NOT NULL, INDEX
type varchar(32) NOT NULL NotificationType
channels json NOT NULL 启用渠道数组 ["in_app","email","push"]
frequency_limit int NULL 新增 频率限制(每小时最多 N 条)
quiet_hours_start varchar(8) NULL 新增 免打扰开始(如 "22:00"
quiet_hours_end varchar(8) NULL 新增 免打扰结束
quiet_hours_timezone varchar(64) NULL DEFAULT 'Asia/Shanghai' 新增 时区
enabled boolean NOT NULL DEFAULT true
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP 补齐
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE 补齐

索引

  • PRIMARY KEY (id)
  • UNIQUE INDEX idx_pref_user_type (user_id, type)

3.1.3 msg_notification_templates 表(新增

字段 类型 约束 说明
id varchar(32) PK cuid2
code varchar(64) NOT NULL, UNIQUE 模板代码(如 exam.published.student
type varchar(32) NOT NULL NotificationType
title_template varchar(255) NOT NULL {{variable}} 占位符
content_template text NOT NULL {{variable}} 占位符
default_channels json NOT NULL 默认渠道数组
variables json NOT NULL 必填变量列表 ["examTitle","className"]
locale varchar(16) NOT NULL DEFAULT 'zh-CN' 国际化(多语言预留)
status varchar(32) NOT NULL DEFAULT 'draft' TemplateStatus
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
updated_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE

索引

  • PRIMARY KEY (id)
  • UNIQUE INDEX idx_tpl_code_locale (code, locale)

3.1.4 msg_outbox_eventsOutbox · 新增)

字段 类型 约束 说明
event_id varchar(64) PK UUID v4
aggregate_type varchar(64) NOT NULL 'Notification' / 'NotificationPreference'
aggregate_id varchar(32) NOT NULL
event_type varchar(64) NOT NULL edu.notification.sent
topic varchar(128) NOT NULL
payload json NOT NULL
status varchar(16) NOT NULL DEFAULT 'PENDING'
retry_count int NOT NULL DEFAULT 0
created_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
published_at timestamp NULL
next_retry_at timestamp NULL

索引

  • PRIMARY KEY (event_id)
  • INDEX idx_outbox_status_retry (status, next_retry_at)

3.1.5 processed_events 表(消费幂等 · 新增)

若不引入 Redis使用 DB 唯一索引去重(推荐 Redis性能更优

字段 类型 约束 说明
event_id varchar(64) PK 消费的事件 ID
topic varchar(128) NOT NULL
processed_at timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP

索引

  • PRIMARY KEY (event_id)
  • INDEX idx_processed_topic (topic, processed_at)

3.2 Elasticsearch 索引设计

3.2.1 notifications 索引 mapping

{
  "mappings": {
    "properties": {
      "id": { "type": "keyword" },
      "user_id": { "type": "keyword" },
      "type": { "type": "keyword" },
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "channel": { "type": "keyword" },
      "status": { "type": "keyword" },
      "group_id": { "type": "keyword" },
      "related_entity_type": { "type": "keyword" },
      "related_entity_id": { "type": "keyword" },
      "sender_id": { "type": "keyword" },
      "is_read": { "type": "boolean" },
      "created_at": { "type": "date" },
      "read_at": { "type": "date" }
    }
  },
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1
  }
}

3.2.2 索引管理

  • 索引创建:服务启动 ensureIndex('notifications', mapping) 幂等
  • 数据同步:业务写入 MySQL 时通过 Outbox 事件触发 ES 索引更新
  • 查询降级ES 不可用时降级到 MySQL LIKE 查询

3.3 Redis 数据结构

3.3.1 幂等去重

KEY: msg:processed:{event_id}
VALUE: 1
TTL: 7 天

使用 SETNX 命令原子性去重,返回 0 表示已处理。

3.3.2 已读状态位图

KEY: msg:read:{user_id}:{yyyyMM}
VALUE: Bitmap

按月分桶存储已读状态,第 N 位表示该月第 N 条通知的已读状态。优势:

  • 1 万条通知仅占 ~1.25KB 内存
  • 支持按月快速统计已读数量
  • 跨端同步(多端共享同一 bitmap

3.3.3 在线用户状态缓存

KEY: msg:online:{user_id}
VALUE: { device: "web"/"app", last_active: timestamp }
TTL: 5 分钟push-gateway 心跳更新)

由 push-gateway 维护msg 查询以决定是否走 push 渠道。

3.3.4 通知频率限流

KEY: msg:ratelimit:{user_id}:{type}:{yyyyMMddHH}
VALUE: count
TTL: 1 小时

按 NotificationPreference.frequency_limit 限制。

3.4 读写分离策略CQRS

操作 路径 说明
写(发送/标记已读) Service → Repository → MySQL + Outbox 单一写模型
读 - 列表查询 Service → Repository → MySQL默认 包含分页
读 - 全文检索 Service → ES Client → ES 检索专用P5 启用)
读 - 已读状态 Service → Redis Bitmap 跨端共享
读 - 未读计数 Service → Redis Bitmap 或 MySQL COUNT 高频查询走 Redis

4. API 设计

4.1 REST API当前 P5 实现 + 待补齐)

Method Path 权限 请求体 响应 说明
POST /notifications/send MSG_NOTIFICATION_SEND {userId, type, title, content, channel, metadata?, relatedEntityType?, relatedEntityId?, groupId?} {id, status} 单条发送
POST /notifications/batch MSG_NOTIFICATION_SEND {items[]: SendItem, groupId?} {ids[], failed[]} 批量发送(改批量 INSERT
GET /notifications/user/:userId MSG_NOTIFICATION_READ ?onlyUnread=&type=&page=1&pageSize=20 {items[], total} 用户通知列表
GET /notifications/user/:userId/page MSG_NOTIFICATION_READ ?cursor=&pageSize=20 {items[], nextCursor} 游标分页(新增
GET /notifications/user/:userId/unread-count MSG_NOTIFICATION_READ {count} 未读数Redis
PUT /notifications/:id/read MSG_NOTIFICATION_READ {success: true} 标记已读
PUT /notifications/batch/read MSG_NOTIFICATION_READ {ids[]} {success: true} 批量标记已读(新增
PUT /notifications/read-all MSG_NOTIFICATION_READ {userId, before?: timestamp} {updated: N} 全部已读(新增
GET /notifications/search MSG_NOTIFICATION_READ ?q=&userId=&type=&page=&pageSize= {items[], total} ES 全文检索
DELETE /notifications/:id MSG_NOTIFICATION_MANAGE {success: true} 删除(管理员)
POST /notifications/recall MSG_NOTIFICATION_MANAGE {groupId, reason?} {recalled: N} 撤回广播(新增
GET /preferences/user/:userId MSG_NOTIFICATION_READ {preferences[]} 用户偏好
PUT /preferences/user/:userId MSG_NOTIFICATION_MANAGE {preferences[]} {success: true} 更新偏好
GET /templates MSG_NOTIFICATION_MANAGE ?type=&status= {items[]} 模板列表
POST /templates MSG_NOTIFICATION_MANAGE {code, type, titleTemplate, contentTemplate, defaultChannels, variables} {id} 创建模板
PUT /templates/:id MSG_NOTIFICATION_MANAGE {...} {id} 更新模板
DELETE /templates/:id MSG_NOTIFICATION_MANAGE {success: true} 删除模板
POST /internal/reindex MSG_NOTIFICATION_MANAGE ?from=&to= {taskId} ES 重建索引(预留

4.2 gRPC API待实现对齐 004 §4.2 P5 启用)

4.2.1 NotificationService

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} 新增
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} 补分页
RecallNotification RecallNotificationRequest{group_id, reason?} RecallNotificationResponse{recalled_count} 新增

4.2.2 NotificationPreferenceService新增

RPC 请求 响应 说明
GetPreferences GetPreferencesRequest{user_id} GetPreferencesResponse{preferences[]}
UpdatePreferences UpdatePreferencesRequest{user_id, preferences[]} Empty

4.2.3 NotificationTemplateService新增

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} 新增:渲染模板

4.3 错误响应结构

{
  "success": false,
  "error": {
    "code": "MSG_VALIDATION_ERROR",
    "message": "User ID is required",
    "details": { "field": "userId" }
  },
  "requestId": "req_xxx",
  "timestamp": 1736000000000
}

4.4 分页统一规范

REST 使用 offset 模式page/pageSizegRPC 同时支持 offset 与 cursor 模式。


5. 事件设计

5.1 消费事件清单(核心 · 5+ 类事件

Topic 来源服务 处理逻辑 触发通知 幂等键
edu.identity.user.created iam 发送欢迎通知 welcome 通知 event_id
edu.identity.user.updated iam 用户信息变更,可选清理缓存 event_id
edu.identity.user.deleted iam 用户删除,归档该用户通知 event_id
edu.identity.user.role_changed iam 角色变更,发送权限变更通知 system 通知 event_id
edu.identity.role.created iam 角色新增(管理通知) system 通知 event_id
edu.identity.role.updated iam 角色权限变更,通知受影响用户 system 通知 event_id
edu.teaching.exam.published core-edu 推送考试通知给班级所有学生 exam 通知fan-out event_id
edu.teaching.assignment.submitted core-edu 通知教师有学生提交作业 homework 通知 event_id
edu.teaching.assignment.graded core-edu 通知学生作业已批改 grade 通知 event_id
edu.teaching.grade.recorded core-edu 通知学生成绩已录入 grade 通知 event_id
edu.teaching.attendance.recorded core-edu 出勤异常通知家长 attendance 通知 event_id
edu.insight.mastery.updated data-ana 学情掌握度下降,触发预警 mastery_alert 通知 event_id

5.2 发布事件清单

Event Type Topic 触发时机 消费者
edu.notification.sent edu.notification.sent 通知发送成功 push-gateway、data-ana统计
edu.notification.read edu.notification.read 通知被标记已读 data-ana统计
edu.notification.recalled edu.notification.recalled 通知被撤回 push-gateway删除已推送消息
edu.notification.failed edu.notification.failed 通知投递失败 data-ana监控告警

5.3 TOPIC_MAP 路由表msg 内部)

// 发布事件路由
const PRODUCER_TOPIC_MAP: Record<string, string> = {
  NotificationSent: "edu.notification.sent",
  NotificationRead: "edu.notification.read",
  NotificationRecalled: "edu.notification.recalled",
  NotificationFailed: "edu.notification.failed",
};

// 消费事件订阅
const CONSUMER_TOPICS = [
  "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",
  "edu.teaching.exam.published",
  "edu.teaching.assignment.submitted",
  "edu.teaching.assignment.graded",
  "edu.teaching.grade.recorded",
  "edu.teaching.attendance.recorded",
  "edu.insight.mastery.updated",
];

5.4 Outbox Publisher 模式

sequenceDiagram
    participant TRIG as Kafka Trigger (core-edu/iam/data-ana)
    participant CONS as Msg Kafka Consumer
    participant IDEM as Redis SETNX (幂等)
    participant SVC as NotificationService
    participant DB as MySQL (notifications + outbox)
    participant DISP as ChannelDispatcher
    participant PUB as Outbox Publisher
    participant OUT as Kafka (edu.notification.*)
    participant PUSH as Push Gateway

    TRIG-->>CONS: consume edu.teaching.exam.published
    CONS->>IDEM: SETNX msg:processed:{event_id}
    alt 已处理(返回 0
        IDEM-->>CONS: skip
    else 未处理(返回 1
        CONS->>SVC: createNotificationFromEvent(event)
        SVC->>DB: BEGIN TX
        SVC->>DB: INSERT INTO msg_notifications
        SVC->>DB: INSERT INTO msg_outbox_events (NotificationSent)
        SVC->>DB: COMMIT
        SVC->>DISP: dispatch(notification, userPreference)
        par 多渠道并行
            DISP->>DB: in_app 已持久化
            DISP->>PUSH: gRPC push (在线用户)
            DISP->>DISP: email/sms/wechat 异步队列
        end
    end

    loop 每 1s 轮询
        PUB->>DB: SELECT PENDING events
        PUB->>OUT: produce(edu.notification.sent)
        PUB->>DB: UPDATE status=SENT
    end

5.5 幂等去重设计

双层去重

  1. 消费层幂等Redis SETNX

    • 基于 event_id 字段去重
    • TTL 7 天(覆盖重试窗口)
    • 防止 Kafka at-least-once 投递的重复消费
  2. 业务层幂等DB UNIQUE INDEX

    • msg_notifications.event_id UNIQUE INDEX
    • INSERT 冲突时跳过INSERT IGNORE / ON DUPLICATE KEY UPDATE
    • 防止 Redis 故障时重复创建

Producer 幂等

  • Kafka producer idempotent=true + transactionalId=msg-producer
  • transactionalId 保证跨事务 Exactly-Once 语义

6. 横切关注点对齐清单

6.1 权限装饰器清单

Controller 方法 权限点 说明
NotificationsController.send MSG_NOTIFICATION_SEND 单条发送
NotificationsController.batch MSG_NOTIFICATION_SEND 批量发送
NotificationsController.listByUser MSG_NOTIFICATION_READ 列表
NotificationsController.listByUserPage MSG_NOTIFICATION_READ 游标分页
NotificationsController.getUnreadCount MSG_NOTIFICATION_READ 未读数
NotificationsController.markAsRead MSG_NOTIFICATION_READ 标记已读
NotificationsController.batchMarkAsRead MSG_NOTIFICATION_READ 批量已读
NotificationsController.markAllAsRead MSG_NOTIFICATION_READ 全部已读
NotificationsController.search MSG_NOTIFICATION_READ 检索
NotificationsController.recall MSG_NOTIFICATION_MANAGE 撤回(管理员)
NotificationsController.remove MSG_NOTIFICATION_MANAGE 删除
PreferencesController.get MSG_NOTIFICATION_READ 偏好查询
PreferencesController.update MSG_NOTIFICATION_MANAGE 偏好更新
TemplatesController.* MSG_NOTIFICATION_MANAGE 模板 CRUD
NotificationsController.reindex MSG_NOTIFICATION_MANAGE 重建索引
gRPC SendNotification MSG_NOTIFICATION_SEND BFF/内部调用
gRPC BatchMarkAsRead MSG_NOTIFICATION_READ BFF 调用

权限点扩展:从当前 3 个扩展为 3 个不变SEND/READ/MANAGE但覆盖更多端点。

6.2 错误码清单

错误码 HTTP 触发条件
MSG_VALIDATION_ERROR 400 Zod 校验失败 / 通知类型非法
MSG_NOT_FOUND 404 通知/偏好/模板不存在
MSG_PERMISSION_DENIED 403 权限不足 / 跨用户访问
MSG_CONFLICT 409 重复发送event_id 冲突)/ 状态机非法转换
MSG_BUSINESS_ERROR 422 业务规则违反(如撤回已撤回的通知)
MSG_DATABASE_ERROR 500 Drizzle 操作异常
MSG_INTERNAL_ERROR 500 未知异常
MSG_ES_UNAVAILABLE 503 ES 不可用且无降级路径
MSG_REDIS_UNAVAILABLE 503 Redis 不可用且无降级路径
MSG_PUSH_GATEWAY_UNAVAILABLE 503 Push Gateway 不可用
MSG_TEMPLATE_NOT_FOUND 404 模板不存在
MSG_TEMPLATE_RENDER_ERROR 422 模板渲染失败(变量缺失)
MSG_RATE_LIMIT_EXCEEDED 429 通知频率超限

6.3 Logger 配置

  • pino
  • 日志级别DEV_MODE debug,生产 info
  • 结构化字段requestId / userId / notificationId / eventType / channel / durationMs / eventId
  • 敏感字段脱敏:通知 content 在生产环境日志中截断到 100 字符

6.4 Metrics 指标清单

指标名 类型 标签 说明
msg_http_requests_total Counter method/route/status_code HTTP 请求总数
msg_http_request_duration_seconds Histogram method/route HTTP 请求延迟
msg_grpc_requests_total Counter rpc_method/status gRPC 调用
msg_notification_sent_total Counter type/channel/status 通知发送总数
msg_notification_send_duration_seconds Histogram type/channel 通知发送延迟
msg_notification_read_total Counter type 通知已读总数
msg_notification_failed_total Counter type/channel/error_code 通知失败总数
msg_channel_dispatch_duration_seconds Histogram channel 渠道分发延迟
msg_kafka_consumer_lag Gauge topic Kafka 消费滞后
msg_kafka_consumer_processed_total Counter topic/status Kafka 消费总数
msg_idempotent_duplicate_total Counter topic 幂等去重命中次数
msg_outbox_pending_count Gauge Outbox 待投递数
msg_push_gateway_call_total Counter status Push Gateway 调用
msg_push_gateway_call_duration_seconds Histogram Push Gateway 延迟
msg_unread_count Gauge user_id 用户未读数(采样)

6.5 Tracer 配置

  • @opentelemetry/sdk-node + auto-instrumentations
  • 采样率DEV_MODE 100%,生产 10%
  • 服务名msg
  • 跨服务传播:消费 Kafka 时从 header 提取 traceparent发送 Push Gateway gRPC 时注入 traceparent

6.6 健康检查

/healthzliveness

{ "status": "ok", "service": "msg", "timestamp": 1736000000000 }

/readyzreadiness · 多依赖检查)

{
  "status": "ok" | "degraded" | "down",
  "checks": {
    "database": { "status": "ok", "latency_ms": 5 },
    "elasticsearch": { "status": "ok", "latency_ms": 8 },
    "redis": { "status": "ok", "latency_ms": 2 },
    "kafka_producer": { "status": "ok" },
    "kafka_consumer": { "status": "ok", "lag": 0 },
    "push_gateway": { "status": "ok", "latency_ms": 15 }
  }
}

判定规则

  • DB 不可用 → status: down
  • Redis 不可用 → status: degraded(降级到 DB 唯一索引去重)
  • ES 不可用 → status: degraded(降级到 MySQL LIKE 查询)
  • Kafka 不可用 → status: degraded(无法消费事件触发通知)
  • Push Gateway 不可用 → status: okpush 仅是渠道之一,不影响整体可用性)

6.7 优雅关闭顺序

sequenceDiagram
    participant SIG as SIGTERM
    participant APP as NestJS App
    participant HTTP as HTTP Server
    participant CONS as Kafka Consumer
    participant PUB as Outbox Publisher
    participant DISP as ChannelDispatcher
    participant REDIS as Redis Client
    participant ES as ES Client
    participant DB as Drizzle Pool
    participant TR as Tracer

    SIG->>APP: onApplicationShutdown
    APP->>HTTP: server.close() (拒绝新请求)
    APP->>CONS: consumer.stop() (停止消费)
    APP->>PUB: publisher.stop() (完成当前批次)
    APP->>DISP: dispatcher.stop() (完成在途渠道分发)
    APP->>REDIS: redis.quit()
    APP->>ES: esClient.close()
    APP->>DB: pool.end()
    APP->>TR: tracer.shutdown()

7. 与其他模块的交互点

7.1 跨模块交互矩阵

方向 对方服务 协议 接口/事件 用途 状态
被调用 teacher-bff / student-bff / parent-bff HTTP REST GET /notifications/user/:userId 等 BFF 聚合查询通知 已实现
被调用 api-gateway HTTP REST POST /notifications/send 等 REST 转发 已实现
调用 push-gateway gRPC待实现,当前 fetch POST /internal/push 降级 PushService.Push 实时推送通知 P5 待实现 gRPC
消费 iam Kafka edu.identity.user.{created,updated,deleted,role_changed} 触发用户通知 P5 待实现 consumer
消费 iam Kafka edu.identity.role.{created,updated} 触发角色变更通知 P5 待实现
消费 core-edu Kafka edu.teaching.exam.published 考试通知 fan-out P5 待实现
消费 core-edu Kafka edu.teaching.assignment.submitted 通知教师 P5 待实现
消费 core-edu Kafka edu.teaching.assignment.graded 通知学生作业已批改 P5 待实现
消费 core-edu Kafka edu.teaching.grade.recorded 通知学生成绩 P5 待实现
消费 core-edu Kafka edu.teaching.attendance.recorded 通知家长出勤 P5 待实现
消费 data-ana Kafka edu.insight.mastery.updated 学情预警通知 P5 待实现
发布 Kafka edu.notification.*4 类事件,见 §5.2 通知 push-gateway / data-ana P5 待实现

7.2 跨模块契约一致性提请coord 仲裁)

# 提请内容 阻塞性 备注
P1 msg.proto 需补 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / RecallNotification RPC 🔴 阻塞 gRPC 完整契约
P2 msg.proto 需补 NotificationPreferenceService + NotificationTemplateService 🔴 阻塞偏好与模板功能
P3 msg.proto 需补 NotificationPreference / NotificationTemplate / RenderedNotification message 🔴 阻塞 gRPC
P4 msg.proto Notification message 需补 metadata / related_entity_type / related_entity_id / group_id / sender_id / template_id / event_id 字段 🔴 阻塞
P5 msg.proto ListNotificationsRequest / SearchNotificationsRequest 需补分页字段 🟡
P6 events.proto 需追加 UserEvent / RoleEvent messageiam 发布事件契约) 🔴 阻塞 msg 消费 iam 事件 coord 已在审查报告中识别
P7 events.proto 需追加 NotificationEvent messagemsg 发布事件契约) 🔴 阻塞 push-gateway 消费 msg 事件
P8 events.proto 需追加 MasteryEvent messagedata-ana 发布事件契约) 🟡 阻塞 msg 消费 data-ana 事件
P9 events.proto ExamEvent / HomeworkEvent / GradeEvent 需补 class_id / student_ids[] 字段msg fan-out 通知需要 🔴 阻塞广播
P10 Push Gateway 与 Msg 调用方向澄清004 §4.1 写"PushGW→Msg",实际是 Msg→PushGWmsg 调 push-gateway 推送) 🟡 文档表述歧义 建议 coord 修正 004 §4.1 表述
P11 core-edu 事件 topic 命名统一004 §7.2 用 edu.teaching.* 新约定events.proto 注释用 edu.exam.events 旧约定 🟡 文档不一致 coord 已仲裁采用新约定
P12 DB 连接模式统一const db vs getDb(),建议统一为 getDb()(对齐 classes 黄金模板) 🟡 与 content P6 一致
P13 ID 策略统一cuid2 vs randomUUID建议统一为 cuid2 🟡 与 content P7 一致

7.3 跨服务调用方向澄清Push Gateway

flowchart LR
    subgraph MsgSvc[msg 服务]
        SVC[NotificationService]
        CLIENT[PushGatewayClient]
    end
    subgraph PushGW[push-gateway 服务]
        PUSH[PushService]
        WS[WebSocket Connections]
    end
    subgraph User[用户端]
        BROWSER[浏览器/App]
    end

    SVC -->|gRPC Push| CLIENT
    CLIENT -.->|gRPC| PUSH
    PUSH --> WS
    WS -.->|WebSocket| BROWSER

澄清

  • msg 调用 push-gateway 的 gRPC PushService.Push 方法投递实时通知
  • push-gateway 维护 WebSocket 连接池,将通知推送到对应用户的浏览器/App
  • push-gateway 水平扩展时通过 Redis Pub/Sub 跨实例广播
  • 004 §4.1 表述"PushGW→Msg 推送通道建立"应理解为"push-gateway 暴露 PushService RPC 供 msg 调用",建议 coord 修正表述

8. 演进路线

8.1 P5 阶段(当前)

目标:通知 CRUD + 多渠道分发 + Kafka 消费 + Push Gateway 推送 + ES 检索

交付物

  • msg_notifications / msg_notification_preferences 表字段扩展
  • msg_notification_templates 表(新增)
  • msg_outbox_events 表 + Outbox Publisher worker
  • processed_events 表(或 Redis SETNX 幂等)
  • Kafka consumer消费 12 类事件)
  • ChannelDispatcher 多渠道抽象in_app/email/sms/wechat/push
  • PushGatewayClient gRPC 调用
  • ES 索引 mapping + ensureIndex + 全文检索 API
  • ZodError 在 GlobalErrorFilter 特殊处理
  • 测试覆盖率 ≥ 80%

8.2 P6+ 长远演进(架构预留点)

演进方向 当前架构预留 触发条件
富媒体消息 metadata jsonb 字段attachments/links/actions 教师发送图文通知需求
消息撤回与编辑 Notification.status=recalled 状态机 + RecallNotification RPC 教师误发广播需求
群组消息 group_id 字段 + fan-out 算法可扩展(班级群/学科群/年级群) 群组协作需求
跨端已读同步 Redis Bitmap 跨端共享 + last_read_at 时间戳 多端登录场景
AI 智能通知 接收 data-ana MasteryUpdated 事件触发预警通知 AI 学情分析上线后
通知疲劳控制 NotificationPreference.frequency_limit + Redis 限流计数 高频通知场景
消息合规审计 notification_audit_log 表预留(操作者/时间/操作类型) 教育合规要求
推送通道扩展 NotificationChannel 抽象 + 渠道配置表(小程序/企业微信/钉钉) 多端推送需求
消息搜索与归档 ES alias 切换 + 冷热分离(按月归档) 历史消息检索需求
多语言通知 NotificationTemplate.locale 字段 + RenderTemplate RPC locale 参数 国际化场景
通知优先级 metadata.priority 字段 + 优先级排序 紧急通知场景
通知模板版本管理 NotificationTemplate.status 状态机 + version 字段 模板迭代需求
通知 A/B 测试 metadata.variant 字段预留 + 统计打开率 通知效果优化

8.3 与黄金模板对齐

msg 在 P5 完成后,应将以下模式回写到 classes 黄金模板known-issues §2.2 提及"长连接模式回写"

  • Kafka consumer + Redis SETNX 幂等去重模式
  • ChannelDispatcher 多渠道分发模式
  • Outbox 表 schema + Publisher worker 模式(与 content 共用模式)
  • 多依赖 /readyz 检查模式DB/ES/Redis/Kafka/PushGateway

9. 风险与假设

9.1 技术风险

# 风险 影响 缓解措施
R1 Kafka 消费滞后导致通知延迟 用户收不到及时通知 监控 consumer lag + 报警阈值 + 水平扩容 consumer
R2 Redis 故障导致幂等去重失效 重复消费创建重复通知 DB UNIQUE INDEX 双层保护event_id 冲突时跳过)
R3 Push Gateway 不可用导致实时推送失败 用户收不到实时通知 降级到 in_app 站内信 + 重试机制
R4 高并发广播(全校 1 万学生)性能瓶颈 DB 写入慢 + ES 索引慢 BatchSendNotification 批量 INSERT + ES bulk indexing + fan-out 异步队列
R5 多渠道发送部分失败 通知状态不一致 ChannelDispatcher 返回 SendResult部分失败标记为 partial_sent
R6 Kafka 消息丢失at-least-once 不可靠时) 通知遗漏 Outbox 保证业务事务 + Kafka acks=all + 监控 consumer lag
R7 通知频率过高打扰用户 用户疲劳 / 投诉 NotificationPreference.frequency_limit + Redis 限流 + 默认 quiet_hours
R8 通知内容敏感信息泄露 合规风险 Logger 脱敏 + content 字段不写入 ES 索引明文(可选加密)
R9 ES 索引重建期间检索不可用 检索中断 alias 切换蓝绿模式
R10 Kafka 事件 schema 演化events.proto 增删字段) 消费失败 proto backward compatible 设计 + schema registry未来

9.2 假设

# 假设 依赖 Fallback
A1 iam 会发布 edu.identity.user.{created,updated,deleted,role_changed} 事件 ai06 设计确认 msg 不消费此类事件,仅靠管理端手动创建通知
A2 iam 会发布 edu.identity.role.{created,updated} 事件 ai06 设计确认 msg 不消费此类事件
A3 core-edu 会发布 edu.teaching.* 5 类事件 ai08 设计确认 msg 不消费此类事件
A4 data-ana 会发布 edu.insight.mastery.updated 事件 ai11 设计确认 msg 不消费预警通知
A5 push-gateway 提供 gRPC PushService.Push 方法 ai02 设计确认 降级到 fetch POST /internal/push当前实现
A6 events.proto 会补充 UserEvent / RoleEvent / NotificationEvent / MasteryEvent message coord 修改 proto msg 消费时使用通用 JSON payload 解析
A7 Redis 集群可用 infra 部署 降级到 DB 唯一索引去重(性能下降)
A8 ES 8.x 可用 infra 部署 ES_URL 未配置时 esClient=null降级到 MySQL LIKE 查询
A9 Kafka 集群可用 infra 部署 Outbox Publisher 失败时重试,最终一致

9.3 待 coord 仲裁项

  1. msg Outbox 启用时机P5按 004 §12.2 强制条款msg 发事件必须 Outbox建议 P5 启用)
  2. msg Redis 引入时机P5pending-features P5 要求 event_id 去重,建议引入 Redis
  3. msg.proto proto 包名:保持 next_edu_cloud.msg.v1(已仲裁)
  4. Push Gateway 调用方向表述004 §4.1 表述歧义,建议修正为"msg → push-gateway (gRPC)"
  5. events.proto 补充 message 优先级UserEvent/RoleEvent/NotificationEvent/MasteryEvent 应在 P5 之前补齐
  6. core-edu 事件 topic 命名:已仲裁采用 edu.teaching.* 新约定events.proto 注释需同步更新
  7. DB 连接模式统一const db vs getDb(),建议统一为 getDb()
  8. ID 策略统一cuid2 vs randomUUID建议统一为 cuid2
  9. NotificationEvent 是否需要回执msg 发 NotificationSent 事件后push-gateway 是否需要回执确认?

10. 实施计划(建议)

10.1 P5 阶段任务拆分

# 任务 优先级 预估文件改动
T1 msg_notifications 表新增 status/metadata/related_entity_*/group_id/sender_id/template_id/event_id 字段 P0 schema 迁移
T2 msg_notification_preferences 表补齐时间戳 + 新增 frequency_limit/quiet_hours 字段 P0 schema 迁移
T3 新建 msg_notification_templates 表 P0 schema 迁移
T4 新建 msg_outbox_events 表 + Outbox Publisher worker P0 新建 shared/outbox/
T5 新建 shared/kafka/ 目录producer + consumer P0 新建 shared/kafka/
T6 引入 redis 客户端ioredis+ 实现 IdempotencyGuardSETNX P0 新建 shared/redis/
T7 实现 ChannelDispatcher 多渠道抽象in_app/email/sms/wechat/push P0 新建 channels/ 目录
T8 实现 PushGatewayClient gRPC 调用(替代 fetch POST 降级) P0 新建 shared/push/
T9 实现 12 类 Kafka 事件 consumeriam/core-edu/data-ana P0 新建 shared/kafka/consumers/
T10 重构 notifications.service.ts移除同步 fetch push-gateway改为 gRPC + 异步分发 P0 重构 service
T11 实现 batchMarkAsRead / markAllAsRead / recall / getUnreadCount 端点 P0 controller + service
T12 实现 NotificationPreference CRUD P1 新建 preferences/ 目录
T13 实现 NotificationTemplate CRUD + render P1 新建 templates/ 目录
T14 ES 索引 mapping + ensureIndex + 数据同步 consumer P1 修改 config/elasticsearch.ts
T15 重构 createBatch 为批量 INSERT替代 for 循环串行) P1 service 重构
T16 NotificationsModule 补 exports: [NotificationsService] P1 module 修改
T17 /readyz 多依赖检查DB/ES/Redis/Kafka/PushGateway P1 修改 health.controller.ts
T18 ZodError 在 GlobalErrorFilter 特殊处理(返回 400 P1 修改 global-error.filter.ts
T19 统一关闭逻辑到 LifecycleService移除 main.ts 重复 close P1 修改 main.ts + lifecycle.service.ts
T20 DB 连接模式改 getDb() 函数式 P2 修改 database.ts
T21 ID 策略改 cuid2 P2 修改 service 层
T22 补齐单元测试Service/Repository/ChannelDispatcher P2 新建 *.spec.ts
T23 修正 README与实现对齐 P2 修改 README.md

10.2 跨模块依赖(前置)

# 任务 阻塞性
D1 events.proto 补 UserEvent / RoleEvent / NotificationEvent / MasteryEvent message 🔴 阻塞 msg 消费事件
D2 events.proto ExamEvent/HomeworkEvent/GradeEvent 补 class_id / student_ids[] 字段 🔴 阻塞广播 fan-out
D3 msg.proto 补 BatchSendNotification / GetUnreadCount / BatchMarkAsRead / MarkAllAsRead / RecallNotification RPC 🔴 阻塞 gRPC 完整契约
D4 msg.proto 补 NotificationPreferenceService + NotificationTemplateService 🔴 阻塞偏好与模板
D5 push-gateway 提供 gRPC PushService.Push 方法ai02 设计确认) 🔴 阻塞实时推送
D6 iam 发布 6 类用户/角色事件ai06 设计确认) 🟡 不阻塞 P5 启动,但消费逻辑无法验证
D7 core-edu 发布 5 类教学事件ai08 设计确认) 🟡 同上
D8 data-ana 发布 mastery 事件ai11 设计确认) 🟡 同上

11. 与黄金模板对齐 checklist

classes 黄金模板 msg 当前 msg 目标P5 完成)
权限装饰器 @RequirePermission 全覆盖 6 端点 扩展到 17+ 端点
错误码前缀 CLASSES_* MSG_* 保持 + 新增 4 个错误码
logger pino 保持 + 敏感字段脱敏
metrics prom-client + /metrics 保持 + 15 个指标
tracer OTel + auto-instrumentations 保持 + 跨 Kafka trace 传递
/healthz 保持
/readyz DB SELECT 1 ⚠️ 仅 DB 多依赖DB/ES/Redis/Kafka/PushGateway
优雅关闭 LifecycleService ⚠️ main.ts 重复关闭 统一到 LifecycleService
测试覆盖率 60% 0% ≥ 80%
Dockerfile 多阶段 保持
Zod 校验 schema.parse ⚠️ ZodError 未处理 Controller 层全 parse + ZodError 400
GlobalErrorFilter + ZodError 分支
DB 连接模式 getDb() 函数式 ⚠️ const db 改 getDb()
ID 生成 cuid2 ⚠️ randomUUID 改 cuid2
Repository 抽象 新建 repository 层
Outbox (黄金模板自身无) 新增 Outbox 模式
Kafka consumer 新增 Kafka 消费模式(回写黄金模板)
多渠道分发 新增 ChannelDispatcher 模式(回写黄金模板)
状态机字段 status 字段

12. 多渠道分发详细设计(附录)

12.1 NotificationChannel 接口

interface NotificationChannel {
  readonly name: NotificationChannelName;
  send(
    notification: Notification,
    userPreference: NotificationPreference,
  ): Promise<SendResult>;
  isAvailable(): boolean;
}

12.2 渠道实现清单

渠道 实现类 依赖 适用通知类型 备注
in_app InAppChannel MySQL 全部 总是发送,写入 msg_notifications
email EmailChannel SMTP Server system/exam/announcement 异步队列
sms SMSChannel SMS Provider API grade/attendance/mastery_alert 紧急通知优先
wechat WeChatChannel WeChat API 全部(用户绑定微信后) 模板消息
push PushChannel Push Gateway gRPC 全部(在线用户) 实时推送

12.3 ChannelDispatcher 调度逻辑

class ChannelDispatcher {
  async dispatch(
    notification: Notification,
    preference: NotificationPreference,
  ): Promise<SendResult[]> {
    const channels = this.resolveChannels(notification, preference);
    const results = await Promise.allSettled(
      channels.map((ch) => ch.send(notification, preference)),
    );
    return results.map((r, i) =>
      r.status === "fulfilled"
        ? r.value
        : {
            success: false,
            channel: channels[i].name,
            error: r.reason.message,
          },
    );
  }

  private resolveChannels(
    notification: Notification,
    preference: NotificationPreference,
  ): NotificationChannel[] {
    if (!preference.enabled) return [this.inAppChannel]; // 仅站内信
    const channels = preference.channels
      .map((name) => this.channelRegistry.get(name))
      .filter((ch) => ch && ch.isAvailable());
    if (!channels.includes(this.inAppChannel)) {
      channels.unshift(this.inAppChannel); // in_app 总是加入
    }
    return channels;
  }
}

AI Agent: ai05 (content + msg) Coordinator: coord-ai Branch: 单仓库并行模式(直接 push main