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
15 KiB
push-gateway 对接契约
负责人:ai02 关联:matrix.md、msg.proto、events.proto、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):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 §6 错误码前缀矩阵)
错误码清单(对齐 02 文档 §6.2):
| 错误码 | 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 应用层消息协议
服务端 → 客户端:
{
"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 §1.5):禁止抽象名
edu.*.events,统一edu.<domain>.<aggregate>.<action>格式。 原 contract v1 写的edu.msg.notification.events已废弃。
消费语义:
- Consumer Group:
push-gateway - 至少一次(at-least-once),消费失败重试 3 次后入死信队列
- 幂等性:基于
event_idRedis 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/ | 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.requestedtopic 有事件发布 - Redis 基础设施(coord P1):Redis 7.x 可访问 ✅ 已就绪
- Kafka 基础设施(coord P1):Kafka 可访问 ✅ 已就绪
- ISSUE-001~007 仲裁(coord):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 Grouppush-gatewaylag=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:"测试通知"})
- 连接成功后每 30 秒推送 1 条 mock 通知(
- 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 |