"""学生模块出入参(需求 2.1)。""" from __future__ import annotations from datetime import date from typing import Any from pydantic import BaseModel, Field, field_validator, model_validator from app.model.constants import Education from app.schema.common import GENDER_VALIDATOR, ORMBase, STRIP_VALIDATOR class _StudentBase(BaseModel): name: str = Field(..., min_length=1, max_length=30, description="姓名") gender: int = Field(1, description="性别 1=男 2=女(也接受 男/女)") birth_date: str | None = Field(None, description="出生日期 YYYY-MM-DD") age: int | None = Field( None, ge=10, le=80, description="年龄。只在拿不到生日时用:填了 age 没填 birth_date,系统按当年推算一个占位生日", ) native_place: str | None = Field(None, max_length=60, description="籍贯") graduate_school: str | None = Field(None, max_length=80, description="毕业院校") major: str | None = Field(None, max_length=60, description="专业") education: str | None = Field(None, max_length=20, description="学历,如 大专/本科", examples=[Education.BACHELOR.value]) enroll_date: str | None = Field(None, description="入学时间 YYYY-MM-DD") graduate_date: str | None = Field(None, description="毕业时间 YYYY-MM-DD") phone: str | None = Field(None, max_length=20, description="联系电话") id_card: str | None = Field(None, max_length=30, description="身份证号") class_id: int | None = Field(None, description="所属班级ID") advisor_id: int | None = Field(None, description="顾问ID") remark: str | None = Field(None, description="备注") _v_gender = GENDER_VALIDATOR _v_strip = STRIP_VALIDATOR @field_validator("birth_date", "enroll_date", "graduate_date", mode="before") @classmethod def _date_like(cls, v): # noqa: ANN001, ANN201 """把 date/datetime 统一成 str,真正的解析放在 API 层用 parse_date。""" if v is None or isinstance(v, str): return v if isinstance(v, date): return v.isoformat() return v class StudentCreate(_StudentBase): stu_no: str | None = Field(None, max_length=30, description="学号。不传则按规则自动生成") status: int | None = Field(None, ge=1, le=3, description="状态 1=在读 2=进入就业 3=已就业,默认在读") @model_validator(mode="after") def _check_age_source(self): if self.birth_date is None and self.age is None: raise ValueError("birth_date 和 age 至少要填一个(用于年龄统计)") return self class StudentUpdate(BaseModel): """全部字段可选,只更新传了的字段。""" name: str | None = Field(None, min_length=1, max_length=30, description="姓名") gender: int | None = Field(None, description="性别 1=男 2=女") birth_date: str | None = Field(None, description="出生日期 YYYY-MM-DD") age: int | None = Field(None, description="年龄(会折算成占位生日)") native_place: str | None = Field(None, max_length=60, description="籍贯") graduate_school: str | None = Field(None, max_length=80, description="毕业院校") major: str | None = Field(None, max_length=60, description="专业") education: str | None = Field(None, max_length=20, description="学历") enroll_date: str | None = Field(None, description="入学时间 YYYY-MM-DD") graduate_date: str | None = Field(None, description="毕业时间 YYYY-MM-DD") phone: str | None = Field(None, max_length=20, description="联系电话") id_card: str | None = Field(None, max_length=30, description="身份证号") class_id: int | None = Field(None, description="所属班级ID(转班会同步就业表的冗余班级)") advisor_id: int | None = Field(None, description="顾问ID") remark: str | None = Field(None, description="备注") _v_gender = GENDER_VALIDATOR _v_strip = STRIP_VALIDATOR class StudentBrief(ORMBase): id: int stu_no: str name: str gender_text: str | None = None class_name: str | None = None status_text: str class StudentOut(ORMBase): id: int stu_no: str = Field(..., description="学号") name: str gender: int gender_text: str | None = Field(None, description="性别文案") age: int | None = Field(None, description="年龄(按出生日期实时计算)") birth_date: date | None = None birth_date_estimated: int = Field(0, description="生日是否为推算值 1=是") native_place: str | None = None graduate_school: str | None = None major: str | None = None education: str | None = None enroll_date: date | None = None graduate_date: date | None = None phone: str | None = None id_card: str | None = None class_id: int | None = None class_no: str | None = Field(None, description="班级编号") class_name: str | None = Field(None, description="班级名称(JOIN 得出)") advisor_id: int | None = None advisor_name: str | None = None status: int status_text: str remark: str | None = None score_count: int = Field(0, description="已录入的考核次数") avg_score: float | None = Field(None, description="平均分") fail_count: int = Field(0, description="低于红线的次数") class StudentImportResult(BaseModel): total: int = Field(0, description="文件里的数据行数") success: int = Field(0, description="成功导入") failed: int = Field(0, description="失败行数") errors: list[dict[str, Any]] = Field(default_factory=list, description="失败明细:行号 + 原因")