"""Debug API — 系统日志与运行状态""" import os from datetime import datetime, timedelta from fastapi import APIRouter, Depends from sqlalchemy import text from app.core.deps import get_current_admin from app.db.database import get_db from app.config import settings, is_trading_hours router = APIRouter(prefix="/api/debug", tags=["debug"]) @router.get("/errors") async def get_errors( limit: int = 50, source: str = None, level: str = None, days: int = 7, _admin: dict = Depends(get_current_admin), ): """获取错误日志(管理员)""" start = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") async with get_db() as db: conditions = ["created_at >= :start"] params = {"start": start} if source: conditions.append("source = :source") params["source"] = source if level: conditions.append("level = :level") params["level"] = level where = " AND " + " AND ".join(conditions) # 总数 count_result = await db.execute( text(f"SELECT COUNT(*) FROM error_logs WHERE {where}"), params ) total = count_result.scalar() or 0 # 查询 params["limit"] = limit result = await db.execute( text( f"SELECT id, source, level, message, detail, created_at " f"FROM error_logs WHERE {where} " f"ORDER BY created_at DESC LIMIT :limit" ), params, ) rows = result.fetchall() errors = [] for row in rows: r = row._mapping errors.append({ "id": r["id"], "source": r["source"], "level": r["level"], "message": r["message"], "detail": r["detail"] or "", "created_at": str(r["created_at"]) if r["created_at"] else "", }) # 可选的 source/level 列表(用于前端过滤) sources_result = await db.execute( text("SELECT DISTINCT source FROM error_logs ORDER BY source") ) sources = [r[0] for r in sources_result.fetchall()] levels_result = await db.execute( text("SELECT DISTINCT level FROM error_logs ORDER BY level") ) levels = [r[0] for r in levels_result.fetchall()] return { "total": total, "errors": errors, "sources": sources, "levels": levels, } @router.delete("/errors") async def clear_errors( days: int = 30, _admin: dict = Depends(get_current_admin), ): """清除旧错误日志(管理员)""" cutoff = (datetime.now() - timedelta(days=days)).strftime("%Y-%m-%d") async with get_db() as db: result = await db.execute( text("DELETE FROM error_logs WHERE created_at < :cutoff"), {"cutoff": cutoff}, ) deleted = result.rowcount await db.commit() return {"status": "ok", "deleted": deleted} @router.get("/system") async def system_status(_admin: dict = Depends(get_current_admin)): """系统运行状态摘要(管理员)""" from app.engine.recommender import _scan_running, _scan_lock async with get_db() as db: # 各表数据量 tables_counts = {} for t in ["recommendations", "sector_heat", "market_temperature", "recommendation_tracking", "stock_diagnoses", "error_logs", "users"]: result = await db.execute(text(f"SELECT COUNT(*) FROM {t}")) tables_counts[t] = result.scalar() or 0 # 最近 24h 错误数 since = (datetime.now() - timedelta(hours=24)).strftime("%Y-%m-%d %H:%M:%S") result = await db.execute( text("SELECT COUNT(*) FROM error_logs WHERE created_at >= :since"), {"since": since}, ) recent_errors = result.scalar() or 0 # 最近错误 result = await db.execute( text("SELECT source, message, created_at FROM error_logs ORDER BY created_at DESC LIMIT 5") ) last_errors = [ {"source": r[0], "message": r[1], "created_at": str(r[2])} for r in result.fetchall() ] # 数据库文件大小 db_path = settings.database_url.replace("sqlite:///", "") db_size_mb = 0 if os.path.exists(db_path): db_size_mb = round(os.path.getsize(db_path) / 1024 / 1024, 2) return { "is_trading": is_trading_hours(), "scan_running": _scan_running, "scan_locked": _scan_lock.locked(), "recent_errors": recent_errors, "last_errors": last_errors, "tables_counts": tables_counts, "db_size_mb": db_size_mb, }