# api/statistics.py # 统计分析模块接口:动态年龄查询、班级统计、成绩统计、就业统计、高级筛选器、聚合统计 from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from core.deps import require_roles from dao.statistics_dao import StatisticsDAO, FilterBuilder from database import get_db from scheme.statistics import ( FilterRequest, FilterResponse, ClassGenderStat, AllAboveStudent, FailStudent, ClassExamAvg, TopSalaryStudent, EmploymentDuration, ClassAvgDuration, ScoreVolatility, EmploymentFunnel, ) from scheme.students import StudentResponse router = APIRouter() # 统计模块统一要求 admin / teacher 角色(各接口通过 Depends(require_roles(...)) 校验) # ==================== 2.6.1 基本信息动态统计 ==================== @router.get("/students/by-age", response_model=list[StudentResponse], summary="动态年龄范围查询") async def students_by_age( op: str = Query(..., description="比较条件:gt/lt/eq/gte/lte/between"), value: int = Query(None, description="op 为 gt/lt/eq/gte/lte 时必填"), min_value: int = Query(None, description="op=between 时的下界"), max_value: int = Query(None, description="op=between 时的上界"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): try: items = StatisticsDAO.students_by_age(db, op=op, value=value, min_value=min_value, max_value=max_value) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) return items @router.get("/class/gender-stats", response_model=list[ClassGenderStat], summary="多维度班级统计(总人数+男女分布)") async def class_gender_stats( db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.class_gender_stats(db) # ==================== 2.6.2 成绩综合统计 ==================== @router.get("/score/all-above", response_model=list[AllAboveStudent], summary="每次考试都在分数线以上的学生") async def score_all_above( line: float = Query(..., ge=0, le=100, description="分数线,如 80"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.students_all_above(db, line) @router.get("/score/fail", response_model=list[FailStudent], summary="不及格次数>=N的学生(含明细)") async def score_fail( times: int = Query(..., ge=1, description="不及格次数阈值,如 2"), line: float = Query(60.0, ge=0, le=100, description="及格线,默认 60"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.fail_students(db, times, line) @router.get("/score/class-avg", response_model=list[ClassExamAvg], summary="每次考试每个班级平均分(支持动态排序)") async def score_class_avg( exam_id: int = Query(None, ge=1, description="考核序次(不传则返回所有考核)"), order: str = Query("desc", pattern="^(asc|desc)$", description="按平均分排序方向"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.class_exam_avg(db, exam_id=exam_id, order=order) # ==================== 2.6.3 就业数据统计 ==================== @router.get("/employment/top-salary", response_model=list[TopSalaryStudent], summary="就业薪资排名 Top N") async def employment_top_salary( n: int = Query(..., ge=1, le=100, description="取前 N 名"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.top_salary(db, n) @router.get("/employment/duration", response_model=list[EmploymentDuration], summary="每个学生的就业时长(天)") async def employment_duration( db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.employment_durations(db) @router.get("/employment/class-avg-duration", response_model=list[ClassAvgDuration], summary="每个班级平均就业时长") async def employment_class_avg_duration( db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.class_avg_duration(db) # ==================== 2.7.1 通用高级筛选器 ==================== @router.post("/filter", response_model=FilterResponse, summary="通用高级筛选器(AND/OR 嵌套规则)") async def advanced_filter( body: FilterRequest, db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): try: total, items = FilterBuilder.query_students(db, body.rules) except ValueError as e: raise HTTPException(status_code=400, detail=str(e)) # 组装为字典响应(含班级名称) data = [] for s in items: row = StudentResponse.model_validate(s).model_dump() if s.classes: row["class_name"] = s.classes.class_name data.append(row) return FilterResponse(total=total, items=data) # ==================== 2.7.2 多维度聚合统计 ==================== @router.get("/score/volatility", response_model=list[ScoreVolatility], summary="成绩波动最大 Top N(最大分差)") async def score_volatility( top_n: int = Query(5, ge=1, le=50, description="取前 N 名,默认 5"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.score_volatility(db, top_n) @router.get("/employment/funnel", response_model=list[EmploymentFunnel], summary="班级就业漏斗(总人数→已就业→高薪→就业率)") async def employment_funnel( high_salary_line: float = Query(10000.0, ge=0, description="高薪线,默认 10000"), db: Session = Depends(get_db), _user=Depends(require_roles("admin", "teacher")), ): return StatisticsDAO.employment_funnel(db, high_salary_line)