Msg 沟通通知中台
版本:1.0(P5 完整实现)
HTTP 端口:3007 / gRPC 端口:50056
设计文档:02-architecture-design.md
契约文档:msg_contract.md
职责
沟通通知限界上下文(P5),承担:
- 多渠道通知分发:站内信 / 邮件 / 短信 / 推送 / 微信(策略模式
NotificationChannel 抽象)
- 事件驱动 fan-out:消费 iam / core-edu / data-ana 共 12 类 Kafka 事件触发通知
- CQRS 读写分离:MySQL 写模型 + Elasticsearch 全文检索 + Redis 位图(已读状态降级)
- Push Gateway 解耦:通过 HTTP POST
/internal/push 委托 push-gateway 推送实时通知(coord M4 仲裁豁免 gRPC)
- Outbox 事务性事件:通知发送 / 已读 / 撤回 / 失败 4 类事件经 Outbox 模式投递到 Kafka
- 幂等去重三层防线:Redis SETNX + processed_events 表 + event_id UNIQUE INDEX
技术栈
| 层 |
技术 |
版本 |
| 运行时 |
Node.js |
22 |
| 框架 |
NestJS(HTTP + gRPC 双协议 microservice) |
10 |
| 语言 |
TypeScript(ESM 模式) |
5 |
| ORM |
Drizzle ORM(函数式 getDb()) |
latest |
| 写模型 |
MySQL |
8.0 |
| 读模型 |
Elasticsearch(safeIndex/safeSearch 降级) |
8 |
| 缓存/位图 |
Redis(幂等去重 + 已读位图) |
7 |
| 消息 |
Kafka(KafkaJS,producer + consumer) |
3.x |
| 可观测性 |
pino + prom-client + OpenTelemetry SDK |
— |
| ID |
cuid2(varchar(32)) |
— |
| 契约 |
protobuf(packages/shared-proto/proto/msg.proto,17 RPC) |
buf v2 |
架构分层
模块结构
API 清单
HTTP REST(18 端点)
Notifications(10 端点)
| 方法 |
路径 |
权限点 |
说明 |
| 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 |
删除 |
Preferences(2 端点)
| 方法 |
路径 |
权限点 |
说明 |
| GET |
/preferences/user/:userId |
MSG_NOTIFICATION_READ |
查询偏好 |
| PUT |
/preferences/user/:userId |
MSG_NOTIFICATION_MANAGE |
更新偏好 |
Templates(6 端点)
| 方法 |
路径 |
权限点 |
说明 |
| 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 |
渲染模板 |
gRPC(3 Service / 17 RPC)
契约源:packages/shared-proto/proto/msg.proto
包名:next_edu_cloud.msg.v1
NotificationService(9 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 |
撤回 |
NotificationPreferenceService(2 RPC)
| RPC |
权限点 |
说明 |
| GetPreferences |
MSG_NOTIFICATION_READ |
查询偏好 |
| UpdatePreferences |
MSG_NOTIFICATION_MANAGE |
更新偏好 |
NotificationTemplateService(6 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_events 表 event_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 返回结构:
可观测性
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 |
通知频率超限 |
开发
环境变量
| 变量 |
默认值 |
必填 |
说明 |
| 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 |
否 |
开发模式(跳过权限校验) |
仲裁依据
- M4:Push Gateway 走 HTTP POST
/internal/push,豁免 gRPC,鉴权头 X-Internal-Key
- M5:消费 topic 用
edu.teaching.* / edu.identity.* / edu.insight.*
- G2:/readyz 多依赖检查(DB/ES/Redis/Kafka/PushGateway)
- G10:getDb() 函数式(非类)
- G11:cuid2 ID(varchar(32))
- G12:ESM 模式,相对 import 带
.js 后缀
- G13:仅类型导入用
import type
详见 coord-final-decisions.md + president-final-rulings.md