269 lines
9.0 KiB
Python
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]
|