代码提交

This commit is contained in:
xwt
2026-09-21 19:03:31 +08:00
parent 34d656492c
commit 97986d9a1a
217 changed files with 12042 additions and 0 deletions
+113
View File
@@ -0,0 +1,113 @@
"""学生表(需求 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])