""" onmyoji_gacha 插件的 Web API 接口 使用 NoneBot 内置的 FastAPI 适配器提供管理员后台接口 修复记录(代码评审后): - P0: 补全5处async/await缺失 - P1: 移除verify_admin_token中的token明文打印 - P2: 模块级实例化改为lazy延迟初始化 - P2: 添加get_user_mention_name统一工具函数 """ import os from typing import Dict, List, Any, Optional from fastapi import APIRouter, Depends, HTTPException, Header, Request from fastapi.responses import HTMLResponse, JSONResponse from fastapi.templating import Jinja2Templates from pydantic import BaseModel from .config import Config from .gacha import GachaSystem # FastAPI 路由(模块级,不触发业务初始化) router = APIRouter(prefix="/onmyoji_gacha", tags=["onmyoji_gacha"]) # 延迟初始化缓存 _config: Optional[Config] = None _gacha_system: Optional[GachaSystem] = None def _get_config() -> Config: """延迟获取配置实例""" global _config if _config is None: _config = Config() return _config def _get_gacha_system() -> GachaSystem: """延迟获取抽卡系统实例""" global _gacha_system if _gacha_system is None: _gacha_system = GachaSystem() return _gacha_system def _get_templates() -> Jinja2Templates: """延迟加载模板目录""" return Jinja2Templates(directory="danding_bot/plugins/onmyoji_gacha/templates") async def verify_admin_token(authorization: Optional[str] = Header(None)): """验证管理员权限,失败时抛出HTTPException""" if not authorization: raise HTTPException(status_code=401, detail="缺少认证令牌") token = authorization.replace("Bearer ", "") expected = _get_config().WEB_ADMIN_TOKEN if token != expected: raise HTTPException(status_code=403, detail="无效的认证令牌") return True # API 响应模型 class DailyStatsResponse(BaseModel): """每日统计数据响应模型""" success: bool date: str stats: Dict[str, Any] class UserStatsResponse(BaseModel): """用户统计数据响应模型""" success: bool user_id: str total_draws: int R_count: int SR_count: int SSR_count: int SP_count: int recent_draws: List[Dict[str, str]] class RankListResponse(BaseModel): """排行榜数据响应模型""" success: bool data: List[Dict[str, Any]] class AchievementResponse(BaseModel): """成就数据响应模型""" success: bool user_id: str achievements: Dict[str, Any] progress: Dict[str, Any] class DailyDetailedRecordsResponse(BaseModel): """每日详细记录响应模型""" success: bool date: str records: List[Dict[str, Any]] total_count: int # 管理后台页面 @router.get("/admin", response_class=HTMLResponse) async def admin_page(request: Request): """管理后台页面""" return _get_templates().TemplateResponse("admin.html", {"request": request}) # API 端点 @router.get("/api/stats/daily", response_model=DailyStatsResponse, dependencies=[Depends(verify_admin_token)]) async def get_daily_stats(): """获取今日抽卡统计""" result = await _get_gacha_system().get_daily_stats() if not result["success"]: return result return { "success": True, "date": result["date"], "stats": result["stats"] } @router.get("/api/stats/user/{user_id}", response_model=UserStatsResponse, dependencies=[Depends(verify_admin_token)]) async def get_user_stats(user_id: str): """获取用户抽卡统计""" result = await _get_gacha_system().get_user_stats(user_id) if not result["success"]: return { "success": False, "user_id": user_id, "total_draws": 0, "R_count": 0, "SR_count": 0, "SSR_count": 0, "SP_count": 0, "recent_draws": [] } return { "success": True, "user_id": user_id, "total_draws": result["total_draws"], "R_count": result["R_count"], "SR_count": result["SR_count"], "SSR_count": result["SSR_count"], "SP_count": result["SP_count"], "recent_draws": result["recent_draws"] } @router.get("/api/stats/rank", response_model=RankListResponse, dependencies=[Depends(verify_admin_token)]) async def get_rank_list(): """获取抽卡排行榜""" rank_data = await _get_gacha_system().get_rank_list() formatted_data = [] for user_id, stats in rank_data: formatted_data.append({ "user_id": user_id, "total_draws": stats["total_draws"], "R_count": stats["R_count"], "SR_count": stats["SR_count"], "SSR_count": stats["SSR_count"], "SP_count": stats["SP_count"], "ssr_sp_total": stats["SSR_count"] + stats["SP_count"] }) return { "success": True, "data": formatted_data } @router.get("/api/achievements/{user_id}", response_model=AchievementResponse, dependencies=[Depends(verify_admin_token)]) async def get_user_achievements(user_id: str): """获取用户成就信息""" result = await _get_gacha_system().get_user_achievements(user_id) if not result["success"]: return { "success": False, "user_id": user_id, "achievements": {}, "progress": {} } return { "success": True, "user_id": user_id, "achievements": result["achievements"], "progress": result["progress"] } @router.get("/api/records/daily", response_model=DailyDetailedRecordsResponse, dependencies=[Depends(verify_admin_token)]) async def get_daily_detailed_records(date: Optional[str] = None): """获取每日详细抽卡记录""" result = await _get_gacha_system().get_daily_detailed_records(date) if not result["success"]: return { "success": False, "date": date or _get_gacha_system().data_manager.get_today_date(), "records": [], "total_count": 0 } return { "success": True, "date": result["date"], "records": result["records"], "total_count": result["total_count"] } def register_web_routes(): """注册 Web 路由到 NoneBot 的 FastAPI 应用""" try: from nonebot import get_driver driver = get_driver() app = driver.server_app app.include_router(router) print("✅ onmyoji_gacha Web API 路由注册成功") return True except Exception as e: print(f"❌ 注册 Web 路由时出错: {e}") return False