163 lines
6.6 KiB
Python
163 lines
6.6 KiB
Python
"""统计分析接口(需求 2.6、2.7.2、4.4),统一挂在 /statistics 前缀下。"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from typing import Annotated
|
||
|
||
from fastapi import APIRouter, Query
|
||
|
||
from app.core.config import settings
|
||
from app.core.deps import DbSession, ReadAccount
|
||
from app.core.response import Resp, ok
|
||
from app.dao.student_dao import StudentDao
|
||
from app.schema.common import PageResult
|
||
from app.schema.statistics_schema import (
|
||
AllAboveItem,
|
||
ClassAvgDurationItem,
|
||
ClassAvgScoreItem,
|
||
ClassOverviewItem,
|
||
FailDetailItem,
|
||
FunnelItem,
|
||
OverviewOut,
|
||
SalaryTopItem,
|
||
StudentDurationItem,
|
||
VolatilityItem,
|
||
)
|
||
from app.schema.student_schema import StudentOut
|
||
from app.service.statistics_service import StatisticsService
|
||
|
||
router = APIRouter(prefix="/statistics", tags=["2.6 统计分析"])
|
||
|
||
|
||
# ==================================================================== 2.6.1
|
||
@router.get(
|
||
"/students/by-age",
|
||
response_model=Resp[PageResult[StudentOut]],
|
||
summary="2.6.1 动态年龄范围查询(gt/gte/lt/lte/eq/between)",
|
||
)
|
||
def students_by_age(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
operator: Annotated[str, Query(description="gt / gte / lt / lte / eq / ne / between")] = "gte",
|
||
value: Annotated[int | None, Query(ge=1, le=100, description="年龄阈值")] = None,
|
||
value2: Annotated[int | None, Query(ge=1, le=100, description="between 时的第二个值")] = None,
|
||
class_id: Annotated[int | None, Query()] = None,
|
||
gender: Annotated[int | None, Query(ge=1, le=2)] = None,
|
||
status: Annotated[int | None, Query(ge=1, le=3)] = None,
|
||
advisor_id: Annotated[int | None, Query()] = None,
|
||
education: Annotated[str | None, Query()] = None,
|
||
page: Annotated[int, Query(ge=1)] = 1,
|
||
page_size: Annotated[int, Query(ge=1, le=200)] = 10,
|
||
order_by: Annotated[str, Query(description="支持按 age 排序")] = "age",
|
||
order: Annotated[str, Query(pattern="^(asc|desc)$")] = "asc",
|
||
):
|
||
stmt = StatisticsService.age_query(
|
||
db, operator, value, value2,
|
||
class_id=class_id, gender=gender, status=status, advisor_id=advisor_id,
|
||
education=education, order_by=order_by, order=order,
|
||
)
|
||
items, total, page, pages = StudentDao.paginate(db, stmt, page, page_size)
|
||
return ok({
|
||
"total": total, "page": page, "page_size": page_size, "pages": pages,
|
||
"items": [StudentOut.model_validate(s) for s in items],
|
||
})
|
||
|
||
|
||
@router.get("/classes/overview", response_model=Resp[list[ClassOverviewItem]], summary="2.6.1 多维度班级统计(人数 + 性别分布)")
|
||
def class_overview(db: DbSession, _: ReadAccount):
|
||
return ok(StatisticsService.class_overview(db))
|
||
|
||
|
||
@router.get("/students/age-distribution", summary="年龄分布(图表用)")
|
||
def age_distribution(db: DbSession, _: ReadAccount):
|
||
from app.dao.statistics_dao import StatisticsDao
|
||
|
||
return ok(StatisticsDao.age_distribution(db))
|
||
|
||
|
||
# ==================================================================== 2.6.2
|
||
@router.get("/scores/all-above", response_model=Resp[list[AllAboveItem]], summary="2.6.2 每次考核都在分数线以上的学生")
|
||
def scores_all_above(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
threshold: Annotated[float, Query(ge=0, le=1000, description="分数线")] = 80,
|
||
class_id: Annotated[int | None, Query()] = None,
|
||
):
|
||
rows = StatisticsService.all_above(db, threshold, class_id)
|
||
return ok(rows, msg=f"{threshold:g} 分以上(每次考核都达标)的学生共 {len(rows)} 人")
|
||
|
||
|
||
@router.get("/scores/failures", response_model=Resp[list[FailDetailItem]], summary="2.6.2 不及格次数达到 N 次的学生及明细")
|
||
def score_failures(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
threshold: Annotated[float, Query(ge=0, le=1000, description="不及格线")] = 60,
|
||
min_times: Annotated[int, Query(ge=1, le=50, description="至少不及格几次")] = 2,
|
||
):
|
||
return ok(StatisticsService.fail_students(db, threshold, min_times))
|
||
|
||
|
||
@router.get("/scores/class-average", response_model=Resp[list[ClassAvgScoreItem]], summary="2.6.2 每场考核每个班级的平均分(可动态排序)")
|
||
def class_average(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
exam_seq: Annotated[int | None, Query(ge=1, description="只看某一场;不传=全部场次")] = None,
|
||
class_id: Annotated[int | None, Query()] = None,
|
||
order: Annotated[str, Query(pattern="^(asc|desc)$", description="按平均分升降序")] = "desc",
|
||
pass_line: Annotated[float | None, Query(ge=0, le=1000, description="及格线,用于算及格率")] = None,
|
||
):
|
||
return ok(
|
||
StatisticsService.class_exam_avg(
|
||
db, exam_seq, class_id, order, pass_line if pass_line is not None else settings.SCORE_PASS_LINE
|
||
)
|
||
)
|
||
|
||
|
||
# ==================================================================== 2.6.3
|
||
@router.get("/employment/top-salary", response_model=Resp[list[SalaryTopItem]], summary="2.6.3 就业薪资 Top N")
|
||
def top_salary(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
top_n: Annotated[int, Query(ge=1, le=100, description="取前几名")] = 5,
|
||
):
|
||
return ok(StatisticsService.salary_top(db, top_n))
|
||
|
||
|
||
@router.get("/employment/durations", response_model=Resp[list[StudentDurationItem]], summary="2.6.3 每个学生的就业时长")
|
||
def student_durations(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
class_id: Annotated[int | None, Query()] = None,
|
||
):
|
||
return ok(StatisticsService.student_durations(db, class_id))
|
||
|
||
|
||
@router.get("/employment/class-avg-duration", response_model=Resp[list[ClassAvgDurationItem]], summary="2.6.3 每个班级的平均就业时长")
|
||
def class_avg_duration(db: DbSession, _: ReadAccount):
|
||
return ok(StatisticsService.class_avg_duration(db))
|
||
|
||
|
||
# ==================================================================== 2.7.2
|
||
@router.get("/scores/volatility", response_model=Resp[list[VolatilityItem]], summary="2.7.2 成绩波动最大的 Top N(标准差 / 最大分差)")
|
||
def score_volatility(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
top_n: Annotated[int, Query(ge=1, le=50)] = 5,
|
||
metric: Annotated[str, Query(description="stddev=标准差,range=最大分差")] = "stddev",
|
||
):
|
||
return ok(StatisticsService.score_volatility(db, top_n, metric))
|
||
|
||
|
||
@router.get("/employment/funnel", response_model=Resp[list[FunnelItem]], summary="2.7.2 班级就业漏斗(按就业率降序)")
|
||
def employment_funnel(
|
||
db: DbSession,
|
||
_: ReadAccount,
|
||
high_line: Annotated[float | None, Query(ge=0, description="高薪线,默认取配置值")] = None,
|
||
):
|
||
return ok(StatisticsService.employment_funnel(db, high_line))
|
||
|
||
|
||
@router.get("/overview", response_model=Resp[OverviewOut], summary="仪表盘总览")
|
||
def overview(db: DbSession, _: ReadAccount):
|
||
return ok(StatisticsService.overview(db))
|