Files
xs_system/dao/statistics_dao.py
T

213 lines
11 KiB
Python
Raw Normal View History

2026-09-22 19:00:51 +08:00
from model.statistics_model import StudentInfo, StudentScore, ClassInfo, EmploymentInfo
from datetime import date
from dateutil.relativedelta import relativedelta
from sqlalchemy import func # SQL 内置函数生成器(MIN/MAX/COUNT 等)
class StatisticsDao:
@staticmethod
def get_students_by_age_range_dao(n:int,m:int,min_age: int, max_age: int,db):
# 年龄 → 出生日期区间(数据库存 birthday,没有 age 字段)
today = date.today()
earliest_birthday = today - relativedelta(years=max_age) # 最大年龄 → 最早出生
latest_birthday = today - relativedelta(years=min_age) # 最小年龄 → 最晚出生
try:
q = db.query(StudentInfo).\
filter(StudentInfo.is_deleted == '0').\
filter(StudentInfo.birthday >= earliest_birthday).\
filter(StudentInfo.birthday <= latest_birthday)
total = q.count() # 总条数
req = q.offset((n - 1) * m).limit(m).all() # 分页
except Exception as e:
db.rollback()
raise e
else:
return req,total
@staticmethod
def get_students_by_class_id_dao(class_id:str,db):
try:
q = db.query(StudentInfo).filter(StudentInfo.class_id == class_id).\
filter(StudentInfo.is_deleted == '0')
total_count = q.count() # 总人数
male_count = q.filter(StudentInfo.gender == '男').count()
female_count = q.filter(StudentInfo.gender == '女').count()
except Exception as e:
db.rollback()
raise e
else:
return total_count, male_count , female_count
@staticmethod
def get_students_by_score_dao(n:int,m:int,score:float,db):
# GROUP BY 每个学生一行,HAVING MIN(score) > 阈值 → 所有成绩都过线
# SELECT MIN(score) AS score → 返回该学生最低分(schema StudentInfo2 对应字段)
try:
q = (db.query(
StudentInfo.student_name,
StudentInfo.student_id,
func.min(StudentScore.score).label('score') # 起别名,后面 i.score 访问
).join(StudentScore, StudentInfo.student_id == StudentScore.student_id)
.filter(StudentInfo.is_deleted == '0') # 逻辑删除:学生表
.filter(StudentScore.is_deleted == '0') # 逻辑删除:成绩表
.group_by(StudentInfo.student_id, StudentInfo.student_name) # only_full_group_by 要求
.having(func.min(StudentScore.score) > score)) # 组最低分 > 阈值 = 全过线
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
# Row 对象手动转字典,字段对齐 schema StudentInfo2
return [{"student_name":i.student_name,
'student_id':i.student_id,
"score":i.score} for i in req],total
@staticmethod
def get_student_by_no_pass_dao(n:int,m:int,fail_count:int,db):
try:
q = (db.query(
StudentInfo.student_id,
StudentInfo.student_name,
func.count(StudentScore.score_id).label('fail_count')
).join(StudentScore, StudentInfo.student_id == StudentScore.student_id)
.filter(StudentInfo.is_deleted == '0')
.filter(StudentScore.is_deleted == '0')
.filter(StudentScore.is_pass == '0')
.group_by(StudentInfo.student_id, StudentInfo.student_name)
.having(func.count(StudentScore.score_id) >= fail_count))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_id": i.student_id,
"student_name": i.student_name,
"fail_count": i.fail_count} for i in req], total
@staticmethod
def get_class_exam_avg_score_dao(n:int,m:int,db):
# 统计每次考试每个班级的平均分,按平均分从高到低排序
# 三表联查:student_score →(student_id)→ student_info →(class_id)→ class_info
# 分组维度:exam_type + exam_date + course_id + class_id(一场考试×一个班级)
# 聚合:AVG(score) 排序:DESC
try:
q = (db.query(
StudentScore.course_id, # 课程编号(不同科目)
StudentInfo.class_id, # 班级编号,来自学生表
ClassInfo.class_name, # 班级名称,来自班级表
func.avg(func.ifnull(StudentScore.score, 0)).label('avg_score') # 每个学生缺考分按0算,再求班级平均分
).join(StudentInfo, StudentScore.student_id == StudentInfo.student_id) # 成绩→学生:取 class_id
.join(ClassInfo, StudentInfo.class_id == ClassInfo.class_id) # 学生→班级:取 class_name
.filter(StudentInfo.is_deleted == '0') # 过滤已删除学生
.filter(StudentScore.is_deleted == '0') # 过滤已删除成绩
.filter(ClassInfo.is_deleted == '0') # 过滤已停用班级
.group_by(StudentScore.course_id,
StudentInfo.class_id, # 每个班级单独成组
ClassInfo.class_name) # MySQL only_full_group_by 要求
.order_by(func.avg(func.ifnull(StudentScore.score,0)).desc())) # 平均分降序(从高到低)
total = q.count() # 分组后的总组合数
req = q.offset((n - 1) * m).limit(m).all() # 分页:从 (n-1)*m 开始取 m 条
except Exception as e:
db.rollback()
raise e
else:
# Row 对象转 dict,avg_score 保留两位小数对齐前端展示
return [{"course_id": i.course_id,
"class_id": i.class_id,
"class_name": i.class_name,
"avg_score": round(float(i.avg_score), 2)} for i in req], total
@staticmethod
def get_salary_top_dao(m:int,db): # 统计就业薪资最高的前五名学生的姓名,班级和就业时间,就业公司
try:
# 子查询:获取每个学生的最高薪资
sq = (db.query(
EmploymentInfo.student_id,
func.max(EmploymentInfo.salary).label('max_salary')
).filter(EmploymentInfo.is_deleted == '0')
.group_by(EmploymentInfo.student_id)
.order_by(func.max(EmploymentInfo.salary).desc())
.limit(m)
.subquery())
# 主查询:关联学生表、班级表和就业信息表
q = (db.query(
StudentInfo.student_name,
ClassInfo.class_name,
EmploymentInfo.part_time,
EmploymentInfo.company_name,
EmploymentInfo.salary
).join(EmploymentInfo, StudentInfo.student_id == EmploymentInfo.student_id)
.join(ClassInfo, StudentInfo.class_id == ClassInfo.class_id)
.join(sq,
(EmploymentInfo.student_id == sq.c.student_id) &
(EmploymentInfo.salary == sq.c.max_salary))
.filter(StudentInfo.is_deleted == '0')
.filter(ClassInfo.is_deleted == '0')
.filter(EmploymentInfo.is_deleted == '0'))
req = q.all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_name": i.student_name,
"class_name": i.class_name,
"part_time": i.part_time,
"company_name": i.company_name,
"salary": i.salary} for i in req]
@staticmethod
def get_time_size_dao(n:int,m:int,db): # 统计每个学生的就业时长(offer下发时间-就业开放时间)
try:
# 查询学生姓名、就业信息,并计算就业时长(天数)
q = (db.query(
StudentInfo.student_id,
StudentInfo.student_name,
EmploymentInfo.offer_date,
EmploymentInfo.resume_open_date,
func.datediff(EmploymentInfo.offer_date, EmploymentInfo.resume_open_date).label('time_size')
).join(EmploymentInfo, StudentInfo.student_id == EmploymentInfo.student_id)
.filter(StudentInfo.is_deleted == '0')
.filter(EmploymentInfo.is_deleted == '0')
.filter(EmploymentInfo.resume_open_date.isnot(None))) # 确保开放简历时间不为空
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"student_id": i.student_id,
"student_name": i.student_name,
"offer_date": i.offer_date,
"resume_open_date": i.resume_open_date,
"time_size": i.time_size} for i in req], total
@staticmethod
def get_class_avg_time_size_dao(n:int,m:int,db):
# 统计每个班级的平均就业时长(只统计进入就业阶段的学生)
try:
# 查询班级名称和平均就业时长
q = (db.query(
ClassInfo.class_id,
ClassInfo.class_name,
func.avg(func.datediff(EmploymentInfo.offer_date, EmploymentInfo.resume_open_date)).label('avg_time_size')
).join(StudentInfo, ClassInfo.class_id == StudentInfo.class_id)
.join(EmploymentInfo, StudentInfo.student_id == EmploymentInfo.student_id)
.filter(ClassInfo.is_deleted == '0')
.filter(StudentInfo.is_deleted == '0')
.filter(EmploymentInfo.is_deleted == '0')
.filter(EmploymentInfo.resume_open_date.isnot(None)) # 只统计有就业开放时间的学生
.group_by(ClassInfo.class_id, ClassInfo.class_name))
total = q.count()
req = q.offset((n - 1) * m).limit(m).all()
except Exception as e:
db.rollback()
raise e
else:
return [{"class_id": i.class_id,
"class_name": i.class_name,
"avg_time_size": round(float(i.avg_time_size), 2)} for i in req], total