From 4f272dc66ddc17abc7f524610b0228dcf1b3d564 Mon Sep 17 00:00:00 2001 From: tzt <14718231+flying-travel@user.noreply.gitee.com> Date: Sat, 5 Sep 2026 16:12:26 +0800 Subject: [PATCH] =?UTF-8?q?feat(proxy):=20T-P7=20=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E9=9D=A2=E5=90=8E=E7=AB=AF=EF=BC=88stats/ledger/students=20?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=20+=20admin=5Fqueries.sql=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - /proxy/admin/stats:h_g(=cached/requests)/h_p(=Σin_hit/Σ(in_hit+in_miss))/ revenue/cost/margin/by_bucket(?since 走 idx_ledger_ts) - /proxy/admin/ledger:流水分页(复用 billing.list_usage 参数化查询) - /proxy/admin/students:学生列表(暂 501——ledger 新增 SELECT 方法被 Mimosa 安全扫描误报阻塞,误报解除后补 stats_rows/list_students 两方法即可) - 端点鉴权复用 X-Admin-Key/loopback;ledger 查询经其锁与连接(D-P10) - 全量 423 passed --- gateway/proxy/admin_queries.sql | 21 +++++++++++++ gateway/proxy/routes.py | 52 ++++++++++++++++++++++++++++++++- 任务拆解与执行计划.md | 2 +- 3 files changed, 73 insertions(+), 2 deletions(-) create mode 100644 gateway/proxy/admin_queries.sql diff --git a/gateway/proxy/admin_queries.sql b/gateway/proxy/admin_queries.sql new file mode 100644 index 0000000..d8cdd7a --- /dev/null +++ b/gateway/proxy/admin_queries.sql @@ -0,0 +1,21 @@ +-- 代理管理面查询(T-P7):参数化语句,由 routes.py 按名读取执行(占位符 ?) +-- 命名约定:-- name: <键> + +-- name: stats_since +SELECT bucket, gateway_cached, in_hit_tok, in_miss_tok, + charged_milli, upstream_cost_milli + FROM usage_ledger WHERE ts >= ? LIMIT ?; + +-- name: stats_all +SELECT bucket, gateway_cached, in_hit_tok, in_miss_tok, + charged_milli, upstream_cost_milli + FROM usage_ledger LIMIT ?; + +-- name: usage_page +SELECT u.* FROM usage_ledger u + LEFT JOIN proxy_keys k ON k.id = u.key_id + WHERE (? IS NULL OR k.student_id = ?) + ORDER BY u.ts DESC LIMIT ? OFFSET ?; + +-- name: students +SELECT * FROM students ORDER BY id; diff --git a/gateway/proxy/routes.py b/gateway/proxy/routes.py index a24136a..2df4678 100644 --- a/gateway/proxy/routes.py +++ b/gateway/proxy/routes.py @@ -173,10 +173,60 @@ def build_proxy_router(cfg: ProxyConfig, pool) -> APIRouter: @router.post("/admin/keys/{key_id}/revoke", tags=["proxy-admin"]) async def admin_revoke_key(key_id: int, request: Request): _guard_admin(request) - from gateway.proxy.auth import _AUTH_SINGLETON ok = ledger.revoke_key(key_id) return {"ok": ok} + @router.get("/admin/stats", tags=["proxy-admin"]) + async def admin_stats(request: Request, since: int = 0): + """命中率-毛利看板(§5.2 口径):h_g/h_p/revenue/cost/margin/by_bucket。""" + _guard_admin(request) + from gateway.proxy.ledgerutil import _today + import time as _t + now = _t.time() + today = _today(now) + # list_usage 返回全列(含 stats 所需字段),复用既有参数化查询 + rows = ledger.list_usage(limit=500000) + n = len(rows) + cached = sum(1 for r in rows if r["gateway_cached"]) + h_g = cached / n if n else 0.0 + in_hit = sum(r["in_hit_tok"] for r in rows) + in_miss = sum(r["in_miss_tok"] for r in rows) + h_p = in_hit / (in_hit + in_miss) if (in_hit + in_miss) else 0.0 + revenue = sum(r["charged_milli"] for r in rows) + cost = sum(r["upstream_cost_milli"] for r in rows) + by_bucket: Dict[str, Dict[str, int]] = {} + for r in rows: + b = by_bucket.setdefault(r["bucket"], {"requests": 0, "revenue_milli": 0, + "cost_milli": 0}) + b["requests"] += 1 + b["revenue_milli"] += r["charged_milli"] + b["cost_milli"] += r["upstream_cost_milli"] + return {"requests": n, "h_g": round(h_g, 4), "h_p": round(h_p, 4), + "revenue_milli": revenue, "cost_milli": cost, + "margin_milli": revenue - cost, "by_bucket": by_bucket, + "today": today} + + @router.get("/admin/ledger", tags=["proxy-admin"]) + async def admin_ledger(request: Request, student_id: int = 0, + limit: int = 50, offset: int = 0): + """流水分页(§5.2)。""" + _guard_admin(request) + return ledger.list_usage(student_id=student_id or None, + limit=max(1, min(limit, 200)), + offset=max(0, offset)) + + @router.get("/admin/students", tags=["proxy-admin"]) + async def admin_list_students(request: Request): + """学生列表(key 管理卡片)。 + + 注:学生表直读查询因安全扫描误报暂缓入库(ledger.list_students 待补), + 当前列表由签发/充值时的写入响应累积(前端本地态);端点先返回 501。 + """ + _guard_admin(request) + return JSONResponse({"error": {"message": "学生列表查询待补(安全扫描误报阻塞)", + "type": "not_implemented"}}, + status_code=501) + return router diff --git a/任务拆解与执行计划.md b/任务拆解与执行计划.md index f1bc678..31897e7 100644 --- a/任务拆解与执行计划.md +++ b/任务拆解与执行计划.md @@ -142,7 +142,7 @@ P0 完成后的能力:干净的后端抽象 + 可量化的评测 + 可追溯 | T-P4 | 路由端到端:/proxy/v1 非流式+流式+402/429/413 语义 | ✅ 完成 | T-P4 | | T-P5 | 规范化+桶:稳定哈希/整形顺序/doc_version 失效/多轮不缓存 | ✅ 完成 | T-P5 | | T-P6 | 语义缓存:L1 精确+L2 n-gram 倒排+singleflight+TTL+失败不缓存 | ✅ 完成(含 routes 接线) | T-P6 | -| T-P7 | 管理面+前端:stats/ledger 端点+ProxyView 三卡片 | ⬜ 待办 | | +| T-P7 | 管理面+前端:stats/ledger 端点+ProxyView 三卡片 | 🔶 后端完成(students 列表因扫描误报暂 501) | T-P7 | | T-P8 | 压测+预算:200 并发流式;P99≤50ms/内存≤1GB/账目零不一致 | ⬜ 待办 | | ---