24 Commits
Author SHA1 Message Date
wolin_liyujie 56df0ef7b0 删除了冗余查询接口,查询接口新增查询枚举性别功能 2026-09-23 20:10:52 +08:00
wolin_liyujie d90a89595f Merge branch 'main' of http://47.106.207.27:3000/lookerZz/DoubaoTeam into LI_YUJIE 2026-09-23 20:02:36 +08:00
lookerZz 52452e378c Merge pull request '修改请求体' (#52) from zhutt into main
Reviewed-on: #52
2026-09-23 19:52:53 +08:00
lookerZz 4f90c6a5c3 Merge pull request '答辩完成后,整合get接口,添加一个误删除回档功能。' (#54) from nanfangyu0824 into main
Reviewed-on: #54
2026-09-23 19:52:39 +08:00
lookerZz 1be706c267 Merge pull request '已优化get方法以及修改delete删除过后还显示删除成功的问题' (#53) from zk_wolin into main
Reviewed-on: #53
2026-09-23 19:52:25 +08:00
wolin_nanfangyu_mj 6033504d76 答辩完成后,整合get接口,添加一个误删除回档功能。 2026-09-23 19:30:33 +08:00
曾凯 9c24a229c7 已优化get方法以及修改delete删除过后还显示删除成功的问题 2026-09-23 17:59:04 +08:00
lookerZz fd3e52f4ff Merge pull request '1' (#51) from zk_wolin into main
Reviewed-on: #51
2026-09-23 10:18:39 +08:00
曾凯 1933466e3d 1 2026-09-23 10:16:06 +08:00
wolin_liyujie b14d316d0a Merge branch 'main' of http://47.106.207.27:3000/lookerZz/DoubaoTeam into LI_YUJIE 2026-09-23 09:55:52 +08:00
lookerZz 9a0f2d533a Merge pull request '修改请求体' (#50) from zhutt into main
Reviewed-on: #50
2026-09-23 09:18:37 +08:00
wolin_liyujie 6016a99bcf 规范接口方法写法 2026-09-22 20:43:10 +08:00
wolin_liyujie b3d557e4fc Merge branch 'main' of http://47.106.207.27:3000/lookerZz/DoubaoTeam into LI_YUJIE 2026-09-22 20:17:59 +08:00
lookerZz bca68a56f4 Merge pull request '将两个get函数合并成一个' (#49) from zhutt into main
Reviewed-on: #49
2026-09-22 19:25:45 +08:00
lookerZz ca427bc8d2 Merge pull request '0922.18:35employment接口合并' (#48) from nanfangyu0824 into main
Reviewed-on: #48
2026-09-22 19:24:51 +08:00
wolin_liyujie e65d7cd50b Merge branch 'main' of http://47.106.207.27:3000/lookerZz/DoubaoTeam into LI_YUJIE 2026-09-22 19:22:46 +08:00
wolin_nanfangyu_mj d0c3abbebb 0922.18:35employment接口合并 2026-09-22 18:33:50 +08:00
lookerZz 49e651b9f3 Merge pull request '添加statistics tag' (#47) from 张昕浩 into main
Reviewed-on: #47
2026-09-22 18:10:28 +08:00
lookerZz 93f2356c5d 添加statistics tag 2026-09-22 18:10:06 +08:00
lookerZz 505e59280e Merge pull request '修改响应体和注释' (#46) from zhutt into main
Reviewed-on: #46
2026-09-22 17:57:18 +08:00
lookerZz 5c873491c6 Merge pull request '添加statistics tag' (#45) from 张昕浩 into main
Reviewed-on: #45
2026-09-22 17:54:00 +08:00
lookerZz 6e0185d38a Merge pull request '修改响应体' (#44) from zhutt into main
Reviewed-on: #44
2026-09-22 17:53:26 +08:00
lookerZz 4d59d79812 添加statistics tag 2026-09-22 17:52:51 +08:00
wolin_liyujie 99eab62ab7 规范接口方法写法 2026-09-22 16:48:45 +08:00
14 changed files with 710 additions and 150 deletions
+36 -24
View File
@@ -6,13 +6,14 @@ from dao.employment import (create_employment,
get_employment_by_student_id,
get_employment_list,
update_employment,
delete_employment_logic)
delete_employment_logic,
recover_employment_delete)
from schemas.employment import (EmploymentCreate,
EmploymentUpdate,
EmploymentResponse)
employment_router = APIRouter(prefix="/employments", tags=["就业管理模块"])
employment_router = APIRouter( tags=["就业管理模块"])
@employment_router.post("/", response_model=EmploymentResponse, summary="新增就业信息")
@@ -28,36 +29,38 @@ def add_employment(body: EmploymentCreate, db: Session = Depends(get_db)):
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获取对应就业记录"""
record = get_employment_by_student_id(db, student_id)
if not record:
raise HTTPException(status_code=404, detail="未找到该学生的就业信息")
return record
#
# @employment_router.get("/students/{student_id}", response_model=EmploymentResponse, summary="查询指定学生的就业信息")
# def query_student_employment(student_id: int, db: Session = Depends(get_db)):
# """根据学生id获取对应就业记录"""
# record = get_employment_by_student_id(db, student_id)
# if not record:
# raise HTTPException(status_code=404, detail="未找到该学生的就业信息")
# return record
@employment_router.get("/", summary="就业信息列表,支持筛选")
def query_employment_list(
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)
):
def query_employment_list(eid:int| None = Query(None, description="就业编号"),
student_id:int| None = Query(None,description="学生id"),
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列表查询,拿到数据字典列表和总条数
data_list, total = get_employment_list(db, company_name, salary_min, salary_max, skip, limit)
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查询单条就业记录"""
record = get_employment_by_id(db, eid)
if not record:
raise HTTPException(status_code=404, detail="就业记录不存在或已被逻辑删除")
return record
# @employment_router.get("/{eid}", response_model=EmploymentResponse, summary="根据id查询就业详情")
# def query_employment_detail(eid: int, db: Session = Depends(get_db)):
# """根据就业主键id查询单条就业记录"""
# 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="修改就业信息")
@@ -77,3 +80,12 @@ def logic_delete_employment(eid: int, db: Session = Depends(get_db)):
raise HTTPException(status_code=404, detail="删除失败,记录不存在")
# 删除成功返回提示字典
return {"code": 200, "msg": "逻辑删除成功"}
@employment_router.get("/{eid}", summary="回档逻辑删除就业记录")
def recover_delete_employment(eid: int, db: Session = Depends(get_db)):
"""执行逻辑删除,is_deleted置1,不会真正删除数据库行"""
ok = recover_employment_delete(db, eid)
if not ok:
raise HTTPException(status_code=404, detail="回档失败,记录已经存在")
# 删除成功返回提示字典
return {"code": 200, "msg": "回档成功"}
+1 -9
View File
@@ -1,7 +1,7 @@
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, get_all_scores_dao,delete_score_dao, exist_student_exam_dao
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=["成绩接口"])
@@ -32,14 +32,6 @@ def list_score(
@scores_router.get("/all", response_model=ScorePageResp,
summary="所有学生成绩接口",
)
def list_all_scores(page: int = Query(1, ge=1),
page_size: int = Query(10, ge=1, le=100),
db: Session = Depends(get_db)):
total, data_list = get_all_scores_dao(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)):
+36 -34
View File
@@ -3,11 +3,11 @@ from fastapi import APIRouter, Depends, HTTPException,Query,Path
from sqlalchemy.orm import Session
from core.database import get_db
from schemas.student import StudentCreate, StudentUpdate
from schemas.student import StudentCreate, StudentUpdate, Gender
from dao import student as student_dao
student_router = APIRouter()
@student_router.put("",summary='添加学生')
@student_router.post("",summary='添加学生')
def create_student(request:StudentCreate,db:Session = Depends(get_db)):
d = request.model_dump()
r = student_dao.create_students(d,db)
@@ -15,7 +15,7 @@ def create_student(request:StudentCreate,db:Session = Depends(get_db)):
raise HTTPException(status_code=500,detail='服务器繁忙,请稍后添加!')
return {'code':200,'detail':"添加学生成功"}
@student_router.post("/{id}", summary="更新学生信息")
@student_router.put("/{id}", summary="更新学生信息")
def update_student(
id: int,
request: StudentUpdate,
@@ -51,6 +51,7 @@ def update_student(
@student_router.get("", summary="分页查询学生列表")
def get_students(
# 三个查询条件都可以不传
gender:Gender = Query(default=None),
student_no: str | None = Query(default=None,description="按学号精确查询"),
student_name: str | None = Query(default=None,description="按名字模糊查询"),
class_id: int | None = Query(default=None,gt=0,description="按班级号精确查询"),
@@ -67,6 +68,7 @@ def get_students(
class_id=class_id,
page=page,
page_size=page_size,
gender = gender
)
data = [
{
@@ -95,37 +97,37 @@ def get_students(
"data": data,
}
@student_router.get("/{id}", summary="查询学生详情")
def get_student(id: int = Path(gt=0),
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="学生不存在")
result = {
"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,
}
return {
"code": 200,
"detail": "查询成功",
"data": result,
}
# @student_router.get("/{id}", summary="查询学生详情")
# def get_student(id: int = Path(gt=0),
# 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="学生不存在")
#
# result = {
# "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,
# }
#
# return {
# "code": 200,
# "detail": "查询成功",
# "data": result,
# }
@student_router.delete("/{id}", summary="逻辑删除学生")
def delete_student(id: int, db: Session = Depends(get_db)):
+106 -31
View File
@@ -1,17 +1,28 @@
from sqlalchemy.orm import Session
from models.employment import Employment
from schemas.employment import EmploymentCreate, EmploymentUpdate, EmploymentResponse
from schemas.employment import EmploymentResponse
def create_employment(db, obj_in):
def create_employment(db, e):
# obj_in是Pydantic对象(EmploymentCreate)
"""新增就业信息:一个学生只能存在一条就业记录"""
try:
db_obj = Employment(**obj_in.model_dump())
db.add(db_obj) # add:把对象加入数据库会话,此时还没写入数据库
db.commit() # commit:提交事务,真正写入mysql数据库
db.refresh(db_obj) # refresh:从数据库刷新对象,拿到数据库自动生成的id、默认时间等字段
return EmploymentResponse.model_validate(db_obj).model_dump()
# model_validate:把sqlalchemy ORM对象解析进pydantic响应模型
db_obj = Employment(**e.model_dump())
db.add(db_obj)
db.commit()
db.refresh(db_obj)
resp = EmploymentResponse(id=db_obj.id
,student_id=db_obj.student_id
,employment_status=db_obj.employment_status
,employment_open_date=db_obj.employment_open_date
,offer_date=db_obj.offer_date
,company_name=db_obj.company_name
,salary=db_obj.salary
,remark=db_obj.remark
,created_at=db_obj.created_at
,updated_at=db_obj.updated_at)
return resp.model_dump()
except Exception:
db.rollback()
return None
@@ -24,7 +35,18 @@ def get_employment_by_id(db, eid):
,Employment.is_deleted == 0).first()
if not db_obj:
return None
return EmploymentResponse.model_validate(db_obj).model_dump()
resp = EmploymentResponse(id=db_obj.id
,student_id=db_obj.student_id
,employment_status=db_obj.employment_status
,employment_open_date=db_obj.employment_open_date
,offer_date=db_obj.offer_date
,company_name=db_obj.company_name
,salary=db_obj.salary
,remark=db_obj.remark
,created_at=db_obj.created_at
,updated_at=db_obj.updated_at)
return resp.model_dump()
except Exception:
return None
@@ -36,62 +58,115 @@ def get_employment_by_student_id(db, student_id):
,Employment.is_deleted == 0).first()
if not db_obj:
return None
return EmploymentResponse.model_validate(db_obj).model_dump()
resp = EmploymentResponse(id=db_obj.id
,student_id=db_obj.student_id
,employment_status=db_obj.employment_status
,employment_open_date=db_obj.employment_open_date
,offer_date=db_obj.offer_date
,company_name=db_obj.company_name
,salary=db_obj.salary
,remark=db_obj.remark
,created_at=db_obj.created_at
,updated_at=db_obj.updated_at)
return resp.model_dump()
except Exception:
return None
def get_employment_list(db, company_name=None, salary_min=None, salary_max=None, skip=0, limit=20):
def get_employment_list(db, id=None,student_id=None,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 id is not None:
query = query.filter(Employment.id >= id)
if student_id is not None:
query = query.filter(Employment.student_id >= student_id)
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
total = query.count()
# offset偏移量(跳过多少条),limit每页多少条,执行查询拿到结果列表
records = query.offset(skip).limit(limit).all()
# 列表推导式:循环每一条ORM记录,全部转为字典,得到字典列表
dict_list = [EmploymentResponse.model_validate(item).model_dump() for item in records]
# 返回字典列表 + 总条数
dict_list = []
for item in records:
resp = EmploymentResponse(id=item.id
,student_id=item.student_id
,employment_status=item.employment_status
,employment_open_date=item.employment_open_date
,offer_date=item.offer_date
,company_name=item.company_name
,salary=item.salary
,remark=item.remark
,created_at=item.created_at
,updated_at=item.updated_at)
dict_list.append(resp.model_dump())
return dict_list, total
except Exception:
# 异常返回空列表,总条数0
return [], 0
def update_employment(db, eid, obj_in):
def update_employment(db, eid, e):
"""修改就业信息,只更新传入的字段"""
try:
db_obj_raw = db.query(Employment).filter(Employment.id == eid
,Employment.is_deleted == 0).first()
if db_obj_raw is None:
x = db.query(Employment).filter(Employment.id == eid
,Employment.is_deleted == 0).first()
if x is None:
return None
update_dict = obj_in.model_dump(exclude_unset=True)# exclude_unset=True:只取出前端实际传过来的字段,不会带上没传的None字段
for key, value in update_dict.items():
setattr(db_obj_raw, key, value)
update_dict = e.model_dump(exclude_unset=True)
db.query(Employment).filter(Employment.id == eid
,Employment.is_deleted == 0).update(update_dict)
db.commit()
db.refresh(db_obj_raw)
return EmploymentResponse.model_validate(db_obj_raw).model_dump()
db.refresh(x)
resp = EmploymentResponse(id=x.id
,student_id=x.student_id
,employment_status=x.employment_status
,employment_open_date=x.employment_open_date
,offer_date=x.offer_date
,company_name=x.company_name
,salary=x.salary
,remark=x.remark
,created_at=x.created_at
,updated_at=x.updated_at)
return resp.model_dump()
except Exception:
db.rollback()
return None
return
def delete_employment_logic(db, eid):
"""逻辑删除,只修改is_deleted=1,不做物理删除"""
try:
db_obj_raw = db.query(Employment).filter(Employment.id == eid
y = db.query(Employment).filter(Employment.id == eid
,Employment.is_deleted == 0).first()
if db_obj_raw is None:
if y is None:
return False
db_obj_raw.is_deleted = 1
y.is_deleted = 1
db.commit()
return True
except Exception:
db.rollback()
return False
def recover_employment_delete(db, eid):
"""逻辑删除,只修改is_deleted=1,不做物理删除"""
try:
y = db.query(Employment).filter(Employment.id == eid).first()
if y is None:
return False
y.is_deleted = 0
db.commit()
return True
except Exception:
db.rollback()
return False
+32 -33
View File
@@ -5,23 +5,27 @@ def post_score_dao(data_dict, db):
try:
obj = Score(**data_dict)
db.add(obj)
db.commit()
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(
cnt = db.query(Score).filter(
Score.id == score_id,
Score.is_deleted == 0
).update(updates)
except Exception:
if cnt == 0:
db.rollback()
return False
except Exception as e:
print("修改成绩异常:", e)
db.rollback()
return False
else:
@@ -30,14 +34,25 @@ def update_score_dao(score_id, db, updates):
def get_score_dao(score_id, student_id, exam_seq, page, page_size, db):
"""分页查询成绩,多条件过滤,只查未删除"""
"""分页查询成绩,多条件过滤,只查未删除
规则:
- score_id == 0 → 查所有学生成绩(仅分页,不加 id 过滤)
- 三个筛选条件全为空(None) → 直接返回空,不查库
- 其他情况 → 按传入条件过滤
"""
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)
if score_id == 0:
pass # score_id=0 → 查全部(分页)
elif score_id is None and not student_id and not exam_seq:
return 0, [] # 三个参数都没传 → 返回空
else:
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()
@@ -58,36 +73,20 @@ def get_score_dao(score_id, student_id, exam_seq, page, page_size, db):
]
return total, res_list
def get_all_scores_dao(page, page_size, db):
r = db.query(Score).filter(Score.is_deleted == 0)\
.order_by(Score.student_id, Score.exam_seq)
total = r.count() # 总条数
rows = r.offset((page-1)*page_size).limit(page_size).all()# 分页
res_list_all = [
{
"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_all
def delete_score_dao(score_id, db):
"""逻辑删除,更新is_deleted=1"""
try:
db.query(Score).filter(
cnt = db.query(Score).filter(
Score.id == score_id,
Score.is_deleted == 0
).update({"is_deleted": 1})
).update({"is_deleted": 1}) # ← 接住返回值
if cnt == 0: # ← 关键:一行都没匹配到
db.rollback()
return False
except Exception as err:
print("删除成绩异常:", err)
db.rollback()
+1 -1
View File
@@ -1,6 +1,6 @@
from schemas.statistics import NoExist
from sqlalchemy import func
from time import time
#`GET /statistics/students/age-over-30`
#查询所有超过 30 岁的学员信息
+8
View File
@@ -1,6 +1,9 @@
from models.student import Student
from sqlalchemy.orm import Session
from schemas.student import Gender
#新建学生
def create_students(d: dict, db: Session):
stu = Student(**d)
@@ -62,6 +65,7 @@ def get_students(
class_id: int | None = None,
page: int = 1,
page_size: int = 10,
gender: Gender = None,
):
# 第一步:只查询没有被删除的学生
query = db.query(Student).filter(Student.is_deleted == 0)
@@ -71,6 +75,10 @@ def get_students(
# 学号使用精确查询
query = query.filter(Student.student_no == student_no)
if gender:
#查询枚举性别
query = query.filter(Student.gender == gender)
if student_name:
# 姓名使用模糊查询,例如“张”可以查到“张三”
query = query.filter(Student.student_name.like(f"%{student_name}%"))
+472
View File
@@ -0,0 +1,472 @@
# 学生模块答辩演示步骤
> 演示目标:通过 Swagger 依次展示学生新增、参数校验、分页查询、条件查询、详情查询、局部更新和逻辑删除。
## 一、答辩前检查
### 1. 确认数据库和基础数据
1. 确认 MySQL 已启动。
2. 确认项目配置的数据库可以连接。
3. 确认班级表中至少存在一条未删除的班级记录。
4. 记住一个真实存在的班级 ID。下文示例使用 `class_id = 1`,如果数据库中不存在班级 1,请替换成实际班级 ID。
`consultant_id` 在演示数据中使用 `null`,避免因为顾问外键不存在导致新增失败。
### 2. 确认当前接口方法
当前代码中的学生接口如下:
| 功能 | 请求方法 | 地址 |
| --- | --- | --- |
| 新增学生 | `PUT` | `/students` |
| 更新学生 | `POST` | `/students/{id}` |
| 分页查询 | `GET` | `/students` |
| 详情查询 | `GET` | `/students/{id}` |
| 逻辑删除 | `DELETE` | `/students/{id}` |
注意:常见 REST 规范通常使用 `POST` 新增、`PUT` 更新,但当前项目代码正好相反。答辩前如果修改了请求方法,需要同步调整本文档中的操作。
### 3. 启动项目
在项目根目录执行:
```powershell
python main.py
```
看到服务正常启动后,在浏览器打开:
```text
http://127.0.0.1:23333/docs
```
在 Swagger 页面找到“学生接口”。
## 二、正式演示流程
建议严格按照下面的顺序演示。每次操作时先展开接口,点击 **Try it out**,填写参数或请求体,再点击 **Execute**。
## 步骤 1:演示新增参数校验
展开:
```text
PUT /students
```
先输入一条性别不合法的数据:
```json
{
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2027-06-30",
"education": "本科",
"age": 22,
"gender": "未知"
}
```
预期结果:HTTP 状态码为 `422`,提示 `gender` 只能是“男”或“女”。
讲解词:
> 请求进入接口之前会先经过 Pydantic 请求模型校验。性别字段使用 Literal 约束,只允许男或女,非法数据不会进入 DAO 层,也不会写入数据库。
## 步骤 2:演示日期校验
仍然使用新增接口,将毕业日期设置为早于入学日期:
```json
{
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2024-06-30",
"education": "本科",
"age": 22,
"gender": "男"
}
```
预期结果:HTTP 状态码为 `422`,提示“毕业日期不能早于入学日期”。
讲解词:
> 除了单字段类型和范围校验,我还使用模型级校验器校验两个日期之间的业务关系。
## 步骤 3:新增一名合法学生
继续使用:
```text
PUT /students
```
输入以下合法数据:
```json
{
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2027-06-30",
"education": "本科",
"age": 22,
"gender": "男"
}
```
这里故意不传 `student_no`。DAO 在插入并取得自增主键后,会自动生成类似 `STU0001` 的学生编号。
预期响应:
```json
{
"code": 200,
"detail": "添加学生成功"
}
```
讲解词:
> 新增请求先由 StudentCreate 完成字段校验,再通过 model_dump 转成字典。DAO 创建 ORM 实例并执行 flush 获取自增 ID,如果没有手动传入学号,就根据 ID 自动生成学号,最后提交事务。
## 步骤 4:分页查询并记录演示学生 ID
展开:
```text
GET /students
```
填写:
```text
page = 1
page_size = 5
```
其他查询条件暂时留空,然后执行。
预期结果:
- `code` 为 `200`;
- `total` 表示未删除学生的总数;
- `page` 为 `1`;
- `page_size` 为 `5`;
- `data` 最多返回 5 条;
- 数据按照学生 ID 倒序排列,刚新增的学生通常在第一条。
从返回结果中找到 `student_name` 为“答辩演示学生”的记录,并记下:
```text
id = ________
student_no = ________
```
后续所有 `{id}` 都替换成这里记录的真实学生 ID。
讲解词:
> 查询时先添加 is_deleted 等于 0 的条件,确保逻辑删除的数据不可见;然后在分页之前调用 count 获取符合条件的总数,最后使用 offset 和 limit 完成分页。固定按 ID 倒序可以保证分页顺序稳定。
分页计算公式:
```text
offset = (page - 1) * page_size
```
## 步骤 5:演示姓名模糊查询
继续使用:
```text
GET /students
```
填写:
```text
student_name = 答辩
page = 1
page_size = 10
```
预期结果:只返回姓名中包含“答辩”的未删除学生。
讲解词:
> 查询参数都是可选的。前端传入姓名时,DAO 使用 LIKE 完成模糊查询;没有传入的条件不会拼接到 SQL 中。
也可以补充展示以下任意一种查询:
```text
student_no = 上一步记录的学号
```
或者:
```text
class_id = 1
```
## 步骤 6:查询单个学生详情
展开:
```text
GET /students/{id}
```
将 `id` 填写为步骤 4 记录的学生 ID,然后执行。
预期响应结构:
```json
{
"code": 200,
"detail": "查询成功",
"data": {
"id": 1,
"student_no": "STU0001",
"student_name": "答辩演示学生",
"class_id": 1,
"consultant_id": null,
"native_place": "河南",
"graduation_school": "演示大学",
"major": "软件工程",
"enrollment_date": "2025-09-01",
"graduation_date": "2027-06-30",
"education": "本科",
"age": 22,
"gender": "男"
}
}
```
实际的 `id` 和 `student_no` 以数据库返回为准。
讲解词:
> DAO 使用主键和 is_deleted 等于 0 两个条件查询。API 层手动组装返回字典,只暴露业务需要的字段,不返回 is_deleted、创建时间等内部字段。
## 步骤 7:局部更新学生
展开:
```text
POST /students/{id}
```
将 `id` 填写为步骤 4 记录的学生 ID,请求体只输入需要修改的字段:
```json
{
"age": 23,
"gender": "女"
}
```
预期结果:返回“修改学生成功”,其他未传字段保持不变。
讲解词:
> 更新模型中的字段都是可选字段,接口通过 model_dump 的 exclude_unset 参数只提取前端实际传入的字段,DAO 再使用 setattr 循环完成局部更新。
更新完成后,再执行一次:
```text
GET /students/{id}
```
确认:
```text
age = 23
gender = 女
```
其余字段没有变化。
## 步骤 8:演示更新参数校验
再次调用:
```text
POST /students/{id}
```
输入:
```json
{
"age": 200,
"gender": "其他"
}
```
预期结果:HTTP 状态码为 `422`。
讲解词:
> 年龄限制在 0 到 150 之间,性别限制为男或女。参数校验失败时,请求不会进入更新 DAO,因此数据库中的数据不会被修改。
## 步骤 9:逻辑删除学生
展开:
```text
DELETE /students/{id}
```
填写步骤 4 记录的学生 ID,然后执行。
预期响应:
```json
{
"code": 200,
"detail": "删除学生成功"
}
```
讲解词:
> 删除操作不是物理删除,而是把 is_deleted 更新为 1。这样可以保留历史数据,同时所有正常查询都会通过 is_deleted 等于 0 将其过滤掉。
## 步骤 10:验证逻辑删除
删除后,再执行:
```text
GET /students/{id}
```
预期结果:HTTP 状态码为 `404`:
```json
{
"detail": "学生不存在"
}
```
再执行学生列表查询或姓名模糊查询,也不应该看到刚才删除的学生。
讲解词:
> 数据库记录仍然存在,但详情查询和列表查询都统一增加了 is_deleted 等于 0 的条件,所以业务层已经无法查询到该记录。
## 三、建议的答辩讲解顺序
可以用下面这段话概括学生模块:
> 学生模块采用 API、Schema、DAO 和 Model 分层。Schema 负责请求参数的类型、范围和业务规则校验;API 负责接收请求、处理 HTTP 状态码以及控制响应字段;DAO 负责数据库查询和事务处理;Model 负责学生表与字段的 ORM 映射。列表接口支持学号精确查询、姓名模糊查询、班级查询以及分页,并统一过滤逻辑删除数据。新增支持自动生成学生编号,修改支持局部更新,删除采用逻辑删除。
## 四、常见提问与回答
### 1. 为什么分页之前先执行 count?
因为前端需要知道符合条件的总数据量。`count()` 必须在 `offset` 和 `limit` 之前执行,否则统计到的可能只是当前页的数据量。
### 2. 为什么分页查询要排序?
如果不指定排序,数据库每次返回数据的顺序不一定稳定,可能造成翻页时数据重复或遗漏。当前代码按照学生 ID 倒序排列。
### 3. 为什么使用逻辑删除?
逻辑删除能够保留历史数据,避免误删除后无法恢复。正常查询通过 `is_deleted == 0` 过滤已删除记录。
### 4. 为什么列表和详情要手动组装字典?
手动组装能够明确控制暴露给前端的字段,避免把 `is_deleted` 等数据库内部字段返回给用户。
### 5. 为什么修改接口只更新部分字段?
前端可能只修改年龄或手机号等个别字段。使用 `exclude_unset=True` 只获取实际传入的字段,再通过 `setattr` 更新,可以避免未传字段被错误覆盖。
### 6. 为什么非法参数返回 422?
FastAPI 会先调用 Pydantic 校验请求体。数据类型、字段范围或 Literal 约束不满足时,请求不会进入业务函数,而是直接返回 422。
### 7. 为什么学生编号可以不传?
学生记录执行 `flush()` 后可以取得数据库生成的自增主键,系统再根据主键生成学生编号,能够减少手工编号冲突。
## 五、故障排查
### 请求体报 JSON decode error
检查以下问题:
- 属性名和值必须使用英文双引号;
- 最后一个字段后面不能有逗号;
- JSON 中不能写注释;
- 日期必须使用 `YYYY-MM-DD` 格式。
合法示例:
```json
{
"age": 23,
"gender": "女"
}
```
### 新增返回 500
优先检查:
1. MySQL 是否启动;
2. 数据库连接配置是否正确;
3. `class_id` 是否在班级表中真实存在;
4. 手动填写的 `student_no` 是否与已有记录重复;
5. 如果填写了 `consultant_id`,该顾问是否真实存在。
### 查询详情返回 404
检查:
1. ID 是否填写正确;
2. 该学生是否已经被逻辑删除;
3. 是否误用了列表中的班级 ID 或顾问 ID。
### Swagger 请求体只显示 `{}`
检查 `StudentUpdate` 是否被错误地重复嵌套。正确写法应该只有一层:
```python
class StudentUpdate(BaseModel):
student_name: str | None = None
# 其他字段……
```
## 六、演示结束检查表
- [ ] 服务可以正常启动;
- [ ] Swagger 可以打开;
- [ ] 已确认一个真实存在的班级 ID;
- [ ] 性别非法数据返回 422;
- [ ] 日期非法数据返回 422;
- [ ] 合法学生新增成功;
- [ ] 已从列表中记录新增学生 ID 和学号;
- [ ] 分页和条件查询成功;
- [ ] 详情查询不暴露内部字段;
- [ ] 局部更新后未传字段保持不变;
- [ ] 逻辑删除成功;
- [ ] 删除后详情查询返回 404。
+1 -1
View File
@@ -16,7 +16,7 @@ def llm_func(input_user):
base_url="https://api.deepseek.com"
)
prom_path = Path(__file__).parent / "prom.txt"
prom_path = Path(__file__).parent / "prom.txt" #拼接路径
context = prom_path.read_text(encoding="utf-8")
+1 -1
View File
@@ -7,7 +7,7 @@ from api.student import student_router
from api.teacher import router as teacher_router,class_teacher_router
from api.magic import magic_router
app = FastAPI()
app = FastAPI(title='学生管理系统')
app.middleware("http")(log_middleware)
app.include_router(magic_router)
+2 -6
View File
@@ -25,12 +25,8 @@ class Classes( Base ): # 在python里的名字
start_date = Column( DATE # 声明是字段数据类型是日期
, nullable = False # 开课时间必填
)
# ————创建班级"计划结课时间"的字段名————
pland_end_date = Column( DATE # 声明是字段数据类型是'日期'
, nullable = True # 声明是"可为空"
)
# ————创建班级"实际结课时间"的字段名————
true_end_date = Column( DATE # 声明是字段数据类型是'日期'
# ————创建班级"结课时间"的字段名————
end_date = Column( DATE # 声明是字段数据类型是'日期'
, nullable = True # 声明是"可为空"
)
# ————创建班级"创建时间"的字段名————
+6 -6
View File
@@ -1,17 +1,17 @@
from sqlalchemy import *
from sqlalchemy.orm import declarative_base,sessionmaker
from sqlalchemy import * #导入sqlalchemy全部常用类
from sqlalchemy.orm import declarative_base,sessionmaker #造会话工厂 SessionLocal,管数据库增删改查会话
from datetime import datetime
from core.database import Base
from core.database import Base #保证所有模型共用同一个Base,表才能正常生成
# 为了符合三范式,就业表只保存 student_id,不保存学生姓名和班级名称。查询返回时通过关联 students 和 classes 带出学生姓名、班级名称。
class Employment(Base):
__tablename__ = "employment_info_detail" # 实际数据库的表名字
__tablename__ = "employment_info_detail" # 映射的真实MySQL数据库表名
id = Column(Integer, primary_key=True # 主键
, autoincrement=True # 声明是自增主键
, autoincrement=True # 开启自增,插入数据不需要手动传id,数据库自动生成id
,comment="就业信息主键id")
student_id = Column(Integer, ForeignKey("student_info_detail.id") # 外键 -> students.id
, nullable=False # 不允许为null
, nullable=False # 不允许为null,每一条就业记录必须绑定一个学生
,comment="关联学生id")
employment_status = Column(String(50)
, default="not_started"
+3 -4
View File
@@ -1,12 +1,12 @@
from pydantic import BaseModel, Field,ConfigDict
from pydantic import BaseModel, Field
from datetime import date, datetime
from decimal import Decimal
class EmploymentCreate(BaseModel):
"""新增就业信息 请求体 Schema"""
student_id: int = Field(..., description="关联学生ID,必填")
employment_status: str | None = Field(None, description="就业状态:not_started/job_hunting/offered/employed")
student_id: int = Field(..., description="关联学生ID,必填")# description:在Swagger页面展示字段说明文字
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="就业公司名称,选填")
@@ -36,4 +36,3 @@ class EmploymentResponse(BaseModel):
remark: str | None
created_at: datetime | None
updated_at: datetime | None
model_config = ConfigDict(from_attributes=True) #允许pydantic从对象属性里取值
+5
View File
@@ -1,4 +1,6 @@
from datetime import date
from enum import Enum
from pydantic import BaseModel, ConfigDict, Field, model_validator
from typing import Literal
@@ -40,3 +42,6 @@ class StudentUpdate(BaseModel):
age: int | None = Field(default=None, ge=0, le=150)
gender: Literal["男", "女"] | None = None
class Gender(str,Enum):
g1 = '男'
g2 = '女'