feat(saas): Phase 2 Admin Web 管理后台 — 完整 CRUD + Dashboard 统计

后端:
- 添加 GET /api/v1/stats/dashboard 聚合统计端点
  (账号数/活跃服务商/今日请求/今日Token用量等7项指标)
- 需要 account:admin 权限

Admin 前端 (Next.js 14 + shadcn/ui + Tailwind + Recharts):
- 设计系统: Dark Mode OLED (#020617 背景, #22C55E CTA)
- 登录页: 双栏布局, 品牌区 + 表单
- Dashboard 布局: Sidebar 导航 + Header + 主内容区
- 仪表盘: 4 统计卡片 + AreaChart 请求趋势 + BarChart Token用量
- 8 个 CRUD 页面:
  - 账号管理 (搜索/角色/状态筛选, 编辑/启用禁用)
  - 服务商 (CRUD + API Key masked)
  - 模型管理 (Provider筛选, CRUD)
  - API 密钥 (创建/撤销, 一次性显示token)
  - 用量统计 (LineChart + BarChart)
  - 中转任务 (状态筛选, 展开详情)
  - 系统配置 (分类Tab, 编辑)
  - 操作日志 (Action筛选, 展开详情)
- 14 个 shadcn 风格 UI 组件 (手写实现)
- 类型化 API 客户端 (SaaSClient, 20+ 方法, 401 自动跳转)
- AuthGuard 路由保护 + useAuth() hook

验证: tsc --noEmit 零 error, pnpm build 13 页面成功, cargo test 21 通过
This commit is contained in:
iven
2026-03-27 14:06:50 +08:00
parent d760b9ca10
commit a66b675675
39 changed files with 6798 additions and 0 deletions

View File

@@ -130,3 +130,39 @@ pub async fn list_operation_logs(
Ok(Json(items))
}
/// GET /api/v1/stats/dashboard — 仪表盘聚合统计 (需要 admin 权限)
pub async fn dashboard_stats(
State(state): State<AppState>,
Extension(ctx): Extension<AuthContext>,
) -> SaasResult<Json<serde_json::Value>> {
require_admin(&ctx)?;
let total_accounts: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM accounts")
.fetch_one(&state.db).await?;
let active_accounts: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM accounts WHERE status = 'active'")
.fetch_one(&state.db).await?;
let tasks_today: (i64,) = sqlx::query_as(
"SELECT COUNT(*) FROM relay_tasks WHERE date(created_at) = date('now')"
).fetch_one(&state.db).await?;
let active_providers: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM providers WHERE enabled = 1")
.fetch_one(&state.db).await?;
let active_models: (i64,) = sqlx::query_as("SELECT COUNT(*) FROM models WHERE enabled = 1")
.fetch_one(&state.db).await?;
let tokens_today_input: (i64,) = sqlx::query_as(
"SELECT COALESCE(SUM(input_tokens), 0) FROM usage_records WHERE date(created_at) = date('now')"
).fetch_one(&state.db).await?;
let tokens_today_output: (i64,) = sqlx::query_as(
"SELECT COALESCE(SUM(output_tokens), 0) FROM usage_records WHERE date(created_at) = date('now')"
).fetch_one(&state.db).await?;
Ok(Json(serde_json::json!({
"total_accounts": total_accounts.0,
"active_accounts": active_accounts.0,
"tasks_today": tasks_today.0,
"active_providers": active_providers.0,
"active_models": active_models.0,
"tokens_today_input": tokens_today_input.0,
"tokens_today_output": tokens_today_output.0,
})))
}

View File

@@ -16,4 +16,5 @@ pub fn routes() -> axum::Router<crate::state::AppState> {
.route("/api/v1/tokens", post(handlers::create_token))
.route("/api/v1/tokens/{id}", delete(handlers::revoke_token))
.route("/api/v1/logs/operations", get(handlers::list_operation_logs))
.route("/api/v1/stats/dashboard", get(handlers::dashboard_stats))
}