From eaa940e751b23a3ca4f286b71adafb893195d004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E5=87=AF?= <1368058312@qq.com> Date: Mon, 21 Sep 2026 15:48:13 +0800 Subject: [PATCH 01/23] =?UTF-8?q?=E6=9B=BE=E5=87=AF=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86student=5Fid=E4=B8=AD=E5=A4=96=E9=94=AE=E7=BA=A6?= =?UTF-8?q?=E6=9D=9F=E6=9B=B4=E6=94=B9=E4=BA=86=E5=A4=A7=E5=86=99Student.i?= =?UTF-8?q?d=EF=BC=8C=E5=8E=9F=E5=85=88=E4=B8=BA=E5=B0=8F=E5=86=99student.?= =?UTF-8?q?id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/score.py b/models/score.py index c7fe39c..24c400a 100644 --- a/models/score.py +++ b/models/score.py @@ -9,7 +9,7 @@ class Score(Base): ,comment="成绩表序号,自增主键" ) student_id = Column(Integer - ,ForeignKey("student.id") + ,ForeignKey("Student.id") ,nullable=False ,comment="学生学号" ) From 37b79d371ac8b6ad3cf7b221998d1ec455f8abb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E5=87=AF?= <1368058312@qq.com> Date: Mon, 21 Sep 2026 16:07:31 +0800 Subject: [PATCH 02/23] =?UTF-8?q?=E6=9B=BE=E5=87=AF=E4=BF=AE=E6=94=B9?= =?UTF-8?q?=E4=BA=86student=5Fid=E4=B8=AD=E5=A4=96=E9=94=AE=E7=BA=A6?= =?UTF-8?q?=E6=9D=9F=E6=9B=B4=E6=94=B9=E4=BA=86student=5Finfo=5Fdetail.id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/score.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/models/score.py b/models/score.py index 24c400a..90e9682 100644 --- a/models/score.py +++ b/models/score.py @@ -9,7 +9,7 @@ class Score(Base): ,comment="成绩表序号,自增主键" ) student_id = Column(Integer - ,ForeignKey("Student.id") + ,ForeignKey("student_info_detail.id") ,nullable=False ,comment="学生学号" ) From 4d811348ac435851b64456253cd8e1008a2872d1 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 17:51:33 +0800 Subject: [PATCH 03/23] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=AD=A6=E7=94=9F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/student.py | 8 ++++---- dao/student.py | 9 +++++---- database.py | 2 +- main.py | 8 ++++---- schemas/student.py | 2 +- utils/code_generator.py | 2 +- 6 files changed, 16 insertions(+), 15 deletions(-) diff --git a/api/student.py b/api/student.py index 9299659..ac3edb9 100644 --- a/api/student.py +++ b/api/student.py @@ -1,14 +1,14 @@ from fastapi import APIRouter,Depends,HTTPException -from sqlalchemy.orm import Session from dao.student import * from schemas.student import StudentCreate - +from sqlalchemy.orm import Session student_router = APIRouter() @student_router.post("/student") -def create_student(request:StudentCreate,db:Session,create = Depends(create_students)): +def create_student(request:StudentCreate,db:Session = Depends(get_db)): d = request.model_dump() r = create_students(d,db) if not r: raise HTTPException(status_code=500,detail='服务器繁忙,请稍后添加!') - return {'code':200,'detail':"添加学生成功"} \ No newline at end of file + return {'code':200,'detail':"添加学生成功"} + diff --git a/dao/student.py b/dao/student.py index 9014fe5..49991d7 100644 --- a/dao/student.py +++ b/dao/student.py @@ -1,16 +1,17 @@ from schemas.student import * +from fastapi import Depends from core.database import get_db from models.student import * from utils.code_generator import generate_no +from sqlalchemy.orm import Session - -def create_students(d,db): +def create_students(d,db:Session = Depends(get_db)): stu = Student(**d) try: db.add(stu) - generate_no("student_no", "STU", stu, db) db.commit() - except:return False + except: + return False else: return True diff --git a/database.py b/database.py index ecff48f..7ee23b9 100644 --- a/database.py +++ b/database.py @@ -10,4 +10,4 @@ engine = create_engine(db_url Base.metadata.create_all(engine) db = SessionLocal() -db.commit() \ No newline at end of file +db.commit() #创建全部表 \ No newline at end of file diff --git a/main.py b/main.py index a0cfb07..d8e7658 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from api.example import example_router from middleware import log_middleware -from api.statistics import statistics_router +# from api.statistics import statistics_router import uvicorn from api.student import student_router app = FastAPI() @@ -12,14 +12,14 @@ from api.class_api import classes_router app.include_router(classes_router,tags=['班级接口'],prefix="/classes") #-------------李玉杰-------------- -app.include_router(student_router) +app.include_router(student_router,tags=['学生接口']) #-------------张义--------------- #-------------薄鑫--------------- #-------------张昕浩--------------- -app.include_router(example_router) -app.include_router(statistics_router, prefix="/statistics") +# app.include_router(example_router) +# app.include_router(statistics_router, prefix="/statistics") #-------------曾凯--------------- diff --git a/schemas/student.py b/schemas/student.py index d4a044a..64c42bb 100644 --- a/schemas/student.py +++ b/schemas/student.py @@ -7,7 +7,7 @@ class StudentCreate(BaseModel): class_id: int consultant_id: int | None = None native_place: str | None = None - graduate_school: str | None = None + graduation_school: str | None = None major: str | None = None enrollment_date: date graduation_date: date | None = None diff --git a/utils/code_generator.py b/utils/code_generator.py index f4b48ce..3320511 100644 --- a/utils/code_generator.py +++ b/utils/code_generator.py @@ -33,7 +33,7 @@ def add_student( ''' - +from sqlalchemy.orm import Session def generate_no(field_name: str, prefix: str, db_object, db: Session): if getattr(db_object, field_name): return From 8b0cdbdb1003b775341369b78bb767396cdf4b5d Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 17:53:12 +0800 Subject: [PATCH 04/23] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E5=A2=9E?= =?UTF-8?q?=E5=8A=A0=E5=AD=A6=E7=94=9F=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/student.py | 2 +- main.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/student.py b/api/student.py index ac3edb9..444dea2 100644 --- a/api/student.py +++ b/api/student.py @@ -4,7 +4,7 @@ from schemas.student import StudentCreate from sqlalchemy.orm import Session student_router = APIRouter() -@student_router.post("/student") +@student_router.post("") def create_student(request:StudentCreate,db:Session = Depends(get_db)): d = request.model_dump() r = create_students(d,db) diff --git a/main.py b/main.py index d8e7658..e15c6e2 100644 --- a/main.py +++ b/main.py @@ -12,7 +12,7 @@ from api.class_api import classes_router app.include_router(classes_router,tags=['班级接口'],prefix="/classes") #-------------李玉杰-------------- -app.include_router(student_router,tags=['学生接口']) +app.include_router(student_router,tags=['学生接口'],prefix="/students") #-------------张义--------------- #-------------薄鑫--------------- From 883b287e668ec8a6b4d872e3f906f927f13fe935 Mon Sep 17 00:00:00 2001 From: wolin_zhangyi <1141397129@qq.com> Date: Mon, 21 Sep 2026 18:46:42 +0800 Subject: [PATCH 05/23] 1.teacher dao 2.class_teacher 3.teacher api 4.teacher models 5.teacher schemas --- api/teacher.py | 129 ++++++++++++++++++++++ dao/teacher.py | 239 ++++++++++++++++++++++++++++++++++++++++ models/class_teacher.py | 52 +++++++++ models/teacher.py | 44 -------- schemas/teacher.py | 41 ++++++- 5 files changed, 460 insertions(+), 45 deletions(-) diff --git a/api/teacher.py b/api/teacher.py index e69de29..19e5985 100644 --- a/api/teacher.py +++ b/api/teacher.py @@ -0,0 +1,129 @@ +from typing import List +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from core import database +from core.database import get_db +from schemas.teacher import TeacherOut, TeacherCreate, TeacherUpdate, ClassTeachersCreate, ClassTeacherOut +# 导入dao层所有函数 +from models.teacher import Teacher +from models.class_teacher import ClassTeachers +from dao.teacher import ( + get_teacher_list, + get_teacher_by_id, + create_teacher, + update_teacher, + delete_teacher, + get_class_teacher_by_class_id, + create_class_teacher, + update_class_teacher_full, + delete_class_teacher +) + +router = APIRouter(prefix="/teacher", tags=["老师管理模块"]) + +#===========================老师表=============================== +@router.get("/list", response_model=List[TeacherOut]) +def api_get_teacher_list(db: Session = Depends(get_db)): + """ + GET 查询全部有效老师列表 + :param db: 数据库会话,Depends自动注入 + :return: List[TeacherOut] 老师列表,使用TeacherOut做响应格式化 + """ + return get_teacher_list(db) + +@router.get("/{teacher_id}", response_model=TeacherOut) +def api_get_teacher_by_id(teacher_id: int,db: Session = Depends(get_db)): + """ + 根据老师主键ID查询单个有效老师 + :param teacher_id: int 老师表主键ID + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: TeacherOut 老师详情信息 + """ + return get_teacher_by_id(db,teacher_id) +@router.post("", response_model=TeacherOut, summary="新增老师") +def api_create_teacher(obj: TeacherCreate, db: Session = Depends(get_db)): + """ + 新增老师记录,自动校验姓名、编号唯一性,并自动生成老师业务编号 + :param obj: TeacherCreate Pydantic模型,前端提交新增老师表单数据 + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: TeacherOut 新增成功后的老师完整信息 + """ + return create_teacher(db, obj) +@router.put("/{teacher_id}", response_model=TeacherOut, summary="修改老师信息(局部更新)") +def api_update_teacher(teacher_id: int, obj: TeacherUpdate, db: Session = Depends(get_db)): + """ + 修改老师信息,支持局部更新,仅更新前端传入的字段 + :param teacher_id: int 需要修改的老师主键ID + :param obj: TeacherUpdate Pydantic模型,前端提交待修改字段 + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: TeacherOut 更新后的老师完整信息 + """ + db_teacher = get_teacher_by_id(db, teacher_id) + if not db_teacher: + raise HTTPException(status_code=404, detail="老师不存在或已删除!") + result = update_teacher(db, teacher_id, obj) + return result +@router.delete("/{teacher_id}", summary="逻辑删除老师") +def api_delete_teacher(teacher_id: int, db: Session = Depends(get_db)): + """ + 逻辑删除老师,存在有效班级带班关系时禁止删除 + :param teacher_id: int 需要删除的老师主键ID + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: dict 操作成功提示 + """ + result = delete_teacher(db, teacher_id) + if not result: + raise HTTPException(status_code=404, detail="老师不存在或已删除!") + return {"msg": "老师逻辑删除成功"} + +# ===================== 班级老师关联路由组 ===================== +class_teacher_router = APIRouter(prefix="/class-teacher", tags=["班级老师分配管理模块"]) + +@class_teacher_router.get("/class/{class_id}", response_model=List[ClassTeacherOut], summary="根据班级ID查询该班级所有带班老师") +def api_get_class_teacher_by_class_id(class_id: int, db: Session = Depends(get_db)): + """ + 根据班级主键ID,查询该班级全部有效的班级-老师绑定记录 + :param class_id: int 班级表主键ID + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: List[ClassTeachersOut] 班级老师关联记录列表 + """ + return get_class_teacher_by_class_id(db, class_id) + +@class_teacher_router.post("", response_model=ClassTeacherOut, summary="分配老师到班级,新增班级老师关联记录") +def api_create_class_teacher(obj: ClassTeachersCreate, db: Session = Depends(get_db)): + """ + 新增班级与老师的绑定关系,做重复性校验与老师有效性校验 + :param obj: ClassTeachersCreate Pydantic模型,前端提交class_id、teacher_id、role + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: ClassTeachersOut 新增成功后的班级老师关联记录 + """ + return create_class_teacher(db, obj) + +@class_teacher_router.put("/{rel_id}", response_model=ClassTeacherOut, summary="更新班级老师绑定关系") +def api_update_class_teacher_full(rel_id: int, obj: ClassTeachersCreate, db: Session = Depends(get_db)): + """ + 修改班级老师中间表记录,支持修改class_id、teacher_id、role;自动校验老师有效性、三元组合唯一性 + :param rel_id: int class_teachers中间表主键ID(待修改的关联记录id) + :param obj: ClassTeachersCreate Pydantic模型,前端提交新的class_id、teacher_id、role + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: ClassTeachersOut 更新后的班级老师关联记录 + """ + result = update_class_teacher_full(db, rel_id, obj) + if not result: + raise HTTPException(status_code=404, detail="班级老师关联记录不存在!") + return result + +@class_teacher_router.delete("/{rel_id}", summary="逻辑删除班级老师带班关联") +def api_delete_class_teacher(rel_id: int, db: Session = Depends(get_db)): + """ + 逻辑删除班级老师绑定关系,修改is_deleted标记为1,非物理删除 + :param rel_id: int class_teachers中间表主键ID + :param db: Session 数据库会话对象,依赖注入自动获取 + :return: dict 操作成功提示 + """ + result = delete_class_teacher(db, rel_id) + if not result: + raise HTTPException(status_code=404, detail="班级老师关联记录不存在!") + return {"msg": "解除班级带班关系成功"} + + diff --git a/dao/teacher.py b/dao/teacher.py index e69de29..239c3d5 100644 --- a/dao/teacher.py +++ b/dao/teacher.py @@ -0,0 +1,239 @@ +from sqlalchemy.orm import Session +from core import database +from models.teacher import Teacher +from models.class_teacher import ClassTeachers +from schemas.teacher import TeacherUpdate, TeacherCreate,ClassTeachersCreate +from utils.code_generator import generate_no +from fastapi import HTTPException + +''' +本文件内容为老师表与班级老师关系表的增删改查函数集合 +''' +#================老师表Teacher的CRUD================ +#查询所有未被删除的老师 +def get_teacher_list(db:Session, teacher_id:int): + teacher = get_teacher_by_id(db, teacher_id) + if not teacher: + raise HTTPException(status_code=404, detail="老师不存在或已删除!") + #查询老师列表,过滤逻辑删除 + return db.query(Teacher).filter(Teacher.is_deleted == 0).all() +#根据id,查询单个符合条件的老师 +def get_teacher_by_id(db:Session,teacher_id:int): + return db.query(Teacher).filter(Teacher.id == teacher_id + ,Teacher.is_deleted == 0).first() +#新增 +def create_teacher(db:Session,obj:TeacherCreate): + ''' + 新增 + :param db: Session 数据库会话 + :param obj:TeacherCreate 前端传入新增数据 + :return: db_teacher 新增成功的ORM对象 + 业务逻辑: + 1、查重:同名有效老师不能重复新增 + 2、如果前端手动添加teacher_no,校验编号不能和已有有效老师编号重复 + 3、创建ORM对象加入会话 + 4、调用generate_no,走动生成业务编号 + 5、commit提交事务入库,refresh刷新对象 + ''' + exist_teacher = db.query(Teacher).filter(Teacher.teacher_name == obj.teacher_name + ,Teacher.is_deleted == 0).first() + if exist_teacher: + raise HTTPException(status_code=409,detail='该用户已存在') + if obj.teacher_no: + exist_teacher = db.query(Teacher).filter(Teacher.teacher_no == obj + .teacher_no,Teacher.is_deleted == 0).first() + if exist_teacher: + raise HTTPException(status_code=409,detail='老师编号已存在') + #开始添加 + data = obj.model_dump(exclude_none=True) + db_teacher = Teacher(**data) + db.add(db_teacher) + # 调用公共函数生成业务编号,内部执行db.flush拿到自增id + generate_no('teacher_no','TCH',db_teacher,db) + db.commit() + db.refresh(db_teacher) + #从数据库重新查询这条记录,刷新 db_teacher 对象。 + #作用:同步数据库默认字段的值,例如`created_at`、`updated_at`,拿到数据库里最新的数据, + # 最终返回给前端。 + return db_teacher +#更新 +def update_teacher(db:Session,teacher_id:int,obj:TeacherUpdate): + """ + 修改老师信息,局部更新 + :param db: Session 数据库会话 + :param teacher_id: int 需要修改的老师id + :param obj: TeacherUpdate 前端传入要修改的字段 + :return: db_teacher ORM对象 / None + """ + # 调用get_teacher_by_id校验用户是否存在 + db_teacher = get_teacher_by_id(db,teacher_id) + if obj.teacher_name: + exist_teacher = db.query(Teacher).filter( + Teacher.teacher_name == obj.teacher_name, + Teacher.id != teacher_id, + Teacher.is_deleted == 0 + ).first() + if exist_teacher: + raise HTTPException(status_code=409, detail="该姓名已被其他老师占用!") + if obj.teacher_no: + exist_no = db.query(Teacher).filter( + Teacher.teacher_no == obj.teacher_no, + Teacher.id != teacher_id, + Teacher.is_deleted == 0 + ).first() + if exist_no: + raise HTTPException(status_code=409, detail="老师编号已被其他老师占用!") + #用户存在,解析键值对,循环使用setattr添加 + update_data = obj.model_dump(exclude_none=True) + for k, v in update_data.items(): + setattr(db_teacher, k, v) + db.commit() + db.refresh(db_teacher) + return db_teacher +#删除 +def delete_teacher(db:Session,teacher_id:int): + """ + 逻辑删除老师,不是物理删除;修改is_deleted=1 + :param db: Session + :param teacher_id:老师主键id + :return: Teacher ORM对象,找不到返回None + 业务限制:老师已经分配班级,则不允许删除,防止脏数据 + """ + # 查询目标老师 + db_teacher = get_teacher_by_id(db, teacher_id) + if not db_teacher: + return None + + # 校验:查询班级老师中间表,判断该老师是否还有有效带班关系 + rel = db.query(ClassTeachers).filter( + ClassTeachers.teacher_id == teacher_id, + ClassTeachers.is_deleted == 0 + ).first() + if rel: + raise HTTPException( + status_code=400, + detail="该老师已分配班级,不能删除,请先解除带班关系!" + ) + + # 逻辑删除标记 + db_teacher.is_deleted = 1 + db.commit() + return db_teacher + +#==================班级老师中间表 CRUD===================== +#查询 +def get_class_teacher_by_class_id(db: Session, class_id: int): + """ + 根据班级id,查询该班级所有有效的带班老师关系 + :param db: Session + :param class_id:班级主键 + :return: list[ClassTeacher] + """ + return ( + db.query(ClassTeachers) + .filter(ClassTeachers.class_id == class_id + ,ClassTeachers.is_deleted == 0) + .all() + ) + +#新增 +def create_class_teacher(db: Session, obj: ClassTeachersCreate): + """ + 分配老师到班级,新增班级-老师关系记录 + :param db: Session + :param obj: ClassTeacherCreate class_id、teacher_id、role + :return: ClassTeacher ORM对象 + 校验: + 1.老师必须存在且未删除 + 2.同一班级同一个老师同一个角色不能重复分配 + """ + # 校验老师是否存在 + teacher = get_teacher_by_id(db, obj.teacher_id) + if not teacher: + raise HTTPException(status_code=404, detail="老师不存在或已删除!") + + # 查重:同一班级+同一老师+同一角色不能重复分配 + rel = db.query(ClassTeachers).filter( + ClassTeachers.class_id == obj.class_id, + ClassTeachers.teacher_id == obj.teacher_id, + ClassTeachers.role == obj.role, + ClassTeachers.is_deleted == 0 + ).first() + if rel: + raise HTTPException(status_code=409, detail="该老师已以相同角色分配到该班级!") + + data = obj.model_dump() + db_obj = ClassTeachers(**data) + db.add(db_obj) + db.commit() + db.refresh(db_obj) + + return db_obj + +#更新 +def update_class_teacher_full(db: Session, rel_id: int, obj: ClassTeachersCreate): + """ + 完整更新班级老师关系:可修改 class_id、teacher_id、role + :param db: Session + :param rel_id: 中间表主键 + :param obj: ClassTeacherCreate 传入的新参数 + :return: ClassTeacher ORM对象 + 校验: + 1. 关系记录存在且未删除 + 2. 如果修改teacher_id,校验老师必须有效存在 + 3. 校验新组合(class_id+teacher_id+role)不能和其他有效记录重复 + """ + # 1. 查询原有记录 + db_rel = db.query(ClassTeachers).filter( + ClassTeachers.id == rel_id, + ClassTeachers.is_deleted == 0 + ).first() + if not db_rel: + return None + + # 2. 如果老师ID发生变化,校验老师是否有效 + if obj.teacher_id != db_rel.teacher_id: + teacher = get_teacher_by_id(db, obj.teacher_id) + if not teacher: + raise HTTPException(status_code=404, detail="老师不存在或已删除!") + + # 3. 查重:新的三元组合不能重复(排除自身这条记录) + exist_rel = db.query(ClassTeachers).filter( + ClassTeachers.class_id == obj.class_id, + ClassTeachers.teacher_id == obj.teacher_id, + ClassTeachers.role == obj.role, + ClassTeachers.is_deleted == 0, + ClassTeachers.id != rel_id + ).first() + if exist_rel: + raise HTTPException(status_code=409, detail="该老师已以相同角色分配到该班级!") + + # 赋值更新 + db_rel.class_id = obj.class_id + db_rel.teacher_id = obj.teacher_id + db_rel.role = obj.role + + db.commit() + db.refresh(db_rel) + return db_rel + + +#删除 +def delete_class_teacher(db: Session, rel_id: int): + """ + 逻辑删除班级老师带班关系 + :param db: Session + :param rel_id: class_teachers表主键id + :return: ClassTeacher ORM对象,找不到返回None + """ + db_obj = ( + db.query(ClassTeachers) + .filter(ClassTeachers.id == rel_id, ClassTeachers.is_deleted == 0) + .first() + ) + if not db_obj: + return None + + db_obj.is_deleted = 1 + db.commit() + return db_obj diff --git a/models/class_teacher.py b/models/class_teacher.py index e69de29..db97dfd 100644 --- a/models/class_teacher.py +++ b/models/class_teacher.py @@ -0,0 +1,52 @@ +from sqlalchemy import (Column, Integer, String, Text, + BigInteger,ForeignKey,VARCHAR,DateTime + ) +from sqlalchemy.dialects.mssql import TINYINT + +from core.database import Base +from datetime import datetime + +#班级老师关系表 class_teachers_info_detail 中间表 ORM 模型完整定义 +#字段包括id,class_id,teacher_id,role,created_at,updated_at,is_deleted +class ClassTeachers(Base): + __tablename__ = "class_teachers_info_detail" + id = Column( + BigInteger + ,primary_key=True + ,autoincrement=True + ,comment='关系') + class_id = Column( + BigInteger + ,ForeignKey('class_id') + ,nullable=False + ,comment='关联 class_id' + ) + teacher_id = Column( + BigInteger + ,ForeignKey('teacher_id') + ,nullable=False + ,comment='关联 teacher_id' + ) + role = Column( + VARCHAR(30) + ,nullable=False + ,comment='老师在班级中的角色:head_teacher班主任 / lecturer授课老师 / assistant助教' + ) + created_at = Column( + DateTime + ,nullable=False + ,default=datetime.now() + ,comment='创建时间' + ) + updated_at = Column( + DateTime + ,nullable=False + ,default=datetime.now() + ,comment='更新时间' + ) + is_deleted = Column( + TINYINT + ,nullable=False + ,default=0 + ,comment='逻辑删除' + ) diff --git a/models/teacher.py b/models/teacher.py index fe6ff57..bcfe6a0 100644 --- a/models/teacher.py +++ b/models/teacher.py @@ -62,48 +62,4 @@ class Teacher(Base): ) -#班级老师关系表 class_teachers_info_detail 中间表 ORM 模型完整定义 -#字段包括id,class_id,teacher_id,role,created_at,updated_at,is_deleted -class ClassTeachers(Base): - __tablename__ = "class_teachers_info_detail" - id = Column( - BigInteger - ,primary_key=True - ,autoincrement=True - ,comment='关系') - class_id = Column( - BigInteger - ,ForeignKey('class_id') - ,nullable=False - ,comment='关联 class_id' - ) - teacher_id = Column( - BigInteger - ,ForeignKey('teacher_id') - ,nullable=False - ,comment='关联 teacher_id' - ) - role = Column( - VARCHAR(30) - ,nullable=False - ,comment='老师在班级中的角色:head_teacher班主任 / lecturer授课老师 / assistant助教' - ) - created_at = Column( - DateTime - ,nullable=False - ,default=datetime.now() - ,comment='创建时间' - ) - updated_at = Column( - DateTime - ,nullable=False - ,default=datetime.now() - ,comment='更新时间' - ) - is_deleted = Column( - TINYINT - ,nullable=False - ,default=0 - ,comment='逻辑删除' - ) diff --git a/schemas/teacher.py b/schemas/teacher.py index 93cb1fb..6ecac07 100644 --- a/schemas/teacher.py +++ b/schemas/teacher.py @@ -1,10 +1,16 @@ from pydantic import BaseModel from typing import Optional from datetime import datetime + +from sqlalchemy import BigInteger +from sqlalchemy.dialects.mssql import TINYINT +from sqlalchemy.dialects.mysql import VARCHAR + + #===============老师表=================== #===============请求体模型================ class TeacherCreate(BaseModel): - teacher_no: Optional[str] = None + teacher_no: Optional[int] = None teacher_name: Optional[str] = None email: Optional[str] = None phone: Optional[str] = None @@ -17,3 +23,36 @@ class TeacherUpdate(BaseModel): phone: Optional[str] = None remark: Optional[str] = None +#================响应体================== +class TeacherOut(BaseModel): + id: int + teacher_no: str + teacher_name: str + phone: Optional[str] + email: Optional[str] + remark: Optional[str] + created_at: datetime + updated_at: datetime + is_deleted: int +#`orm_mode = True` 是 Pydantic v1 的配置项。开启之后,Pydantic 可以直接读取 SQLAlchemy +# 查询得到的 ORM 对象的属性,自动映射生成接口响应 JSON,不用手动把 ORM 对象转为字典。 +# 这个配置只用在响应体 Out 模型,新增和修改的请求模型不需要。 + class Config: + from_attributes=True + +#=====班级老师中间表 class_teachers ========= +class ClassTeachersCreate(BaseModel): + class_id: int + teacher_id: int + role: str +#=================响应体=================== +class ClassTeacherOut(BaseModel): + id: int + class_id: int + teacher_id: int + role: str + created_at: datetime + updated_at: datetime + is_deleted: int + class Config: + from_attributes=True From 30e6af308d9d13437d381a48db064414fc0f4278 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 19:28:15 +0800 Subject: [PATCH 06/23] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AD=A6=E7=94=9F=E6=8E=A5=E5=8F=A3=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90studentno=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/student.py | 4 +++- dao/student.py | 37 ++++++++++++++++++++++++++----------- models/student.py | 2 +- schemas/student.py | 2 +- 4 files changed, 31 insertions(+), 14 deletions(-) diff --git a/api/student.py b/api/student.py index 444dea2..50311ab 100644 --- a/api/student.py +++ b/api/student.py @@ -4,7 +4,7 @@ from schemas.student import StudentCreate from sqlalchemy.orm import Session student_router = APIRouter() -@student_router.post("") +@student_router.post("",summary='添加学生') def create_student(request:StudentCreate,db:Session = Depends(get_db)): d = request.model_dump() r = create_students(d,db) @@ -12,3 +12,5 @@ def create_student(request:StudentCreate,db:Session = Depends(get_db)): raise HTTPException(status_code=500,detail='服务器繁忙,请稍后添加!') return {'code':200,'detail':"添加学生成功"} + + diff --git a/dao/student.py b/dao/student.py index 49991d7..9fc678b 100644 --- a/dao/student.py +++ b/dao/student.py @@ -1,17 +1,32 @@ from schemas.student import * -from fastapi import Depends from core.database import get_db from models.student import * -from utils.code_generator import generate_no from sqlalchemy.orm import Session -def create_students(d,db:Session = Depends(get_db)): - stu = Student(**d) - try: - db.add(stu) - db.commit() - except: - return False - else: - return True +def create_students(d: dict, db: Session): + stu = Student(**d) + + try: + # 1. 添加到 Session + db.add(stu) + + # 2. flush,让 MySQL 先生成自增 id + db.flush() + + # 3. 根据 id 生成学生编号 + if not stu.student_no: + stu.student_no = f"STU{stu.id:04d}" + + # 4. 提交事务 + db.commit() + + # 5. 刷新对象,拿到数据库最终数据 + db.refresh(stu) + + return stu + + except Exception as e: + db.rollback() + print(f"插入学生失败: {e}") + return None \ No newline at end of file diff --git a/models/student.py b/models/student.py index ab805e4..881c9b8 100644 --- a/models/student.py +++ b/models/student.py @@ -10,7 +10,7 @@ class Student(Base): , comment = '学生编号,自增主键' ) student_no = Column(VARCHAR(50) - , unique = True) + ,nullable = True) student_name = Column(VARCHAR(50)) class_id = Column(Integer ,ForeignKey('class_info_detail.id') diff --git a/schemas/student.py b/schemas/student.py index 64c42bb..6265446 100644 --- a/schemas/student.py +++ b/schemas/student.py @@ -2,7 +2,7 @@ from pydantic import BaseModel from datetime import date class StudentCreate(BaseModel): - student_no: str|None = None + student_no:str|None = None student_name: str class_id: int consultant_id: int | None = None From 946d428c8b1055c1310ee077143cfd08181b203f Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Mon, 21 Sep 2026 19:55:51 +0800 Subject: [PATCH 07/23] =?UTF-8?q?=E5=AE=8C=E5=96=84=E7=BB=9F=E8=AE=A1?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E5=8F=8A=E6=A8=A1=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 7 +- api/statistics.py | 65 ++++++++++- dao/statistics.py | 58 +++++++++- docs/模型与建表改动说明.md | 217 +++++++++++++++++++++++++++++++++++++ models/class_model.py | 12 +- models/class_teacher.py | 33 +++--- models/consultant.py | 9 +- models/employment.py | 17 ++- models/score.py | 22 ++-- models/student.py | 33 ++++-- models/teacher.py | 14 ++- 11 files changed, 428 insertions(+), 59 deletions(-) create mode 100644 docs/模型与建表改动说明.md diff --git a/.gitignore b/.gitignore index bde1ffa..b220eb5 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,9 @@ env/ # 系统垃圾 .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db + + +.idea/ +.idea.backup/ +.run/ \ No newline at end of file diff --git a/api/statistics.py b/api/statistics.py index 082f7f4..4875d0f 100644 --- a/api/statistics.py +++ b/api/statistics.py @@ -1,9 +1,10 @@ from fastapi import APIRouter,HTTPException,Depends -from DoubaoTeam.core.database import get_db -from DoubaoTeam.models.student import Student -from DoubaoTeam.models.score import Score -from DoubaoTeam.dao.statistics import * -from DoubaoTeam.schemas.common import SuccessResponse +from core.database import get_db +from models.employment import Employment +from models.student import Student +from models.score import Score +from dao.statistics import * +from schemas.common import SuccessResponse from sqlalchemy.orm import Session @@ -26,3 +27,57 @@ def api_gender_count(db:Session=Depends(get_db)): except NoExist as e: raise HTTPException(status_code=404,detail=str(e)) +@statistics_router.get("/scores/all-above-80",response_model=SuccessResponse) +def all_above_eighty(db:Session=Depends(get_db)): + try: + result = score_over_eighty(Score,Student,db) + return SuccessResponse(status_code=200,msg=result) + except NoExist as e: + raise HTTPException(status_code=404,detail=str(e)) + +@statistics_router.get("/scores/fail-more-than-two",response_model=SuccessResponse) +def fail_two(db:Session=Depends(get_db)): + try: + result = twice_error(Score,Student,db) + return SuccessResponse(status_code=200,msg=result) + except NoExist as e: + raise HTTPException(status_code=404,detail=str(e)) + +@statistics_router.get("/scores/class-average",response_model=SuccessResponse) +def class_average(db:Session=Depends(get_db)): + try: + result = avg_class(Score,Student,db) + return SuccessResponse(status_code=200,msg=result) + except NoExist as e: + raise HTTPException(status_code=404,detail=str(e)) + + +@statistics_router.get("/employments/top5-salary",response_model=SuccessResponse) +def top_salary(db:Session=Depends(get_db)): + try: + result=max_salary(Student,Employment,db) + return SuccessResponse(status_code=200,msg=result) + except NoExist as e: + raise HTTPException(status_code=404,detail=str(e)) + +@statistics_router.get("/employments/student-duration",response_model=SuccessResponse) +def employ_time(db:Session=Depends(get_db)): + try: + result = student_market(Employment,db) + return SuccessResponse(status_code=200,msg=result) + except NoExist as e: + raise HTTPException(status_code=404,detail=str(e)) + +@statistics_router.get("/employments/class-average-duration",response_model=SuccessResponse) +def avg_employ_time(db:Session=Depends(get_db)): + try: + result = avg_class(Employment,Student,db) + return SuccessResponse(status_code=200,msg=result) + except NoExist as e: + raise HTTPException(status_code=404,detail=str(e)) + + + + + + diff --git a/dao/statistics.py b/dao/statistics.py index 6b7c9e8..fdfe20b 100644 --- a/dao/statistics.py +++ b/dao/statistics.py @@ -1,6 +1,6 @@ -from DoubaoTeam.schemas.statistics import NoExist +from schemas.statistics import NoExist from sqlalchemy import func - +from time import time #`GET /statistics/students/age-over-30` #查询所有超过 30 岁的学员信息 @@ -23,8 +23,58 @@ def gender_count(Student,db): #查询每次考试成绩都在 80 分以上的学生编号、姓名和成绩 def score_over_eighty(Score,Student,db): student_list=db.query(Student.id, Student.student_name)\ - .join(Student,Score.student_id==Student.id).group_by(Student.student_no,Student.student_name)\ + .join(Score,Score.student_id==Student.id).group_by(Student.student_no,Student.student_name)\ .having(func.min(Score.score)>80).all() + if not student_list : + raise NoExist('全是学渣') + list1 = [] for i in student_list: - student_list2 = db.query(Student.student_no, Student.student_name,Score.score).join(Student,Score.student_id==Student.id).filter(i.id==Student.id).all() + student_list2 = db.query(Student.student_no, Student.student_name,Score.score).join(Score,Score.student_id==Student.id).filter(i.id==Student.id).all() + if student_list2 : + list1.extend(student_list2) + return list1 +#查询有两次以上不及格的学生姓名、班级和不及格成绩 +def twice_error(Score,Student,db): + student_list=db.query(Score.student_id).filter(Score.score<60).group_by(Score.student_id).having(func.count(Score.id)>1).all() + if not student_list : + raise NoExist('全是学霸') + list1=[] + for i in student_list: + result=db.query(Student.student_name,Student.class_id,Score.score).join(Score, Student.id==Score.student_id)\ + .filter(i.student_id==Score.student_id,Score.score<60).all() + list1.extend(result) + return list1 + +#统计每次考试每个班级的平均分,并按照平均分从高到低排序 +def avg_class(Score,Student,db): + student_list = db.query(Student.class_id,Score.exam_seq,func.avg(Score.score)).join(Score, Student.id == Score.student_id).group_by(Student.class_id,Score.exam_seq).order_by(func.avg(Score.score).desc()).all() + if not student_list : + raise NoExist('招个人吧') + return student_list + +#统计就业薪资最高的前五名学生姓名、班级、就业时间、就业公司 +def max_salary(Student,Employment,db): + student_list = db.query(Student.student_name,Student.class_id,Employment.offer_date,Employment.company_name).join(Employment, Student.id == Employment.student_id).order_by(Employment.salary_max.desc()).limit(5).all() + if not student_list : + raise NoExist('没就业的啊兄弟?') + return student_list + +# - 统计每个学生的就业时长 +# - 就业时长 = `offer_date - employment_open_date` +def student_market(Employment,db): + student_list = db.query(Employment.student_id,func.datediff(Employment.offer_date,Employment.employment_open_date)).all() + return student_list + +# - 统计每个班级的平均就业时长 +# - 只统计有就业开放时间的学生 +def avg_emptime(Employment,Student,db): + student_list = (db.query(Student.class_id,\ + func.avg(func.datediff(Employment.offer_date,Employment.employment_open_date)))\ + .join(Employment, Student.id==Employment.student_id).filter(Employment.employment_open_date.is_not(None)))\ + .group_by(Student.class_id).all() + if not student_list : + raise NoExist('一个就业都没有吗') + return student_list + + diff --git a/docs/模型与建表改动说明.md b/docs/模型与建表改动说明.md new file mode 100644 index 0000000..933eb42 --- /dev/null +++ b/docs/模型与建表改动说明.md @@ -0,0 +1,217 @@ +# 数据库模型与建表工具改动说明 + +本次按照《沃林学生管理系统小组分工说明书 V1》修正模型定义,整理范围为 7 个 Model 文件和 1 个建表脚本。各模块继续使用 `core.database.Base`,保留原有 Python 类名和项目目录。 + +## 一、改动总览 + +| 模块 | 负责人 | 修改文件 | 模型类 | 最终表名 | +|---|---|---|---|---| +| 学生管理 | 李玉杰 | `models/student.py` | `Student` | `students` | +| 班级管理 | 婷婷 | `models/class_model.py` | `Classes` | `classes` | +| 老师管理 | 张义 | `models/teacher.py` | `Teacher` | `teachers` | +| 班级老师关系 | 张义 | `models/class_teacher.py` | `ClassTeachers` | `class_teachers` | +| 顾问管理 | 薄鑫 | `models/consultant.py` | `Consultant` | `consultants` | +| 成绩管理 | 曾凯 | `models/score.py` | `Score` | `scores` | +| 就业管理 | 南方宇 | `models/employment.py` | `Employment` | `employments` | +| 公共建库建表工具 | 张昕浩 | `utils/create database.py` | — | 使用上述全部模型 | + +### 经确认保留的类型 + +本次没有将所有主键统一为文档中的 `BIGINT`,成绩精度也按已确认的要求保留。 + +| 项目 | 当前模型类型 | +|---|---| +| 学生、班级、老师、顾问、成绩表的 `id` | `Integer` | +| 班级老师关系表、就业表的 `id` | `BigInteger` | +| 各表关联其他表的外键字段 | `Integer`,与对应模型的主键一致 | +| 成绩字段 `scores.score` | `DECIMAL(4,1)` | +| 薪资字段 `employments.salary` | `DECIMAL(10,2)` | + +其中 `class_teachers.class_id`、`class_teachers.teacher_id`、`employments.student_id` 原为 `BigInteger`,经确认改为 `Integer`,解决 MySQL 要求外键与被引用主键类型一致的问题。 + +## 二、公共字段调整 + +以下规则落实在各个 Model 文件中,没有增加公共模型文件或额外继承层。 + +| 字段 | 最终定义及作用 | +|---|---| +| `created_at` | `DATETIME`、不可为空;保留 ORM 创建时间默认值,增加数据库默认值 `CURRENT_TIMESTAMP` | +| `updated_at` | `DATETIME`、不可为空;使用 `onupdate=datetime.now`,增加数据库默认值及 `ON UPDATE CURRENT_TIMESTAMP` | +| `is_deleted` | MySQL `TINYINT`、不可为空;ORM 默认值和数据库默认值均为 `0` | + +原先误用 `sqlalchemy.dialects.mssql.TINYINT` 的老师、班级老师关系模型,改为从 `sqlalchemy.dialects.mysql` 导入。 + +`is_deleted` 是逻辑删除标志。Model 中声明这个字段不会自动过滤查询;接口查询仍需添加 `is_deleted == 0`,删除业务记录应更新该标志。 + +## 三、学生管理模块 + +文件:`models/student.py`;负责人:李玉杰。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 表名 | `student_info_detail` | `students` | +| 学生编号 | 唯一,但允许为空 | 唯一且 `nullable=False` | +| 学生姓名 | 允许为空 | `nullable=False` | +| 班级外键 | `foreign_key='class_info_detail.id'`,不是有效外键声明 | `ForeignKey('classes.id')`,并设为非空 | +| 顾问外键 | `foreign_key='consultant_info_detail.id'`,不是有效外键声明 | `ForeignKey('consultants.id')`,继续允许为空 | +| 毕业院校字段 | `graduation_school` | `graduate_school` | +| 创建时间字段 | `create_at` | `created_at` | +| 更新时间字段 | `update_at` | `updated_at` | +| 逻辑删除 | `Integer`,允许为空 | MySQL `TINYINT`、非空、默认 `0` | + +时间字段按公共规则补齐数据库默认值及非空约束。学生主键、班级外键和顾问外键均保留 `Integer`。 + +## 四、班级管理模块 + +文件:`models/class_model.py`;负责人:婷婷。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 开课日期 `start_date` | 允许为空 | `nullable=False`,开课时间必填 | +| 更新时间字段 | `update_at` | `updated_at` | +| 备注长度 | `String(100)` | `String(255)` | +| 逻辑删除 | `INTEGER` | MySQL `TINYINT`,补充数据库默认值 `0` | + +时间字段增加数据库默认值和数据库自动更新时间设置。保留表名 `classes`、类名 `Classes`、`Integer` 自增主键,以及班级编号唯一且非空的约束。 + +## 五、老师管理模块 + +文件:`models/teacher.py`;负责人:张义。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 表名 | `teachers_info_detail` | `teachers` | +| 备注长度 | `VARCHAR(2550)` | `VARCHAR(255)` | +| `TINYINT` 来源 | SQL Server 方言 | MySQL 方言 | +| 更新时间 | 仅设置创建时的默认时间 | 补充 `onupdate=datetime.now` 和数据库自动更新时间 | + +创建时间增加数据库默认值,逻辑删除增加数据库默认值 `0`。保留 `Integer` 自增主键和老师编号唯一约束。 + +## 六、班级老师关系模块 + +文件:`models/class_teacher.py`;负责人:张义。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 表名 | `class_teachers_info_detail` | `class_teachers` | +| 班级外键目标 | `ForeignKey('class_id')` | `ForeignKey('classes.id')` | +| 老师外键目标 | `ForeignKey('teacher_id')` | `ForeignKey('teachers.id')` | +| 两个外键字段类型 | `BigInteger` | `Integer`,与班级、老师模型主键匹配 | +| 联合唯一约束 | 未设置 | `(class_id, teacher_id, role)` 联合唯一 | +| 联合唯一约束名称 | — | `uk_class_teacher_role` | +| 时间默认值 | `datetime.now()`,导入模型时就求值 | `datetime.now`,插入记录时再求值 | +| `TINYINT` 来源 | SQL Server 方言 | MySQL 方言 | + +补齐时间及逻辑删除的数据库默认值,并为更新时间增加自动更新设置。关系表自身的 `id` 保留 `BigInteger`,类名保留 `ClassTeachers`。 + +联合唯一约束防止同一个老师以同一个角色被重复分配到同一个班级。 + +## 七、顾问管理模块 + +文件:`models/consultant.py`;负责人:薄鑫。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 联系电话长度 | `String(11)` | `String(20)` | +| 逻辑删除类型 | `Integer` | MySQL `TINYINT` | +| 数据库默认值 | 未声明 | 补充创建时间、更新时间和逻辑删除默认值 | + +保留表名 `consultants`、`Integer` 自增主键、顾问编号唯一且非空的约束,以及其余业务字段。 + +## 八、成绩管理模块 + +文件:`models/score.py`;负责人:曾凯。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 表名 | `score_info_detail` | `scores` | +| 学生外键目标 | `ForeignKey('student.id')` | `ForeignKey('students.id')` | +| 成绩是否必填 | 允许为空 | `nullable=False` | +| 联合唯一约束名称 | `uk_student_exam_seq` | `uk_scores_student_exam` | +| 备注长度 | `String(225)` | `String(255)` | +| 逻辑删除 | `Integer`,允许为空 | MySQL `TINYINT`、非空、默认 `0` | + +创建时间和更新时间补齐非空约束、数据库默认值及自动更新设置;修正注释,明确联合唯一约束是“学生 ID + 考核序次”。 + +保留 `Integer` 主键及学生外键,成绩精度保留 `DECIMAL(4,1)`,保留成绩范围 `0~100` 的检查约束。一个学生同一次考核只能有一条成绩。 + +## 九、就业管理模块 + +文件:`models/employment.py`;负责人:南方宇。 + +| 修改项 | 修改前 | 修改后 | +|---|---|---| +| 学生外键类型 | `BigInteger` | `Integer`,与学生模型主键匹配 | +| 学生就业唯一约束 | 未设置 | `student_id` 增加 `unique=True` | +| 就业状态长度 | `String(50)` | `String(30)` | +| 就业状态是否必填 | 允许为空 | `nullable=False` | +| 逻辑删除 | `SmallInteger`,允许为空 | MySQL `TINYINT`、非空、默认 `0` | + +创建时间和更新时间补齐非空约束、数据库默认值及自动更新设置。修正薪资字段注释:`DECIMAL` 是定点小数。 + +保留表名 `employments`、`BigInteger` 自增主键、`ForeignKey('students.id')`,以及薪资精度 `DECIMAL(10,2)`。学生外键唯一约束确保每个学生最多对应一条就业信息。 + +## 十、公共建库建表工具 + +文件:`utils/create database.py`;负责人:张昕浩。文件名中包含一个空格。 + +脚本执行流程: + +1. 将项目根目录加入当前进程的导入路径,使直接运行此文件时也能导入 `core` 和 `models`。 +2. 导入全部 7 个模型,将模型表注册到公共 `Base.metadata`。 +3. 检查整数外键与被引用主键的类型是否一致;不一致时列出具体字段并以失败状态退出。 +4. 从现有 `core.database.engine` 读取连接设置,先连接不指定数据库的 MySQL 服务。 +5. 创建缺失的数据库,字符集为 `utf8mb4`、排序规则为 `utf8mb4_unicode_ci`;当前配置库名为 `student_manager`。 +6. 调用 `Base.metadata.create_all(bind=engine)` 创建缺失的表。 +7. 查询实际表名,确认全部模型对应的表均存在。 +8. 释放数据库连接池;发生数据库或检查错误时输出原因,并返回非零退出码。 + +在项目根目录运行: + +```powershell +python "utils/create database.py" +``` + +本机已验证使用的解释器命令: + +```powershell +& 'D:\conda\envs\ai0720\python.exe' -B -X utf8 '.\utils\create database.py' +``` + +脚本只创建缺失的库和表,不清空数据、不删除表,也不自动修改已有表结构。 + +## 十一、验证结果及范围 + +完成三处外键类型修正后,脚本退出码为 `0`,输出如下: + +```text +数据库已就绪:student_manager +已导入的模型表: classes, teachers, consultants, students, class_teachers, scores, employments +建表检查通过:全部模型对应的表均已存在。 +``` + +已经完成的检查包括: + +- 7 个模型可以导入,6 个外键目标均能解析。 +- 外键与被引用主键在模型中的类型检查通过。 +- 7 张表的 MySQL 建表 SQL 可以编译。 +- 字段名、非空设置、字符串长度与本次对照结果一致。 +- 班级老师关系、成绩和就业的必要唯一约束已在模型中定义。 +- 7 个模型可以读取现有数据库中的对应表。 +- 文件差异格式检查通过。 + +本次执行针对已有的 `student_manager` 数据库。`create_all()` 会跳过已有表,因此此结果不等同于已经在全新空库中完成端到端创建测试,也不表示现有数据库已经迁移成当前模型定义。 + +现有库中的相关主外键仍为原来的 `BIGINT`,成绩列仍为原来的 `DECIMAL(5,2)`;本次没有执行 `ALTER TABLE`,模型里经确认保留的 `Integer` 和 `DECIMAL(4,1)` 不会自动改变这些实库类型。 + +## 十二、后续接口联调事项 + +以下配套代码不在本次修改范围内,需要模块负责人继续核对: + +| 位置 | 需要核对的内容 | +|---|---| +| `schemas/class_schema.py` 的 `ClassCreate` | 当前缺少 `start_date`,而模型规定开课日期必填 | +| `schemas/class_schema.py` 的 `ClassUpdate` | 当前仍使用 `update_at`,模型字段已统一为 `updated_at` | +| 学生新增及业务编号生成逻辑 | `student_no` 已设为非空;先以空编号执行 `flush()` 再生成编号会违反非空约束,需要与编号生成方案协调 | + +本次验证覆盖模型和建表工具,尚未完成上述业务接口的联调验证。 diff --git a/models/class_model.py b/models/class_model.py index 8c7935d..0610b8d 100644 --- a/models/class_model.py +++ b/models/class_model.py @@ -1,6 +1,7 @@ from sqlalchemy import * # 导入sqlalchemy模块下所有的类,以便下面创建班级表模型和声明表的字段 from datetime import datetime # 导入datetime模块下的datetime类 from core.database import Base # 从core软件包下的database模块导入Base +from sqlalchemy.dialects.mysql import TINYINT class Classes( Base ): # 在python里的名字 __tablename__ = 'classes' # 在数据库中表的名字 @@ -21,7 +22,7 @@ class Classes( Base ): # 在python里的名字 ) # ————创建班级"开课时间"的字段名———— start_date = Column( DATE # 声明是字段数据类型是日期 - , nullable = True # 声明是'可为空' + , nullable = False # 开课时间必填 ) # ————创建班级"结课时间"的字段名———— end_date = Column( DATE # 声明是字段数据类型是'日期' @@ -30,20 +31,23 @@ class Classes( Base ): # 在python里的名字 # ————创建班级"创建时间"的字段名———— created_at = Column( DATETIME # 声明是字段数据类型是"日期时间" , default=datetime.now # 声明是创建的默认值就是"当前时间" + , server_default=text("CURRENT_TIMESTAMP") , nullable = False # 声明是"非空" ) # ————创建班级"更新时间"的字段名———— - update_at = Column( DATETIME # 声明是字段数据类型是"日期时间" + updated_at = Column( DATETIME # 声明是字段数据类型是"日期时间" , default=datetime.now #声明是第一次更新的时间就是第一次创建的时间一致 , onupdate=datetime.now # 声明"最后修改的时间" + , server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") , nullable = False # 声明是"非空" ) # ————创建班级"备注信息"的字段名———— - remark = Column( String(100) # 声明是字段数据类型是"字符串" + remark = Column( String(255) # 声明是字段数据类型是"字符串" , nullable = True # 声明是"可以为空" ) # ————创建班级"逻辑删除"的字段名———— - is_deleted = Column( INTEGER # 声明是字段数据类型是"数字" + is_deleted = Column( TINYINT # 逻辑删除标记 , default = 0 # 声明默认值是"0,未删除" + , server_default = text("0") , nullable = False # 声明是"非空" ) diff --git a/models/class_teacher.py b/models/class_teacher.py index 537ef65..701b9ce 100644 --- a/models/class_teacher.py +++ b/models/class_teacher.py @@ -1,32 +1,35 @@ from sqlalchemy import (Column, Integer, String, Text, - BigInteger,ForeignKey,VARCHAR,DateTime + BigInteger,ForeignKey,VARCHAR,DateTime,UniqueConstraint,text ) -from sqlalchemy.dialects.mssql import TINYINT +from sqlalchemy.dialects.mysql import TINYINT from core.database import Base from datetime import datetime -#班级老师关系表 class_teachers_info_detail 中间表 ORM 模型完整定义 +#班级老师关系表 class_teachers 中间表 ORM 模型完整定义 #字段包括id,class_id,teacher_id,role,created_at,updated_at,is_deleted class ClassTeachers(Base): - __tablename__ = "class_teachers_info_detail" + __tablename__ = "class_teachers" + __table_args__ = ( + UniqueConstraint("class_id", "teacher_id", "role", name="uk_class_teacher_role"), + ) id = Column( BigInteger ,primary_key=True ,autoincrement=True ,comment='关系') class_id = Column( - BigInteger - ,ForeignKey('class_id') + Integer + ,ForeignKey('classes.id') ,nullable=False - ,comment='关联 class_id' + ,comment='关联 classes.id' ) teacher_id = Column( - BigInteger - ,ForeignKey('teacher_id') + Integer + ,ForeignKey('teachers.id') ,nullable=False - ,comment='关联 teacher_id' + ,comment='关联 teachers.id' ) role = Column( VARCHAR(30) @@ -36,18 +39,22 @@ class ClassTeachers(Base): created_at = Column( DateTime ,nullable=False - ,default=datetime.now() + ,default=datetime.now + ,server_default=text("CURRENT_TIMESTAMP") ,comment='创建时间' ) updated_at = Column( DateTime ,nullable=False - ,default=datetime.now() + ,default=datetime.now + ,onupdate=datetime.now + ,server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") ,comment='更新时间' ) is_deleted = Column( TINYINT ,nullable=False ,default=0 + ,server_default=text("0") ,comment='逻辑删除' - ) \ No newline at end of file + ) diff --git a/models/consultant.py b/models/consultant.py index d51ac3d..d261f52 100644 --- a/models/consultant.py +++ b/models/consultant.py @@ -1,5 +1,6 @@ from sqlalchemy import * +from sqlalchemy.dialects.mysql import TINYINT from datetime import datetime from core.database import Base class Consultant(Base): @@ -8,10 +9,10 @@ class Consultant(Base): id = Column(Integer, primary_key=True, autoincrement=True, comment="主键") consultant_no = Column(String(50), unique=True, nullable=False, comment="顾问编号") consultant_name = Column(String(50), nullable=False, comment="顾问姓名") - phone = Column(String(11), nullable=True, comment="联系电话") + phone = Column(String(20), nullable=True, comment="联系电话") email = Column(String(100), nullable=True, comment="邮箱") remark = Column(String(255), nullable=True, comment="备注") - created_at = Column(DateTime, default=datetime.now, nullable=False, comment="创建时间") - updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, nullable=False, comment="更新时间") - is_deleted = Column(Integer, default=0, nullable=False, comment="逻辑删除:0正常,1删除") + created_at = Column(DateTime, default=datetime.now, server_default=text("CURRENT_TIMESTAMP"), nullable=False, comment="创建时间") + updated_at = Column(DateTime, default=datetime.now, onupdate=datetime.now, server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP"), nullable=False, comment="更新时间") + is_deleted = Column(TINYINT, default=0, server_default=text("0"), nullable=False, comment="逻辑删除:0正常,1删除") diff --git a/models/employment.py b/models/employment.py index 69e2706..199d275 100644 --- a/models/employment.py +++ b/models/employment.py @@ -1,4 +1,5 @@ from sqlalchemy import * +from sqlalchemy.dialects.mysql import TINYINT from sqlalchemy.orm import declarative_base,sessionmaker from datetime import datetime from core.database import Base, engine @@ -10,11 +11,13 @@ class Employment(Base): id = Column(BigInteger, primary_key=True # 主键 , autoincrement=True # 声明是自增主键 ,comment="就业信息主键id") - student_id = Column(BigInteger, ForeignKey("students.id") # 外键 -> students.id + student_id = Column(Integer, ForeignKey("students.id") # 外键 -> students.id , nullable=False # 不允许为null + , unique=True # 每个学生只对应一条就业信息 ,comment="关联学生id") - employment_status = Column(String(50) + employment_status = Column(String(30) , default="not_started" + , nullable=False ,comment="就业状态:not_started/job_hunting/offered/employed") employment_open_date = Column(Date ,comment="就业开放时间") @@ -22,19 +25,25 @@ class Employment(Base): ,comment="offer下发时间") company_name = Column(String(100) , comment="就业公司名称") - salary = Column(DECIMAL(10, 2) # 高精度浮点数薪资,最多 10 位数字,小数占 2 位 + salary = Column(DECIMAL(10, 2) # 定点小数薪资,最多10位数字,小数占2位 , comment="就业薪资") remark = Column(String(255) ,default=None , comment="备注信息") created_at = Column(DateTime , default=datetime.now + , server_default=text("CURRENT_TIMESTAMP") + , nullable=False , comment="创建时间") updated_at = Column(DateTime , default=datetime.now , onupdate=datetime.now + , server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + , nullable=False ,comment="更新时间") - is_deleted = Column(SmallInteger + is_deleted = Column(TINYINT ,default=0 + ,server_default=text("0") + ,nullable=False ,comment="逻辑删除:0正常,1删除") diff --git a/models/score.py b/models/score.py index c7fe39c..c7ebe9d 100644 --- a/models/score.py +++ b/models/score.py @@ -1,20 +1,22 @@ from sqlalchemy import * +from sqlalchemy.dialects.mysql import TINYINT from core.database import Base from datetime import datetime,date class Score(Base): - __tablename__ = "score_info_detail" + __tablename__ = "scores" id = Column(Integer , primary_key=True ,autoincrement=True ,comment="成绩表序号,自增主键" ) student_id = Column(Integer - ,ForeignKey("student.id") + ,ForeignKey("students.id") ,nullable=False ,comment="学生学号" ) score = Column(DECIMAL(4,1) ,CheckConstraint("score between 0 and 100") + ,nullable=False ) exam_seq= Column(Integer , nullable=False @@ -22,25 +24,31 @@ class Score(Base): __table_args__ = ( UniqueConstraint("student_id" , "exam_seq" - , name="uk_student_exam_seq"), - )#学生id和学生姓名应为联合唯一,即一个学生一次考试只能有一个分数 + , name="uk_scores_student_exam"), + )#学生id和考核序次联合唯一,即一个学生一次考试只能有一个分数 exam_date=Column(Date ,default=date.today ,comment="考试日期" ) created_at=Column(DateTime ,default=datetime.now + ,server_default=text("CURRENT_TIMESTAMP") + ,nullable=False ,comment="创建时间" ) updated_at=Column(DateTime ,default=datetime.now ,onupdate=datetime.now + ,server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + ,nullable=False ,comment="更新时间" ) - remark=Column(String(225) + remark=Column(String(255) ,comment="备注说明" ) - is_deleted=Column(Integer + is_deleted=Column(TINYINT ,default=0 + ,server_default=text("0") + ,nullable=False ,comment="逻辑删除标记:未删除0,已删除1" - ) \ No newline at end of file + ) diff --git a/models/student.py b/models/student.py index 990a693..97a70a9 100644 --- a/models/student.py +++ b/models/student.py @@ -1,27 +1,30 @@ from core.database import Base from sqlalchemy import * +from sqlalchemy.dialects.mysql import TINYINT from datetime import datetime class Student(Base): - __tablename__ = 'student_info_detail' + __tablename__ = 'students' id = Column(Integer , primary_key=True , autoincrement=True , comment = '学生编号,自增主键' ) student_no = Column(VARCHAR(50) - , unique = True) - student_name = Column(VARCHAR(50)) + , unique = True + , nullable = False) + student_name = Column(VARCHAR(50), nullable=False) class_id = Column(Integer - ,foreign_key = 'class_info_detail.id' + ,ForeignKey('classes.id') + ,nullable = False )#外键约束 班级表的主键id,存在这个班级,学生表才可以输入 consultant_id = Column(Integer + ,ForeignKey('consultants.id') ,nullable = True - ,foreign_key = 'consultant_info_detail.id' )#外键约束 顾问表的主键id,存在这个顾问,才可以写入,允许为空 native_place = Column(VARCHAR(100) ,nullable = True) - graduation_school = Column(VARCHAR(100) + graduate_school = Column(VARCHAR(100) ,nullable = True) major = Column(VARCHAR(100) ,nullable = True) @@ -35,11 +38,17 @@ class Student(Base): ,nullable = False) gender = Column(VARCHAR(10) ,nullable = False) - create_at = Column(DATETIME - ,default = datetime.now) - update_at = Column(DATETIME + created_at = Column(DATETIME ,default = datetime.now - ,onupdate = datetime.now) - is_deleted = Column(Integer - ,default = 0)#逻辑删除,默认为0,为1则表示已删除 + ,server_default = text("CURRENT_TIMESTAMP") + ,nullable = False) + updated_at = Column(DATETIME + ,default = datetime.now + ,onupdate = datetime.now + ,server_default = text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + ,nullable = False) + is_deleted = Column(TINYINT + ,default = 0 + ,server_default = text("0") + ,nullable = False)#逻辑删除,默认为0,为1则表示已删除 diff --git a/models/teacher.py b/models/teacher.py index 47998d0..180d342 100644 --- a/models/teacher.py +++ b/models/teacher.py @@ -1,6 +1,6 @@ -from sqlalchemy import (Column, Integer,VARCHAR,DateTime +from sqlalchemy import (Column, Integer,VARCHAR,DateTime,text ) -from sqlalchemy.dialects.mssql import TINYINT +from sqlalchemy.dialects.mysql import TINYINT from core.database import Base from datetime import datetime @@ -9,7 +9,7 @@ from datetime import datetime # teachers 老师表 ORM 模型 #字段包括id,teacher_no,teacher_name,phone,email,remark,created_at,updated_at,is_deleted class Teacher(Base): - __tablename__ = 'teachers_info_detail' + __tablename__ = 'teachers' id = Column( Integer , primary_key=True @@ -37,26 +37,30 @@ class Teacher(Base): ,comment='邮箱' ) remark = Column( - VARCHAR(2550) + VARCHAR(255) ,nullable=True ,comment='备注' ) created_at = Column( - DateTime + DateTime ,nullable=False ,default=datetime.now + ,server_default=text("CURRENT_TIMESTAMP") ,comment='创建时间' ) updated_at = Column( DateTime ,nullable=False ,default=datetime.now + ,onupdate=datetime.now + ,server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") ,comment='更新时间' ) is_deleted = Column( TINYINT ,nullable=False ,default=0 + ,server_default=text("0") ,comment='逻辑删除' ) From 1c220c2a3e1132a5cccb9e6b9874e5d470927d0d Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Mon, 21 Sep 2026 19:59:58 +0800 Subject: [PATCH 08/23] =?UTF-8?q?=E7=A7=BB=E9=99=A4=E4=B8=8Emain=E5=86=B2?= =?UTF-8?q?=E7=AA=81=E7=9A=84model=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- models/class_teacher.py | 29 +++++++++++------------------ models/employment.py | 21 ++++++--------------- models/score.py | 22 +++++++--------------- models/student.py | 33 ++++++++++++--------------------- models/teacher.py | 15 +++++---------- 5 files changed, 41 insertions(+), 79 deletions(-) diff --git a/models/class_teacher.py b/models/class_teacher.py index 701b9ce..37d1e35 100644 --- a/models/class_teacher.py +++ b/models/class_teacher.py @@ -1,35 +1,32 @@ from sqlalchemy import (Column, Integer, String, Text, - BigInteger,ForeignKey,VARCHAR,DateTime,UniqueConstraint,text + BigInteger,ForeignKey,VARCHAR,DateTime ) -from sqlalchemy.dialects.mysql import TINYINT +from sqlalchemy.dialects.mssql import TINYINT from core.database import Base from datetime import datetime -#班级老师关系表 class_teachers 中间表 ORM 模型完整定义 +#班级老师关系表 class_teachers_info_detail 中间表 ORM 模型完整定义 #字段包括id,class_id,teacher_id,role,created_at,updated_at,is_deleted class ClassTeachers(Base): - __tablename__ = "class_teachers" - __table_args__ = ( - UniqueConstraint("class_id", "teacher_id", "role", name="uk_class_teacher_role"), - ) + __tablename__ = "class_teachers_info_detail" id = Column( - BigInteger + Integer ,primary_key=True ,autoincrement=True ,comment='关系') class_id = Column( Integer - ,ForeignKey('classes.id') + ,ForeignKey('class_info_detail.id') ,nullable=False - ,comment='关联 classes.id' + ,comment='关联 class_id' ) teacher_id = Column( Integer - ,ForeignKey('teachers.id') + ,ForeignKey("teacher_info_detail.id") ,nullable=False - ,comment='关联 teachers.id' + ,comment='关联 teacher_id' ) role = Column( VARCHAR(30) @@ -39,22 +36,18 @@ class ClassTeachers(Base): created_at = Column( DateTime ,nullable=False - ,default=datetime.now - ,server_default=text("CURRENT_TIMESTAMP") + ,default=datetime.now() ,comment='创建时间' ) updated_at = Column( DateTime ,nullable=False - ,default=datetime.now - ,onupdate=datetime.now - ,server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") + ,default=datetime.now() ,comment='更新时间' ) is_deleted = Column( TINYINT ,nullable=False ,default=0 - ,server_default=text("0") ,comment='逻辑删除' ) diff --git a/models/employment.py b/models/employment.py index 199d275..76a1f7c 100644 --- a/models/employment.py +++ b/models/employment.py @@ -1,5 +1,4 @@ from sqlalchemy import * -from sqlalchemy.dialects.mysql import TINYINT from sqlalchemy.orm import declarative_base,sessionmaker from datetime import datetime from core.database import Base, engine @@ -7,17 +6,15 @@ from core.database import Base, engine # 为了符合三范式,就业表只保存 student_id,不保存学生姓名和班级名称。查询返回时通过关联 students 和 classes 带出学生姓名、班级名称。 class Employment(Base): - __tablename__ = "employments" # 实际数据库的表名字 - id = Column(BigInteger, primary_key=True # 主键 + __tablename__ = "employment_info_detail" # 实际数据库的表名字 + id = Column(Integer, primary_key=True # 主键 , autoincrement=True # 声明是自增主键 ,comment="就业信息主键id") - student_id = Column(Integer, ForeignKey("students.id") # 外键 -> students.id + student_id = Column(Integer, ForeignKey("student_info_detail.id") # 外键 -> students.id , nullable=False # 不允许为null - , unique=True # 每个学生只对应一条就业信息 ,comment="关联学生id") - employment_status = Column(String(30) + employment_status = Column(String(50) , default="not_started" - , nullable=False ,comment="就业状态:not_started/job_hunting/offered/employed") employment_open_date = Column(Date ,comment="就业开放时间") @@ -25,25 +22,19 @@ class Employment(Base): ,comment="offer下发时间") company_name = Column(String(100) , comment="就业公司名称") - salary = Column(DECIMAL(10, 2) # 定点小数薪资,最多10位数字,小数占2位 + salary = Column(DECIMAL(10, 2) # 高精度浮点数薪资,最多 10 位数字,小数占 2 位 , comment="就业薪资") remark = Column(String(255) ,default=None , comment="备注信息") created_at = Column(DateTime , default=datetime.now - , server_default=text("CURRENT_TIMESTAMP") - , nullable=False , comment="创建时间") updated_at = Column(DateTime , default=datetime.now , onupdate=datetime.now - , server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") - , nullable=False ,comment="更新时间") - is_deleted = Column(TINYINT + is_deleted = Column(SmallInteger ,default=0 - ,server_default=text("0") - ,nullable=False ,comment="逻辑删除:0正常,1删除") diff --git a/models/score.py b/models/score.py index c7ebe9d..90e9682 100644 --- a/models/score.py +++ b/models/score.py @@ -1,22 +1,20 @@ from sqlalchemy import * -from sqlalchemy.dialects.mysql import TINYINT from core.database import Base from datetime import datetime,date class Score(Base): - __tablename__ = "scores" + __tablename__ = "score_info_detail" id = Column(Integer , primary_key=True ,autoincrement=True ,comment="成绩表序号,自增主键" ) student_id = Column(Integer - ,ForeignKey("students.id") + ,ForeignKey("student_info_detail.id") ,nullable=False ,comment="学生学号" ) score = Column(DECIMAL(4,1) ,CheckConstraint("score between 0 and 100") - ,nullable=False ) exam_seq= Column(Integer , nullable=False @@ -24,31 +22,25 @@ class Score(Base): __table_args__ = ( UniqueConstraint("student_id" , "exam_seq" - , name="uk_scores_student_exam"), - )#学生id和考核序次联合唯一,即一个学生一次考试只能有一个分数 + , name="uk_student_exam_seq"), + )#学生id和学生姓名应为联合唯一,即一个学生一次考试只能有一个分数 exam_date=Column(Date ,default=date.today ,comment="考试日期" ) created_at=Column(DateTime ,default=datetime.now - ,server_default=text("CURRENT_TIMESTAMP") - ,nullable=False ,comment="创建时间" ) updated_at=Column(DateTime ,default=datetime.now ,onupdate=datetime.now - ,server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") - ,nullable=False ,comment="更新时间" ) - remark=Column(String(255) + remark=Column(String(225) ,comment="备注说明" ) - is_deleted=Column(TINYINT + is_deleted=Column(Integer ,default=0 - ,server_default=text("0") - ,nullable=False ,comment="逻辑删除标记:未删除0,已删除1" - ) + ) \ No newline at end of file diff --git a/models/student.py b/models/student.py index 97a70a9..ab805e4 100644 --- a/models/student.py +++ b/models/student.py @@ -1,30 +1,27 @@ from core.database import Base from sqlalchemy import * -from sqlalchemy.dialects.mysql import TINYINT from datetime import datetime class Student(Base): - __tablename__ = 'students' + __tablename__ = 'student_info_detail' id = Column(Integer , primary_key=True , autoincrement=True , comment = '学生编号,自增主键' ) student_no = Column(VARCHAR(50) - , unique = True - , nullable = False) - student_name = Column(VARCHAR(50), nullable=False) + , unique = True) + student_name = Column(VARCHAR(50)) class_id = Column(Integer - ,ForeignKey('classes.id') - ,nullable = False + ,ForeignKey('class_info_detail.id') )#外键约束 班级表的主键id,存在这个班级,学生表才可以输入 consultant_id = Column(Integer - ,ForeignKey('consultants.id') + , ForeignKey('consultant_info_detail.id') ,nullable = True )#外键约束 顾问表的主键id,存在这个顾问,才可以写入,允许为空 native_place = Column(VARCHAR(100) ,nullable = True) - graduate_school = Column(VARCHAR(100) + graduation_school = Column(VARCHAR(100) ,nullable = True) major = Column(VARCHAR(100) ,nullable = True) @@ -38,17 +35,11 @@ class Student(Base): ,nullable = False) gender = Column(VARCHAR(10) ,nullable = False) - created_at = Column(DATETIME + create_at = Column(DATETIME + ,default = datetime.now) + update_at = Column(DATETIME ,default = datetime.now - ,server_default = text("CURRENT_TIMESTAMP") - ,nullable = False) - updated_at = Column(DATETIME - ,default = datetime.now - ,onupdate = datetime.now - ,server_default = text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") - ,nullable = False) - is_deleted = Column(TINYINT - ,default = 0 - ,server_default = text("0") - ,nullable = False)#逻辑删除,默认为0,为1则表示已删除 + ,onupdate = datetime.now) + is_deleted = Column(Integer + ,default = 0)#逻辑删除,默认为0,为1则表示已删除 diff --git a/models/teacher.py b/models/teacher.py index 180d342..9c8c6bf 100644 --- a/models/teacher.py +++ b/models/teacher.py @@ -1,15 +1,14 @@ -from sqlalchemy import (Column, Integer,VARCHAR,DateTime,text +from sqlalchemy import (Column, Integer,VARCHAR,DateTime ) -from sqlalchemy.dialects.mysql import TINYINT +from sqlalchemy.dialects.mssql import TINYINT from core.database import Base from datetime import datetime - # teachers 老师表 ORM 模型 #字段包括id,teacher_no,teacher_name,phone,email,remark,created_at,updated_at,is_deleted class Teacher(Base): - __tablename__ = 'teachers' + __tablename__ = 'teacher_info_detail' id = Column( Integer , primary_key=True @@ -37,30 +36,26 @@ class Teacher(Base): ,comment='邮箱' ) remark = Column( - VARCHAR(255) + VARCHAR(2550) ,nullable=True ,comment='备注' ) created_at = Column( - DateTime + DateTime ,nullable=False ,default=datetime.now - ,server_default=text("CURRENT_TIMESTAMP") ,comment='创建时间' ) updated_at = Column( DateTime ,nullable=False ,default=datetime.now - ,onupdate=datetime.now - ,server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") ,comment='更新时间' ) is_deleted = Column( TINYINT ,nullable=False ,default=0 - ,server_default=text("0") ,comment='逻辑删除' ) From a11da93412bff4328f3c9f3344efdecda2dc5600 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 20:42:53 +0800 Subject: [PATCH 09/23] =?UTF-8?q?=E5=AE=8C=E6=88=90=E4=BA=86=E6=B7=BB?= =?UTF-8?q?=E5=8A=A0=E5=AD=A6=E7=94=9F=E6=8E=A5=E5=8F=A3=E7=9A=84=E8=87=AA?= =?UTF-8?q?=E5=8A=A8=E7=94=9F=E6=88=90studentno=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/statistics.py | 56 +++++++++++++++++++++++------------------------ api/student.py | 1 + dao/student.py | 16 ++++---------- 3 files changed, 33 insertions(+), 40 deletions(-) diff --git a/api/statistics.py b/api/statistics.py index 082f7f4..37a3393 100644 --- a/api/statistics.py +++ b/api/statistics.py @@ -1,28 +1,28 @@ -from fastapi import APIRouter,HTTPException,Depends -from DoubaoTeam.core.database import get_db -from DoubaoTeam.models.student import Student -from DoubaoTeam.models.score import Score -from DoubaoTeam.dao.statistics import * -from DoubaoTeam.schemas.common import SuccessResponse -from sqlalchemy.orm import Session - - -statistics_router=APIRouter() - - -@statistics_router.get("/scores/age-over-30",response_model=SuccessResponse) -def api_age_over_thirty(db:Session=Depends(get_db)): - try: - age_over_thirty_list=age_over_thirty(Student,db) - return SuccessResponse(status_code=200,msg=age_over_thirty_list) - except NoExist as e: - raise HTTPException(status_code=404,detail=str(e)) -# 这边功能函数返回的是一个字典 -@statistics_router.get("/classes/gender-count",response_model=SuccessResponse) -def api_gender_count(db:Session=Depends(get_db)): - try: - result = gender_count(Student,db) - return SuccessResponse(status_code=200,msg=result) - except NoExist as e: - raise HTTPException(status_code=404,detail=str(e)) - +# from fastapi import APIRouter,HTTPException,Depends +# from DoubaoTeam.core.database import get_db +# from DoubaoTeam.models.student import Student +# from DoubaoTeam.models.score import Score +# from DoubaoTeam.dao.statistics import * +# from DoubaoTeam.schemas.common import SuccessResponse +# from sqlalchemy.orm import Session +# +# +# statistics_router=APIRouter() +# +# +# @statistics_router.get("/scores/age-over-30",response_model=SuccessResponse) +# def api_age_over_thirty(db:Session=Depends(get_db)): +# try: +# age_over_thirty_list=age_over_thirty(Student,db) +# return SuccessResponse(status_code=200,msg=age_over_thirty_list) +# except NoExist as e: +# raise HTTPException(status_code=404,detail=str(e)) +# # 这边功能函数返回的是一个字典 +# @statistics_router.get("/classes/gender-count",response_model=SuccessResponse) +# def api_gender_count(db:Session=Depends(get_db)): +# try: +# result = gender_count(Student,db) +# return SuccessResponse(status_code=200,msg=result) +# except NoExist as e: +# raise HTTPException(status_code=404,detail=str(e)) +# diff --git a/api/student.py b/api/student.py index 50311ab..c606935 100644 --- a/api/student.py +++ b/api/student.py @@ -14,3 +14,4 @@ def create_student(request:StudentCreate,db:Session = Depends(get_db)): + diff --git a/dao/student.py b/dao/student.py index 9fc678b..021ad90 100644 --- a/dao/student.py +++ b/dao/student.py @@ -8,25 +8,17 @@ def create_students(d: dict, db: Session): stu = Student(**d) try: - # 1. 添加到 Session db.add(stu) - - # 2. flush,让 MySQL 先生成自增 id + # 把 SQL 发给数据库执行 db.flush() - - # 3. 根据 id 生成学生编号 + # 根据 id 生成学生编号 if not stu.student_no: stu.student_no = f"STU{stu.id:04d}" - - # 4. 提交事务 db.commit() - - # 5. 刷新对象,拿到数据库最终数据 db.refresh(stu) - return stu - except Exception as e: db.rollback() print(f"插入学生失败: {e}") - return None \ No newline at end of file + return None + From 77aa63b17b6c9bf2c2fdc8744bbed67b4d35c1d6 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 20:45:11 +0800 Subject: [PATCH 10/23] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E5=AD=A6=E7=94=9F?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=E6=8E=A5=E5=8F=A3=E8=AF=B4=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 学生板块接口实现说明.md | 524 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 524 insertions(+) create mode 100644 学生板块接口实现说明.md diff --git a/学生板块接口实现说明.md b/学生板块接口实现说明.md new file mode 100644 index 0000000..da038ec --- /dev/null +++ b/学生板块接口实现说明.md @@ -0,0 +1,524 @@ +# 学生板块:5 个必做接口完整实现 + +本文按当前项目的 FastAPI、Pydantic 2、SQLAlchemy 同步 Session 编写。代码按文件整理,可分别替换对应文件内容;本文仅提供实现文档,没有改动现有 Python 文件。 + +## 一、接口与约定 + +| 方法 | 路径 | 功能 | +| --- | --- | --- | +| GET | `/students` | 分页查询,按学号、姓名、班级筛选 | +| POST | `/students` | 新增学生,未提供学号时自动生成 | +| GET | `/students/{id}` | 查询未删除学生详情 | +| PUT | `/students/{id}` | 修改学生,只更新本次提交的字段 | +| DELETE | `/students/{id}` | 将 `is_deleted` 改为 1 | + +- `id` 是数据库自增主键,`student_no` 是业务学号,二者不同。 +- 学号精确匹配,姓名按包含关系匹配,班级按 `class_id` 精确匹配;多个条件同时生效。 +- 列表默认第 1 页、每页 20 条,每页最多 100 条;无结果返回空数组。 +- PUT 在这里约定为部分更新:未传字段保持原值;可选字段传 `null` 表示清空。 +- 已删除学生不出现在列表、详情中,也不能再修改;重复删除返回 404。 +- 删除学生不自动删除成绩、就业记录,其他模块查询时需要自行处理学生的删除状态。 +- 性别暂不规定枚举,仅要求非空且不超过数据库长度,避免擅自改变团队约定。 +- 年龄暂设为 0~150;毕业日期不能早于入学日期。 + +## 二、请求和响应模型:`schemas/student.py` + +作用:在进入接口业务代码前检查字段类型、长度和取值;定义返回给前端的数据结构。 + +```python +from datetime import date, datetime +from pydantic import BaseModel, ConfigDict, Field, model_validator + + +class StudentCreate(BaseModel): + # 1. 自动去除字符串首尾空格;禁止多传 id、is_deleted 等未声明字段。 + model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") + + # 2. 学号可省略或传 null,此时由后端生成;空字符串不合法。 + student_no: str | None = Field(default=None, min_length=1, max_length=50) + student_name: str = Field(min_length=1, max_length=50) + class_id: int = Field(gt=0) + consultant_id: int | None = Field(default=None, gt=0) + native_place: str | None = Field(default=None, max_length=100) + graduation_school: str | None = Field(default=None, max_length=100) + major: str | None = Field(default=None, max_length=100) + enrollment_date: date + graduation_date: date | None = None + education: str | None = Field(default=None, max_length=50) + age: int = Field(ge=0, le=150) + gender: str = Field(min_length=1, max_length=10) + + @model_validator(mode="after") + def check_dates(self): + # 3. 新增时两个日期都已确定,可以直接校验先后顺序。 + if self.graduation_date and self.graduation_date < self.enrollment_date: + raise ValueError("毕业日期不能早于入学日期") + return self + + +class StudentUpdate(BaseModel): + model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") + + # 1. 默认值为 None 是为了允许省略字段,不代表每个字段都能清空。 + student_no: str | None = Field(default=None, min_length=1, max_length=50) + student_name: str | None = Field(default=None, min_length=1, max_length=50) + class_id: int | None = Field(default=None, gt=0) + consultant_id: int | None = Field(default=None, gt=0) + native_place: str | None = Field(default=None, max_length=100) + graduation_school: str | None = Field(default=None, max_length=100) + major: str | None = Field(default=None, max_length=100) + enrollment_date: date | None = None + graduation_date: date | None = None + education: str | None = Field(default=None, max_length=50) + age: int | None = Field(default=None, ge=0, le=150) + gender: str | None = Field(default=None, min_length=1, max_length=10) + + @model_validator(mode="after") + def check_explicit_null(self): + # 2. model_fields_set 记录客户端实际提交过的字段。 + # 没传姓名可以;明确传 student_name: null 不可以。 + required = { + "student_no", "student_name", "class_id", + "enrollment_date", "age", "gender", + } + for name in required & self.model_fields_set: + if getattr(self, name) is None: + raise ValueError(f"{name} 不能为 null") + return self + + +class StudentOut(BaseModel): + # 允许 Pydantic 从 SQLAlchemy 对象的属性读取数据。 + model_config = ConfigDict(from_attributes=True) + + id: int + student_no: str | None + student_name: str | None + class_id: int | None + consultant_id: int | None + native_place: str | None + graduation_school: str | None + major: str | None + enrollment_date: date + graduation_date: date | None + education: str | None + age: int + gender: str + # 当前 Student 模型的实际字段名是 create_at 和 update_at。 + create_at: datetime | None + update_at: datetime | None + + +class StudentResponse(BaseModel): + code: int = 200 + detail: str = "操作成功" + data: StudentOut + + +class StudentListResponse(BaseModel): + code: int = 200 + detail: str = "查询成功" + total: int + page: int + page_size: int + data: list[StudentOut] + + +class StudentDeleteResponse(BaseModel): + code: int = 200 + detail: str = "删除学生成功" + id: int +``` + +`StudentOut` 的姓名、班级允许为空,是为了兼容当前数据库模型允许为空的历史记录;新增请求仍要求这些字段必填。 + +## 三、数据库操作:`dao/student.py` + +为方便在现有项目直接使用,下列 DAO 使用 `HTTPException` 返回明确业务错误。项目扩大后,可改成自定义业务异常,再由 API 统一转换。 + +```python +import logging +from contextlib import contextmanager + +from fastapi import HTTPException +from sqlalchemy.exc import IntegrityError, SQLAlchemyError +from sqlalchemy.orm import Session + +from models.student import Student +from models.class_model import Classes +from models.consultant import Consultant + +logger = logging.getLogger(__name__) + + +@contextmanager +def database_errors(db: Session): + # 统一处理数据库异常,避免每个接口重复写 try/except。 + try: + yield + except HTTPException: + # 业务检查失败:撤销本次事务,保留原来的 404/409/422 等状态。 + db.rollback() + raise + except IntegrityError: + # 外键、唯一约束等冲突,不能当作成功返回。 + db.rollback() + logger.exception("学生数据约束冲突") + raise HTTPException(409, "数据冲突,请检查学号、班级和顾问信息") from None + except SQLAlchemyError: + db.rollback() + logger.exception("学生数据库操作失败") + raise HTTPException(500, "数据库操作失败,请稍后重试") from None + + +def require_student(student_id: int, db: Session): + # 所有详情、更新和删除操作都复用这一步,排除逻辑删除记录。 + student = db.query(Student).filter( + Student.id == student_id, + Student.is_deleted == 0, + ).first() + if student is None: + raise HTTPException(404, "学生不存在或已删除") + return student + + +def check_references(data: dict, db: Session): + # 1. 仅检查本次涉及的关联字段;更新其他字段时不用重复检查。 + if "class_id" in data: + classroom = db.query(Classes).filter( + Classes.id == data["class_id"], + Classes.is_deleted == 0, + ).first() + if classroom is None: + raise HTTPException(422, "班级不存在或已删除") + + # 2. 顾问允许为 null,此时代表不关联顾问,无需查询。 + if data.get("consultant_id") is not None: + consultant = db.query(Consultant).filter( + Consultant.id == data["consultant_id"], + Consultant.is_deleted == 0, + ).first() + if consultant is None: + raise HTTPException(422, "顾问不存在或已删除") + + +def check_student_no(student_no: str, db: Session, exclude_id=None): + # 学号占用检查包含已删除记录,避免旧学号被其他学生重复使用。 + query = db.query(Student).filter(Student.student_no == student_no) + if exclude_id is not None: + # 修改时必须排除自己,否则原学号也会被判定为重复。 + query = query.filter(Student.id != exclude_id) + if query.first() is not None: + raise HTTPException(409, "学号已存在") + + +def get_student_list(db, page, page_size, student_no=None, + student_name=None, class_id=None): + with database_errors(db): + # 1. 先建立基础查询,只查询未删除学生。 + query = db.query(Student).filter(Student.is_deleted == 0) + # 2. 参数有值时才增加筛选条件,多个 filter 之间为 AND。 + if student_no is not None: + query = query.filter(Student.student_no == student_no) + if student_name is not None: + # 转义 % 和 _,让它们作为姓名的普通字符参与匹配。 + query = query.filter(Student.student_name.contains( + student_name, autoescape=True, + )) + if class_id is not None: + query = query.filter(Student.class_id == class_id) + # 3. 分页前统计符合条件的总数。 + total = query.count() + # 4. 固定按主键倒序,最新记录优先,再跳过前面页的数据。 + rows = query.order_by(Student.id.desc()).offset( + (page - 1) * page_size + ).limit(page_size).all() + return total, rows + + +def get_student_detail(student_id: int, db: Session): + with database_errors(db): + return require_student(student_id, db) + + +def create_students(data: dict, db: Session): + with database_errors(db): + # 1. 校验班级、顾问是否存在且未删除。 + check_references(data, db) + # 2. 用户指定学号时,先检查是否被占用。 + if data.get("student_no") is not None: + check_student_no(data["student_no"], db) + # 3. 创建 ORM 对象并加入当前事务。 + student = Student(**data) + db.add(student) + # 4. flush 执行 INSERT 并获取自增 id,但此时尚未提交事务。 + db.flush() + # 5. 未提供学号时生成 STU0001 格式;四位是最小宽度。 + if student.student_no is None: + generated_no = f"STU{student.id:04d}" + check_student_no(generated_no, db, exclude_id=student.id) + student.student_no = generated_no + # 6. 在提交前生成响应快照,避免提交后再查询失败而误报新增失败。 + db.flush() + db.refresh(student) + from schemas.student import StudentOut + result = StudentOut.model_validate(student) + # 7. 所有步骤成功后提交;失败由 database_errors 回滚。 + db.commit() + return result + + +def update_student(student_id: int, updates: dict, db: Session): + with database_errors(db): + # 1. 先确认学生存在,再检查更新内容。 + student = require_student(student_id, db) + if not updates: + raise HTTPException(422, "请至少提供一个需要修改的字段") + # 2. 若修改班级或顾问,校验新的关联对象。 + check_references(updates, db) + if "student_no" in updates: + check_student_no(updates["student_no"], db, exclude_id=student.id) + # 3. 合并新旧日期再检查,防止只修改一个日期时绕过校验。 + enrollment = updates.get("enrollment_date", student.enrollment_date) + graduation = updates.get("graduation_date", student.graduation_date) + if graduation is not None and graduation < enrollment: + raise HTTPException(422, "毕业日期不能早于入学日期") + # 4. 仅更新实际提交的字段;可选字段传 None 会清空数据库值。 + for name, value in updates.items(): + setattr(student, name, value) + # 5. 执行更新并读取模型自动维护的 update_at,构造响应后提交。 + db.flush() + db.refresh(student) + from schemas.student import StudentOut + result = StudentOut.model_validate(student) + db.commit() + return result + + +def delete_student(student_id: int, db: Session): + with database_errors(db): + # 1. 找到正常学生;不存在或已删除时返回 404。 + student = require_student(student_id, db) + # 2. 只改删除标记,不调用 db.delete,不执行物理删除。 + student.is_deleted = 1 + # 3. 提交后,所有带 is_deleted == 0 的查询都会排除此学生。 + db.commit() + return student_id +``` + +注意:自动生成的学号若已被手动指定的学号占用,会返回 409 并回滚。自增 ID 在回滚后可能留下空号,这是正常现象。 + +## 四、接口层:`api/student.py` + +```python +from fastapi import APIRouter, Depends, Path, Query +from sqlalchemy.orm import Session + +from core.database import get_db +from dao import student as student_dao +from schemas.student import ( + StudentCreate, StudentUpdate, StudentResponse, + StudentListResponse, StudentDeleteResponse, +) + +student_router = APIRouter() + + +@student_router.get("", response_model=StudentListResponse, summary="查询学生列表") +def list_students( + page: int = Query(default=1, ge=1, description="页码,从 1 开始"), + page_size: int = Query(default=20, ge=1, le=100, description="每页条数"), + student_no: str | None = Query(default=None, min_length=1, max_length=50), + student_name: str | None = Query(default=None, min_length=1, max_length=50), + class_id: int | None = Query(default=None, gt=0), + db: Session = Depends(get_db), +): + # 1. FastAPI 校验查询参数,并通过 get_db 注入本次请求的数据库会话。 + # 2. DAO 按条件查询,返回总数和本页数据。 + total, rows = student_dao.get_student_list( + db, page, page_size, student_no, student_name, class_id, + ) + # 3. 返回分页信息,便于前端计算总页数。 + return StudentListResponse( + total=total, page=page, page_size=page_size, data=rows, + ) + + +@student_router.post("", response_model=StudentResponse, summary="新增学生") +def create_student(request: StudentCreate, db: Session = Depends(get_db)): + # 1. 请求体通过 StudentCreate 校验后转为字典。 + # 2. DAO 完成关联检查、学号生成和事务提交。 + result = student_dao.create_students(request.model_dump(), db) + # 3. 返回完整数据,前端可以立即取得 id 和生成的 student_no。 + return StudentResponse(detail="添加学生成功", data=result) + + +@student_router.get("/{id}", response_model=StudentResponse, summary="查询学生详情") +def read_student( + id: int = Path(gt=0), + db: Session = Depends(get_db), +): + # 路径 id 必须是正整数;DAO 对不存在或已删除的记录返回 404。 + result = student_dao.get_student_detail(id, db) + return StudentResponse(detail="查询成功", data=result) + + +@student_router.put("/{id}", response_model=StudentResponse, summary="修改学生") +def update_student( + request: StudentUpdate, + id: int = Path(gt=0), + db: Session = Depends(get_db), +): + # 1. exclude_unset=True 只保留客户端实际传入的字段。 + # 不要换成 exclude_none=True,否则无法通过 null 清空顾问等字段。 + updates = request.model_dump(exclude_unset=True) + # 2. DAO 校验目标学生、关联字段、学号和合并后的日期,再提交修改。 + result = student_dao.update_student(id, updates, db) + return StudentResponse(detail="修改学生成功", data=result) + + +@student_router.delete("/{id}", response_model=StudentDeleteResponse, + summary="逻辑删除学生") +def delete_student( + id: int = Path(gt=0), + db: Session = Depends(get_db), +): + # DAO 只修改删除标记,成功提交后返回被删除学生的主键。 + deleted_id = student_dao.delete_student(id, db) + return StudentDeleteResponse(id=deleted_id) +``` + +## 五、路由和数据库衔接 + +当前 `main.py` 已经有以下代码,保留即可,不要重复注册: + +```python +from api.student import student_router +app.include_router(student_router, tags=["学生接口"], prefix="/students") +``` + +路由文件内部使用空路径或 `/{id}`,是因为 `/students` 前缀由 `main.py` 统一添加。 + +`core.database.get_db()` 负责创建会话并在请求结束后关闭。写入操作由 DAO 提交或回滚,读取操作无需提交。模型继续使用 `core.database.Base`,不要重新创建 Base。 + +### 学号唯一性:当前模型需要补充的约束 + +当前 `models/student.py` 的 `student_no` 没有唯一约束。DAO 的预检查可以处理一般重复输入,但两个并发请求可能同时通过检查;要可靠防重,需配合数据库唯一约束。 + +模型中的字段可改为: + +```python +student_no = Column(VARCHAR(50), nullable=True, unique=True) +``` + +保持允许 NULL,是因为自动编号流程先插入并获取 ID,再写入学号。 + +对于已经存在的表,修改模型或执行 `create_all()` 不会自动添加约束。先检查历史重复数据: + +```sql +SELECT student_no, COUNT(*) AS duplicate_count +FROM student_info_detail +WHERE student_no IS NOT NULL +GROUP BY student_no +HAVING COUNT(*) > 1; +``` + +确认无重复且该唯一约束尚不存在后,再在项目使用的数据库中执行一次: + +```sql +ALTER TABLE student_info_detail +ADD CONSTRAINT uk_student_no UNIQUE (student_no); +``` + +本文没有执行以上 SQL。该约束包含已逻辑删除的数据,与 DAO 的学号占用规则一致。 + +## 六、调用示例 + +按项目当前入口启动后,可在 `http://127.0.0.1:23333/docs` 调试。需要先配置 MySQL、创建表,并准备一个真实存在且未删除的班级。以下 `class_id: 1`、路径中的学生 ID 均为示例,应替换为实际值。 + +### 1. 新增学生 + +`POST /students` + +```json +{ + "student_name": "张三", + "class_id": 1, + "enrollment_date": "2026-09-01", + "age": 20, + "gender": "男", + "major": "软件技术" +} +``` + +成功 HTTP 状态为 200,响应包含 `code`、`detail` 和 `data`,其中 `data.id`、`data.student_no` 是后续操作使用的主键和学号。这里保留原接口成功返回 200 的约定。 + +### 2. 查询列表 + +```text +GET /students?page=1&page_size=20 +GET /students?student_no=STU0001 +GET /students?student_name=张&class_id=1 +``` + +响应包含 `total`、`page`、`page_size` 和 `data` 数组。超出实际页数时,`total` 仍为匹配总数,`data` 为空。 + +### 3. 查询详情 + +```text +GET /students/1 +``` + +### 4. 修改学生 + +`PUT /students/1` + +```json +{ + "student_name": "张三丰", + "age": 21, + "consultant_id": null +} +``` + +这次只更新姓名、年龄和顾问;顾问关联被清空,班级、日期等未传字段保持原值。 + +### 5. 逻辑删除 + +```text +DELETE /students/1 +``` + +```json +{ + "code": 200, + "detail": "删除学生成功", + "id": 1 +} +``` + +删除后数据库行仍存在,`is_deleted` 为 1;后续详情、更新、再次删除均返回 404。 + +## 七、验证清单与错误说明 + +| 场景 | 预期 | +| --- | --- | +| 不传学号新增 | 自动生成学号,响应带主键与学生信息 | +| 指定已占用学号新增或修改 | 409,事务回滚 | +| 班级不存在或已删除 | 422 | +| 顾问不存在或已删除 | 422 | +| 姓名为空白、年龄越界、日期格式错误 | 422 | +| 毕业日期早于入学日期 | 422,新增与部分更新都检查 | +| 查询参数 page=0 或 page_size=101 | 422 | +| 同时按学号、姓名和班级筛选 | 返回满足全部条件的未删除学生 | +| 查询无匹配记录 | 200,total=0,data=[] | +| 更新仅传 age | 只更新年龄,其他字段保留 | +| 更新 consultant_id=null | 清空顾问关联 | +| 更新 student_name=null 或空请求体 | 422 | +| 查询、修改或删除不存在的学生 | 404 | +| 删除已有学生后重新查询 | 列表不可见,详情为 404 | +| 数据库操作失败 | 500,写事务回滚,服务端记录日志 | + +主动抛出的业务错误通常返回 `{"detail": "错误说明"}`;FastAPI 参数校验错误的 `detail` 是包含字段位置的数组。这两种结构应由前端分别处理。 + +以上是供接入项目的实现代码和验证步骤;文档生成时未连接真实 MySQL,实际数据库约束、历史数据和接口联调仍需按清单验证。 From 85c33509d7875b7471b658a413bdf98db2610615 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 20:49:11 +0800 Subject: [PATCH 11/23] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 学生板块接口实现说明.md | 524 ---------------------------------------- 1 file changed, 524 deletions(-) delete mode 100644 学生板块接口实现说明.md diff --git a/学生板块接口实现说明.md b/学生板块接口实现说明.md deleted file mode 100644 index da038ec..0000000 --- a/学生板块接口实现说明.md +++ /dev/null @@ -1,524 +0,0 @@ -# 学生板块:5 个必做接口完整实现 - -本文按当前项目的 FastAPI、Pydantic 2、SQLAlchemy 同步 Session 编写。代码按文件整理,可分别替换对应文件内容;本文仅提供实现文档,没有改动现有 Python 文件。 - -## 一、接口与约定 - -| 方法 | 路径 | 功能 | -| --- | --- | --- | -| GET | `/students` | 分页查询,按学号、姓名、班级筛选 | -| POST | `/students` | 新增学生,未提供学号时自动生成 | -| GET | `/students/{id}` | 查询未删除学生详情 | -| PUT | `/students/{id}` | 修改学生,只更新本次提交的字段 | -| DELETE | `/students/{id}` | 将 `is_deleted` 改为 1 | - -- `id` 是数据库自增主键,`student_no` 是业务学号,二者不同。 -- 学号精确匹配,姓名按包含关系匹配,班级按 `class_id` 精确匹配;多个条件同时生效。 -- 列表默认第 1 页、每页 20 条,每页最多 100 条;无结果返回空数组。 -- PUT 在这里约定为部分更新:未传字段保持原值;可选字段传 `null` 表示清空。 -- 已删除学生不出现在列表、详情中,也不能再修改;重复删除返回 404。 -- 删除学生不自动删除成绩、就业记录,其他模块查询时需要自行处理学生的删除状态。 -- 性别暂不规定枚举,仅要求非空且不超过数据库长度,避免擅自改变团队约定。 -- 年龄暂设为 0~150;毕业日期不能早于入学日期。 - -## 二、请求和响应模型:`schemas/student.py` - -作用:在进入接口业务代码前检查字段类型、长度和取值;定义返回给前端的数据结构。 - -```python -from datetime import date, datetime -from pydantic import BaseModel, ConfigDict, Field, model_validator - - -class StudentCreate(BaseModel): - # 1. 自动去除字符串首尾空格;禁止多传 id、is_deleted 等未声明字段。 - model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") - - # 2. 学号可省略或传 null,此时由后端生成;空字符串不合法。 - student_no: str | None = Field(default=None, min_length=1, max_length=50) - student_name: str = Field(min_length=1, max_length=50) - class_id: int = Field(gt=0) - consultant_id: int | None = Field(default=None, gt=0) - native_place: str | None = Field(default=None, max_length=100) - graduation_school: str | None = Field(default=None, max_length=100) - major: str | None = Field(default=None, max_length=100) - enrollment_date: date - graduation_date: date | None = None - education: str | None = Field(default=None, max_length=50) - age: int = Field(ge=0, le=150) - gender: str = Field(min_length=1, max_length=10) - - @model_validator(mode="after") - def check_dates(self): - # 3. 新增时两个日期都已确定,可以直接校验先后顺序。 - if self.graduation_date and self.graduation_date < self.enrollment_date: - raise ValueError("毕业日期不能早于入学日期") - return self - - -class StudentUpdate(BaseModel): - model_config = ConfigDict(str_strip_whitespace=True, extra="forbid") - - # 1. 默认值为 None 是为了允许省略字段,不代表每个字段都能清空。 - student_no: str | None = Field(default=None, min_length=1, max_length=50) - student_name: str | None = Field(default=None, min_length=1, max_length=50) - class_id: int | None = Field(default=None, gt=0) - consultant_id: int | None = Field(default=None, gt=0) - native_place: str | None = Field(default=None, max_length=100) - graduation_school: str | None = Field(default=None, max_length=100) - major: str | None = Field(default=None, max_length=100) - enrollment_date: date | None = None - graduation_date: date | None = None - education: str | None = Field(default=None, max_length=50) - age: int | None = Field(default=None, ge=0, le=150) - gender: str | None = Field(default=None, min_length=1, max_length=10) - - @model_validator(mode="after") - def check_explicit_null(self): - # 2. model_fields_set 记录客户端实际提交过的字段。 - # 没传姓名可以;明确传 student_name: null 不可以。 - required = { - "student_no", "student_name", "class_id", - "enrollment_date", "age", "gender", - } - for name in required & self.model_fields_set: - if getattr(self, name) is None: - raise ValueError(f"{name} 不能为 null") - return self - - -class StudentOut(BaseModel): - # 允许 Pydantic 从 SQLAlchemy 对象的属性读取数据。 - model_config = ConfigDict(from_attributes=True) - - id: int - student_no: str | None - student_name: str | None - class_id: int | None - consultant_id: int | None - native_place: str | None - graduation_school: str | None - major: str | None - enrollment_date: date - graduation_date: date | None - education: str | None - age: int - gender: str - # 当前 Student 模型的实际字段名是 create_at 和 update_at。 - create_at: datetime | None - update_at: datetime | None - - -class StudentResponse(BaseModel): - code: int = 200 - detail: str = "操作成功" - data: StudentOut - - -class StudentListResponse(BaseModel): - code: int = 200 - detail: str = "查询成功" - total: int - page: int - page_size: int - data: list[StudentOut] - - -class StudentDeleteResponse(BaseModel): - code: int = 200 - detail: str = "删除学生成功" - id: int -``` - -`StudentOut` 的姓名、班级允许为空,是为了兼容当前数据库模型允许为空的历史记录;新增请求仍要求这些字段必填。 - -## 三、数据库操作:`dao/student.py` - -为方便在现有项目直接使用,下列 DAO 使用 `HTTPException` 返回明确业务错误。项目扩大后,可改成自定义业务异常,再由 API 统一转换。 - -```python -import logging -from contextlib import contextmanager - -from fastapi import HTTPException -from sqlalchemy.exc import IntegrityError, SQLAlchemyError -from sqlalchemy.orm import Session - -from models.student import Student -from models.class_model import Classes -from models.consultant import Consultant - -logger = logging.getLogger(__name__) - - -@contextmanager -def database_errors(db: Session): - # 统一处理数据库异常,避免每个接口重复写 try/except。 - try: - yield - except HTTPException: - # 业务检查失败:撤销本次事务,保留原来的 404/409/422 等状态。 - db.rollback() - raise - except IntegrityError: - # 外键、唯一约束等冲突,不能当作成功返回。 - db.rollback() - logger.exception("学生数据约束冲突") - raise HTTPException(409, "数据冲突,请检查学号、班级和顾问信息") from None - except SQLAlchemyError: - db.rollback() - logger.exception("学生数据库操作失败") - raise HTTPException(500, "数据库操作失败,请稍后重试") from None - - -def require_student(student_id: int, db: Session): - # 所有详情、更新和删除操作都复用这一步,排除逻辑删除记录。 - student = db.query(Student).filter( - Student.id == student_id, - Student.is_deleted == 0, - ).first() - if student is None: - raise HTTPException(404, "学生不存在或已删除") - return student - - -def check_references(data: dict, db: Session): - # 1. 仅检查本次涉及的关联字段;更新其他字段时不用重复检查。 - if "class_id" in data: - classroom = db.query(Classes).filter( - Classes.id == data["class_id"], - Classes.is_deleted == 0, - ).first() - if classroom is None: - raise HTTPException(422, "班级不存在或已删除") - - # 2. 顾问允许为 null,此时代表不关联顾问,无需查询。 - if data.get("consultant_id") is not None: - consultant = db.query(Consultant).filter( - Consultant.id == data["consultant_id"], - Consultant.is_deleted == 0, - ).first() - if consultant is None: - raise HTTPException(422, "顾问不存在或已删除") - - -def check_student_no(student_no: str, db: Session, exclude_id=None): - # 学号占用检查包含已删除记录,避免旧学号被其他学生重复使用。 - query = db.query(Student).filter(Student.student_no == student_no) - if exclude_id is not None: - # 修改时必须排除自己,否则原学号也会被判定为重复。 - query = query.filter(Student.id != exclude_id) - if query.first() is not None: - raise HTTPException(409, "学号已存在") - - -def get_student_list(db, page, page_size, student_no=None, - student_name=None, class_id=None): - with database_errors(db): - # 1. 先建立基础查询,只查询未删除学生。 - query = db.query(Student).filter(Student.is_deleted == 0) - # 2. 参数有值时才增加筛选条件,多个 filter 之间为 AND。 - if student_no is not None: - query = query.filter(Student.student_no == student_no) - if student_name is not None: - # 转义 % 和 _,让它们作为姓名的普通字符参与匹配。 - query = query.filter(Student.student_name.contains( - student_name, autoescape=True, - )) - if class_id is not None: - query = query.filter(Student.class_id == class_id) - # 3. 分页前统计符合条件的总数。 - total = query.count() - # 4. 固定按主键倒序,最新记录优先,再跳过前面页的数据。 - rows = query.order_by(Student.id.desc()).offset( - (page - 1) * page_size - ).limit(page_size).all() - return total, rows - - -def get_student_detail(student_id: int, db: Session): - with database_errors(db): - return require_student(student_id, db) - - -def create_students(data: dict, db: Session): - with database_errors(db): - # 1. 校验班级、顾问是否存在且未删除。 - check_references(data, db) - # 2. 用户指定学号时,先检查是否被占用。 - if data.get("student_no") is not None: - check_student_no(data["student_no"], db) - # 3. 创建 ORM 对象并加入当前事务。 - student = Student(**data) - db.add(student) - # 4. flush 执行 INSERT 并获取自增 id,但此时尚未提交事务。 - db.flush() - # 5. 未提供学号时生成 STU0001 格式;四位是最小宽度。 - if student.student_no is None: - generated_no = f"STU{student.id:04d}" - check_student_no(generated_no, db, exclude_id=student.id) - student.student_no = generated_no - # 6. 在提交前生成响应快照,避免提交后再查询失败而误报新增失败。 - db.flush() - db.refresh(student) - from schemas.student import StudentOut - result = StudentOut.model_validate(student) - # 7. 所有步骤成功后提交;失败由 database_errors 回滚。 - db.commit() - return result - - -def update_student(student_id: int, updates: dict, db: Session): - with database_errors(db): - # 1. 先确认学生存在,再检查更新内容。 - student = require_student(student_id, db) - if not updates: - raise HTTPException(422, "请至少提供一个需要修改的字段") - # 2. 若修改班级或顾问,校验新的关联对象。 - check_references(updates, db) - if "student_no" in updates: - check_student_no(updates["student_no"], db, exclude_id=student.id) - # 3. 合并新旧日期再检查,防止只修改一个日期时绕过校验。 - enrollment = updates.get("enrollment_date", student.enrollment_date) - graduation = updates.get("graduation_date", student.graduation_date) - if graduation is not None and graduation < enrollment: - raise HTTPException(422, "毕业日期不能早于入学日期") - # 4. 仅更新实际提交的字段;可选字段传 None 会清空数据库值。 - for name, value in updates.items(): - setattr(student, name, value) - # 5. 执行更新并读取模型自动维护的 update_at,构造响应后提交。 - db.flush() - db.refresh(student) - from schemas.student import StudentOut - result = StudentOut.model_validate(student) - db.commit() - return result - - -def delete_student(student_id: int, db: Session): - with database_errors(db): - # 1. 找到正常学生;不存在或已删除时返回 404。 - student = require_student(student_id, db) - # 2. 只改删除标记,不调用 db.delete,不执行物理删除。 - student.is_deleted = 1 - # 3. 提交后,所有带 is_deleted == 0 的查询都会排除此学生。 - db.commit() - return student_id -``` - -注意:自动生成的学号若已被手动指定的学号占用,会返回 409 并回滚。自增 ID 在回滚后可能留下空号,这是正常现象。 - -## 四、接口层:`api/student.py` - -```python -from fastapi import APIRouter, Depends, Path, Query -from sqlalchemy.orm import Session - -from core.database import get_db -from dao import student as student_dao -from schemas.student import ( - StudentCreate, StudentUpdate, StudentResponse, - StudentListResponse, StudentDeleteResponse, -) - -student_router = APIRouter() - - -@student_router.get("", response_model=StudentListResponse, summary="查询学生列表") -def list_students( - page: int = Query(default=1, ge=1, description="页码,从 1 开始"), - page_size: int = Query(default=20, ge=1, le=100, description="每页条数"), - student_no: str | None = Query(default=None, min_length=1, max_length=50), - student_name: str | None = Query(default=None, min_length=1, max_length=50), - class_id: int | None = Query(default=None, gt=0), - db: Session = Depends(get_db), -): - # 1. FastAPI 校验查询参数,并通过 get_db 注入本次请求的数据库会话。 - # 2. DAO 按条件查询,返回总数和本页数据。 - total, rows = student_dao.get_student_list( - db, page, page_size, student_no, student_name, class_id, - ) - # 3. 返回分页信息,便于前端计算总页数。 - return StudentListResponse( - total=total, page=page, page_size=page_size, data=rows, - ) - - -@student_router.post("", response_model=StudentResponse, summary="新增学生") -def create_student(request: StudentCreate, db: Session = Depends(get_db)): - # 1. 请求体通过 StudentCreate 校验后转为字典。 - # 2. DAO 完成关联检查、学号生成和事务提交。 - result = student_dao.create_students(request.model_dump(), db) - # 3. 返回完整数据,前端可以立即取得 id 和生成的 student_no。 - return StudentResponse(detail="添加学生成功", data=result) - - -@student_router.get("/{id}", response_model=StudentResponse, summary="查询学生详情") -def read_student( - id: int = Path(gt=0), - db: Session = Depends(get_db), -): - # 路径 id 必须是正整数;DAO 对不存在或已删除的记录返回 404。 - result = student_dao.get_student_detail(id, db) - return StudentResponse(detail="查询成功", data=result) - - -@student_router.put("/{id}", response_model=StudentResponse, summary="修改学生") -def update_student( - request: StudentUpdate, - id: int = Path(gt=0), - db: Session = Depends(get_db), -): - # 1. exclude_unset=True 只保留客户端实际传入的字段。 - # 不要换成 exclude_none=True,否则无法通过 null 清空顾问等字段。 - updates = request.model_dump(exclude_unset=True) - # 2. DAO 校验目标学生、关联字段、学号和合并后的日期,再提交修改。 - result = student_dao.update_student(id, updates, db) - return StudentResponse(detail="修改学生成功", data=result) - - -@student_router.delete("/{id}", response_model=StudentDeleteResponse, - summary="逻辑删除学生") -def delete_student( - id: int = Path(gt=0), - db: Session = Depends(get_db), -): - # DAO 只修改删除标记,成功提交后返回被删除学生的主键。 - deleted_id = student_dao.delete_student(id, db) - return StudentDeleteResponse(id=deleted_id) -``` - -## 五、路由和数据库衔接 - -当前 `main.py` 已经有以下代码,保留即可,不要重复注册: - -```python -from api.student import student_router -app.include_router(student_router, tags=["学生接口"], prefix="/students") -``` - -路由文件内部使用空路径或 `/{id}`,是因为 `/students` 前缀由 `main.py` 统一添加。 - -`core.database.get_db()` 负责创建会话并在请求结束后关闭。写入操作由 DAO 提交或回滚,读取操作无需提交。模型继续使用 `core.database.Base`,不要重新创建 Base。 - -### 学号唯一性:当前模型需要补充的约束 - -当前 `models/student.py` 的 `student_no` 没有唯一约束。DAO 的预检查可以处理一般重复输入,但两个并发请求可能同时通过检查;要可靠防重,需配合数据库唯一约束。 - -模型中的字段可改为: - -```python -student_no = Column(VARCHAR(50), nullable=True, unique=True) -``` - -保持允许 NULL,是因为自动编号流程先插入并获取 ID,再写入学号。 - -对于已经存在的表,修改模型或执行 `create_all()` 不会自动添加约束。先检查历史重复数据: - -```sql -SELECT student_no, COUNT(*) AS duplicate_count -FROM student_info_detail -WHERE student_no IS NOT NULL -GROUP BY student_no -HAVING COUNT(*) > 1; -``` - -确认无重复且该唯一约束尚不存在后,再在项目使用的数据库中执行一次: - -```sql -ALTER TABLE student_info_detail -ADD CONSTRAINT uk_student_no UNIQUE (student_no); -``` - -本文没有执行以上 SQL。该约束包含已逻辑删除的数据,与 DAO 的学号占用规则一致。 - -## 六、调用示例 - -按项目当前入口启动后,可在 `http://127.0.0.1:23333/docs` 调试。需要先配置 MySQL、创建表,并准备一个真实存在且未删除的班级。以下 `class_id: 1`、路径中的学生 ID 均为示例,应替换为实际值。 - -### 1. 新增学生 - -`POST /students` - -```json -{ - "student_name": "张三", - "class_id": 1, - "enrollment_date": "2026-09-01", - "age": 20, - "gender": "男", - "major": "软件技术" -} -``` - -成功 HTTP 状态为 200,响应包含 `code`、`detail` 和 `data`,其中 `data.id`、`data.student_no` 是后续操作使用的主键和学号。这里保留原接口成功返回 200 的约定。 - -### 2. 查询列表 - -```text -GET /students?page=1&page_size=20 -GET /students?student_no=STU0001 -GET /students?student_name=张&class_id=1 -``` - -响应包含 `total`、`page`、`page_size` 和 `data` 数组。超出实际页数时,`total` 仍为匹配总数,`data` 为空。 - -### 3. 查询详情 - -```text -GET /students/1 -``` - -### 4. 修改学生 - -`PUT /students/1` - -```json -{ - "student_name": "张三丰", - "age": 21, - "consultant_id": null -} -``` - -这次只更新姓名、年龄和顾问;顾问关联被清空,班级、日期等未传字段保持原值。 - -### 5. 逻辑删除 - -```text -DELETE /students/1 -``` - -```json -{ - "code": 200, - "detail": "删除学生成功", - "id": 1 -} -``` - -删除后数据库行仍存在,`is_deleted` 为 1;后续详情、更新、再次删除均返回 404。 - -## 七、验证清单与错误说明 - -| 场景 | 预期 | -| --- | --- | -| 不传学号新增 | 自动生成学号,响应带主键与学生信息 | -| 指定已占用学号新增或修改 | 409,事务回滚 | -| 班级不存在或已删除 | 422 | -| 顾问不存在或已删除 | 422 | -| 姓名为空白、年龄越界、日期格式错误 | 422 | -| 毕业日期早于入学日期 | 422,新增与部分更新都检查 | -| 查询参数 page=0 或 page_size=101 | 422 | -| 同时按学号、姓名和班级筛选 | 返回满足全部条件的未删除学生 | -| 查询无匹配记录 | 200,total=0,data=[] | -| 更新仅传 age | 只更新年龄,其他字段保留 | -| 更新 consultant_id=null | 清空顾问关联 | -| 更新 student_name=null 或空请求体 | 422 | -| 查询、修改或删除不存在的学生 | 404 | -| 删除已有学生后重新查询 | 列表不可见,详情为 404 | -| 数据库操作失败 | 500,写事务回滚,服务端记录日志 | - -主动抛出的业务错误通常返回 `{"detail": "错误说明"}`;FastAPI 参数校验错误的 `detail` 是包含字段位置的数组。这两种结构应由前端分别处理。 - -以上是供接入项目的实现代码和验证步骤;文档生成时未连接真实 MySQL,实际数据库约束、历史数据和接口联调仍需按清单验证。 From 35a34c785f21d66325150e02c62fa541f44a5e99 Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Mon, 21 Sep 2026 20:51:07 +0800 Subject: [PATCH 12/23] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/statistics.py | 4 ++-- dao/statistics.py | 23 +++++++++++++---------- utils/code_generator.py | 2 +- 3 files changed, 16 insertions(+), 13 deletions(-) diff --git a/api/statistics.py b/api/statistics.py index 4875d0f..764f04b 100644 --- a/api/statistics.py +++ b/api/statistics.py @@ -11,7 +11,7 @@ from sqlalchemy.orm import Session statistics_router=APIRouter() -@statistics_router.get("/scores/age-over-30",response_model=SuccessResponse) +@statistics_router.get("/students/age-over-30",response_model=SuccessResponse) def api_age_over_thirty(db:Session=Depends(get_db)): try: age_over_thirty_list=age_over_thirty(Student,db) @@ -71,7 +71,7 @@ def employ_time(db:Session=Depends(get_db)): @statistics_router.get("/employments/class-average-duration",response_model=SuccessResponse) def avg_employ_time(db:Session=Depends(get_db)): try: - result = avg_class(Employment,Student,db) + result = avg_emptime(Employment,Student,db) return SuccessResponse(status_code=200,msg=result) except NoExist as e: raise HTTPException(status_code=404,detail=str(e)) diff --git a/dao/statistics.py b/dao/statistics.py index fdfe20b..a01a1ae 100644 --- a/dao/statistics.py +++ b/dao/statistics.py @@ -5,25 +5,28 @@ from time import time #查询所有超过 30 岁的学员信息 def age_over_thirty(Student,db): - student_list=db.query(Student).filter(Student.age>=30).all() + student_list=db.query(Student).filter(Student.age>30,Student.is_delted==0).all() if not student_list : raise NoExist('无目标年龄大于30岁') return student_list def gender_count(Student,db): - result=db.query(Student.class_id,Student.gender,func.count(Student.id)).group_by(Student.class_id,Student.gender).all() + result=db.query(Student.class_id,Student.gender,func.count(Student.id)).filter(Student.is_deleted==0).group_by(Student.class_id,Student.gender).all() if not result : raise NoExist('班里招点人吧,要倒闭了') dict1 = {} + sum_stu = 0 for i,j,k in result: if i not in dict1 : dict1[i]={'男':0,'女':0} - dict[i][j]+=k + dict1[i][j]+=k + sum_stu+=k + dict1['总人数'] = sum_stu return dict1 #查询每次考试成绩都在 80 分以上的学生编号、姓名和成绩 def score_over_eighty(Score,Student,db): student_list=db.query(Student.id, Student.student_name)\ - .join(Score,Score.student_id==Student.id).group_by(Student.student_no,Student.student_name)\ + .join(Score,Score.student_id==Student.id).filter(Student.is_deleted==0,Score.is_deleted==0).group_by(Student.student_id,Student.student_name)\ .having(func.min(Score.score)>80).all() if not student_list : raise NoExist('全是学渣') @@ -35,26 +38,26 @@ def score_over_eighty(Score,Student,db): return list1 #查询有两次以上不及格的学生姓名、班级和不及格成绩 def twice_error(Score,Student,db): - student_list=db.query(Score.student_id).filter(Score.score<60).group_by(Score.student_id).having(func.count(Score.id)>1).all() + student_list=db.query(Score.student_id).filter(Score.score<60,Score.is_deleted==0).group_by(Score.student_id).having(func.count(Score.id)>2).all() if not student_list : raise NoExist('全是学霸') list1=[] for i in student_list: result=db.query(Student.student_name,Student.class_id,Score.score).join(Score, Student.id==Score.student_id)\ - .filter(i.student_id==Score.student_id,Score.score<60).all() + .filter(i.student_id==Score.student_id,Score.score<60,Student.is_deleted==0,Score.is_deleted==0).all() list1.extend(result) return list1 #统计每次考试每个班级的平均分,并按照平均分从高到低排序 def avg_class(Score,Student,db): - student_list = db.query(Student.class_id,Score.exam_seq,func.avg(Score.score)).join(Score, Student.id == Score.student_id).group_by(Student.class_id,Score.exam_seq).order_by(func.avg(Score.score).desc()).all() + student_list = db.query(Student.class_id,Score.exam_seq,func.avg(Score.score)).join(Score, Student.id == Score.student_id).filter(Score.is_deleted==0,Student.is_deleted==0).group_by(Student.class_id,Score.exam_seq).order_by(func.avg(Score.score).desc()).all() if not student_list : raise NoExist('招个人吧') return student_list #统计就业薪资最高的前五名学生姓名、班级、就业时间、就业公司 def max_salary(Student,Employment,db): - student_list = db.query(Student.student_name,Student.class_id,Employment.offer_date,Employment.company_name).join(Employment, Student.id == Employment.student_id).order_by(Employment.salary_max.desc()).limit(5).all() + student_list = db.query(Student.student_name,Student.class_id,Employment.offer_date,Employment.company_name).join(Employment, Student.id == Employment.student_id).filter(Student.is_deleted==0,Employment.is_deleted==0).order_by(Employment.salary.desc()).limit(5).all() if not student_list : raise NoExist('没就业的啊兄弟?') return student_list @@ -62,7 +65,7 @@ def max_salary(Student,Employment,db): # - 统计每个学生的就业时长 # - 就业时长 = `offer_date - employment_open_date` def student_market(Employment,db): - student_list = db.query(Employment.student_id,func.datediff(Employment.offer_date,Employment.employment_open_date)).all() + student_list = db.query(Employment.student_id,func.datediff(Employment.offer_date,Employment.employment_open_date)).filter(Employment.is_deleted==0).all() return student_list # - 统计每个班级的平均就业时长 @@ -70,7 +73,7 @@ def student_market(Employment,db): def avg_emptime(Employment,Student,db): student_list = (db.query(Student.class_id,\ func.avg(func.datediff(Employment.offer_date,Employment.employment_open_date)))\ - .join(Employment, Student.id==Employment.student_id).filter(Employment.employment_open_date.is_not(None)))\ + .join(Employment, Student.id==Employment.student_id).filter(Student.is_deleted==0,Employment.is_deleted==0,Employment.employment_open_date.is_not(None)))\ .group_by(Student.class_id).all() if not student_list : raise NoExist('一个就业都没有吗') diff --git a/utils/code_generator.py b/utils/code_generator.py index f4b48ce..07b61d0 100644 --- a/utils/code_generator.py +++ b/utils/code_generator.py @@ -34,7 +34,7 @@ def add_student( -def generate_no(field_name: str, prefix: str, db_object, db: Session): +def generate_no(field_name: str, prefix: str, db_object, db): if getattr(db_object, field_name): return From 0b92b17283b036f0710183c5c3f9aa7e97145742 Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 21:09:38 +0800 Subject: [PATCH 13/23] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dao/student.py | 1 + models/student.py | 3 ++- schemas/student.py | 42 ++++++++++++++++++++++++++++++++---------- 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/dao/student.py b/dao/student.py index 021ad90..0030b49 100644 --- a/dao/student.py +++ b/dao/student.py @@ -22,3 +22,4 @@ def create_students(d: dict, db: Session): print(f"插入学生失败: {e}") return None +# def updet_students(): \ No newline at end of file diff --git a/models/student.py b/models/student.py index 881c9b8..fc73e95 100644 --- a/models/student.py +++ b/models/student.py @@ -10,7 +10,8 @@ class Student(Base): , comment = '学生编号,自增主键' ) student_no = Column(VARCHAR(50) - ,nullable = True) + ,nullable = True + ,unique = True) student_name = Column(VARCHAR(50)) class_id = Column(Integer ,ForeignKey('class_info_detail.id') diff --git a/schemas/student.py b/schemas/student.py index 6265446..213ae53 100644 --- a/schemas/student.py +++ b/schemas/student.py @@ -1,18 +1,40 @@ from pydantic import BaseModel from datetime import date +from pydantic import BaseModel, ConfigDict, Field, model_validator class StudentCreate(BaseModel): - student_no:str|None = None - student_name: str - class_id: int - consultant_id: int | None = None - native_place: str | None = None - graduation_school: str | None = None - major: str | None = None + student_no: str | None = Field(default=None, min_length=1, max_length=50) + student_name: str = Field(min_length=1, max_length=50) + class_id: int = Field(gt=0) + consultant_id: int | None = Field(default=None, gt=0) + native_place: str | None = Field(default=None, max_length=100) + graduation_school: str | None = Field(default=None, max_length=100) + major: str | None = Field(default=None, max_length=100) enrollment_date: date graduation_date: date | None = None - education: str | None = None - age: int - gender: str + education: str | None = Field(default=None, max_length=50) + age: int = Field(ge=0, le=150) + gender: str = Field(min_length=1, max_length=10) + @model_validator(mode="after") + def check_dates(self): + # 3. 新增时两个日期都已确定,可以直接校验先后顺序。 + if self.graduation_date and self.graduation_date < self.enrollment_date: + raise ValueError("毕业日期不能早于入学日期") + return self + +class StudentUpdate(BaseModel): + # 1. 默认值为 None 是为了允许省略字段,不代表每个字段都能清空。 + student_no: str | None = Field(default=None, min_length=1, max_length=50) + student_name: str | None = Field(default=None, min_length=1, max_length=50) + class_id: int | None = Field(default=None, gt=0) + consultant_id: int | None = Field(default=None, gt=0) + native_place: str | None = Field(default=None, max_length=100) + graduation_school: str | None = Field(default=None, max_length=100) + major: str | None = Field(default=None, max_length=100) + enrollment_date: date | None = None + graduation_date: date | None = None + education: str | None = Field(default=None, max_length=50) + age: int | None = Field(default=None, ge=0, le=150) + gender: str | None = Field(default=None, min_length=1, max_length=10) From ce3cae0b826f07487dd6e8d5dad3d2e02a72e345 Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Mon, 21 Sep 2026 21:21:51 +0800 Subject: [PATCH 14/23] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dao/statistics.py | 84 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 70 insertions(+), 14 deletions(-) diff --git a/dao/statistics.py b/dao/statistics.py index a01a1ae..ddaaa2e 100644 --- a/dao/statistics.py +++ b/dao/statistics.py @@ -5,36 +5,60 @@ from time import time #查询所有超过 30 岁的学员信息 def age_over_thirty(Student,db): - student_list=db.query(Student).filter(Student.age>30,Student.is_delted==0).all() + student_list=db.query(Student).filter(Student.age>30,Student.is_deleted==0).all() if not student_list : raise NoExist('无目标年龄大于30岁') - return student_list + # 把每个学生对象需要返回的字段,明确放进普通字典。 + result = [] + for student in student_list: + result.append({ + "id": student.id, + "student_no": student.student_no, + "student_name": student.student_name, + "class_id": student.class_id, + "consultant_id": student.consultant_id, + "native_place": student.native_place, + "graduation_school": student.graduation_school, + "major": student.major, + "enrollment_date": student.enrollment_date, + "graduation_date": student.graduation_date, + "education": student.education, + "age": student.age, + "gender": student.gender, + "create_at": student.create_at, + "update_at": student.update_at, + "is_deleted": student.is_deleted, + }) + return result def gender_count(Student,db): result=db.query(Student.class_id,Student.gender,func.count(Student.id)).filter(Student.is_deleted==0).group_by(Student.class_id,Student.gender).all() if not result : raise NoExist('班里招点人吧,要倒闭了') dict1 = {} - sum_stu = 0 for i,j,k in result: if i not in dict1 : - dict1[i]={'男':0,'女':0} + dict1[i]={'男':0,'女':0,'总人数':0} dict1[i][j]+=k - sum_stu+=k - dict1['总人数'] = sum_stu + dict1[i]['总人数']+=k return dict1 #查询每次考试成绩都在 80 分以上的学生编号、姓名和成绩 def score_over_eighty(Score,Student,db): student_list=db.query(Student.id, Student.student_name)\ - .join(Score,Score.student_id==Student.id).filter(Student.is_deleted==0,Score.is_deleted==0).group_by(Student.student_id,Student.student_name)\ + .join(Score,Score.student_id==Student.id).filter(Student.is_deleted==0,Score.is_deleted==0).group_by(Student.id,Student.student_name)\ .having(func.min(Score.score)>80).all() if not student_list : raise NoExist('全是学渣') list1 = [] for i in student_list: - student_list2 = db.query(Student.student_no, Student.student_name,Score.score).join(Score,Score.student_id==Student.id).filter(i.id==Student.id).all() + student_list2 = db.query(Student.student_no, Student.student_name,Score.score).join(Score,Score.student_id==Student.id).filter(i.id==Student.id,Score.is_deleted==0).all() if student_list2 : - list1.extend(student_list2) + for student_no, student_name, score in student_list2: + list1.append({ + "student_no": student_no, + "student_name": student_name, + "score": float(score) if score is not None else None, + }) return list1 #查询有两次以上不及格的学生姓名、班级和不及格成绩 def twice_error(Score,Student,db): @@ -45,7 +69,12 @@ def twice_error(Score,Student,db): for i in student_list: result=db.query(Student.student_name,Student.class_id,Score.score).join(Score, Student.id==Score.student_id)\ .filter(i.student_id==Score.student_id,Score.score<60,Student.is_deleted==0,Score.is_deleted==0).all() - list1.extend(result) + for student_name, class_id, score in result: + list1.append({ + "student_name": student_name, + "class_id": class_id, + "score": float(score) if score is not None else None, + }) return list1 #统计每次考试每个班级的平均分,并按照平均分从高到低排序 @@ -53,20 +82,41 @@ def avg_class(Score,Student,db): student_list = db.query(Student.class_id,Score.exam_seq,func.avg(Score.score)).join(Score, Student.id == Score.student_id).filter(Score.is_deleted==0,Student.is_deleted==0).group_by(Student.class_id,Score.exam_seq).order_by(func.avg(Score.score).desc()).all() if not student_list : raise NoExist('招个人吧') - return student_list + result = [] + for class_id, exam_seq, average_score in student_list: + result.append({ + "class_id": class_id, + "exam_seq": exam_seq, + "average_score": float(average_score) if average_score is not None else None, + }) + return result #统计就业薪资最高的前五名学生姓名、班级、就业时间、就业公司 def max_salary(Student,Employment,db): student_list = db.query(Student.student_name,Student.class_id,Employment.offer_date,Employment.company_name).join(Employment, Student.id == Employment.student_id).filter(Student.is_deleted==0,Employment.is_deleted==0).order_by(Employment.salary.desc()).limit(5).all() if not student_list : raise NoExist('没就业的啊兄弟?') - return student_list + result = [] + for student_name, class_id, offer_date, company_name in student_list: + result.append({ + "student_name": student_name, + "class_id": class_id, + "offer_date": offer_date, + "company_name": company_name, + }) + return result # - 统计每个学生的就业时长 # - 就业时长 = `offer_date - employment_open_date` def student_market(Employment,db): student_list = db.query(Employment.student_id,func.datediff(Employment.offer_date,Employment.employment_open_date)).filter(Employment.is_deleted==0).all() - return student_list + result = [] + for student_id, duration_days in student_list: + result.append({ + "student_id": student_id, + "duration_days": duration_days, + }) + return result # - 统计每个班级的平均就业时长 # - 只统计有就业开放时间的学生 @@ -77,7 +127,13 @@ def avg_emptime(Employment,Student,db): .group_by(Student.class_id).all() if not student_list : raise NoExist('一个就业都没有吗') - return student_list + result = [] + for class_id, average_days in student_list: + result.append({ + "class_id": class_id, + "average_days": float(average_days) if average_days is not None else None, + }) + return result From 0b72cb78ee9e955cbd517af0c7f03819e4d28095 Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Mon, 21 Sep 2026 21:23:43 +0800 Subject: [PATCH 15/23] =?UTF-8?q?bug=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/student.py | 2 +- models/class_model.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/student.py b/api/student.py index 9299659..99daec7 100644 --- a/api/student.py +++ b/api/student.py @@ -6,7 +6,7 @@ from schemas.student import StudentCreate student_router = APIRouter() @student_router.post("/student") -def create_student(request:StudentCreate,db:Session,create = Depends(create_students)): +def create_student(request:StudentCreate,db:Session=Depends(create_students)): d = request.model_dump() r = create_students(d,db) if not r: diff --git a/models/class_model.py b/models/class_model.py index 50ff873..dc6ce4a 100644 --- a/models/class_model.py +++ b/models/class_model.py @@ -35,7 +35,7 @@ class Classes( Base ): # 在python里的名字 , nullable = False # 声明是"非空" ) # ————创建班级"更新时间"的字段名———— - updated_at = Column( DATETIME # 声明是字段数据类型是"日期时间" + update_at = Column( DATETIME # 声明是字段数据类型是"日期时间" , default=datetime.now #声明是第一次更新的时间就是第一次创建的时间一致 , onupdate=datetime.now # 声明"最后修改的时间" , server_default=text("CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP") From 3e3e6d44eba92caa9dc8caf77bef7486d0eed307 Mon Sep 17 00:00:00 2001 From: nfy <2425896042@qq.com> Date: Mon, 21 Sep 2026 21:29:06 +0800 Subject: [PATCH 16/23] test --- api/employment.py | 117 ++++++++++++++++++++++++++++++ dao/employment.py | 165 ++++++++++++++++++++++++++++++++++++++++++ schemas/employment.py | 49 +++++++++++++ 3 files changed, 331 insertions(+) diff --git a/api/employment.py b/api/employment.py index e69de29..48b61d2 100644 --- a/api/employment.py +++ b/api/employment.py @@ -0,0 +1,117 @@ +# APIRouter:路由对象,用来分组接口 +from fastapi import APIRouter, Depends, HTTPException, Query +# Session:数据库会话类型注解 +from sqlalchemy.orm import Session +# 类型提示:可选参数 +from typing import Optional + +# 导入get_db,数据库会话依赖函数,每次请求生成/释放数据库连接 +from core.database import get_db +# 导入dao层全部数据库操作函数 +from dao.employment import ( + create_employment, + get_employment_by_id, + get_employment_by_student_id, + get_employment_list, + update_employment, + delete_employment_logic +) +# 导入pydantic请求体、响应体模型 +from schemas.employment import ( + EmploymentCreate, + EmploymentUpdate, + EmploymentResponse, + EmploymentListResponse +) + +employment_router = APIRouter(prefix="/employments", tags=["就业管理模块"]) + + +@employment_router.post("", response_model=EmploymentResponse, summary="新增就业信息") +def add_employment(body: EmploymentCreate, db: Session = Depends(get_db)): + """ + 新增就业信息 + - 一个学生只能保存一条就业记录(数据库唯一约束控制) + - student_id必须在学生表student_info_detail中真实存在,否则外键报错 + :param body:POST请求的JSON请求体,自动解析为EmploymentCreate对象 + :param db: Depends(get_db),依赖注入,自动获取数据库会话,请求结束自动关闭连接 + :return: 返回EmploymentResponse结构数据给前端 + """ + # 调用dao层新增函数 + res = create_employment(db, body) + # dao返回None,代表新增出错(外键不存在/学生重复/数据库异常) + if res is None: + # 抛出HTTP异常,500状态码,返回提示信息给前端/Swagger + raise HTTPException(status_code=500, detail="新增失败,学生id不存在或该学生已有就业记录") + # 成功,直接返回ORM对象,FastAPI会按照response_model自动转为json返回 + return res + + +@employment_router.get("/students/{student_id}", response_model=EmploymentResponse, summary="查询指定学生的就业信息") +def query_student_employment(student_id: int, db: Session = Depends(get_db)): + """ + 根据学生id查询该学生的就业记录 + :param student_id: url路径中的路径参数,学生id + :param db:数据库会话 + """ + # 调用dao,按学生id查询 + record = get_employment_by_student_id(db, student_id) + # 如果查询结果为空,抛出404找不到资源 + if not record: + raise HTTPException(status_code=404, detail="未找到该学生的就业信息") + return record + + +@employment_router.get("", response_model=EmploymentListResponse, summary="就业信息列表,支持筛选") +def query_employment_list( + # Query:获取url中?后面的查询参数;description显示在Swagger文档 + company_name: Optional[str] = Query(None, description="公司名称模糊查询"), + salary_min: Optional[float] = Query(None, description="最低薪资"), + salary_max: Optional[float] = Query(None, description="最高薪资"), + skip: int = Query(0, ge=0, description="偏移量"), # ge=0参数校验,不能传负数 + limit: int = Query(20, ge=1, le=100, description="每页条数"), # 1‑100条限制,防止一次性查大量数据 + db: Session = Depends(get_db) +): + """就业分页列表接口,支持公司名模糊,薪资区间筛选""" + # 调用dao层列表查询,拿到记录列表和总条数 + data_list, total = get_employment_list(db, company_name, salary_min, salary_max, skip, limit) + # 返回符合EmploymentListResponse格式的字典,FastAPI自动序列化 + return {"total": total, "data": data_list} + + +@employment_router.get("/{eid}", response_model=EmploymentResponse, summary="根据id查询就业详情") +def query_employment_detail(eid: int, db: Session = Depends(get_db)): + """ + 根据就业主键id查询单条就业详情 + :param eid: url路径参数,就业记录主键 + """ + record = get_employment_by_id(db, eid) + if not record: + raise HTTPException(status_code=404, detail="就业记录不存在或已被逻辑删除") + return record + + +@employment_router.put("/{eid}", response_model=EmploymentResponse, summary="修改就业信息") +def edit_employment(eid: int, body: EmploymentUpdate, db: Session = Depends(get_db)): + """ + 修改就业信息;请求体只传需要修改的字段即可,不需要全部传 + :param eid:待修改记录id,路径参数 + :param body:PUT请求的JSON请求体 + """ + res = update_employment(db, eid, body) + if res is None: + raise HTTPException(status_code=404, detail="修改失败,记录不存在") + return res + + +@employment_router.delete("/{eid}", summary="逻辑删除就业记录") +def logic_delete_employment(eid: int, db: Session = Depends(get_db)): + """ + 逻辑删除接口:不会物理删除数据库行,只设置is_deleted=1 + DELETE请求,RESTful风格 + """ + ok = delete_employment_logic(db, eid) + if not ok: + raise HTTPException(status_code=404, detail="删除失败,记录不存在") + # 返回简单json提示成功 + return {"code": 200, "msg": "逻辑删除成功"} diff --git a/dao/employment.py b/dao/employment.py index e69de29..2812d10 100644 --- a/dao/employment.py +++ b/dao/employment.py @@ -0,0 +1,165 @@ +# 导入sqlalchemy会话对象,数据库会话,所有数据库操作都依靠Session +from sqlalchemy.orm import Session +# 类型提示:可选对象、列表 +from typing import Optional, List +# 导入数据库ORM模型 +from models.employment import Employment +# 导入pydantic入参模型 +from schemas.employment import EmploymentCreate, EmploymentUpdate + + +def create_employment(db: Session, obj_in: EmploymentCreate) -> Optional[Employment]: + """ + 新增就业信息:一个学生只能存在一条就业记录 + :param db: 数据库会话对象,由FastAPI依赖注入传入 + :param obj_in: 新增的入参对象 EmploymentCreate + :return: 成功返回ORM对象,发生任何异常返回None + """ + try: + # model_dump()把pydantic对象转字典;**解包字典,赋值给Employment模型字段,创建ORM对象 + db_obj = Employment(**obj_in.model_dump()) + # 将对象加入会话,此时还没有写入数据库 + db.add(db_obj) + # 提交事务,真正执行insert SQL写入数据库 + db.commit() + # db.refresh:从数据库刷新对象,回填数据库自动生成的id等字段到db_obj + db.refresh(db_obj) + return db_obj + except Exception: + # 只要出现任意异常,执行回滚,撤销本次会话所有未提交操作,防止脏数据残留 + db.rollback() + return None + + +def get_employment_by_id(db: Session, eid: int) -> Optional[Employment]: + """ + 根据主键id查询就业详情,过滤逻辑删除的数据 + :param db: 数据库会话 + :param eid:就业记录主键id + :return:查到返回ORM对象;没找到/异常返回None + """ + try: + # 查询Employment表,条件:id等于eid,并且is_deleted=0(没有被逻辑删除) + return db.query(Employment).filter( + Employment.id == eid, + Employment.is_deleted == 0 + ).first() # first()获取第一条结果,没有匹配返回None + except Exception: + return None + + +def get_employment_by_student_id(db: Session, student_id: int) -> Optional[Employment]: + """ + 根据学生id查询就业信息,过滤逻辑删除 + :param db:数据库会话 + :param student_id:学生主键id + :return:查到返回ORM对象,否则None + """ + try: + return db.query(Employment).filter( + Employment.student_id == student_id, + Employment.is_deleted == 0 + ).first() + except Exception: + return None + + +def get_employment_list( + db: Session, + company_name: Optional[str] = None, + salary_min: Optional[float] = None, + salary_max: Optional[float] = None, + skip: int = 0, + limit: int = 20 +) -> tuple[List[Employment], int]: + """ + 就业列表查询;支持公司名模糊、薪资范围筛选;只查询is_deleted=0正常数据 + :param db:数据库会话 + :param company_name:公司名称,模糊查询,不传不做过滤 + :param salary_min:薪资下限,不传不做过滤 + :param salary_max:薪资上限,不传不做过滤 + :param skip:分页偏移量,跳过多少条 + :param limit:分页每页最多取多少条 + :return:元组 (记录列表,总条数);异常返回 [],0 + """ + try: + # 构建基础查询对象,只查询未被逻辑删除的数据 + query = db.query(Employment).filter(Employment.is_deleted == 0) + + # 如果传入公司名,增加模糊匹配条件,%代表通配符 + if company_name: + query = query.filter(Employment.company_name.like(f"%{company_name}%")) + + # 如果传入最低薪资,增加薪资>=下限条件 + if salary_min is not None: + query = query.filter(Employment.salary >= salary_min) + + # 如果传入最高薪资,增加薪资<=上限条件 + if salary_max is not None: + query = query.filter(Employment.salary <= salary_max) + + # count()统计符合条件的总记录数,用于前端分页 + total = query.count() + # offset跳过skip条,limit最多取limit条,执行查询拿到结果列表 + records = query.offset(skip).limit(limit).all() + return records, total + except Exception: + # 异常返回空列表、总条数0 + return [], 0 + + +def update_employment(db: Session, eid: int, obj_in: EmploymentUpdate) -> Optional[Employment]: + """ + 修改就业信息,只更新前端传入的字段 + :param db:数据库会话 + :param eid:待修改记录主键id + :param obj_in:修改入参对象 EmploymentUpdate + :return:成功返回更新后的ORM对象;记录不存在/异常返回None + """ + try: + # 先根据id查询这条记录(自动过滤逻辑删除) + db_obj = get_employment_by_id(db, eid) + # 如果记录不存在,直接返回None + if not db_obj: + return None + + # exclude_unset=True:只拿到前端实际传过来的字段,没有传的字段不会出现在字典中 + update_dict = obj_in.model_dump(exclude_unset=True) + + # 循环字典,把每一个key‑value赋值给ORM对象属性 + for key, value in update_dict.items(): + setattr(db_obj, key, value) + + # 提交事务,把修改写入数据库 + db.commit() + # 刷新对象,同步数据库最新数据到对象 + db.refresh(db_obj) + return db_obj + except Exception: + # 发生异常回滚,放弃修改 + db.rollback() + return None + + +def delete_employment_logic(db: Session, eid: int) -> bool: + """ + 逻辑删除,只修改is_deleted=1,**不做物理删除db.delete()**,符合项目文档要求 + :param db:数据库会话 + :param eid:待删除记录id + :return:成功返回True;记录不存在/异常返回False + """ + try: + # 查询这条记录,过滤已逻辑删除 + db_obj = get_employment_by_id(db, eid) + # 记录不存在返回False + if not db_obj: + return False + # 修改逻辑删除标记为1 + db_obj.is_deleted = 1 + # 提交事务,把修改写入数据库 + db.commit() + return True + except Exception: + # 异常回滚,返回False + db.rollback() + return False diff --git a/schemas/employment.py b/schemas/employment.py index e69de29..2eb203d 100644 --- a/schemas/employment.py +++ b/schemas/employment.py @@ -0,0 +1,49 @@ +from pydantic import BaseModel, Field +from typing import Optional +from datetime import date,datetime +from decimal import Decimal +# int + + +class EmploymentCreate(BaseModel): + """新增就业信息 请求体 Schema""" + # 学生ID 必填,一个学生只能有一条就业记录 + student_id: int = Field(..., description="关联学生ID,必填") + # 就业状态必填,限定可选值业务枚举 + employment_status: str = Field(None, description="就业状态:not_started/job_hunting/offered/employed") + employment_open_date: Optional[date] = Field(None, description="就业开放时间,选填") + offer_date: Optional[date] = Field(None, description="offer下发时间,选填") + company_name: Optional[str] = Field(None, max_length=100, description="就业公司名称,选填") + salary: Optional[Decimal] = Field(None, ge=0, description="就业薪资,不能负数,选填") + remark: Optional[str] = Field(None, max_length=255, description="备注,选填") + + +class EmploymentUpdate(BaseModel): + """修改就业信息 请求体 Schema:全部字段可选,只传要修改的字段""" + employment_status: Optional[str] = Field(None, description="就业状态") + employment_open_date: Optional[date] = Field(None, description="就业开放时间") + offer_date: Optional[date] = Field(None, description="offer下发时间") + company_name: Optional[str] = Field(None, max_length=100, description="就业公司名称") + salary: Optional[Decimal] = Field(None, ge=0, description="就业薪资") + remark: Optional[str] = Field(None, max_length=255, description="备注") + + +class EmploymentResponse(BaseModel): + """就业信息返回响应体Schema,接口返回给前端的数据结构""" + id: int + student_id: int + employment_status: str + employment_open_date: Optional[date] + offer_date: Optional[date] + company_name: Optional[str] + salary: Optional[Decimal] + remark: Optional[str] + created_at: date + updated_at: date + + class Config: + from_attributes = True + +class EmploymentListResponse(BaseModel): + total: int + data: list[EmploymentResponse] From 3577495b6c5a42fece4da8b42176aaf2402fbc0a Mon Sep 17 00:00:00 2001 From: wolin_liyujie <168155038@qq.com> Date: Mon, 21 Sep 2026 21:39:13 +0800 Subject: [PATCH 17/23] =?UTF-8?q?=E5=88=A0=E9=99=A4=E4=BA=86=E6=96=87?= =?UTF-8?q?=E6=A1=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/student.py | 87 +++++++++++++++++++++++++++++++++++++++++++--- dao/student.py | 80 +++++++++++++++++++++++++++++++++++++++++- schemas/student.py | 22 ++++++------ 3 files changed, 173 insertions(+), 16 deletions(-) diff --git a/api/student.py b/api/student.py index c606935..996c47a 100644 --- a/api/student.py +++ b/api/student.py @@ -1,17 +1,96 @@ -from fastapi import APIRouter,Depends,HTTPException -from dao.student import * -from schemas.student import StudentCreate +from fastapi import APIRouter, Depends, HTTPException from sqlalchemy.orm import Session + +from core.database import get_db +from schemas.student import StudentCreate, StudentUpdate +from dao import student as student_dao student_router = APIRouter() @student_router.post("",summary='添加学生') def create_student(request:StudentCreate,db:Session = Depends(get_db)): d = request.model_dump() - r = create_students(d,db) + r = student_dao.create_students(d,db) if not r: raise HTTPException(status_code=500,detail='服务器繁忙,请稍后添加!') return {'code':200,'detail':"添加学生成功"} +@student_router.put("/{id}", summary="修改学生") +def update_student( + id: int, + request: StudentUpdate, + db: Session = Depends(get_db), +): + # 只取前端真正传来的字段 + # 例如只传 age,就只修改 age + data = request.model_dump(exclude_unset=True) + student = student_dao.update_student(id, data, db) + # None 表示学生不存在 + if student is None: + raise HTTPException(status_code=404, detail="学生不存在") + # False 表示操作数据库失败 + if student is False: + raise HTTPException(status_code=500, detail="修改学生失败") + + return { + "code": 200, + "detail": "修改学生成功", + "data": student, + } + +@student_router.get("", summary="查询学生列表") +def get_students( + # 三个查询条件都可以不传 + student_no: str | None = None, + student_name: str | None = None, + class_id: int | None = None, + # 获取本次请求使用的数据库连接 + db: Session = Depends(get_db), +): + # 调用 DAO 查询数据库 + students = student_dao.get_students( + db=db, + student_no=student_no, + student_name=student_name, + class_id=class_id, + ) + + return { + "code": 200, + "detail": "查询成功", + "total": len(students), + "data": students, + } + +@student_router.get("/{id}", summary="查询学生详情") +def get_student(id: int, db: Session = Depends(get_db)): + # 根据路径中的 ID 查询学生 + student = student_dao.get_student_by_id(id, db) + + if not student: + # 404 表示请求的数据不存在 + raise HTTPException(status_code=404, detail="学生不存在") + + return { + "code": 200, + "detail": "查询成功", + "data": student, + } + +@student_router.delete("/{id}", summary="逻辑删除学生") +def delete_student(id: int, db: Session = Depends(get_db)): + student = student_dao.delete_student(id, db) + + # None 表示不存在,或者以前已经删除 + if student is None: + raise HTTPException(status_code=404, detail="学生不存在或已经删除") + + if student is False: + raise HTTPException(status_code=500, detail="删除学生失败") + + return { + "code": 200, + "detail": "删除学生成功", + } diff --git a/dao/student.py b/dao/student.py index 0030b49..e11ce68 100644 --- a/dao/student.py +++ b/dao/student.py @@ -22,4 +22,82 @@ def create_students(d: dict, db: Session): print(f"插入学生失败: {e}") return None -# def updet_students(): \ No newline at end of file +def get_student_by_id(student_id: int, db: Session): + # 同时判断 is_deleted,已经逻辑删除的学生不会被查到 + return db.query(Student).filter( + Student.id == student_id, + Student.is_deleted == 0, + ).first() + +def update_student(student_id: int, data: dict, db: Session): + # 第一步:先查询学生 + student = get_student_by_id(student_id, db) + + # 没有查到就返回 None + if not student: + return None + + try: + # 第二步:循环修改前端传来的字段 + # field 是字段名,value 是新的值 + for field, value in data.items(): + setattr(student, field, value) + + # 第三步:提交修改并重新读取数据 + db.commit() + db.refresh(student) + return student + + except Exception as e: + # 修改失败时回滚 + db.rollback() + print("修改学生失败:", e) + return False + +def get_students( + db: Session, + student_no: str | None = None, + student_name: str | None = None, + class_id: int | None = None, +): + # 第一步:只查询没有被删除的学生 + query = db.query(Student).filter(Student.is_deleted == 0) + + # 第二步:前端传了哪个条件,就增加哪个条件 + if student_no: + # 学号使用精确查询 + query = query.filter(Student.student_no == student_no) + + if student_name: + # 姓名使用模糊查询,例如“张”可以查到“张三” + query = query.filter(Student.student_name.like(f"%{student_name}%")) + + if class_id: + # 班级 ID 使用精确查询 + query = query.filter(Student.class_id == class_id) + + # 第三步:执行查询并返回全部结果 + return query.all() + +#逻辑删除学生 +def delete_student(student_id: int, db: Session): + # 第一步:查询学生 + student = get_student_by_id(student_id, db) + + if not student: + return None + + try: + # 第二步:不真正删除记录,只把删除标记改成 1 + student.is_deleted = 1 + + # 第三步:提交修改 + db.commit() + return student + + except Exception as e: + # 删除失败时回滚 + db.rollback() + print("删除学生失败:", e) + return False + diff --git a/schemas/student.py b/schemas/student.py index 213ae53..47f9b13 100644 --- a/schemas/student.py +++ b/schemas/student.py @@ -24,17 +24,17 @@ class StudentCreate(BaseModel): return self class StudentUpdate(BaseModel): - # 1. 默认值为 None 是为了允许省略字段,不代表每个字段都能清空。 - student_no: str | None = Field(default=None, min_length=1, max_length=50) - student_name: str | None = Field(default=None, min_length=1, max_length=50) - class_id: int | None = Field(default=None, gt=0) - consultant_id: int | None = Field(default=None, gt=0) - native_place: str | None = Field(default=None, max_length=100) - graduation_school: str | None = Field(default=None, max_length=100) - major: str | None = Field(default=None, max_length=100) + # 全部设为可选,前端只传需要修改的字段 + student_no: str | None = None + student_name: str | None = None + class_id: int | None = None + consultant_id: int | None = None + native_place: str | None = None + graduation_school: str | None = None + major: str | None = None enrollment_date: date | None = None graduation_date: date | None = None - education: str | None = Field(default=None, max_length=50) - age: int | None = Field(default=None, ge=0, le=150) - gender: str | None = Field(default=None, min_length=1, max_length=10) + education: str | None = None + age: int | None = None + gender: str | None = None From 43b547ece76ddd4996b5d59e6d1becf4a8c09788 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9B=BE=E5=87=AF?= <1368058312@qq.com> Date: Mon, 21 Sep 2026 21:55:04 +0800 Subject: [PATCH 18/23] =?UTF-8?q?api,dao,schemas=E5=B7=B2=E5=AE=8C?= =?UTF-8?q?=E6=88=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/score.py | 51 ++++++++++++++++++++++++++++ dao/score.py | 86 ++++++++++++++++++++++++++++++++++++++++++++++++ schemas/score.py | 36 ++++++++++++++++++++ 3 files changed, 173 insertions(+) diff --git a/api/score.py b/api/score.py index e69de29..a87e4c1 100644 --- a/api/score.py +++ b/api/score.py @@ -0,0 +1,51 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from core.database import get_db +from dao.score import post_score_dao, update_score_dao, get_score_dao, delete_score_dao, exist_student_exam_dao +from schemas.score import ScoreCreate, ScoreUpdate, ScorePageResp + +scores_router = APIRouter(prefix="/scores", tags=["成绩接口"]) + + +@scores_router.post("", summary="新增成绩接口") +def create_score(req: ScoreCreate, db: Session = Depends(get_db)): + # 业务校验:联合唯一 + if exist_student_exam_dao(req.student_id, req.exam_seq, db): + raise HTTPException(status_code=400, detail="该学生本次考试成绩已存在,不能重复录入") + + ok = post_score_dao(req.model_dump(), db) + if not ok: + raise HTTPException(status_code=400, detail="新增成绩失败,请检查学生id是否存在") + return {"msg": "新增成功"} + + +@scores_router.get("", response_model=ScorePageResp, summary="查询成绩接口") +def list_score( + score_id: int|None=None, + student_id: int |None=None, + exam_seq: int|None=None, + page: int = Query(1, ge=1), + page_size: int = Query(10, ge=1, le=100), + db: Session = Depends(get_db) +): + total, data_list = get_score_dao(score_id, student_id, exam_seq, page, page_size, db) + return {"total": total, "data": data_list} + + +@scores_router.put("{score_id}", summary="更新成绩接口") +def modify_score(score_id: int, req: ScoreUpdate, db: Session = Depends(get_db)): + update_dict = req.model_dump(exclude_unset=True) + if not update_dict: + raise HTTPException(status_code=400, detail="没有传入任何修改字段") + ok = update_score_dao(score_id, db, update_dict) + if not ok: + raise HTTPException(status_code=400, detail="修改失败,记录不存在或已删除") + return {"msg": "修改成功"} + + +@scores_router.delete("{score_id}", summary="删除成绩接口") +def remove_score(score_id: int, db: Session = Depends(get_db)): + ok = delete_score_dao(score_id, db) + if not ok: + raise HTTPException(status_code=400, detail="删除失败,记录不存在或已删除") + return {"msg": "逻辑删除成功"} \ No newline at end of file diff --git a/dao/score.py b/dao/score.py index e69de29..6a427d1 100644 --- a/dao/score.py +++ b/dao/score.py @@ -0,0 +1,86 @@ +from sqlalchemy import DECIMAL +from models.score import Score + +def post_score_dao(data_dict, db): + """新增成绩""" + try: + obj = Score(**data_dict) + db.add(obj) + except Exception as e: + print("新增成绩异常:", e) + db.rollback() + return False + else: + db.commit() + return True + + +def update_score_dao(score_id, db, updates): + """修改成绩,query.update方式""" + try: + db.query(Score).filter( + Score.id == score_id, + Score.is_deleted == 0 + ).update(updates) + except Exception: + db.rollback() + return False + else: + db.commit() + return True + + +def get_score_dao(score_id, student_id, exam_seq, page, page_size, db): + """分页查询成绩,多条件过滤,只查未删除""" + q = db.query(Score).filter(Score.is_deleted == 0) + if score_id: + q = q.filter(Score.id == score_id) + if student_id: + q = q.filter(Score.student_id == student_id) + if exam_seq: + q = q.filter(Score.exam_seq == exam_seq) + + total = q.count() + rows = q.offset((page - 1) * page_size).limit(page_size).all() + + res_list = [ + { + "id": i.id, + "student_id": i.student_id, + "score": i.score, + "exam_seq": i.exam_seq, + "exam_date": i.exam_date, + "remark": i.remark, + "created_at": i.created_at, + "updated_at": i.updated_at, + "is_deleted": i.is_deleted + } + for i in rows + ] + return total, res_list + + +def delete_score_dao(score_id, db): + """逻辑删除,更新is_deleted=1""" + try: + db.query(Score).filter( + Score.id == score_id, + Score.is_deleted == 0 + ).update({"is_deleted": 1}) + except Exception as err: + print("删除成绩异常:", err) + db.rollback() + return False + else: + db.commit() + return True + + +def exist_student_exam_dao(student_id, exam_seq, db): + """校验:同一个学生+考试序次是否已经存在,用于联合唯一业务判断""" + rec = db.query(Score).filter( + Score.student_id == student_id, + Score.exam_seq == exam_seq, + Score.is_deleted == 0 + ).first() + return rec is not None \ No newline at end of file diff --git a/schemas/score.py b/schemas/score.py index e69de29..6e766ce 100644 --- a/schemas/score.py +++ b/schemas/score.py @@ -0,0 +1,36 @@ +from pydantic import BaseModel, Field +from datetime import date, datetime +from typing import Optional, List +from decimal import Decimal + + +class ScoreCreate(BaseModel): + student_id: int + exam_seq: int + score: Decimal = Field(ge=0, le=100, description="分数0‑100") + exam_date: Optional[date] = None + remark: Optional[str] = None + + +class ScoreUpdate(BaseModel): + score: Optional[Decimal] = Field(None, ge=0, le=100) + exam_seq: Optional[int] = None + exam_date: Optional[date] = None + remark: Optional[str] = None + + +class ScoreItem(BaseModel): + id: int + student_id: int + score: Decimal + exam_seq: int + exam_date: Optional[date] + remark: Optional[str] + created_at: Optional[datetime] + updated_at: Optional[datetime] + is_deleted: int + + +class ScorePageResp(BaseModel): + total: int + data: List[ScoreItem] \ No newline at end of file From a602dc9de1a782cabbc53819d8cb2025eefcb4dd Mon Sep 17 00:00:00 2001 From: wolin_zhangyi <1141397129@qq.com> Date: Mon, 21 Sep 2026 22:00:46 +0800 Subject: [PATCH 19/23] 1.teacher dao 2.teacher api --- api/teacher.py | 2 +- dao/teacher.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/api/teacher.py b/api/teacher.py index 19e5985..c300850 100644 --- a/api/teacher.py +++ b/api/teacher.py @@ -21,7 +21,7 @@ from dao.teacher import ( router = APIRouter(prefix="/teacher", tags=["老师管理模块"]) -#===========================老师表=============================== +#============================老师表=============================== @router.get("/list", response_model=List[TeacherOut]) def api_get_teacher_list(db: Session = Depends(get_db)): """ diff --git a/dao/teacher.py b/dao/teacher.py index 239c3d5..bef465c 100644 --- a/dao/teacher.py +++ b/dao/teacher.py @@ -9,7 +9,7 @@ from fastapi import HTTPException ''' 本文件内容为老师表与班级老师关系表的增删改查函数集合 ''' -#================老师表Teacher的CRUD================ +#=================老师表Teacher的CRUD================ #查询所有未被删除的老师 def get_teacher_list(db:Session, teacher_id:int): teacher = get_teacher_by_id(db, teacher_id) From b489c0d87034be86b8871fdca403a8a0b6172f6f Mon Sep 17 00:00:00 2001 From: Windows <2295777569@qq.com> Date: Mon, 21 Sep 2026 22:03:30 +0800 Subject: [PATCH 20/23] =?UTF-8?q?api,models=20consultant.py=E5=B7=B2?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=AE=8C=E6=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/consultant.py | 12 +++++------- models/consultant.py | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/api/consultant.py b/api/consultant.py index 2e9a897..59be5b5 100644 --- a/api/consultant.py +++ b/api/consultant.py @@ -9,8 +9,7 @@ router = APIRouter() @router.get( path="/consultants", - tags=['顾问管理模块'] -) + tags=['顾问管理模块'],summary='顾问模糊分页查询') def read_consultants( db: Session = Depends(get_db), page:int=1, @@ -22,7 +21,7 @@ def read_consultants( -@router.post("/consultants", response_model=ConsultantResponse,tags=['顾问管理模块']) +@router.post("/consultants", response_model=ConsultantResponse,tags=['顾问管理模块'],summary='顾问增加') def create_consultant( obj_in: ConsultantCreate, db: Session = Depends(get_db) @@ -30,7 +29,7 @@ def create_consultant( return dao_consultant.create_consultant(db, obj_in) -@router.get("/consultants/{id}", response_model=ConsultantResponse,tags=['顾问管理模块']) +@router.get("/consultants/{id}", response_model=ConsultantResponse,tags=['顾问管理模块'],summary='顾问查询') def read_consultant(id: int, db: Session = Depends(get_db)): res = dao_consultant.get_consultant_by_id(db, cid=id) if res is None: @@ -41,8 +40,7 @@ def read_consultant(id: int, db: Session = Depends(get_db)): @router.put( path="/consultants/{id}", response_model=ConsultantResponse, - tags=['顾问管理模块'] -) + tags=['顾问管理模块'],summary='顾问更新') def update_consultant( id: int, obj_in: ConsultantUpdate, @@ -57,7 +55,7 @@ def update_consultant( -@router.delete("/consultants/{id}", response_model=ConsultantResponse,tags=['顾问管理模块']) +@router.delete("/consultants/{id}", response_model=ConsultantResponse,tags=['顾问管理模块'],summary='顾问删除') def delete_consultant(id: int, db: Session = Depends(get_db)): res = dao_consultant.delete_consultant(db, cid=id) if res is None: diff --git a/models/consultant.py b/models/consultant.py index 5f6f4ed..94702d7 100644 --- a/models/consultant.py +++ b/models/consultant.py @@ -8,7 +8,7 @@ class Consultant(Base): id = Column(Integer, primary_key=True, autoincrement=True, comment="主键") consultant_no = Column(String(50), unique=True, nullable=False, comment="顾问编号") consultant_name = Column(String(50), nullable=False, comment="顾问姓名") - phone = Column(String(11), nullable=True, comment="联系电话") + phone = Column(String(20), nullable=True, comment="联系电话") email = Column(String(100), nullable=True, comment="邮箱") remark = Column(String(255), nullable=True, comment="备注") From 4225c19c07d704cc004ef1e1d93323a17c9dd23f Mon Sep 17 00:00:00 2001 From: nfy <2425896042@qq.com> Date: Mon, 21 Sep 2026 22:04:35 +0800 Subject: [PATCH 21/23] test --- api/employment.py | 67 +++++------------- dao/employment.py | 160 +++++++++++++++--------------------------- schemas/employment.py | 51 ++++++-------- 3 files changed, 91 insertions(+), 187 deletions(-) diff --git a/api/employment.py b/api/employment.py index 48b61d2..9ea1a0c 100644 --- a/api/employment.py +++ b/api/employment.py @@ -1,13 +1,6 @@ -# APIRouter:路由对象,用来分组接口 from fastapi import APIRouter, Depends, HTTPException, Query -# Session:数据库会话类型注解 from sqlalchemy.orm import Session -# 类型提示:可选参数 -from typing import Optional - -# 导入get_db,数据库会话依赖函数,每次请求生成/释放数据库连接 from core.database import get_db -# 导入dao层全部数据库操作函数 from dao.employment import ( create_employment, get_employment_by_id, @@ -16,75 +9,54 @@ from dao.employment import ( update_employment, delete_employment_logic ) -# 导入pydantic请求体、响应体模型 from schemas.employment import ( EmploymentCreate, EmploymentUpdate, - EmploymentResponse, - EmploymentListResponse + EmploymentResponse ) employment_router = APIRouter(prefix="/employments", tags=["就业管理模块"]) -@employment_router.post("", response_model=EmploymentResponse, summary="新增就业信息") +@employment_router.post("/", response_model=EmploymentResponse, summary="新增就业信息") def add_employment(body: EmploymentCreate, db: Session = Depends(get_db)): """ 新增就业信息 - - 一个学生只能保存一条就业记录(数据库唯一约束控制) - - student_id必须在学生表student_info_detail中真实存在,否则外键报错 - :param body:POST请求的JSON请求体,自动解析为EmploymentCreate对象 - :param db: Depends(get_db),依赖注入,自动获取数据库会话,请求结束自动关闭连接 - :return: 返回EmploymentResponse结构数据给前端 + - 一个学生只能保存一条就业记录 + - student_id必须在学生表student_info_detail中真实存在 """ - # 调用dao层新增函数 res = create_employment(db, body) - # dao返回None,代表新增出错(外键不存在/学生重复/数据库异常) if res is None: - # 抛出HTTP异常,500状态码,返回提示信息给前端/Swagger raise HTTPException(status_code=500, detail="新增失败,学生id不存在或该学生已有就业记录") - # 成功,直接返回ORM对象,FastAPI会按照response_model自动转为json返回 return res @employment_router.get("/students/{student_id}", response_model=EmploymentResponse, summary="查询指定学生的就业信息") def query_student_employment(student_id: int, db: Session = Depends(get_db)): - """ - 根据学生id查询该学生的就业记录 - :param student_id: url路径中的路径参数,学生id - :param db:数据库会话 - """ - # 调用dao,按学生id查询 + """根据学生id获取对应就业记录""" record = get_employment_by_student_id(db, student_id) - # 如果查询结果为空,抛出404找不到资源 if not record: raise HTTPException(status_code=404, detail="未找到该学生的就业信息") return record -@employment_router.get("", response_model=EmploymentListResponse, summary="就业信息列表,支持筛选") +@employment_router.get("/", summary="就业信息列表,支持筛选") def query_employment_list( - # Query:获取url中?后面的查询参数;description显示在Swagger文档 - company_name: Optional[str] = Query(None, description="公司名称模糊查询"), - salary_min: Optional[float] = Query(None, description="最低薪资"), - salary_max: Optional[float] = Query(None, description="最高薪资"), - skip: int = Query(0, ge=0, description="偏移量"), # ge=0参数校验,不能传负数 - limit: int = Query(20, ge=1, le=100, description="每页条数"), # 1‑100条限制,防止一次性查大量数据 + company_name: str | None = Query(None, description="公司名称模糊查询"), + salary_min: float | None = Query(None, description="最低薪资"), + salary_max: float | None = Query(None, description="最高薪资"), + skip: int = Query(0, ge=0, description="偏移量"), + limit: int = Query(20, ge=1, le=100, description="每页条数"), db: Session = Depends(get_db) ): - """就业分页列表接口,支持公司名模糊,薪资区间筛选""" - # 调用dao层列表查询,拿到记录列表和总条数 + # 调用dao列表查询,拿到数据字典列表和总条数 data_list, total = get_employment_list(db, company_name, salary_min, salary_max, skip, limit) - # 返回符合EmploymentListResponse格式的字典,FastAPI自动序列化 return {"total": total, "data": data_list} @employment_router.get("/{eid}", response_model=EmploymentResponse, summary="根据id查询就业详情") def query_employment_detail(eid: int, db: Session = Depends(get_db)): - """ - 根据就业主键id查询单条就业详情 - :param eid: url路径参数,就业记录主键 - """ + """根据就业主键id查询单条就业记录""" record = get_employment_by_id(db, eid) if not record: raise HTTPException(status_code=404, detail="就业记录不存在或已被逻辑删除") @@ -93,11 +65,7 @@ def query_employment_detail(eid: int, db: Session = Depends(get_db)): @employment_router.put("/{eid}", response_model=EmploymentResponse, summary="修改就业信息") def edit_employment(eid: int, body: EmploymentUpdate, db: Session = Depends(get_db)): - """ - 修改就业信息;请求体只传需要修改的字段即可,不需要全部传 - :param eid:待修改记录id,路径参数 - :param body:PUT请求的JSON请求体 - """ + """修改就业记录,只传想要修改的字段即可""" res = update_employment(db, eid, body) if res is None: raise HTTPException(status_code=404, detail="修改失败,记录不存在") @@ -106,12 +74,9 @@ def edit_employment(eid: int, body: EmploymentUpdate, db: Session = Depends(get_ @employment_router.delete("/{eid}", summary="逻辑删除就业记录") def logic_delete_employment(eid: int, db: Session = Depends(get_db)): - """ - 逻辑删除接口:不会物理删除数据库行,只设置is_deleted=1 - DELETE请求,RESTful风格 - """ + """执行逻辑删除,is_deleted置1,不会真正删除数据库行""" ok = delete_employment_logic(db, eid) if not ok: raise HTTPException(status_code=404, detail="删除失败,记录不存在") - # 返回简单json提示成功 + # 删除成功返回提示字典 return {"code": 200, "msg": "逻辑删除成功"} diff --git a/dao/employment.py b/dao/employment.py index 2812d10..d760185 100644 --- a/dao/employment.py +++ b/dao/employment.py @@ -1,165 +1,115 @@ -# 导入sqlalchemy会话对象,数据库会话,所有数据库操作都依靠Session from sqlalchemy.orm import Session -# 类型提示:可选对象、列表 -from typing import Optional, List -# 导入数据库ORM模型 from models.employment import Employment -# 导入pydantic入参模型 -from schemas.employment import EmploymentCreate, EmploymentUpdate +from schemas.employment import EmploymentCreate, EmploymentUpdate, EmploymentResponse -def create_employment(db: Session, obj_in: EmploymentCreate) -> Optional[Employment]: - """ - 新增就业信息:一个学生只能存在一条就业记录 - :param db: 数据库会话对象,由FastAPI依赖注入传入 - :param obj_in: 新增的入参对象 EmploymentCreate - :return: 成功返回ORM对象,发生任何异常返回None - """ +def create_employment(db, obj_in): + """新增就业信息:一个学生只能存在一条就业记录""" try: - # model_dump()把pydantic对象转字典;**解包字典,赋值给Employment模型字段,创建ORM对象 + # obj_in.model_dump()把pydantic请求对象转为字典,解包传给Employment生成ORM对象 db_obj = Employment(**obj_in.model_dump()) - # 将对象加入会话,此时还没有写入数据库 + # add:把对象加入数据库会话,此时还没写入数据库 db.add(db_obj) - # 提交事务,真正执行insert SQL写入数据库 + # commit:提交事务,真正写入mysql数据库 db.commit() - # db.refresh:从数据库刷新对象,回填数据库自动生成的id等字段到db_obj + # refresh:从数据库刷新对象,拿到数据库自动生成的id、默认时间等字段 db.refresh(db_obj) - return db_obj + # model_validate:把sqlalchemy ORM对象解析进pydantic响应模型 + # model_dump:把pydantic模型转为普通python字典返回给上层api + return EmploymentResponse.model_validate(db_obj).model_dump() except Exception: - # 只要出现任意异常,执行回滚,撤销本次会话所有未提交操作,防止脏数据残留 + # 发生任何异常,执行回滚,撤销本次会话中未提交的数据库改动 db.rollback() + # 异常情况返回None,上层api可以判断新增失败 return None -def get_employment_by_id(db: Session, eid: int) -> Optional[Employment]: - """ - 根据主键id查询就业详情,过滤逻辑删除的数据 - :param db: 数据库会话 - :param eid:就业记录主键id - :return:查到返回ORM对象;没找到/异常返回None - """ +def get_employment_by_id(db, eid): + """根据主键id查询就业详情,过滤逻辑删除""" try: - # 查询Employment表,条件:id等于eid,并且is_deleted=0(没有被逻辑删除) - return db.query(Employment).filter( + db_obj = db.query(Employment).filter( Employment.id == eid, Employment.is_deleted == 0 - ).first() # first()获取第一条结果,没有匹配返回None + ).first() + if not db_obj: + return None + return EmploymentResponse.model_validate(db_obj).model_dump() except Exception: return None -def get_employment_by_student_id(db: Session, student_id: int) -> Optional[Employment]: - """ - 根据学生id查询就业信息,过滤逻辑删除 - :param db:数据库会话 - :param student_id:学生主键id - :return:查到返回ORM对象,否则None - """ +def get_employment_by_student_id(db, student_id): + """根据学生id查询就业信息,过滤逻辑删除""" try: - return db.query(Employment).filter( + db_obj = db.query(Employment).filter( Employment.student_id == student_id, Employment.is_deleted == 0 ).first() + if not db_obj: + return None + return EmploymentResponse.model_validate(db_obj).model_dump() except Exception: return None -def get_employment_list( - db: Session, - company_name: Optional[str] = None, - salary_min: Optional[float] = None, - salary_max: Optional[float] = None, - skip: int = 0, - limit: int = 20 -) -> tuple[List[Employment], int]: - """ - 就业列表查询;支持公司名模糊、薪资范围筛选;只查询is_deleted=0正常数据 - :param db:数据库会话 - :param company_name:公司名称,模糊查询,不传不做过滤 - :param salary_min:薪资下限,不传不做过滤 - :param salary_max:薪资上限,不传不做过滤 - :param skip:分页偏移量,跳过多少条 - :param limit:分页每页最多取多少条 - :return:元组 (记录列表,总条数);异常返回 [],0 - """ +def get_employment_list(db, company_name=None, salary_min=None, salary_max=None, skip=0, limit=20): + """就业列表查询;支持公司名模糊、薪资范围筛选;只查询is_deleted=0""" try: - # 构建基础查询对象,只查询未被逻辑删除的数据 query = db.query(Employment).filter(Employment.is_deleted == 0) - - # 如果传入公司名,增加模糊匹配条件,%代表通配符 if company_name: query = query.filter(Employment.company_name.like(f"%{company_name}%")) - - # 如果传入最低薪资,增加薪资>=下限条件 if salary_min is not None: query = query.filter(Employment.salary >= salary_min) - - # 如果传入最高薪资,增加薪资<=上限条件 if salary_max is not None: query = query.filter(Employment.salary <= salary_max) - - # count()统计符合条件的总记录数,用于前端分页 + # count()统计满足条件总条数,用于分页total total = query.count() - # offset跳过skip条,limit最多取limit条,执行查询拿到结果列表 + # offset偏移量(跳过多少条),limit每页多少条,执行查询拿到结果列表 records = query.offset(skip).limit(limit).all() - return records, total + # 列表推导式:循环每一条ORM记录,全部转为字典,得到字典列表 + dict_list = [EmploymentResponse.model_validate(item).model_dump() for item in records] + # 返回字典列表 + 总条数 + return dict_list, total except Exception: - # 异常返回空列表、总条数0 + # 异常返回空列表,总条数0 return [], 0 -def update_employment(db: Session, eid: int, obj_in: EmploymentUpdate) -> Optional[Employment]: - """ - 修改就业信息,只更新前端传入的字段 - :param db:数据库会话 - :param eid:待修改记录主键id - :param obj_in:修改入参对象 EmploymentUpdate - :return:成功返回更新后的ORM对象;记录不存在/异常返回None - """ +def update_employment(db, eid, obj_in): + """修改就业信息,只更新传入的字段""" try: - # 先根据id查询这条记录(自动过滤逻辑删除) - db_obj = get_employment_by_id(db, eid) - # 如果记录不存在,直接返回None - if not db_obj: + # 根据id查询未删除的记录 + db_obj_raw = db.query(Employment).filter( + Employment.id == eid, + Employment.is_deleted == 0 + ).first() + # 记录不存在直接返回None + if db_obj_raw is None: return None - - # exclude_unset=True:只拿到前端实际传过来的字段,没有传的字段不会出现在字典中 + # exclude_unset=True:只取出前端实际传过来的字段,不会带上没传的None字段 update_dict = obj_in.model_dump(exclude_unset=True) - - # 循环字典,把每一个key‑value赋值给ORM对象属性 for key, value in update_dict.items(): - setattr(db_obj, key, value) - - # 提交事务,把修改写入数据库 + setattr(db_obj_raw, key, value) db.commit() - # 刷新对象,同步数据库最新数据到对象 - db.refresh(db_obj) - return db_obj + db.refresh(db_obj_raw) + return EmploymentResponse.model_validate(db_obj_raw).model_dump() except Exception: - # 发生异常回滚,放弃修改 db.rollback() return None -def delete_employment_logic(db: Session, eid: int) -> bool: - """ - 逻辑删除,只修改is_deleted=1,**不做物理删除db.delete()**,符合项目文档要求 - :param db:数据库会话 - :param eid:待删除记录id - :return:成功返回True;记录不存在/异常返回False - """ +def delete_employment_logic(db, eid): + """逻辑删除,只修改is_deleted=1,不做物理删除""" try: - # 查询这条记录,过滤已逻辑删除 - db_obj = get_employment_by_id(db, eid) - # 记录不存在返回False - if not db_obj: + db_obj_raw = db.query(Employment).filter( + Employment.id == eid, + Employment.is_deleted == 0 + ).first() + if db_obj_raw is None: return False - # 修改逻辑删除标记为1 - db_obj.is_deleted = 1 - # 提交事务,把修改写入数据库 + db_obj_raw.is_deleted = 1 db.commit() return True except Exception: - # 异常回滚,返回False db.rollback() return False diff --git a/schemas/employment.py b/schemas/employment.py index 2eb203d..bfe27ed 100644 --- a/schemas/employment.py +++ b/schemas/employment.py @@ -1,31 +1,27 @@ from pydantic import BaseModel, Field -from typing import Optional -from datetime import date,datetime +from datetime import date, datetime from decimal import Decimal -# int class EmploymentCreate(BaseModel): """新增就业信息 请求体 Schema""" - # 学生ID 必填,一个学生只能有一条就业记录 student_id: int = Field(..., description="关联学生ID,必填") - # 就业状态必填,限定可选值业务枚举 - employment_status: str = Field(None, description="就业状态:not_started/job_hunting/offered/employed") - employment_open_date: Optional[date] = Field(None, description="就业开放时间,选填") - offer_date: Optional[date] = Field(None, description="offer下发时间,选填") - company_name: Optional[str] = Field(None, max_length=100, description="就业公司名称,选填") - salary: Optional[Decimal] = Field(None, ge=0, description="就业薪资,不能负数,选填") - remark: Optional[str] = Field(None, max_length=255, description="备注,选填") + employment_status: str | None = Field(None, description="就业状态:not_started/job_hunting/offered/employed") + employment_open_date: date | None = Field(None, description="就业开放时间,选填") + offer_date: date | None = Field(None, description="offer下发时间,选填") + company_name: str | None = Field(None, max_length=100, description="就业公司名称,选填") + salary: Decimal | None = Field(None, ge=0, description="就业薪资,不能负数,选填") + remark: str | None = Field(None, max_length=255, description="备注,选填") class EmploymentUpdate(BaseModel): """修改就业信息 请求体 Schema:全部字段可选,只传要修改的字段""" - employment_status: Optional[str] = Field(None, description="就业状态") - employment_open_date: Optional[date] = Field(None, description="就业开放时间") - offer_date: Optional[date] = Field(None, description="offer下发时间") - company_name: Optional[str] = Field(None, max_length=100, description="就业公司名称") - salary: Optional[Decimal] = Field(None, ge=0, description="就业薪资") - remark: Optional[str] = Field(None, max_length=255, description="备注") + employment_status: str | None = Field(None, description="就业状态") + employment_open_date: date | None = Field(None, description="就业开放时间") + offer_date: date | None = Field(None, description="offer下发时间") + company_name: str | None = Field(None, max_length=100, description="就业公司名称") + salary: Decimal | None = Field(None, ge=0, description="就业薪资") + remark: str | None = Field(None, max_length=255, description="备注") class EmploymentResponse(BaseModel): @@ -33,17 +29,10 @@ class EmploymentResponse(BaseModel): id: int student_id: int employment_status: str - employment_open_date: Optional[date] - offer_date: Optional[date] - company_name: Optional[str] - salary: Optional[Decimal] - remark: Optional[str] - created_at: date - updated_at: date - - class Config: - from_attributes = True - -class EmploymentListResponse(BaseModel): - total: int - data: list[EmploymentResponse] + employment_open_date: date | None + offer_date: date | None + company_name: str | None + salary: Decimal | None + remark: str | None + created_at: datetime | None + updated_at: datetime | None From e282f3f7dbe54ef1933bd7aea918222b237f5e2b Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Tue, 22 Sep 2026 09:17:20 +0800 Subject: [PATCH 22/23] =?UTF-8?q?=E5=8F=96=E6=B6=88=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/main.py b/main.py index a145808..025c7d8 100644 --- a/main.py +++ b/main.py @@ -1,7 +1,7 @@ from fastapi import FastAPI from api.example import example_router from middleware import log_middleware -# from api.statistics import statistics_router +from api.statistics import statistics_router import uvicorn from api.student import student_router app = FastAPI() @@ -19,8 +19,8 @@ app.include_router(student_router,tags=['学生接口'],prefix="/students") from api import consultant app.include_router(consultant.router) #-------------张昕浩--------------- -# app.include_router(example_router) -# app.include_router(statistics_router, prefix="/statistics") +app.include_router(example_router) +app.include_router(statistics_router, prefix="/statistics") #-------------曾凯--------------- From d24937ecc3c125cba7c0b660543166ae6e0171e3 Mon Sep 17 00:00:00 2001 From: lookerzz <1031516901@qq.com> Date: Tue, 22 Sep 2026 09:50:57 +0800 Subject: [PATCH 23/23] =?UTF-8?q?=E6=B7=BB=E5=8A=A0statistics=20tag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/statistics.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/statistics.py b/api/statistics.py index 764f04b..d139b83 100644 --- a/api/statistics.py +++ b/api/statistics.py @@ -8,7 +8,7 @@ from schemas.common import SuccessResponse from sqlalchemy.orm import Session -statistics_router=APIRouter() +statistics_router=APIRouter(tags=['statistics']) @statistics_router.get("/students/age-over-30",response_model=SuccessResponse)