Files
Edu/services/msg/docs/02-architecture.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

42 KiB
Raw Blame History

模块架构设计文档 — msg

AI 标识ai10 负责模块msgP5 沟通通知) 阶段:架构设计外包 · 阶段 2模块架构设计 日期2026-07-09 关联文档:01-understanding.mdai-allocation.md004 架构影响地图pending-features.mdproject_rules.md 参考实现:classes 黄金模板core-edu Outboxiam RBAC


0. 设计原则与长远目标

本设计遵循以下原则,确保既能实现 P5 退出标准,又为 P6 硬化与未来扩展铺路:

  1. 通知即基础设施msg 是全校所有业务事件的下收件人,设计上按"通知中台"标准而非"业务 CRUD"标准,预留多租户、多渠道、多策略扩展点。
  2. 契约先行proto → 实现。NotificationService gRPC + events.proto NotificationEvent 必须先定。
  3. 最终一致 + 幂等Kafka 消费 at-least-once业务侧 event_id 去重Outbox 保证通知请求事件不丢。
  4. 降级优先每个外部依赖DB/ES/Redis/Push/SMS/邮件)都有降级路径,单点故障不阻断核心链路。
  5. 策略模式贯穿渠道层:新增渠道(微信/钉钉/企业微信)只需实现 ChannelStrategy 接口,不改核心。
  6. 读路径分级:列表/计数走 Redis 缓存 → DB → ES 三级降级;全文检索走 ES → DB like 降级。
  7. 可观测可追踪:每条通知从"事件触发 → 模板渲染 → 渠道投递 → 已读"全链路 traceId 串联,投递结果落 deliveries 表。
  8. 向前兼容:所有 schema 变更只增不删proto 字段只加编号不复用,事件版本用 v1/v2 后缀。

1. 模块内部分层图

flowchart TD
    subgraph Gateway["API Gateway (L4)"]
        GW[路由转发 + JWT 校验]
    end

    subgraph BFF["BFF (L3)"]
        BFF1[teacher-bff / student-bff / parent-bff]
    end

    subgraph Msg["msg 服务 (L5 业务服务)"]
        direction TB

        subgraph Entry["入口层"]
            CTRL[NotificationsController<br/>REST]
            GRPC[NotificationGrpcController<br/>gRPC · P5 启用]
            KCONS[KafkaConsumer<br/>事件消费]
        end

        subgraph Guard["横切层"]
            PERM[PermissionGuard<br/>@RequirePermission]
            VALID[Zod 校验]
            FILTER[GlobalErrorFilter]
        end

        subgraph App["应用服务层 (ApplicationService)"]
            NS[NotificationService<br/>编排:幂等 → 模板渲染 → fan-out]
            TS[TemplateService<br/>模板 CRUD + 变量替换 + i18n]
            DS[DeliveryService<br/>投放编排 + 重试 + DLQ]
            PREF[PreferenceService<br/>偏好查询 + 静默时段]
            READ[ReadStateService<br/>已读/未读 + Redis 位图]
            IDEM[IdempotencyService<br/>event_id 去重 Redis SETNX]
        end

        subgraph Strategy["渠道策略层 (ChannelStrategy)"]
            INAPP[InAppChannel<br/>DB+ES 写入]
            EMAIL[EmailChannel<br/>SMTP async]
            SMS[SmsChannel<br/>短信网关 + 限流 + 配额]
            PUSH[PushChannel<br/>gRPC → push-gateway]
            WECHAT[WechatChannel<br/>未来·预留接口]
        end

        subgraph Domain["领域服务 (DomainService)"]
            REPO[NotificationRepository<br/>Drizzle 数据访问]
            OUTBOX[OutboxPublisher<br/>NotificationRequested 事件]
        end

        subgraph Data["数据层"]
            DB[(MySQL<br/>写模型)]
            ES[(Elasticsearch<br/>全文检索 + 降级读)]
            REDIS[(Redis<br/>幂等/位图/计数/限流)]
            KAFKA[(Kafka<br/>消费+发布)]
        end
    end

    subgraph External["外部依赖"]
        PUSHGW[push-gateway<br/>WebSocket 推送]
        SMTP[SMTP 邮件服务]
        SMSGW[短信服务商]
    end

    GW --> CTRL
    BFF1 --> GRPC
    KAFKA --> KCONS
    KCONS --> IDEM --> NS
    CTRL --> PERM --> VALID --> NS
    GRPC --> NS
    NS --> TS
    NS --> PREF
    NS --> DS
    NS --> READ
    DS --> INAPP & EMAIL & SMS & PUSH
    INAPP --> REPO --> DB
    INAPP --> ES
    PUSH --> PUSHGW
    EMAIL --> SMTP
    SMS --> SMSGW
    NS --> OUTBOX --> KAFKA
    READ --> REDIS
    READ --> REPO
    IDEM --> REDIS

