91 lines
4.1 KiB
Python
91 lines
4.1 KiB
Python
"""就业信息表(需求 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
|