Files
Edu/services/msg
SpecialX 81a539b9ab
Some checks failed
CI / quality-ts (push) Failing after 6s
CI / quality-go (push) Failing after 25s
CI / quality-proto (push) Failing after 6s
CI / deploy (push) Has been skipped
chore(deps): 统一依赖管理 - pnpm 11 + node:22 + golang:1.25 + python:3.12 + shared-* 集中化
- Node.js 统一到 node:22-alpine,Go 统一到 golang:1.25-alpine,Python 统一到 python:3.12-slim

- pnpm 升级到 11.13.0(corepack),新增 allowBuilds 白名单解决 ERR_PNPM_IGNORED_BUILDS

- 新增 packages/shared-py 集中 Python 共享依赖,shared-ts 补充 graphql-yoga/prom-client

- api-gateway 修复 go.mod 的 shared-go 依赖 + Dockerfile 改用 repo 根作 context

- Python 服务(data-ana/ai)Dockerfile 改用 repo 根作 context + 声明 uv workspace sources

- 16 个服务的 Dockerfile + CI + docker-compose.tools.yml 全部对齐版本矩阵

- known-issues.md 沉淀 9 条 pnpm 11 / uv workspace / Go shared-go 迁移经验

- 验证:4 服务完全成功(api-gateway /healthz 200),其余 install 成功(build 失败为预存 TS 错误)
2026-07-14 12:04:49 +08:00
..

Msg 沟通通知中台

版本1.0P5 完整实现) HTTP 端口3007 / gRPC 端口50056 设计文档:02-architecture-design.md 契约文档:msg_contract.md

职责

沟通通知限界上下文P5承担

  1. 多渠道通知分发:站内信 / 邮件 / 短信 / 推送 / 微信(策略模式 NotificationChannel 抽象)
  2. 事件驱动 fan-out:消费 iam / core-edu / data-ana 共 12 类 Kafka 事件触发通知
  3. CQRS 读写分离MySQL 写模型 + Elasticsearch 全文检索 + Redis 位图(已读状态降级)
  4. Push Gateway 解耦:通过 HTTP POST /internal/push 委托 push-gateway 推送实时通知coord M4 仲裁豁免 gRPC
  5. Outbox 事务性事件:通知发送 / 已读 / 撤回 / 失败 4 类事件经 Outbox 模式投递到 Kafka
  6. 幂等去重三层防线Redis SETNX + processed_events 表 + event_id UNIQUE INDEX

技术栈

技术 版本
运行时 Node.js 22
框架 NestJSHTTP + gRPC 双协议 microservice 10
语言 TypeScriptESM 模式) 5
ORM Drizzle ORM函数式 getDb() latest
写模型 MySQL 8.0
读模型 ElasticsearchsafeIndex/safeSearch 降级) 8
缓存/位图 Redis幂等去重 + 已读位图) 7
消息 KafkaKafkaJSproducer + consumer 3.x
可观测性 pino + prom-client + OpenTelemetry SDK
ID cuid2varchar(32)
契约 protobufpackages/shared-proto/proto/msg.proto17 RPC buf v2

架构分层

flowchart TB
  subgraph Gateway
    AG[api-gateway]
  end
  subgraph BFF
    TBFF[teacher-bff]
  end
  subgraph MsgService[msg service · HTTP:3007 + gRPC:50056]
    HTTP[HTTP Controller]
    GRPC[gRPC Controller · 3 Service 17 RPC]
    SVC[Application Service · notifications/preferences/templates]
    DISP[ChannelDispatcher · 策略模式]
    CH[InApp/Email/SMS/Push/WeChat Channels]
    KAFKA_C[KafkaConsumer · 12 类事件]
    OUTBOX[OutboxPublisher · 4 类事件]
    REPO[Repository · Drizzle]
    ES[Elasticsearch 读模型]
    REDIS[Redis 幂等/位图]
    PUSH[Push Gateway Client · HTTP]
  end
  AG --> HTTP
  TBFF --> GRPC
  KAFKA_C --> SVC
  SVC --> DISP
  DISP --> CH
  CH --> PUSH
  SVC --> REPO
  SVC --> ES
  SVC --> REDIS
  SVC --> OUTBOX
  OUTBOX -->|publish| KT[(Kafka · edu.notification.*)]
  KAFKA_K[(Kafka · edu.identity/teaching/insight.*)] --> KAFKA_C

