Files
Edu/services/push-gateway/docs/01-understanding.md
SpecialX a0f6c228af docs(push-gateway): ai01 阶段1+2 模块理解确认书与架构设计文档
阶段1交付:services/push-gateway/docs/01-understanding.md
- 8节:架构位置/限界上下文/契约/技术栈/阶段归属P5骨架/审计
- 审计16项差距(6高/5中/5低)

阶段2交付:services/push-gateway/docs/02-architecture-design.md
- 13节:分层图/Connection+Hub模型/Redis key/WS端点
- 双通道协议/生命周期/心跳RFC6455/多实例PubSub
- 容量目标10w+连接

AI Agent: ai01 (api-gateway/push-gateway)
Branch: main
Coordinator: coord
2026-07-09 17:22:23 +08:00

162 lines
14 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
> AIai01Go 网关层)
> 阶段:阶段 1 交付物
> 日期2026-07-09
> 关联:[004 架构影响地图](../../../docs/architecture/004_architecture_impact_map.md)、[AI 分配方案](../../../docs/architecture/ai-allocation.md)
---
## 1. 我在架构中的位置
- **层级**L3 网关层004 §3.1 六层架构,与 api-gateway 并列)
- **上游**
- 4 个微前端(浏览器/移动端通过 WebSocket 长连接接入)
- msg 服务(通过 HTTP `/internal/push` `/internal/broadcast` 调用)
- **下游**
- msg 服务P5 设计重点之一是与 msg 的 gRPC 推送通道协议;当前骨架走 HTTP
- iam 服务(拉取 JWKS 公钥校验 JWTP2 起)
- Redis跨实例 Pub/Sub 广播P5 待实现)
- **通信方式**
- 入口WebSocket`gorilla/websocket`+ HTTP内部 API
- 出口HTTP调 msg/ Redis Pub/Sub跨实例广播
- **不持有业务状态**:仅持有 WebSocket 连接池in-memory `map[userID]map[*Connection]bool`),无 DB
## 2. 我的限界上下文
- **我负责**
- WebSocket 长连接生命周期(认证 → 心跳 → 断线清理)
- 在线用户 session 映射userID → 多个 WebSocket 连接)
- 接收 msg 服务的推送请求,投递到目标用户的在线连接
- 跨实例广播P5 待实现Redis Pub/Sub
- **聚合/实体**:无业务聚合,仅 `Connection` 值对象UserID + conn + send chan
- **业务领域**:不属于 D1-D6属于基础设施层
- **我不负责**
- 通知业务逻辑msg 服务负责:通知模板、已读/未读、通知偏好)
- 推送内容持久化msg 服务落 MySQL
- 离线推送msg 服务通过 SMS/邮件渠道补推)
- JWT 签发iam 服务签发,本服务只校验)
## 3. 我与外部的契约
### 3.1 我消费的 proto message
| proto | message | 用途 |
| --------- | -------------- | ----------------------------------------------------------------------------------- |
| msg.proto | `Notification` | 接收 msg 服务的推送请求时,结构对齐(虽走 HTTP JSON但字段与 `Notification` 一致) |
| iam.proto | `UserInfo` | P2 起 RS256 校验 JWT通过 JWKS 端点,非 gRPC |
> 当前骨架用 HTTP JSON 推送(`{userId, event, data}`**ai-allocation §5 设计重点要求**与 msg 的 gRPC 推送通道协议。阶段 2 设计文档须明确 gRPC 契约(`PushService.PushToUser` / `PushService.Broadcast`)。
### 3.2 我暴露的 API 端点
| 方法 | 路径 | 鉴权 | 说明 |
| ---- | --------------------- | ------------------------------------------- | ------------------------------ |
| GET | `/healthz` | 无 | liveness |
| GET | `/readyz` | 无(待实现) | readiness应检查 Redis 连接) |
| GET | `/ws?token=JWT` | JWTquery 参数或 `Authorization: Bearer` | WebSocket 升级端点 |
| POST | `/internal/push` | 待补(内部 API应校验调用方身份 | msg 服务定向推送 |
| POST | `/internal/broadcast` | 待补 | msg 服务广播 |
| GET | `/metrics` | 无(待实现) | Prometheus 指标 |
### 3.3 我发布/消费的 Kafka 事件
**当前**:无(骨架用 HTTP `/internal/*`)。
**P5 设计目标**ai-allocation §5
- **消费**`edu.notification.events`msg 服务发布的 `NotificationRequested` 事件),消费后投递到对应用户的在线连接
- **不发布**任何 Kafka 事件(推送结果通过 gRPC response 返 msg
> 阶段 2 设计文档须明确:是 HTTP `/internal/push` 还是 Kafka 消费?建议**双通道**
>
> - HTTP `/internal/push`msg 同步调用,需立即知道推送结果(用户在线/离线)
> - Kafka 消费:广播类通知(全校广播),解耦 + 削峰
### 3.4 错误码前缀
| 错误码 | HTTP | 触发条件 |
| ----------------- | ---- | --------------------------------------------------- |
| `UNAUTHORIZED` | 401 | 缺失 token / token 无效 / claims 解析失败 |
| `INVALID_REQUEST` | 400 | 请求体 JSON 解析失败 |
| `INVALID_PAYLOAD` | 400 | event/data 字段缺失或格式错误 |
| `PUSH_FAILED` | 500 | 投递失败(当前不会触发,`SendToUser` 静默返回 nil |
| `INTERNAL_ERROR` | 500 | panic 兜底 |
## 4. 我的技术栈
- **语言**Go 1.22+go.mod 声明 1.25.0,需与 Dockerfile 对齐)
- **框架**Gin v1.12.0
- **核心依赖**
- `github.com/gorilla/websocket` v1.5.3WebSocket
- `github.com/golang-jwt/jwt/v5` v5.2.1JWT 校验)
- `go.opentelemetry.io/otel` v1.44.0 + `otelgin` v0.69.0
- **存储**:无 DBRedisP5 待引入,用于跨实例 Pub/Sub
- **构建**Dockerfile 单阶段builder→runner 但未做非 root 用户、healthcheck、ldflags 优化)
## 5. 我的阶段归属
- **阶段**P5沟通与 AI 阶段)
- **当前阶段目标**pending-features §P5
- WebSocket 长连接管理 + 消费 Kafka 广播 + Redis PubSub 跨实例同步
- 单节点支撑 10w+ 连接
- 全校广播推送实时到达
- **依赖上游阶段产出**
- P2 iam 服务JWT RS256 签发 + JWKS 端点)
- P5 msg 服务(推送请求发起方)
- P1 Redis 基础设施(已就绪)
## 6. 我需要对齐的黄金模板项(对照 classes 服务)
| 项 | classes | push-gateway 现状 | 差距 |
| ----------------- | ------------------------------ | ----------------------------------------- | ------------------------------------ |
| 权限装饰器 | `@RequirePermission()` | N/A基础设施JWT 校验 + 内部 API 鉴权) | ⚠️ 内部 API `/internal/*` 当前无鉴权 |
| 错误码前缀 | `CLASSES_*` | 无前缀 | ⚠️ 待统一为 `PUSH_*` |
| logger | pino | ❌ 标准 `log` | ⚠️ 待补 `log/slog` |
| metrics | `/metrics` | ❌ 无 | ⚠️ 待补:连接数/推送 QPS/心跳 |
| tracer | OTel SDK | ✅ | ✅ 对齐 |
| `/healthz` | ✅ | ✅ | ✅ 对齐 |
| `/readyz` | ✅ 检查 DB | ❌ 无 | ⚠️ 待补:检查 Redis 连接 |
| 优雅关闭 | SIGTERM → app.close() | ✅ 5s 超时 | ⚠️ 但未关闭所有 WebSocket 连接 |
| 测试覆盖率 | ≥ 80% | 0% | ⚠️ 待补 hub/ws/config 测试 |
| Dockerfile | 多阶段 + 非 root + healthcheck | ❌ 单阶段 + root + 无 healthcheck | ⚠️ 待重构 |
| Zod 输入验证 | `safeParse` | ✅ `ShouldBindJSON` 等价 | ✅ 对齐 |
| GlobalErrorFilter | `GlobalErrorFilter` | ⚠️ `gin.Recovery()` 默认 | ⚠️ 待自定义 JSON 信封错误 |
## 7. 服务审计表
| 服务 | 权限装饰器 | 错误码前缀 | logger | metrics | tracer | /healthz | /readyz | 优雅关闭 | 测试覆盖率 | Dockerfile |
| ------------ | ------------------ | ---------- | ----------- | ------- | ------- | -------- | ------- | ---------- | ---------- | ---------- |
| push-gateway | ⚠️ 内部 API 无鉴权 | ❌ 无前缀 | ❌ 标准 log | ❌ 无 | ✅ OTel | ✅ | ❌ 无 | ⚠️ 未关 WS | 0% | ❌ 单阶段 |
### 7.1 详细问题清单(按严重度排序)
| # | 严重度 | 文件 | 问题 | 修复建议 |
| --- | ------ | ------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| 1 | 高 | 全局 | 无 Redis Pub/Sub多实例部署时用户连接分散在各自实例跨实例推送失败 | 引入 Redis Pub/Sub本实例订阅 `edu.push.user.<userID>` channel跨实例推送通过 PUBLISH 投递 |
| 2 | 高 | `internal/ws/handler.go` L22-26 | `CheckOrigin` 永远返回 trueCSRF/Origin 校验缺失 | 从环境变量 `WS_ALLOWED_ORIGINS` 读取白名单 |
| 3 | 高 | `internal/ws/handler.go` L67-75 | 心跳用文本消息 `ping/pong`,不符合 RFC 6455 控制帧规范;无空闲超时,僵尸连接永久占用 | 改用 `conn.SetReadDeadline` + WebSocket Ping/Pong 控制帧60s 无心跳关闭连接 |
| 4 | 高 | `internal/ws/handler.go` L19 | `Send` 通道满时静默丢弃,无指标无日志 | 增加 `messages_dropped_total` 指标 + slog.Warn |
| 5 | 高 | `internal/hub/hub.go` L44-57 | 无单用户最大连接数限制DoS 风险 | 限制每用户 ≤ 5 连接,超限拒绝新连接或踢掉最旧 |
| 6 | 高 | `main.go` L44-46 | `/internal/*` 无鉴权,内网任何服务可调用 | 引入 `X-Internal-Token` 头校验(与 msg 服务共享密钥) |
| 7 | 中 | `internal/ws/handler.go` L75 | 无 reconnect 协议,客户端断线后无法恢复未送达消息 | 协议层支持 `session_id` + `last_seq`,重连后 msg 服务补推(与 msg 协调) |
| 8 | 中 | 全局 | HS256 而非 RS256 | P2 起改 RS256与 api-gateway 共用 JWKS 拉取逻辑 |
| 9 | 中 | `internal/config/config.go` | `getEnvBool``=="true"` 字符串比较,不健壮 | 用 `strconv.ParseBool` |
| 10 | 中 | `Dockerfile` | 单阶段构建、root 用户、无 healthcheck、无 ldflags | 重构为多阶段 + 非 root + `wget /healthz` healthcheck |
| 11 | 中 | `main.go` L66-71 | 优雅关闭未关闭所有 WebSocket 连接 | Hub 新增 `CloseAll()` 方法Shutdown 前调用 |
| 12 | 中 | 全局 | 无 `/metrics` 端点 | 新增 `metrics.go`:连接数 Gauge / 推送 QPS Counter / 心跳 Counter / 丢弃 Counter |
| 13 | 低 | `go.mod` L3 vs `Dockerfile` L1 | go.mod `1.25.0` vs Dockerfile `golang:1.22-alpine` | 统一 |
| 14 | 低 | `internal/hub/hub.go` L17-23 | `Send` 非阻塞丢弃,但 `Outgoing` 暴露 chan 给外部读,封装不彻底 | 改为 `Connection` 内部启动写协程,外部只调 `Send` |
| 15 | 低 | `README.md` L17 | 写 "Gin 1.10",实际 go.mod 是 v1.12.0 | 修正 |
| 16 | 低 | 测试 | 0% 覆盖率 | 补 hub_test.go注册/注销/推送/广播、ws_test.go鉴权/心跳) |
## 8. 风险与假设
- **假设**P5 msg 服务会通过 gRPC 或 HTTP 调用本服务推送(双通道待设计文档定)
- **假设**Redis 在 P5 部署时可用P1 已就绪)
- **风险**:单实例 in-memory 连接池在水平扩展时失效,必须 Redis Pub/Sub
- **风险**:僵尸连接耗尽资源(无空闲超时)
- **风险**:内部 API 无鉴权,内网穿透后可被滥用
- **未决**:是否在 push-gateway 消费 Kafka 广播?建议是,避免 msg 服务同步调用造成推送延迟
- **未决**reconnect 协议是否在本期实现建议本期仅做基础协议reconnect 协议作为 P6 硬化项