智教助手平台:完整初始化

- 前端:Vue3 + TS + Element Plus,24 个页面路由(课件/组题/教案/动画/思维导图/作文批改/命题/课堂/资源/社区等)
- 后端:FastAPI + SQLAlchemy + SQLite,17 个路由模块,AI 服务层含降级模板
- AI:glm-5.x 推理模型已禁用思维链,确保输出真实内容
- 修复:ai_service 两处请求体注入 enable_thinking/thinking disabled
- 测试账号:13900999999 / test1234
This commit is contained in:
Zhang Jing Xuan
2026-07-29 16:29:10 +08:00
commit 0841b1a103
193 changed files with 44995 additions and 0 deletions
+236
View File
@@ -0,0 +1,236 @@
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
]