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

151 lines
5.9 KiB
Python
Raw 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):红线预警、按序次唯一、逻辑删除后复活。"""
from __future__ import annotations
from decimal import Decimal
from sqlalchemy.orm import Session
from app.core.config import settings
from app.core.exceptions import BusinessError, ConflictError, NotFoundError
from app.core.utils import parse_date
from app.dao.score_dao import ScoreDao
from app.dao.student_dao import StudentDao
from app.model import Score, Student
from app.schema.score_schema import ScoreCreate, ScoreUpdate
class ScoreService:
# ================================================================ 红线
@staticmethod
def check_warning(score: Decimal | float) -> tuple[int, str | None]:
"""低于红线返回 (1, 提示文案)。阈值来自配置,不写死 60。"""
value = float(score)
if value < settings.SCORE_WARN_LINE:
if value < 30:
level = "严重偏低"
elif value < settings.SCORE_WARN_LINE:
level = "低于及格线"
else:
level = "偏低"
msg = (
f"成绩 {value:g} 分{level}(红线 {settings.SCORE_WARN_LINE:g} 分),"
f"已标记为需重点关注"
)
return 1, msg
return 0, None
# ================================================================ 录入
@classmethod
def create(cls, db: Session, payload: ScoreCreate) -> tuple[Score, bool, str | None]:
student = StudentDao.get(db, payload.stu_id)
if student is None:
raise NotFoundError(f"学生不存在(id={payload.stu_id})")
exam_date = parse_date(payload.exam_date, "考核日期")
flag, warning_msg = cls.check_warning(payload.score)
# (学生, 序次) 是唯一键:已存在就报冲突并告诉调用方走修改;
# 若是被逻辑删除过的老记录,直接"复活"它,避免唯一键撞车。
existing = ScoreDao.get_by_stu_seq(db, payload.stu_id, payload.exam_seq, with_deleted=True)
if existing is not None:
if existing.is_del == 0:
raise ConflictError(
f"{student.name} 的第 {payload.exam_seq} 次考核成绩已存在"
f"({float(existing.score):g} 分),请改用修改接口"
)
existing.is_del = 0
existing.score = payload.score
existing.flag = flag
existing.exam_name = payload.exam_name
existing.exam_date = exam_date
existing.remark = payload.remark
db.flush()
return existing, flag == 1, warning_msg
score = Score(
stu_id=payload.stu_id,
exam_seq=payload.exam_seq,
exam_name=payload.exam_name,
exam_date=exam_date,
score=payload.score,
flag=flag,
remark=payload.remark,
)
db.add(score)
db.flush()
if flag == 1:
cls._append_student_note(student, f"第{payload.exam_seq}次考核 {float(payload.score):g} 分")
return score, flag == 1, warning_msg
@classmethod
def batch_create(cls, db: Session, stu_id: int, items: list[ScoreCreate]) -> list[tuple[Score, bool, str | None]]:
results = []
for item in items:
if item.stu_id != stu_id:
raise BusinessError("批量录入的成绩必须属于同一个学生")
results.append(cls.create(db, item))
return results
# ================================================================ 修改
@classmethod
def update(cls, db: Session, score: Score, payload: ScoreUpdate) -> tuple[Score, bool, str | None]:
data = payload.model_dump(exclude_unset=True)
warning_msg = None
flag = score.flag
if data.get("score") is not None:
score.score = data["score"]
flag, warning_msg = cls.check_warning(score.score)
score.flag = flag
if "exam_name" in data:
score.exam_name = data["exam_name"]
if "exam_date" in data:
score.exam_date = parse_date(data["exam_date"], "考核日期")
if "remark" in data:
score.remark = data["remark"]
db.flush()
if flag == 1 and score.student is not None:
cls._append_student_note(score.student, f"第{score.exam_seq}次考核 {float(score.score):g} 分")
return score, flag == 1, warning_msg
# ================================================================ 删除
@classmethod
def delete(cls, db: Session, score: Score) -> None:
score.soft_delete()
db.flush()
# ================================================================ 辅助
@staticmethod
def _append_student_note(student: Student, note: str) -> None:
"""把预警写进学生备注,顾问打开学生列表就能看到。"""
tag = f"[{note}]"
if student.remark and tag in student.remark:
return
prefix = "成绩预警:"
student.remark = f"{student.remark} {prefix}{note}".strip() if student.remark else f"{prefix}{note}"
@staticmethod
def student_summary(db: Session, stu_id: int) -> dict:
scores = ScoreDao.list_by_student(db, stu_id)
values = [float(s.score) for s in scores]
return {
"count": len(values),
"avg": round(sum(values) / len(values), 2) if values else None,
"max": max(values) if values else None,
"min": min(values) if values else None,
"fail": len([s for s in scores if s.flag == 1]),
"scores": [
{
"exam_seq": s.exam_seq,
"exam_name": s.exam_name,
"score": float(s.score),
"flag": s.flag,
"exam_date": s.exam_date.isoformat() if s.exam_date else None,
}
for s in scores
],
}