22 Commits
Author SHA1 Message Date
lookerZz 236f2665a6 Merge pull request '张昕浩' (#56) from 张昕浩 into main
Reviewed-on: #56
2026-09-24 10:28:40 +08:00
lookerZz e80d463bbc 添加statistics tag 2026-09-24 10:28:00 +08:00
lookerZz a90c240b5a 添加statistics tag 2026-09-24 10:26:22 +08:00
lookerZz d70705e7fe 添加statistics tag 2026-09-23 20:56:36 +08:00
lookerZz ee468216cb Merge pull request 'Li yujie' (#55) from LI_YUJIE into main
Reviewed-on: #55
2026-09-23 20:55:09 +08:00
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
zhutt c1deb9f545 修改请求体 2026-09-23 17:58:27 +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
zhutt 2807c30683 修改请求体 2026-09-22 22:21:14 +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
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_liyujie 99eab62ab7 规范接口方法写法 2026-09-22 16:48:45 +08:00
17 changed files with 689 additions and 156 deletions
+1 -1
View File
@@ -51,6 +51,6 @@ def get_classes( id:int|None = None # id参数接收前端输
@classes_router.delete('/{id}',response_model=ClassResponse,summary='删除班级信息') # delete删除请求,指定返回数据格式为 ClassResponse
def delete_classes(id:int,db=Depends(get_db)): # 定义删除接口函数,id参数接收前端输入的数据,参数db依赖于数据库会话的上下文管理器
r = delete_classes_dao( id , db ) # 变量r用来接收dao层delete_classes_dao函数的返回值,执行函数时以以前端输入的id 数据、db依赖函数入参
if r : # 判断 DAO 层的返回结果,如果是True
if r!=0 : # 判断 DAO 层的返回结果,如果是True
return ClassResponse( total=1,data='已成功删除班级信息' ) # 返回成功响应和提示给前端 # 如果是False
raise HTTPException(status_code=500, detail='删除失败,该班级信息已不存在或服务器繁忙,请稍后更新!') # 如果为False,抛出 500 内部服务器错误 和提示
+30 -16
View File
@@ -6,7 +6,8 @@ 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,
@@ -28,17 +29,21 @@ 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="公司名称模糊查询"),
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="偏移量"),
@@ -49,13 +54,13 @@ def query_employment_list(company_name: str | None = Query(None, description="
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="修改就业信息")
@@ -75,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)):
+15 -14
View File
@@ -3,6 +3,7 @@ from core.database import get_db
from models.employment import Employment
from models.student import Student
from models.score import Score
from schemas.statistics import *
from dao.statistics import *
from schemas.common import SuccessResponse
from sqlalchemy.orm import Session
@@ -11,34 +12,34 @@ from sqlalchemy.orm import Session
statistics_router=APIRouter(tags=['统计接口'])
@statistics_router.get("/students/age-over-30",response_model=SuccessResponse)
def api_age_over_thirty(db:Session=Depends(get_db)):
@statistics_router.post("/students/age-over-30",response_model=SuccessResponse)
def api_age_over_thirty(input_age:GenderCount,db:Session=Depends(get_db)):
try:
age_over_thirty_list=age_over_thirty(Student,db)
age_over_thirty_list=age_over_thirty(Student,input_age.input_class,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)):
def api_gender_count(input_class:int|None= None,db:Session=Depends(get_db)):
try:
result = gender_count(Student,db)
result = gender_count(Student,db,input_class)
return SuccessResponse(status_code=200,msg=result)
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)):
@statistics_router.post("/scores/all-above-80",response_model=SuccessResponse)
def all_above_eighty(page:int,score:StudentScore,db:Session=Depends(get_db)):
try:
result = score_over_eighty(Score,Student,db)
result = score_over_eighty(score.score,page,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)):
def fail_two(num:int,offset_set:int,db:Session=Depends(get_db)):
try:
result = twice_error(Score,Student,db)
result = twice_error(num,offset_set,Score,Student,db)
return SuccessResponse(status_code=200,msg=result)
except NoExist as e:
raise HTTPException(status_code=404,detail=str(e))
@@ -53,17 +54,17 @@ def class_average(db:Session=Depends(get_db)):
@statistics_router.get("/employments/top5-salary",response_model=SuccessResponse)
def top_salary(db:Session=Depends(get_db)):
def top_salary(limit_set:int,db:Session=Depends(get_db)):
try:
result=max_salary(Student,Employment,db)
result=max_salary(limit_set,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)):
def employ_time(limit_set,db:Session=Depends(get_db)):
try:
result = student_market(Employment,db)
result = student_market(limit_set,Employment,db)
return SuccessResponse(status_code=200,msg=result)
except NoExist as e:
raise HTTPException(status_code=404,detail=str(e))
+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)):
+4 -4
View File
@@ -73,10 +73,10 @@ def get_classes_dao( class_id, class_no , class_name , page , page_size , db ):
# ————定义班级表"删除"信息的函数————
def delete_classes_dao( class_id , db ): # 定义函数,设置两个个形参class_id,db
try : # 执行以下可能报错的代码
db.query( Classes ).filter( Classes.id == class_id , Classes.is_deleted == 0 ).update( {"is_deleted": 1} ) # 将未删除且前端输入的id和数据库id可以匹配上的数据更新is_deleted字段为1,表示已逻辑删除
r = db.query( Classes ).filter( Classes.id == class_id , Classes.is_deleted == 0 ).update( {"is_deleted": 1} ) # 将未删除且前端输入的id和数据库id可以匹配上的数据更新is_deleted字段为1,表示已逻辑删除
if r !=0:
db.commit()
return r
except : # 捕获异常:
db.rollback() # 如果中间出错,回滚事务,防止产生脏数据
return False # 返回 False,返回接口,表示操作失败
else : # 执行成功:
db.commit() # 提交事务:更新is_deleted字段为1,表示已逻辑删除,前端无法查询到已逻辑删除的数据
return True # 返回 True,返回接口,表示操作成功
+41 -24
View File
@@ -1,13 +1,12 @@
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_obj = Employment(**e.model_dump())
db.add(db_obj)
db.commit()
db.refresh(db_obj)
@@ -75,10 +74,14 @@ def get_employment_by_student_id(db, student_id):
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:
@@ -108,48 +111,62 @@ def get_employment_list(db, company_name=None, salary_min=None, salary_max=None,
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
x = db.query(Employment).filter(Employment.id == eid
,Employment.is_deleted == 0).first()
if db_obj_raw is None:
if x is None:
return None
update_dict = obj_in.model_dump(exclude_unset=True)
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)
db.refresh(x)
resp = EmploymentResponse(id=db_obj_raw.id
,student_id=db_obj_raw.student_id
,employment_status=db_obj_raw.employment_status
,employment_open_date=db_obj_raw.employment_open_date
,offer_date=db_obj_raw.offer_date
,company_name=db_obj_raw.company_name
,salary=db_obj_raw.salary
,remark=db_obj_raw.remark
,created_at=db_obj_raw.created_at
,updated_at=db_obj_raw.updated_at)
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()
+22 -16
View File
@@ -3,9 +3,8 @@ from sqlalchemy import func
#`GET /statistics/students/age-over-30`
#查询所有超过 30 岁的学员信息
def age_over_thirty(Student,db):
student_list=db.query(Student).filter(Student.age>30,Student.is_deleted==0).all()
def age_over_thirty(Student,input_age,db):
student_list=db.query(Student).filter(Student.age>input_age,Student.is_deleted==0).all()
if not student_list :
raise NoExist('无目标年龄大于30岁')
# 把每个学生对象需要返回的字段,明确放进普通字典。
@@ -30,9 +29,13 @@ def age_over_thirty(Student,db):
"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()
#统计男、女、总人数
def gender_count(Student,db,input_class=None):
if input_class :
result = db.query(Student.class_id, Student.gender, func.count(Student.id)).filter(
Student.class_id == input_class, Student.is_deleted == 0).group_by(Student.class_id, Student.gender).all()
else:
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 = {}
@@ -43,10 +46,11 @@ def gender_count(Student,db):
dict1[i]['总人数']+=k
return dict1
#查询每次考试成绩都在 80 分以上的学生编号、姓名和成绩
def score_over_eighty(Score,Student,db):
#首先,拿到最低值>80的人的列表,row。然后,遍历元素拿到符合条件的id的属性。
def score_over_eighty(score,offset_set,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.id,Student.student_name)\
.having(func.min(Score.score)>80).all()
.having(func.min(Score.score)>score).all()
if not student_list :
raise NoExist('全是学渣')
list1 = []
@@ -59,10 +63,11 @@ def score_over_eighty(Score,Student,db):
"student_name": student_name,
"score": float(score) if score is not None else None,
})
return list1
list2=list1[5*(offset_set-1):5*offset_set]
return list2
#查询有两次以上不及格的学生姓名、班级和不及格成绩
def twice_error(Score,Student,db):
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()
def twice_error(num,offset_set,Score,Student,db):
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)>num).all()
if not student_list :
raise NoExist('全是学霸')
list1=[]
@@ -75,7 +80,8 @@ def twice_error(Score,Student,db):
"class_id": class_id,
"score": float(score) if score is not None else None,
})
return list1
list2 = list1[5 * (offset_set - 1):5 * offset_set]
return list2
#统计每次考试每个班级的平均分,并按照平均分从高到低排序
def avg_class(Score,Student,db):
@@ -92,8 +98,8 @@ def avg_class(Score,Student,db):
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()
def max_salary(limit_set,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(limit_set).all()
if not student_list :
raise NoExist('没就业的啊兄弟?')
result = []
@@ -108,8 +114,8 @@ 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)).filter(Employment.is_deleted==0).all()
def student_market(limit_set,Employment,db):
student_list = db.query(Employment.student_id,func.datediff(Employment.offer_date,Employment.employment_open_date)).filter(Employment.is_deleted==0).limit(limit_set).all()
result = []
for student_id, duration_days in student_list:
result.append({
+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。
+9 -1
View File
@@ -1,4 +1,7 @@
from fastapi import FastAPI
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from pathlib import Path
from api.example import example_router
from middleware import log_middleware
from api.statistics import statistics_router
@@ -8,6 +11,12 @@ from api.teacher import router as teacher_router,class_teacher_router
from api.magic import magic_router
app = FastAPI(title='学生管理系统')
app.mount("/ui", StaticFiles(directory=Path(__file__).resolve().parent / "frontend", html=True), name="frontend")
@app.get("/", include_in_schema=False)
def home():
return RedirectResponse("/ui/")
app.middleware("http")(log_middleware)
app.include_router(magic_router)
@@ -25,7 +34,6 @@ from api import consultant
app.include_router(consultant.router)
#-------------张昕浩---------------
app.include_router(example_router)
app.include_router(magic_router)
app.include_router(statistics_router, prefix="/statistics")
#-------------曾凯---------------
+1 -1
View File
@@ -5,7 +5,7 @@ from core.database import Base # 从core软件包下的database模块导入B
from sqlalchemy.dialects.mysql import TINYINT
class Classes( Base ): # 在python里的名字
__tablename__ = 'class_info_detail' # 在数据库中表的名字
__tablename__ = 'class_info_detail' # 在数据库中表的名字
#————创建班级"主键"的字段名————
id = Column( Integer # 声明字段的数据类型是"整数"
, primary_key = True # 声明是"主键"
+1 -1
View File
@@ -8,7 +8,7 @@ from datetime import datetime,date # 导入datetime模块下的datetime,da
class ClassCreate( BaseModel ): # "创建"班级信息的请求体模型
class_no: str # 班级编号,不可为空
class_name: str # 班级名字,不可为空
start_date : date|None = None # 班级开课日期,可以为空,默认为空
start_date : date # 班级开课日期,不可为空
end_date : date|None = None # 班级结课日期,可以为空,默认为空
remark : str|None = None # 班级备注,可以为空,默认为空
+1 -1
View File
@@ -1,4 +1,4 @@
from pydantic import BaseModel, Field,ConfigDict#专门存放模型配置参数的字典类。把各种开关配置放里面。
from pydantic import BaseModel, Field
from datetime import date, datetime
from decimal import Decimal
+10 -1
View File
@@ -1,4 +1,13 @@
from pydantic import BaseModel,Field
class NoExist(Exception):
def __init__(self,value):
self.value = value
pass
pass
class GenderCount(BaseModel):
input_class:int|None = None
class StudentScore(BaseModel):
score:int = Field(default = 80, ge = 0, le = 100)
+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 = '女'