Files
test/app/api/clazz_api.py
T
2026-09-21 19:03:31 +08:00

175 lines
7.4 KiB
Python

"""班级管理接口(需求 2.4)。"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Query
from app.core.deps import DbSession, ReadAccount, WriteAccount
from app.core.exceptions import BusinessError, ConflictError, NotFoundError
from app.core.response import Resp, ok
from app.core.utils import parse_date
from app.dao.advisor_dao import AdvisorDao
from app.dao.clazz_dao import ClazzDao
from app.dao.teacher_dao import TeacherDao
from app.model import Clazz
from app.schema.clazz_schema import ClazzCreate, ClazzOut, ClazzUpdate
from app.schema.common import PageResult
router = APIRouter(prefix="/classes", tags=["2.4 班级管理"])
def _to_out(clazz: Clazz, student_count: int | None = None) -> ClazzOut:
out = ClazzOut.model_validate(clazz)
if student_count is not None:
out.student_count = student_count
return out
@router.get("", response_model=Resp[PageResult[ClazzOut]], summary="班级列表")
def list_classes(
db: DbSession,
_: ReadAccount,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=200)] = 10,
keyword: Annotated[str | None, Query(description="班级名称/编号/方向")] = None,
status: Annotated[int | None, Query(ge=1, le=3, description="1=在读 2=已结课 3=已解散")] = None,
advisor_id: Annotated[int | None, Query(description="带班顾问")] = None,
head_teacher_id: Annotated[int | None, Query(description="班主任")] = None,
teacher_id: Annotated[int | None, Query(description="授课老师")] = None,
order_by: Annotated[str, Query()] = "id",
order: Annotated[str, Query(pattern="^(asc|desc)$")] = "desc",
):
stmt = ClazzDao.build_stmt(
keyword=keyword, status=status, advisor_id=advisor_id,
head_teacher_id=head_teacher_id, teacher_id=teacher_id, order_by=order_by, order=order,
)
items, total, page, pages = ClazzDao.paginate(db, stmt, page, page_size)
counts = ClazzDao.student_counts(db)
return ok({
"total": total, "page": page, "page_size": page_size, "pages": pages,
"items": [_to_out(c, counts.get(c.id, 0)) for c in items],
})
@router.get("/meta/options", summary="班级下拉选项(含老师)")
def class_options(db: DbSession, _: ReadAccount):
classes = ClazzDao.all(db, ClazzDao.build_stmt(order_by="class_no", order="asc"))
teachers = TeacherDao.all(db, TeacherDao.build_stmt(order_by="id", order="asc"))
advisors = AdvisorDao.all(db, AdvisorDao.build_stmt(order_by="id", order="asc"))
counts = ClazzDao.student_counts(db)
return ok({
"classes": [
{"id": c.id, "name": c.name, "class_no": c.class_no, "student_count": counts.get(c.id, 0)}
for c in classes
],
"teachers": [{"id": t.id, "name": t.name, "subject": t.subject} for t in teachers],
"advisors": [{"id": a.id, "name": a.name} for a in advisors],
})
@router.post("", response_model=Resp[ClazzOut], summary="创建班级")
def create_class(db: DbSession, _: WriteAccount, payload: ClazzCreate):
class_no = (payload.class_no or "").strip()
if not class_no:
prefix = _direction_prefix(payload.direction or payload.name)
from datetime import date
class_no = ClazzDao.next_class_no(db, prefix, (parse_date(payload.open_date, "开课时间") or date.today()).year)
if ClazzDao.get_by_class_no(db, class_no, with_deleted=True):
raise ConflictError(f"班级编号 {class_no} 已存在")
_check_refs(db, payload.head_teacher_id, payload.advisor_id, payload.teacher_ids)
klass = Clazz(
class_no=class_no,
name=payload.name,
direction=payload.direction,
open_date=parse_date(payload.open_date, "开课时间"),
close_date=parse_date(payload.close_date, "结课时间"),
classroom=payload.classroom,
capacity=payload.capacity,
status=payload.status,
head_teacher_id=payload.head_teacher_id,
advisor_id=payload.advisor_id,
description=payload.description,
)
if payload.teacher_ids:
all_teachers = {t.id: t for t in TeacherDao.all(db, TeacherDao.build_stmt())}
missing = [tid for tid in payload.teacher_ids if tid not in all_teachers]
if missing:
raise NotFoundError(f"老师不存在:{missing}")
klass.teachers = [all_teachers[tid] for tid in payload.teacher_ids]
db.add(klass)
db.flush()
db.commit()
db.refresh(klass)
return ok(_to_out(klass, 0), msg=f"班级 {klass.name} 创建成功,编号 {klass.class_no}")
@router.get("/{class_id}", response_model=Resp[ClazzOut], summary="班级详情")
def get_class(db: DbSession, _: ReadAccount, class_id: int):
klass = ClazzDao.get_or_404(db, class_id, "班级")
counts = ClazzDao.student_counts(db)
return ok(_to_out(klass, counts.get(klass.id, 0)))
@router.put("/{class_id}", response_model=Resp[ClazzOut], summary="更新班级")
def update_class(db: DbSession, _: WriteAccount, class_id: int, payload: ClazzUpdate):
klass = ClazzDao.get_or_404(db, class_id, "班级")
data = payload.model_dump(exclude_unset=True)
_check_refs(db, data.get("head_teacher_id"), data.get("advisor_id"), data.get("teacher_ids"))
for field in ("open_date", "close_date"):
if field in data:
setattr(klass, field, parse_date(data.pop(field), field))
teacher_ids = data.pop("teacher_ids", None)
for key, value in data.items():
if value is not None and hasattr(klass, key):
setattr(klass, key, value)
if teacher_ids is not None:
all_teachers = {t.id: t for t in TeacherDao.all(db, TeacherDao.build_stmt())}
missing = [tid for tid in teacher_ids if tid not in all_teachers]
if missing:
raise NotFoundError(f"老师不存在:{missing}")
klass.teachers = [all_teachers[tid] for tid in teacher_ids]
db.commit()
db.refresh(klass)
counts = ClazzDao.student_counts(db)
return ok(_to_out(klass, counts.get(klass.id, 0)), msg="更新成功")
@router.delete("/{class_id}", summary="逻辑删除班级(班内还有学生时拒绝)")
def delete_class(db: DbSession, _: WriteAccount, class_id: int):
klass = ClazzDao.get_or_404(db, class_id, "班级")
counts = ClazzDao.student_counts(db)
if counts.get(class_id, 0) > 0:
# 直接删会让这些学生"无班可归",统计里凭空少人,所以这里拦一道
raise BusinessError(
f"班级「{klass.name}」下还有 {counts[class_id]} 名学生,请先转班或删除学生后再删班级"
)
klass.soft_delete()
db.commit()
return ok(msg=f"已删除班级 {klass.name}")
def _direction_prefix(text: str) -> str:
letters = "".join(ch for ch in text if ch.isascii() and ch.isalpha()).upper()
return letters[:6] or "CLS"
def _check_refs(db, head_teacher_id, advisor_id, teacher_ids) -> None:
if head_teacher_id is not None and TeacherDao.get(db, head_teacher_id) is None:
raise NotFoundError(f"班主任(老师 id={head_teacher_id})不存在")
if advisor_id is not None and AdvisorDao.get(db, advisor_id) is None:
raise NotFoundError(f"顾问(id={advisor_id})不存在")
if teacher_ids:
exist = {t.id for t in TeacherDao.all(db, TeacherDao.build_stmt())}
missing = [tid for tid in teacher_ids if tid not in exist]
if missing:
raise NotFoundError(f"老师不存在:{missing}")