diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..94a25f7 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/BLL/__init__.py b/BLL/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/BLL/add_student.py b/BLL/add_student.py new file mode 100644 index 0000000..17d3ecf --- /dev/null +++ b/BLL/add_student.py @@ -0,0 +1,173 @@ +from datetime import date +from typing import List, Optional, Dict +from copy import deepcopy +from model.student import Student +from DAL.clear_all_student import IStudentDAL + + + +class StudentBLL: + def __init__(self, dal: IStudentDAL): + self.dal = dal + + def add_student(self, student: Student) -> bool: + """添加学生信息,进行完整校验和唯一性检查""" + # 1. 数据校验 + if not student.is_valid: + raise ValueError(f"学生数据校验不通过。错误信息: {student.get_errors()}") + + # 2. 唯一性检查 + if self.dal.check_student_exists_by_id_card(student.id_card): + raise ValueError(f"身份证号 {student.id_card} 已存在") + if self.dal.check_student_exists_by_stu_id(student.stu_id): + raise ValueError(f"学号 {student.stu_id} 已存在") + + # 3. 业务规则检查 + today = date.today() + if student.birth_date and student.birth_date > today: + raise ValueError("出生日期不能晚于当前日期") + if student.enrollment_date and student.enrollment_date > today: + raise ValueError("入学日期不能晚于当前日期") + if student.height and (student.height <= 0 or student.height > 250): + raise ValueError("身高必须在0-250厘米之间") + if student.weight and (student.weight <= 0 or student.weight > 300): + raise ValueError("体重必须在0-300公斤之间") + + # 4. 添加学生 + return self.dal.add_student(student) + + def delete_student_by_id_card(self, id_card: str) -> bool: + """根据身份证号删除学生信息""" + if not self.dal.check_student_exists_by_id_card(id_card): + raise ValueError(f"身份证号 {id_card} 不存在") + return self.dal.delete_student_by_id_card(id_card) + + def delete_student_by_stu_id(self, stu_id: str) -> bool: + """根据学号删除学生信息""" + if not self.dal.check_student_exists_by_stu_id(stu_id): + raise ValueError(f"学号 {stu_id} 不存在") + return self.dal.delete_student_by_stu_id(stu_id) + + def update_student(self, id_card: str, student: Student) -> bool: + """完整更新学生信息""" + # 1. 存在性检查 + if not self.dal.check_student_exists_by_id_card(id_card): + raise ValueError(f"身份证号 {id_card} 不存在") + + # 2. 数据校验 + if not student.is_valid: + raise ValueError(f"学生数据校验不通过。错误信息: {student.get_errors()}") + + # 3. 业务规则检查 + today = date.today() + if student.birth_date and student.birth_date > today: + raise ValueError("出生日期不能晚于当前日期") + if student.enrollment_date and student.enrollment_date > today: + raise ValueError("入学日期不能晚于当前日期") + if student.height and (student.height <= 0 or student.height > 250): + raise ValueError("身高必须在0-250厘米之间") + if student.weight and (student.weight <= 0 or student.weight > 300): + raise ValueError("体重必须在0-300公斤之间") + + # 4. 执行更新 + return self.dal.update_student(id_card, student) + + def update_student_partial(self, id_card: str, **kwargs) -> bool: + """部分更新学生信息""" + # 1. 存在性检查 + student = self.dal.find_student_by_id_card(id_card) + if not student: + raise ValueError(f"身份证号 {id_card} 不存在") + + # 2. 保护关键字段 - 身份证号不可更改 + if 'id_card' in kwargs: + raise ValueError("身份证号不可更改") + + # 3. 创建更新后的学生对象 + updated_student = deepcopy(student) + for key, value in kwargs.items(): + if hasattr(updated_student, key): + setattr(updated_student, key, value) + + # 4. 数据校验 + if not updated_student.is_valid: + raise ValueError(f"学生数据校验不通过。错误信息: {updated_student.get_errors()}") + + # 5. 业务规则检查 + today = date.today() + if updated_student.birth_date and updated_student.birth_date > today: + raise ValueError("出生日期不能晚于当前日期") + if updated_student.enrollment_date and updated_student.enrollment_date > today: + raise ValueError("入学日期不能晚于当前日期") + if updated_student.height and (updated_student.height <= 0 or updated_student.height > 250): + raise ValueError("身高必须在0-250厘米之间") + if updated_student.weight and (updated_student.weight <= 0 or updated_student.weight > 300): + raise ValueError("体重必须在0-300公斤之间") + + # 6. 学号唯一性检查(如果学号有变化) + if 'stu_id' in kwargs and kwargs['stu_id'] != student.stu_id: + if self.dal.check_student_exists_by_stu_id(kwargs['stu_id']): + raise ValueError(f"学号 {kwargs['stu_id']} 已存在") + + # 7. 执行更新 + return self.dal.update_student(id_card, updated_student) + + def get_student_by_id_card(self, id_card: str) -> Optional[Student]: + """根据身份证号查询学生信息""" + return self.dal.find_student_by_id_card(id_card) + + def get_student_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号查询学生信息""" + return self.dal.find_student_by_stu_id(stu_id) + + def get_students_by_name(self, name: str) -> List[Student]: + """根据姓名查询学生信息(模糊匹配)""" + return self.dal.find_students_by_name(name) + + def get_students_by_class_name(self, class_name: str) -> List[Student]: + """根据班级名称查询学生信息(模糊匹配)""" + return self.dal.find_students_by_class_name(class_name) + + def get_students_by_major(self, major: str) -> List[Student]: + """根据专业查询学生信息(模糊匹配)""" + return self.dal.find_students_by_major(major) + + def get_all_students(self) -> List[Student]: + """获取所有学生信息""" + return self.dal.get_all_students() + + def get_student_count(self) -> int: + """获取学生总数""" + return self.dal.get_student_count() + + def get_students_by_height_range(self, min_height: int, max_height: int) -> List[Student]: + """按身高范围查询学生""" + return self.dal.get_students_by_height_range(min_height, max_height) + + def get_students_by_enrollment_year(self, year: int) -> List[Student]: + """按入学年份查询学生""" + return self.dal.get_students_by_enrollment_year(year) + + def get_students_by_age_range(self, min_age: int, max_age: int) -> List[Student]: + """按年龄范围查询学生""" + return self.dal.get_students_by_age_range(min_age, max_age) + + def get_students_by_major_group(self) -> Dict[str, List[Student]]: + """按专业分组学生""" + return self.dal.get_students_by_major_group() + + def get_average_height(self) -> float: + """计算平均身高""" + return self.dal.get_average_height() + + def get_average_weight(self) -> float: + """计算平均体重""" + return self.dal.get_average_weight() + + def get_gender_ratio(self) -> Dict[Optional[bool], float]: + """计算性别比例""" + return self.dal.get_gender_ratio() + + def clear_all_students(self) -> None: + """清空所有学生信息""" + self.dal.clear_all_students() diff --git a/DAL/__init__.py b/DAL/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/DAL/clear_all_student.py b/DAL/clear_all_student.py new file mode 100644 index 0000000..f2d361a --- /dev/null +++ b/DAL/clear_all_student.py @@ -0,0 +1,476 @@ +import os +import json +import csv +from abc import ABC, abstractmethod +from datetime import date +from typing import List, Optional, Dict +from model.student import Student + +class IStudentDAL(ABC): + """学生信息数据访问层接口""" + + @abstractmethod + def add_student(self, student: Student) -> bool: + """添加学生""" + pass + + @abstractmethod + def delete_student_by_id_card(self, id_card: str) -> bool: + """根据身份证号删除学生""" + pass + + @abstractmethod + def delete_student_by_stu_id(self, stu_id: str) -> bool: + """根据学号删除学生""" + pass + + @abstractmethod + def update_student(self, id_card: str, updated_student: Student) -> bool: + """更新学生信息""" + pass + + @abstractmethod + def find_student_by_id_card(self, id_card: str) -> Optional[Student]: + """根据身份证号查找学生""" + pass + + @abstractmethod + def find_student_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号查找学生""" + pass + + @abstractmethod + def find_students_by_name(self, name: str) -> List[Student]: + """根据姓名查找学生(模糊匹配)""" + pass + + @abstractmethod + def find_students_by_class_name(self, class_name: str) -> List[Student]: + """根据班级名称查找学生(模糊匹配)""" + pass + + @abstractmethod + def find_students_by_major(self, major: str) -> List[Student]: + """根据专业查找学生(模糊匹配)""" + pass + + @abstractmethod + def get_all_students(self) -> List[Student]: + """获取所有学生信息""" + pass + + @abstractmethod + def get_student_count(self) -> int: + """获取学生总数""" + pass + + @abstractmethod + def get_students_by_height_range(self, min_height: int, max_height: int) -> List[Student]: + """按身高范围查找学生""" + pass + + @abstractmethod + def get_students_by_enrollment_year(self, year: int) -> List[Student]: + """按入学年份查找学生""" + pass + + @abstractmethod + def get_students_by_age_range(self, min_age: int, max_age: int) -> List[Student]: + """按年龄范围查找学生""" + pass + + @abstractmethod + def get_students_by_major_group(self) -> Dict[str, List[Student]]: + """按专业分组学生""" + pass + + @abstractmethod + def get_average_height(self) -> float: + """计算平均身高""" + pass + + @abstractmethod + def get_average_weight(self) -> float: + """计算平均体重""" + pass + + @abstractmethod + def get_gender_ratio(self) -> Dict[Optional[bool], float]: + """计算性别比例""" + pass + + @abstractmethod + def clear_all_students(self) -> None: + """清空所有学生信息""" + pass + + @abstractmethod + def check_student_exists_by_id_card(self, id_card: str) -> bool: + """检查身份证号是否存在""" + pass + + @abstractmethod + def check_student_exists_by_stu_id(self, stu_id: str) -> bool: + """检查学号是否存在""" + pass + + +class JsonStudentDAL(IStudentDAL): + """JSON文件存储实现""" + + def __init__(self, file_path: str): + self.file_path = file_path + self.__ensure_file_exists() + + def __ensure_file_exists(self): + """确保JSON文件存在""" + if not os.path.exists(self.file_path): + with open(self.file_path, 'w', encoding='utf-8') as file: + # noinspection PyTypeChecker + json.dump([], file) + + def __load_students(self) -> List[Student]: + """从JSON文件加载学生列表""" + try: + with open(self.file_path, 'r', encoding='utf-8') as file: + student_dicts = json.load(file) + return [Student.from_dict(sd) for sd in student_dicts] + except json.JSONDecodeError: + return [] + + def __save_students(self, students: List[Student]) -> None: + """将学生列表保存到JSON文件""" + student_dicts = [s.to_dict() for s in students] + with open(self.file_path, 'w', encoding='utf-8') as file: + json.dump(student_dicts, file, ensure_ascii=False, indent=4) + + # 实现接口方法 + def add_student(self, student: Student) -> bool: + students = self.__load_students() + if any(s.id_card == student.id_card for s in students): + return False + if any(s.stu_id == student.stu_id for s in students): + return False + students.append(student) + self.__save_students(students) + return True + + def delete_student_by_id_card(self, id_card: str) -> bool: + students = self.__load_students() + original_count = len(students) + students = [s for s in students if s.id_card != id_card] + if len(students) < original_count: + self.__save_students(students) + return True + return False + + def delete_student_by_stu_id(self, stu_id: str) -> bool: + students = self.__load_students() + original_count = len(students) + students = [s for s in students if s.stu_id != stu_id] + if len(students) < original_count: + self.__save_students(students) + return True + return False + + def update_student(self, id_card: str, updated_student: Student) -> bool: + students = self.__load_students() + for i, s in enumerate(students): + if s.id_card == id_card: + students[i] = updated_student + self.__save_students(students) + return True + return False + + def find_student_by_id_card(self, id_card: str) -> Optional[Student]: + students = self.__load_students() + for s in students: + if s.id_card == id_card: + return s + return None + + def find_student_by_stu_id(self, stu_id: str) -> Optional[Student]: + students = self.__load_students() + for s in students: + if s.stu_id == stu_id: + return s + return None + + def find_students_by_name(self, name: str) -> List[Student]: + students = self.__load_students() + return [s for s in students if name.lower() in s.name.lower()] + + def find_students_by_class_name(self, class_name: str) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.class_name and class_name.lower() in s.class_name.lower()] + + def find_students_by_major(self, major: str) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.major and major.lower() in s.major.lower()] + + def get_all_students(self) -> List[Student]: + return self.__load_students() + + def get_student_count(self) -> int: + return len(self.__load_students()) + + def get_students_by_height_range(self, min_height: int, max_height: int) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.height and min_height <= s.height <= max_height] + + def get_students_by_enrollment_year(self, year: int) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.enrollment_date and s.enrollment_date.year == year] + + def get_students_by_age_range(self, min_age: int, max_age: int) -> List[Student]: + students = self.__load_students() + today = date.today() + result = [] + for s in students: + if s.birth_date: + age = today.year - s.birth_date.year + if (today.month, today.day) < (s.birth_date.month, s.birth_date.day): + age -= 1 + if min_age <= age <= max_age: + result.append(s) + return result + + def get_students_by_major_group(self) -> Dict[str, List[Student]]: + students = self.__load_students() + groups = {} + for s in students: + if s.major: + if s.major not in groups: + groups[s.major] = [] + groups[s.major].append(s) + return groups + + def get_average_height(self) -> float: + students = self.__load_students() + heights = [s.height for s in students if s.height is not None] + if not heights: + return 0.0 + return sum(heights) / len(heights) + + def get_average_weight(self) -> float: + students = self.__load_students() + weights = [s.weight for s in students if s.weight is not None] + if not weights: + return 0.0 + return sum(weights) / len(weights) + + def get_gender_ratio(self) -> Dict[Optional[bool], float]: + students = self.__load_students() + genders = [s.gender for s in students if s.gender is not None] + total = len(genders) + if total == 0: + return {True: 0.0, False: 0.0} + + male_count = sum(1 for g in genders if g) + female_count = total - male_count + return { + True: male_count / total * 100, + False: female_count / total * 100 + } + + def clear_all_students(self) -> None: + self.__save_students([]) + + def check_student_exists_by_id_card(self, id_card: str) -> bool: + return self.find_student_by_id_card(id_card) is not None + + def check_student_exists_by_stu_id(self, stu_id: str) -> bool: + return self.find_student_by_stu_id(stu_id) is not None + + +class CsvStudentDAL(IStudentDAL): + """CSV文件存储实现""" + + def __init__(self, file_path: str): + self.file_path = file_path + self.__ensure_file_exists() + + def __ensure_file_exists(self): + """确保CSV文件存在""" + if not os.path.exists(self.file_path): + with open(self.file_path, 'w', encoding='utf-8', newline='') as file: + writer = csv.writer(file) + writer.writerow([ + 'name', 'id_card', 'stu_id', 'gender', 'height', 'weight', + 'enrollment_date', 'class_name', 'major', 'email', 'phone' + ]) + + def __load_students(self) -> List[Student]: + """从CSV文件加载学生列表""" + students = [] + try: + with open(self.file_path, 'r', encoding='utf-8', newline='') as file: + reader = csv.DictReader(file) + for row in reader: + # 转换数据类型 + if row['gender']: + row['gender'] = True if row['gender'] == 'True' else False + + if row['height']: + row['height'] = int(row['height']) + + if row['weight']: + row['weight'] = float(row['weight']) + + if row['enrollment_date']: + row['enrollment_date'] = date.fromisoformat(row['enrollment_date']) + + students.append(Student.from_dict(row)) + except (FileNotFoundError, csv.Error, ValueError): + pass + return students + + def __save_students(self, students: List[Student]) -> None: + """将学生列表保存到CSV文件""" + with open(self.file_path, 'w', encoding='utf-8', newline='') as file: + fieldnames = [ + 'name', 'id_card', 'stu_id', 'gender', 'height', 'weight', + 'enrollment_date', 'class_name', 'major', 'email', 'phone' + ] + writer = csv.DictWriter(file, fieldnames=fieldnames) + writer.writeheader() + for student in students: + writer.writerow(student.to_dict()) + + # 实现接口方法 + def add_student(self, student: Student) -> bool: + students = self.__load_students() + if any(s.id_card == student.id_card for s in students): + return False + if any(s.stu_id == student.stu_id for s in students): + return False + students.append(student) + self.__save_students(students) + return True + + def delete_student_by_id_card(self, id_card: str) -> bool: + students = self.__load_students() + original_count = len(students) + students = [s for s in students if s.id_card != id_card] + if len(students) < original_count: + self.__save_students(students) + return True + return False + + def delete_student_by_stu_id(self, stu_id: str) -> bool: + students = self.__load_students() + original_count = len(students) + students = [s for s in students if s.stu_id != stu_id] + if len(students) < original_count: + self.__save_students(students) + return True + return False + + def update_student(self, id_card: str, updated_student: Student) -> bool: + students = self.__load_students() + for i, s in enumerate(students): + if s.id_card == id_card: + students[i] = updated_student + self.__save_students(students) + return True + return False + + def find_student_by_id_card(self, id_card: str) -> Optional[Student]: + students = self.__load_students() + for s in students: + if s.id_card == id_card: + return s + return None + + def find_student_by_stu_id(self, stu_id: str) -> Optional[Student]: + students = self.__load_students() + for s in students: + if s.stu_id == stu_id: + return s + return None + + def find_students_by_name(self, name: str) -> List[Student]: + students = self.__load_students() + return [s for s in students if name.lower() in s.name.lower()] + + def find_students_by_class_name(self, class_name: str) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.class_name and class_name.lower() in s.class_name.lower()] + + def find_students_by_major(self, major: str) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.major and major.lower() in s.major.lower()] + + def get_all_students(self) -> List[Student]: + return self.__load_students() + + def get_student_count(self) -> int: + return len(self.__load_students()) + + def get_students_by_height_range(self, min_height: int, max_height: int) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.height and min_height <= s.height <= max_height] + + def get_students_by_enrollment_year(self, year: int) -> List[Student]: + students = self.__load_students() + return [s for s in students if s.enrollment_date and s.enrollment_date.year == year] + + def get_students_by_age_range(self, min_age: int, max_age: int) -> List[Student]: + students = self.__load_students() + today = date.today() + result = [] + for s in students: + if s.birth_date: + age = today.year - s.birth_date.year + if (today.month, today.day) < (s.birth_date.month, s.birth_date.day): + age -= 1 + if min_age <= age <= max_age: + result.append(s) + return result + + def get_students_by_major_group(self) -> Dict[str, List[Student]]: + students = self.__load_students() + groups = {} + for s in students: + if s.major: + if s.major not in groups: + groups[s.major] = [] + groups[s.major].append(s) + return groups + + def get_average_height(self) -> float: + students = self.__load_students() + heights = [s.height for s in students if s.height is not None] + if not heights: + return 0.0 + return sum(heights) / len(heights) + + def get_average_weight(self) -> float: + students = self.__load_students() + weights = [s.weight for s in students if s.weight is not None] + if not weights: + return 0.0 + return sum(weights) / len(weights) + + def get_gender_ratio(self) -> Dict[Optional[bool], float]: + students = self.__load_students() + genders = [s.gender for s in students if s.gender is not None] + total = len(genders) + if total == 0: + return {True: 0.0, False: 0.0} + + male_count = sum(1 for g in genders if g) + female_count = total - male_count + return { + True: male_count / total * 100, + False: female_count / total * 100 + } + + def clear_all_students(self) -> None: + self.__save_students([]) + + def check_student_exists_by_id_card(self, id_card: str) -> bool: + return self.find_student_by_id_card(id_card) is not None + + def check_student_exists_by_stu_id(self, stu_id: str) -> bool: + return self.find_student_by_stu_id(stu_id) is not None diff --git a/model/__init__.py b/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/model/student.py b/model/student.py new file mode 100644 index 0000000..3aec604 --- /dev/null +++ b/model/student.py @@ -0,0 +1,324 @@ + +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)})" \ No newline at end of file diff --git a/ui/__init__.py b/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/add_student.py b/ui/add_student.py new file mode 100644 index 0000000..dc9e64e --- /dev/null +++ b/ui/add_student.py @@ -0,0 +1,488 @@ +import json +import csv +from datetime import date +from typing import List, Tuple +from BLL.add_student import StudentBLL +from model.student import Student +from DAL.clear_all_student import CsvStudentDAL +from DAL.clear_all_student import JsonStudentDAL + + +def display_menu(): + print("\n=== 学生信息管理系统 ====") + print("1. 添加学生信息") + print("2. 删除学生信息") + print("3. 更新学生信息") + print("4. 查询学生信息") + print("5. 统计分析") + print("6. 数据导入导出") + print("7. 清空所有学生信息") + print("8. 退出系统") + print("---") + + +def display_query_menu(): + print("\n==== 查询学生信息 ====") + print("1. 查询所有学生") + print("2. 按身份证号查询") + print("3. 按学号查询") + print("4. 按姓名查询") + print("5. 按班级查询") + print("6. 按专业查询") + print("7. 返回上一级") + print("=======================") + + +def display_stats_menu(): + print("\n==== 统计分析 ====") + print("1. 学生总数") + print("2. 平均身高") + print("3. 按身高范围统计") + print("4. 按入学年份统计") + print("5. 按年龄范围统计") + print("6. 按专业分组统计") + print("7. 性别比例") + print("8. 返回上一级") + print("=======================") + + +def display_import_export_menu(): + print("\n==== 数据导入导出 ====") + print("1. 导出数据到JSON") + print("2. 从JSON导入数据") + print("3. 导出数据到CSV") + print("4. 从CSV导入数据") + print("5. 返回上一级") + print("=======================") + + +def display_students(students: List[Student]): + if not students: + print("没有找到学生信息") + return + + print("\n学生信息列表:") + print("-" * 120) + print( + f"{'姓名':<10}{'学号':<15}{'身份证号':<20}{'性别':<6}{'年龄':<6}{'身高':<6}{'体重':<6}{'班级':<10}{'专业':<15}{'入学日期':<12}") + print("-" * 120) + + for student in students: + gender = '男' if student.gender else '女' if student.gender is not None else '未知' + print( + f"{student.name:<10}{student.stu_id:<15}{student.id_card:<20}" + f"{gender:<6}{student.age or '-':<6}{student.height or '-':<6}" + f"{student.weight or '-':<6}{student.class_name or '-':<10}" + f"{student.major or '-':<15}{student.enrollment_date or '-'}" + ) + + print("-" * 120) + print(f"共找到 {len(students)} 名学生") + + + +class StudentTUI: + def __init__(self, bll: StudentBLL): + self.bll = bll + + def run(self): + while True: + display_menu() + choice = input("请选择操作: ").strip() + + if choice == '1': + self.add_student() + elif choice == '2': + self.delete_student() + elif choice == '3': + self.update_student() + elif choice == '4': + self.query_students() + elif choice == '5': + self.stats_students() + elif choice == '6': + self.import_export_data() + elif choice == '7': + self.clear_all_students() + elif choice == '8': + print("感谢使用学生信息管理系统,再见!") + break + else: + print("无效的选择,请重新输入!") + + def add_student(self): + print("\n==== 添加学生信息 ====") + try: + name = input("姓名: ").strip() + id_card = input("身份证号: ").strip() + stu_id = input("学号: ").strip() + gender = input("性别(男/女): ").strip().lower() == '男' + height = int(input("身高(cm): ").strip()) + weight = float(input("体重(kg): ").strip()) + enrollment_date = input("入学日期(YYYY-MM-DD): ").strip() + class_name = input("班级名称: ").strip() + major = input("专业: ").strip() + email = input("邮箱(可选): ").strip() or None + phone = input("手机号(可选): ").strip() or None + + 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, + email=email, + phone=phone + ) + + if self.bll.add_student(student): + print("学生信息添加成功!") + else: + print("添加失败,可能学号或身份证号已存在") + except ValueError as e: + print(f"输入错误: {e}") + except Exception as e: + print(f"添加学生信息时出错: {e}") + + def delete_student(self): + print("\n==== 删除学生信息 ====") + print("1. 按身份证号删除") + print("2. 按学号删除") + choice = input("请选择删除方式: ").strip() + + if choice == '1': + id_card = input("请输入身份证号: ").strip() + try: + if self.bll.delete_student_by_id_card(id_card): + print("学生信息删除成功!") + else: + print("删除失败,未找到匹配的学生") + except ValueError as e: + print(f"错误: {e}") + elif choice == '2': + stu_id = input("请输入学号: ").strip() + try: + if self.bll.delete_student_by_stu_id(stu_id): + print("学生信息删除成功!") + else: + print("删除失败,未找到匹配的学生") + except ValueError as e: + print(f"错误: {e}") + else: + print("无效的选择") + + def update_student(self): + print("\n==== 更新学生信息 ====") + id_card = input("请输入要更新的学生身份证号: ").strip() + + try: + student = self.bll.get_student_by_id_card(id_card) + if not student: + print("未找到该学生") + return + + print("当前学生信息:") + print(student) + + print("\n请输入要更新的字段 (留空表示不更新):") + name = input(f"姓名({student.name}): ").strip() or student.name + stu_id = input(f"学号({student.stu_id}): ").strip() or student.stu_id + gender = input(f"性别(男/女)({'男' if student.gender else '女'}): ").strip() + gender = gender.lower() == '男' if gender else student.gender + height = input(f"身高({student.height}): ").strip() + height = int(height) if height else student.height + weight = input(f"体重({student.weight}): ").strip() + weight = float(weight) if weight else student.weight + enrollment_date = input(f"入学日期({student.enrollment_date}): ").strip() + enrollment_date = enrollment_date or student.enrollment_date + class_name = input(f"班级名称({student.class_name}): ").strip() or student.class_name + major = input(f"专业({student.major}): ").strip() or student.major + email = input(f"邮箱({student.email}): ").strip() or student.email + phone = input(f"手机号({student.phone}): ").strip() or student.phone + + updated_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, + email=email, + phone=phone + ) + + if self.bll.update_student(id_card, updated_student): + print("学生信息更新成功!") + else: + print("更新失败") + except ValueError as e: + print(f"输入错误: {e}") + except Exception as e: + print(f"更新学生信息时出错: {e}") + + def query_students(self): + while True: + display_query_menu() + choice = input("请选择查询方式: ").strip() + + if choice == '1': # 查询所有学生 + students = self.bll.get_all_students() + display_students(students) + elif choice == '2': # 按身份证号查询 + id_card = input("请输入身份证号: ").strip() + student = self.bll.get_student_by_id_card(id_card) + if student: + display_students([student]) + else: + print("未找到匹配的学生") + elif choice == '3': # 按学号查询 + stu_id = input("请输入学号: ").strip() + student = self.bll.get_student_by_stu_id(stu_id) + if student: + display_students([student]) + else: + print("未找到匹配的学生") + elif choice == '4': # 按姓名查询 + name = input("请输入姓名: ").strip() + students = self.bll.get_students_by_name(name) + display_students(students) + elif choice == '5': # 按班级查询 + class_name = input("请输入班级名称: ").strip() + students = self.bll.get_students_by_class_name(class_name) + display_students(students) + elif choice == '6': # 按专业查询 + major = input("请输入专业名称: ").strip() + students = self.bll.get_students_by_major(major) + display_students(students) + elif choice == '7': # 返回上一级 + break + else: + print("无效的选择,请重新输入!") + + def stats_students(self): + while True: + display_stats_menu() + choice = input("请选择统计方式: ").strip() + + if choice == '1': # 学生总数 + count = self.bll.get_student_count() + print(f"\n学生总数: {count}") + elif choice == '2': # 平均身高 + avg_height = self.bll.get_average_height() + print(f"\n平均身高: {avg_height:.2f} cm") + elif choice == '3': # 按身高范围统计 + min_height = int(input("最小身高(cm): ").strip()) + max_height = int(input("最大身高(cm): ").strip()) + students = self.bll.get_students_by_height_range(min_height, max_height) + print(f"\n身高在 {min_height}-{max_height} cm 的学生 ({len(students)} 人):") + display_students(students) + elif choice == '4': # 按入学年份统计 + year = int(input("请输入入学年份: ").strip()) + students = self.bll.get_students_by_enrollment_year(year) + print(f"\n{year}年入学的学生 ({len(students)} 人):") + display_students(students) + elif choice == '5': # 按年龄范围统计 + min_age = int(input("最小年龄: ").strip()) + max_age = int(input("最大年龄: ").strip()) + students = self.bll.get_students_by_age_range(min_age, max_age) + print(f"\n年龄在 {min_age}-{max_age} 岁的学生 ({len(students)} 人):") + display_students(students) + elif choice == '6': # 按专业分组统计 + groups = self.bll.get_students_by_major_group() + for major, students in groups.items(): + print(f"\n专业: {major} ({len(students)} 人)") + display_students(students) + elif choice == '7': # 性别比例 + ratio = self.bll.get_gender_ratio() + print(f"\n性别比例:") + print(f"男生: {ratio.get(True, 0.0):.2f}%") + print(f"女生: {ratio.get(False, 0.0):.2f}%") + elif choice == '8': # 返回上一级 + break + else: + print("无效的选择,请重新输入!") + + def import_export_data(self): + while True: + display_import_export_menu() + choice = input("请选择操作: ").strip() + + if choice == '1': # 导出到JSON + file_path = input("请输入导出文件路径: ").strip() + if self.export_to_json(file_path): + print("导出成功!") + else: + print("导出失败") + elif choice == '2': # 从JSON导入 + file_path = input("请输入导入文件路径: ").strip() + success, error = self.import_from_json(file_path) + print(f"导入完成: 成功 {success} 条, 失败 {error} 条") + elif choice == '3': # 导出到CSV + file_path = input("请输入导出文件路径: ").strip() + if self.export_to_csv(file_path): + print("导出成功!") + else: + print("导出失败") + elif choice == '4': # 从CSV导入 + file_path = input("请输入导入文件路径: ").strip() + success, error = self.import_from_csv(file_path) + print(f"导入完成: 成功 {success} 条, 失败 {error} 条") + elif choice == '5': # 返回上一级 + break + else: + print("无效的选择,请重新输入!") + + def clear_all_students(self): + confirm = input("确定要清空所有学生信息吗? (y/n): ").strip().lower() + if confirm == 'y': + self.bll.clear_all_students() + print("所有学生信息已清空!") + else: + print("操作已取消") + + def export_to_json(self, file_path: str) -> bool: + """导出学生信息到JSON文件""" + students = self.bll.get_all_students() + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump([s.to_dict() for s in students], f, ensure_ascii=False, indent=4) + return True + except Exception as e: + print(f"导出学生数据到JSON时出错: {e}") + return False + + def import_from_json(self, file_path: str) -> Tuple[int, int]: + """从JSON文件导入学生信息""" + success_count = 0 + error_count = 0 + + try: + with open(file_path, 'r', encoding='utf-8') as f: + student_dicts = json.load(f) + + for s_dict in student_dicts: + try: + student = Student.from_dict(s_dict) + if self.bll.add_student(student): + success_count += 1 + except Exception as e: + error_count += 1 + print(f"导入学生数据时出错: {e}") + + return success_count, error_count + except Exception as e: + print(f"从JSON导入学生数据时出错: {e}") + return 0, 0 + + def export_to_csv(self, file_path: str) -> bool: + """导出学生信息到CSV文件""" + students = self.bll.get_all_students() + if not students: + return False + + try: + with open(file_path, 'w', encoding='utf-8', newline='') as f: + # 获取第一个学生的字段作为CSV表头 + fieldnames = list(students[0].to_dict().keys()) + writer = csv.DictWriter(f, fieldnames=fieldnames) + writer.writeheader() + for student in students: + writer.writerow(student.to_dict()) + return True + except Exception as e: + print(f"导出学生数据到CSV时出错: {e}") + return False + + def import_from_csv(self, file_path: str) -> Tuple[int, int]: + """从CSV文件导入学生信息""" + success_count = 0 + error_count = 0 + + try: + with open(file_path, 'r', encoding='utf-8') as f: + reader = csv.DictReader(f) + for row in reader: + try: + # 转换数据类型 + converted_row = {} + for key, value in row.items(): + if key in ['birth_date', 'enrollment_date'] and value: + converted_row[key] = date.fromisoformat(value) + elif key in ['height', 'weight'] and value: + converted_row[key] = float(value) if value else 0.0 + else: + converted_row[key] = value + + student = Student.from_dict(converted_row) + if self.bll.add_student(student): + success_count += 1 + except Exception as e: + error_count += 1 + print(f"导入学生数据时出错: {e}") + + return success_count, error_count + except Exception as e: + print(f"从CSV导入学生数据时出错: {e}") + return 0, 0 + + +def main(): + # 选择存储方式 + print("请选择数据存储方式:") + print("1. JSON") + print("2. CSV") + storage_choice = input("请输入选择 (1/2): ").strip() + + file_path = "students.json" if storage_choice == '1' else "students.csv" + + # 创建数据访问层 + if storage_choice == '1': + dal = JsonStudentDAL(file_path) + elif storage_choice == '2': + dal = CsvStudentDAL(file_path) + else: + print("无效的选择,默认使用JSON存储") + dal = JsonStudentDAL("students.json") + + # 创建业务逻辑层 + bll = StudentBLL(dal) + + # 创建用户界面 + tui = StudentTUI(bll) + tui.run() +def fun(self): + """运行UI""" + while True: + self.display_menu() + choice = input("请选择操作: ").strip() + if choice == '1': + self.add_student() + elif choice == '2': + self.delete_student() + + elif choice == '3': + self.update_student() + + elif choice == '5': + self.show_stats() + elif choice == '6': + self.import_export_data() + elif choice == '7': + self.clear_students() + elif choice == '8': + self.query_student() + elif choice == '4': + print("感谢使用学生信息管理系统!") + break + else: + print("无效的选择,请重新输入!") + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/ui/main.py b/ui/main.py new file mode 100644 index 0000000..e69de29 diff --git a/ui/students.csv b/ui/students.csv new file mode 100644 index 0000000..82867a7 --- /dev/null +++ b/ui/students.csv @@ -0,0 +1 @@ +name,id_card,stu_id,gender,height,weight,enrollment_date,class_name,major,email,phone diff --git a/ui/students.json b/ui/students.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/ui/students.json @@ -0,0 +1 @@ +[] \ No newline at end of file