关键拦截点

  • PermissionGuardAPP_GUARD每个 Controller 方法 @RequirePermission 校验
  • GlobalErrorFilterZodError → 400、ApplicationError → 对应状态码、兜底 500
  • IdempotencyServiceKafka 消费与 HTTP send 均先过幂等检查
  • OutboxPublisher:事务内写 outbox 表,独立轮询发布到 Kafka

2. 领域模型

2.1 聚合根与实体

classDiagram
    class Notification {
        +id: UUID
        +userId: string
        +type: NotificationType
        +category: NotificationCategory
        +priority: Priority
        +title: string
        +content: string
        +channel: Channel
        +metadata: JSON
        +scheduledAt?: DateTime
        +expiresAt?: DateTime
        +isRead: boolean
        +readAt?: DateTime
        +idempotencyKey?: string
        +sourceEventId?: string
        +createdAt: DateTime
        +markAsRead()
        +isExpired()
    }

    class NotificationTemplate {
        +id: UUID
        +code: string
        +name: string
        +category: NotificationCategory
        +channelTemplates: Map~Channel, ChannelTemplate~
        +variables: TemplateVariable[]
        +isActive: boolean
        +version: int
        +render(ctx): RenderedContent
    }

    class ChannelTemplate {
        +channel: Channel
        +subjectTpl: string
        +bodyTpl: string
        +i18n: Map~locale, LocalizedTpl~
    }

    class NotificationPreference {
        +userId: string
        +channelEnabled: Map~Channel, boolean~
        +categoryEnabled: Map~Category, boolean~
        +quietHours: QuietHours
        +digestConfig: DigestConfig
        +isChannelAllowed(channel, category): boolean
    }

    class NotificationDelivery {
        +id: UUID
        +notificationId: UUID
        +channel: Channel
        +status: DeliveryStatus
        +externalId?: string
        +attemptCount: int
        +lastError?: string
        +deliveredAt?: DateTime
        +markSent()
        +markFailed(err)
        +canRetry(): boolean
    }

    class QuietHours {
        +start: string
        +end: string
        +timezone: string
        +isInQuietHours(now): boolean
    }

    Notification "1" --> "0..*" NotificationDelivery : has
    NotificationTemplate "1" --> "0..*" ChannelTemplate : contains
    NotificationPreference "1" --> "1" QuietHours : has

    enum NotificationType { SYSTEM ANNOUNCEMENT EXAM HOMEWORK GRADE ATTENDANCE MASTERY }
    enum NotificationCategory { ACADEMIC BEHAVIOR SYSTEM MARKETING }
    enum Priority { URGENT HIGH NORMAL LOW }
    enum Channel { IN_APP EMAIL SMS PUSH WECHAT }
    enum DeliveryStatus { PENDING SENT DELIVERED FAILED RETRYING }

2.2 聚合边界与通信

  • Notification 聚合(根):包含 1:N Delivery。聚合内直接调用聚合间通过事件。
  • NotificationTemplate 聚合(根):独立管理,被 NotificationService 引用渲染。
  • NotificationPreference 聚合(根):按 userId 隔离,被 PreferenceService 查询。
  • 跨聚合通信TemplateService / PreferenceService / ReadStateService 同服务内直接方法调用(应用层编排),不发事件。
  • 跨服务通信:仅通过 Kafka 事件(消费 core-edu/iam/data-ana 事件;发布 NotificationRequested 给 push-gateway

3. 数据模型

3.1 表清单MySQLmsg 独占库)

