148 lines
5.9 KiB
Python
148 lines
5.9 KiB
Python
# 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)
|