Files
test/app/api/statistics_api.py
T
2026-09-21 19:03:31 +08:00

163 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""统计分析接口(需求 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))