表名 用途 现状
msg_notifications 通知主表 已有,需扩展
msg_notification_preferences 用户偏好 已有,需扩展
msg_notification_templates 通知模板 新增
msg_notification_deliveries 投递记录per-channel 新增
msg_outbox Outbox 事件表 新增
msg_idempotency 幂等键Redis 不可用降级) 新增

3.2 Schema 定义

msg_notifications扩展现有

字段 类型 约束 说明
id char(36) PK UUID
user_id char(36) NOT NULL, idx 接收人
type varchar(50) NOT NULL, idx EXAM/HOMEWORK/GRADE/SYSTEM...
category varchar(30) NOT NULL, default 'ACADEMIC' ACADEMIC/BEHAVIOR/SYSTEM/MARKETING
priority varchar(10) NOT NULL, default 'NORMAL' URGENT/HIGH/NORMAL/LOW
title varchar(200) NOT NULL
content text NOT NULL
channel varchar(20) NOT NULL, default 'in_app' in_app/email/sms/push/wechat
metadata json nullable 透传业务上下文
scheduled_at timestamp nullable, idx 调度发送时间(未来)
expires_at timestamp nullable 过期清理
is_read boolean NOT NULL, default false
read_at timestamp nullable
idempotency_key varchar(128) nullable, idx 幂等键HTTP send 用)
source_event_id varchar(128) nullable, idx 触发该通知的 Kafka event_id
created_at timestamp NOT NULL, default now, idx

索引

  • idx_user_created (user_id, created_at DESC) — 用户列表查询主索引
  • idx_user_unread (user_id, is_read, created_at DESC) — 未读列表
  • idx_source_event (source_event_id) — 事件溯源
  • idx_idempotency (idempotency_key) — 唯一索引(防重)
  • idx_scheduled (scheduled_at) WHERE scheduled_at IS NOT NULL — 调度扫描
  • idx_expires (expires_at) WHERE expires_at IS NOT NULL — 过期清理

msg_notification_preferences扩展现有

字段 类型 约束 说明
user_id char(36) PK
email_enabled boolean default true
sms_enabled boolean default false
push_enabled boolean default true
in_app_enabled boolean default true
wechat_enabled boolean default false 新增,未来渠道
academic_enabled boolean default true 新增,按分类退订
behavior_enabled boolean default true 新增
system_enabled boolean default true 新增
marketing_enabled boolean default false 新增
quiet_start varchar(5) nullable 新增 "22:00" 静默开始
quiet_end varchar(5) nullable 新增 "07:00" 静默结束
quiet_timezone varchar(40) default 'Asia/Shanghai' 新增
digest_email boolean default false 新增 每日摘要开关
updated_at timestamp default now 新增

msg_notification_templates新增

字段 类型 约束 说明
id char(36) PK UUID
code varchar(64) UNIQUE, idx 模板编码(如 EXAM_PUBLISHED
name varchar(100) NOT NULL 模板名称
category varchar(30) NOT NULL 分类
channel varchar(20) NOT NULL 渠道
subject_tpl varchar(200) nullable 标题模板(含 {{var}}
body_tpl text NOT NULL 正文模板
variables json nullable 变量定义 [{name,required,desc}]
locale varchar(10) default 'zh-CN' i18n
is_active boolean default true
version int default 1 版本号
created_at timestamp default now
updated_at timestamp default now

唯一索引uk_code_channel_locale (code, channel, locale)

msg_notification_deliveries新增

字段 类型 约束 说明
id char(36) PK UUID
notification_id char(36) NOT NULL, idx FK→notifications
channel varchar(20) NOT NULL 投递渠道
status varchar(20) NOT NULL PENDING/SENT/DELIVERED/FAILED/RETRYING
external_id varchar(128) nullable 外部网关返回 ID
attempt_count int default 0 重试次数
max_retry int default 3 最大重试
last_error text nullable 失败原因
next_retry_at timestamp nullable, idx 下次重试时间
delivered_at timestamp nullable 投递成功时间
created_at timestamp default now
updated_at timestamp default now

索引idx_notification (notification_id)idx_retry (status, next_retry_at)

msg_outbox新增参照 core-edu

字段 类型 约束 说明
id char(36) PK UUID
aggregate_id char(36) NOT NULL, idx notificationId
aggregate_type varchar(30) NOT NULL 'Notification'
event_type varchar(50) NOT NULL notification.requested/sent
payload text NOT NULL JSON 序列化事件
processed boolean default false, idx
retry_count int default 0
created_at timestamp default now
processed_at timestamp nullable

msg_idempotency新增Redis 降级用)

字段 类型 约束 说明
key varchar(128) PK event_id 或 idempotencyKey
result json NOT NULL 首次执行结果(缓存复用)
created_at timestamp default now
expires_at timestamp NOT NULL, idx TTL默认 7 天)

