Files
Edu/services/push-gateway/docs/nextstep-v2.md

353 lines
25 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# push-gateway 模块上下游依赖与工作清单Next Steps v2
> 版本v2
> 日期2026-07-14
> 负责人ai09
> 关联:
>
> - [push-gateway nextstep.md (v1)](./nextstep.md)
> - [msg nextstep.md](../../msg/docs/nextstep.md) §2.4 / §4
> - [student-bff nextstep-v2.md](../../student-bff/docs/nextstep-v2.md) §4.7/§4.8
> - [teacher-portal nextstep-v2.md](../../../apps/teacher-portal/docs/nextstep-v2.md) §3.2.3
> - ARB-013 Kafka topic 命名规范
---
## 1. 概述
push-gateway 是 Edu 平台的实时 WebSocket 推送基础设施L3 网关层,端口 8081负责维护用户在线连接、接收 msg 服务通过 HTTP `/internal/*` 和 Kafka 推送的消息、并通过 WebSocket 实时投递给在线客户端。
**v2 核心目标**:对齐 ARB-013 Kafka topic 命名规范和 msg 服务的 API 契约,消除 v1 中存在的 4 个不一致点。
---
## 2. v2 完成的工作API 契约对齐)
### 2.1 Kafka topic 对齐ARB-013
| 项 | v1 | v2 | 状态 |
| ---------- | -------------------------------- | ---------------------------------------------- | ---- |
| 消费 topic | `edu.notification.requested` | `edu.notify.notification.sent` | ✅ |
| 命名规范 | 旧命名(违反 ARB-013 | `edu.<domain>.<aggregate>.<action>` | ✅ |
| DLQ topic | `edu.notification.requested.dlq` | `edu.notify.notification.sent.dlq`(自动派生) | ✅ |
**修改文件**
- `internal/config/config.go``KAFKA_NOTIFICATION_TOPIC` 默认值改为 `edu.notify.notification.sent`
- `internal/kafkaconsumer/consumer.go`包注释更新metric label 使用 `event.EventType`(而非硬编码 "notification.requested"
- `internal/config/config_test.go`:断言更新为新 topic 名
### 2.2 鉴权头对齐
| 项 | v1 | v2 | 状态 |
| -------- | -------------------- | -------------------------------------------------------------------- | ---- |
| 规范头名 | `X-Internal-Token` | `X-Internal-Key`ARB-013 对齐 msg | ✅ |
| 向后兼容 | — | 同时接受 `X-Internal-Token`legacy alias | ✅ |
| 环境变量 | `INTERNAL_API_TOKEN` | `PUSH_INTERNAL_TOKEN`canonical+ `INTERNAL_API_TOKEN`fallback | ✅ |
**修改文件**
- `internal/ws/handler.go`
- `internalTokenHeader` 常量改为 `X-Internal-Key`
- 新增 `legacyInternalTokenHeader = "X-Internal-Token"` 兼容别名
- `checkInternalToken()` 依次尝试两个头
- `internal/config/config.go`:优先读取 `PUSH_INTERNAL_TOKEN`,为空时回退到 `INTERNAL_API_TOKEN`
### 2.3 请求体字段命名对齐
| 项 | v1 | v2 | 状态 |
| ------------ | ------------------------ | --------------------------------------------------------- | ---- |
| 用户 ID 字段 | `user_id`snake_case | `userId`camelCasemsg canonical+ `user_id`legacy | ✅ |
| 其他字段 | `event` / `data` / `ttl` | 保持不变 | ✅ |
**修改文件**
- `internal/ws/handler.go` `PushHandler()`
- 结构体同时定义 `UserID json:"userId"``UserIDLeg json:"user_id"`
- 绑定后若 `UserID` 为空则回退到 `UserIDLeg`
### 2.4 环境变量对齐
| v1 环境变量 | v2 规范环境变量 | 兼容性 |
| --------------------------------------------------------------- | ----------------------------------------------------------------- | ----------------------------- |
| `INTERNAL_API_TOKEN` | `PUSH_INTERNAL_TOKEN` | v1 变量作为 fallback 别名保留 |
| `KAFKA_NOTIFICATION_TOPIC`(默认 `edu.notification.requested` | `KAFKA_NOTIFICATION_TOPIC`(默认 `edu.notify.notification.sent` | 环境变量名不变,默认值更新 |
**修改文件**
- `infra/docker-compose.deploy.yml`push-gateway 段使用 `PUSH_INTERNAL_TOKEN`(支持 `${PUSH_INTERNAL_TOKEN:-${INTERNAL_API_TOKEN:-edu-internal-token}}` 回退链)
- `infra/deploy.env.example``PUSH_INTERNAL_TOKEN` 为规范变量,`INTERNAL_API_TOKEN` 标注为兼容别名
### 2.5 部署配置更新
| 文件 | 修改内容 |
| --------------------------------- | ---------------------------------------------------------------------------------------- |
| `infra/docker-compose.deploy.yml` | `KAFKA_NOTIFICATION_TOPIC: edu.notify.notification.sent``PUSH_INTERNAL_TOKEN` 带回退链 |
| `infra/deploy.env.example` | 新增 `PUSH_INTERNAL_TOKEN`,保留 `INTERNAL_API_TOKEN` 作为兼容别名(留空) |
---
## 3. 本地 Docker 验证结果2026-07-14 v2
测试环境:本地 Docker Desktop`edu-full_default` 网络,接入真实 edu-kafka / edu-redis / edu-iam-test。
```
镜像edu/push-gateway:test
容器edu-push-gateway-testDEV_MODE=true, PUSH_INTERNAL_TOKEN=test-internal-token-v2
网络edu-full_default直连 edu-kafka:29092 / edu-redis:6379 / edu-iam-test:3002
```
| 验证项 | 结果 | 说明 |
| --------------------------------------- | ---- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `go vet ./...` | ✅ | 零警告 |
| `go build ./...` | ✅ | 零错误 |
| `go test ./...` | ✅ | config/hub/ws 全部通过(含新增 `TestLoadProdModeLegacyToken` |
| Docker 镜像构建 | ✅ | `edu/push-gateway:test` 构建成功 |
| `/healthz` | ✅ | `{"service":"push-gateway","status":"ok"}` |
| `/readyz` | ✅ | `degraded:false`kafka+redis 均 ok |
| 容器日志确认 topic | ✅ | `kafka_topic=edu.notify.notification.sent` |
| **v2 契约X-Internal-Key + userId** | ✅ | `POST /internal/push` 返回 `{"success":true}` |
| **v1 兼容X-Internal-Token + user_id** | ✅ | 旧调用方式仍正常工作 |
| **Kafka 全链路:新 topic → WS** | ✅ | 发布到 `edu.notify.notification.sent` → WebSocket 客户端收到消息 |
| **考试事件透传3 类)** | ✅ | ExamExtended / ExamForceSubmitted / ExamQuestionReordered 全部经 Kafka → WS 投递成功 |
| Prometheus metrics | ✅ | `push_gateway_kafka_consumed_total{topic="edu.notify.notification.sent"}` 计数正确;`push_gateway_messages_pushed_total{event="ExamExtended",result="delivered"}` 等标签正确 |
### 3.1 测试脚本
| 脚本 | 用途 |
| ---------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `services/push-gateway/scripts/test-kafka-ws.mjs` | 验证单条 Kafka 消息经 `edu.notify.notification.sent` topic 到达 WebSocket 客户端 |
| `services/push-gateway/scripts/test-exam-events.mjs` | 验证 3 类考试事件ExamExtended/ExamForceSubmitted/ExamQuestionReordered的 event_type 透传 |
运行方式(需在仓库根目录):
```bash
# 前置push-gateway 容器已启动并接入 edu-full_default 网络
node services/push-gateway/scripts/test-kafka-ws.mjs
node services/push-gateway/scripts/test-exam-events.mjs
```
---
## 4. 上游依赖(调用 push-gateway 的模块)
### 4.1 4 个前端 Portalteacher/student/parent/admin— P0
| Portal | WebSocket 接入状态 | v2 新增工作 | 状态 |
| -------------- | --------------------------------------------- | ----------- | ---- |
| teacher-portal | ✅ 已接入(`useNotificationsWebSocket` hook | 无 | ✅ |
| student-portal | ✅ 已接入(考试事件监听) | 无 | ✅ |
| parent-portal | ✅ 已接入 | 无 | ✅ |
| admin-portal | ✅ 已接入 | 无 | ✅ |
**说明**4 个 portal 在 v1 阶段已完成 WebSocket 接入,使用 `?token=<jwt>` query param 鉴权。v2 的 API 契约变更Kafka topic / 鉴权头 / 请求体字段)对 portal 透明,无需 portal 侧改动。
### 4.2 3 个 BFFteacher-bff / student-bff / parent-bff— P1
| BFF | 依赖 push-gateway 的方式 | v2 新增工作 | 状态 |
| ----------- | ------------------------------------- | ----------- | ---- |
| teacher-bff | 无直接依赖BFF 不调用 push-gateway | 无 | ✅ |
| student-bff | 无直接依赖BFF 不调用 push-gateway | 无 | ✅ |
| parent-bff | 无直接依赖BFF 不调用 push-gateway | 无 | ✅ |
**说明**BFF 层不直接调用 push-gateway。推送由 msg 服务通过 HTTP `/internal/push` 或 Kafka 发起push-gateway 负责投递到 portal 的 WebSocket。
---
## 5. 下游依赖push-gateway 调用的模块)
### 5.1 iam 服务ai06 负责)— P0
| 依赖项 | 用途 | 状态 |
| ----------------------------------- | ------------------------------------- | --------- |
| `GET /v1/iam/.well-known/jwks.json` | RS256 JWKS 公钥WebSocket JWT 验签) | ✅ 已就绪 |
**环境变量**`JWKS_URL=http://iam:3002/v1/iam/.well-known/jwks.json`
**说明**shared-go/jwks Fetcher 每 5 分钟刷新 JWKS 缓存。DevMode 接受 `dev-token` 旁路 JWT 验签。
### 5.2 msg 服务ai10 负责)— P0
| 依赖项 | 用途 | 状态 |
| -------------------------------------------- | ------------------------------------------------- | --------- |
| HTTP `POST /internal/push` | 定向推送msg → push-gateway | ✅ 已对齐 |
| Kafka topic `edu.notify.notification.sent` | 事件驱动推送msg Outbox → Kafka → push-gateway | ✅ 已对齐 |
| 鉴权头 `X-Internal-Key: PUSH_INTERNAL_TOKEN` | /internal/* API 鉴权 | ✅ 已对齐 |
| 请求体 `{ userId, event, data }` | camelCase 字段命名 | ✅ 已对齐 |
**环境变量**
- push-gateway 侧:`PUSH_INTERNAL_TOKEN`canonical/ `INTERNAL_API_TOKEN`fallback
- msg 侧:`PUSH_INTERNAL_TOKEN`(已对齐)
**v2 对齐结果**4 个不一致点Kafka topic / 鉴权头 / 请求体字段 / 环境变量全部修复push-gateway 同时保持对 v1 调用方式的向后兼容。
### 5.3 Redis基础设施— P0
| 依赖项 | 用途 | 状态 |
| ------------------------ | --------------------------------------------------- | --------- |
| `redis://edu-redis:6379` | 在线状态 SET + 跨实例 Pub/Sub 扇出 + 事件去重 SETNX | ✅ 已就绪 |
**环境变量**`REDIS_URL=redis://edu-redis:6379/0`
### 5.4 Kafka基础设施— P0
| 依赖项 | 用途 | 状态 |
| ----------------- | ----------------------------------------- | --------- |
| `edu-kafka:29092` | 消费 `edu.notify.notification.sent` topic | ✅ 已就绪 |
**环境变量**
- `KAFKA_BROKERS=edu-kafka:29092`
- `KAFKA_NOTIFICATION_TOPIC=edu.notify.notification.sent`ARB-013
- `KAFKA_CONSUMER_GROUP=push-gateway`
### 5.5 OTel Collector基础设施— P1
| 依赖项 | 用途 | 状态 |
| ---------------------------- | --------------- | --------- |
| `http://otel-collector:4318` | OTLP trace 上报 | ⏳ 待部署 |
**环境变量**`OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318`
**说明**:未配置时 push-gateway 跳过 trace 上报,不影响功能。
---
## 6. 待协调事项(需上下游配合)
### 6.1 core-edu 考试事件发布(下游阻塞 student-portal
| 项 | 详情 |
| ------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| 阻塞方 | core-eduai07 |
| 影响方 | student-portal / student-bff |
| 问题 | student-portal 期望接收 `ExamExtended` / `ExamForceSubmitted` / `ExamQuestionReordered` 三类考试事件,但这些事件在 core-edu 源码中未找到发布逻辑 |
| 期望流程 | core-edu Outbox publisher → Kafka `edu.notify.notification.sent` → push-gateway → student-portal WebSocket |
| push-gateway 侧状态 | ✅ 已验证可消费这三类事件并透传到 WebSocket见 §3 测试结果) |
| 协调动作 | 请 core-edu 在考试相关业务逻辑中发布这三类事件到 `edu.notify.notification.sent` topic |
### 6.2 msg 服务 Outbox 发布 topic 对齐(待确认)
| 项 | 详情 |
| ------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| 阻塞方 | msgai10 |
| 问题 | 需确认 msg 的 Outbox publisher 已将发布 topic 从 `edu.notification.requested` 改为 `edu.notify.notification.sent`ARB-013 |
| push-gateway 侧状态 | ✅ 已消费 `edu.notify.notification.sent` |
| 协调动作 | 请 msg 确认 Outbox `TOPIC_MAP``NotificationSent` 事件映射到 `edu.notify.notification.sent`;如仍发往旧 topic需更新 |
### 6.3 生产环境环境变量迁移(部署时)
| 项 | 详情 |
| ------------------- | -------------------------------------------------------------------------------------------------------------- |
| 阻塞方 | SRE AI / 人类决策者 |
| 问题 | 生产环境需将 `INTERNAL_API_TOKEN` 迁移为 `PUSH_INTERNAL_TOKEN`(或在 deploy.env 中同时设置两者) |
| push-gateway 侧状态 | ✅ 支持双环境变量(`PUSH_INTERNAL_TOKEN` 优先,`INTERNAL_API_TOKEN` 回退) |
| 协调动作 | 部署时在 `infra/security/secrets.example.env` 或 K8s Secret 中设置 `PUSH_INTERNAL_TOKEN`;旧变量可保留作为兼容 |
---
## 7. v2 文件变更清单
### 7.1 源码修改
| 文件 | 修改内容 |
| ---------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `services/push-gateway/internal/config/config.go` | Kafka topic 默认值 → `edu.notify.notification.sent`;支持 `PUSH_INTERNAL_TOKEN` 环境变量(带 `INTERNAL_API_TOKEN` 回退) |
| `services/push-gateway/internal/ws/handler.go` | 鉴权头 `X-Internal-Key`canonical+ `X-Internal-Token`legacy请求体 `userId`canonical+ `user_id`legacy |
| `services/push-gateway/internal/kafkaconsumer/consumer.go` | 包注释更新metric label 使用 `event.EventType` 而非硬编码 |
| `services/push-gateway/internal/config/config_test.go` | 断言新 topic 名;新增 `TestLoadProdModeLegacyToken` 验证 fallback |
| `services/push-gateway/main.go` | 启动流程注释更新topic 名 + 鉴权头名) |
### 7.2 部署配置修改
| 文件 | 修改内容 |
| --------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `infra/docker-compose.deploy.yml` | push-gateway 段:`KAFKA_NOTIFICATION_TOPIC: edu.notify.notification.sent``PUSH_INTERNAL_TOKEN` 带回退链 |
| `infra/deploy.env.example` | `PUSH_INTERNAL_TOKEN` 为规范变量;`INTERNAL_API_TOKEN` 标注为兼容别名 |
### 7.3 测试脚本新增
| 文件 | 用途 |
| ---------------------------------------------------- | ---------------------------------------- |
| `services/push-gateway/scripts/test-kafka-ws.mjs` | Kafka → WebSocket 全链路验证(单条消息) |
| `services/push-gateway/scripts/test-exam-events.mjs` | 3 类考试事件透传验证 |
### 7.4 根 package.json
| 文件 | 修改内容 |
| -------------- | --------------------------------------------------------- |
| `package.json` | `devDependencies` 新增 `kafkajs` + `ws`(仅用于测试脚本) |
---
## 8. 环境变量清单v2
| 环境变量 | 必填 | 默认值 | 用途 |
| ----------------------------- | -------- | ------------------------------ | ----------------------------------------------- |
| `PUSH_GATEWAY_PORT` | 否 | `8081` | HTTP 端口 |
| `JWT_SECRET` | 生产必填 | — | JWT HS256 签名密钥DevMode 有默认值) |
| `DEV_MODE` | 否 | `false` | 开发模式(旁路 JWT + InternalToken 校验) |
| `REDIS_URL` | 是 | `redis://localhost:6379/0` | Redis 连接地址 |
| `KAFKA_BROKERS` | 是 | `localhost:9092` | Kafka broker 列表(逗号分隔) |
| `KAFKA_NOTIFICATION_TOPIC` | 否 | `edu.notify.notification.sent` | 消费的 Kafka topicARB-013 |
| `KAFKA_CONSUMER_GROUP` | 否 | `push-gateway` | Kafka 消费者组 |
| `PUSH_INTERNAL_TOKEN` | 生产必填 | — | /internal/* API 鉴权 tokenARB-013 canonical |
| `INTERNAL_API_TOKEN` | 兼容别名 | — | /internal/* API 鉴权 tokenfallbackv1 兼容) |
| `JWKS_URL` | 生产必填 | — | iam JWKS 端点RS256 公钥) |
| `WS_ALLOWED_ORIGINS` | 生产必填 | — | WebSocket 允许的 Origin 白名单(逗号分隔) |
| `MAX_CONNS_PER_USER` | 否 | `5` | 单用户最大连接数 |
| `HEARTBEAT_INTERVAL_SECONDS` | 否 | `30` | 心跳间隔(秒) |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | 否 | `localhost:4318` | OTLP collector 端点 |
| `INSTANCE_ID` | 否 | hostname | 实例标识Redis SET 成员) |
---
## 9. API 契约v2
### 9.1 WebSocket 端点
```
GET /ws?token=<jwt>
```
- 鉴权JWT RS256via iam JWKS或 DevMode `dev-token`
- 消息格式:`{ "type": "message", "event": "<event_type>", "data": {...}, "timestamp": "<RFC3339>" }`
- 心跳RFC 6455 Ping/Pong30s 间隔60s 空闲超时)
### 9.2 内部 HTTP API
| 方法 | 路径 | 鉴权头 | 请求体 | 响应 |
| ---- | --------------------------- | ------------------------- | ------------------------------- | ---------------------------------------------------- |
| POST | `/internal/push` | `X-Internal-Key: <token>` | `{ userId, event, data, ttl? }` | `{ success, delivered?, online?, reached?, error? }` |
| POST | `/internal/broadcast` | `X-Internal-Key: <token>` | `{ event, data, filter? }` | `{ success, reached? }` |
| GET | `/internal/online/<userID>` | `X-Internal-Key: <token>` | — | `{ online: bool, instances: [] }` |
**向后兼容**
- 鉴权头同时接受 `X-Internal-Token`v1 legacy
- `/internal/push` 请求体同时接受 `user_id`snake_casev1 legacy
### 9.3 Kafka 消费
| Topic | 消息格式 | 来源 |
| ---------------------------------- | ---------------------------- | --------------------- |
| `edu.notify.notification.sent` | `NotificationRequested` JSON | msg Outbox publisher |
| `edu.notify.notification.sent.dlq` | 原始消息(失败 3 次后投递) | push-gateway 自动派生 |
**消息字段**`event_id` / `user_id` / `event_type` / `channel` / `title` / `content` / `data` / `broadcast` / `occurred_at`
**event_type 透传**push-gateway 从 Kafka 消息的 `event_type` 字段读取事件类型,作为 WebSocket 消息的 `event` 字段透传,不硬编码事件类型。
---
## 10. 后续工作v3 展望)
| 工作项 | 优先级 | 说明 |
| ------------------- | ------ | ------------------------------------------------------------------------ |
| P6 Reconnect 协议 | P1 | `session_id` + `last_seq` + ring buffer100 条/用户)支持断线重连后补发 |
| P6 离线消息推送 | P2 | 与 msg 服务集成,离线用户消息落库后通过 SMS/邮件/应用内通知补发 |
| Prometheus 告警规则 | P2 | 配置 `push_gateway_kafka_consumed_total` 停止增长告警 |
| Grafana 面板 | P2 | WebSocket 连接数、Kafka 消费延迟、推送成功率面板 |
| 多副本部署验证 | P2 | 验证 Redis Pub/Sub 跨实例扇出在多副本场景下的正确性 |