Files
2026-09-21 19:03:31 +08:00

166 lines
6.5 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""成绩管理接口(需求 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)} 条成绩")