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

114 lines
5.0 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.1),系统的主表。
关于「年龄」的一个设计取舍
--------------------------------------------------------------------------
需求里把年龄列成了学生字段。这里**不落 age 字段**,只存 birth_date:
* 年龄 = 按今天算出来的派生值。今天是 24 岁,明年今天还是 24 就错了,
必须靠定时任务去刷,多一个必定会脏的字段。
* 需要"按年龄筛选"的场景(需求 2.6.1)用 SQL 表达式算:
``app.core.utils.age_expression()`` 把 birth_date 折成年数参与 WHERE。
* 出参同时给 birth_date 和算好的 age,前端拿来直接用。
唯一例外:录入时对方只知道年龄不知道生日,入参允许只传 age,
service 会用 ``guess_birth_date()`` 折算一个占位生日并标记 ``birth_date_estimated=True``。
"""
from __future__ import annotations
from datetime import date
from sqlalchemy import Date, ForeignKey, String, Text
from sqlalchemy.orm import Mapped, mapped_column, relationship
from app.model.base import Base, SoftDeleteMixin, TimestampMixin
from app.model.constants import GENDER_TEXT, STUDENT_STATUS_TEXT, StudentStatus
from app.core.utils import calc_age
class Student(Base, TimestampMixin, SoftDeleteMixin):
__tablename__ = "student"
__table_args__ = {"comment": "学生基本信息表"}
id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True)
stu_no: Mapped[str] = mapped_column(String(30), unique=True, nullable=False, index=True, comment="学号(按规则生成,非自增)")
name: Mapped[str] = mapped_column(String(30), nullable=False, index=True, comment="姓名")
gender: Mapped[int] = mapped_column(default=1, nullable=False, index=True, comment="性别 1=男 2=女")
birth_date: Mapped[date | None] = mapped_column(Date, comment="出生日期(年龄由此实时计算,不落库)")
birth_date_estimated: Mapped[int] = mapped_column(default=0, nullable=False, comment="生日是否为按年龄推算的占位值 0=否 1=是")
native_place: Mapped[str | None] = mapped_column(String(60), comment="籍贯")
graduate_school: Mapped[str | None] = mapped_column(String(80), comment="毕业院校")
major: Mapped[str | None] = mapped_column(String(60), comment="专业")
education: Mapped[str | None] = mapped_column(String(20), comment="学历")
enroll_date: Mapped[date | None] = mapped_column(Date, comment="入学时间")
graduate_date: Mapped[date | None] = mapped_column(Date, comment="毕业时间")
phone: Mapped[str | None] = mapped_column(String(20), comment="联系电话")
id_card: Mapped[str | None] = mapped_column(String(30), comment="身份证号")
class_id: Mapped[int | None] = mapped_column(ForeignKey("clazz.id"), index=True, comment="所属班级")
advisor_id: Mapped[int | None] = mapped_column(ForeignKey("advisor.id"), index=True, comment="顾问编号")
status: Mapped[int] = mapped_column(
default=int(StudentStatus.STUDYING), nullable=False, index=True,
comment="状态 1=在读 2=进入就业 3=已就业",
)
remark: Mapped[str | None] = mapped_column(Text, comment="备注(成绩预警会自动追加)")
# ---------------- 关系 ----------------
klass: Mapped["Clazz"] = relationship( # noqa: F821
back_populates="students", lazy="joined", foreign_keys=[class_id]
)
advisor: Mapped["Advisor"] = relationship(lazy="joined") # noqa: F821
scores: Mapped[list["Score"]] = relationship( # noqa: F821
back_populates="student",
lazy="selectin",
order_by="Score.exam_seq",
cascade="all, delete-orphan",
)
employment: Mapped["Employment"] = relationship( # noqa: F821
back_populates="student",
uselist=False,
lazy="joined",
)
# ---------------- 出参派生字段 ----------------
@property
def age(self) -> int | None:
return calc_age(self.birth_date)
@property
def gender_text(self) -> str | None:
return GENDER_TEXT.get(self.gender)
@property
def status_text(self) -> str:
return STUDENT_STATUS_TEXT.get(self.status, "未知")
@property
def class_name(self) -> str | None:
return self.klass.name if self.klass and self.klass.is_del == 0 else None
@property
def class_no(self) -> str | None:
return self.klass.class_no if self.klass and self.klass.is_del == 0 else None
@property
def advisor_name(self) -> str | None:
return self.advisor.name if self.advisor and self.advisor.is_del == 0 else None
@property
def score_count(self) -> int:
return len([s for s in self.scores if s.is_del == 0])
@property
def avg_score(self) -> float | None:
valid = [float(s.score) for s in self.scores if s.is_del == 0 and s.score is not None]
return round(sum(valid) / len(valid), 2) if valid else None
@property
def fail_count(self) -> int:
return len([s for s in self.scores if s.is_del == 0 and s.flag == 1])