# push-gateway 对接契约 > 负责人:ai02 > 关联:[matrix.md](./matrix.md)、[msg.proto](../../../packages/shared-proto/proto/msg.proto)、[events.proto](../../../packages/shared-proto/proto/events.proto)、[02-architecture-design.md](../../../services/push-gateway/docs/02-architecture-design.md) > 版本:v2(2026-07-10,对齐总裁裁决 ISSUE-053/055/056/058 + 02 文档 + 现码) > 变更摘要:① topic 改 `edu.notification.requested`(ISSUE-053);② /internal/send → /internal/push(对齐代码 + 总裁 §4.2);③ 鉴权 mTLS → X-Internal-Token(对齐总裁 §7.2);④ 移除 /sse(待 ISSUE-001 仲裁);⑤ 新增 /internal/online 端点 --- ## §1 我提供什么(对外接口) ### 1.1 gRPC 接口(如有) **无对外 gRPC**。push-gateway 是 WebSocket 推送入口,仅通过 HTTP /internal/* 接收 msg 服务调用。 > 协议选型决策(coord 已采纳 P1,见 [02 文档 §5.4](../../../services/push-gateway/docs/02-architecture-design.md)):HTTP /internal/* + Kafka 双通道,不走 gRPC。理由:推送结果需同步返回(delivered/online),HTTP 同步响应更直接;广播走 Kafka 解耦。 ### 1.2 HTTP 端点 | Method | Path | 用途 | 认证 | 请求体 | 响应 | | ------ | --------------------------- | ---------------------------------- | ----------------------------------------------- | ------------------------------ | ----------------------------------------------------------- | | GET | /ws | WebSocket 升级端点(实时推送通知) | JWT RS256(query `?token=` 或 `Authorization`) | — | 升级为 WebSocket 长连接 | | POST | /internal/push | 内部单推接口(msg 服务触发) | `X-Internal-Token` 头 | `{user_id, event, data, ttl?}` | `{success, delivered, online}` | | POST | /internal/broadcast | 内部广播接口(msg 服务触发) | `X-Internal-Token` 头 | `{event, data, filter?}` | `{success, reached}` | | GET | /internal/online/\ | 查询用户在线状态 | `X-Internal-Token` 头 | — | `{online: bool, instances: []}` | | GET | /healthz | 健康检查(liveness) | 公开 | — | `{status:"ok", service, version, connections}` | | GET | /readyz | 就绪检查(readiness) | 公开 | — | `{status, degraded?}`(Redis/Kafka 软失败时 degraded:true) | | GET | /metrics | Prometheus 指标端点 | 公开(内网) | — | Prometheus 文本格式 | **SSE 端点裁决**(ARB-015 §17.3,ISSUE-001 已裁决): - ❌ 不支持 `GET /sse`(仅 WebSocket /ws;删除 contract v1 的 /sse 行,02 文档 §10 正确) - 理由:简化实现 + 统一推送通道 + WebSocket 双向通信能力更强 **鉴权头裁决**(ARB-015 §17.3,ISSUE-002 已裁决): - 统一采用 `X-Internal-Token` 头(对齐总裁裁决 §7.2) - 现码 `X-Internal-Key` 需 ai02 统一为 `X-Internal-Token` **`X-Internal-Token` 校验机制**: - 启动时从 `INTERNAL_API_TOKEN` 环境变量加载 - 与 msg 服务共享同一密钥(K8s Secret 注入) - DevMode(`DEV_MODE=true`)下跳过校验,便于本地联调 - 缺失/不匹配 → 401 + `PUSH_UNAUTHORIZED` **响应语义**(/internal/push): - `delivered: true`:本实例或跨实例成功投递到至少一个连接 - `online: false`:用户离线,msg 应走离线推送(SMS/邮件) - `delivered: false, online: true`:投递失败(连接满/异常),msg 应重试或落库 ### 1.3 GraphQL schema(如 BFF) 不适用。push-gateway 非 BFF。 ### 1.4 Kafka 事件发布(如有) **无**。push-gateway 不发布任何 Kafka 事件,仅消费事件触发推送。推送结果通过 HTTP /internal/push 同步响应返 msg 服务。 ### 1.5 错误码前缀 `PUSH_`(见 [matrix.md](./matrix.md) §6 错误码前缀矩阵) **错误码清单**(对齐 [02 文档 §6.2](../../../services/push-gateway/docs/02-architecture-design.md)): | 错误码 | HTTP | 触发条件 | | --------------------------- | ---- | ---------------------- | | `PUSH_UNAUTHORIZED` | 401 | 缺失/无效 token | | `PUSH_INVALID_REQUEST` | 400 | JSON 解析失败 | | `PUSH_INVALID_PAYLOAD` | 400 | event/data 字段缺失 | | `PUSH_TOO_MANY_CONNECTIONS` | 429 | 单用户连接数超限(>5) | | `PUSH_INTERNAL_ERROR` | 500 | panic / Redis 不可达 | ### 1.6 WebSocket 应用层消息协议 **服务端 → 客户端**: ```json { "type": "message", "event": "notification.created", "data": { ... }, "seq": 12345, "timestamp": "2026-07-09T..." } ``` **客户端 → 服务端**: - WebSocket Ping 控制帧(心跳,30s 间隔,非文本消息) - 重连时:`GET /ws?token=&session_id=&last_seq=`(P6 实现) **心跳规则**(RFC 6455 控制帧): - 客户端每 30s 发送 Ping - 服务端自动回 Pong(gorilla/websocket 默认) - 服务端 `SetReadDeadline(60s)`,60s 无消息则关闭连接 --- ## §2 我消费什么(依赖上游) ### 2.1 gRPC 调用(同步) **无主动 gRPC 调用上游**。 > JWT 公钥通过 HTTP `GET iam/.well-known/jwks.json` 拉取(非 gRPC),由 `shared-go/auth/jwks` 实现,5 分钟缓存刷新。 ### 2.2 Kafka 事件订阅(异步) | Topic | Event | 发布方 | mock 策略 | | ---------------------------- | --------------------- | ---------- | --------------------------------------------------------------------------- | | `edu.notification.requested` | NotificationRequested | msg (ai10) | msg 就绪前不订阅,使用本地定时器每 30 秒推送 1 条 mock 通知到所有在线客户端 | > **topic 命名对齐 ISSUE-053 裁决**([president-final-rulings.md](../president-final-rulings.md) §1.5):禁止抽象名 `edu.*.events`,统一 `edu...` 格式。 > 原 contract v1 写的 `edu.msg.notification.events` 已废弃。 **消费语义**: - Consumer Group:`push-gateway` - 至少一次(at-least-once),消费失败重试 3 次后入死信队列 - 幂等性:基于 `event_id` Redis SETNX 去重(TTL 24h) ### 2.3 HTTP 调用(如有) | 调用方 | Method | Path | 用途 | 时机 | | ------------ | ------ | --------------------------- | --------------------------------- | ---------- | | push-gateway | GET | `iam/.well-known/jwks.json` | 拉取 RS256 公钥校验 WebSocket JWT | iam 就绪后 | ### 2.4 Redis 协议 | 用途 | 数据结构 | Key 模式 | TTL | | ---------------------- | --------------- | ------------------------------------ | --------------- | | 在线用户所在实例集合 | SET | `edu:push:online:` | 60s(心跳续期) | | 单连接元数据 | HASH | `edu:push:session::` | 60s | | 跨实例定向推送 channel | Pub/Sub channel | `edu:push:channel:user:` | — | | 跨实例广播 channel | Pub/Sub channel | `edu:push:channel:broadcast` | — | | 幂等去重 | SETNX | `edu:push:idempotent:` | 24h | > **Hub 启动重建机制**(对齐 ISSUE-058):实例启动时遍历内存连接 SADD + EXPIRE 60s;先清空 Redis 中本 instanceID 旧成员避免幽灵成员;实例崩溃 SET 自然过期(60s)。 ### 2.5 内部接口(msg 调用 push-gateway) | 被调用方 | Method.Path | 用途 | 说明 | | ------------ | ----------------------------- | ------------------ | ------------------------------------------ | | push-gateway | POST /internal/push | msg 服务单用户推送 | msg 渲染模板后定向推送给目标用户 | | push-gateway | POST /internal/broadcast | msg 服务批量推送 | msg 收到业务事件后渲染模板,调此接口广播 | | push-gateway | GET /internal/online/ | msg 查在线状态 | msg 决定走在线推送还是离线推送(SMS/邮件) | --- ## §3 就绪信号 ### 3.1 我依赖的上游就绪标志 - [ ] **shared-go 包骨架**(coord 批次 0.14):`packages/shared-go` 含 tracer/logger/jwks/env 4 模块 - [ ] **iam JWT RS256 + JWKS 端点**(ai06 批次 1):iam gRPC 50052 + `/.well-known/jwks.json` 可访问 - [ ] **msg gRPC + Kafka topic**(ai10 批次 4):msg gRPC 50056 + `edu.notification.requested` topic 有事件发布 - [x] **Redis 基础设施**(coord P1):Redis 7.x 可访问 ✅ 已就绪 - [x] **Kafka 基础设施**(coord P1):Kafka 可访问 ✅ 已就绪 - [ ] **ISSUE-001~007 仲裁**(coord):[coord.md](../coord.md) 追加 ARB-003+ 仲裁章节 ### 3.2 我的就绪标志(供下游消费) - [ ] push-gateway HTTP :8081 启用(`GET /healthz` 返 200) - [ ] /readyz 返 200(含 Redis/Kafka 软失败检查,`degraded` 字段) - [ ] WebSocket /ws 端点可升级连接(JWT RS256 鉴权后建立长连接) - [ ] /internal/push + /internal/broadcast 接收 msg 推送并下发到在线客户端 - [ ] /internal/online/ 查在线状态可调用 - [ ] Kafka consumer `edu.notification.requested` 订阅成功(Consumer Group `push-gateway` lag=0) - [ ] /metrics 暴露 8+ 自定义指标(`push_gateway_*` 系列) --- ## §4 Mock 策略 ### 4.1 我提供的 mock 在 push-gateway 真实就绪前,为下游(各前端 portal)提供以下 mock: - **WebSocket mock**:前端开发期使用 mock-socket 库模拟 WS 连接 - 连接成功后每 30 秒推送 1 条 mock 通知(`type="system"`, `event="notification.created"`, `data={title:"测试通知"}`) - **HTTP mock**:/internal/* 接口返回 `{success:true, delivered:true, online:true}` ### 4.2 我消费的 mock 在真实上游就绪前,push-gateway 使用以下 mock: - **NotificationEvent mock**:msg 就绪前,push-gateway 内置定时器每 30 秒生成 1 条 mock NotificationRequested 事件,推送到所有在线客户端 - **JWT 验签 mock**:iam 就绪前使用本地固定 mock RS256 公钥验签 WebSocket 连接的 token(或 DevMode `dev-token` 跳过) - **Kafka 订阅 mock**:msg 就绪前不启动 Kafka consumer,使用本地定时器替代 - **JWKS fetcher mock**:iam 就绪前 shared-go/jwks 返回硬编码公钥 --- ## §5 与 02 文档、现码、总裁裁决的对齐说明 | 维度 | 现 code | 02 文档 | 总裁裁决 | 本 contract 采用 | 对齐任务 | | ---------------------- | ------------------ | ------------------------- | --------------------------------------------- | ----------------------------- | ------------------------------------------ | | 内部端点路径 | `/internal/push` | `/internal/push` | §4.2 as-is 采纳 02 §4.2 | `/internal/push` | ✅ 已对齐(v1 写 `/internal/send` 已修正) | | 鉴权头名 | `X-Internal-Key` | `X-Internal-Token` | §7.2 "X-Internal-Token 重命名" | `X-Internal-Token` | 待 ISSUE-002 仲裁后改代码 | | 鉴权环境变量 | `INTERNAL_API_KEY` | `INTERNAL_API_TOKEN` | §7.2 | `INTERNAL_API_TOKEN` | 待 ISSUE-002 仲裁后改代码 | | 鉴权机制 | 共享密钥 | 共享密钥 | — | 共享密钥(v1 写 mTLS 已废弃) | ✅ 已对齐 | | Kafka topic | —(未实现) | `edu.notification.events` | §1.5 ISSUE-053 → `edu.notification.requested` | `edu.notification.requested` | ✅ 已对齐 ISSUE-053 | | /readyz Redis 失败策略 | —(仅返连接数) | 返 503 硬失败 | §4.3 ISSUE-058 "仅告警不阻塞" | 软失败 200 + `degraded:true` | 待 ISSUE-006 仲裁最终策略 | | /readyz Kafka 失败策略 | — | 未描述 | §3.3 ISSUE-055 软失败 | 软失败 200 + `degraded:true` | ✅ 已对齐 ISSUE-055 | | /sse 端点 | 无 | §10 建议不支持 | — | 不提供(待 ISSUE-001 仲裁) | 待 ISSUE-001 仲裁 | | 容量目标 | — | 10w+ | — | 10w+ | 待 ISSUE-003 仲裁更新 modules/README | | 错误码前缀 | 无前缀 | `PUSH_*` | — | `PUSH_*` | 待 P5 实现时统一 | | 心跳协议 | 文本 ping/pong | RFC 6455 控制帧 | — | RFC 6455 控制帧 | 待 P5 任务 4.3 重构 | --- ## §6 变更历史 | 版本 | 日期 | 变更内容 | 变更依据 | | ---- | ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | v1 | 2026-07-09 | 初始版本(coord 生成) | — | | v2 | 2026-07-10 | topic 改 `edu.notification.requested`;/internal/send → /internal/push;鉴权 mTLS → X-Internal-Token;移除 /sse(待仲裁);新增 /internal/online;新增 §5 对齐表 | ISSUE-053/055/056/058 + 总裁 §4.2/§7.2 |