style:统计分析模块删除ai风格注释
This commit is contained in:
+41
-22
@@ -1,5 +1,4 @@
|
|||||||
#统计分析 数据访问层(动态查询 + 聚合)
|
#统计分析 数据访问层(动态查询 + 聚合)
|
||||||
from os.path import join
|
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
from sqlalchemy import and_, case, func
|
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 model.wl_student_model import Student
|
||||||
from scheme.wl_statistics_scheme import AgeCompareOp, SortOrder
|
from scheme.wl_statistics_scheme import AgeCompareOp, SortOrder
|
||||||
|
|
||||||
# stu_gender 字段的实际取值,确认数据库里存的到底是什么后改这两行即可
|
# stu_gender 字段的实际取值
|
||||||
MALE_VALUE = "男"
|
MALE_VALUE = "男"
|
||||||
FEMALE_VALUE = "女"
|
FEMALE_VALUE = "女"
|
||||||
|
|
||||||
@@ -34,6 +33,14 @@ class StatisticDao:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def search_students_by_age(db: Session, op: AgeCompareOp,
|
def search_students_by_age(db: Session, op: AgeCompareOp,
|
||||||
value: int, value2: Optional[int] = None):
|
value: int, value2: Optional[int] = None):
|
||||||
|
"""
|
||||||
|
输入比较条件动态查询符合条件的学员信息
|
||||||
|
:param db:数据库会话
|
||||||
|
:param op:比较条件(如大于、小于、等于、区间等)
|
||||||
|
:param value:对比值1
|
||||||
|
:param value2:对比值2,选了between后的上界
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
if op == AgeCompareOp.between:
|
if op == AgeCompareOp.between:
|
||||||
if value2 is None:
|
if value2 is None:
|
||||||
raise ValueError("区间查询(between)需要同时提供上界 value2")
|
raise ValueError("区间查询(between)需要同时提供上界 value2")
|
||||||
@@ -45,12 +52,13 @@ class StatisticDao:
|
|||||||
.filter(Student.is_deleted == 0, condition) \
|
.filter(Student.is_deleted == 0, condition) \
|
||||||
.order_by(Student.stu_age) \
|
.order_by(Student.stu_age) \
|
||||||
.all()
|
.all()
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def statistic_classes_count(db: Session):
|
def statistic_classes_count(db: Session):
|
||||||
"""每个班级的总人数 + 按性别细分的分布(含 0 人的空班级)"""
|
"""每个班级的总人数 + 按性别细分的分布(含 0 人的空班级)"""
|
||||||
result=[]
|
result=[]
|
||||||
for c in db.query(Class_).filter(Class_.is_deleted==0).all():
|
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({
|
result.append({
|
||||||
"class_id":c.class_id,
|
"class_id":c.class_id,
|
||||||
"total":len(students),
|
"total":len(students),
|
||||||
@@ -80,6 +88,12 @@ class StatisticDao:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query_score_by_line(db:Session,score_line:float):
|
def query_score_by_line(db:Session,score_line:float):
|
||||||
|
"""
|
||||||
|
查询每次考试都在分数线以上学生信息
|
||||||
|
:param db:
|
||||||
|
:param score_line: 分数线
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
result=[]
|
result=[]
|
||||||
for s in db.query(Student).filter(Student.is_deleted==0).all():
|
for s in db.query(Student).filter(Student.is_deleted==0).all():
|
||||||
if all(sc.score>score_line for sc in s.scores):
|
if all(sc.score>score_line for sc in s.scores):
|
||||||
@@ -92,6 +106,12 @@ class StatisticDao:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def query_by_fail_count(db:Session,fail_count:int):
|
def query_by_fail_count(db:Session,fail_count:int):
|
||||||
|
"""
|
||||||
|
查询有指定次数次以上不及格学生的信息
|
||||||
|
:param db:
|
||||||
|
:param fail_count: 指定次数
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
result=[]
|
result=[]
|
||||||
students=db.query(Student).filter(Student.is_deleted==0).all()
|
students=db.query(Student).filter(Student.is_deleted==0).all()
|
||||||
for stu in students:
|
for stu in students:
|
||||||
@@ -129,6 +149,12 @@ class StatisticDao:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_info_by_rank(db:Session,rank:int):
|
def get_info_by_rank(db:Session,rank:int):
|
||||||
|
"""
|
||||||
|
统计就业薪资排名TopN的学生信息
|
||||||
|
:param db:
|
||||||
|
:param rank: Top N 中的N
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
q=(db.query(
|
q=(db.query(
|
||||||
Student.stu_name,
|
Student.stu_name,
|
||||||
Student.class_id,
|
Student.class_id,
|
||||||
@@ -144,15 +170,15 @@ class StatisticDao:
|
|||||||
return q.all()
|
return q.all()
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_emp_time(db:Session):
|
def get_emp_time(db:Session):
|
||||||
"""每个学生的就业时长(天)= offer下发时间 - 就业开放时间
|
"""统计每个学生的就业时长(天)= offer下发时间 - 就业开放时间
|
||||||
|
未就业(offer_time,和emp_open_time至少一个为null)
|
||||||
"""
|
"""
|
||||||
emp_total_time = func.coalesce(
|
emp_total_time = func.coalesce(
|
||||||
# 未就业学生左外连接后 Emp 列为 NULL,DATEDIFF 也返回 NULL,统一兜成 0
|
|
||||||
func.datediff(Emp.offer_time, Emp.emp_open_time),
|
func.datediff(Emp.offer_time, Emp.emp_open_time),
|
||||||
0,
|
'', # 未进入就业阶段或者未拿到offer
|
||||||
).label('emp_total_time')
|
).label('emp_total_time')
|
||||||
|
|
||||||
# 逻辑删除条件写在 ON 里,写进 where 会变成 INNER JOIN,未就业学生会被滤掉
|
# 每个学生都要统计,所以用外连接
|
||||||
q=db.query(
|
q=db.query(
|
||||||
Student.stu_name,
|
Student.stu_name,
|
||||||
emp_total_time,
|
emp_total_time,
|
||||||
@@ -164,18 +190,15 @@ class StatisticDao:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_class_avg_emp_time(db:Session, sort_order: SortOrder | None = None):
|
def get_class_avg_emp_time(db:Session, sort_order: SortOrder | None = None):
|
||||||
"""每个班级的平均就业时长(天)= offer下发时间 - 就业开放时间
|
"""
|
||||||
|
统计每个班级平均就业时长
|
||||||
口径:
|
:param db:
|
||||||
- 以班级表为主表左外连接,保证没有任何就业学生的班级也出现在结果里
|
:param sort_order:排序(可选)
|
||||||
- 只统计已进入就业阶段(emp_open_time 非空)且已拿到 offer(offer_time 非空)的学生
|
:return:
|
||||||
- 该班可统计的人数为 0 时 AVG 返回 NULL,出参里 avg_emp_time 给 None,即"无就业学生"
|
|
||||||
"""
|
"""
|
||||||
emp_time = func.datediff(Emp.offer_time, Emp.emp_open_time)
|
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")
|
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(
|
q = db.query(
|
||||||
Class_.class_id.label("class_id"),
|
Class_.class_id.label("class_id"),
|
||||||
@@ -184,7 +207,6 @@ class StatisticDao:
|
|||||||
).select_from(Class_) \
|
).select_from(Class_) \
|
||||||
.outerjoin(
|
.outerjoin(
|
||||||
Student,
|
Student,
|
||||||
# 逻辑删除的过滤条件必须写在 ON 里,写进 where 会把空班级整行滤掉
|
|
||||||
and_(Student.class_id == Class_.class_id, Student.is_deleted == 0),
|
and_(Student.class_id == Class_.class_id, Student.is_deleted == 0),
|
||||||
) \
|
) \
|
||||||
.outerjoin(
|
.outerjoin(
|
||||||
@@ -198,9 +220,8 @@ class StatisticDao:
|
|||||||
) \
|
) \
|
||||||
.group_by(Class_.class_id)
|
.group_by(Class_.class_id)
|
||||||
|
|
||||||
# 动态排序逻辑与 get_class_avg_score 保持一致
|
|
||||||
if sort_order == SortOrder.desc:
|
if sort_order == SortOrder.desc:
|
||||||
# MySQL 里 DESC 时 NULL 排最后,"无就业学生"的班级自然落到末尾
|
|
||||||
q = q.order_by(avg_emp_time.desc())
|
q = q.order_by(avg_emp_time.desc())
|
||||||
elif sort_order == SortOrder.asc:
|
elif sort_order == SortOrder.asc:
|
||||||
q = q.order_by(avg_emp_time.asc())
|
q = q.order_by(avg_emp_time.asc())
|
||||||
@@ -210,9 +231,7 @@ class StatisticDao:
|
|||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"class_id": r.class_id,
|
"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,
|
"emp_stu_count": r.emp_stu_count,
|
||||||
}
|
}
|
||||||
for r in q.all()
|
for r in q.all()
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict
|
|||||||
from model.wl_score_model import Score
|
from model.wl_score_model import Score
|
||||||
|
|
||||||
|
|
||||||
# ---- 入参:动态查询的比较条件 ----
|
|
||||||
class AgeCompareOp(str, Enum):
|
class AgeCompareOp(str, Enum):
|
||||||
"""年龄比较条件:大于/大于等于/小于/小于等于/等于/区间"""
|
"""年龄比较条件:大于/大于等于/小于/小于等于/等于/区间"""
|
||||||
gt = "gt"
|
gt = "gt"
|
||||||
@@ -25,7 +25,7 @@ class SortOrder(str, Enum):
|
|||||||
desc = "desc"
|
desc = "desc"
|
||||||
|
|
||||||
|
|
||||||
# ---- 出参:后续统计接口的聚合结果 DTO 放这里 ----
|
|
||||||
class ClassGenderStatOut(BaseModel):
|
class ClassGenderStatOut(BaseModel):
|
||||||
"""班级多维度统计:总人数 + 男女分布"""
|
"""班级多维度统计:总人数 + 男女分布"""
|
||||||
class_id: Optional[int] = None
|
class_id: Optional[int] = None
|
||||||
@@ -49,7 +49,7 @@ class FailCountModel(BaseModel):
|
|||||||
|
|
||||||
class ClassAvgScoreOut(BaseModel):
|
class ClassAvgScoreOut(BaseModel):
|
||||||
"""每次考试每个班级的平均分"""
|
"""每次考试每个班级的平均分"""
|
||||||
# DAO 返回的是 SQLAlchemy Row 对象,必须开 from_attributes 才能按属性名读取
|
# DAO 返回的是 SQLAlchemy Row 对象,开 from_attributes 属性名读取
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
class_id:Optional[int]=None
|
class_id:Optional[int]=None
|
||||||
@@ -58,26 +58,24 @@ class ClassAvgScoreOut(BaseModel):
|
|||||||
|
|
||||||
class EmpTopOut(BaseModel):
|
class EmpTopOut(BaseModel):
|
||||||
"""就业薪资排名 Top N"""
|
"""就业薪资排名 Top N"""
|
||||||
# 同样是 Row 对象,必须开 from_attributes
|
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
stu_name:Optional[str]=None
|
stu_name:Optional[str]=None
|
||||||
class_id:Optional[int]=None
|
class_id:Optional[int]=None
|
||||||
offer_time:Optional[datetime]=None
|
offer_time:Optional[datetime]=None
|
||||||
company_name:Optional[str]=None
|
company_name:Optional[str]=None
|
||||||
# 接口是按薪资排序取前 N 名的,薪资也得一起给出来,
|
|
||||||
# 否则前端只能看到"谁排前面"却看不出差了多少钱
|
|
||||||
salary:Optional[int]=None
|
salary:Optional[int]=None
|
||||||
|
|
||||||
class ClassAvgEmpTimeOut(BaseModel):
|
class ClassAvgEmpTimeOut(BaseModel):
|
||||||
"""每个班级的平均就业时长"""
|
"""每个班级的平均就业时长"""
|
||||||
class_id: Optional[int] = None
|
class_id: Optional[int] = None
|
||||||
# 单位:天,保留 1 位小数;为 None 表示该班无就业学生,前端展示"无就业学生"
|
# 单位:天,保留 1 位小数;为 None 表示该班无就业学生
|
||||||
avg_emp_time: Optional[float] = None
|
avg_emp_time: Optional[float] = None
|
||||||
# 参与统计的人数(平均值的分母),为 0 同样表示该班无就业学生
|
# 参与统计的人数,为 0 表示该班无就业学生
|
||||||
emp_stu_count: int = 0
|
emp_stu_count: int = 0
|
||||||
|
|
||||||
class EmpTimeOut(BaseModel):
|
class EmpTimeOut(BaseModel):
|
||||||
model_config = ConfigDict(from_attributes=True)
|
model_config = ConfigDict(from_attributes=True)
|
||||||
stu_name:str=''
|
stu_name:str=''
|
||||||
emp_total_time:int=0 # 单位为天
|
emp_total_time:int|str=""# 单位为天,""表示没有开放简历或者没有拿到offer
|
||||||
|
|||||||
Reference in New Issue
Block a user