diff --git a/api1/employ_api.py b/api1/employ_api.py new file mode 100644 index 0000000..009beed --- /dev/null +++ b/api1/employ_api.py @@ -0,0 +1,108 @@ +# api/employ_api.py +# 本文件定义 学生就业信息 的所有 API 路由(Controller 层) +from datetime import date +from typing import List + +from fastapi import APIRouter,HTTPException +from fastapi.params import Depends +from sqlalchemy.orm import Session + +from database import get_db +from scheme.employ_scheme import EmployStatusCreate as EModel, EmployStatusResponse, EmployStatusQuery as QModel, \ + EmployQueryResponse, EmployStatusDelete as DModel +from dao.employ_dao import StudentEmployManageDAO as Emp + +# 创建路由器,前缀将在 main.py 中统一添加 +router = APIRouter() + +# ---------- 登记学生就业信息 ---------- +@router.post('/add_emp_reco',response_model=EmployStatusResponse,status_code=201, + responses={404: {"description": "学生不存在"},405: {"description": "该学生就业记录已存在"}}) +async def add_emp_reco(emp_sta:EModel,db:Session = Depends(get_db)): + + """ + 登记学生就业信息 接口 + :param emp_sta: 请求体参数,参考scheme.employ_scheme模块中定义的请求体模型 EmployStatusCreate + :return:登记的学生就业信息 + """ + if Emp.is_in_stu(db, emp_sta): # 查询 请求体参数(模型) 的学生是否在学生信息表中存在,逻辑删除(不存在) + if Emp.is_exist_visible(db,emp_sta): # 存在并可见 + raise HTTPException(status_code=405, detail="该学生就业记录已存在") + if Emp.is_exist_invisible(db,emp_sta): # 存在 不可见 + Emp.recover_emp_info(db, emp_sta) # 恢复 ---> 可见 + re = Emp.set_emp_info(db, emp_sta) # 修改 学生就业信息 + if re: + Emp.set_stu_state(db, emp_sta) # 设置 学生就业状态 + return emp_sta + # 学生就就业记录 不存在 直接登记 + Emp.add_emp_reco(db, emp_sta) # 登记 学生就业信息 + Emp.set_stu_state(db, emp_sta) # 设置 学生就业状态 + return emp_sta + else: # 学生 在学生信息表中 不可查 + raise HTTPException(status_code = 404,detail = "学生不存在") + +# ---------- 修改学生就业信息 ---------- +@router.put('/set_emp_reco',response_model=EmployStatusResponse, + responses={404: {"description": "该学生就业记录不存在"}}) +async def set_emp_reco(emp_sta:EModel,db:Session = Depends(get_db)): + """ + 修改学生就业信息 接口 + """ + if Emp.is_exist_visible(db,emp_sta): # 存在并可见 + re = Emp.set_emp_info(db, emp_sta) # 修改 学生就业信息 + if re: + Emp.set_stu_state(db, emp_sta) # 设置 学生就业状态 + return emp_sta + raise HTTPException(status_code=404, detail="该学生就业记录不存在") + +# ---------- 逻辑删除学生就业信息 ---------- +@router.delete('/delete_emp_reco/{emp_sta}',status_code=204, # response_model=EmployStatusResponse, + responses={404: {"description": "该学生就业记录不存在"}}) +async def delete_emp_reco(emp_sta:str,db:Session = Depends(get_db)): + """ + 逻辑删除 学生就业信息 接口 + """ + if Emp.is_exist_visible(db, emp_sta): # 存在并可见 + re = Emp.del_emp_info(db, emp_sta) # 逻辑删除 成 不可见 + if re: + return None + raise HTTPException(status_code=404, detail="该学生就业记录不存在") + +# ---------- 查询学生就业信息(多条件查询) ---------- +@router.post('/query_emp_reco',response_model=EmployQueryResponse, + responses={404: {"description": "该学生就业记录不存在"}}) +async def query_emp_reco(emp_query:QModel,db:Session = Depends(get_db)): + """ + 查询 学生就业信息 接口 + """ + e_all,total = Emp.query_emp_info(db, emp_query) # 调用查询方法 + if e_all: + return {"employ_info":e_all,"total":total} # 查询结果e_all不为None,返回查询到的记录列表; total:总条数 + return {"employ_info":None,"total":total} + +# ---------- 批量删除学生就业信息 ---------- +@router.delete('/delete_emp_reco/batch/',status_code=204, # response_model=EmployStatusResponse, + responses={404: {"description": "该学生就业记录不存在"}}) +async def batch_delete_emp_reco(emp_sta:List[DModel],db:Session = Depends(get_db)): + if Emp.is_exist_visible(db, emp_sta): # 存在并可见 + re = Emp.del_emp_info(db, emp_sta) # 逻辑删除 成 不可见 + if re: + return None + raise HTTPException(status_code=404, detail="该学生就业记录不存在") + +# ---------- 批量修改学生就业信息 ---------- +@router.delete('/set_emp_reco/batch/',status_code=204, # response_model=EmployStatusResponse, + responses={404: {"description": "该学生就业记录不存在"}}) +async def batch_set_emp_reco(emp_sta:List[EModel],db:Session = Depends(get_db)): + """ + 批量修改学生就业信息 接口 + """ + for i in emp_sta: + if Emp.is_exist_visible(db, i): # 存在并可见 + Emp.set_emp_info(db, i) # 修改 学生就业信息 + Emp.set_stu_state(db, i) # 设置 学生就业状态 + else: + raise HTTPException(status_code=404, detail="该学生就业记录不存在") + else: + db.commit() + return emp_sta diff --git a/api1/statistics_api.py b/api1/statistics_api.py deleted file mode 100644 index 17573ab..0000000 --- a/api1/statistics_api.py +++ /dev/null @@ -1,18 +0,0 @@ -# api/stats -# 本文件定义统计分析相关的所有 API 路由(Controller 层) -from itertools import count - -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from typing import List - -from database import get_db -from dao.users_dao import UserDAO -from scheme.statistics_scheme import StatsResponse -from scheme.users import UserCreate, UserUpdate, UserResponse - -router = APIRouter() -# **多维度班级统计**:统计每个班级的总人数,以及按性别(男、女)细分的人数分布。 -@router.get("/class/stats/gender", response_model=List[StatsResponse]) -def get_class_stats(db: Session = Depends(get_db)): - sbg = db.query(StuInfo.cls_id, count(StuInfo.id)).group_by(StuInfo.cls_id, StuInfo.gender).all() \ No newline at end of file diff --git a/api1/stu_api.py b/api1/stu_api.py new file mode 100644 index 0000000..a1cfae0 --- /dev/null +++ b/api1/stu_api.py @@ -0,0 +1,149 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List + +from database import get_db +from dao.stu_dao import StudentDao +from scheme.stu_scheme import StudentCreate, StudentUpdate, StudentResponse + + +router = APIRouter() +# ============= 查询 ============= +#-----------分页查询所有学生-----------此处限制,dao层只是默认 +@router.get("/",response_model=List[StudentResponse],summary="查询所有学生") +async def get_students( + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(50, ge=1, le=80, description="每页数量最大80"), + db: Session = Depends(get_db) # 依赖注入获得数据库会话 +): + students = StudentDao.get_all(db, skip=skip, limit=limit) + return students + +#-----------根据id查询学生----------- +@router.get("/{stu_id}", response_model=StudentResponse,summary="根据ID查询学生") +async def get_student_by_id( + stu_id: str, + db: Session = Depends(get_db) +): + id_student = StudentDao.get_by_id(db,stu_id) + if not id_student: + raise HTTPException(status_code=404, detail="学生不存在") + return id_student + +#-----------根据name查询学生----------- +@router.get("/name/{stu_name}", response_model=List[StudentResponse],summary="根据姓名查询学生") +async def get_student_by_name( + stu_name: str, + db: Session = Depends(get_db) +): + name_student = StudentDao.get_by_name(db,stu_name) + if not name_student: + raise HTTPException(status_code=404, detail="学生不存在") + return name_student + +#-----------根据班级ID查询学生----------- +@router.get("/class/{cls_id}", response_model=List[StudentResponse],summary="班级ID查询学生") +async def get_student_by_class( + cls_id: str, + db: Session = Depends(get_db) +): + class_student = StudentDao.get_by_class(db,cls_id) + if not class_student: + raise HTTPException(status_code=404, detail="学生不存在") + return class_student + +#===========创建学生信息=========== +@router.post("/", response_model=StudentResponse, status_code=201,summary="新建学生") +async def create_student( + student_data: StudentCreate, + db: Session = Depends(get_db) +): + """ + 创建新学生: + 1. 校验班级是否存在 + 2. 生成学号(s + 班级ID + 两位序号) + 3. 组装数据,存入数据库 + """ + #----------- 校验班级是否存在 ----------- + cls_exists = StudentDao.get_class_by_id(db, student_data.cls_id) + if not cls_exists: + raise HTTPException(status_code=400,detail=f"班级 {student_data.cls_id} 不存在,请先创建班级") + #----------- 生成学号 ----------- + count = StudentDao.count_by_class(db, student_data.cls_id) + next_seq = count + 1 + student_id = f"s{student_data.cls_id}{next_seq:02d}" + + #----------- 组装字典(补上后端生成的字段) ----------- + student_dict = { + "id": student_id, # 后端生成 + "cls_id": student_data.cls_id, # 来自请求体 + "name": student_data.name, + "gender": student_data.gender, + "age": student_data.age, + "hometown": student_data.hometown, + "grad_school": student_data.grad_school, + "major": student_data.major, + "education": student_data.education, + "enr_date": student_data.enr_date, + "grad_date": student_data.grad_date, + "advisor_id": student_data.advisor_id, + "state": "在读", # 表模型默认值 + "is_deleted": 0} # 表模型默认值 + + #----------- 调用 DAO 创建 ----------- + try: + new_student = StudentDao.create(db, student_dict) + except Exception: + db.rollback() + raise HTTPException(status_code=400, detail="网络卡顿或有其他人同步上传,请重试") + return new_student +#============= 修改 ============ +@router.put("/{stu_id}", response_model=StudentResponse,summary="通过ID查找修改学生信息") +async def update_student( + stu_id: str, + stu_data: StudentUpdate, + db: Session = Depends(get_db)): + """ + 修改学生信息: + 1. 校验学生是否存在 + 2. 如果修改了班级,校验新班级是否存在 + 3. 调用 DAO 更新 + """ + #----------- 校验学生是否存在 ----------- + db_student = StudentDao.get_by_id(db, stu_id) + if not db_student: + raise HTTPException(status_code=404, detail="学生不存在") + #----------- 如果修改了班级,校验新班级是否存在 ----------- + if stu_data.cls_id and stu_data.cls_id != db_student.cls_id: + cls_exists = StudentDao.get_class_by_id(db, stu_data.cls_id) + if not cls_exists: + raise HTTPException( + status_code=400, + detail=f"班级 {stu_data.cls_id} 不存在") + # 请求体只校验了两个日期都传的情况,这里补上“只传一个”的情况 + enr = stu_data.enr_date or db_student.enr_date + grad = stu_data.grad_date or db_student.grad_date + if grad <= enr: + raise HTTPException( + status_code=400, + detail="毕业日期必须晚于入学日期") + #----------- 调用 DAO 更新 ----------- + updated_student = StudentDao.update(db, stu_id, stu_data) + return updated_student + +#=============删除学生(软删除)=============== +@router.delete("/{stu_id}", response_model=StudentResponse,summary="通过ID查找删除学生信息") +async def delete_student( + stu_id: str, + db: Session = Depends(get_db) +): + """ + 删除学生信息(逻辑删除): + 1. 校验学生是否存在 + 2. 把 is_deleted 标记为 1 + """ + # ------------- 调用 DAO 删除 ------------- + deleted_student = StudentDao.delete(db, stu_id) + if not deleted_student: + raise HTTPException(status_code=404, detail="学生不存在") + return deleted_student \ No newline at end of file diff --git a/api1/stu_score_api.py b/api1/stu_score_api.py new file mode 100644 index 0000000..a76465f --- /dev/null +++ b/api1/stu_score_api.py @@ -0,0 +1,112 @@ +# api/stu_score_api.py +# 本文件定义用户相关的所有 API 路由(Controller 层) + +from fastapi import APIRouter, Depends, HTTPException, Query,Path +from sqlalchemy.orm import Session +from typing import List + +from database import get_db +from dao.stu_score_dao import StuScoreDAO +from scheme.stu_score_scheme import StuScoreResponse,StuScoreCreateResponse + +router = APIRouter() + +# ---------- 添加成绩信息 ---------- +@router.post("/{stu_id}/{exam_attempt}", response_model=StuScoreCreateResponse, status_code=201) +async def create_score( + stu_id: str = Path(..., min_length=3, max_length=50,description="学生id"), + exam_attempt: int = Path(..., ge=1,description="考试轮次"), + exam_score:int = Query(..., ge=0, le=100,description="成绩"), + is_deleted: bool = False, + db: Session = Depends(get_db) +): + score_level = "优秀" if exam_score >= 90 else "普通" if exam_score >= 60 else "差劲" + warnings="该学生成绩被标记为差劲"if score_level=="差劲" else None + existing = StuScoreDAO.get_by_id_attempt_all(db, stu_id, exam_attempt) + if existing: + raise HTTPException(status_code=400, detail="本次考核成绩已存在,请重新确认") + + new_score = StuScoreDAO.add_score(db,stu_id,exam_attempt,exam_score,score_level,is_deleted) + return StuScoreCreateResponse( + stu_id=new_score.stu_id, + exam_attempt=new_score.exam_attempt, + exam_score=new_score.exam_score, + score_level=new_score.score_level, + warnings=warnings, + ) +# ---------- 修改成绩信息 ---------- +@router.put("/{stu_id}/{exam_attempt}/{exam_score}", response_model=StuScoreResponse) +async def update_user( + stu_id: str = Path(..., min_length=3, max_length=50, description="学生id"), + exam_attempt: int = Path(..., ge=1, description="考试轮次"), + exam_score: int = Path(..., ge=0, le=100, description="成绩"), + db: Session = Depends(get_db) +): + """ + 更新成绩信息 + """ + score_level = "优秀" if exam_score > 90 else "普通" if exam_score > 60 else "差劲" + existing = StuScoreDAO.get_by_id_attempt_all(db, stu_id, exam_attempt) + if not existing: + raise HTTPException(status_code=404, detail="没有找到对应考核成绩信息,请重新确认") + new_score_info = StuScoreDAO.update_score(db,stu_id,exam_attempt,exam_score,score_level) + return new_score_info +# ---------- 查询成绩信息 ---------- +@router.get("/", response_model=List[StuScoreResponse]) +async def get_score( + stu_id: str | None = Query(None, min_length=3, max_length=50, description="学生id"), + exam_attempt: int | None = Query(None, ge=1, le=100, description="考试轮次"), + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(10, ge=1, le=100, description="返回的最大记录数"), + db: Session = Depends(get_db) # 依赖注入获得数据库会话 +): + """ + 查询学生成绩 + - 传 stu_id和exam_attempt:精准查找该学生指定轮次成绩 + - 传 stu_id:精准查找该学生所有成绩记录 + - 传 exam_attempt:查找该轮次所有成绩记录 + - 不传 stu_id和exam_attempt:查询所有记录 + """ + if stu_id is not None and exam_attempt is not None: + score = StuScoreDAO.get_by_id_attempt(db, stu_id, exam_attempt) + if not score: + raise HTTPException(status_code=404, detail="没有找到对应信息,请重新确认") + return [score] + elif stu_id is None and exam_attempt is not None: + scores = StuScoreDAO.get_by_exam_attempt(db, exam_attempt) + if not scores: + raise HTTPException(status_code=404, detail="未找到该考试轮次成绩,请重新确认") + return scores + elif stu_id is not None and exam_attempt is None: + scores = StuScoreDAO.get_by_id(db, stu_id) + if not scores: + raise HTTPException(status_code=404, detail="未找到该学生成绩,请重新确认") + return scores + else: + scores = StuScoreDAO.get_all_score(db, skip, limit) + return scores # FastAPI 自动根据 response_model 转换为 JSON + + # ---------- 删除成绩信息 ---------- +@router.delete("/{stu_id}", status_code=204) +async def delete_score( + stu_id: str = Path(..., min_length=3, max_length=50, description="学生id"), + exam_attempt: int | None = Query(None, ge=1, le=100, description="考试轮次"), + db: Session = Depends(get_db) +): + """ + 删除学生成绩 + - 传 exam_attempt:删除该学生指定轮次成绩 + - 不传 exam_attempt:删除该学生全部成绩 + 成功返回204 No Content + """ + score = StuScoreDAO.get_by_id_attempt(db, stu_id, exam_attempt) + if not score: + raise HTTPException(status_code=404, detail="没有找到对应信息,请重新确认") + # 返回 None 表示 204 状态码(无内容) + if exam_attempt is not None: + success = StuScoreDAO.delete_score(db, stu_id,exam_attempt) + return success + else: + success = StuScoreDAO.delete_all_score(db, stu_id) + return success + diff --git a/api1/teacher_api.py b/api1/teacher_api.py new file mode 100644 index 0000000..b34dc85 --- /dev/null +++ b/api1/teacher_api.py @@ -0,0 +1,82 @@ + +from fastapi import APIRouter, Depends, HTTPException, Query, Path +from sqlalchemy.orm import Session +from typing import List + +from database import get_db +from dao.teacher_dao import TeacherDao +from scheme.teacher_scheme import TeacherAdd,TeacherUpdate, TeacherResponse + +router = APIRouter() + +# 查询所有教师(支持分页) +@router.get("/get_all_teachers", response_model=List[TeacherResponse]) +async def get_all_teachers( + skip: int = Query(0, ge=0, description="跳过的记录数"), + limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"), + db: Session = Depends(get_db) # 依赖注入获得数据库会话 +): + + teachers = TeacherDao.get_all(db, skip=skip, limit=limit) + return teachers # FastAPI 自动根据 response_model 转换为 JSON + +# 根据 ID 和 name 查询单个教师 +@router.get("/get_teacher/{teacher_id_name}", response_model=TeacherResponse) +async def get_teacher( + teacher_id_name: str = Path(..., description="要查询教师的id或姓名"), + db: Session = Depends(get_db) +): + teacher = TeacherDao.get_by_id(db, teacher_id_name) or TeacherDao.get_by_name(db, teacher_id_name) + if not teacher: + raise HTTPException(status_code=404, detail="用户不存在") + return teacher + +# 根据 ID 查询教师带教的班级的信息 +@router.get("/get_class_info/{teacher_id}") +async def get_class_info( + teacher_id: str = Path(..., description="要查询教师的id"), + db: Session = Depends(get_db) +): + teacher = TeacherDao.get_clss_info(db, teacher_id) + if not teacher: + raise HTTPException(status_code=404, detail="用户不存在") + return teacher + +# 添加教师信息 +@router.post("/add_teacher", response_model=TeacherResponse) +async def add_teacher( + teacher_data: TeacherAdd, + db: Session = Depends(get_db) +): + # 调用 TeacherDao 创建教师信息 + new_teacher = TeacherDao.add(db, teacher_data) + return new_teacher + +# 根据教师id更新教师信息 +@router.put("/update_teacher/{teacher_id}", response_model=TeacherResponse) +async def update_teacher( + teacher_id: str, + teacher_data: TeacherUpdate, + db: Session = Depends(get_db) +): + # 检查传入的id教师是否存在 + existing = TeacherDao.get_by_id(db, teacher_id) + if not existing: + raise HTTPException(status_code=404, detail="用户不存在") + + # 执行更新 + updated = TeacherDao.update(db, teacher_id, teacher_data) + return updated + +# 删除教师 +@router.delete("/delete_teacher/{teacher_id}") +async def delete_teacher( + teacher_id: str, + db: Session = Depends(get_db) +): + + success = TeacherDao.delete(db, teacher_id) + if not success: + raise HTTPException(status_code=404, detail="用户不存在") + + return "该教师已删除" \ No newline at end of file diff --git a/api1/users.py b/api1/users.py deleted file mode 100644 index afdcf75..0000000 --- a/api1/users.py +++ /dev/null @@ -1,98 +0,0 @@ -# api/users.py -# 本文件定义用户相关的所有 API 路由(Controller 层) - -from fastapi import APIRouter, Depends, HTTPException, Query -from sqlalchemy.orm import Session -from typing import List - -from database import get_db -from dao.users_dao import UserDAO -from scheme.users import UserCreate, UserUpdate, UserResponse - -# 创建路由器,前缀将在 main0814.py 中统一添加 -router = APIRouter() - -# ---------- 查询所有用户(分页) ---------- -@router.get("/", response_model=List[UserResponse]) -async def get_users( - skip: int = Query(0, ge=0, description="跳过的记录数"), - limit: int = Query(100, ge=1, le=200, description="返回的最大记录数"), - db: Session = Depends(get_db) # 依赖注入获得数据库会话 -): - """ - 获取用户列表,支持分页 - """ - users = UserDAO.get_all(db, skip=skip, limit=limit) - return users # FastAPI 自动根据 response_model 转换为 JSON - -# ---------- 根据 ID 查询单个用户 ---------- -@router.get("/{user_id}", response_model=UserResponse) -async def get_user( - user_id: int, - db: Session = Depends(get_db) -): - """ - 根据用户 ID 获取详细信息 - """ - user = UserDAO.get_by_id(db, user_id) - if not user: - raise HTTPException(status_code=404, detail="用户不存在") - return user - -# ---------- 创建新用户 ---------- -@router.post("/", response_model=UserResponse, status_code=201) -async def create_user( - user_data: UserCreate, - db: Session = Depends(get_db) -): - """ - 创建新用户,需要提供用户名、邮箱和全名(全名可选) - 注意:用户名必须唯一 - """ - # 检查用户名是否已被占用 - existing = UserDAO.get_by_username(db, user_data.username) - if existing: - raise HTTPException(status_code=400, detail="用户名已被占用") - # 调用 DAO 创建用户 - new_user = UserDAO.create(db, user_data) - return new_user - -# ---------- 更新用户信息 ---------- -@router.put("/{user_id}", response_model=UserResponse) -async def update_user( - user_id: int, - user_data: UserUpdate, - db: Session = Depends(get_db) -): - """ - 更新用户信息(只更新传入的字段) - """ - # 检查用户是否存在 - existing = UserDAO.get_by_id(db, user_id) - if not existing: - raise HTTPException(status_code=404, detail="用户不存在") - - # 如果更新用户名,需要检查新用户名是否与其他用户冲突(排除自身) - if user_data.username is not None: - conflict = UserDAO.get_by_username(db, user_data.username) - if conflict and conflict.id != user_id: - raise HTTPException(status_code=400, detail="用户名已被其他用户占用") - - # 执行更新 - updated = UserDAO.update(db, user_id, user_data) - return updated - -# ---------- 删除用户 ---------- -@router.delete("/{user_id}", status_code=204) -async def delete_user( - user_id: int, - db: Session = Depends(get_db) -): - """ - 删除用户,成功返回 204 No Content - """ - success = UserDAO.delete(db, user_id) - if not success: - raise HTTPException(status_code=404, detail="用户不存在") - # 返回 None 表示 204 状态码(无内容) - return None \ No newline at end of file diff --git a/dao/employ_dao.py b/dao/employ_dao.py new file mode 100644 index 0000000..8f4e043 --- /dev/null +++ b/dao/employ_dao.py @@ -0,0 +1,218 @@ +# dao/employ_dao.py +# 本文件封装 stu_emp_mgmt 表的所有数据库操作(增、删、改、查) +# author:王博 +from datetime import date +from sqlalchemy import and_ +from sqlalchemy.orm import Session +from model.employ_model import StudentEmployManage as ETable +from scheme.employ_scheme import EmployStatusCreate as EModel, EmployStatusQuery as QModel,EmployStatusDelete as DModel +from model.stu_model import StuInfo + +from typing import Optional, List +class StudentEmployManageDAO: + """ + 封装stu_emp_mgmt 表的所有数据库操作(增、删、改、查) + """ + @staticmethod + def add_emp_reco(db:Session,emp_sta_add:EModel): + """ + 登记学生就业信息 + :param db: 数据库会话 + :param emp_sta_add: 参数,对象,类型属于 学生就业管理表 模型类 + :return: 添加的stu_emp_info对象 + """ + d_stu = emp_sta_add.model_dump() # 将请求体参数对象 ,转为字典 + if d_stu['emp_open_time']: + d_stu['emp_open_time'] = date(**d_stu['emp_open_time']) # 就业开放时间 + if d_stu['send_offer_time']: + d_stu['send_offer_time'] = date(**d_stu['send_offer_time']) # offer下发时间 + db_emp = ETable(**d_stu) + db.add(db_emp) + db.commit() + db.refresh(db_emp) + return db_emp + + + @staticmethod + def is_in_stu(db: Session, emp_sta_in: EModel): + """ + 判断 参数中 学生id 是否在 学生表StuInfo 中存在 + 根据 学生id、is_deleted 过滤 + :return: 存在 返回True;不存在返回False + """ + stu = db.query(StuInfo).filter(and_(StuInfo.id==emp_sta_in.stu_id, + StuInfo.is_deleted==0)).first() + if stu: # 能找到学号,并且 是否 删除状态为 0 + return True + return False + + @staticmethod + def is_exist_visible(db: Session, emp_sta_exist:List[DModel] |EModel | QModel | str): + """ + 判断学生就业信息是否在 学生就业信息表中 存在 + is_deleted 0,用来判断可见、可查询 + :return: 存在、可见 返回True;否则 返回False + """ + emp = None + emp_info = db.query(ETable) + if isinstance(emp_sta_exist,str): + emp = emp_info.filter(and_(ETable.stu_id == emp_sta_exist, + ETable.is_deleted == 0)).first() + + elif isinstance(emp_sta_exist, list): + for i in emp_sta_exist: + emp = emp_info.filter(and_(ETable.stu_id == i.stu_id, + ETable.is_deleted == 0)).first() + if not emp: + emp = False + break + + else: + emp = emp_info.filter(and_(ETable.stu_id == emp_sta_exist.stu_id, + ETable.is_deleted == 0)).first() + if emp: + return True + return False + + @staticmethod + def is_exist_invisible(db: Session, emp_sta_exist: EModel): + """ + 判断学生就业信息是否在 学生就业信息表中 存在 + is_deleted 1,用来判断不可见、不可查询 + :return: 存在、不可见 返回True;否则 返回False + """ + emp_info = db.query(ETable).filter(and_(ETable.stu_id == emp_sta_exist.stu_id, + ETable.is_deleted == 1)).first() + if emp_info: + return True + return False + + @staticmethod + def set_stu_state(db: Session, emp_sta_set: EModel): + """ + 根据就业开放时间、offer下发时间 设置 学生就业状态 + 就业未开放 ---》在读 + 就业开放&offer未下发 ---》进入就业 + 就业开放&offer下发 ---》已就业 + :return: 在读、进入就业、已就业 + """ + stu = db.query(StuInfo).filter(StuInfo.id == emp_sta_set.stu_id).first() + if emp_sta_set.emp_open_time is None: # 就业开放时间为 None + stu.state = '在读' + db.commit() + db.refresh(stu) + return stu.state + + elif emp_sta_set.send_offer_time is None:# 有就业开放时间,offer下发时间为 None + stu.state = '进入就业' + db.commit() + db.refresh(stu) + return stu.state + + else: # 有offer下发时间 + stu.state = '已就业' + db.commit() + db.refresh(stu) + return stu.state + + @staticmethod + def set_emp_info(db: Session, emp_sta_update: EModel): + """ + 根据请求体参数 修改 学生 就业信息 + :return: 修改条数0或1;0代表 修改失败 ;1代表 修改成功 + """ + d_stu = emp_sta_update.model_dump() # 将请求体参数对象 ,转为字典 + if d_stu['emp_open_time']: + d_stu['emp_open_time'] = date(**d_stu['emp_open_time']) # 转化成 就业开放时间 + if d_stu['send_offer_time']: + d_stu['send_offer_time'] = date(**d_stu['send_offer_time']) # 转化成 offer下发时间 + row_count = db.query(ETable).filter(ETable.stu_id == emp_sta_update.stu_id).update(d_stu) #修改条数0或1 + db.commit() + return row_count + + @staticmethod + def batch_set_emp_info(db: Session, emp_sta_update:EModel): + """ + 根据请求体参数列表 批量修改 学生 就业信息 + :return: 修改条数0或1;0代表 修改失败 ;1代表 修改成功 + """ + d_stu = emp_sta_update.model_dump() # 将请求体参数对象 ,转为字典 + if d_stu['emp_open_time']: + d_stu['emp_open_time'] = date(**d_stu['emp_open_time']) # 转化成 就业开放时间 + if d_stu['send_offer_time']: + d_stu['send_offer_time'] = date(**d_stu['send_offer_time']) # 转化成 offer下发时间 + row_count = db.query(ETable).filter(ETable.stu_id == emp_sta_update.stu_id).update(d_stu) # 修改条数0或1 + return row_count + + + @staticmethod + def is_deleted(db: Session, emp_info): + if emp_info: + emp_info.is_deleted = 1 + db.commit() + db.refresh(emp_info) + + @staticmethod + def del_emp_info(db: Session, emp_sta_del: List[DModel] | str): + """ + 根据请求体参数 逻辑删除 学生就业信息 + :return: 返回逻辑删除的 学生就业信息 + """ + emp_info = db.query(ETable) + + if isinstance(emp_sta_del, str): + emp = emp_info.filter(and_(ETable.stu_id == emp_sta_del, + ETable.is_deleted == 0)).first() + StudentEmployManageDAO.is_deleted(db,emp) + db.commit() + return True + if isinstance(emp_sta_del, list): + emp_copy = emp_sta_del.copy() + for i in emp_copy: + emp = emp_info.filter(and_(ETable.stu_id == i.stu_id, + ETable.is_deleted ==0)).first() + StudentEmployManageDAO.is_deleted(db, emp) + db.commit() + return True + return False + + @staticmethod + def recover_emp_info(db: Session, emp_sta_recover: EModel): + """ + 根据请求体参数 逻辑删除 学生就业信息 + :return: 返回逻辑删除的 学生就业信息 + """ + emp_info = db.query(ETable).filter(and_(ETable.stu_id == emp_sta_recover.stu_id, + ETable.is_deleted ==0)).first() + if emp_info: + emp_info.is_deleted = 0 + db.commit() + db.refresh(emp_info) + return emp_info + + @staticmethod + def query_emp_info(db: Session, emp_sta_query: QModel): + """ + 请求体传入学生编号(可选)、就业公司(可选)、薪资范围(可选) 可多条件查询 学生就业信息 + :return: 返回查询到的 所有 学生就业信息 + """ + e_all = db.query(ETable) + if emp_sta_query.stu_id: # 根据 学生编号 筛选出 学生就业信息 + e_all = e_all.filter(ETable.stu_id == emp_sta_query.stu_id) + if emp_sta_query.emp_company: # 根据 就业公司名称 模糊筛选出 学生就业信息 + e_all = e_all.filter(ETable.emp_company.like(f"%{emp_sta_query.emp_company}%")) + if emp_sta_query.min_salary: # 根据 最小薪资范围 筛选出 大于 最小薪资范围的 学生就业信息 + e_all = e_all.filter(ETable.salary >= emp_sta_query.min_salary) + if emp_sta_query.max_salary: # 根据 最大薪资范围 筛选出 小于 最大薪资范围的 学生就业信息 + e_all = e_all.filter(ETable.salary <= emp_sta_query.max_salary) + + total = e_all.count() + e_all = e_all.offset((emp_sta_query.skip-1)*emp_sta_query.limit).limit(emp_sta_query.limit).all() + + return e_all,total + + + + + + diff --git a/dao/stu_dao.py b/dao/stu_dao.py new file mode 100644 index 0000000..298d633 --- /dev/null +++ b/dao/stu_dao.py @@ -0,0 +1,83 @@ +from sqlalchemy.orm import Session +from model.stu_model import StuInfo +from model.cls_mgmt_model import ClsMgmt +from scheme.stu_scheme import StudentUpdate + + +class StudentDao: + """查学生""" +#---------分页查询所有学生信息---------- + @staticmethod + def get_all(db: Session, skip: int = 0, limit: int = 20): + return (db.query(StuInfo) + .filter(StuInfo.is_deleted == 0) + .offset(skip).limit(limit).all()) +#---------根据学生id查询学生信息---------- + @staticmethod + def get_by_id(db: Session, stu_id: str) : + return (db.query(StuInfo). + filter(StuInfo.id == stu_id) + .filter(StuInfo.is_deleted == 0) + .first()) +#---------根据学生姓名查询学生信息---------- + @staticmethod + def get_by_name(db: Session, stu_name: str) : + return (db.query(StuInfo) + .filter(StuInfo.name == stu_name) + .filter(StuInfo.is_deleted == 0) + .all()) +#---------根据班级id查询班级学生信息---------- + @staticmethod + def get_by_class(db: Session, cls_id: str) : + return (db.query(StuInfo) + .filter(StuInfo.cls_id == cls_id) + .filter(StuInfo.is_deleted == 0) + .all()) +# """增加学生""" +# 1 ---------添加学生信息---------- + @staticmethod + def create(db: Session, student_data: dict): + db_student=StuInfo(**student_data) + db.add(db_student) + db.commit() + db.refresh(db_student) # 刷新对象,获取数据库生成的默认值(如 created_at) + return db_student +# 2 ---------查询班级是否存在(可能班级创建了但还没人)---------- + @staticmethod + def get_class_by_id(db: Session,cls_id:str): + return (db.query(ClsMgmt) + .filter(ClsMgmt.id==cls_id) + .first()) #不存在自动返回None +# 3 ---------查询对应的班级人数用来生成学号---------- + @staticmethod + def count_by_class(db: Session, cls_id: str) -> int: + return (db.query(StuInfo) + .filter(StuInfo.cls_id == cls_id) + .filter(StuInfo.is_deleted == 0) + .count()) +# """修改学生信息""" +#---------根据学生id查找并修改学生信息---------- + @staticmethod + def update(db:Session,stu_id:str,stu_data:StudentUpdate): + db_student = StudentDao.get_by_id(db,stu_id) + if not db_student: + return None +# model_dump把用户传入修改请求体的信息转成字典,exclude_unset=True把没传入的字段排除掉,否则会None来修改其他字段 + update_data = stu_data.model_dump(exclude_unset=True) + for key, value in update_data.items(): # 动态设置属性 + setattr(db_student, key, value) + + db.commit() # 提交事务 + db.refresh(db_student) # 刷新对象,获取 onupdate 时间等 + return db_student +# """删除学生信息""" 逻辑删除 +#---------根据学生id查找并删除学生信息---------- + @staticmethod + def delete(db:Session,stu_id:str): + db_student=StudentDao.get_by_id(db,stu_id) + if not db_student: + return False + db_student.is_deleted=1 + db.commit() + db.refresh(db_student) + return db_student diff --git a/dao/stu_score_dao.py b/dao/stu_score_dao.py new file mode 100644 index 0000000..d5da1fe --- /dev/null +++ b/dao/stu_score_dao.py @@ -0,0 +1,109 @@ +# dao/stu_score_dao.py +# 本文件封装对 User 表的所有数据库操作(增、删、改、查) +from sqlalchemy import and_ +from sqlalchemy.orm import Session +from model.stu_score_model import StuScore +# from scheme.stu_score_scheme import +from typing import Optional, List + +class StuScoreDAO: + """用户数据访问对象,所有方法均为静态方法,方便调用""" + +# # ------------------增添手段---------------------- + @staticmethod + def add_score(db: Session,stu_id: str,exam_attempt:int,exam_score:int,score_level:str,is_deleted: bool + ) -> StuScore: + new_score = StuScore(stu_id=stu_id, + exam_attempt=exam_attempt, + exam_score=exam_score, + score_level=score_level, + is_deleted=is_deleted) + db.add(new_score) + db.commit() + return new_score + +# ------------------查询手段---------------------- + @staticmethod + def get_all_score(db: Session, skip: int = 0, limit: int = 10) -> List[StuScore]: + """ + 获取所有用户(支持分页) + :param db: 数据库会话 + :param skip: 偏移量(跳过前 skip 条) + :param limit: 最大返回条数 + :return: 用户对象列表 + """ + return db.query(StuScore).filter(StuScore.is_deleted == False).offset(skip).limit(limit).all() + + + @staticmethod + def get_by_id(db: Session, stu_id: str) -> List[StuScore]: + """ + 根据学生ID 获取成绩 + :return: 用户对象或 None + """ + return db.query(*StuScoreDAO.get_field()).filter(and_(StuScore.stu_id == stu_id,StuScore.is_deleted == False)).all() + + @staticmethod + def get_by_exam_attempt(db: Session, exam_attempt: int) -> List[StuScore]: + """ + 根据考试轮次 获取成绩 + :return: 用户对象或 None + """ + return db.query(*StuScoreDAO.get_field()).filter(and_(StuScore.exam_attempt == exam_attempt,StuScore.is_deleted == False)).all() + + @staticmethod + def get_field(): + get_field = StuScore.exam_score,StuScore.score_level,StuScore.stu_id,StuScore.exam_attempt + return get_field + + @staticmethod + def get_by_id_attempt(db: Session, stu_id: str,exam_attempt: int) -> Optional[StuScore]: + """ + 根据学生ID 和考试轮次精确定位成绩 + :return: 用户对象或 None + """ + return db.query(*StuScoreDAO.get_field()).filter(and_(StuScore.stu_id == stu_id , StuScore.exam_attempt == exam_attempt,StuScore.is_deleted == False)).first() + + @staticmethod + def get_by_id_attempt_all(db: Session, stu_id: str, exam_attempt: int) -> Optional[StuScore]: + """ + 根据学生ID 和考试轮次精确定位成绩 + :return: 用户对象或 None + """ + return db.query(StuScore).filter( + and_(StuScore.stu_id == stu_id, StuScore.exam_attempt == exam_attempt, + StuScore.is_deleted == False)).first() + + # ------------------更新手段---------------------- + @staticmethod + def update_score(db: Session, stu_id: str,exam_attempt:int,exam_score:int,score_leval) -> Optional[StuScore]: + + + db_score = StuScoreDAO.get_by_id_attempt_all(db,stu_id, exam_attempt) + if not db_score: + return None + db_score.exam_score = exam_score + db_score.score_level = score_leval + + db.commit() # 提交事务 + db.refresh(db_score) # 刷新对象,获取 onupdate 时间等 + return db_score + + # ------------------删除手段---------------------- + @staticmethod + def delete_score(db: Session, stu_id: str,exam_attempt:int) -> bool: + db_score = StuScoreDAO.get_by_id_attempt_all(db, stu_id, exam_attempt) + if not db_score: + return False + db_score.is_deleted=True # 标记删除 + db.commit() # 提交事务 + return True + @staticmethod + def delete_all_score(db: Session, stu_id: str) -> bool: + db_score = StuScoreDAO.get_by_id(db, stu_id) + if not db_score: + return False + for score in db_score: + score.is_delete=True # 标记删除 + db.commit() # 提交事务 + return True \ No newline at end of file diff --git a/dao/teacher_dao.py b/dao/teacher_dao.py new file mode 100644 index 0000000..87f4e46 --- /dev/null +++ b/dao/teacher_dao.py @@ -0,0 +1,146 @@ +# dao/teacher_dao.py +# 本文件封装对 tes_info 表的所有数据库操作(增、删、改、查) +from http.client import HTTPException +from typing import List, Optional + +from sqlalchemy import or_ +from sqlalchemy.orm import Session + +from model.cls_mgmt_model import ClsMgmt +from model.teacher_model import Teacher +from scheme.teacher_scheme import TeacherUpdate, TeacherAdd +import random + + +class TeacherDao: + """教师数据访问对象,所有方法均为静态方法,方便调用""" + +#获取所有教师基本信息 + @staticmethod + def get_all(db: Session, skip: int = 0, limit: int = 100) : + """ + 获取所有用户(支持分页) + :param db: 数据库会话 + :param skip: 偏移量(跳过前 skip 条) + :param limit: 最大返回条数 + :return: 教师对象列表 + """ + return db.query(Teacher).filter(Teacher.is_deleted == 0).offset(skip).limit(limit).all() + + +#获取单个教师信息 + @staticmethod + def get_by_id(db: Session, teacher_id: str) : + """ + 根据主键 ID 获取单个教师信息 + :return:教师对象或 None + """ + return db.query(Teacher).filter(Teacher.id == teacher_id,Teacher.is_deleted == 0).first() + + @staticmethod + def get_by_name(db: Session, teacher_name: str) : + """ + 根据教师姓名获取信息(用于唯一性检查) + """ + return db.query(Teacher).filter(Teacher.name == teacher_name,Teacher.is_deleted == 0).first() + +# 根据教师id获取教师带教班级信息 + @staticmethod + def get_clss_info(db: Session, teacher_id: str): + """ + 根据主键 ID 获取单个教师信息 + :return: 带教信息或 None + """ + return db.query(ClsMgmt).filter(or_(ClsMgmt.head_tea_id == teacher_id,ClsMgmt.lecturer_id == teacher_id), ClsMgmt.is_deleted == 0).all() + + +#添加教师 + @staticmethod + def add(db: Session, teacher_data: TeacherAdd) : + """ + 添加新教师 + :param db: 数据库会话 + :param teacher_data: 符合 TeacherAddUpdate 请求体模型的数据 + :return: 创建后的 teacher 对象(含id ) + """ + + # 教师id自动生成 格式:T+随机四位数 并进行重复验证 + while True: + new_id = f"T{random.randint(100, 999)}" # 确保是4位数,不会出现 0012 这种情况 + + # 查询数据库是否已存在这个 id + exists = db.query(Teacher).filter(Teacher.id == new_id).first() + if not exists: + break # 不存在,跳出循环,使用这个 id + + # 把传入的参数变成字典 + data_dict = teacher_data.model_dump() + + # 把生成id加到字典里 + data_dict["id"] = new_id + + #软删除逻辑is_delete默认为0 + data_dict["is_deleted"] = 0 + + # 将 Pydantic 模型转为字典,并解包构建 SQLAlchemy 模型实例 + # 对字典进行拆包变成Teacher(id='',name='', phone='', type='',is_delete='0') + db_teacher = Teacher(**data_dict) + db.add(db_teacher) # 添加到会话 + db.commit() # 提交事务,此时会执行 INSERT,并自动填充自增字段 + db.refresh(db_teacher) # 刷新对象,获取数据库生成的默认值(如 created_at) + return db_teacher + +#更新教师信息 + @staticmethod + def update(db: Session, teacher_id: str, teacher_data: TeacherUpdate) : + """ + 更新教师信息(只更新传入的非空字段) + :param db: 数据库会话 + :param teacher_id: 要更新的教师 ID + :param teacher_data: 包含要更新字段的 Pydantic 模型 + :return: 更新后的 Teacher 对象,如果没有此教师则返回None + """ + db_teacher = TeacherDao.get_by_id(db, teacher_id) + if not db_teacher: + return None + + # 只更新客户端显式传入的字段(exclude_unset=True 排除未设置的字段) + # 不加 exclude_unset=True;teacher_data.model_dump() 会输出所有字段,没传的字段值是 None: + # 将传入参数转变成字典 + update_data = teacher_data.model_dump(exclude_unset=True) + #如果不传数据,直接返回本id教师 + if not update_data: + return db_teacher + + #拆分遍历传入的数据 + for key, value in update_data.items(): + #滚动插入数据 动态赋值 + setattr(db_teacher, key, value) + + """" + 不使用setattr的写法 + if 'name' in update_data: + db_teacher.name = update_data['name'] + if 'phone' in update_data: + db_teacher.phone = update_data['phone'] + if 'type' in update_data: + db_teacher.type = update_data['type'] + """ + + db.commit() # 提交事务 + db.refresh(db_teacher) # 刷新对象,获取 onupdate 时间等 + return db_teacher + +# 通过id删除教师 + @staticmethod + def delete(db: Session, teacher_id: str): + db_teacher = TeacherDao.get_by_id(db, teacher_id) + #如果不存在返回None,供接口判断 + if not db_teacher: + return None + #存在的情况下,只需把is_delete改成1 + db_teacher.is_deleted = 1 #软删除,1代表已删除 + # db.delete(db_teacher) # 正常删除 + db.commit() # 提交事务 + db.refresh(db_teacher) + return True \ No newline at end of file diff --git a/dao/users_dao.py b/dao/users_dao.py deleted file mode 100644 index 34cdc09..0000000 --- a/dao/users_dao.py +++ /dev/null @@ -1,86 +0,0 @@ -# dao/users_dao.py -# 本文件封装对 User 表的所有数据库操作(增、删、改、查) - -from sqlalchemy.orm import Session -from model.users import User -from scheme.users import UserCreate, UserUpdate -from typing import Optional, List - -class UserDAO: - """用户数据访问对象,所有方法均为静态方法,方便调用""" - - @staticmethod - def get_all(db: Session, skip: int = 0, limit: int = 100) -> List[User]: - """ - 获取所有用户(支持分页) - :param db: 数据库会话 - :param skip: 偏移量(跳过前 skip 条) - :param limit: 最大返回条数 - :return: 用户对象列表 - """ - return db.query(User).offset(skip).limit(limit).all() - - @staticmethod - def get_by_id(db: Session, user_id: int) -> Optional[User]: - """ - 根据主键 ID 获取单个用户 - :return: 用户对象或 None - """ - return db.query(User).filter(User.id == user_id).first() - - @staticmethod - def get_by_username(db: Session, username: str) -> Optional[User]: - """ - 根据用户名获取用户(用于唯一性检查) - """ - return db.query(User).filter(User.username == username).first() - - @staticmethod - def create(db: Session, user_data: UserCreate) -> User: - """ - 创建新用户 - :param db: 数据库会话 - :param user_data: 符合 UserCreate 模型的数据 - :return: 创建后的 User 对象(含自增 id 和默认时间) - """ - # 将 Pydantic 模型转为字典,并解包构建 SQLAlchemy 模型实例 - db_user = User(**user_data.model_dump()) - db.add(db_user) # 添加到会话 - db.commit() # 提交事务,此时会执行 INSERT,并自动填充自增字段 - db.refresh(db_user) # 刷新对象,获取数据库生成的默认值(如 created_at) - return db_user - - @staticmethod - def update(db: Session, user_id: int, user_data: UserUpdate) -> Optional[User]: - """ - 更新用户信息(只更新传入的非空字段) - :param db: 数据库会话 - :param user_id: 要更新的用户 ID - :param user_data: 包含要更新字段的 Pydantic 模型 - :return: 更新后的 User 对象,如果用户不存在则返回 None - """ - db_user = UserDAO.get_by_id(db, user_id) - if not db_user: - return None - - # 只更新客户端显式传入的字段(exclude_unset=True 排除未设置的字段) - update_data = user_data.model_dump(exclude_unset=True) - for key, value in update_data.items(): - setattr(db_user, key, value) # 动态设置属性 - - db.commit() # 提交事务 - db.refresh(db_user) # 刷新对象,获取 onupdate 时间等 - return db_user - - @staticmethod - def delete(db: Session, user_id: int) -> bool: - """ - 删除用户 - :return: True 表示删除成功,False 表示用户不存在 - """ - db_user = UserDAO.get_by_id(db, user_id) - if not db_user: - return False - db.delete(db_user) # 标记删除 - db.commit() # 提交事务 - return True \ No newline at end of file diff --git a/main.py b/main.py index 25daeed..934830a 100644 --- a/main.py +++ b/main.py @@ -5,7 +5,7 @@ from fastapi import FastAPI from fastapi.middleware.cors import CORSMiddleware from database import engine, Base -from api1 import users, statistics_api # 导入 users 子路由 +from api1 import users, statistics_api, stu_score_api, cls_mgmt_api, employ_api,stu_api, teacher_api # 导入 users 子路由 # 1. 创建数据库表(如果表不存在) # Base.metadata.create_all 会扫描所有继承 Base 的模型,生成对应的 CREATE TABLE 语句 @@ -34,6 +34,15 @@ app.include_router(users.router, prefix="/api/users", tags=["用户管理"]) app.include_router(statistics_api.router, prefix="/api/stats", tags=["统计分析"]) +app.include_router(stu_score_api.router, prefix="/api/score", tags=["学生成绩管理"]) + +app.include_router(cls_mgmt_api.router, prefix="/api/classes", tags=["班级管理"]) + +app.include_router(employ_api.router,prefix="/api/employ",tags=["学生就业管理模块"]) + +app.include_router(teacher_api.router, prefix="/api/teacher", tags=["教师信息管理模块"]) + +app.include_router(stu_api.router, prefix="/api/student", tags=["学生信息管理模块"]) # 5. 根路径 @app.get("/") async def root(): diff --git a/model/__init__.py b/model/__init__.py index e69de29..8e46cdc 100644 --- a/model/__init__.py +++ b/model/__init__.py @@ -0,0 +1,23 @@ +# model/__init__.py +# 这个文件不是「模型之间互相导入」,而是「聚合入口」,单向扇出,不成环 + +from database import Base + +from model.advisor_model import AdvisorInfo +from model.cls_mgmt_model import ClsMgmt +from model.employ_model import StudentEmployManage +from model.stu_model import StuInfo +from model.stu_score_model import StuScore +from model.teacher_model import Teacher +from model.users import User + +__all__ = [ + "Base", + "AdvisorInfo", + "ClsMgmt", + "StudentEmployManage", + "StuInfo", + "StuScore", + "Teacher", + "User", +] \ No newline at end of file diff --git a/model/advisor_model.py b/model/advisor_model.py new file mode 100644 index 0000000..4f0971f --- /dev/null +++ b/model/advisor_model.py @@ -0,0 +1,15 @@ +# model/advisor_model.py +from sqlalchemy import Column, String, Integer +from sqlalchemy.orm import relationship +from database import Base + + +class AdvisorInfo(Base): + __tablename__ = "advisor_info" + + id = Column(String(15), primary_key=True) + name = Column(String(20), nullable=False) + phone = Column(String(20), nullable=False) + is_deleted = Column(Integer, nullable=False, default=0) + + students = relationship("StuInfo", back_populates="adv") \ No newline at end of file diff --git a/model/cls_mgmt_model.py b/model/cls_mgmt_model.py index 1456ed6..90e16f7 100644 --- a/model/cls_mgmt_model.py +++ b/model/cls_mgmt_model.py @@ -1,7 +1,9 @@ -from sqlalchemy import Column, Integer, String, DateTime, Date +from sqlalchemy import Column, Integer, String, DateTime, Date, ForeignKey +from sqlalchemy.orm import relationship from sqlalchemy.sql import func from database import Base + class ClsMgmt(Base): """ 班级管理表模型 @@ -12,11 +14,19 @@ class ClsMgmt(Base): # 字段定义 id = Column(String(15), primary_key=True, nullable=False) # 班级编号,主键,非空 cls_start_date= Column(Date, nullable=False) #开课时间 - head_tea_id =Column(String(15),nullable=False) # 班主任id - lecturer_id=Column(String(15),nullable=False) # 主讲老师id - is_deleted=Column(Integer,nullable=False) - # created_at:创建时间,自动设置为当前时间(服务器时间) - # server_default=func.now() 表示由数据库生成默认值 - created_at = Column(DateTime(timezone=True), server_default=func.now()) - # updated_at:更新时间,当记录更新时自动设置为当前时间(由 SQLAlchemy 的 onupdate 触发) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file + head_tea_id =Column(String(15), ForeignKey("tea_info.id"),nullable=False) # 班主任id + lecturer_id=Column(String(15), ForeignKey("tea_info.id"),nullable=False) # 主讲老师id + is_deleted=Column(Integer,nullable=False, default=0) + + stu = relationship("StuInfo",back_populates="cls") + + head_teacher = relationship("Teacher", + foreign_keys=[head_tea_id], + back_populates="head_classes") + + lecturer = relationship("Teacher", + foreign_keys=[lecturer_id], + back_populates="lecturer_classes") + + + diff --git a/model/employ_model.py b/model/employ_model.py new file mode 100644 index 0000000..5b2996d --- /dev/null +++ b/model/employ_model.py @@ -0,0 +1,36 @@ +# model/employ_model.py +# 本文件定义 stu_emp_mgmt 表的结构,映射到 MySQL 数据库 +# author:王博 + +from sqlalchemy import Column, String, Date, Integer, ForeignKey, DECIMAL +from sqlalchemy.orm import relationship +from database import Base + + +class StudentEmployManage(Base): + """ + 学生就业管理表 的映射 模型表 + 对应 MYSQL 中的 stu_emp_mgmt 表 + """ + __tablename__ = 'stu_emp_mgmt' + stu_id = Column(String(15),ForeignKey('stu_info.id'),primary_key=True,nullable=False,) # 主键 索引 + emp_open_time = Column(Date,nullable=True,default=None) # 就业开放时间 + send_offer_time = Column(Date, nullable=True,default=None) # 收到Offer时间 + emp_company = Column(String(50),nullable=True,default=None) # 就业公司名称 + salary = Column(DECIMAL(10,2),nullable=True,default=None) # 薪资待遇(月薪/元),共10位数,最多两位小数 + is_deleted = Column(Integer, nullable=False,default=0) # 软删除: 0-正常 1-已删除 + + stu = relationship('StuInfo',back_populates="stu_emp_mgmt") # 与学生信息表一对一关联,stu为 学生就业管理表 的 学生属性 + + + def __repr__(self): + """ + 返回对象时,自定义 返回的对象信息 + :return: + """ + return (f'') \ No newline at end of file diff --git a/model/stu_model.py b/model/stu_model.py index faf4b37..3ea9c45 100644 --- a/model/stu_model.py +++ b/model/stu_model.py @@ -4,8 +4,9 @@ from database import Base #学生信息表模型 class StuInfo(Base): __tablename__="stu_info" + id = Column(String(15),primary_key=True) - cls_id=Column(String(15),ForeignKey(cls_mgmt.id),nullable=False) + cls_id=Column(String(15),ForeignKey("cls_mgmt.id"),nullable=False) name=Column(String(20),nullable=False) gender=Column(String(1),default="男") age=Column(Integer,default=None) @@ -15,8 +16,18 @@ class StuInfo(Base): education=Column(String(20),default=None) enr_date=Column(Date,default=None) grad_date = Column(Date, default=None) - advisor_id=Column(String(15),ForeignKey(advisor_info.id),default=None) + advisor_id=Column(String(15),ForeignKey("advisor_info.id"),default=None) state=Column(String(10),default="在读") - is_deleted=Column(Integer,default=0) + is_deleted=Column(Integer, nullable=False, default=0) + cls=relationship("ClsMgmt",back_populates="stu") - adv=relationship("AdvisorInfo",back_populates="stu") + + score = relationship("StuScore",back_populates="student") + + adv = relationship("AdvisorInfo", back_populates="students") + + stu_emp_mgmt = relationship( + "StudentEmployManage", + back_populates="stu", + uselist=False, + ) diff --git a/model/stu_score_model.py b/model/stu_score_model.py index 0713662..a74ff5f 100644 --- a/model/stu_score_model.py +++ b/model/stu_score_model.py @@ -2,11 +2,12 @@ # 本文件定义 User 表的结构,映射到 MySQL 数据库 from sqlalchemy import Column, Integer, String, DateTime, Boolean, ForeignKey +from sqlalchemy.dialects.mysql import DECIMAL from sqlalchemy.orm import relationship from sqlalchemy.sql import func from database import Base -class Stu_score(Base): +class StuScore(Base): """ 用户表模型 对应 MySQL 中的 users 表 @@ -15,20 +16,15 @@ class Stu_score(Base): # 字段定义 id = Column(Integer,primary_key=True, nullable=False,autoincrement=True) # - # stu_id = Column(String(10),ForeignKey(stu_info.id),nullable=False) #学生id,非空 - exam_attempt = Column(Integer, nullable=False) # 考试轮次,非空 - exam_score = Column(Integer, nullable=False,) # 成绩,非空,设置范围 - score_level=Column(String(10),nullable=False) #成绩评级 + stu_id = Column(String(15),ForeignKey("stu_info.id"),nullable=False) #学生id,非空 + exam_attempt = Column(Integer, nullable=False, default= 1) # 考试轮次,非空 + exam_score = Column(DECIMAL(5, 2), nullable=False,) # 成绩,非空,设置范围 + score_level=Column(String(10),nullable=False, default="普通") #成绩评级 is_deleted=Column(Boolean,default=False,nullable=False) #软删除: 0-正常 1-已删除 - name = relationship("Stu_info", back_populates="score") + + student = relationship("StuInfo", back_populates="score") def __repr__(self): return (f"") - # created_at:创建时间,自动设置为当前时间(服务器时间) - # server_default=func.now() 表示由数据库生成默认值 - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - # updated_at:更新时间,当记录更新时自动设置为当前时间(由 SQLAlchemy 的 onupdate 触发) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file + f"is_deleted={self.is_deleted})>") \ No newline at end of file diff --git a/model/teacher_model.py b/model/teacher_model.py index dbddb7b..8d391f3 100644 --- a/model/teacher_model.py +++ b/model/teacher_model.py @@ -1,17 +1,26 @@ #教师建表语句 from sqlalchemy import Column, String, Integer +from sqlalchemy.orm import relationship from database import Base class Teacher(Base): - __tablename__ = "ted_info" # 表名 + __tablename__ = "tea_info" # 表名 # 字段定义 - id = Column(Integer, primary_key=True, index=True) # 主键,索引 + id = Column(String(15), primary_key=True, index=True) # 主键,索引 name = Column(String(20),nullable=False) # 用户名,非空 phone = Column(String(20),unique=True,nullable=False) # 电话,唯一,非空 type = Column(String(20), nullable=False) # 教师职位,非空 - is_deleted = Column(Integer, default=0) # 逻辑删除(软删除),0是未删除,1是删除,默认为0 + is_deleted = Column(Integer, nullable=False, default=0) # 逻辑删除(软删除),0是未删除,1是删除,默认为0 + head_classes = relationship("ClsMgmt", + foreign_keys="ClsMgmt.head_tea_id", + back_populates="head_teacher") + + lecturer_classes = relationship("ClsMgmt", + foreign_keys="ClsMgmt.lecturer_id", + back_populates="lecturer") + diff --git a/model/users.py b/model/users.py deleted file mode 100644 index ce2d623..0000000 --- a/model/users.py +++ /dev/null @@ -1,26 +0,0 @@ -# model/users.py -# 本文件定义 User 表的结构,映射到 MySQL 数据库 - -from sqlalchemy import Column, Integer, String, DateTime -from sqlalchemy.sql import func -from database import Base - -class User(Base): - """ - 用户表模型 - 对应 MySQL 中的 users 表 - """ - __tablename__ = "users" # 表名 - - # 字段定义 - id = Column(Integer, primary_key=True, index=True) # 主键,自增,索引 - username = Column(String(50), unique=True, index=True, nullable=False) # 用户名,唯一,非空 - email = Column(String(100), unique=True, index=True, nullable=False) # 邮箱,唯一,非空 - full_name = Column(String(100), nullable=True) # 全名,可为空 - - # created_at:创建时间,自动设置为当前时间(服务器时间) - # server_default=func.now() 表示由数据库生成默认值 - created_at = Column(DateTime(timezone=True), server_default=func.now()) - - # updated_at:更新时间,当记录更新时自动设置为当前时间(由 SQLAlchemy 的 onupdate 触发) - updated_at = Column(DateTime(timezone=True), onupdate=func.now()) \ No newline at end of file diff --git a/scheme/employ_scheme.py b/scheme/employ_scheme.py new file mode 100644 index 0000000..ef36d36 --- /dev/null +++ b/scheme/employ_scheme.py @@ -0,0 +1,122 @@ +# scheme/employ.py +from datetime import date +from decimal import Decimal +from typing import List + +from pydantic import Field, field_validator, model_validator, ValidationError, BaseModel +from database import Base + +# ---------- -------------------------------请求模型 ------------------------------------------------ +class YearMonthDay(BaseModel): + year : int | None = Field(None,description="年份") + month: int | None = Field(None, le=12,ge=1 ,description="月份") + day: int | None = Field(None,ge=1 ,description="日期") + @model_validator(mode='after') + def check_year_month_day(self): + having_none = (self.year and self.month and self.day) + all_none= (not self.year and not self.month and not self.day) + all_has = (self.year and self.month and self.day and len(str(self.year))==4) + # if not having_none: + # raise ValueError("年月日必须全部填写或者全部为空") + if all_none: + return None + if all_has: + if self.month==2: + if (self.year % 4 == 0 and self.year % 100 != 0) or (self.year % 400 == 0): + if self.day>29: + raise ValueError("闰年2月最多29天") + return self + else: + if self.day > 28: + raise ValueError("平年2月最多28天") + return self + elif self.month in (1,3,5,7,8,10,12): + if self.day > 31: + raise ValueError("大月最多31天") + return self + else: + if self.day > 30: + raise ValueError("小月最多30天") + return self + + raise ValueError("年月日必须全部填写或者全部为空,且年份必须为4位数") + + +# 定义 记录学生就业状态 请求体模型 +class EmployStatusCreate(BaseModel): + """ + 记录学生就业状态接口,需要传入的请求体 + """ + stu_id: str = Field(..., description="学号,非空唯一") + emp_open_time: YearMonthDay | None = Field(None, description="就业开放时间,可更改学生就业状态") + send_offer_time: YearMonthDay | None = Field(None, description="offer下发时间,可更改学生就业状态") + emp_company: str | None = Field(None, description="就业公司名称") + salary: Decimal | None = Field(None, decimal_places=2,description="就业薪资,默认为空") # 薪资精确到2小数 + + + @model_validator(mode='after') + def check_offer_and_open_time(self): + """ + 自定义校验,就业开放时间必须 早于等于 offer下发时间,否则抛出422参数异常状态码; + 并且返回 就业状态记录 对象 自己 + :return: + """ + s, e = self.send_offer_time, self.emp_open_time + if s and e: + send_offer_time = date(s.year,s.month,s.day) + emp_open_time = date(e.year,e.month,e.day) + if send_offer_time < emp_open_time: # offer下发时间不能早于就业开放时间 + raise ValueError("offer下发时间不能早于就业开放时间") + return self + if not e: + if s and self.emp_company and self.salary: # 就业开放时间为null,offer下发时间、就业公司、薪水不能填写 + raise ValueError("学生在读,无法添加就业信息") + return self + else: + if not s: + if self.emp_company and self.salary: # offer下发时间为null,就业公司、薪水不能填写 + raise ValueError("学生进入就业中,无法添加已就业信息") + return self + if not self.emp_company and not self.salary: + raise ValueError("学生已就业,必须填写已就业信息") # 学生已就业,就业公司、薪水必须填写 + return self + +# 定义 查询学生就业状态 请求体模型 +class EmployStatusQuery(BaseModel): + """ + 请求体传入学生编号(可选)、就业公司(可选)、薪资范围(可选) + 可多条件查询 学生就业信息 + """ + stu_id: str | None = Field(None, description="根据学生编号查询") + emp_company: str | None = Field(None, description="根据就业公司名称查询") + min_salary: Decimal | None = Field(None, decimal_places=2,description="查询的就业薪资范围最小值") # 薪资精确到2小数 + max_salary: Decimal | None = Field(None, decimal_places=2, description="查询的就业薪资范围最大值") # 薪资精确到2小数 + skip:int = Field(1, description="页码") + limit: int = Field(10, description="每页条数") + +# 定义 删除学生就业状态 请求体模型 +class EmployStatusDelete(BaseModel): + """ + 请求体传入学生编号(可选)、就业公司(可选)、薪资范围(可选) + 可多条件查询 学生就业信息 + """ + stu_id: str = Field(..., description="根据学生编号查询") + +# ------------------------------------------ 响应模型 ------------------------------------------------ +class EmployStatusResponse(BaseModel): + stu_id: str + emp_open_time: YearMonthDay | None + send_offer_time: YearMonthDay | None + emp_company: str | None + salary: Decimal | None = Field(None, decimal_places=2,description="就业薪资,默认为空") # 薪资精确到2小数 + +class EmployDateResponse(BaseModel): + stu_id: str + emp_open_time: date | None + send_offer_time: date | None + emp_company: str | None + salary: Decimal | None = Field(None, decimal_places=2,description="就业薪资,默认为空") # 薪资精确到2小数 + +class EmployQueryResponse(BaseModel): + employ_info : List[EmployDateResponse] | None + total:int diff --git a/scheme/statistics_scheme.py b/scheme/statistics_scheme.py deleted file mode 100644 index 79d950b..0000000 --- a/scheme/statistics_scheme.py +++ /dev/null @@ -1,22 +0,0 @@ -# scheme/statistics_scheme.py -from pydantic import BaseModel, Field, EmailStr -from datetime import datetime -from typing import Optional - -# ---------- 请求模型 ---------- -# class UserCreate(BaseModel): -# username: str = Field(..., min_length=3, max_length=50) -# email: EmailStr -# full_name: Optional[str] = Field(None, max_length=100) -# -# class UserUpdate(BaseModel): -# username: Optional[str] = Field(None, min_length=3, max_length=50) -# email: Optional[EmailStr] = None -# full_name: Optional[str] = Field(None, max_length=100) - -# ---------- 响应模型 ---------- -# 多维度班级统计 -class StatsResponse(BaseModel): - total_count: int - man_count: int - female_count: int \ No newline at end of file diff --git a/scheme/stu_scheme.py b/scheme/stu_scheme.py new file mode 100644 index 0000000..afaa22b --- /dev/null +++ b/scheme/stu_scheme.py @@ -0,0 +1,80 @@ +from pydantic import BaseModel, Field, model_validator, field_validator, ConfigDict +from datetime import date, datetime +from typing import Optional + +#id用cls_id来拼接生成,不用请求体过滤 +#------------新增请求体------------- +class StudentCreate(BaseModel): + cls_id: int | str = Field(..., description="班级ID,6位数字") + name: str = Field(..., min_length=1, max_length=20, description="姓名") + gender: str = Field(..., min_length=1, max_length=1, description="性别") + age: int = Field(..., ge=1, le=100, description="年龄") + hometown: Optional[str] = Field(None, min_length=3, max_length=50, description="籍贯") + grad_school: str = Field(..., min_length=1, max_length=50, description="毕业院校") + major: str = Field(..., min_length=1, max_length=50, description="专业名称") + education: str = Field(..., min_length=1, max_length=20, description="学历") + enr_date: date = Field(..., description="入学日期") + grad_date: date = Field(..., description="毕业日期") + advisor_id: str = Field(..., min_length=1, max_length=15, description="顾问编号") +#校验入学时间必须比毕业时间小 + @model_validator(mode='after') + def check_dates(self): + if self.grad_date <= self.enr_date: + raise ValueError("毕业日期必须晚于入学日期") + return self +# 校验传入的6位数是不是都是数字/都转化为字符串判断 + @field_validator("cls_id", mode="before") + def convert_str(cls, v): + # 不管前端传数字还是字符串,统一转字符串 + v = str(v) + if len(v) != 6 or not v.isdigit(): + raise ValueError("班级ID必须是6位纯数字") + return v +#------------修改请求体------------- +class StudentUpdate(BaseModel): + cls_id: Optional[int|str] = Field(None, description="班级ID,6位数字") + name: Optional[str] = Field(None, min_length=1, max_length=20, description="姓名") + gender: Optional[str] = Field(None, min_length=1, max_length=1, description="性别") + age: Optional[int] = Field(None, ge=1, le=100, description="年龄") + hometown: Optional[str] = Field(None, min_length=3, max_length=50, description="籍贯") + grad_school: Optional[str] = Field(None, min_length=1, max_length=50, description="毕业院校") + major: Optional[str] = Field(None, min_length=1, max_length=50, description="专业名称") + education: Optional[str] = Field(None, min_length=1, max_length=20, description="学历") + enr_date: Optional[date]= Field(None, description="入学日期") + grad_date: Optional[date]= Field(None, description="毕业日期") + advisor_id: Optional[str] = Field(None, min_length=1, max_length=15, description="顾问编号") + + @field_validator("cls_id", mode="before") + def convert_str(cls, v): + # 更新:不传cls_id(v=None)直接返回,跳过校验 + if v is None: + return v + v = str(v) + if len(v) != 6 or not v.isdigit(): + raise ValueError("班级ID必须是6位纯数字") + return v + + @model_validator(mode='after') + def check_dates(self): + # 只在两个日期【都传了,不为None】的时候,才校验大小,传单个时在api层校验 + if self.grad_date is not None and self.enr_date is not None: + if self.grad_date <= self.enr_date: + raise ValueError("毕业日期必须晚于入学日期") + return self +#--------------响应体模型-------------- +class StudentResponse(BaseModel): + id: str + cls_id: str + name: str + gender: str + age: int + hometown: Optional[str] + grad_school: Optional[str] + major: Optional[str] + education: Optional[str] + enr_date: Optional[date] + grad_date: Optional[date] + advisor_id: Optional[str] + state: Optional[str] + + model_config = ConfigDict(from_attributes=True) # 支持 ORM 对象转换 \ No newline at end of file diff --git a/scheme/stu_score_scheme.py b/scheme/stu_score_scheme.py new file mode 100644 index 0000000..915346d --- /dev/null +++ b/scheme/stu_score_scheme.py @@ -0,0 +1,28 @@ +# scheme/stu_score_scheme.py +import decimal + +from pydantic import BaseModel, Field +# from datetime import datetime +# from typing import Optional + + + +# ---------- 请求模型 ---------- + + +# ---------- 响应模型 ---------- +class StuScoreResponse(BaseModel): + stu_id: str + exam_attempt: int + exam_score: decimal.Decimal = Field(max_digits=5, decimal_places=2) + score_level:str +class StuScoreCreateResponse(BaseModel): + stu_id: str + exam_attempt: int + exam_score: decimal.Decimal = Field(max_digits=5, decimal_places=2) + score_level: str + warnings:str|None= None + + + class Config: + from_attributes = True # 支持 ORM 对象转换 \ No newline at end of file diff --git a/scheme/teacher_scheme.py b/scheme/teacher_scheme.py new file mode 100644 index 0000000..fb26f38 --- /dev/null +++ b/scheme/teacher_scheme.py @@ -0,0 +1,46 @@ +#老师表请求体模型 +from http.client import HTTPException +from typing import Optional + +from fastapi.openapi.utils import status_code_ranges +from pydantic import Field, BaseModel, field_validator +from database import Base # Base 在 database.py 中 + +#添加教师信息请求体 +class TeacherAdd(BaseModel): + name : str = Field(..., max_length=50,description="姓名") + phone : str = Field(..., description="手机号") + type : str = Field(..., max_length=50, description="教师类型") + + # 校验器 输入手机号必须为11位 + @field_validator("phone") + @classmethod #类方法 + def phone_length(cls, v:str): + if len(v) != 11: + raise ValueError('手机号必须为11位') + return v + +class TeacherUpdate(BaseModel): + name: Optional[str] = Field(None,max_length=50, description="姓名") + phone: Optional[str] = Field(None,description="手机号") + type: Optional[str] = Field(None,max_length=50, description="教师类型") + + #校验器 输入手机号必须为11位 + @field_validator("phone") + @classmethod #类方法 + def phone_length(cls, v: Optional[str]): + if v is not None and len(v) != 11: + raise ValueError('手机号必须为11位') + return v + + + +#教师响应体模型 +class TeacherResponse(BaseModel): + id: str + name: str + phone: str + type: str + + + diff --git a/scheme/users.py b/scheme/users.py deleted file mode 100644 index 6e6c7e8..0000000 --- a/scheme/users.py +++ /dev/null @@ -1,27 +0,0 @@ -# scheme/users.py -from pydantic import BaseModel, Field, EmailStr -from datetime import datetime -from typing import Optional - -# ---------- 请求模型 ---------- -class UserCreate(BaseModel): - username: str = Field(..., min_length=3, max_length=50) - email: EmailStr - full_name: Optional[str] = Field(None, max_length=100) - -class UserUpdate(BaseModel): - username: Optional[str] = Field(None, min_length=3, max_length=50) - email: Optional[EmailStr] = None - full_name: Optional[str] = Field(None, max_length=100) - -# ---------- 响应模型 ---------- -class UserResponse(BaseModel): - id: int - username: str - email: str - full_name: Optional[str] - created_at: datetime - updated_at: Optional[datetime] - - class Config: - from_attributes = True # 支持 ORM 对象转换 \ No newline at end of file