"""就业信息表(需求 2.3)。 关于「学生姓名、学生班级属于冗余字段」的处理 -------------------------------------------------------------------------- 需求点名要自行设计表结构来平衡查询效率与一致性。这里的取舍: * **学生姓名不落库。** 它是 student.name 的投影,1:1 关系, JOIN 一次的成本可以忽略;而一旦落库,改名就会两处不一致,收益为零。查询接口 统一 `JOIN student` 取名字。 * **班级落库(class_id 而非 class_name)。** 因为"按班级筛就业、按班级算就业率/ 平均就业时长"是高频统计场景,每次 JOIN student 再 JOIN clazz 成本更高; 而且存 id 不存名字,班级改名不会导致脏数据。 代价是学生转班时要同步——由 ``EmploymentService.sync_class_id()`` 在学生 接口里兜住,不给"记得改"留口子。 * **就业时长不落库**,= offer_date - open_date,用 SQL DATEDIFF 或 Python 现算。 状态联动(需求 2.3 可选扩展,这里是必做): 登记就业 -> 有 open_date 则学生 status=2(进入就业), -> 有 offer_date 则学生 status=3(已就业) 删除就业记录 -> 学生状态回退(3 -> 2 -> 1),不允许跨级跳。 """ from __future__ import annotations from datetime import date from decimal import Decimal from sqlalchemy import Date, ForeignKey, Numeric, String, Text from sqlalchemy.orm import Mapped, mapped_column, relationship from app.model.base import Base, SoftDeleteMixin, TimestampMixin class Employment(Base, TimestampMixin, SoftDeleteMixin): __tablename__ = "employment" __table_args__ = {"comment": "学生就业信息表"} id: Mapped[int] = mapped_column(primary_key=True, autoincrement=True) stu_id: Mapped[int] = mapped_column( ForeignKey("student.id"), unique=True, nullable=False, index=True, comment="学生ID(一学生一条就业记录)", ) class_id: Mapped[int | None] = mapped_column( ForeignKey("clazz.id"), index=True, comment="班级ID(冗余,用于按班统计,转班时由 service 同步)", ) open_date: Mapped[date | None] = mapped_column(Date, comment="就业开放时间") offer_date: Mapped[date | None] = mapped_column(Date, comment="offer 下发时间") company: Mapped[str | None] = mapped_column(String(80), index=True, comment="就业公司名称") salary: Mapped[Decimal | None] = mapped_column(Numeric(10, 2), index=True, comment="就业薪资(元/月)") position: Mapped[str | None] = mapped_column(String(50), comment="岗位名称") city: Mapped[str | None] = mapped_column(String(30), comment="就业城市") remark: Mapped[str | None] = mapped_column(Text, comment="备注") student: Mapped["Student"] = relationship(back_populates="employment", lazy="joined") # noqa: F821 klass: Mapped["Clazz"] = relationship(lazy="joined", foreign_keys=[class_id]) # noqa: F821 # ---------------- 出参派生字段 ---------------- @property def student_name(self) -> str | None: return self.student.name if self.student else None @property def stu_no(self) -> str | None: return self.student.stu_no if self.student else None @property def student_status(self) -> int | None: return self.student.status if self.student else None @property def student_status_text(self) -> str | None: return self.student.status_text if self.student else None @property def class_name(self) -> str | None: # 冗余字段只存了 id,名字实时取,保证与班级表一致 return self.klass.name if self.klass and self.klass.is_del == 0 else None @property def duration_days(self) -> int | None: """就业时长 = offer 下发时间 - 就业开放时间(天)。""" if self.open_date and self.offer_date: return (self.offer_date - self.open_date).days return None @property def salary_wan(self) -> float | None: return round(float(self.salary) / 10000, 2) if self.salary is not None else None