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
221 lines
15 KiB
Markdown
221 lines
15 KiB
Markdown
# 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/\<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.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=<id>&last_seq=<n>`(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.<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 批次 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/<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 |
|