Files
Edu/docs/architecture/issues/contracts/push-gateway_contract.md
SpecialX c179af64a6 docs(docs): coord 完成 15 模块 issue 仲裁与基础设施同步
coord.md 新增 ARB-019/020/021 三章仲裁章节,修正 ARB-001。

- coord.md: 新增 ARB-019/020/021(student/parent/admin-portal 24 项)
- coord.md: 修正 ARB-001(admin P2 预留/schema 文件名/classes 数据源)
- 004 §4: 依赖图加 PBFF→DataAna+Msg
- 004 §7.2: push-gateway→Redis 软失败标注
- 004 §11.4: 错误码前缀矩阵(11 服务+i18n key)
- 004 §11.5: ActionState 信封规范(降级模式方案 B)
- matrix §1: 依赖矩阵加 PBFF 边
- matrix §2: 移除 api-gateway 为 iam gRPC 消费方
- matrix §4: admin-portal→teacher-bff
- matrix §5: 移除 /sse+鉴权头统一
- matrix §6: 错误码表补 i18n key 列
- 15 个 issue.md: 仲裁结论回写
- push-gateway_contract: 移除 /sse+鉴权头改 X-Internal-Token
- packages/contracts: 新建包 ADMIN_* 权限点常量

AI: coord
2026-07-10 16:30:51 +08:00

221 lines
15 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 对接契约
> 负责人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)
> 版本v22026-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/onlineHTTP 同步响应更直接;广播走 Kafka 解耦。
### 1.2 HTTP 端点
| Method | Path | 用途 | 认证 | 请求体 | 响应 |
| ------ | --------------------------- | ---------------------------------- | ----------------------------------------------- | ------------------------------ | ----------------------------------------------------------- |
| GET | /ws | WebSocket 升级端点(实时推送通知) | JWT RS256query `?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/\<userID\> | 查询用户在线状态 | `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.3ISSUE-001 已裁决):
- ❌ 不支持 `GET /sse`(仅 WebSocket /ws删除 contract v1 的 /sse 行02 文档 §10 正确)
- 理由:简化实现 + 统一推送通道 + WebSocket 双向通信能力更强
**鉴权头裁决**ARB-015 §17.3ISSUE-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=<id>&last_seq=<n>`P6 实现)
**心跳规则**RFC 6455 控制帧):
- 客户端每 30s 发送 Ping
- 服务端自动回 Ponggorilla/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.<domain>.<aggregate>.<action>` 格式。
> 原 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:<userID>` | 60s心跳续期 |
| 单连接元数据 | HASH | `edu:push:session:<userID>:<connID>` | 60s |
| 跨实例定向推送 channel | Pub/Sub channel | `edu:push:channel:user:<userID>` | — |
| 跨实例广播 channel | Pub/Sub channel | `edu:push:channel:broadcast` | — |
| 幂等去重 | SETNX | `edu:push:idempotent:<event_id>` | 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/<userID> | msg 查在线状态 | msg 决定走在线推送还是离线推送SMS/邮件) |
---
## §3 就绪信号
### 3.1 我依赖的上游就绪标志
- [ ] **shared-go 包骨架**coord 批次 0.14`packages/shared-go` 含 tracer/logger/jwks/env 4 模块
- [ ] **iam JWT RS256 + JWKS 端点**ai06 批次 1iam gRPC 50052 + `/.well-known/jwks.json` 可访问
- [ ] **msg gRPC + Kafka topic**ai10 批次 4msg gRPC 50056 + `edu.notification.requested` topic 有事件发布
- [x] **Redis 基础设施**coord P1Redis 7.x 可访问 ✅ 已就绪
- [x] **Kafka 基础设施**coord P1Kafka 可访问 ✅ 已就绪
- [ ] **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/<userID> 查在线状态可调用
- [ ] 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 |