2026-09-23 09:28:16 +08:00
|
|
|
|
# ============================================================
|
|
|
|
|
|
# API 接口层 — 对外暴露的 HTTP 路由
|
|
|
|
|
|
# 职责:
|
|
|
|
|
|
# 1. 定义 URL 路径、请求方法(GET/POST)和参数校验规则
|
|
|
|
|
|
# 2. 调用 dao 层获取数据
|
|
|
|
|
|
# 3. 用 schema 层的 ResponseModel 封装统一响应格式返回给前端
|
|
|
|
|
|
# 依赖注入:
|
|
|
|
|
|
# db = Depends(get_db) — 由 FastAPI 自动为每个请求创建并回收数据库会话
|
|
|
|
|
|
# ============================================================
|
|
|
|
|
|
|
2026-09-22 19:00:51 +08:00
|
|
|
|
from fastapi import APIRouter, Depends, Query
|
|
|
|
|
|
from database import get_db
|
|
|
|
|
|
from dao.statistics_dao import StatisticsDao
|
2026-09-23 09:28:16 +08:00
|
|
|
|
from schema.statistics_schema import (
|
|
|
|
|
|
ResponseModel, ResponseModel1, ResponseModel2, ResponseModel3,
|
|
|
|
|
|
ResponseModel4, ResponseModel5, ResponseModel6, ResponseModel7, ResponseModel8
|
|
|
|
|
|
)
|
2026-09-22 19:00:51 +08:00
|
|
|
|
from math import ceil
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
# 统一路由前缀分组,所有接口在 Swagger 里归到"统计分析"标签下
|
2026-09-22 19:00:51 +08:00
|
|
|
|
BasicInformationAPI = APIRouter(tags=['统计分析'])
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/basic-information', summary='查询年龄区间内的学员信息')
|
2026-09-22 19:00:51 +08:00
|
|
|
|
def get_students_by_age_range_api(
|
2026-09-23 09:28:16 +08:00
|
|
|
|
n: int = Query(..., description='页码,从1开始', ge=1),
|
|
|
|
|
|
m: int = Query(..., description='每页条数', ge=1),
|
2026-09-22 19:00:51 +08:00
|
|
|
|
min_age: int = Query(..., description='最小年龄', ge=0),
|
|
|
|
|
|
max_age: int = Query(..., description='最大年龄', le=100),
|
2026-09-23 09:28:16 +08:00
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
按年龄区间分页查询学员基本信息。
|
|
|
|
|
|
前端传入 min_age / max_age,后端换算为出生日期区间后在 student_info 表中过滤。
|
|
|
|
|
|
"""
|
|
|
|
|
|
# 参数合法性校验:最小年龄不能大于最大年龄
|
2026-09-22 19:00:51 +08:00
|
|
|
|
if min_age > max_age:
|
2026-09-23 09:28:16 +08:00
|
|
|
|
return {'code': 400, 'message': '最小年龄不能大于最大年龄', 'total': 0, 'total_pages': 0, 'data': []}
|
2026-09-22 19:00:51 +08:00
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
# 调用 DAO 层查询
|
|
|
|
|
|
req, total = StatisticsDao.get_students_by_age_range_dao(n, m, min_age, max_age, db)
|
|
|
|
|
|
|
|
|
|
|
|
# 计算总页数:ceil 向上取整;total 为 0 时避免除以 0
|
|
|
|
|
|
total_pages = ceil(total / m) if total > 0 else 0
|
2026-09-22 19:00:51 +08:00
|
|
|
|
|
|
|
|
|
|
return ResponseModel(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
total=total,
|
2026-09-23 09:28:16 +08:00
|
|
|
|
total_pages=total_pages,
|
2026-09-22 19:00:51 +08:00
|
|
|
|
data=req)
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/basic-information/{class_id}', summary='统计指定班级的人数以及男生/女生人数')
|
|
|
|
|
|
def get_students_by_class_id_api(class_id: str, db=Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
根据班级编号查询该班级的总人数、男生人数、女生人数。
|
|
|
|
|
|
URL 路径参数 class_id 由 FastAPI 自动解析。
|
|
|
|
|
|
"""
|
|
|
|
|
|
data = StatisticsDao.get_students_by_class_id_dao(class_id, db)
|
2026-09-22 20:18:44 +08:00
|
|
|
|
return ResponseModel7(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
data=data)
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/class-stats', summary='统计所有班级的人数以及男生/女生人数')
|
|
|
|
|
|
def get_class_stats_api(db=Depends(get_db)):
|
|
|
|
|
|
"""
|
|
|
|
|
|
一次性返回所有班级的统计结果(不需要分页,数据量通常可控)。
|
|
|
|
|
|
"""
|
2026-09-22 20:18:44 +08:00
|
|
|
|
data = StatisticsDao.get_class_stats_dao(db)
|
|
|
|
|
|
return ResponseModel8(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
data=data)
|
2026-09-22 19:00:51 +08:00
|
|
|
|
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
@BasicInformationAPI.get('/scores', summary='查询所有考试成绩都大于某个分数阈值的学生')
|
2026-09-22 19:00:51 +08:00
|
|
|
|
def get_students_by_score_api(
|
2026-09-23 09:28:16 +08:00
|
|
|
|
n: int = Query(..., description='页码,从1开始', ge=1),
|
|
|
|
|
|
m: int = Query(..., description='每页条数', ge=1),
|
|
|
|
|
|
score: float = Query(ge=0, le=100, description='分数阈值,0~100'),
|
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
查询最低单科成绩 > score 的学生(即所有考试成绩都超过阈值)。
|
|
|
|
|
|
例如 score=60,表示没有任何一门不及格的学生。
|
|
|
|
|
|
"""
|
2026-09-22 19:00:51 +08:00
|
|
|
|
if score < 0 or score > 100:
|
2026-09-23 09:28:16 +08:00
|
|
|
|
return {'code': 400, 'message': '分数需要在0~100!', 'total': 0, 'total_pages': 0, 'data': []}
|
|
|
|
|
|
|
|
|
|
|
|
req, total = StatisticsDao.get_students_by_score_dao(n, m, score, db)
|
|
|
|
|
|
|
2026-09-22 19:00:51 +08:00
|
|
|
|
return ResponseModel1(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
total=total,
|
2026-09-23 09:28:16 +08:00
|
|
|
|
total_pages=ceil(total / m) if total > 0 else 0,
|
2026-09-22 19:00:51 +08:00
|
|
|
|
data=req)
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
@BasicInformationAPI.get('/scores_no_pass', summary='查询不及格门数 >= fail_count 的学生')
|
|
|
|
|
|
def get_students_by_no_pass_api(
|
|
|
|
|
|
n: int = Query(..., description='页码,从1开始', ge=1),
|
|
|
|
|
|
m: int = Query(..., description='每页条数', ge=1),
|
|
|
|
|
|
fail_count: int = Query(..., description='不及格门数阈值', ge=1),
|
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
统计每个学生不及格门数,返回不及格门数 >= fail_count 的学生名单。
|
|
|
|
|
|
"""
|
|
|
|
|
|
req, total = StatisticsDao.get_student_by_no_pass_dao(n, m, fail_count, db)
|
|
|
|
|
|
return ResponseModel2(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
total=total,
|
|
|
|
|
|
total_pages=ceil(total / m) if total > 0 else 0,
|
|
|
|
|
|
data=req)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/scores_avg', summary='统计每个班级每次考试的平均分(从高到低排序)')
|
2026-09-22 19:00:51 +08:00
|
|
|
|
def get_class_exam_avg_api(
|
2026-09-23 09:28:16 +08:00
|
|
|
|
n: int = Query(..., description='页码,从1开始', ge=1),
|
|
|
|
|
|
m: int = Query(..., description='每页条数', ge=1),
|
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
三表联查 student_score → student_info → class_info,
|
|
|
|
|
|
按 course_id + class_id 分组,计算每个班级每门课的平均分,按平均分降序排列。
|
|
|
|
|
|
"""
|
|
|
|
|
|
req, total = StatisticsDao.get_class_exam_avg_score_dao(n, m, db)
|
2026-09-22 19:00:51 +08:00
|
|
|
|
return ResponseModel3(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
total=total,
|
2026-09-23 09:28:16 +08:00
|
|
|
|
total_pages=ceil(total / m) if total > 0 else 0,
|
2026-09-22 19:00:51 +08:00
|
|
|
|
data=req)
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/salary_top', summary='查询就业薪资前 m 名的学生')
|
2026-09-22 19:00:51 +08:00
|
|
|
|
def get_salary_top_api(
|
2026-09-23 09:28:16 +08:00
|
|
|
|
m: int = Query(5, description='返回前 m 名,默认5', ge=1),
|
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
查询就业薪资最高的前 m 名学生的姓名、班级、就业公司、就业时间和薪资。
|
|
|
|
|
|
每人只取自己历史最高薪资那条记录。
|
|
|
|
|
|
"""
|
|
|
|
|
|
req = StatisticsDao.get_salary_top_dao(m, db)
|
2026-09-22 19:00:51 +08:00
|
|
|
|
return ResponseModel4(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
data=req)
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/time_size', summary='统计每个学生的就业时长(offer下发时间 - 开放简历时间)')
|
2026-09-22 19:00:51 +08:00
|
|
|
|
def get_time_size_api(
|
2026-09-23 09:28:16 +08:00
|
|
|
|
n: int = Query(..., description='页码,从1开始', ge=1),
|
|
|
|
|
|
m: int = Query(..., description='每页条数', ge=1),
|
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
计算每个学生从开放简历到拿到 offer 的天数差 time_size。
|
|
|
|
|
|
只统计开放简历时间不为空的记录。
|
|
|
|
|
|
"""
|
|
|
|
|
|
req, total = StatisticsDao.get_time_size_dao(n, m, db)
|
2026-09-22 19:00:51 +08:00
|
|
|
|
return ResponseModel5(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
total=total,
|
2026-09-23 09:28:16 +08:00
|
|
|
|
total_pages=ceil(total / m) if total > 0 else 0,
|
2026-09-22 19:00:51 +08:00
|
|
|
|
data=req)
|
|
|
|
|
|
|
2026-09-23 09:28:16 +08:00
|
|
|
|
|
|
|
|
|
|
@BasicInformationAPI.get('/class_avg_time_size', summary='统计每个班级的平均就业时长')
|
2026-09-22 19:00:51 +08:00
|
|
|
|
def get_class_avg_time_size_api(
|
2026-09-23 09:28:16 +08:00
|
|
|
|
n: int = Query(..., description='页码,从1开始', ge=1),
|
|
|
|
|
|
m: int = Query(..., description='每页条数', ge=1),
|
|
|
|
|
|
db=Depends(get_db)
|
|
|
|
|
|
):
|
|
|
|
|
|
"""
|
|
|
|
|
|
按班级分组,计算进入就业阶段(resume_open_date 不为空)学生的平均就业时长。
|
|
|
|
|
|
"""
|
|
|
|
|
|
req, total = StatisticsDao.get_class_avg_time_size_dao(n, m, db)
|
2026-09-22 19:00:51 +08:00
|
|
|
|
return ResponseModel6(code=200,
|
|
|
|
|
|
message='查询成功',
|
|
|
|
|
|
total=total,
|
2026-09-23 09:28:16 +08:00
|
|
|
|
total_pages=ceil(total / m) if total > 0 else 0,
|
2026-09-22 19:00:51 +08:00
|
|
|
|
data=req)
|