100 lines
3.7 KiB
Python
100 lines
3.7 KiB
Python
# api/stu_api.py
|
|||
|
|
# 学生信息管理模块:接口层只负责 HTTP 出入参与响应组装,查库/校验/事务交给 dao.stu_dao
|
||
|
|
# 业务异常(NotFoundError / ConflictError)由 main.py 注册的全局处理器统一转成状态码
|
||
|
|
|
||
|
|
from typing import Optional
|
||
|
|
|
||
|
|
from fastapi import APIRouter, Depends, Query
|
||
|
|
from sqlalchemy.orm import Session
|
||
|
|
|
||
|
|
from dao import stu_dao
|
||
|
|
from database import get_db
|
||
|
|
from schemas.stu_schema import StuInfoCreate, StuInfoUpdate, StuInfoOut
|
||
|
|
|
||
|
|
router = APIRouter()
|
||
|
|
|
||
|
|
|
||
|
|
def _to_detail(stu) -> dict:
|
||
|
|
"""把学生实体组装成详情响应:基础字段 + 班级 / 顾问 / 成绩 / 就业信息。"""
|
||
|
|
data = StuInfoOut.model_validate(stu).model_dump()
|
||
|
|
|
||
|
|
# 班级信息
|
||
|
|
data["cls"] = None
|
||
|
|
if stu.cls:
|
||
|
|
data["cls"] = {
|
||
|
|
"id": stu.cls.id,
|
||
|
|
"cls_start_date": stu.cls.cls_start_date.isoformat() if stu.cls.cls_start_date else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
# 顾问信息
|
||
|
|
data["advisor"] = {"id": stu.adv.id, "name": stu.adv.name} if stu.adv else None
|
||
|
|
|
||
|
|
# 成绩列表(过滤已删除)
|
||
|
|
data["scores"] = [
|
||
|
|
{
|
||
|
|
"exam_attempt": s.exam_attempt,
|
||
|
|
"exam_score": s.exam_score,
|
||
|
|
"score_level": s.score_level,
|
||
|
|
}
|
||
|
|
for s in stu.score
|
||
|
|
if not s.is_deleted
|
||
|
|
]
|
||
|
|
|
||
|
|
# 就业信息(一对一)
|
||
|
|
data["employ"] = None
|
||
|
|
if stu.stu_emp_mgmt and not stu.stu_emp_mgmt.is_deleted:
|
||
|
|
e = stu.stu_emp_mgmt
|
||
|
|
data["employ"] = {
|
||
|
|
"emp_company": e.emp_company,
|
||
|
|
"salary": e.salary,
|
||
|
|
"emp_open_time": e.emp_open_time.isoformat() if e.emp_open_time else None,
|
||
|
|
"send_offer_time": e.send_offer_time.isoformat() if e.send_offer_time else None,
|
||
|
|
}
|
||
|
|
|
||
|
|
return data
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("", summary="分页查询学生列表")
|
||
|
|
def list_students(
|
||
|
|
page: int = Query(1, ge=1, description="页码"),
|
||
|
|
size: int = Query(10, ge=1, le=100, description="每页数量"),
|
||
|
|
name: Optional[str] = Query(None, description="按姓名模糊查询"),
|
||
|
|
cls_id: Optional[str] = Query(None, description="按班级编号过滤"),
|
||
|
|
state: Optional[str] = Query(None, description="按状态过滤"),
|
||
|
|
db: Session = Depends(get_db),
|
||
|
|
):
|
||
|
|
"""查询未删除的学生,支持分页和按姓名/班级/状态过滤。"""
|
||
|
|
total, items = stu_dao.list_students(
|
||
|
|
db, page=page, size=size, name=name, cls_id=cls_id, state=state
|
||
|
|
)
|
||
|
|
return {
|
||
|
|
"total": total,
|
||
|
|
"page": page,
|
||
|
|
"size": size,
|
||
|
|
"items": [StuInfoOut.model_validate(x) for x in items],
|
||
|
|
}
|
||
|
|
|
||
|
|
|
||
|
|
@router.get("/{stu_id}", summary="查询单个学生详情")
|
||
|
|
def get_student(stu_id: str, db: Session = Depends(get_db)):
|
||
|
|
"""查询学生详情,并附带班级、顾问、成绩与就业信息。"""
|
||
|
|
return _to_detail(stu_dao.assert_stu_alive(db, stu_id, "学生不存在或已删除"))
|
||
|
|
|
||
|
|
|
||
|
|
@router.post("", response_model=StuInfoOut, status_code=201, summary="新增学生")
|
||
|
|
def create_student(payload: StuInfoCreate, db: Session = Depends(get_db)):
|
||
|
|
"""新增学生(学号唯一、班级与顾问必须存在,由 DAO 校验)。"""
|
||
|
|
return stu_dao.create_student(db, payload.model_dump())
|
||
|
|
|
||
|
|
|
||
|
|
@router.put("/{stu_id}", response_model=StuInfoOut, summary="更新学生信息")
|
||
|
|
def update_student(stu_id: str, payload: StuInfoUpdate, db: Session = Depends(get_db)):
|
||
|
|
"""按传入字段局部更新学生信息。"""
|
||
|
|
return stu_dao.update_student(db, stu_id, payload.model_dump(exclude_unset=True))
|
||
|
|
|
||
|
|
|
||
|
|
@router.delete("/{stu_id}", summary="删除学生(软删除)")
|
||
|
|
def delete_student(stu_id: str, db: Session = Depends(get_db)):
|
||
|
|
"""软删除学生:仅置 is_deleted 标记,数据保留。"""
|
||
|
|
stu_dao.soft_delete_student(db, stu_id)
|
||
|
|
return {"message": "删除成功", "id": stu_id}
|