You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

324 lines
11 KiB

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

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