# 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...` | ✅ | | 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`(camelCase,msg 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-test(DEV_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 个前端 Portal(teacher/student/parent/admin)— P0 | Portal | WebSocket 接入状态 | v2 新增工作 | 状态 | | -------------- | --------------------------------------------- | ----------- | ---- | | teacher-portal | ✅ 已接入(`useNotificationsWebSocket` hook) | 无 | ✅ | | student-portal | ✅ 已接入(考试事件监听) | 无 | ✅ | | parent-portal | ✅ 已接入 | 无 | ✅ | | admin-portal | ✅ 已接入 | 无 | ✅ | **说明**:4 个 portal 在 v1 阶段已完成 WebSocket 接入,使用 `?token=` query param 鉴权。v2 的 API 契约变更(Kafka topic / 鉴权头 / 请求体字段)对 portal 透明,无需 portal 侧改动。 ### 4.2 3 个 BFF(teacher-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-edu(ai07) | | 影响方 | 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 对齐(待确认) | 项 | 详情 | | ------------------- | ---------------------------------------------------------------------------------------------------------------------------- | | 阻塞方 | msg(ai10) | | 问题 | 需确认 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 topic(ARB-013) | | `KAFKA_CONSUMER_GROUP` | 否 | `push-gateway` | Kafka 消费者组 | | `PUSH_INTERNAL_TOKEN` | 生产必填 | — | /internal/* API 鉴权 token(ARB-013 canonical) | | `INTERNAL_API_TOKEN` | 兼容别名 | — | /internal/* API 鉴权 token(fallback,v1 兼容) | | `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 RS256(via iam JWKS)或 DevMode `dev-token` - 消息格式:`{ "type": "message", "event": "", "data": {...}, "timestamp": "" }` - 心跳:RFC 6455 Ping/Pong(30s 间隔,60s 空闲超时) ### 9.2 内部 HTTP API | 方法 | 路径 | 鉴权头 | 请求体 | 响应 | | ---- | --------------------------- | ------------------------- | ------------------------------- | ---------------------------------------------------- | | POST | `/internal/push` | `X-Internal-Key: ` | `{ userId, event, data, ttl? }` | `{ success, delivered?, online?, reached?, error? }` | | POST | `/internal/broadcast` | `X-Internal-Key: ` | `{ event, data, filter? }` | `{ success, reached? }` | | GET | `/internal/online/` | `X-Internal-Key: ` | — | `{ online: bool, instances: [] }` | **向后兼容**: - 鉴权头同时接受 `X-Internal-Token`(v1 legacy) - `/internal/push` 请求体同时接受 `user_id`(snake_case,v1 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 buffer(100 条/用户)支持断线重连后补发 | | P6 离线消息推送 | P2 | 与 msg 服务集成,离线用户消息落库后通过 SMS/邮件/应用内通知补发 | | Prometheus 告警规则 | P2 | 配置 `push_gateway_kafka_consumed_total` 停止增长告警 | | Grafana 面板 | P2 | WebSocket 连接数、Kafka 消费延迟、推送成功率面板 | | 多副本部署验证 | P2 | 验证 Redis Pub/Sub 跨实例扇出在多副本场景下的正确性 |