模块结构

services/msg/src/
├─ notifications/                    # 通知限界上下文
│  ├─ notifications.controller.ts    # HTTP REST10 端点)
│  ├─ notifications.service.ts       # Application Service
│  ├─ notifications.repository.ts    # Drizzle 数据访问
│  ├─ notifications.schema.ts        # Drizzle 表定义4 张表)
│  └─ notifications.dto.ts           # Zod 验证 + DTO
├─ preferences/                      # 用户偏好限界上下文
│  ├─ preferences.controller.ts      # HTTP REST2 端点)
│  ├─ preferences.service.ts
│  ├─ preferences.repository.ts
│  └─ preferences.dto.ts
├─ templates/                        # 通知模板限界上下文
│  ├─ templates.controller.ts        # HTTP REST6 端点)
│  ├─ templates.service.ts
│  ├─ templates.repository.ts
│  └─ templates.dto.ts
├─ channels/                         # 多渠道分发(策略模式)
│  ├─ channel.types.ts               # NotificationChannelStrategy 接口
│  ├─ channel-dispatcher.service.ts  # ChannelDispatcher 编排
│  ├─ in-app.channel.ts
│  ├─ email.channel.ts
│  ├─ sms.channel.ts
│  ├─ push.channel.ts                # 走 PushGatewayClient
│  └─ wechat.channel.ts
├─ grpc/                             # gRPC 入口3 Service 17 RPC
│  ├─ grpc.module.ts
│  ├─ notifications.grpc.controller.ts  # 9 RPC
│  ├─ preferences.grpc.controller.ts    # 2 RPC
│  └─ templates.grpc.controller.ts      # 6 RPC
├─ config/                           # 配置
│  ├─ env.ts                         # @t3-oss/env-nextjs + Zod
│  ├─ database.ts                    # getDb() 函数式
│  ├─ elasticsearch.ts               # safeIndex/safeSearch 降级
│  └─ grpc.ts                        # gRPC microservice options
├─ middleware/
│  ├─ auth.middleware.ts             # JWT 校验 + userId/roles 注入
│  └─ permission.guard.ts            # @RequirePermission 3 个权限点
├─ shared/
│  ├─ errors/
│  │  ├─ application-error.ts        # 13 个错误码
│  │  └─ global-error.filter.ts      # GlobalErrorFilter
│  ├─ health/
│  │  └─ health.controller.ts        # /healthz + /readyz5 项依赖)
│  ├─ kafka/
│  │  ├─ kafka.client.ts             # producer + consumer + isKafkaHealthy
│  │  ├─ kafka.consumer.ts           # 12 类事件路由
│  │  └─ topic-map.ts                # PRODUCER_TOPIC_MAP + CONSUMER_TOPICS
│  ├─ lifecycle/
│  │  └─ lifecycle.service.ts        # DB/ES/Redis/Kafka/Outbox 生命周期
│  ├─ observability/
│  │  ├─ logger.ts                   # pino
│  │  ├─ metrics.ts                  # 15 个业务指标
│  │  └─ tracer.ts                   # OpenTelemetry SDK
│  ├─ outbox/
│  │  ├─ outbox.publisher.ts         # 轮询 pending 投递 Kafka
│  │  ├─ outbox.repository.ts        # findPending/markPublished/incrementRetry
│  │  ├─ outbox.service.ts           # 事务内写入 outbox
│  │  └─ outbox.schema.ts            # msg_outbox_events + processed_events
│  ├─ push/
│  │  └─ push-gateway.client.ts      # HTTP POST /internal/pushM4 仲裁)
│  └─ redis/
│     ├─ redis.client.ts             # getRedis/checkRedisConnection/closeRedis
│     └─ idempotency.guard.ts        # checkAndMarkRedis SETNX + DB 降级)
├─ app.module.ts                     # 根 Module
└─ main.ts                           # HTTP + gRPC 双协议启动

API 清单

HTTP REST18 端点)

Notifications10 端点)

