|
|
import json
|
|
|
import csv
|
|
|
import os
|
|
|
from typing import List, Optional, Dict, Union, Any
|
|
|
from datetime import date, datetime
|
|
|
from abc import ABC, abstractmethod
|
|
|
from enum import Enum
|
|
|
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):
|
|
|
"""初始化学生对象"""
|
|
|
self.name = name
|
|
|
self.id_card = id_card
|
|
|
self.stu_id = stu_id
|
|
|
self.gender = gender
|
|
|
self.height = height
|
|
|
self.weight = weight
|
|
|
self.enrollment_date = enrollment_date
|
|
|
self.class_name = class_name
|
|
|
self.major = major
|
|
|
|
|
|
# 从身份证号生成的属性
|
|
|
self._birthday = None
|
|
|
self._age = None
|
|
|
|
|
|
# 错误信息字典
|
|
|
self._errors = {}
|
|
|
|
|
|
# 初始化时进行数据校验
|
|
|
self.validate()
|
|
|
|
|
|
@property
|
|
|
def birthday(self) -> date:
|
|
|
"""从身份证号提取出生日期"""
|
|
|
if not self._birthday and self.id_card and len(self.id_card) == 18:
|
|
|
birth_str = self.id_card[6:14]
|
|
|
try:
|
|
|
self._birthday = date.fromisoformat(birth_str)
|
|
|
except:
|
|
|
self._birthday = date(1900, 1, 1)
|
|
|
return self._birthday
|
|
|
|
|
|
@property
|
|
|
def age(self) -> int:
|
|
|
"""计算年龄"""
|
|
|
if not self._age and self.birthday:
|
|
|
today = date.today()
|
|
|
self._age = today.year - self.birthday.year
|
|
|
if today < date(today.year, self.birthday.month, self.birthday.day):
|
|
|
self._age -= 1
|
|
|
return self._age
|
|
|
|
|
|
@property
|
|
|
def enrollment_date(self) -> Optional[date]:
|
|
|
"""入学日期属性"""
|
|
|
return self._enrollment_date
|
|
|
|
|
|
@enrollment_date.setter
|
|
|
def enrollment_date(self, value: Optional[Union[date, str]]):
|
|
|
"""入学日期设置器,支持日期对象或字符串"""
|
|
|
if value is None:
|
|
|
self._enrollment_date = None
|
|
|
elif isinstance(value, str):
|
|
|
try:
|
|
|
self._enrollment_date = date.fromisoformat(value)
|
|
|
except:
|
|
|
self._enrollment_date = None
|
|
|
else:
|
|
|
self._enrollment_date = value
|
|
|
|
|
|
@property
|
|
|
def errors(self) -> Dict[str, str]:
|
|
|
"""获取所有验证错误信息"""
|
|
|
return self._errors
|
|
|
|
|
|
@property
|
|
|
def is_valid(self) -> bool:
|
|
|
"""判断学生信息是否有效"""
|
|
|
return not bool(self._errors)
|
|
|
|
|
|
def validate(self) -> None:
|
|
|
"""验证学生信息的合法性"""
|
|
|
self._errors = {}
|
|
|
|
|
|
# 验证姓名
|
|
|
if not self.name or len(self.name) < 2 or len(self.name) > 20:
|
|
|
self._errors["name"] = "姓名必须在2到20个字符之间"
|
|
|
elif not self.name.isalpha():
|
|
|
self._errors["name"] = "姓名不能包含数字和特殊符号"
|
|
|
|
|
|
# 验证身份证号
|
|
|
if not self.__validate_id_card(self.id_card):
|
|
|
self._errors["id_card"] = "身份证号格式不正确"
|
|
|
|
|
|
# 验证学号
|
|
|
if not self.stu_id:
|
|
|
self._errors["stu_id"] = "学号不能为空"
|
|
|
|
|
|
# 验证身高
|
|
|
if self.height is not None and (self.height < 50 or self.height > 250):
|
|
|
self._errors["height"] = "身高必须在50到250厘米之间"
|
|
|
|
|
|
# 验证体重
|
|
|
if self.weight is not None and (self.weight < 5 or self.weight > 300):
|
|
|
self._errors["weight"] = "体重必须在5到300千克之间"
|
|
|
|
|
|
# 验证入学日期
|
|
|
if self.enrollment_date and self.birthday and self.enrollment_date < self.birthday:
|
|
|
self._errors["enrollment_date"] = "入学日期不能早于出生日期"
|
|
|
|
|
|
@staticmethod
|
|
|
def __validate_id_card(id_card: str) -> bool:
|
|
|
"""验证身份证号是否符合国家标准"""
|
|
|
if not id_card or len(id_card) != 18:
|
|
|
return False
|
|
|
|
|
|
# 检查前17位是否为数字
|
|
|
if not id_card[:17].isdigit():
|
|
|
return False
|
|
|
|
|
|
# 验证出生日期
|
|
|
birth_str = id_card[6:14]
|
|
|
try:
|
|
|
date.fromisoformat(birth_str)
|
|
|
except:
|
|
|
return False
|
|
|
|
|
|
# 验证校验位
|
|
|
factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] # 加权因子
|
|
|
check_codes = '10X98765432' # 校验码映射
|
|
|
total = sum(int(id_card[i]) * factors[i] for i in range(17))
|
|
|
check_code = check_codes[total % 11]
|
|
|
|
|
|
return check_code == id_card[17].upper()
|
|
|
|
|
|
def to_dict(self) -> Dict[str, Any]:
|
|
|
"""将学生对象转换为字典"""
|
|
|
result = self.__dict__.copy()
|
|
|
# 处理日期类型
|
|
|
if result.get('_enrollment_date'):
|
|
|
result['enrollment_date'] = result['_enrollment_date'].isoformat()
|
|
|
# 移除内部属性
|
|
|
if '_birthday' in result:
|
|
|
del result['_birthday']
|
|
|
if '_age' in result:
|
|
|
del result['_age']
|
|
|
if '_errors' in result:
|
|
|
del result['_errors']
|
|
|
return result
|
|
|
|
|
|
@classmethod
|
|
|
def from_dict(cls, data: Dict[str, Any]) -> 'Student':
|
|
|
"""从字典创建学生对象"""
|
|
|
if not isinstance(data, dict):
|
|
|
raise TypeError("数据必须是字典类型")
|
|
|
|
|
|
# 处理入学日期
|
|
|
enrollment_date = data.get('enrollment_date')
|
|
|
if enrollment_date and isinstance(enrollment_date, str):
|
|
|
try:
|
|
|
data['enrollment_date'] = date.fromisoformat(enrollment_date)
|
|
|
except:
|
|
|
data['enrollment_date'] = None
|
|
|
|
|
|
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')
|
|
|
)
|
|
|
|
|
|
def __repr__(self) -> str:
|
|
|
"""学生对象的字符串表示"""
|
|
|
attrs = [f"{k}='{v}'" for k, v in self.to_dict().items() if v is not None]
|
|
|
return f"Student({', '.join(attrs)})"
|
|
|
|
|
|
|
|
|
# ============== 数据访问层 ==============
|
|
|
class IStudentDAL(ABC):
|
|
|
"""学生信息数据访问层接口"""
|
|
|
|
|
|
@abstractmethod
|
|
|
def get_by_id(self, id_card: str) -> Optional[Student]:
|
|
|
"""根据身份证号获取学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
|
"""根据学号获取学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def get_all(self) -> List[Student]:
|
|
|
"""获取所有学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def add(self, student: Student) -> bool:
|
|
|
"""添加学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def delete(self, id_card: str) -> bool:
|
|
|
"""根据身份证号删除学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def delete_by_stu_id(self, stu_id: str) -> bool:
|
|
|
"""根据学号删除学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def update(self, student: Student) -> bool:
|
|
|
"""更新学生信息"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def is_exist(self, id_card: str) -> bool:
|
|
|
"""检查学生是否存在"""
|
|
|
pass
|
|
|
|
|
|
@abstractmethod
|
|
|
def is_exist_stu_id(self, stu_id: str) -> bool:
|
|
|
"""检查学号是否存在"""
|
|
|
pass
|
|
|
|
|
|
|
|
|
class JsonStudentDAL(IStudentDAL):
|
|
|
"""JSON文件数据访问实现"""
|
|
|
|
|
|
def __init__(self, file_path: str):
|
|
|
"""初始化JSON数据访问层"""
|
|
|
self.file_path = file_path
|
|
|
self._ensure_file_exists()
|
|
|
|
|
|
def _ensure_file_exists(self) -> None:
|
|
|
"""确保数据文件存在"""
|
|
|
if not os.path.exists(self.file_path):
|
|
|
with open(self.file_path, 'w', encoding='utf-8') as f:
|
|
|
json.dump([], f, ensure_ascii=False, indent=4)
|
|
|
|
|
|
def _load(self) -> List[Dict[str, Any]]:
|
|
|
"""从文件加载学生数据"""
|
|
|
try:
|
|
|
with open(self.file_path, 'r', encoding='utf-8') as f:
|
|
|
return json.load(f)
|
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
|
return []
|
|
|
|
|
|
def _save(self, students: List[Dict[str, Any]]) -> None:
|
|
|
"""保存学生数据到文件"""
|
|
|
with open(self.file_path, 'w', encoding='utf-8') as f:
|
|
|
json.dump(students, f, ensure_ascii=False, indent=4)
|
|
|
|
|
|
def get_by_id(self, id_card: str) -> Optional[Student]:
|
|
|
"""根据身份证号获取学生信息"""
|
|
|
students = self._load()
|
|
|
for data in students:
|
|
|
if data.get('id_card') == id_card:
|
|
|
return Student.from_dict(data)
|
|
|
return None
|
|
|
|
|
|
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
|
"""根据学号获取学生信息"""
|
|
|
students = self._load()
|
|
|
for data in students:
|
|
|
if data.get('stu_id') == stu_id:
|
|
|
return Student.from_dict(data)
|
|
|
return None
|
|
|
|
|
|
def get_all(self) -> List[Student]:
|
|
|
"""获取所有学生信息"""
|
|
|
students_data = self._load()
|
|
|
return [Student.from_dict(data) for data in students_data]
|
|
|
|
|
|
def add(self, student: Student) -> bool:
|
|
|
"""添加学生信息"""
|
|
|
if self.is_exist(student.id_card) or self.is_exist_stu_id(student.stu_id):
|
|
|
return False
|
|
|
|
|
|
students = self._load()
|
|
|
students.append(student.to_dict())
|
|
|
self._save(students)
|
|
|
return True
|
|
|
|
|
|
def delete(self, id_card: str) -> bool:
|
|
|
"""根据身份证号删除学生信息"""
|
|
|
students = self._load()
|
|
|
original_length = len(students)
|
|
|
students = [s for s in students if s.get('id_card') != id_card]
|
|
|
|
|
|
if len(students) < original_length:
|
|
|
self._save(students)
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def delete_by_stu_id(self, stu_id: str) -> bool:
|
|
|
"""根据学号删除学生信息"""
|
|
|
students = self._load()
|
|
|
original_length = len(students)
|
|
|
students = [s for s in students if s.get('stu_id') != stu_id]
|
|
|
|
|
|
if len(students) < original_length:
|
|
|
self._save(students)
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def update(self, student: Student) -> bool:
|
|
|
"""更新学生信息"""
|
|
|
students = self._load()
|
|
|
updated = False
|
|
|
|
|
|
for i, data in enumerate(students):
|
|
|
if data.get('id_card') == student.id_card:
|
|
|
students[i] = student.to_dict()
|
|
|
updated = True
|
|
|
break
|
|
|
|
|
|
if updated:
|
|
|
self._save(students)
|
|
|
return updated
|
|
|
|
|
|
def is_exist(self, id_card: str) -> bool:
|
|
|
"""检查学生是否存在"""
|
|
|
return self.get_by_id(id_card) is not None
|
|
|
|
|
|
def is_exist_stu_id(self, stu_id: str) -> bool:
|
|
|
"""检查学号是否存在"""
|
|
|
return self.get_by_stu_id(stu_id) is not None
|
|
|
|
|
|
|
|
|
class CsvStudentDAL(IStudentDAL):
|
|
|
"""CSV文件数据访问实现"""
|
|
|
|
|
|
FIELD_TYPES = {
|
|
|
'height': int,
|
|
|
'weight': float,
|
|
|
'gender': lambda x: True if x == 'True' else False if x == 'False' else None,
|
|
|
}
|
|
|
|
|
|
def __init__(self, file_path: str):
|
|
|
"""初始化CSV数据访问层"""
|
|
|
self.file_path = file_path
|
|
|
self._ensure_file_exists()
|
|
|
|
|
|
def _ensure_file_exists(self) -> None:
|
|
|
"""确保数据文件存在"""
|
|
|
if not os.path.exists(self.file_path):
|
|
|
with open(self.file_path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
|
writer = csv.DictWriter(f, fieldnames=self._get_fieldnames())
|
|
|
writer.writeheader()
|
|
|
|
|
|
def _get_fieldnames(self) -> List[str]:
|
|
|
"""获取CSV文件字段名"""
|
|
|
return [
|
|
|
'name', 'id_card', 'stu_id', 'gender', 'height',
|
|
|
'weight', 'enrollment_date', 'class_name', 'major'
|
|
|
]
|
|
|
|
|
|
def _convert_row(self, row: Dict[str, str]) -> Dict[str, Any]:
|
|
|
"""转换CSV行数据为字典"""
|
|
|
result = {}
|
|
|
for key, value in row.items():
|
|
|
if not value:
|
|
|
result[key] = None
|
|
|
else:
|
|
|
converter = self.FIELD_TYPES.get(key)
|
|
|
if converter:
|
|
|
result[key] = converter(value)
|
|
|
elif key == 'enrollment_date':
|
|
|
try:
|
|
|
result[key] = date.fromisoformat(value)
|
|
|
except:
|
|
|
result[key] = value
|
|
|
else:
|
|
|
result[key] = value
|
|
|
return result
|
|
|
|
|
|
def _load(self) -> List[Dict[str, Any]]:
|
|
|
"""从文件加载学生数据"""
|
|
|
try:
|
|
|
with open(self.file_path, 'r', encoding='utf-8-sig', newline='') as f:
|
|
|
reader = csv.DictReader(f)
|
|
|
return [self._convert_row(row) for row in reader]
|
|
|
except FileNotFoundError:
|
|
|
return []
|
|
|
|
|
|
def _save(self, students: List[Dict[str, Any]]) -> None:
|
|
|
"""保存学生数据到文件"""
|
|
|
with open(self.file_path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
|
writer = csv.DictWriter(f, fieldnames=self._get_fieldnames())
|
|
|
writer.writeheader()
|
|
|
for student in students:
|
|
|
# 转换日期为字符串
|
|
|
if student.get('enrollment_date') and isinstance(student['enrollment_date'], date):
|
|
|
student['enrollment_date'] = student['enrollment_date'].isoformat()
|
|
|
writer.writerow(student)
|
|
|
|
|
|
def get_by_id(self, id_card: str) -> Optional[Student]:
|
|
|
"""根据身份证号获取学生信息"""
|
|
|
students = self._load()
|
|
|
for data in students:
|
|
|
if data.get('id_card') == id_card:
|
|
|
return Student.from_dict(data)
|
|
|
return None
|
|
|
|
|
|
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
|
"""根据学号获取学生信息"""
|
|
|
students = self._load()
|
|
|
for data in students:
|
|
|
if data.get('stu_id') == stu_id:
|
|
|
return Student.from_dict(data)
|
|
|
return None
|
|
|
|
|
|
def get_all(self) -> List[Student]:
|
|
|
"""获取所有学生信息"""
|
|
|
students_data = self._load()
|
|
|
return [Student.from_dict(data) for data in students_data]
|
|
|
|
|
|
def add(self, student: Student) -> bool:
|
|
|
"""添加学生信息"""
|
|
|
if self.is_exist(student.id_card) or self.is_exist_stu_id(student.stu_id):
|
|
|
return False
|
|
|
|
|
|
students = self._load()
|
|
|
students.append(student.to_dict())
|
|
|
self._save(students)
|
|
|
return True
|
|
|
|
|
|
def delete(self, id_card: str) -> bool:
|
|
|
"""根据身份证号删除学生信息"""
|
|
|
students = self._load()
|
|
|
original_length = len(students)
|
|
|
students = [s for s in students if s.get('id_card') != id_card]
|
|
|
|
|
|
if len(students) < original_length:
|
|
|
self._save(students)
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def delete_by_stu_id(self, stu_id: str) -> bool:
|
|
|
"""根据学号删除学生信息"""
|
|
|
students = self._load()
|
|
|
original_length = len(students)
|
|
|
students = [s for s in students if s.get('stu_id') != stu_id]
|
|
|
|
|
|
if len(students) < original_length:
|
|
|
self._save(students)
|
|
|
return True
|
|
|
return False
|
|
|
|
|
|
def update(self, student: Student) -> bool:
|
|
|
"""更新学生信息"""
|
|
|
students = self._load()
|
|
|
updated = False
|
|
|
|
|
|
for i, data in enumerate(students):
|
|
|
if data.get('id_card') == student.id_card:
|
|
|
students[i] = student.to_dict()
|
|
|
updated = True
|
|
|
break
|
|
|
|
|
|
if updated:
|
|
|
self._save(students)
|
|
|
return updated
|
|
|
|
|
|
def is_exist(self, id_card: str) -> bool:
|
|
|
"""检查学生是否存在"""
|
|
|
return self.get_by_id(id_card) is not None
|
|
|
|
|
|
def is_exist_stu_id(self, stu_id: str) -> bool:
|
|
|
"""检查学号是否存在"""
|
|
|
return self.get_by_stu_id(stu_id) is not None
|
|
|
|
|
|
|
|
|
# ============== 业务逻辑层 ==============
|
|
|
class StudentBLL:
|
|
|
"""学生信息业务逻辑层"""
|
|
|
|
|
|
def __init__(self, file_path: str):
|
|
|
"""初始化业务逻辑层"""
|
|
|
self.file_path = file_path
|
|
|
self.dal = self._get_dal()
|
|
|
|
|
|
def _get_dal(self) -> IStudentDAL:
|
|
|
"""获取数据访问层实例"""
|
|
|
if self.file_path.endswith('.json'):
|
|
|
return JsonStudentDAL(self.file_path)
|
|
|
elif self.file_path.endswith('.csv'):
|
|
|
return CsvStudentDAL(self.file_path)
|
|
|
else:
|
|
|
raise ValueError("不支持的文件格式,仅支持JSON和CSV")
|
|
|
|
|
|
def add_student(self, student: Student) -> bool:
|
|
|
"""添加学生信息"""
|
|
|
# 校验学生信息
|
|
|
if not student.is_valid:
|
|
|
raise ValueError(f"学生信息无效: {student.errors}")
|
|
|
|
|
|
# 检查唯一性
|
|
|
if self.dal.is_exist(student.id_card):
|
|
|
raise ValueError("该身份证号已存在")
|
|
|
if self.dal.is_exist_stu_id(student.stu_id):
|
|
|
raise ValueError("该学号已存在")
|
|
|
|
|
|
return self.dal.add(student)
|
|
|
|
|
|
def delete_student(self, id_card: str) -> bool:
|
|
|
"""根据身份证号删除学生"""
|
|
|
if not self.dal.is_exist(id_card):
|
|
|
raise ValueError("学生不存在")
|
|
|
return self.dal.delete(id_card)
|
|
|
|
|
|
def delete_student_by_stu_id(self, stu_id: str) -> bool:
|
|
|
"""根据学号删除学生"""
|
|
|
if not self.dal.is_exist_stu_id(stu_id):
|
|
|
raise ValueError("学生不存在")
|
|
|
return self.dal.delete_by_stu_id(stu_id)
|
|
|
|
|
|
def update_student(self, student: Student) -> bool:
|
|
|
"""更新学生信息"""
|
|
|
if not student.is_valid:
|
|
|
raise ValueError(f"学生信息无效: {student.errors}")
|
|
|
|
|
|
if not self.dal.is_exist(student.id_card):
|
|
|
raise ValueError("学生不存在")
|
|
|
|
|
|
return self.dal.update(student)
|
|
|
|
|
|
def update_student_partial(self, id_card: str, **kwargs) -> bool:
|
|
|
"""部分更新学生信息"""
|
|
|
student = self.dal.get_by_id(id_card)
|
|
|
if not student:
|
|
|
raise ValueError("学生不存在")
|
|
|
|
|
|
# 更新提供的字段
|
|
|
for key, value in kwargs.items():
|
|
|
if hasattr(student, key) and key not in ['id_card', 'stu_id']:
|
|
|
setattr(student, key, value)
|
|
|
|
|
|
# 校验更新后的信息
|
|
|
student.validate()
|
|
|
if student.errors:
|
|
|
raise ValueError(f"更新后学生信息无效: {student.errors}")
|
|
|
|
|
|
return self.dal.update(student)
|
|
|
|
|
|
def get_student_by_id(self, id_card: str) -> Optional[Student]:
|
|
|
"""根据身份证号查询学生"""
|
|
|
return self.dal.get_by_id(id_card)
|
|
|
|
|
|
def get_student_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
|
"""根据学号查询学生"""
|
|
|
return self.dal.get_by_stu_id(stu_id)
|
|
|
|
|
|
def get_all_students(self) -> List[Student]:
|
|
|
"""获取所有学生"""
|
|
|
return self.dal.get_all()
|
|
|
|
|
|
def search_by_name(self, name: str, fuzzy: bool = True) -> List[Student]:
|
|
|
"""按姓名查询学生"""
|
|
|
all_students = self.get_all_students()
|
|
|
if fuzzy:
|
|
|
return [s for s in all_students if name.lower() in s.name.lower()]
|
|
|
return [s for s in all_students if s.name == name]
|
|
|
|
|
|
def search_by_class(self, class_name: str, fuzzy: bool = True) -> List[Student]:
|
|
|
"""按班级查询学生"""
|
|
|
all_students = self.get_all_students()
|
|
|
if fuzzy:
|
|
|
return [s for s in all_students if class_name.lower() in (s.class_name or '').lower()]
|
|
|
return [s for s in all_students if s.class_name == class_name]
|
|
|
|
|
|
def search_by_major(self, major: str, fuzzy: bool = True) -> List[Student]:
|
|
|
"""按专业查询学生"""
|
|
|
all_students = self.get_all_students()
|
|
|
if fuzzy:
|
|
|
return [s for s in all_students if major.lower() in (s.major or '').lower()]
|
|
|
return [s for s in all_students if s.major == major]
|
|
|
|
|
|
def count_students(self) -> int:
|
|
|
"""统计学生总数"""
|
|
|
return len(self.get_all_students())
|
|
|
|
|
|
def count_by_major(self) -> Dict[str, int]:
|
|
|
"""按专业统计学生人数"""
|
|
|
result = {}
|
|
|
for student in self.get_all_students():
|
|
|
if student.major:
|
|
|
result[student.major] = result.get(student.major, 0) + 1
|
|
|
return result
|
|
|
|
|
|
def average_height(self) -> float:
|
|
|
"""计算平均身高"""
|
|
|
students = [s for s in self.get_all_students() if s.height is not None]
|
|
|
if not students:
|
|
|
return 0
|
|
|
return sum(s.height for s in students) / len(students)
|
|
|
|
|
|
def average_weight(self) -> float:
|
|
|
"""计算平均体重"""
|
|
|
students = [s for s in self.get_all_students() if s.weight is not None]
|
|
|
if not students:
|
|
|
return 0
|
|
|
return sum(s.weight for s in students) / len(students)
|
|
|
|
|
|
def export_to_json(self, file_path: str) -> bool:
|
|
|
"""导出数据到JSON文件"""
|
|
|
students = [s.to_dict() for s in self.get_all_students()]
|
|
|
try:
|
|
|
with open(file_path, 'w', encoding='utf-8') as f:
|
|
|
json.dump(students, f, ensure_ascii=False, indent=4)
|
|
|
return True
|
|
|
except:
|
|
|
return False
|
|
|
|
|
|
def export_to_csv(self, file_path: str) -> bool:
|
|
|
"""导出数据到CSV文件"""
|
|
|
students = self.get_all_students()
|
|
|
if not students:
|
|
|
return False
|
|
|
|
|
|
fieldnames = list(students[0].to_dict().keys())
|
|
|
try:
|
|
|
with open(file_path, 'w', encoding='utf-8-sig', newline='') as f:
|
|
|
writer = csv.DictWriter(f, fieldnames=fieldnames)
|
|
|
writer.writeheader()
|
|
|
for student in students:
|
|
|
writer.writerow(student.to_dict())
|
|
|
return True
|
|
|
except:
|
|
|
return False
|
|
|
|
|
|
def import_from_json(self, file_path: str) -> int:
|
|
|
"""从JSON文件导入数据"""
|
|
|
try:
|
|
|
with open(file_path, 'r', encoding='utf-8') as f:
|
|
|
students_data = json.load(f)
|
|
|
|
|
|
count = 0
|
|
|
for data in students_data:
|
|
|
try:
|
|
|
student = Student.from_dict(data)
|
|
|
if self.add_student(student):
|
|
|
count += 1
|
|
|
except:
|
|
|
continue
|
|
|
return count
|
|
|
except:
|
|
|
return 0
|
|
|
|
|
|
def import_from_csv(self, file_path: str) -> int:
|
|
|
"""从CSV文件导入数据"""
|
|
|
try:
|
|
|
with open(file_path, 'r', encoding='utf-8-sig', newline='') as f:
|
|
|
reader = csv.DictReader(f)
|
|
|
students_data = [row for row in reader]
|
|
|
|
|
|
count = 0
|
|
|
for data in students_data:
|
|
|
try:
|
|
|
student = Student.from_dict(data)
|
|
|
if self.add_student(student):
|
|
|
count += 1
|
|
|
except:
|
|
|
continue
|
|
|
return count
|
|
|
except:
|
|
|
return 0
|
|
|
|
|
|
|
|
|
# ============== 表示层 ==============
|
|
|
class StudentUI:
|
|
|
"""学生信息管理系统用户界面"""
|
|
|
|
|
|
def __init__(self, file_path: str):
|
|
|
"""初始化用户界面"""
|
|
|
self.bll = StudentBLL(file_path)
|
|
|
self.running = True
|
|
|
|
|
|
def display_main_menu(self) -> None:
|
|
|
"""显示主菜单"""
|
|
|
print("\n===== 学生信息管理系统 =====")
|
|
|
print("1. 添加学生信息")
|
|
|
print("2. 删除学生信息")
|
|
|
print("3. 更新学生信息")
|
|
|
print("4. 查询学生信息")
|
|
|
print("5. 统计分析")
|
|
|
print("6. 数据导入导出")
|
|
|
print("7. 清空所有学生信息")
|
|
|
print("0. 退出系统")
|
|
|
print("==========================")
|
|
|
|
|
|
def display_query_menu(self) -> None:
|
|
|
"""显示查询菜单"""
|
|
|
print("\n===== 查询学生信息 =====")
|
|
|
print("1. 查询所有学生")
|
|
|
print("2. 按身份证号查询")
|
|
|
print("3. 按学号查询")
|
|
|
print("4. 按姓名查询")
|
|
|
print("5. 按班级查询")
|
|
|
print("6. 按专业查询")
|
|
|
print("0. 返回上一级")
|
|
|
print("======================")
|
|
|
|
|
|
def display_stats_menu(self) -> None:
|
|
|
"""显示统计菜单"""
|
|
|
print("\n===== 统计分析 =====")
|
|
|
print("1. 学生总数")
|
|
|
print("2. 各专业学生人数")
|
|
|
print("3. 平均身高")
|
|
|
print("4. 平均体重")
|
|
|
print("0. 返回上一级")
|
|
|
print("====================")
|
|
|
|
|
|
def display_import_export_menu(self) -> None:
|
|
|
"""显示数据导入导出菜单"""
|
|
|
print("\n===== 数据导入导出 =====")
|
|
|
print("1. 导出到JSON")
|
|
|
print("2. 从JSON导入")
|
|
|
print("3. 导出到CSV")
|
|
|
print("4. 从CSV导入")
|
|
|
print("0. 返回上一级")
|
|
|
print("========================")
|
|
|
|
|
|
def get_input(self, prompt: str) -> str:
|
|
|
"""获取用户输入"""
|
|
|
return input(prompt).strip()
|
|
|
|
|
|
def add_student(self) -> None:
|
|
|
"""添加学生信息"""
|
|
|
print("\n--- 添加学生信息 ---")
|
|
|
name = self.get_input("姓名: ")
|
|
|
id_card = self.get_input("身份证号: ")
|
|
|
stu_id = self.get_input("学号: ")
|
|
|
|
|
|
gender = self.get_input("性别(1-男, 0-女, 直接回车-未知): ")
|
|
|
if gender == '1':
|
|
|
gender = True
|
|
|
elif gender == '0':
|
|
|
gender = False
|
|
|
else:
|
|
|
gender = None
|
|
|
|
|
|
height_str = self.get_input("身高(cm, 直接回车-未知): ")
|
|
|
height = int(height_str) if height_str else None
|
|
|
|
|
|
weight_str = self.get_input("体重(kg, 保留一位小数, 直接回车-未知): ")
|
|
|
weight = float(weight_str) if weight_str else None
|
|
|
|
|
|
enrollment_date = self.get_input("入学日期(YYYY-MM-DD, 直接回车-未知): ")
|
|
|
enrollment_date = date.fromisoformat(enrollment_date) if enrollment_date else None
|
|
|
|
|
|
class_name = self.get_input("班级名称(直接回车-未知): ")
|
|
|
major = self.get_input("专业(直接回车-未知): ")
|
|
|
|
|
|
try:
|
|
|
student = Student(
|
|
|
name=name,
|
|
|
id_card=id_card,
|
|
|
stu_id=stu_id,
|
|
|
gender=gender,
|
|
|
height=height,
|
|
|
weight=weight,
|
|
|
enrollment_date=enrollment_date,
|
|
|
class_name=class_name,
|
|
|
major=major
|
|
|
)
|
|
|
|
|
|
if self.bll.add_student(student):
|
|
|
print("学生添加成功!")
|
|
|
else:
|
|
|
print("学生添加失败,身份证号或学号已存在!")
|
|
|
except ValueError as e:
|
|
|
print(f"操作失败: {e}")
|
|
|
|
|
|
def delete_student(self) -> None:
|
|
|
"""删除学生信息"""
|
|
|
print("\n--- 删除学生信息 ---")
|
|
|
print("1. 按身份证号删除")
|
|
|
print("2. 按学号删除")
|
|
|
choice = self.get_input("请选择: ")
|
|
|
|
|
|
try:
|
|
|
if choice == '1':
|
|
|
id_card = self.get_input("请输入身份证号: ")
|
|
|
if self.bll.delete_student(id_card):
|
|
|
print("学生删除成功!")
|
|
|
else:
|
|
|
print("学生不存在!")
|
|
|
elif choice == '2':
|
|
|
stu_id = self.get_input("请输入学号: ")
|
|
|
if self.bll.delete_student_by_stu_id(stu_id):
|
|
|
print("学生删除成功!")
|
|
|
else:
|
|
|
print("学生不存在!")
|
|
|
else:
|
|
|
print("无效选择!")
|
|
|
except ValueError as e:
|
|
|
print(f"操作失败: {e}")
|
|
|
|
|
|
def update_student(self) -> None:
|
|
|
"""更新学生信息"""
|
|
|
print("\n--- 更新学生信息 ---")
|
|
|
id_card = self.get_input("请输入要更新的学生身份证号: ")
|
|
|
|
|
|
student = self.bll.get_student_by_id(id_card)
|
|
|
if not student:
|
|
|
print("学生不存在!")
|
|
|
return
|
|
|
|
|
|
print(f"\n当前学生信息: {student}")
|
|
|
|
|
|
name = self.get_input(f"姓名[{student.name}]: ") or student.name
|
|
|
gender = self.get_input(f"性别(1-男, 0-女, 直接回车[{student.gender}]): ")
|
|
|
if gender:
|
|
|
gender = True if gender == '1' else False
|
|
|
else:
|
|
|
gender = student.gender
|
|
|
|
|
|
height_str = self.get_input(f"身高(cm, 直接回车[{student.height}]): ")
|
|
|
height = int(height_str) if height_str else student.height
|
|
|
|
|
|
weight_str = self.get_input(f"体重(kg, 直接回车[{student.weight}]): ")
|
|
|
weight = float(weight_str) if weight_str else student.weight
|
|
|
|
|
|
enrollment_date = self.get_input(f"入学日期(YYYY-MM-DD, 直接回车[{student.enrollment_date}]): ")
|
|
|
enrollment_date = date.fromisoformat(enrollment_date) if enrollment_date else student.enrollment_date
|
|
|
|
|
|
class_name = self.get_input(f"班级名称[{student.class_name}]: ") or student.class_name
|
|
|
major = self.get_input(f"专业[{student.major}]: ") or student.major
|
|
|
|
|
|
try:
|
|
|
updated_student = Student(
|
|
|
name=name,
|
|
|
id_card=id_card,
|
|
|
stu_id=student.stu_id,
|
|
|
gender=gender,
|
|
|
height=height,
|
|
|
weight=weight,
|
|
|
enrollment_date=enrollment_date,
|
|
|
class_name=class_name,
|
|
|
major=major
|
|
|
)
|
|
|
|
|
|
if self.bll.update_student(updated_student):
|
|
|
print("学生信息更新成功!")
|
|
|
else:
|
|
|
print("学生信息更新失败!")
|
|
|
except ValueError as e:
|
|
|
print(f"操作失败: {e}")
|
|
|
|
|
|
def query_student(self) -> None:
|
|
|
"""查询学生信息"""
|
|
|
while True:
|
|
|
self.display_query_menu()
|
|
|
choice = self.get_input("请选择操作: ")
|
|
|
|
|
|
if choice == '0':
|
|
|
break
|
|
|
|
|
|
try:
|
|
|
if choice == '1':
|
|
|
# 查询所有学生
|
|
|
students = self.bll.get_all_students()
|
|
|
if not students:
|
|
|
print("没有学生信息!")
|
|
|
else:
|
|
|
print("\n--- 所有学生信息 ---")
|
|
|
for i, student in enumerate(students, 1):
|
|
|
print(f"{i}. {student}")
|
|
|
|
|
|
elif choice == '2':
|
|
|
# 按身份证号查询
|
|
|
id_card = self.get_input("请输入身份证号: ")
|
|
|
student = self.bll.get_student_by_id(id_card)
|
|
|
if student:
|
|
|
print(f"\n--- 学生信息 ---: {student}")
|
|
|
else:
|
|
|
print("未找到该学生!")
|
|
|
|
|
|
elif choice == '3':
|
|
|
# 按学号查询
|
|
|
stu_id = self.get_input("请输入学号: ")
|
|
|
student = self.bll.get_student_by_stu_id(stu_id)
|
|
|
if student:
|
|
|
print(f"\n--- 学生信息 ---: {student}")
|
|
|
else:
|
|
|
print("未找到该学生!")
|
|
|
|
|
|
elif choice == '4':
|
|
|
# 按姓名查询
|
|
|
name = self.get_input("请输入姓名(支持模糊查询): ")
|
|
|
students = self.bll.search_by_name(name)
|
|
|
if not students:
|
|
|
print("未找到该学生!")
|
|
|
else:
|
|
|
print(f"\n--- 查找结果({len(students)}人) ---")
|
|
|
for i, student in enumerate(students, 1):
|
|
|
print(f"{i}. {student}")
|
|
|
|
|
|
elif choice == '5':
|
|
|
# 按班级查询
|
|
|
class_name = self.get_input("请输入班级名称(支持模糊查询): ")
|
|
|
students = self.bll.search_by_class(class_name)
|
|
|
if not students:
|
|
|
print("未找到该班级的学生!")
|
|
|
else:
|
|
|
print(f"\n--- 查找结果({len(students)}人) ---")
|
|
|
for i, student in enumerate(students, 1):
|
|
|
print(f"{i}. {student}")
|
|
|
|
|
|
elif choice == '6':
|
|
|
# 按专业查询
|
|
|
major = self.get_input("请输入专业名称(支持模糊查询): ")
|
|
|
students = self.bll.search_by_major(major)
|
|
|
if not students:
|
|
|
print("未找到该专业的学生!")
|
|
|
else:
|
|
|
print(f"\n--- 查找结果({len(students)}人) ---")
|
|
|
for i, student in enumerate(students, 1):
|
|
|
print(f"{i}. {student}")
|
|
|
|
|
|
else:
|
|
|
print("无效选择!")
|
|
|
except Exception as e:
|
|
|
print(f"查询出错: {e}")
|
|
|
|
|
|
def show_stats(self) -> None:
|
|
|
"""显示统计信息"""
|
|
|
while True:
|
|
|
self.display_stats_menu()
|
|
|
choice = self.get_input("请选择操作: ")
|
|
|
|
|
|
if choice == '0':
|
|
|
break
|
|
|
|
|
|
try:
|
|
|
if choice == '1':
|
|
|
# 学生总数
|
|
|
count = self.bll.count_students()
|
|
|
print(f"\n学生总数: {count}人")
|
|
|
|
|
|
elif choice == '2':
|
|
|
# 各专业学生人数
|
|
|
major_counts = self.bll.count_by_major()
|
|
|
if not major_counts:
|
|
|
print("没有学生信息!")
|
|
|
else:
|
|
|
print("\n各专业学生人数:")
|
|
|
for major, count in major_counts.items():
|
|
|
print(f"{major}: {count}人")
|
|
|
|
|
|
elif choice == '3':
|
|
|
# 平均身高
|
|
|
avg_height = self.bll.average_height()
|
|
|
print(f"\n平均身高: {avg_height:.2f}cm")
|
|
|
|
|
|
elif choice == '4':
|
|
|
# 平均体重
|
|
|
avg_weight = self.bll.average_weight()
|
|
|
print(f"\n平均体重: {avg_weight:.2f}kg")
|
|
|
|
|
|
else:
|
|
|
print("无效选择!")
|
|
|
except Exception as e:
|
|
|
print(f"统计出错: {e}")
|
|
|
|
|
|
def import_export_data(self) -> None:
|
|
|
"""数据导入导出"""
|
|
|
while True:
|
|
|
self.display_import_export_menu()
|
|
|
choice = self.get_input("请选择操作: ")
|
|
|
|
|
|
if choice == '0':
|
|
|
break
|
|
|
|
|
|
try:
|
|
|
if choice == '1':
|
|
|
# 导出到JSON
|
|
|
file_path = self.get_input("请输入导出文件路径: ")
|
|
|
if self.bll.export_to_json(file_path):
|
|
|
print(f"数据已成功导出到 {file_path}")
|
|
|
else:
|
|
|
print("导出失败!")
|
|
|
|
|
|
elif choice == '2':
|
|
|
# 从JSON导入
|
|
|
file_path = self.get_input("请输入导入文件路径: ")
|
|
|
count = self.bll.import_from_json(file_path)
|
|
|
print(f"成功导入 {count} 条学生记录")
|
|
|
|
|
|
elif choice == '3':
|
|
|
# 导出到CSV
|
|
|
file_path = self.get_input("请输入导出文件路径: ")
|
|
|
if self.bll.export_to_csv(file_path):
|
|
|
print(f"数据已成功导出到 {file_path}")
|
|
|
else:
|
|
|
print("导出失败!")
|
|
|
|
|
|
elif choice == '4':
|
|
|
# 从CSV导入
|
|
|
file_path = self.get_input("请输入导入文件路径: ")
|
|
|
count = self.bll.import_from_csv(file_path)
|
|
|
print(f"成功导入 {count} 条学生记录")
|
|
|
|
|
|
else:
|
|
|
print("无效选择!")
|
|
|
except Exception as e:
|
|
|
print(f"操作出错: {e}")
|
|
|
|
|
|
def clear_students(self) -> None:
|
|
|
"""清空所有学生信息"""
|
|
|
print("\n--- 清空所有学生信息 ---")
|
|
|
confirm = self.get_input("确定要清空所有学生信息吗?(y/n): ")
|
|
|
if confirm.lower() == 'y':
|
|
|
# 这里简单实现,实际应更安全
|
|
|
open(self.bll.file_path, 'w', encoding='utf-8').close()
|
|
|
print("所有学生信息已清空!")
|
|
|
else:
|
|
|
print("已取消清空操作。")
|
|
|
|
|
|
def run(self) -> None:
|
|
|
"""运行系统"""
|
|
|
print("学生信息管理系统启动中...")
|
|
|
|
|
|
while self.running:
|
|
|
self.display_main_menu()
|
|
|
choice = self.get_input("请选择操作: ")
|
|
|
|
|
|
try:
|
|
|
if choice == '1':
|
|
|
self.add_student()
|
|
|
elif choice == '2':
|
|
|
self.delete_student()
|
|
|
elif choice == '3':
|
|
|
self.update_student()
|
|
|
elif choice == '4':
|
|
|
self.query_student()
|
|
|
elif choice == '5':
|
|
|
self.show_stats()
|
|
|
elif choice == '6':
|
|
|
self.import_export_data()
|
|
|
elif choice == '7':
|
|
|
self.clear_students()
|
|
|
elif choice == '0':
|
|
|
self.running = False
|
|
|
print("感谢使用学生信息管理系统,再见!")
|
|
|
else:
|
|
|
print("无效选择,请重新输入!")
|
|
|
except Exception as e:
|
|
|
print(f"操作出错: {e}")
|
|
|
|
|
|
|
|
|
# ============== 主程序 ==============
|
|
|
if __name__ == "__main__":
|
|
|
# 默认为JSON文件存储
|
|
|
default_file = "students.json"
|
|
|
print(f"使用默认数据文件: {default_file}")
|
|
|
|
|
|
ui = StudentUI(default_file)
|
|
|
ui.run() |