From 0ed93df6ea32eabf86fe7c0d6df9c6ff6c7d23ce Mon Sep 17 00:00:00 2001 From: pzc Date: Mon, 14 Sep 2026 11:50:50 +0800 Subject: [PATCH] =?UTF-8?q?style:=E7=BB=9F=E8=AE=A1=E5=88=86=E6=9E=90?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=88=A0=E9=99=A4ai=E9=A3=8E=E6=A0=BC?= =?UTF-8?q?=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dao/wl_statistics_dao.py | 63 ++++++++++++++++++++++------------ scheme/wl_statistics_scheme.py | 16 ++++----- 2 files changed, 48 insertions(+), 31 deletions(-) diff --git a/dao/wl_statistics_dao.py b/dao/wl_statistics_dao.py index 08fdf53..bfd3fe9 100644 --- a/dao/wl_statistics_dao.py +++ b/dao/wl_statistics_dao.py @@ -1,5 +1,4 @@ #统计分析 数据访问层(动态查询 + 聚合) -from os.path import join from typing import Optional from sqlalchemy import and_, case, func @@ -11,7 +10,7 @@ from model.wl_score_model import Score from model.wl_student_model import Student from scheme.wl_statistics_scheme import AgeCompareOp, SortOrder -# stu_gender 字段的实际取值,确认数据库里存的到底是什么后改这两行即可 +# stu_gender 字段的实际取值 MALE_VALUE = "男" FEMALE_VALUE = "女" @@ -34,6 +33,14 @@ class StatisticDao: @staticmethod def search_students_by_age(db: Session, op: AgeCompareOp, value: int, value2: Optional[int] = None): + """ + 输入比较条件动态查询符合条件的学员信息 + :param db:数据库会话 + :param op:比较条件(如大于、小于、等于、区间等) + :param value:对比值1 + :param value2:对比值2,选了between后的上界 + :return: + """ if op == AgeCompareOp.between: if value2 is None: raise ValueError("区间查询(between)需要同时提供上界 value2") @@ -45,12 +52,13 @@ class StatisticDao: .filter(Student.is_deleted == 0, condition) \ .order_by(Student.stu_age) \ .all() + @staticmethod def statistic_classes_count(db: Session): """每个班级的总人数 + 按性别细分的分布(含 0 人的空班级)""" result=[] for c in db.query(Class_).filter(Class_.is_deleted==0).all(): - students=[s for s in c.students if s.is_deleted == 0] + students=[s for s in c.students if s.is_deleted == 0]# 班级里的学生,过滤逻辑删除后的 result.append({ "class_id":c.class_id, "total":len(students), @@ -80,6 +88,12 @@ class StatisticDao: @staticmethod def query_score_by_line(db:Session,score_line:float): + """ + 查询每次考试都在分数线以上学生信息 + :param db: + :param score_line: 分数线 + :return: + """ result=[] for s in db.query(Student).filter(Student.is_deleted==0).all(): if all(sc.score>score_line for sc in s.scores): @@ -92,6 +106,12 @@ class StatisticDao: @staticmethod def query_by_fail_count(db:Session,fail_count:int): + """ + 查询有指定次数次以上不及格学生的信息 + :param db: + :param fail_count: 指定次数 + :return: + """ result=[] students=db.query(Student).filter(Student.is_deleted==0).all() for stu in students: @@ -129,6 +149,12 @@ class StatisticDao: @staticmethod def get_info_by_rank(db:Session,rank:int): + """ + 统计就业薪资排名TopN的学生信息 + :param db: + :param rank: Top N 中的N + :return: + """ q=(db.query( Student.stu_name, Student.class_id, @@ -144,15 +170,15 @@ class StatisticDao: return q.all() @staticmethod def get_emp_time(db:Session): - """每个学生的就业时长(天)= offer下发时间 - 就业开放时间 + """统计每个学生的就业时长(天)= offer下发时间 - 就业开放时间 + 未就业(offer_time,和emp_open_time至少一个为null) """ emp_total_time = func.coalesce( - # 未就业学生左外连接后 Emp 列为 NULL,DATEDIFF 也返回 NULL,统一兜成 0 func.datediff(Emp.offer_time, Emp.emp_open_time), - 0, + '', # 未进入就业阶段或者未拿到offer ).label('emp_total_time') - # 逻辑删除条件写在 ON 里,写进 where 会变成 INNER JOIN,未就业学生会被滤掉 + # 每个学生都要统计,所以用外连接 q=db.query( Student.stu_name, emp_total_time, @@ -164,18 +190,15 @@ class StatisticDao: @staticmethod def get_class_avg_emp_time(db:Session, sort_order: SortOrder | None = None): - """每个班级的平均就业时长(天)= offer下发时间 - 就业开放时间 - - 口径: - - 以班级表为主表左外连接,保证没有任何就业学生的班级也出现在结果里 - - 只统计已进入就业阶段(emp_open_time 非空)且已拿到 offer(offer_time 非空)的学生 - - 该班可统计的人数为 0 时 AVG 返回 NULL,出参里 avg_emp_time 给 None,即"无就业学生" + """ + 统计每个班级平均就业时长 + :param db: + :param sort_order:排序(可选) + :return: """ emp_time = func.datediff(Emp.offer_time, Emp.emp_open_time) - # AVG 和 COUNT(表达式) 都会自动忽略 NULL,所以没 offer 时间的学生不参与分子也不占分母 avg_emp_time = func.avg(emp_time).label("avg_emp_time") - # 用 COUNT(表达式) 而不是 COUNT(*),它正好是平均值的分母 - emp_stu_count = func.count(emp_time).label("emp_stu_count") + emp_stu_count = func.count(emp_time).label("emp_stu_count")# 统计有就业时长学生数量 q = db.query( Class_.class_id.label("class_id"), @@ -184,7 +207,6 @@ class StatisticDao: ).select_from(Class_) \ .outerjoin( Student, - # 逻辑删除的过滤条件必须写在 ON 里,写进 where 会把空班级整行滤掉 and_(Student.class_id == Class_.class_id, Student.is_deleted == 0), ) \ .outerjoin( @@ -198,9 +220,8 @@ class StatisticDao: ) \ .group_by(Class_.class_id) - # 动态排序逻辑与 get_class_avg_score 保持一致 + if sort_order == SortOrder.desc: - # MySQL 里 DESC 时 NULL 排最后,"无就业学生"的班级自然落到末尾 q = q.order_by(avg_emp_time.desc()) elif sort_order == SortOrder.asc: q = q.order_by(avg_emp_time.asc()) @@ -210,9 +231,7 @@ class StatisticDao: return [ { "class_id": r.class_id, - # AVG 出来是 Decimal,转 float 并按天保留 1 位小数;None 表示该班无就业学生 - "avg_emp_time": round(float(r.avg_emp_time), 1) - if r.avg_emp_time is not None else None, + "avg_emp_time": round(float(r.avg_emp_time), 1) if r.avg_emp_time is not None else None, "emp_stu_count": r.emp_stu_count, } for r in q.all() diff --git a/scheme/wl_statistics_scheme.py b/scheme/wl_statistics_scheme.py index 997b12d..de232b5 100644 --- a/scheme/wl_statistics_scheme.py +++ b/scheme/wl_statistics_scheme.py @@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict from model.wl_score_model import Score -# ---- 入参:动态查询的比较条件 ---- + class AgeCompareOp(str, Enum): """年龄比较条件:大于/大于等于/小于/小于等于/等于/区间""" gt = "gt" @@ -25,7 +25,7 @@ class SortOrder(str, Enum): desc = "desc" -# ---- 出参:后续统计接口的聚合结果 DTO 放这里 ---- + class ClassGenderStatOut(BaseModel): """班级多维度统计:总人数 + 男女分布""" class_id: Optional[int] = None @@ -49,7 +49,7 @@ class FailCountModel(BaseModel): class ClassAvgScoreOut(BaseModel): """每次考试每个班级的平均分""" - # DAO 返回的是 SQLAlchemy Row 对象,必须开 from_attributes 才能按属性名读取 + # DAO 返回的是 SQLAlchemy Row 对象,开 from_attributes 属性名读取 model_config = ConfigDict(from_attributes=True) class_id:Optional[int]=None @@ -58,26 +58,24 @@ class ClassAvgScoreOut(BaseModel): class EmpTopOut(BaseModel): """就业薪资排名 Top N""" - # 同样是 Row 对象,必须开 from_attributes + model_config = ConfigDict(from_attributes=True) stu_name:Optional[str]=None class_id:Optional[int]=None offer_time:Optional[datetime]=None company_name:Optional[str]=None - # 接口是按薪资排序取前 N 名的,薪资也得一起给出来, - # 否则前端只能看到"谁排前面"却看不出差了多少钱 salary:Optional[int]=None class ClassAvgEmpTimeOut(BaseModel): """每个班级的平均就业时长""" class_id: Optional[int] = None - # 单位:天,保留 1 位小数;为 None 表示该班无就业学生,前端展示"无就业学生" + # 单位:天,保留 1 位小数;为 None 表示该班无就业学生 avg_emp_time: Optional[float] = None - # 参与统计的人数(平均值的分母),为 0 同样表示该班无就业学生 + # 参与统计的人数,为 0 表示该班无就业学生 emp_stu_count: int = 0 class EmpTimeOut(BaseModel): model_config = ConfigDict(from_attributes=True) stu_name:str='' - emp_total_time:int=0 # 单位为天 + emp_total_time:int|str=""# 单位为天,""表示没有开放简历或者没有拿到offer