方法 路径 权限点 说明
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 未读数
PUT /notifications/:id/read MSG_NOTIFICATION_READ 标记已读
PUT /notifications/batch/read MSG_NOTIFICATION_READ 批量已读
PUT /notifications/read-all MSG_NOTIFICATION_READ 全部已读
GET /notifications/search MSG_NOTIFICATION_READ ES 全文检索
POST /notifications/recall MSG_NOTIFICATION_MANAGE 撤回广播
DELETE /notifications/:id MSG_NOTIFICATION_MANAGE 删除

Preferences2 端点)

方法 路径 权限点 说明
GET /preferences/user/:userId MSG_NOTIFICATION_READ 查询偏好
PUT /preferences/user/:userId MSG_NOTIFICATION_MANAGE 更新偏好

Templates6 端点)

方法 路径 权限点 说明
GET /templates MSG_NOTIFICATION_MANAGE 列表
POST /templates MSG_NOTIFICATION_MANAGE 创建
GET /templates/:id MSG_NOTIFICATION_MANAGE 查询
PUT /templates/:id MSG_NOTIFICATION_MANAGE 更新
DELETE /templates/:id MSG_NOTIFICATION_MANAGE 删除
POST /templates/render MSG_NOTIFICATION_SEND 渲染模板

gRPC3 Service / 17 RPC

契约源:packages/shared-proto/proto/msg.proto 包名:next_edu_cloud.msg.v1

NotificationService9 RPC

RPC 权限点 说明
SendNotification MSG_NOTIFICATION_SEND 单条发送
BatchSendNotification MSG_NOTIFICATION_SEND 批量发送
ListNotifications MSG_NOTIFICATION_READ 列表
GetUnreadCount MSG_NOTIFICATION_READ 未读数
MarkAsRead MSG_NOTIFICATION_READ 标记已读
BatchMarkAsRead MSG_NOTIFICATION_READ 批量已读
MarkAllAsRead MSG_NOTIFICATION_READ 全部已读
SearchNotifications MSG_NOTIFICATION_READ ES 检索
RecallNotification MSG_NOTIFICATION_MANAGE 撤回

NotificationPreferenceService2 RPC

RPC 权限点 说明
GetPreferences MSG_NOTIFICATION_READ 查询偏好
UpdatePreferences MSG_NOTIFICATION_MANAGE 更新偏好

NotificationTemplateService6 RPC

RPC 权限点 说明
CreateTemplate MSG_NOTIFICATION_MANAGE 创建
GetTemplate MSG_NOTIFICATION_MANAGE 查询
ListTemplates MSG_NOTIFICATION_MANAGE 列表
UpdateTemplate MSG_NOTIFICATION_MANAGE 更新
DeleteTemplate MSG_NOTIFICATION_MANAGE 删除
RenderTemplate MSG_NOTIFICATION_SEND 渲染

Kafka 事件流

消费12 类事件)

Topic 来源 触发通知
edu.identity.user.created iam 欢迎通知
edu.identity.user.updated iam (幂等标记,无通知)
edu.identity.user.deleted iam (幂等标记,无通知)
edu.identity.user.role_changed iam 角色变更通知
edu.identity.role.created iam (幂等标记,无通知)
edu.identity.role.updated iam 权限变更通知fan-out affectedUserIds
edu.teaching.exam.published core-edu 考试通知fan-out studentIds
edu.teaching.assignment.submitted core-edu 作业提交通知(教师)
edu.teaching.assignment.graded core-edu 作业批改通知(学生)
edu.teaching.grade.recorded core-edu 成绩录入通知
edu.teaching.attendance.recorded core-edu 出勤异常通知(家长)
edu.insight.mastery.updated data-ana 学情预警通知

发布4 类事件 · Outbox 模式)

事件类型 Topic 触发条件
notification.sent edu.notification.sent 通知发送成功
notification.read edu.notification.read 通知标记已读
notification.recalled edu.notification.recalled 通知撤回
notification.failed edu.notification.failed 通知发送失败

幂等去重

  • Redis SETNX(首选):SETNX msg:event:{event_id} 1 EX 86400
  • DB 降级processed_eventsevent_id UNIQUE INDEX
  • 三层防线Redis → DB → 业务层 event_id 检查

数据库表

建表脚本:scripts/msg-init.sql

