Files
qihuaiflow/backend/python/app/api/admin/dashboard.py
T

269 lines
9.0 KiB
Python

"""
管理员仪表盘 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]