from datetime import date from typing import List, Optional, Dict, Union import re class Student: def __init__(self, name: str, id_card: str, stu_id: str, gender: Optional[bool] = None, height: Optional[int] = None, weight: Optional[float] = None, enrollment_date: Optional[Union[date, str]] = None, class_name: Optional[str] = None, major: Optional[str] = None, email: Optional[str] = None, phone: Optional[str] = None): # 初始化错误字典 self._errors: Dict[str, List[str]] = {} # 基本属性(必填) self.name = name.strip() # 数据清洗:去除首尾空格 self.id_card = id_card.strip().upper() # 统一转为大写 self.stu_id = stu_id.strip() # 可选属性 self.gender = gender self.height = height self.weight = round(weight, 1) if weight is not None else None self.email = email.strip() if email else None self.phone = phone.strip() if phone else None # 解析入学日期 self.enrollment_date = None if enrollment_date is not None: if isinstance(enrollment_date, date): self.enrollment_date = enrollment_date elif isinstance(enrollment_date, str): try: self.enrollment_date = date.fromisoformat(enrollment_date) except ValueError: self._add_error('enrollment_date', '入学日期格式无效,应为YYYY-MM-DD') else: self._add_error('enrollment_date', '入学日期类型错误,应为date对象或字符串') self.class_name = class_name.strip() if class_name else None self.major = major.strip() if major else None # 执行校验 self._validate() def _validate(self) -> None: """执行所有校验""" self._validate_name() self._validate_id_card() self._validate_stu_id() self._validate_height() self._validate_weight() self._validate_enrollment_date() self._validate_email() self._validate_phone() def _validate_name(self) -> None: """验证姓名格式""" if not self.name: self._add_error('name', '姓名不能为空') return if not (2 <= len(self.name) <= 20): self._add_error('name', '姓名长度需在2-20个字符之间') # 允许空格但必须是可打印字符 if not all(c.isprintable() or c.isspace() for c in self.name): self._add_error('name', '姓名包含非法字符') def _validate_id_card(self) -> None: """验证身份证号格式""" # 长度校验 if len(self.id_card) != 18: self._add_error('id_card', f'身份证号应为18位,当前为{len(self.id_card)}位') return # 前17位必须为数字 if not self.id_card[:17].isdigit(): self._add_error('id_card', '身份证号前17位必须为数字') return # 提前返回,避免后续操作出错 # 最后一位校验 last_char = self.id_card[17] if last_char not in '0123456789X': self._add_error('id_card', f'身份证号最后一位无效: {last_char}') return # 校验码验证 weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] check_codes = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] total = sum(int(d) * w for d, w in zip(self.id_card[:17], weights)) calculated_code = check_codes[total % 11] if calculated_code != last_char: self._add_error('id_card', f'校验码错误,应为{calculated_code},实际为{last_char}') return # 校验码错误时不进行后续验证 # 出生日期校验 birth_str = self.id_card[6:14] try: year = int(birth_str[0:4]) month = int(birth_str[4:6]) day = int(birth_str[6:8]) # 尝试创建日期对象验证有效性 birth_date = date(year, month, day) # 检查出生日期是否合理 today = date.today() if birth_date > today: self._add_error('id_card', f'出生日期不能在未来: {birth_str}') elif birth_date.year < 1900: self._add_error('id_card', f'出生年份不合理: {birth_str}') except ValueError: self._add_error('id_card', f'无效的出生日期: {birth_str}') def _validate_stu_id(self) -> None: """验证学号""" if not self.stu_id: self._add_error('stu_id', '学号不能为空') def _validate_height(self) -> None: """验证身高是否在合理范围内""" if self.height is None: return if not (50 <= self.height <= 250): self._add_error('height', f'身高{self.height}cm超出合理范围(50-250cm)') def _validate_weight(self) -> None: """验证体重是否在合理范围内""" if self.weight is None: return if not (5.0 <= self.weight <= 300.0): self._add_error('weight', f'体重{self.weight}kg超出合理范围(5-300kg)') def _validate_enrollment_date(self) -> None: """验证入学日期""" if self.enrollment_date is None: return today = date.today() if self.enrollment_date > today: self._add_error('enrollment_date', '入学日期不能晚于当前日期') # 如果出生日期有效,检查入学日期是否晚于出生日期 birthday = self.birthday if birthday: if self.enrollment_date < birthday: self._add_error('enrollment_date', '入学日期不能早于出生日期') def _validate_email(self) -> None: """验证邮箱格式(选做)""" if not self.email: return pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$' if not re.match(pattern, self.email): self._add_error('email', '邮箱格式无效') def _validate_phone(self) -> None: """验证手机号格式(选做)""" if not self.phone: return pattern = r'^1[3-9]\d{9}$' if not re.match(pattern, self.phone): self._add_error('phone', '手机号格式无效(应为11位数字)') def _add_error(self, field: str, message: str) -> None: """添加错误信息 :rtype: object """ if field not in self._errors: self._errors[field] = [] self._errors[field].append(message) @property def birthday(self) -> Optional[date]: """从身份证号解析出生日期""" if len(self.id_card) < 14: return None birth_str = self.id_card[6:14] try: return date( int(birth_str[0:4]), int(birth_str[4:6]), int(birth_str[6:8])) except (ValueError, TypeError): return None @property def birth_date(self) -> Optional[date]: """birthday的别名""" return self.birthday @property def age(self) -> Optional[int]: """计算年龄""" birthday = self.birthday if not birthday: return None today = date.today() age = today.year - birthday.year # 如果生日还没到,减一岁 if (today.month, today.day) < (birthday.month, birthday.day): age -= 1 return age @property def sid(self) -> str: """stu_id的别名""" return self.stu_id @property def errors(self) -> dict: """获取所有错误信息""" return self._errors.copy() def get_errors(self) -> dict: """获取错误信息(方法形式)""" return self.errors @property def is_valid(self) -> bool: """数据有效性标记""" return not bool(self._errors) # 对象比较方法 def __eq__(self, other) -> bool: if not isinstance(other, Student): return NotImplemented return ( self.name == other.name and self.id_card == other.id_card and self.stu_id == other.stu_id and self.gender == other.gender and self.height == other.height and self.weight == other.weight and self.enrollment_date == other.enrollment_date and self.class_name == other.class_name and self.major == other.major ) # 序列化方法 def to_dict(self) -> dict: """将对象序列化为字典""" return { 'name': self.name, 'id_card': self.id_card, 'stu_id': self.stu_id, 'gender': self.gender, 'height': self.height, 'weight': self.weight, 'enrollment_date': self.enrollment_date.isoformat() if self.enrollment_date else None, 'class_name': self.class_name, 'major': self.major, 'email': self.email, 'phone': self.phone } # 反序列化方法 @classmethod def from_dict(cls, data: dict) -> 'Student': """从字典创建对象""" if not isinstance(data, dict): raise TypeError("输入数据必须是字典类型") return cls( name=data.get('name', ''), id_card=data.get('id_card', ''), stu_id=data.get('stu_id', ''), gender=data.get('gender'), height=data.get('height'), weight=data.get('weight'), enrollment_date=data.get('enrollment_date'), class_name=data.get('class_name'), major=data.get('major'), email=data.get('email'), phone=data.get('phone') ) @classmethod def get_field_names(cls) -> List[str]: """获取所有字段名称(包括别名)""" return [ 'name', 'id_card', 'stu_id', 'sid', 'gender', 'height', 'weight', 'enrollment_date', 'class_name', 'major', 'birthday', 'birth_date', 'age', 'email', 'phone' ] def __repr__(self) -> str: """修复被截断的__repr__方法""" attrs = [ f"name='{self.name}'", f"id_card='{self.id_card}'", f"stu_id='{self.stu_id}'", f"sid='{self.sid}'", f"gender={self.gender}", f"height={self.height}", f"weight={self.weight}", f"enrollment_date={self.enrollment_date.isoformat() if self.enrollment_date else None}", f"class_name='{self.class_name}'" if self.class_name else "class_name=None", f"major='{self.major}'" if self.major else "major=None", f"birthday={self.birthday.isoformat() if self.birthday else None}", f"birth_date={self.birth_date.isoformat() if self.birth_date else None}", f"age={self.age}", f"email='{self.email}'" if self.email else "email=None", f"phone='{self.phone}'" if self.phone else "phone=None" ] return f"Student({', '.join(attrs)})"