|
|
|
|
|
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)})" |