mirror of
https://github.com/shy3130/tick-stock-panel.git
synced 2026-11-02 10:26:13 +00:00
二开闭环补齐三块: 数据进得来 (写入)、事件推得出 (SSE)、契约有人守 (快照+CI)。
- write:ext scope (第六档): POST /api/ext-data/{id}/ingest 对 Token 开放,
复用管理端同一校验/落盘/视图刷新路径; 结构配置/上传/拉取仍不开放,
外部只能写行数据, 不能改写表结构
- SSE 短期票据 (V2 收尾): POST /api/events/ticket 换 60s 一次性票据 →
GET /api/events?ticket=… 订阅事件流; 票据只继承 Token scope 不放大权限;
进程内事件总线 (services/events.py, 慢消费者丢旧保新), 告警落盘点已挂
(alert_store.append/append_many → bus), 事件按 scope 过滤双向验证
- 契约守护: test_openapi_contract.py 冻结 tier=a 开放面 (61 端点),
增删须显式更新快照; 新增 ci.yml (后端全量测试 + 前端构建) 把契约
变化拦在 PR 上
- examples/open-api: 四个零依赖可运行示例 (行情/写入/回测/事件流) + README
- 菜单设置架构标注: 每页核心/扩展徽标 + 悬停原因 + 图例 (按 open-platform-plan
§2 依赖方向分类, 核心 8 页/扩展 11 页); 顺带补上 MenuSettings 缺失的 模拟盘
- 测试: 网关 write矩阵/票据语义 + 票据生命周期/总线/SSE帧/告警联动 + 契约快照,
全量 2408 passed / 102s; 线上实测: ingest 200 / 票据一次性 / SSE hello+
alert 推送 / scope 过滤双向 / 契约 61 路径
54 lines
1.8 KiB
Python
54 lines
1.8 KiB
Python
"""SSE 短期票据 — EventSource 无法携带 Authorization 头的替代通道。
|
|
|
|
流程 (docs/open-platform-plan.md §6 V2):
|
|
1. POST /api/events/ticket (Bearer Token, 任意 scope) → 一次性票据 (tse_ 前缀)
|
|
2. GET /api/events?ticket=... (EventSource) → 校验+消费票据 → 按票据
|
|
继承的 scope 过滤事件流
|
|
3. 票据 60 秒过期 / 消费即作废; 重连需重新走第 1 步换新票据
|
|
|
|
安全边界: 票据只继承 Token 已有 scope, 不放大权限; 存活窗口短,
|
|
即使泄漏也只能订阅事件流, 不能调用任何 REST 端点。
|
|
进程内存储 (单进程 uvicorn 场景), 重启清零无害。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
import threading
|
|
import time
|
|
|
|
TICKET_PREFIX = "tse_"
|
|
TTL_S = 60.0
|
|
_MAX_OUTSTANDING = 1000 # 防堆积: 远超正常并发, 超限先清理过期
|
|
|
|
_LOCK = threading.Lock()
|
|
_TICKETS: dict[str, tuple[float, list[str]]] = {} # ticket → (过期时刻, scopes)
|
|
|
|
|
|
def _prune(now: float) -> None:
|
|
expired = [t for t, (exp, _) in _TICKETS.items() if exp <= now]
|
|
for t in expired:
|
|
_TICKETS.pop(t, None)
|
|
|
|
|
|
def issue(scopes: list[str]) -> tuple[str, float]:
|
|
"""签发一次性票据; 返回 (票据, 有效期秒)。"""
|
|
now = time.monotonic()
|
|
with _LOCK:
|
|
_prune(now)
|
|
if len(_TICKETS) >= _MAX_OUTSTANDING:
|
|
raise RuntimeError("待使用票据过多, 稍后重试")
|
|
ticket = TICKET_PREFIX + secrets.token_hex(16)
|
|
_TICKETS[ticket] = (now + TTL_S, list(scopes))
|
|
return ticket, TTL_S
|
|
|
|
|
|
def consume(ticket: str) -> list[str] | None:
|
|
"""校验并消费票据; 返回继承的 scopes, 无效/过期/已用 → None。"""
|
|
now = time.monotonic()
|
|
with _LOCK:
|
|
_prune(now)
|
|
hit = _TICKETS.pop(ticket, None)
|
|
if hit is None or hit[0] <= now:
|
|
return None
|
|
return hit[1]
|