3.3 读写分离策略

读场景 主路径 降级路径 1 降级路径 2
用户通知列表 DBidx_user_created ESuser_id filter
未读计数 Redis 位图 BITCOUNT DB COUNT(*) ES count
全文检索 ES multi_match DB LIKE降级 返回空(不阻断)
用户偏好 Redis 缓存5min TTL DB 默认偏好(内存)
模板渲染 Redis 缓存10min DB

写路径:所有写走 MySQL 主库单主无读写分离ES 通过同步索引异步写入safeIndex 失败不阻断)。

3.4 Elasticsearch 索引设计

索引名:msg_notifications_v1(带版本号,便于 reindex

{
  "mappings": {
    "properties": {
      "userId": { "type": "keyword" },
      "type": { "type": "keyword" },
      "category": { "type": "keyword" },
      "priority": { "type": "keyword" },
      "channel": { "type": "keyword" },
      "title": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "content": {
        "type": "text",
        "analyzer": "ik_max_word",
        "search_analyzer": "ik_smart"
      },
      "isRead": { "type": "boolean" },
      "createdAt": { "type": "date" },
      "scheduledAt": { "type": "date" }
    }
  },
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 1,
    "refresh_interval": "1s"
  }
}

索引管理

  • 启动时 ensureIndex():若索引不存在则创建(带 mapping
  • 别名 msg_notificationsmsg_notifications_v1reindex 时切换别名实现零停机
  • 中文分词需 ik 插件(已在 infra 配置)

4. API 设计

4.1 REST APIHTTP经 api-gateway

method path 权限 说明
POST /notifications MSG_NOTIFICATION_SEND 发送单条通知
POST /notifications/batch MSG_NOTIFICATION_SEND 批量发送(广播)
GET /notifications/user/:userId MSG_NOTIFICATION_READ 查询用户通知(?unread=true
GET /notifications/user/:userId/page MSG_NOTIFICATION_READ 分页查询
GET /notifications/user/:userId/unread-count MSG_NOTIFICATION_READ 未读计数Redis 位图优先)
PUT /notifications/:id/read MSG_NOTIFICATION_MANAGE 标记已读
PUT /notifications/read-all MSG_NOTIFICATION_MANAGE 全部已读(按 userId
GET /notifications/search MSG_NOTIFICATION_READ 全文检索
DELETE /notifications/:id MSG_NOTIFICATION_MANAGE 删除通知(软删,未来)
GET /notifications/preferences/:userId MSG_NOTIFICATION_READ 查询偏好
PUT /notifications/preferences/:userId MSG_NOTIFICATION_MANAGE 更新偏好
POST /notifications/templates MSG_TEMPLATE_MANAGE 创建模板
GET /notifications/templates MSG_TEMPLATE_MANAGE 模板列表
GET /notifications/templates/:code MSG_TEMPLATE_MANAGE 查询单个模板
PUT /notifications/templates/:id MSG_TEMPLATE_MANAGE 更新模板
DELETE /notifications/templates/:id MSG_TEMPLATE_MANAGE 停用模板(软删)

4.2 gRPC APINotificationServiceproto 包 next_edu_cloud.msg.v1

方法 请求 响应 说明
SendNotification SendNotificationRequest Notification 发送通知
ListNotifications ListNotificationsRequest ListNotificationsResponse 列表查询
MarkAsRead MarkAsReadRequest Empty 标记已读
MarkAllAsRead MarkAllAsReadRequest Empty 全部已读(新增)
GetUnreadCount GetUnreadCountRequest GetUnreadCountResponse 未读计数(新增)
SearchNotifications SearchNotificationsRequest SearchNotificationsResponse 全文检索
GetPreference GetPreferenceRequest NotificationPreference 偏好查询(新增)
UpdatePreference UpdatePreferenceRequest NotificationPreference 偏好更新(新增)

proto 字段扩展(在现有 msg.proto 基础上新增 category/priority/scheduledAt/expiresAt/idempotencyKey 等),按 §5 契约规范走 coord 变更流程。

4.3 请求/响应示例(发送通知)

POST /notifications
{
  "userId": "u-123",
  "type": "EXAM",
  "category": "ACADEMIC",
  "priority": "HIGH",
  "title": "数学期中考试已发布",
  "content": "请于 2026-07-15 前完成",
  "channel": "in_app",
  "metadata": { "examId": "e-456" },
  "idempotencyKey": "client-uuid-789",
  "scheduledAt": null
}
{
  "success": true,
  "data": {
    "id": "notif-uuid",
    "skipped": false,
    "deliveries": [
      { "channel": "in_app", "status": "SENT" },
      { "channel": "push", "status": "SENT", "externalId": "push-msg-id" }
    ]
  }
}

5. 事件设计

5.1 消费的事件Kafka Consumer

消费组:msg-servicepartition 按 aggregate_id 保证同聚合有序。

Topic 生产者 事件类型 msg 动作
edu.identity.user.created iam UserRegistered 发欢迎通知in_app + email
edu.identity.user.updated iam UserUpdated 通知关键信息变更
edu.identity.user.role_changed iam UserRoleChanged 通知角色变更
edu.teaching.exam.published core-edu ExamPublished 给班级全体学生发考试通知fan-out
edu.teaching.assignment.submitted core-edu HomeworkSubmitted 通知教师有学生提交作业
edu.teaching.grade.recorded core-edu GradeRecorded 通知学生成绩已录入
edu.insight.mastery.updated data-ana MasteryUpdated 掌握度低于阈值触发预警通知

消费幂等:每个消息的 event_idproto 字段)→ Redis SETNX msg:idem:{event_id} TTL 7 天。SETNX 成功才处理失败说明已处理跳过。Redis 不可用时降级到 msg_idempotency 表唯一键。

5.2 发布的事件Outbox → Kafka

Topic 事件类型 触发时机 消费者
edu.notification.events notification.requested 通知创建后,请求推送 push-gateway
edu.notification.events notification.delivered 渠道投递成功(未来) data-ana
edu.notification.events notification.read 用户标记已读(未来) data-ana

NotificationRequested 事件需 coord 在 events.proto 追加 NotificationEvent message见 §7 交互点)。

