""" 管理员仪表盘 API """ from fastapi import APIRouter, Depends, HTTPException, Query from fastapi.responses import StreamingResponse from sqlalchemy.orm import Session from sqlalchemy import func from ...database import get_db, SessionLocal from ...models import ( User, AIModel, Order, TokenUsageLog, OperationLog, AuditLog, Conversation, Message, KnowledgeBase, KnowledgeDocument, ) from ...auth import get_current_admin from ...middleware.log_service import LogService from datetime import datetime, date, timedelta, time import csv import io router = APIRouter() def _serialize_op_log(log): return { "id": log.id, "user_id": log.user_id, "username": log.username, "action": log.action, "target": log.target, "target_id": log.target_id, "detail": log.detail, "ip": log.ip, "created_at": log.created_at.isoformat() if log.created_at else None, } def _serialize_audit_log(log): return { "id": log.id, "admin_id": log.admin_id, "admin_name": log.admin_name, "action": log.action, "target_type": log.target_type, "target_id": log.target_id, "detail": log.detail, "created_at": log.created_at.isoformat() if log.created_at else None, } @router.get("/stats") async def get_dashboard_stats( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取管理员仪表盘统计数据""" today_start = datetime.combine(date.today(), time.min) seven_days_ago = datetime.now() - timedelta(days=7) total_users = db.query(func.count(User.id)).scalar() or 0 active_users = db.query( func.count(func.distinct(Conversation.user_id)) ).filter(Conversation.created_at >= seven_days_ago).scalar() or 0 total_models = db.query(func.count(AIModel.id)).scalar() or 0 enabled_models = db.query(func.count(AIModel.id)).filter( AIModel.is_enabled == True ).scalar() or 0 total_conversations = db.query(func.count(Conversation.id)).scalar() or 0 active_conversations = db.query(func.count(Conversation.id)).filter( Conversation.updated_at >= seven_days_ago ).scalar() or 0 valid_conversations = db.query(func.count(Conversation.id)).filter( Conversation.id.in_(db.query(Message.conversation_id).distinct()) ).scalar() or 0 total_knowledge_bases = db.query(func.count(KnowledgeBase.id)).scalar() or 0 total_documents = db.query(func.count(KnowledgeDocument.id)).scalar() or 0 today_orders = db.query(func.count(Order.id)).filter( Order.status == "paid", Order.paid_at >= today_start, ).scalar() or 0 today_subscription_orders = db.query(func.count(Order.id)).filter( Order.order_type == "membership", Order.status == "paid", Order.paid_at >= today_start, ).scalar() or 0 today_conversations = db.query(func.count(Conversation.id)).filter( Conversation.created_at >= today_start ).scalar() or 0 today_tokens = db.query(func.coalesce(func.sum(TokenUsageLog.total_tokens), 0)).filter( TokenUsageLog.created_at >= today_start ).scalar() or 0 week_tokens = db.query(func.coalesce(func.sum(TokenUsageLog.total_tokens), 0)).filter( TokenUsageLog.created_at >= seven_days_ago ).scalar() or 0 total_tokens = db.query(func.coalesce(func.sum(TokenUsageLog.total_tokens), 0)).scalar() or 0 return { "total_users": total_users, "active_users": int(active_users), "total_models": total_models, "enabled_models": enabled_models, "total_conversations": total_conversations, "active_conversations": int(active_conversations), "valid_conversations": int(valid_conversations), "total_knowledge_bases": total_knowledge_bases, "total_documents": total_documents, "today_orders": today_orders, "today_subscription_orders": today_subscription_orders, "today_conversations": today_conversations, "today_tokens": int(today_tokens), "week_tokens": int(week_tokens), "total_tokens": int(total_tokens), "server_time": datetime.now().isoformat(), } @router.get("/daily-stats") async def get_daily_token_stats( days: int = Query(30, ge=1, le=365), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取近 N 天 Token 消耗趋势(按天聚合)""" end = date.today() start = end - timedelta(days=days - 1) start_dt = datetime.combine(start, time.min) rows = db.query( func.date(TokenUsageLog.created_at), func.coalesce(func.sum(TokenUsageLog.total_tokens), 0), ).filter(TokenUsageLog.created_at >= start_dt).group_by( func.date(TokenUsageLog.created_at) ).all() m = {str(r[0]): int(r[1] or 0) for r in rows} data = [] for i in range(days): d = start + timedelta(days=i) ds = d.isoformat() data.append({"date": ds, "tokens": m.get(ds, 0)}) return {"code": 200, "data": {"days": days, "data": data}} @router.get("/operation-logs") async def list_operation_logs( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """分页获取操作日志(默认 20 条/页)""" total = db.query(func.count(OperationLog.id)).scalar() or 0 items = db.query(OperationLog).order_by( OperationLog.created_at.desc() ).offset((page - 1) * page_size).limit(page_size).all() return { "code": 200, "data": { "items": [_serialize_op_log(l) for l in items], "total": total, "page": page, "page_size": page_size, }, } @router.get("/audit-logs") async def list_audit_logs( page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=100), admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """分页获取审计日志(默认 20 条/页)""" total = db.query(func.count(AuditLog.id)).scalar() or 0 items = db.query(AuditLog).order_by( AuditLog.created_at.desc() ).offset((page - 1) * page_size).limit(page_size).all() return { "code": 200, "data": { "items": [_serialize_audit_log(l) for l in items], "total": total, "page": page, "page_size": page_size, }, } @router.get("/operation-logs/export") async def export_operation_logs( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """导出全部操作日志为 CSV""" logs = db.query(OperationLog).order_by(OperationLog.created_at.desc()).all() buf = io.StringIO() writer = csv.writer(buf) writer.writerow(["时间", "用户", "操作", "目标", "目标ID", "详情", "IP"]) for l in logs: writer.writerow([ l.created_at.strftime("%Y-%m-%d %H:%M:%S") if l.created_at else "", l.username or "", l.action or "", l.target or "", l.target_id or "", (l.detail or "").replace("\n", " ").replace("\r", ""), l.ip or "", ]) content = buf.getvalue().encode("utf-8-sig") return StreamingResponse( io.BytesIO(content), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=operation_logs.csv"}, ) @router.get("/audit-logs/export") async def export_audit_logs( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """导出全部审计日志为 CSV""" logs = db.query(AuditLog).order_by(AuditLog.created_at.desc()).all() buf = io.StringIO() writer = csv.writer(buf) writer.writerow(["时间", "操作人", "操作类型", "目标类型", "目标ID", "详情"]) for l in logs: writer.writerow([ l.created_at.strftime("%Y-%m-%d %H:%M:%S") if l.created_at else "", l.admin_name or "", l.action or "", l.target_type or "", l.target_id or "", (l.detail or "").replace("\n", " ").replace("\r", ""), ]) content = buf.getvalue().encode("utf-8-sig") return StreamingResponse( io.BytesIO(content), media_type="text/csv", headers={"Content-Disposition": "attachment; filename=audit_logs.csv"}, ) @router.get("/recent-logs") async def get_recent_logs( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取最近10条操作日志(兼容旧调用)""" logs = db.query(OperationLog).order_by( OperationLog.created_at.desc() ).limit(10).all() return [_serialize_op_log(log) for log in logs] @router.get("/recent-audit") async def get_recent_audit( admin_user=Depends(get_current_admin), db: Session = Depends(get_db), ): """获取最近10条审计日志(兼容旧调用)""" logs = db.query(AuditLog).order_by( AuditLog.created_at.desc() ).limit(10).all() return [_serialize_audit_log(log) for log in logs]