feat(push-gateway): docker 本地验证通过 + nextstep 上下游依赖文档
- Dockerfile 修复:GOPROXY 国内镜像 + go.mod 路径修正 + 构建目录
- docker-compose.deploy.yml 补全 push-gateway 全部 12 个环境变量
- deploy.env.example 新增 push-gateway 段(INTERNAL_API_TOKEN/WS_ALLOWED_ORIGINS)
- 本地 Docker 验证通过(非 mock):
- /healthz + /readyz(Redis + Kafka 健康)
- WebSocket /ws + dev-token 鉴权
- /internal/push HTTP API 投递 3 类考试事件
- Kafka → push-gateway → WebSocket 全链路透传
ExamExtended / ExamForceSubmitted / ExamQuestionReordered
- 新增 docs/nextstep.md:上游依赖(iam/Redis/Kafka)+
下游依赖(4 portal + msg)+ 事件投递架构图 + 环境变量清单
This commit is contained in:
@@ -31,6 +31,14 @@ ADMIN_PORTAL_PORT=4003
|
|||||||
# ============ Kafka(P3+ 启用,留空则 Outbox publisher 持续重试)============
|
# ============ Kafka(P3+ 启用,留空则 Outbox publisher 持续重试)============
|
||||||
KAFKA_BROKERS=
|
KAFKA_BROKERS=
|
||||||
|
|
||||||
|
# ============ push-gateway(WebSocket 推送服务)============
|
||||||
|
# /internal/push 鉴权 token(msg 服务调用时需携带 X-Internal-Token 头)
|
||||||
|
INTERNAL_API_TOKEN=CHANGE_ME_TO_STRONG_INTERNAL_TOKEN
|
||||||
|
# WebSocket 允许的 Origin 白名单(逗号分隔)
|
||||||
|
WS_ALLOWED_ORIGINS=http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003
|
||||||
|
# push-gateway 对外端口
|
||||||
|
PUSH_GATEWAY_PORT=8081
|
||||||
|
|
||||||
# ============ 可观测性(P6 启用)============
|
# ============ 可观测性(P6 启用)============
|
||||||
# OTLP collector 端点,留空则服务跳过 trace 上报
|
# OTLP collector 端点,留空则服务跳过 trace 上报
|
||||||
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
|
OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
|
||||||
|
|||||||
@@ -279,7 +279,18 @@ services:
|
|||||||
PUSH_GATEWAY_PORT: 8081
|
PUSH_GATEWAY_PORT: 8081
|
||||||
JWT_SECRET: ${JWT_SECRET}
|
JWT_SECRET: ${JWT_SECRET}
|
||||||
REDIS_URL: ${REDIS_URL}
|
REDIS_URL: ${REDIS_URL}
|
||||||
|
KAFKA_BROKERS: ${KAFKA_BROKERS:-kafka:29092}
|
||||||
|
KAFKA_NOTIFICATION_TOPIC: edu.notification.requested
|
||||||
|
KAFKA_CONSUMER_GROUP: push-gateway
|
||||||
|
INTERNAL_API_TOKEN: ${INTERNAL_API_TOKEN:-edu-internal-token}
|
||||||
|
JWKS_URL: http://iam:3002/v1/iam/.well-known/jwks.json
|
||||||
|
WS_ALLOWED_ORIGINS: ${WS_ALLOWED_ORIGINS:-http://localhost:3000,http://localhost:4000,http://localhost:4001,http://localhost:4002,http://localhost:4003}
|
||||||
|
MAX_CONNS_PER_USER: "5"
|
||||||
|
HEARTBEAT_INTERVAL_SECONDS: "30"
|
||||||
|
OTEL_EXPORTER_OTLP_ENDPOINT: ${OTEL_EXPORTER_OTLP_ENDPOINT:-http://otel-collector:4318}
|
||||||
DEV_MODE: "false"
|
DEV_MODE: "false"
|
||||||
|
ports:
|
||||||
|
- "${PUSH_GATEWAY_PORT:-8081}:8081"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8081/healthz"]
|
test: ["CMD", "wget", "--quiet", "--spider", "http://localhost:8081/healthz"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
|
|||||||
@@ -8,19 +8,19 @@ WORKDIR /app
|
|||||||
# git 与 ca-certificates 为 go mod 下载所需
|
# git 与 ca-certificates 为 go mod 下载所需
|
||||||
RUN apk add --no-cache git ca-certificates
|
RUN apk add --no-cache git ca-certificates
|
||||||
|
|
||||||
# 利用 go.work 时需拷贝根 go.work 与 shared-go 包
|
# 拷贝 shared-go 和 push-gateway 的 go.mod(用于 go mod download 缓存层)
|
||||||
COPY go.mod go.sum ./
|
COPY packages/shared-go/go.mod ./packages/shared-go/
|
||||||
COPY packages/shared-go ./packages/shared-go
|
|
||||||
COPY services/push-gateway/go.mod services/push-gateway/go.sum ./services/push-gateway/
|
COPY services/push-gateway/go.mod services/push-gateway/go.sum ./services/push-gateway/
|
||||||
|
ENV GOPROXY=https://goproxy.cn,direct
|
||||||
RUN cd services/push-gateway && go mod download
|
RUN cd services/push-gateway && go mod download
|
||||||
|
|
||||||
# 拷源码并构建(静态编译便于 alpine 运行)
|
# 拷源码并构建(静态编译便于 alpine 运行)
|
||||||
COPY services/push-gateway ./services/push-gateway
|
COPY services/push-gateway ./services/push-gateway
|
||||||
COPY packages/shared-go ./packages/shared-go
|
COPY packages/shared-go ./packages/shared-go
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build \
|
RUN cd services/push-gateway && CGO_ENABLED=0 GOOS=linux go build \
|
||||||
-ldflags="-s -w -X main.serviceName=push-gateway" \
|
-ldflags="-s -w -X main.serviceName=push-gateway" \
|
||||||
-o /app/bin/push-gateway \
|
-o /app/bin/push-gateway \
|
||||||
./services/push-gateway
|
.
|
||||||
|
|
||||||
# ============ Runtime ============
|
# ============ Runtime ============
|
||||||
FROM alpine:3.20 AS runner
|
FROM alpine:3.20 AS runner
|
||||||
|
|||||||
217
services/push-gateway/docs/nextstep.md
Normal file
217
services/push-gateway/docs/nextstep.md
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
# push-gateway 下一步工作与上下游依赖
|
||||||
|
|
||||||
|
> 模块:push-gateway(WebSocket 实时推送网关,L3 基础设施)
|
||||||
|
> 负责人:ai09
|
||||||
|
> 更新日期:2026-07-13
|
||||||
|
> 关联文档:[02-architecture-design.md](./02-architecture-design.md) · [push-gateway_workline.md](../../../docs/architecture/issues/worklines/push-gateway_workline.md)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 模块当前状态
|
||||||
|
|
||||||
|
push-gateway 的 P1–P6 全部批次已完成并经本地 Docker 验证通过(非 mock 数据)。
|
||||||
|
|
||||||
|
| 能力 | 状态 | 验证方式 |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| WebSocket `/ws` 端点(JWT RS256 + DevMode dev-token) | ✅ 完成 | Docker 容器本地测试 |
|
||||||
|
| `/internal/push` HTTP API(X-Internal-Token 鉴权) | ✅ 完成 | curl + WS 客户端联调 |
|
||||||
|
| `/internal/broadcast` 广播 API | ✅ 完成 | 代码实现 + 单元测试 |
|
||||||
|
| `/internal/online/:userID` 在线查询 | ✅ 完成 | curl 验证 |
|
||||||
|
| Kafka 消费 `edu.notification.requested` | ✅ 完成 | Kafka producer → WS 客户端联调 |
|
||||||
|
| Redis Pub/Sub 跨实例消息扇出 | ✅ 完成 | 容器连接 edu-redis 验证 |
|
||||||
|
| Redis 在线状态 SET + 启动重建(ISSUE-058) | ✅ 完成 | /readyz 返回 redis ok |
|
||||||
|
| P6 Reconnect 协议(session_id + last_seq + ring buffer 100 条/用户) | ✅ 完成 | 代码实现 + 单元测试 |
|
||||||
|
| 幂等性(event_id Redis SETNX 24h 去重) | ✅ 完成 | Kafka 重复消息测试 |
|
||||||
|
| /healthz + /readyz(软失败探活) | ✅ 完成 | curl 验证 |
|
||||||
|
| /metrics Prometheus 指标 | ✅ 完成 | curl 验证 |
|
||||||
|
| Docker 多阶段构建(golang:1.25-alpine → alpine:3.20) | ✅ 完成 | docker build 成功 |
|
||||||
|
| 事件透传(event_type 字段无需硬编码) | ✅ 完成 | 3 类考试事件 Kafka→WS 验证 |
|
||||||
|
|
||||||
|
### 1.1 本地 Docker 验证结果(2026-07-13)
|
||||||
|
|
||||||
|
测试环境:本地 Docker(edu-redis + edu-kafka + edu-push-gateway-test 容器,均接入 `edu-full_default` 网络)
|
||||||
|
|
||||||
|
```
|
||||||
|
镜像:edu/push-gateway:test(63.2MB)
|
||||||
|
容器:edu-push-gateway-test(DEV_MODE=true)
|
||||||
|
|
||||||
|
测试 1:健康检查
|
||||||
|
GET /healthz → {"service":"push-gateway","status":"ok"} ✅
|
||||||
|
GET /readyz → {"status":"ok","degraded":false,"dependencies":{"kafka":{"ok":true},"redis":{"ok":true}}} ✅
|
||||||
|
|
||||||
|
测试 2:HTTP /internal/push → WebSocket 投递
|
||||||
|
WS 客户端连接 ws://localhost:8081/ws?token=dev-token → 连接成功 ✅
|
||||||
|
POST /internal/push × 3(ExamExtended / ExamForceSubmitted / ExamQuestionReordered)
|
||||||
|
→ 三个事件均 delivered=true,WS 客户端全部收到 ✅
|
||||||
|
|
||||||
|
测试 3:Kafka → WebSocket 投递
|
||||||
|
Kafka producer → topic edu.notification.requested × 3 条消息
|
||||||
|
→ push-gateway 消费 → WS 客户端全部收到 ✅
|
||||||
|
指标:push_gateway_kafka_consumed_total = 3 ✅
|
||||||
|
|
||||||
|
测试 4:在线状态查询
|
||||||
|
GET /internal/online/dev-user(无连接) → {"online":false} ✅
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 上游依赖(push-gateway 依赖谁)
|
||||||
|
|
||||||
|
push-gateway 作为 L3 基础设施网关,运行时依赖以下服务/组件:
|
||||||
|
|
||||||
|
### 2.1 iam(ai01)— JWT RS256 公钥
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | iam `/.well-known/jwks.json` 端点提供 RS256 公钥 |
|
||||||
|
| 端点 | `GET http://iam:3002/v1/iam/.well-known/jwks.json` |
|
||||||
|
| 用途 | WebSocket `/ws` 连接时校验客户端 JWT 签名(RS256) |
|
||||||
|
| 当前状态 | DevMode 下使用 `dev-token` 旁路,生产环境需要 iam 就绪 |
|
||||||
|
| 缓存策略 | shared-go/jwks Fetcher 每 5 分钟刷新 JWKS 缓存 |
|
||||||
|
| 环境变量 | `JWKS_URL=http://iam:3002/v1/iam/.well-known/jwks.json` |
|
||||||
|
|
||||||
|
### 2.2 Redis(基础设施)— 在线状态 + 跨实例消息扇出
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | Redis SET(在线用户→实例映射)+ Pub/Sub(跨实例消息扇出) |
|
||||||
|
| 端点 | `redis://edu-redis:6379` |
|
||||||
|
| 用途 | 1) 多实例在线状态同步 2) 跨实例 WebSocket 消息投递 3) event_id 幂等去重(SETNX 24h TTL) |
|
||||||
|
| 当前状态 | ✅ 本地 Docker edu-redis 已就绪并验证通过 |
|
||||||
|
| 环境变量 | `REDIS_URL=redis://edu-redis:6379` |
|
||||||
|
| 降级策略 | Redis 不可用时 /readyz 标记 degraded 但不返回 503(软失败,ARB-015 §17.4) |
|
||||||
|
|
||||||
|
### 2.3 Kafka(基础设施)— 通知事件消费
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | Kafka topic `edu.notification.requested` 消费 |
|
||||||
|
| 端点 | `kafka:29092`(容器内 INSIDE listener) |
|
||||||
|
| 用途 | 消费 msg 服务发出的 NotificationRequested 事件,投递到在线用户的 WebSocket |
|
||||||
|
| 当前状态 | ✅ 本地 Docker edu-kafka 已就绪并验证通过 |
|
||||||
|
| 环境变量 | `KAFKA_BROKERS=kafka:29092`、`KAFKA_NOTIFICATION_TOPIC=edu.notification.requested`、`KAFKA_CONSUMER_GROUP=push-gateway` |
|
||||||
|
| 消费策略 | Consumer Group `push-gateway`,StartOffset=LastOffset(首次启动跳过历史积压) |
|
||||||
|
| 幂等性 | event_id Redis SETNX 去重,24h TTL |
|
||||||
|
| DLQ | 失败 3 次后投递到 `edu.notification.requested.dlq` |
|
||||||
|
| 降级策略 | Kafka 不可用时 /readyz 标记 degraded,HTTP /internal/push 仍可用 |
|
||||||
|
|
||||||
|
### 2.4 OTel Collector(基础设施)— 可观测性
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | OpenTelemetry trace 上报 |
|
||||||
|
| 端点 | `http://otel-collector:4318`(本地使用 edu-jaeger:4318) |
|
||||||
|
| 当前状态 | 可选依赖,初始化失败时 tracing 自动禁用(不影响业务) |
|
||||||
|
| 环境变量 | `OTEL_EXPORTER_OTLP_ENDPOINT` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 下游依赖(谁依赖 push-gateway)
|
||||||
|
|
||||||
|
以下 4 个前端 portal 模块和 1 个后端服务依赖 push-gateway:
|
||||||
|
|
||||||
|
### 3.1 teacher-portal(ai13)— WebSocket 通知通道
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | WebSocket `/ws` 端点,用于 `/notifications` 页面实时推送 |
|
||||||
|
| 端点 | `ws://push-gateway:8081/ws?token=<JWT>` |
|
||||||
|
| 事件类型 | 通知类事件(由 msg 服务通过 Kafka 发出,push-gateway 透传) |
|
||||||
|
| 当前状态 | ✅ push-gateway 已就绪,等待 teacher-portal 切换 MSW mock → 真实 WS |
|
||||||
|
| portal 需求来源 | `apps/teacher-portal/nextstep.md` §2.3 |
|
||||||
|
|
||||||
|
### 3.2 student-portal(ai14)— 考试实时事件
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | WebSocket `/ws` 端点,接收考试实时事件 |
|
||||||
|
| 端点 | `ws://push-gateway:8081/ws?token=<JWT>` |
|
||||||
|
| 需要的事件类型 | `ExamExtended`(教师延长考试)、`ExamForceSubmitted`(教师强制收卷)、`ExamQuestionReordered`(教师调整题目顺序) |
|
||||||
|
| 当前状态 | ✅ 已验证:3 类考试事件经 Kafka → push-gateway → WebSocket 全链路投递成功 |
|
||||||
|
| 事件来源 | msg 服务(ai10)通过 Outbox → Kafka `edu.notification.requested` 发出 |
|
||||||
|
| 透传机制 | push-gateway 从 Kafka 消息的 `event_type` 字段读取事件类型,作为 WebSocket 消息的 `event` 字段透传,**无需在 push-gateway 中硬编码事件类型** |
|
||||||
|
| portal 需求来源 | `apps/student-portal/docs/nextstep.md` §3 |
|
||||||
|
|
||||||
|
### 3.3 parent-portal(ai15)— WebSocket 通知
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | WebSocket `/ws` 端点 |
|
||||||
|
| 当前状态 | ✅ push-gateway /ws 已标记就绪(parent-portal nextstep.md §6 确认) |
|
||||||
|
| portal 需求来源 | `apps/parent-portal/docs/nextstep.md` §6 |
|
||||||
|
|
||||||
|
### 3.4 admin-portal(ai16)— WebSocket 实时通知联调
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | WebSocket `/ws` 端点,管理端实时通知联调 |
|
||||||
|
| 当前状态 | ✅ push-gateway 已就绪,等待 admin-portal 联调 |
|
||||||
|
| portal 需求来源 | `apps/admin-portal/docs/nextstep.md` §3.5 |
|
||||||
|
|
||||||
|
### 3.5 msg 服务(ai10)— /internal/push HTTP API
|
||||||
|
|
||||||
|
| 项 | 内容 |
|
||||||
|
| --- | --- |
|
||||||
|
| 依赖内容 | POST `/internal/push`(定向推送)、POST `/internal/broadcast`(广播)、GET `/internal/online/:userID`(在线查询) |
|
||||||
|
| 端点 | `http://push-gateway:8081/internal/*` |
|
||||||
|
| 鉴权 | `X-Internal-Token` 头(生产环境),DevMode 跳过 |
|
||||||
|
| 当前状态 | ✅ 全部 API 已实现并验证 |
|
||||||
|
| 环境变量(msg 侧) | `PUSH_GATEWAY_URL=http://push-gateway:8081` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 事件投递架构(portal 关注)
|
||||||
|
|
||||||
|
```
|
||||||
|
┌──────────────┐ gRPC/HTTP ┌──────────┐ Outbox→Kafka ┌─────────────┐
|
||||||
|
│ core-edu │ ──────────────────→ │ msg │ ──────────────→ │ Kafka │
|
||||||
|
│ (考试业务) │ ExamExtended 等 │ (ai10) │ edu.notification│ (edu-kafka) │
|
||||||
|
└──────────────┘ └──────────┘ .requested └──────┬──────┘
|
||||||
|
│ consume
|
||||||
|
▼
|
||||||
|
┌──────────────┐ WS /ws?token=JWT ┌──────────────────┐ Redis ┌─────────────┐
|
||||||
|
│ 4× portal │ ←────────────────── │ push-gateway │ ←────────→ │ edu-redis │
|
||||||
|
│ (前端) │ 事件透传(event字段) │ (ai09, 本模块) │ Pub/Sub │ (在线状态) │
|
||||||
|
└──────────────┘ └──────────────────┘ └─────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
**关键设计**:push-gateway 不硬编码事件类型。Kafka 消息中的 `event_type` 字段被原样作为 WebSocket 消息的 `event` 字段透传给前端。前端根据 `event` 字段路由处理。这意味着:
|
||||||
|
- 新增事件类型无需修改 push-gateway
|
||||||
|
- msg 服务只需在 Kafka 消息中设置正确的 `event_type`
|
||||||
|
- 前端在 WebSocket 消息回调中按 `event` 字段分发
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 环境变量清单(Docker 部署)
|
||||||
|
|
||||||
|
| 变量 | 必填 | 示例值 | 说明 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| `PUSH_GATEWAY_PORT` | 是 | `8081` | HTTP 监听端口 |
|
||||||
|
| `JWT_SECRET` | 生产必填 | — | DevMode 自动使用 dev 默认值 |
|
||||||
|
| `DEV_MODE` | 否 | `false` | true 时跳过 JWT/InternalToken 校验 |
|
||||||
|
| `REDIS_URL` | 是 | `redis://edu-redis:6379` | Redis 连接地址 |
|
||||||
|
| `KAFKA_BROKERS` | 是 | `kafka:29092` | Kafka broker 地址(逗号分隔) |
|
||||||
|
| `KAFKA_NOTIFICATION_TOPIC` | 是 | `edu.notification.requested` | 消费的 Kafka topic |
|
||||||
|
| `KAFKA_CONSUMER_GROUP` | 是 | `push-gateway` | Consumer Group ID |
|
||||||
|
| `INTERNAL_API_TOKEN` | 生产必填 | — | /internal/* API 鉴权 token |
|
||||||
|
| `JWKS_URL` | 是 | `http://iam:3002/v1/iam/.well-known/jwks.json` | iam JWKS 端点 |
|
||||||
|
| `WS_ALLOWED_ORIGINS` | 是 | `http://localhost:3000,...` | WebSocket 允许的 Origin 白名单 |
|
||||||
|
| `MAX_CONNS_PER_USER` | 否 | `5` | 每用户最大连接数 |
|
||||||
|
| `HEARTBEAT_INTERVAL_SECONDS` | 否 | `30` | 心跳间隔 |
|
||||||
|
| `OTEL_EXPORTER_OTLP_ENDPOINT` | 否 | `http://otel-collector:4318` | OTLP 上报端点 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 剩余工作
|
||||||
|
|
||||||
|
push-gateway 模块自身功能已全部完成,无剩余开发任务。以下为联调阶段事项:
|
||||||
|
|
||||||
|
| 任务 | 负责方 | 依赖 | 状态 |
|
||||||
|
| --- | --- | --- | --- |
|
||||||
|
| teacher-portal 切换 MSW mock → 真实 WS | teacher-portal(ai13) | push-gateway 就绪 ✅ | 待 portal 执行 |
|
||||||
|
| student-portal 接入考试事件 WS | student-portal(ai14) | push-gateway 就绪 ✅ + msg 发出考试事件 | 待 msg + portal 联调 |
|
||||||
|
| parent-portal 接入 WS | parent-portal(ai15) | push-gateway 就绪 ✅ | 待 portal 执行 |
|
||||||
|
| admin-portal WS 实时通知联调 | admin-portal(ai16) | push-gateway 就绪 ✅ | 待 portal 执行 |
|
||||||
|
| msg 服务接入 /internal/push | msg(ai10) | push-gateway 就绪 ✅ | 待 msg 执行 |
|
||||||
|
| iam JWKS 端点就绪 | iam(ai01) | — | 生产环境必需,DevMode 可旁路 |
|
||||||
|
| Redis 生产部署 | SRE | — | P6.1 限流迁移依赖 |
|
||||||
Reference in New Issue
Block a user