"""管理面板路由(/api/admin) 提供: - 登录认证(密码 + token) - 触发 AI 分析 - 浏览统计 - 报告管理 """ import json import time from datetime import datetime, timezone, timedelta from fastapi import APIRouter, HTTPException, Request from fastapi.responses import JSONResponse from pydantic import BaseModel from services import admin_auth from database import get_connection, dict_from_row _CST = timezone(timedelta(hours=8)) _NO_CACHE_HEADERS = {"Cache-Control": "no-store, no-cache, must-revalidate, max-age=0"} router = APIRouter() def _require_auth(request: Request) -> str: """从 header 或 cookie 提取 token 并验证""" token = request.headers.get("X-Admin-Token", "") if not token: # fallback: 从 cookie 读 token = request.cookies.get("admin_token", "") if not admin_auth.verify_token(token): raise HTTPException(status_code=401, detail="未登录或登录已过期") return token class LoginRequest(BaseModel): password: str @router.post("/admin/login", summary="管理员登录") async def admin_login(body: LoginRequest): if not admin_auth.is_configured(): # 自动设置默认密码 import os default_pw = os.getenv("ADMIN_PASSWORD", "!auvauv") admin_auth.set_password(default_pw) if not admin_auth.verify_password(body.password): raise HTTPException(status_code=401, detail="密码错误") token = admin_auth.create_token() return JSONResponse({"data": {"token": token, "expiresIn": admin_auth.TOKEN_TTL}}) @router.post("/admin/logout", summary="管理员登出") async def admin_logout(request: Request): token = request.headers.get("X-Admin-Token", "") or request.cookies.get("admin_token", "") admin_auth.revoke_token(token) return JSONResponse({"data": "ok"}) class ChangePasswordRequest(BaseModel): old_password: str new_password: str @router.post("/admin/change-password", summary="修改管理密码") async def admin_change_password(body: ChangePasswordRequest, request: Request): _require_auth(request) if not admin_auth.verify_password(body.old_password): raise HTTPException(status_code=401, detail="原密码错误") if len(body.new_password) < 4: raise HTTPException(status_code=400, detail="密码至少4位") admin_auth.set_password(body.new_password) return JSONResponse({"data": "密码已更新"}) @router.get("/admin/stats", summary="管理面板统计数据") async def admin_stats(request: Request): _require_auth(request) conn = get_connection() try: stats = {} # 报告总数 row = conn.execute("SELECT COUNT(*) as cnt FROM ai_reports").fetchone() stats["reportCount"] = row["cnt"] # 总 tokens row = conn.execute("SELECT COALESCE(SUM(tokens_used), 0) as total FROM ai_reports").fetchone() stats["totalTokens"] = row["total"] # 最新报告 row = conn.execute( "SELECT trade_date, title FROM ai_reports ORDER BY trade_date DESC LIMIT 1" ).fetchone() stats["latestReport"] = dict_from_row(row) if row else None # 集合数 row = conn.execute("SELECT COUNT(*) as cnt FROM stock_collections").fetchone() stats["collectionCount"] = row["cnt"] # 关注股票数 row = conn.execute("SELECT COUNT(*) as cnt FROM collection_stocks").fetchone() stats["stockCount"] = row["cnt"] # 分享链接数 row = conn.execute("SELECT COUNT(*) as cnt FROM share_links").fetchone() stats["shareLinkCount"] = row["cnt"] # 核心股天数 row = conn.execute( "SELECT COUNT(DISTINCT trade_date) as cnt FROM daily_core_stocks" ).fetchone() stats["coreStockDays"] = row["cnt"] # 近7天报告 rows = conn.execute( "SELECT trade_date, title, tokens_used, created_at FROM ai_reports " "ORDER BY trade_date DESC LIMIT 7" ).fetchall() stats["recentReports"] = [dict_from_row(r) for r in rows] return JSONResponse({"data": stats}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.post("/admin/trigger-analysis", summary="触发 AI 分析") async def admin_trigger_analysis(request: Request): _require_auth(request) now = datetime.now(_CST) trade_date = now.strftime("%Y-%m-%d") from services.ai_service import collect_ai_analysis result = await collect_ai_analysis(trade_date) return JSONResponse({"data": result}) @router.get("/admin/reports", summary="报告列表(管理用)") async def admin_list_reports(request: Request): _require_auth(request) conn = get_connection() try: rows = conn.execute( "SELECT id, trade_date, report_type, title, summary, tools_used, model, tokens_used, created_at " "FROM ai_reports ORDER BY trade_date DESC, id DESC LIMIT 50" ).fetchall() items = [] for r in rows: item = dict_from_row(r) item["toolsUsed"] = json.loads(item.pop("tools_used") or "[]") items.append(item) return JSONResponse({"data": items}, headers=_NO_CACHE_HEADERS) finally: conn.close() @router.delete("/admin/reports/{report_id}", summary="删除报告") async def admin_delete_report(report_id: int, request: Request): _require_auth(request) conn = get_connection() try: conn.execute("DELETE FROM ai_reports WHERE id = ?", (report_id,)) conn.commit() return JSONResponse({"data": "已删除"}) finally: conn.close()