from datetime import datetime, time, timedelta from fastapi import APIRouter, Depends, HTTPException, Query, Request from sqlalchemy import func from sqlalchemy.orm import Session from database import get_db from models.animation import Animation from models.courseware import Courseware from models.credit import CreditAccount, CreditTransaction from models.exercise import Exercise from models.lesson_plan import LessonPlan from models.material import Material from models.resource import Resource, ResourceFavorite from models.user import User from models.audit import AuditLog from schemas.admin import AdminDashboardOut, AdminGrantCredits, AdminUserOut, AdminUserUpdate from schemas.resource import ResourceOut, ResourceAuthorOut from services.auth import get_admin_user from services.audit import log_action from services.credits import grant_credits router = APIRouter(prefix="/api/admin", tags=["管理后台"]) def _user_with_credits(user: User, db: Session) -> dict: account = db.query(CreditAccount).filter(CreditAccount.user_id == user.id).first() data = AdminUserOut.model_validate(user).model_dump() data["credits"] = account.balance if account else 0 return data @router.get("/dashboard", response_model=AdminDashboardOut) def dashboard(current_user: User = Depends(get_admin_user), db: Session = Depends(get_db)): today = datetime.now().date() today_start = datetime.combine(today, time.min) total_users = db.query(func.count(User.id)).scalar() or 0 active_users = db.query(func.count(User.id)).filter(User.is_active.is_(True)).scalar() or 0 admin_users = db.query(func.count(User.id)).filter(User.role == "admin").scalar() or 0 new_users_today = db.query(func.count(User.id)).filter(User.created_at >= today_start).scalar() or 0 total_resources = db.query(func.count(Resource.id)).filter(Resource.status == "active").scalar() or 0 public_resources = db.query(func.count(Resource.id)).filter(Resource.status == "active", Resource.is_public == 1).scalar() or 0 total_coursewares = db.query(func.count(Courseware.id)).filter(Courseware.status != "archived").scalar() or 0 total_animations = db.query(func.count(Animation.id)).filter(Animation.status != "archived").scalar() or 0 total_exercises = db.query(func.count(Exercise.id)).scalar() or 0 total_lesson_plans = db.query(func.count(LessonPlan.id)).scalar() or 0 total_views = db.query(func.coalesce(func.sum(Resource.views), 0)).scalar() or 0 total_downloads = db.query(func.coalesce(func.sum(Resource.downloads), 0)).scalar() or 0 total_favorites = db.query(func.count(ResourceFavorite.id)).scalar() or 0 total_credits_used = db.query(func.coalesce(func.sum(CreditAccount.total_used), 0)).scalar() or 0 exam_count = 0 try: from models.exam import Exam exam_count = db.query(func.count(Exam.id)).scalar() or 0 except Exception: pass return AdminDashboardOut( total_users=total_users, active_users=active_users, admin_users=admin_users, new_users_today=new_users_today, total_resources=total_resources, public_resources=public_resources, total_coursewares=total_coursewares, total_animations=total_animations, total_exercises=total_exercises, total_lesson_plans=total_lesson_plans, total_exams=exam_count, total_views=total_views, total_downloads=total_downloads, total_favorites=total_favorites, total_credits_used=total_credits_used, ) @router.get("/users", response_model=list[AdminUserOut]) def list_users( keyword: str = Query("", description="搜索手机号或姓名"), role: str = Query("", description="按角色筛选"), skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): query = db.query(User) if keyword: like = f"%{keyword}%" query = query.filter((User.phone.contains(like)) | (User.name.contains(like))) if role: query = query.filter(User.role == role) users = query.order_by(User.created_at.desc()).offset(skip).limit(limit).all() return [_user_with_credits(u, db) for u in users] @router.put("/users/{user_id}", response_model=AdminUserOut) def update_user( user_id: int, data: AdminUserUpdate, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="用户不存在") changes = [] if data.role is not None: if data.role not in {"teacher", "admin"}: raise HTTPException(status_code=400, detail="角色无效") changes.append(f"role:{user.role}->{data.role}") user.role = data.role if data.is_active is not None: changes.append(f"active:{user.is_active}->{data.is_active}") user.is_active = data.is_active if data.name is not None: changes.append(f"name:{user.name}->{data.name}") user.name = data.name db.commit() db.refresh(user) log_action(db, action="admin_update_user", user=current_user, request=request, target_type="user", target_id=user.id, detail=f"修改用户 {user.name or user.phone or user.id}:{', '.join(changes) or '无变更'}") return _user_with_credits(user, db) @router.post("/users/{user_id}/grant-credits", response_model=AdminUserOut) def grant_user_credits( user_id: int, data: AdminGrantCredits, request: Request, current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): user = db.query(User).filter(User.id == user_id).first() if not user: raise HTTPException(status_code=404, detail="用户不存在") grant_credits(db, user, data.amount, action="admin_grant", description=f"管理员 {current_user.name} 发放 {data.amount} 积分") db.commit() db.refresh(user) log_action(db, action="admin_grant_credits", user=current_user, request=request, target_type="user", target_id=user.id, detail=f"向 {user.name or user.phone or user.id} 发放 {data.amount} 积分") return _user_with_credits(user, db) @router.get("/resources", response_model=list[ResourceOut]) def list_all_resources( keyword: str = Query(""), resource_type: str = Query(""), status: str = Query("active"), skip: int = Query(0, ge=0), limit: int = Query(20, ge=1, le=100), current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): from sqlalchemy import or_ query = db.query(Resource) if status: query = query.filter(Resource.status == status) if resource_type: query = query.filter(Resource.resource_type == resource_type) if keyword: like = f"%{keyword}%" query = query.filter(or_(Resource.title.contains(like), Resource.description.contains(like))) resources = query.order_by(Resource.created_at.desc()).offset(skip).limit(limit).all() result = [] for res in resources: out = ResourceOut.model_validate(res) out.is_favorited = False result.append(out) return result @router.put("/resources/{resource_id}/moderate", response_model=dict) def moderate_resource( resource_id: int, request: Request, status: str = Query(..., description="active 或 archived"), current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): if status not in {"active", "archived"}: raise HTTPException(status_code=400, detail="状态无效") res = db.query(Resource).filter(Resource.id == resource_id).first() if not res: raise HTTPException(status_code=404, detail="资源不存在") prev = res.status res.status = status db.commit() log_action(db, action="admin_moderate_resource", user=current_user, request=request, target_type="resource", target_id=res.id, detail=f"资源《{res.title}》状态 {prev} -> {status}") return {"success": True, "status": res.status} @router.get("/audit-logs", response_model=list[dict]) def list_audit_logs( action: str | None = None, status: str | None = None, user_id: int | None = None, keyword: str | None = None, skip: int = Query(0, ge=0), limit: int = Query(50, ge=1, le=200), current_user: User = Depends(get_admin_user), db: Session = Depends(get_db), ): """查询安全审计日志(仅管理员)。""" from sqlalchemy import or_ q = db.query(AuditLog) if action: q = q.filter(AuditLog.action == action) if status: q = q.filter(AuditLog.status == status) if user_id: q = q.filter(AuditLog.user_id == user_id) if keyword: like = f"%{keyword}%" q = q.filter(or_(AuditLog.username.contains(like), AuditLog.detail.contains(like), AuditLog.ip.contains(like))) rows = q.order_by(AuditLog.created_at.desc()).offset(skip).limit(limit).all() return [ { "id": r.id, "user_id": r.user_id, "username": r.username, "action": r.action, "target_type": r.target_type, "target_id": r.target_id, "detail": r.detail, "ip": r.ip, "user_agent": r.user_agent, "status": r.status, "created_at": r.created_at.isoformat() if r.created_at else None, } for r in rows ]