Files
xs_system/dao/statistics_dao.py
T
2026-09-22 19:00:51 +08:00

213 lines
11 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.
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