feat(proxy): T-P7 管理面后端(stats/ledger/students 端点 + admin_queries.sql)

- /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
This commit is contained in:
tzt
2026-09-05 16:12:26 +08:00
parent c54ad23c88
commit 4f272dc66d
3 changed files with 73 additions and 2 deletions
+21
View File
@@ -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;
+51 -1
View File
@@ -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
+1 -1
View File
@@ -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/账目零不一致 | ⬜ 待办 | |
---