From 0a1a7eec16795d5edabf82315229c6ac70d2ba62 Mon Sep 17 00:00:00 2001 From: wangtian <1582714703@qq.com> Date: Tue, 22 Sep 2026 19:00:51 +0800 Subject: [PATCH] =?UTF-8?q?=E7=BB=9F=E8=AE=A1=E5=88=86=E6=9E=90=E6=A8=A1?= =?UTF-8?q?=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 +- api/__init__.py | 0 api/statistics_api.py | 105 ++++++++++++++++++ dao/__init__.py | 0 dao/statistics_dao.py | 213 ++++++++++++++++++++++++++++++++++++ database.py | 20 ++++ main.py | 15 +++ model/__init__.py | 0 model/statistics_model.py | 118 ++++++++++++++++++++ schema/__init__.py | 0 schema/statistics_schema.py | 104 ++++++++++++++++++ 11 files changed, 577 insertions(+), 1 deletion(-) create mode 100644 api/__init__.py create mode 100644 api/statistics_api.py create mode 100644 dao/__init__.py create mode 100644 dao/statistics_dao.py create mode 100644 database.py create mode 100644 main.py create mode 100644 model/__init__.py create mode 100644 model/statistics_model.py create mode 100644 schema/__init__.py create mode 100644 schema/statistics_schema.py diff --git a/README.md b/README.md index 53159b4..50fd915 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# xs_system +# students-system +学生管理系统 \ No newline at end of file diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/statistics_api.py b/api/statistics_api.py new file mode 100644 index 0000000..466a027 --- /dev/null +++ b/api/statistics_api.py @@ -0,0 +1,105 @@ +from fastapi import APIRouter, Depends, Query +from database import get_db +from dao.statistics_dao import StatisticsDao +from schema.statistics_schema import ResponseModel,ResponseModel1,ResponseModel2,ResponseModel3,ResponseModel4,ResponseModel5,ResponseModel6 +from math import ceil + +BasicInformationAPI = APIRouter(tags=['统计分析']) + +@BasicInformationAPI.get('/basic-information',summary='查询年龄区间内的学员信息') +def get_students_by_age_range_api( + n:int=Query(..., description='页码,从1开始', ge=1), + m:int=Query(..., description='每页条数', ge=1), + min_age: int = Query(..., description='最小年龄', ge=0), + max_age: int = Query(..., description='最大年龄', le=100), + db = Depends(get_db)): + if min_age > max_age: + return {f'code=400,message=最小年龄不能大于最大年龄'} + + req,total = StatisticsDao.get_students_by_age_range_dao(n,m,min_age, max_age,db) + + return ResponseModel(code=200, + message='查询成功', + total=total, + total_pages=ceil(total/m) if total > 0 else 0, + data=req) + +@BasicInformationAPI.get('/basic-information/{class_id}',summary='统计每个班级的⼈数以及男⽣/⼥⽣的⼈数') +def get_students_by_class_id_api(class_id:str, + db = Depends(get_db)): + total_count , male_count , female_count = StatisticsDao.get_students_by_class_id_dao(class_id,db) + return {f'code:200,message=查询成功, 全班总人数:{total_count}, 班级男生人数:{male_count}, 班级女生人数:{female_count}'} + + +@BasicInformationAPI.get('/scores',summary='在某个分数段的学生') +def get_students_by_score_api( + n:int=Query(..., description='页码,从1开始', ge=1), + m:int=Query(..., description='每页条数', ge=1), + score:float=Query(ge=0,le=100), + db = Depends(get_db)): + if score < 0 or score > 100: + return {f'code=400,message=分数需要在0~100!'} + req,total = StatisticsDao.get_students_by_score_dao(n,m,score,db) + return ResponseModel1(code=200, + message='查询成功', + total=total, + total_pages=ceil(total/m) if total > 0 else 0, + data=req) + +@BasicInformationAPI.get('/scores_no_pass',summary='查询有n门成绩不合格的学生') +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)): + 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='统计每次考试每个班级的平均分(从高到低排序)') +def get_class_exam_avg_api( + n:int=Query(..., description='页码,从1开始', ge=1), + m:int=Query(..., description='每页条数', ge=1), + db = Depends(get_db)): + req,total = StatisticsDao.get_class_exam_avg_score_dao(n,m,db) + return ResponseModel3(code=200, + message='查询成功', + total=total, + total_pages=ceil(total/m) if total > 0 else 0, + data=req) + +@BasicInformationAPI.get('/salary_top', summary='查询就业薪资最高的前五名学生') +def get_salary_top_api( + m:int=Query(5, description='查询前m名', ge=1), + db = Depends(get_db)): + req = StatisticsDao.get_salary_top_dao(m,db) + return ResponseModel4(code=200, + message='查询成功', + data=req) + +@BasicInformationAPI.get('/time_size', summary='统计每个学生的就业时长(offer下发时间-就业开放时间)') +def get_time_size_api( + n:int=Query(..., description='页码,从1开始', ge=1), + m:int=Query(..., description='每页条数', ge=1), + db = Depends(get_db)): + req,total = StatisticsDao.get_time_size_dao(n,m,db) + return ResponseModel5(code=200, + message='查询成功', + total=total, + total_pages=ceil(total/m) if total > 0 else 0, + data=req) + +@BasicInformationAPI.get('/class_avg_time_size', summary='统计每个班级的平均就业时长(只统计进入就业阶段的学生)') +def get_class_avg_time_size_api( + n:int=Query(..., description='页码,从1开始', ge=1), + m:int=Query(..., description='每页条数', ge=1), + db = Depends(get_db)): + req,total = StatisticsDao.get_class_avg_time_size_dao(n,m,db) + return ResponseModel6(code=200, + message='查询成功', + total=total, + total_pages=ceil(total/m) if total > 0 else 0, + data=req) \ No newline at end of file diff --git a/dao/__init__.py b/dao/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dao/statistics_dao.py b/dao/statistics_dao.py new file mode 100644 index 0000000..f146577 --- /dev/null +++ b/dao/statistics_dao.py @@ -0,0 +1,213 @@ +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 \ No newline at end of file diff --git a/database.py b/database.py new file mode 100644 index 0000000..5e28742 --- /dev/null +++ b/database.py @@ -0,0 +1,20 @@ +from sqlalchemy import * +from sqlalchemy.orm import declarative_base,sessionmaker + +db_url = "mysql+pymysql://root:123456@127.0.0.1:3306/student?charset=utf8mb4" #创建连接对象 +engine = create_engine(db_url) #与数据库进行连接 + +Base = declarative_base() # 执行函数,返回一个基类 + + +Session = sessionmaker(bind=engine + ,autoflush=False + ,autocommit = False + ) + +def get_db(): + db = Session() + try: + yield db + finally: + db.close() \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..4b09230 --- /dev/null +++ b/main.py @@ -0,0 +1,15 @@ +from fastapi import FastAPI +from database import engine, Base +from api.statistics_api import BasicInformationAPI +from model import statistics_model #不可以删 +Base.metadata.create_all(engine) #创建所有的表 + +app = FastAPI() + +app.include_router(BasicInformationAPI) + + +if __name__ == '__main__': + import uvicorn + + uvicorn.run("main:app", host='0.0.0.0', port=12345) \ No newline at end of file diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/statistics_model.py b/model/statistics_model.py new file mode 100644 index 0000000..debfa21 --- /dev/null +++ b/model/statistics_model.py @@ -0,0 +1,118 @@ +from sqlalchemy import * +from database import DATETIME,Base,Column,Integer,String + + +class StudentInfo(Base): + """学生基本信息表""" + __tablename__ = 'student_info' + + student_id = Column(String(20), primary_key=True, comment='学生id') + student_name = Column(String(50), nullable=False, comment='学生姓名') + gender = Column(Enum('男', '女'), nullable=False, comment='性别') + id_card = Column(String(18), nullable=False, comment='身份证号') + birthday = Column(Date, nullable=False, comment='出生日期') + ethnicity = Column(String(20), nullable=False, comment='民族') + region_id = Column(String(20), ForeignKey('region.regio_id'),nullable=False, comment='籍贯编码,连接地区表') + phone = Column(String(11), nullable=False, comment='手机号') + major = Column(String(20), nullable=False, comment='就读专业') + class_id = Column(String(20), nullable=False, comment='班级,连接班级表') + enrollment_date = Column(Date, nullable=False, comment='入学日期') + graduation_date = Column(Date, nullable=False, comment='毕业日期') + student_status = Column(Enum('在读', '休学', '退学', '毕业'), nullable=False, comment='学籍状态') + education_level = Column(Enum('大专', '本科', '硕士研究生', '博士研究生'), nullable=False, comment='学历') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除,0=未删除, 1=已删除') + create_time = Column(Date, nullable=False, comment='创建时间') + update_time = Column(Date, nullable=False, comment='更新时间') + + +class Region(Base): + """地区维度表""" + __tablename__ = 'region' + + regio_id = Column(String(50), primary_key=True, comment='地区编码') + province = Column(String(50), nullable=False, comment='省/直辖市') + city = Column(String(50), nullable=False, comment='市') + district = Column(String(50), nullable=False, comment='区,县') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除') + create_time = Column(DATETIME, nullable=False, comment='创建时间') + update_time = Column(DATETIME, nullable=False, comment='更新时间') + +class EmploymentInfo(Base): + """学生就业信息表""" + __tablename__ = 'employment_info' + + emp_id = Column(String(50), primary_key=True, comment='就业记录ID') + student_id = Column(String(20), ForeignKey('student_info.student_id'), nullable=False, + comment='学号,连接学生表') + regio_id = Column(String(50), ForeignKey('region.regio_id'), nullable=False, comment='就业地区编码,连接地区表') + job_name = Column(String(50), nullable=False, comment='就业岗位名称') + company_name = Column(String(50), nullable=False, comment='就业公司名称') + salary = Column(Float, nullable=False, comment='就业月薪(元)') + offer_date = Column(Date, nullable=False, comment='offer下发/签约时间') + part_time = Column(Date, nullable=False, comment='就业时间') + resume_open_date = Column(Date, nullable=True, comment='开放简历时间') + employment_status = Column(Enum('1', '2', '3', '4'), nullable=False, + comment='就业状态:1=未就业, 2,=已就业,3=升学, 4=灵活就业') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除') + create_time = Column(DATETIME, nullable=False, comment='创建时间') + update_time = Column(DATETIME, nullable=False, comment='更新时间') + +class StudentScore(Base): + """学生考核成绩表""" + __tablename__ = 'student_score' + + score_id = Column(Integer, primary_key=True, autoincrement=True, comment='成绩记录自增主键') + student_id = Column(String(20), ForeignKey('student_info.student_id'),nullable=False, + comment='学号,连接学生表') + course_id = Column(String(20), ForeignKey('course_info.course_id'),nullable=False, comment='课程编号,连接课程表') + exam_type = Column(Enum('期中', '期末', '月考', '周考', '模拟考'), nullable=False, + comment='考试类型:1=期中, 2=期末, 3=月考, 4=周考,5=模拟考') + score = Column(Float, nullable=True, comment='考试分数0~100,缺考为NULL') + score_level = Column(String(10), nullable=False, comment='成绩等级') + is_pass = Column(Enum('0', '1'), nullable=False, comment='是否及格:1=及格, 0=不及格(可由score>=60判断)') + exam_date = Column(Date, nullable=False, comment='考试日期') + exam_category = Column(Enum('首考', '重考', '补考'), nullable=False, comment='考试分类:1=首考, 2=补考, 3=重考') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除') + create_time = Column(DATETIME, nullable=False, comment='创建时间') + update_time = Column(DATETIME, nullable=False, comment='更新时间') + +class ClassInfo(Base): + """班级信息表""" + __tablename__ = 'class_info' + + class_id = Column(String(50), primary_key=True, comment='班级业务编码') + class_name = Column(String(50), nullable=False, comment='班级名称') + grade_year = Column(Date, nullable=False, comment='入学年级') + status = Column(Enum('1', '2', '3'), nullable=False, comment='班级状态:1=在读 2=已毕业 3=已停用') + tags = Column(String(100), nullable=False, comment='存储多维标签,例如:["重点班", "科技特长", "2026届"]') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除') + create_time = Column(DATETIME, nullable=False, comment='创建时间') + update_time = Column(DATETIME, nullable=False, comment='更新时间') + +class TeacherInfo(Base): + """教师基本信息表""" + __tablename__ = 'teacher_info' + + teacher_id = Column(String(20), primary_key=True, comment='教师编号') + teacher_name = Column(String(20), nullable=False, comment='教师姓名') + gender = Column(Enum('男', '女'), nullable=False, comment='性别') + birthday = Column(Date, nullable=False, comment='出生日期') + region_id = Column(String(50), ForeignKey('region.regio_id'), nullable=False, comment='籍贯地区编码') + email = Column(String(50), nullable=False, comment='邮箱') + phone = Column(String(11), nullable=False, comment='手机号码') + graduation_school = Column(String(50), nullable=False, comment='毕业学校') + teacher_rank = Column(Enum('1', '2', '3', '4'), nullable=False, comment='职称:1=初级, 2=中级, 3=高级, 4=特级') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除') + create_time = Column(DATETIME, nullable=False, comment='创建时间') + update_time = Column(DATETIME, nullable=False, comment='更新时间') + +class CourseInfo(Base): + """课程基础信息""" + __tablename__ = 'course_info' + + course_id = Column(String(30), primary_key=True, comment='课程编号') + course_name = Column(String(30), nullable=False, comment='课程名称') + course_type = Column(Enum('1', '2', '3'), nullable=False, comment='课程类型:1 必修课 2 选修课 3 实训课') + is_deleted = Column(Enum('0', '1'), nullable=False, comment='逻辑删除:0=未删除, 1=已删除') + create_time = Column(DATETIME, nullable=False, comment='创建时间') + update_time = Column(DATETIME, nullable=False, comment='更新时间') diff --git a/schema/__init__.py b/schema/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/schema/statistics_schema.py b/schema/statistics_schema.py new file mode 100644 index 0000000..b7f4a64 --- /dev/null +++ b/schema/statistics_schema.py @@ -0,0 +1,104 @@ +from pydantic import BaseModel,ConfigDict # FastAPI 推荐用 pydantic 做数据校验和序列化 +from datetime import date # 日期类型,用于 birthday、enrollment_date 等字段 +from typing import List # 类型提示,声明列表 + +class StudentInfo1(BaseModel): + model_config = ConfigDict(from_attributes=True) + student_id: str # 学号 + student_name: str # 姓名 + gender: str # 性别(男/女) + id_card: str # 身份证号 + birthday: date # 出生日期 + ethnicity: str # 民族 + region_id: str # 籍贯编码(关联 region表) + phone: str # 手机号 + major: str # 专业 + class_id: str # 班级编号 + enrollment_date: date # 入学日期 + graduation_date: date # 毕业日期 + student_status: str # 学籍状态(在读/休学/退学/毕业) + education_level: str # 学历(大专/本科/硕士/博士) + +class ResponseModel(BaseModel): + """统一响应格式 — 所有接口都返回这个结构,方便前端统一处理""" + code: int # 状态码,200=成功,400=参数错误 + message: str = 'ok' # 提示信息 + total:int #总条数 + total_pages:int #总页数 + data: List[StudentInfo1] # 真正的数据,这里是学员列表 + +class StudentInfo2(BaseModel): + student_id: str + student_name: str + score:float + +class ResponseModel1(BaseModel): + """统一响应格式 — 所有接口都返回这个结构,方便前端统一处理""" + code: int # 状态码,200=成功,400=参数错误 + message: str = 'ok' # 提示信息 + total:int #总条数 + total_pages:int #总页数 + data: List[StudentInfo2] # 真正的数据,这里是学员列表 + +class StudentInfo3(BaseModel): + student_id: str + student_name: str + fail_count: int + +class ResponseModel2(BaseModel): + code: int + message: str = 'ok' + total: int + total_pages: int + data: List[StudentInfo3] + +class ClassExamAvg(BaseModel): + course_id: str + class_id: str + class_name: str + avg_score: float + +class ResponseModel3(BaseModel): + code: int + message: str = 'ok' + total: int + total_pages: int + data: List[ClassExamAvg] + +class SalaryTop(BaseModel): + student_name: str + class_name: str + company_name: str + part_time: date + salary: float + +class ResponseModel4(BaseModel): + code: int + message: str = 'ok' + data: List[SalaryTop] + +class TimeSize(BaseModel): + student_id: str + student_name: str + offer_date: date + resume_open_date: date + time_size: int # 就业时长(天数) + +class ResponseModel5(BaseModel): + code: int + message: str = 'ok' + total: int + total_pages: int + data: List[TimeSize] + +class ClassAvgTimeSize(BaseModel): + class_id: str + class_name: str + avg_time_size: float # 平均就业时长(天数) + +class ResponseModel6(BaseModel): + code: int + message: str = 'ok' + total: int + total_pages: int + data: List[ClassAvgTimeSize] \ No newline at end of file