用途 关键索引
msg_notifications 通知主表 event_id UNIQUE / user_id+is_read / group_id
msg_notification_preferences 用户偏好 user_id+type UNIQUE
msg_notification_templates 通知模板 code UNIQUE
msg_notification_deliveries 投递记录 notification_id / status / next_retry_at
msg_outbox_events Outbox 事件 status+next_retry_at / topic
processed_events 消费幂等 event_id PK

健康检查

端点 用途 检查项
GET /healthz liveness 仅进程状态
GET /readyz readiness DB关键/ ES可选/ Redis可选/ Kafka / PushGateway软失败

/readyz 返回结构:

{
  "status": "ok" | "degraded",
  "service": "msg",
  "timestamp": "2026-07-10T...",
  "checks": {
    "database": { "status": "ok" },
    "elasticsearch": { "status": "ok" },
    "redis": { "status": "ok" },
    "kafka": { "status": "ok" },
    "pushGateway": { "status": "skipped" }
  }
}

可观测性

Metrics/metrics 端点 · 15 个指标)

指标 类型 标签
msg_http_requests_total Counter method/route/status_code
msg_http_request_duration_seconds Histogram method/route
msg_grpc_requests_total Counter rpc_method/status
msg_notification_sent_total Counter type/channel/status
msg_notification_send_duration_seconds Histogram type/channel
msg_notification_failed_total Counter type/channel/error_code
msg_notification_read_total Counter type
msg_channel_dispatch_duration_seconds Histogram channel
msg_kafka_consumer_lag Gauge topic
msg_kafka_consumer_processed_total Counter topic/status
msg_idempotent_duplicate_total Counter topic
msg_outbox_pending_count Gauge
msg_push_gateway_call_total Counter status
msg_push_gateway_call_duration_seconds Histogram
msg_unread_count Gauge user_id

Tracer

  • OpenTelemetry SDK + auto-instrumentations
  • 服务名:msg
  • OTLP exporter{OTEL_EXPORTER_OTLP_ENDPOINT}/v1/traces

错误码

错误码 HTTP 触发条件
MSG_VALIDATION_ERROR 400 Zod 校验失败
MSG_NOT_FOUND 404 通知/偏好/模板不存在
MSG_PERMISSION_DENIED 403 权限不足 / 跨用户访问
MSG_CONFLICT 409 重复发送 / 状态机非法转换
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 通知频率超限

开发

# 安装依赖monorepo 根目录)
pnpm install

# 启动开发服务HTTP:3007 + gRPC:50056
pnpm --filter msg dev

# 构建产物
pnpm --filter msg build

# 启动生产服务
pnpm --filter msg start

环境变量

变量 默认值 必填 说明
PORT 3007 HTTP 端口
GRPC_PORT 50056 gRPC 端口
DATABASE_URL MySQL 连接串
REDIS_URL Redis 连接串(未配置降级到 DB 去重)
JWT_SECRET JWT 密钥Gateway 已校验,可选)
JWT_ISSUER next-edu-cloud JWT 签发者
KAFKA_BROKERS localhost:9092 Kafka broker 列表
KAFKA_CLIENT_ID msg-service Kafka client ID
KAFKA_CONSUMER_GROUP_ID msg-service-group Kafka consumer group
ES_URL Elasticsearch 地址(未配置则降级到 DB 查询)
PUSH_GATEWAY_URL Push Gateway 地址M4 仲裁 HTTP
PUSH_INTERNAL_TOKEN Push Gateway 内部鉴权 token
OTEL_EXPORTER_OTLP_ENDPOINT OpenTelemetry OTLP endpoint
LOG_LEVEL info pino 日志级别
NODE_ENV development 运行环境
DEV_MODE false 开发模式(跳过权限校验)

仲裁依据

  • M4Push Gateway 走 HTTP POST /internal/push,豁免 gRPC鉴权头 X-Internal-Key
  • M5:消费 topic 用 edu.teaching.* / edu.identity.* / edu.insight.*
  • G2/readyz 多依赖检查DB/ES/Redis/Kafka/PushGateway
  • G10getDb() 函数式(非类)
  • G11cuid2 IDvarchar(32)
  • G12ESM 模式,相对 import 带 .js 后缀
  • G13:仅类型导入用 import type

详见 coord-final-decisions.md + president-final-rulings.md