5.3 事件版本演化

  • 字段只增不减proto 编号不复用
  • 破坏性变更新建 v2 后缀 topicedu.notification.events.v2),消费者双消费过渡期后下线 v1

6. 横切关注点对齐清单

6.1 权限装饰器(权限点清单)

权限常量 说明 端点
MSG_NOTIFICATION_SEND 发送通知 POST /notifications, POST /notifications/batch, gRPC Send
MSG_NOTIFICATION_READ 读取通知 GET /notifications/user/*, search, unread-count, GET preferences
MSG_NOTIFICATION_MANAGE 管理通知(已读/删除) PUT /read, read-all, DELETE, PUT preferences
MSG_TEMPLATE_MANAGE 模板管理(新增) POST/GET/PUT/DELETE /notifications/templates/*

权限模型从硬编码 ROLE_PERMISSIONS 迁移到对齐 iam 的 RBAC角色 → 权限点映射由 iam 通过 getEffectivePermissions 下发PermissionGuard 读取 x-user-permissions header由 Gateway 注入)。过渡期保留 ROLE_PERMISSIONS 兜底。

6.2 错误码清单(前缀 MSG_

错误码 触发条件 HTTP
MSG_VALIDATION_ERROR Zod 校验失败 400
MSG_NOT_FOUND 通知/模板/偏好不存在 404
MSG_PERMISSION_DENIED 权限不足 403
MSG_CONFLICT 幂等键冲突/重复发送 409
MSG_BUSINESS_ERROR 偏好禁用该渠道/静默时段跳过 422
MSG_TEMPLATE_RENDER_ERROR 模板变量缺失/渲染失败(新增) 422
MSG_RATE_LIMITED 短信/邮件限流(新增) 429
MSG_QUOTA_EXCEEDED 短信配额耗尽(新增) 429
MSG_DATABASE_ERROR DB 操作失败 500
MSG_EXTERNAL_GATEWAY_ERROR SMTP/SMS 网关错误(新增) 502
MSG_INTERNAL_ERROR 兜底 500

6.3 可观测性

Loggerpino结构化 JSON注入 traceIdx-request-id header。初始化位置 logger.ts

Metrics 指标清单prom-client前缀 msg_

指标名 类型 标签 说明
msg_notification_sent_total Counter channel,type,category,priority 通知发送总数
msg_notification_delivered_total Counter channel,status 投递结果总数
msg_notification_read_total Counter type 已读总数
msg_kafka_consumed_total Counter topic,event_type Kafka 消费总数
msg_kafka_consumed_errors_total Counter topic Kafka 消费失败
msg_outbox_pending Gauge Outbox 待发布数
msg_outbox_publish_duration_seconds Histogram Outbox 发布耗时
msg_template_render_duration_seconds Histogram code,channel 模板渲染耗时
msg_idempotency_dedup_total Counter source(kafka/http) 幂等去重命中数
msg_channel_delivery_duration_seconds Histogram channel 渠道投递耗时

TracerOpenTelemetry SDK + OTLP exporter初始化位置 tracer.ts。span 命名:msg.<service>.<method>(如 msg.notification.sendmsg.kafka.consume)。

6.4 健康检查

端点 检查逻辑 失败状态
GET /healthz 进程存活,不检查依赖 200
GET /readyz DB SELECT 1 + Redis PING + ES ping(任一失败返回 503 503

当前 /readyz 仅查 DB阶段 2 实现需补 Redis + ES 检查ai10 修订)。

6.5 优雅关闭顺序

统一到 LifecycleService移除 main.ts 重复逻辑),顺序:

  1. HTTP server 停止接收新请求(app.close()
  2. Kafka consumer 提交 offset 并停止(consumer.disconnect()
  3. OutboxPublisher 停止轮询(stop()
  4. Elasticsearch 关闭(closeEs()
  5. Redis 关闭(redisClient.quit()
  6. MySQL 连接池关闭(closeDb()
  7. Tracer flush 并关闭(shutdownTracer()

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

方向 对方服务 协议 接口/事件 用途
被调用 api-gateway HTTP /notifications/* REST 转发
被调用 teacher-bff gRPC NotificationService.* 教师端通知聚合
被调用 student-bff gRPC NotificationService.* 学生端通知查询
被调用 parent-bff gRPC NotificationService.* 家长端通知查询
调用 push-gateway gRPC PushService.Pushai02 定义) 实时推送(替代当前 fetch /internal/push
消费 iam Kafka edu.identity.user.* 用户创建/更新/角色变更触发通知
消费 core-edu Kafka edu.teaching.exam.published 等 考试/作业/成绩事件触发通知
消费 data-ana Kafka edu.insight.mastery.updated 掌握度预警
发布 Kafka edu.notification.events NotificationRequested → push-gateway
iam gRPC UserService.GetUser可选查用户信息渲染模板 模板变量填充(如学生姓名)

跨模块契约对齐提请 coord

  1. events.proto 补充 NotificationEventmsg 发布 NotificationRequested 需 proto 定义coord 维护)
  2. push-gateway gRPC PushServiceai02 需定义 gRPC Push 方法msg 从 fetch 迁移到 gRPC当前 fetch 作为降级保留)
  3. iam 用户信息查询模板渲染需用户姓名msg 是否调 iam.GetUser还是事件 payload 携带足够信息?(倾向后者,避免同步依赖)
  4. msg.proto 扩展:新增 MarkAllAsRead / GetUnreadCount / GetPreference / UpdatePreference 方法coord 统一更新 proto

8. 渠道策略模式设计ai-allocation §5 核心)

classDiagram
    class ChannelStrategy {
        <<interface>>
        +channel: Channel
        +send(notification, rendered): Promise~DeliveryResult~
        +isAvailable(): boolean
        +checkRateLimit(userId): boolean
    }

    class InAppChannel {
        +send(): DB insert + ES index
        +isAvailable(): DB up
    }
    class EmailChannel {
        +send(): SMTP send (async queue)
        +isAvailable(): SMTP reachable
        +checkRateLimit(): Redis 滑动窗口
    }
    class SmsChannel {
        +send(): 短信网关 API
        +isAvailable(): 网关可达 + 配额未满
        +checkRateLimit(): Redis 严格限流
        +checkQuota(): Redis 计数
    }
    class PushChannel {
        +send(): gRPC → push-gateway
        +isAvailable(): push-gateway up
        +fallback(): InAppChannel
    }
    class WechatChannel {
        +send(): 未来实现
        +isAvailable(): false (P5 不实现)
    }

    ChannelStrategy <|.. InAppChannel
    ChannelStrategy <|.. EmailChannel
    ChannelStrategy <|.. SmsChannel
    ChannelStrategy <|.. PushChannel
    ChannelStrategy <|.. WechatChannel

渠道编排逻辑DeliveryService

  1. 按 NotificationPreference 过滤被禁用渠道
  2. 按 QuietHours 过滤非紧急通知URGENT 绕过静默)
  3. 对每个启用渠道并行调用 ChannelStrategy.send()
  4. 失败渠道记录到 deliveries 表,按 max_retry 重试(指数退避)
  5. PushChannel 失败降级到 InAppChannel保证不丢

未来扩展(不改核心):新增微信/钉钉/企业微信渠道,只需实现 ChannelStrategy 并注册到 ChannelRegistry。


9. 已读/未读状态管理Redis 位图)

设计

  • 每个用户一个 Redis Bitmapkey msg:unread:{userId}bit offset = 通知序号(自增)
  • 新通知:SETBIT msg:unread:{userId} {seq} 1
  • 标记已读:SETBIT msg:unread:{userId} {seq} 0
  • 未读计数:BITCOUNT msg:unread:{userId}
  • 全部已读:SET msg:unread:{userId} 0(清空)

序号分配

  • Redis INCR msg:seq:{userId} 获取递增序号,存入 notifications.seq 字段(新增)

降级策略

  • Redis 不可用 → 降级到 DB COUNT(*) WHERE user_id=? AND is_read=false
  • DB 是 source of truthRedis 是缓存标记已读时双写DB update + Redis SETBITRedis 失败仅 log 不阻断

未来扩展

  • 位图支持按 category 分桶(msg:unread:{userId}:ACADEMIC)实现分类未读数

10. ES 降级查询策略ai-allocation §5

双向降级

触发场景 主路径 降级路径
正常列表查询 DB ESDB 不可用时)
正常全文检索 ES DB LIKEES 不可用时)
DB 不可用 ES 读模型(可能延迟 1s
ES 不可用 DB LIKE 或返回空(检索类)

实现要点

  • NotificationRepository 内置 listByUser() 优先 DBcatch DB error 后 fallback 到 esSearch()
  • search() 优先 EScatch ES error 后 fallback 到 dbLikeSearch()
  • 降级时 metrics 记录 msg_db_fallback_total / msg_es_fallback_total 便于监控降级频率
  • ES 读模型通过 safeIndex 异步同步,可能存在 1s 延迟,降级时在响应头标注 X-Read-Model: es-degraded

11. Kafka 消费幂等设计ai-allocation §5

flowchart TD
    K[Kafka 消息] --> CHECK{Redis SETNX<br/>msg:idem:event_id}
    CHECK -- 设置成功 --> PROC[处理事件<br/>渲染模板→fan-out]
    CHECK -- 已存在 --> SKIP[跳过,返回原结果]
    PROC --> WRITE[写 notifications + deliveries + outbox]
    WRITE --> CACHE[缓存结果到 Redis]
    PROC -.失败.-> DLQ[Dead Letter Topic<br/>edu.notification.dlq]
    DLQ --> ALERT[告警 + 人工处理]

双保险

  • L1Redis SETNX快速去重TTL 7 天)
  • L2msg_idempotency 表唯一键Redis 不可用时兜底)
  • L3notifications.source_event_id 唯一索引(最终防线)

死信队列:消费失败超过 3 次的消息投递到 edu.notification.dlq,触发告警,人工介入。


12. 风险与假设

12.1 技术风险

风险 影响 缓解措施
广播场景 fan-out 性能1 通知→N 学生) DB 写入瓶颈 批量 INSERT + 异步 ES 索引 + Push 走 push-gateway 批量
短信成本失控 资金损失 严格限流 + 配额管理 + 降级到 in_app
Redis 单点故障 未读计数/幂等失效 降级到 DB双写保证最终一致
Kafka 消费积压 通知延迟 监控 lagHPA 扩容 consumer 实例
模板变量缺失导致渲染失败 通知发不出 渲染失败记 MSG_TEMPLATE_RENDER_ERROR降级用默认文案
ES 与 DB 数据不一致 检索结果缺失 定期 reconcile job未来 P6降级读标注

12.2 假设

  • 假设 iam 在 P2 已提供 getEffectivePermissions APImsg 的 PermissionGuard 可读取权限(过渡期用 ROLE_PERMISSIONS 兜底)
  • 假设 push-gatewayai02P5 提供 gRPC PushService当前 fetch /internal/push 作为降级)
  • 假设 coord 在 events.proto 补充 NotificationEvent message
  • 假设 Redis 纳入 P5 基础设施infra/docker-compose.yml 已有 edu-redismsg 共用)
  • 假设 core-edu 事件 payload 携带足够字段classId/studentId/title供模板渲染msg 不需同步调 iam

12.3 未决决策(提请 coord 仲裁)

  1. Redis 是否 P5 必选msg 幂等/位图/限流强依赖 Redis若 P5 不引入则需全部降级到 DB性能折损
  2. gRPC 启用时机msg gRPC controller 是否 P5 实现(影响 BFF 调用方式)
  3. 短信/邮件服务商选型:影响 ChannelStrategy 实现P5 是否实现真实发送,还是 mock + 接口预留)
  4. 每日摘要digest是否 P5:影响 scheduler 设计(倾向 P6 实现P5 仅预留 schema

13. 实施路线P5 → P6 演进)

P5当前阶段最小可用

优先级 工作项 依赖
P0 Repository 抽象层M4
P0 Kafka consumer + 幂等M1/M5 Redis
P0 Outbox + NotificationRequested 事件M2 events.proto 补充
P0 渠道策略模式 + InApp/Push/Email/SmsM14 push-gateway gRPC
P0 通知模板模块M13
P0 Redis 位图已读/未读M15 Redis
P0 ES mapping + ensureIndexM3
P0 DB→ES 降级读路径M16
P1 投递记录表 + 重试M17
P1 幂等键HTTP sendM18
P1 优先级/分类M19
P1 /readyz 补 Redis+ES 检查
P1 优雅关闭统一到 LifecycleServiceM11
P1 NotificationsModule exportsM7
P1 权限模型对齐 iam RBACM21 iam getEffectivePermissions
P1 gRPC controllerM9待 coord 决策) gRPC 启用决策
P2 README 修正M10
P2 测试覆盖率 ≥ 80%

P6+未来硬化schema 预留)

  • 调度/延迟发送scheduledAt 字段已预留)→ Scheduler worker
  • 每日摘要邮件digest_email 字段已预留)→ Digest worker
  • 通知过期清理expires_at 字段已预留)→ TTL job
  • 微信/钉钉渠道WechatChannel 接口已预留)
  • 多租户隔离tenant_id 字段预留)
  • Webhook 渠道(系统对接)

14. 与黄金模板classes对齐 checklist

检查项 msg 现状 阶段 2 目标
@RequirePermission 覆盖 6 端点 全部新端点
错误码前缀 MSG_ 保持
logger/metrics/tracer 补 msg 专属指标
/healthz + /readyz ⚠️ readyz 仅 DB 补 Redis+ES
优雅关闭 ⚠️ 重复关闭 统一 Lifecycle
测试覆盖率 0% ≥ 80%
Dockerfile 多阶段
Zod 输入验证 新 DTO 同步
GlobalErrorFilter 含 ZodError
Repository 抽象 新增
Outbox 新增
Module exports 新增

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