Files

166 lines
6.5 KiB
Python
Raw Permalink Normal View History

2026-09-21 19:03:31 +08:00
"""成绩管理接口(需求 2.2、4.2)。"""
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Query
from app.core.config import settings
from app.core.deps import DbSession, ReadAccount, WriteAccount
from app.core.response import Resp, ok
from app.dao.score_dao import ScoreDao
from app.dao.student_dao import StudentDao
from app.schema.common import PageResult
from app.schema.score_schema import ScoreBatchCreate, ScoreCreate, ScoreOut, ScoreRecordResult, ScoreUpdate
from app.service.score_service import ScoreService
router = APIRouter(prefix="/scores", tags=["2.2 学生考核成绩管理"])
@router.get("", response_model=Resp[PageResult[ScoreOut]], summary="成绩列表(多条件筛选)")
def list_scores(
db: DbSession,
_: ReadAccount,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=200)] = 10,
stu_id: Annotated[int | None, Query(description="学生ID")] = None,
exam_seq: Annotated[int | None, Query(description="考核序次")] = None,
class_id: Annotated[int | None, Query(description="班级ID")] = None,
flag: Annotated[int | None, Query(ge=0, le=1, description="1=只看预警")] = None,
min_score: Annotated[float | None, Query(description="分数下限")] = None,
max_score: Annotated[float | None, Query(description="分数上限")] = None,
keyword: Annotated[str | None, Query(description="学生姓名/学号")] = None,
order_by: Annotated[str, Query()] = "id",
order: Annotated[str, Query(pattern="^(asc|desc)$")] = "desc",
):
stmt = ScoreDao.build_stmt(
stu_id=stu_id, exam_seq=exam_seq, class_id=class_id, flag=flag,
min_score=min_score, max_score=max_score, keyword=keyword, order_by=order_by, order=order,
)
items, total, page, pages = ScoreDao.paginate(db, stmt, page, page_size)
return ok({
"total": total, "page": page, "page_size": page_size, "pages": pages,
"items": [ScoreOut.model_validate(s) for s in items],
})
@router.get("/meta", summary="成绩模块元信息:已用过的考核序次 / 红线 / 课程")
def score_meta(db: DbSession, _: ReadAccount):
from sqlalchemy import select
from app.model import Score
exam_names = [
name
for name in db.scalars(
select(Score.exam_name).where(Score.alive(), Score.exam_name.is_not(None)).distinct()
).all()
if name
]
return ok({
"exam_seqs": ScoreDao.exam_seq_list(db),
"max_exam_seq": ScoreDao.max_exam_seq(db),
"exam_names": exam_names,
"pass_line": settings.SCORE_PASS_LINE,
"warn_line": settings.SCORE_WARN_LINE,
})
@router.get("/student/{stu_id}", summary="某个学生的全部成绩 + 汇总(需求 4.2 的 GET /score/{stu_id})")
def scores_of_student(db: DbSession, _: ReadAccount, stu_id: int):
StudentDao.get_or_404(db, stu_id, "学生")
return ok(ScoreService.student_summary(db, stu_id))
@router.get("/warnings", summary="红线预警名单(低于红线的学生汇总)")
def warning_list(
db: DbSession,
_: ReadAccount,
page: Annotated[int, Query(ge=1)] = 1,
page_size: Annotated[int, Query(ge=1, le=200)] = 10,
):
stmt = ScoreDao.build_stmt(flag=1, order_by="score", order="asc")
items, total, page, pages = ScoreDao.paginate(db, stmt, page, page_size)
return ok({
"total": total, "page": page, "page_size": page_size, "pages": pages,
"warn_line": settings.SCORE_WARN_LINE,
"items": [ScoreOut.model_validate(s) for s in items],
})
@router.post("", response_model=Resp[ScoreRecordResult], summary="录入成绩(低于红线自动触发预警)")
def create_score(db: DbSession, _: WriteAccount, payload: ScoreCreate):
score, warning, warning_msg = ScoreService.create(db, payload)
db.commit()
db.refresh(score)
summary = ScoreService.student_summary(db, score.stu_id)
return ok(
{
"record": ScoreOut.model_validate(score),
"warning": warning,
"warning_msg": warning_msg,
"avg_score": summary["avg"],
},
msg=warning_msg or "成绩录入成功",
)
@router.post("/batch", summary="一次录入同一个学生的多次成绩")
def create_scores_batch(db: DbSession, _: WriteAccount, payload: ScoreBatchCreate):
results = ScoreService.batch_create(db, payload.stu_id, payload.scores)
db.commit()
items = []
for score, warning, warning_msg in results:
db.refresh(score)
items.append(
{
"record": ScoreOut.model_validate(score),
"warning": warning,
"warning_msg": warning_msg,
}
)
warnings = [i for i in items if i["warning"]]
return ok(
{"count": len(items), "warning_count": len(warnings), "items": items},
msg=f"录入 {len(items)} 条成绩" + (f",其中 {len(warnings)} 条触发红线预警" if warnings else ""),
)
@router.put("/{score_id}", response_model=Resp[ScoreRecordResult], summary="修改成绩(重新计算预警标记)")
def update_score(db: DbSession, _: WriteAccount, score_id: int, payload: ScoreUpdate):
score = ScoreDao.get_or_404(db, score_id, "成绩记录")
score, warning, warning_msg = ScoreService.update(db, score, payload)
db.commit()
db.refresh(score)
summary = ScoreService.student_summary(db, score.stu_id)
return ok(
{
"record": ScoreOut.model_validate(score),
"warning": warning,
"warning_msg": warning_msg,
"avg_score": summary["avg"],
},
msg="修改成功" + (f"({warning_msg})" if warning_msg else ""),
)
@router.delete("/{score_id}", summary="删除成绩(逻辑删除,同序次可重新录入)")
def delete_score(db: DbSession, _: WriteAccount, score_id: int):
score = ScoreDao.get_or_404(db, score_id, "成绩记录")
student_name = score.student_name
exam_seq = score.exam_seq
ScoreService.delete(db, score)
db.commit()
return ok(msg=f"已删除 {student_name} 第 {exam_seq} 次考核成绩")
@router.post("/student/{stu_id}/clear", summary="清空某个学生的全部成绩")
def clear_student_scores(db: DbSession, _: WriteAccount, stu_id: int):
StudentDao.get_or_404(db, stu_id, "学生")
scores = ScoreDao.list_by_student(db, stu_id)
for score in scores:
score.soft_delete()
db.commit()
return ok({"deleted": len(scores)}, msg=f"已清空 {len(scores)} 条成绩")