Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
56df0ef7b0 | ||
|
|
d90a89595f | ||
|
|
52452e378c | ||
|
|
4f90c6a5c3 | ||
|
|
1be706c267 | ||
|
|
6033504d76 | ||
|
|
9c24a229c7 | ||
|
|
fd3e52f4ff | ||
|
|
1933466e3d | ||
|
|
b14d316d0a | ||
|
|
9a0f2d533a | ||
|
|
6016a99bcf | ||
|
|
b3d557e4fc | ||
|
|
bca68a56f4 | ||
|
|
ca427bc8d2 | ||
|
|
e65d7cd50b | ||
|
|
d0c3abbebb | ||
|
|
49e651b9f3 | ||
|
|
93f2356c5d | ||
|
|
505e59280e | ||
|
|
5c873491c6 | ||
|
|
6e0185d38a | ||
|
|
4d59d79812 | ||
|
|
99eab62ab7 |
+36
-24
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,6 +1,6 @@
|
||||
from schemas.statistics import NoExist
|
||||
from sqlalchemy import func
|
||||
from time import time
|
||||
|
||||
#`GET /statistics/students/age-over-30`
|
||||
#查询所有超过 30 岁的学员信息
|
||||
|
||||
|
||||
@@ -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}%"))
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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 # 声明是"可为空"
|
||||
)
|
||||
# ————创建班级"创建时间"的字段名————
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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从对象属性里取值
|
||||
@@ -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 = '女'
|
||||
Reference in New Issue
Block a user