diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..359bb53 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,3 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml diff --git a/.idea/PythonProject1.iml b/.idea/PythonProject1.iml new file mode 100644 index 0000000..909438d --- /dev/null +++ b/.idea/PythonProject1.iml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..811e3a4 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..9dfa2d1 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..f9cb772 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file 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/student/__init__.py b/student/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/student/bll/__init__.py b/student/bll/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/student/bll/student_bll.py b/student/bll/student_bll.py new file mode 100644 index 0000000..6418a95 --- /dev/null +++ b/student/bll/student_bll.py @@ -0,0 +1,304 @@ +from typing import List, Optional +from ..dal.student_dal import IStudentDAL +from ..model.student import Student +from ..util.validator import Validator + + +class StudentBLL: + """学生信息管理系统的业务逻辑层,处理学生信息的业务逻辑和数据验证""" + + def __init__(self, dal: IStudentDAL): + """ + 初始化业务逻辑层 + + Args: + dal: 数据访问层对象,实现IStudentDAL接口 + """ + self.dal = dal + + def add_student(self, student: Student) -> (bool, str): + """ + 添加学生信息,包含完整的数据验证流程 + + Args: + student: 要添加的学生对象 + + Returns: + 元组(操作是否成功, 操作结果信息) + """ + # 验证学生基本信息格式 + if not Validator.validate_id_card(student.id_card): + return False, "身份证号码格式不正确" + + if not Validator.validate_stu_id(student.stu_id): + return False, "学号格式不正确" + + if not Validator.validate_name(student.name): + return False, "姓名格式不正确,必须为2-20个中文字符" + + if student.height is not None and not Validator.validate_height(student.height): + return False, "身高必须在50-250cm之间" + + if student.weight is not None and not Validator.validate_weight(student.weight): + return False, "体重必须在5-300kg之间" + + if student.email and not Validator.validate_email(student.email): + return False, "电子邮箱格式不正确" + + if student.phone and not Validator.validate_phone(student.phone): + return False, "手机号码格式不正确" + + # 验证入学日期与出生日期的逻辑关系 + if student.enrollment_date and student.birthday: + if not Validator.validate_enrollment_date(student.enrollment_date, student.birthday): + return False, "入学日期不能早于出生日期" + + # 检查数据唯一性 + if self.dal.get_by_id(student.id_card): + return False, "该身份证号已存在" + + if self.dal.get_by_stu_id(student.stu_id): + return False, "该学号已存在" + + # 执行数据添加操作 + if self.dal.add(student): + return True, "学生添加成功" + else: + return False, "学生添加失败" + + def update_student(self, student: Student) -> (bool, str): + """ + 更新学生信息,包含与添加操作相同的数据验证流程 + + Args: + student: 包含更新信息的学生对象,通过id_card匹配记录 + + Returns: + 元组(操作是否成功, 操作结果信息) + """ + # 重复添加操作中的数据验证逻辑 + if not Validator.validate_id_card(student.id_card): + return False, "身份证号码格式不正确" + + if not Validator.validate_stu_id(student.stu_id): + return False, "学号格式不正确" + + if not Validator.validate_name(student.name): + return False, "姓名格式不正确,必须为2-20个中文字符" + + if student.height is not None and not Validator.validate_height(student.height): + return False, "身高必须在50-250cm之间" + + if student.weight is not None and not Validator.validate_weight(student.weight): + return False, "体重必须在5-300kg之间" + + if student.email and not Validator.validate_email(student.email): + return False, "电子邮箱格式不正确" + + if student.phone and not Validator.validate_phone(student.phone): + return False, "手机号码格式不正确" + + if student.enrollment_date and student.birthday: + if not Validator.validate_enrollment_date(student.enrollment_date, student.birthday): + return False, "入学日期不能早于出生日期" + + # 执行数据更新操作 + if self.dal.update(student): + return True, "学生信息更新成功" + else: + return False, "学生信息更新失败,未找到该学生" + + def delete_student_by_id(self, id_card: str) -> bool: + """ + 根据身份证号删除学生记录 + + Args: + id_card: 要删除学生的身份证号 + + Returns: + 操作是否成功 + """ + return self.dal.delete_by_id(id_card) + + def delete_student_by_stu_id(self, stu_id: str) -> bool: + """ + 根据学号删除学生记录 + + Args: + stu_id: 要删除学生的学号 + + Returns: + 操作是否成功 + """ + return self.dal.delete_by_stu_id(stu_id) + + def get_student_by_id(self, id_card: str) -> Optional[Student]: + """ + 根据身份证号查询学生信息 + + Args: + id_card: 要查询学生的身份证号 + + Returns: + 匹配的学生对象,未找到返回None + """ + return self.dal.get_by_id(id_card) + + def get_student_by_stu_id(self, stu_id: str) -> Optional[Student]: + """ + 根据学号查询学生信息 + + Args: + stu_id: 要查询学生的学号 + + Returns: + 匹配的学生对象,未找到返回None + """ + return self.dal.get_by_stu_id(stu_id) + + def get_all_students(self) -> List[Student]: + """ + 获取系统中所有学生记录 + + Returns: + 包含所有学生对象的列表 + """ + return self.dal.get_all() + + def search_students_by_name(self, name: str) -> List[Student]: + """ + 根据姓名模糊查询学生信息 + + Args: + name: 要查询的姓名关键词 + + Returns: + 包含匹配学生对象的列表 + """ + return self.dal.search_by_name(name) + + def search_students_by_class(self, class_name: str) -> List[Student]: + """ + 根据班级模糊查询学生信息 + + Args: + class_name: 要查询的班级关键词 + + Returns: + 包含匹配学生对象的列表 + """ + return self.dal.search_by_class(class_name) + + def search_students_by_major(self, major: str) -> List[Student]: + """ + 根据专业模糊查询学生信息 + + Args: + major: 要查询的专业关键词 + + Returns: + 包含匹配学生对象的列表 + """ + return self.dal.search_by_major(major) + + def get_total_students(self) -> int: + """ + 获取系统中学生总数 + + Returns: + 学生总人数 + """ + return len(self.get_all_students()) + + def get_students_by_major(self) -> dict: + """ + 统计各专业学生分布情况 + + Returns: + 字典,键为专业名称,值为该专业学生人数 + """ + major_count = {} + for student in self.get_all_students(): + if student.major: + major_count[student.major] = major_count.get(student.major, 0) + 1 + return major_count + + def calculate_average_height(self, group_by: str = None, group_value: str = None) -> float: + """ + 计算学生平均身高,支持按班级或专业分组计算 + + Args: + group_by: 分组依据,可选'class'或'major',不分组时为None + group_value: 分组值,如具体班级名称或专业名称 + + Returns: + 平均身高,单位厘米,无有效数据时返回0 + """ + students = self.get_all_students() + + # 按指定条件筛选学生群体 + if group_by == 'class': + students = [s for s in students if s.class_name == group_value] + elif group_by == 'major': + students = [s for s in students if s.major == group_value] + + # 过滤有效身高数据并计算平均值 + heights = [s.height for s in students if s.height is not None] + return sum(heights) / len(heights) if heights else 0 + + def calculate_average_weight(self, group_by: str = None, group_value: str = None) -> float: + """ + 计算学生平均体重,支持按班级或专业分组计算 + + Args: + group_by: 分组依据,可选'class'或'major',不分组时为None + group_value: 分组值,如具体班级名称或专业名称 + + Returns: + 平均体重,单位千克,无有效数据时返回0 + """ + students = self.get_all_students() + + # 按指定条件筛选学生群体 + if group_by == 'class': + students = [s for s in students if s.class_name == group_value] + elif group_by == 'major': + students = [s for s in students if s.major == group_value] + + # 过滤有效体重数据并计算平均值 + weights = [s.weight for s in students if s.weight is not None] + return sum(weights) / len(weights) if weights else 0 + + def get_gender_ratio(self) -> dict: + """ + 统计学生性别比例 + + Returns: + 包含以下键的字典: + - total: 总人数 + - male: 男生人数 + - female: 女生人数 + - male_ratio: 男生比例 + - female_ratio: 女生比例 + """ + total = 0 + male_count = 0 + female_count = 0 + + # 遍历统计性别分布 + for student in self.get_all_students(): + if student.gender is not None: + total += 1 + if student.gender: + male_count += 1 + else: + female_count += 1 + + # 计算比例并返回结果 + return { + 'total': total, + 'male': male_count, + 'female': female_count, + 'male_ratio': male_count / total if total > 0 else 0, + 'female_ratio': female_count / total if total > 0 else 0 + } \ No newline at end of file diff --git a/student/dal/__init__.py b/student/dal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/student/dal/csv_student_dal.py b/student/dal/csv_student_dal.py new file mode 100644 index 0000000..8844664 --- /dev/null +++ b/student/dal/csv_student_dal.py @@ -0,0 +1,192 @@ +import csv +from typing import List, Optional +from .student_dal import IStudentDAL +from ..model.student import Student +from datetime import datetime + + +class CsvStudentDAL(IStudentDAL): + """学生信息的CSV文件存储实现""" + + def __init__(self, file_path: str = 'students.csv'): + self.file_path = file_path + self.headers = ['name', 'id_card', 'stu_id', 'gender', 'height', 'weight', + 'enrollment_date', 'class_name', 'major', 'email', 'phone'] + + # 确保文件存在 + self._ensure_file_exists() + + def _ensure_file_exists(self): + """确保CSV文件存在并包含表头""" + try: + with open(self.file_path, 'r', encoding='utf-8') as file: + reader = csv.reader(file) + if not any(reader): # 文件为空 + self._write_headers() + except FileNotFoundError: + self._write_headers() + + def _write_headers(self): + """写入CSV文件表头""" + with open(self.file_path, 'w', encoding='utf-8', newline='') as file: + writer = csv.DictWriter(file, fieldnames=self.headers) + writer.writeheader() + + def _student_to_row(self, student: Student) -> dict: + """将Student对象转换为CSV行数据""" + return { + 'name': student.name, + 'id_card': student.id_card, + 'stu_id': student.stu_id, + 'gender': student.gender, + 'height': student.height, + 'weight': student.weight, + 'enrollment_date': str(student.enrollment_date) if student.enrollment_date else '', + 'class_name': student.class_name, + 'major': student.major, + 'email': student.email, + 'phone': student.phone + } + + def _row_to_student(self, row: dict) -> Student: + """将CSV行数据转换为Student对象""" + # 处理日期类型,将字符串转换为datetime.date对象 + enrollment_date = row['enrollment_date'] + if enrollment_date: + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() + + # 处理布尔类型,将字符串转换为布尔值 + gender = row['gender'] + if gender: + gender = gender.lower() == 'true' + + # 处理数值类型,将字符串转换为数值 + height = int(row['height']) if row['height'] else None + weight = float(row['weight']) if row['weight'] else None + # 创建并返回学生对象 + return Student( + name=row['name'], + id_card=row['id_card'], + stu_id=row['stu_id'], + gender=gender, + height=height, + weight=weight, + enrollment_date=enrollment_date, + class_name=row['class_name'], + major=row['major'], + email=row['email'], + phone=row['phone'] + ) + + def get_by_id(self, id_card: str) -> Optional[Student]: + """根据身份证号获取学生信息""" + with open(self.file_path, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + for row in reader: + if row['id_card'] == id_card: + return self._row_to_student(row) + return None + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号获取学生信息""" + with open(self.file_path, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + for row in reader: + if row['stu_id'] == stu_id: + return self._row_to_student(row) + return None + + def get_all(self) -> List[Student]: + """获取所有学生信息""" + students = [] + with open(self.file_path, 'r', encoding='utf-8') as file: + reader = csv.DictReader(file) + for row in reader: + students.append(self._row_to_student(row)) + return students + + def add(self, student: Student) -> bool: + """添加学生信息""" + # 检查学号和身份证号是否已存在 + if self.get_by_id(student.id_card) or self.get_by_stu_id(student.stu_id): + return False + # 将学生信息追加到CSV文件 + with open(self.file_path, 'a', encoding='utf-8', newline='') as file: + writer = csv.DictWriter(file, fieldnames=self.headers) + writer.writerow(self._student_to_row(student)) + return True + + def update(self, student: Student) -> bool: + """ + 更新学生信息 + 通过身份证号匹配要更新的记录 + """ + students = self.get_all() # 获取所有学生信息 + updated = False + + # 重写整个CSV文件,替换匹配的学生记录 + with open(self.file_path, 'w', encoding='utf-8', newline='') as file: + writer = csv.DictWriter(file, fieldnames=self.headers) + writer.writeheader()# 写入表头 + + for s in students: + if s.id_card == student.id_card: # 找到匹配的学生记录 + writer.writerow(self._student_to_row(student)) + updated = True + else: + writer.writerow(self._student_to_row(s)) # 保持原有记录不变 + + return updated + + def delete_by_id(self, id_card: str) -> bool: + """ + 根据身份证号删除学生信息 + 通过重写整个文件排除要删除的记录 + """ + students = self.get_all() # 获取所有学生信息 + deleted = False + + # 重写整个CSV文件,排除要删除的学生记录 + with open(self.file_path, 'w', encoding='utf-8', newline='') as file: + writer = csv.DictWriter(file, fieldnames=self.headers) + writer.writeheader() # 写入表头 + + for s in students: + if s.id_card == id_card: # 找到匹配的学生记录 + deleted = True # 标记已删除 + else: + writer.writerow(self._student_to_row(s)) # 保留其他记录 + + return deleted + + def delete_by_stu_id(self, stu_id: str) -> bool: + """ + 根据学号删除学生信息 + 通过重写整个文件排除要删除的记录 + """ + students = self.get_all() # 获取所有学生信息 + deleted = False + + # 重写整个CSV文件,排除要删除的学生记录 + with open(self.file_path, 'w', encoding='utf-8', newline='') as file: + writer = csv.DictWriter(file, fieldnames=self.headers) + writer.writeheader() # 写入表头 + + for s in students: + if s.stu_id == stu_id: # 找到匹配的学生记录 + deleted = True # 标记已删除 + else: + writer.writerow(self._student_to_row(s)) # 保留其他记录 + + return deleted + def search_by_name(self, name: str) -> List[Student]: + """根据姓名模糊查询学生信息""" + return [s for s in self.get_all() if name in s.name] + + def search_by_class(self, class_name: str) -> List[Student]: + """根据班级模糊查询学生信息""" + return [s for s in self.get_all() if class_name in (s.class_name or '')] + + def search_by_major(self, major: str) -> List[Student]: + """根据专业模糊查询学生信息""" + return [s for s in self.get_all() if major in (s.major or '')] \ No newline at end of file diff --git a/student/dal/json_student_dal.py b/student/dal/json_student_dal.py new file mode 100644 index 0000000..52d2259 --- /dev/null +++ b/student/dal/json_student_dal.py @@ -0,0 +1,226 @@ +import json +from typing import List, Optional +from .student_dal import IStudentDAL +from ..model.student import Student +from datetime import datetime + + +class JsonStudentDAL(IStudentDAL): + """学生信息的JSON文件存储实现,实现了IStudentDAL接口,提供基于JSON文件的学生数据增删改查功能""" + + def __init__(self, file_path: str = 'students.json'): + """初始化JSON数据访问层 + :param file_path: JSON数据文件路径,默认使用当前目录下的students.json""" + self.file_path = file_path + self._ensure_file_exists() # 调用私有方法确保数据文件存在,避免后续操作报错 + + def _ensure_file_exists(self): + """确保JSON数据文件存在且格式正确 + 处理两种异常情况: + 1. 文件不存在时创建空JSON数组文件 + 2. 文件存在但非JSON格式时(如损坏)重建空文件""" + try: + with open(self.file_path, 'r', encoding='utf-8') as file: + json.load(file) # 尝试解析文件,验证是否为合法JSON + except (FileNotFoundError, json.JSONDecodeError): + # 异常处理:文件不存在或解析失败时,创建包含空数组的新文件 + with open(self.file_path, 'w', encoding='utf-8') as file: + json.dump([], file) # 写入空数组,确保文件格式正确 + + def _student_to_dict(self, student: Student) -> dict: + """将Student实体对象转换为JSON兼容的字典格式 + :param student: 学生实体对象 + :return: 可序列化的字典,日期类型转换为字符串 + 处理逻辑: + - 日期字段enrollment_date转为%Y-%m-%d格式字符串 + - None值保持为None,确保JSON序列化合法""" + return { + 'name': student.name, # 直接存储字符串属性 + 'id_card': student.id_card, # 身份证号(字符串) + 'stu_id': student.stu_id, # 学号(字符串) + 'gender': student.gender, # 性别(布尔值) + 'height': student.height, # 身高(整数) + 'weight': student.weight, # 体重(整数) + # 日期转字符串:若存在日期则格式化为%Y-%m-%d,否则保持None + 'enrollment_date': str(student.enrollment_date) if student.enrollment_date else None, + 'class_name': student.class_name, # 班级(字符串) + 'major': student.major, # 专业(字符串) + 'email': student.email, # 邮箱(字符串) + 'phone': student.phone # 电话(字符串) + } + + def _dict_to_student(self, data: dict) -> Student: + """将JSON字典转换为Student实体对象 + :param data: 从JSON文件读取的字典数据 + :return: 初始化的Student对象,字符串日期转换为date对象 + 处理逻辑: + - enrollment_date字段从字符串解析为datetime.date对象 + - 缺失字段使用None或默认值初始化""" + enrollment_date = data.get('enrollment_date') + if enrollment_date: + # 字符串转日期:使用strptime解析为datetime对象,再提取date部分 + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() + + return Student( + name=data.get('name'), # 获取姓名,缺失时为None + 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=enrollment_date, # 转换后的入学日期 + class_name=data.get('class_name'), # 获取班级 + major=data.get('major'), # 获取专业 + email=data.get('email'), # 获取邮箱 + phone=data.get('phone') # 获取电话 + ) + + def _load_data(self) -> List[dict]: + """从JSON文件加载学生数据 + :return: 包含学生字典的列表 + 封装文件读取逻辑,统一处理编码和JSON解析""" + with open(self.file_path, 'r', encoding='utf-8') as file: + return json.load(file) # 读取文件并解析为Python列表 + + def _save_data(self, data: List[dict]): + """将学生数据保存至JSON文件 + :param data: 包含学生字典的列表 + 保存逻辑: + - ensure_ascii=False确保中文正常存储 + - indent=2添加缩进,提升文件可读性""" + with open(self.file_path, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=2) # 写入文件,禁用ASCII转义 + + def get_by_id(self, id_card: str) -> Optional[Student]: + """根据身份证号查询学生信息 + :param id_card: 待查询的身份证号 + :return: 匹配的Student对象或None + 实现逻辑: + - 加载全部数据遍历匹配 + - 找到后转换为Student对象返回""" + for data in self._load_data(): + if data.get('id_card') == id_card: # 精确匹配身份证号 + return self._dict_to_student(data) # 转换为实体对象 + return None # 未找到返回None + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号查询学生信息,逻辑同get_by_id + :param stu_id: 待查询的学号 + :return: 匹配的Student对象或None""" + for data in self._load_data(): + if data.get('stu_id') == stu_id: # 精确匹配学号 + return self._dict_to_student(data) + return None + + def get_all(self) -> List[Student]: + """获取所有学生信息 + :return: 包含所有Student对象的列表 + 实现逻辑: + - 加载全部字典数据 + - 逐个转换为Student对象""" + return [self._dict_to_student(data) for data in self._load_data()] # 列表推导式批量转换 + + def add(self, student: Student) -> bool: + """添加新学生信息 + :param student: 待添加的Student对象 + :return: 添加成功返回True,失败返回False + 业务逻辑: + 1. 先校验身份证号和学号的唯一性 + 2. 校验通过后追加数据并保存 + 3. 唯一性冲突时返回False""" + # 检查身份证号或学号是否已存在 + if self.get_by_id(student.id_card) or self.get_by_stu_id(student.stu_id): + return False # 已存在相同标识,添加失败 + + data = self._load_data() # 加载当前数据 + data.append(self._student_to_dict(student)) # 转换并追加 + self._save_data(data) # 保存更新后的数据 + return True # 添加成功 + + def update(self, student: Student) -> bool: + """更新学生信息 + :param student: 包含更新后信息的Student对象 + :return: 更新成功返回True,失败返回False + 实现逻辑: + 1. 按身份证号定位记录 + 2. 找到后替换为新数据 + 3. 未找到或无变更时返回False""" + data = self._load_data() # 加载当前数据 + updated = False # 标记是否更新 + + for i, item in enumerate(data): + if item.get('id_card') == student.id_card: # 匹配身份证号 + data[i] = self._student_to_dict(student) # 替换为新数据 + updated = True + break + + if updated: + self._save_data(data) # 保存更新后的数据 + return updated # 返回是否更新成功 + + def delete_by_id(self, id_card: str) -> bool: + """根据身份证号删除学生信息 + :param id_card: 待删除学生的身份证号 + :return: 删除成功返回True,失败返回False + 实现逻辑: + 1. 过滤掉目标身份证号的记录 + 2. 比较删除前后数据长度 + 3. 长度变化时保存并返回True""" + data = self._load_data() # 加载当前数据 + original_length = len(data) # 记录原始长度 + + # 过滤掉目标身份证号的记录 + data = [item for item in data if item.get('id_card') != id_card] + + if len(data) < original_length: # 长度减少表示删除成功 + self._save_data(data) # 保存更新后的数据 + return True + return False # 未找到目标记录 + + def delete_by_stu_id(self, stu_id: str) -> bool: + """根据学号删除学生信息,逻辑同delete_by_id + :param stu_id: 待删除学生的学号 + :return: 删除成功返回True,失败返回False""" + data = self._load_data() + original_length = len(data) + + data = [item for item in data if item.get('stu_id') != stu_id] + + if len(data) < original_length: + self._save_data(data) + return True + return False + + def search_by_name(self, name: str) -> List[Student]: + """根据姓名模糊查询学生 + :param name: 待查询的姓名片段 + :return: 包含匹配学生的列表 + 实现逻辑: + - 遍历所有数据 + - 使用in操作符实现包含匹配 + - 处理name字段为None的情况""" + return [ + self._dict_to_student(data) for data in self._load_data() + if name in data.get('name', '') # data.get('name', '')避免KeyError,空字符串确保in操作合法 + ] + + def search_by_class(self, class_name: str) -> List[Student]: + """根据班级模糊查询学生 + :param class_name: 待查询的班级片段 + :return: 包含匹配学生的列表 + 实现逻辑: + - 处理class_name为None的情况(替换为空字符串) + - 使用in操作符实现包含匹配""" + return [ + self._dict_to_student(data) for data in self._load_data() + if class_name in (data.get('class_name') or '') # data.get('class_name')为None时替换为空字符串 + ] + + def search_by_major(self, major: str) -> List[Student]: + """根据专业模糊查询学生,逻辑同search_by_class + :param major: 待查询的专业片段 + :return: 包含匹配学生的列表""" + return [ + self._dict_to_student(data) for data in self._load_data() + if major in (data.get('major') or '') # 处理major为None的情况 + ] diff --git a/student/dal/sqlite_student_dal.py b/student/dal/sqlite_student_dal.py new file mode 100644 index 0000000..b1d953a --- /dev/null +++ b/student/dal/sqlite_student_dal.py @@ -0,0 +1,234 @@ +import sqlite3 +from typing import List, Optional +from .student_dal import IStudentDAL +from ..model.student import Student +from datetime import datetime + + +class SQLiteStudentDAL(IStudentDAL): + """学生信息的SQLite数据库存储实现,实现了IStudentDAL接口,提供基于SQLite的学生数据持久化功能""" + + def __init__(self, db_path: str = 'students.db'): + """初始化SQLite数据访问层 + :param db_path: 数据库文件路径,默认使用当前目录下的students.db""" + self.db_path = db_path + self._create_table() # 确保学生表存在,不存在则创建 + + def _create_table(self): + """创建学生信息表(如果不存在) + 表结构设计: + - id:自增主键 + - id_card/stu_id:设置唯一约束,确保数据唯一性 + - enrollment_date:存储为YYYY-MM-DD格式字符串 + - gender:布尔值存储为整数(0/1)""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute(''' + CREATE TABLE IF NOT EXISTS students ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, -- 学生姓名,非空 + id_card TEXT UNIQUE NOT NULL, -- 身份证号,唯一且非空 + stu_id TEXT UNIQUE NOT NULL, -- 学号,唯一且非空 + gender INTEGER, -- 性别(0/1) + height INTEGER, -- 身高(厘米) + weight REAL, -- 体重(公斤) + enrollment_date TEXT, -- 入学日期(YYYY-MM-DD) + class_name TEXT, -- 班级名称 + major TEXT, -- 专业 + email TEXT, -- 邮箱 + phone TEXT -- 电话 + ) + ''') + conn.commit() # 提交DDL语句 + + def _row_to_student(self, row: tuple) -> Optional[Student]: + """将数据库查询结果行转换为Student对象 + :param row: 查询结果元组 + :return: 转换后的Student对象或None + 处理逻辑: + - 索引映射:将数据库列索引映射到Student属性 + - 类型转换:日期字符串转date对象,整数转布尔值""" + if not row: # 处理空结果 + return None + + # 处理日期类型:从字符串转换为datetime.date + enrollment_date = row[7] # 注意:索引7对应enrollment_date列 + if enrollment_date: + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() + + # 处理布尔类型:数据库中存储为0/1,转换为True/False + gender = row[4] # 注意:索引4对应gender列 + if gender is not None: + gender = bool(gender) + + # 构建并返回Student对象 + return Student( + name=row[1], # 姓名 + id_card=row[2], # 身份证号 + stu_id=row[3], # 学号 + gender=gender, # 性别(布尔值) + height=row[4], # 身高 + weight=row[5], # 体重 + enrollment_date=enrollment_date, # 入学日期 + class_name=row[8], # 班级 + major=row[9], # 专业 + email=row[10], # 邮箱 + phone=row[11] # 电话 + ) + + def get_by_id(self, id_card: str) -> Optional[Student]: + """根据身份证号查询学生信息 + :param id_card: 待查询的身份证号 + :return: 匹配的Student对象或None + 实现逻辑: + - 使用参数化查询防止SQL注入 + - 通过id_card唯一索引快速定位记录""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM students WHERE id_card = ?", (id_card,)) + row = cursor.fetchone() # 获取单条记录 + return self._row_to_student(row) # 转换为Student对象 + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号查询学生信息,逻辑同get_by_id + :param stu_id: 待查询的学号 + :return: 匹配的Student对象或None""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM students WHERE stu_id = ?", (stu_id,)) + row = cursor.fetchone() + return self._row_to_student(row) + + def get_all(self) -> List[Student]: + """获取所有学生信息 + :return: 包含所有学生的列表 + 实现逻辑: + - 查询全量数据 + - 批量转换为Student对象""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM students") + rows = cursor.fetchall() # 获取所有记录 + return [self._row_to_student(row) for row in rows] # 列表推导式批量转换 + + def add(self, student: Student) -> bool: + """添加学生信息 + :param student: 待添加的Student对象 + :return: 添加成功返回True,失败返回False + 业务逻辑: + 1. 使用事务确保数据一致性 + 2. 自动处理日期类型转换 + 3. 利用数据库唯一约束防止重复数据 + 4. 异常处理:IntegrityError表示违反唯一性约束""" + try: + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """INSERT INTO students + (name, id_card, stu_id, gender, height, weight, enrollment_date, class_name, major, email, phone) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", + ( + student.name, + student.id_card, + student.stu_id, + student.gender, + student.height, + student.weight, + str(student.enrollment_date) if student.enrollment_date else None, # 日期转字符串 + student.class_name, + student.major, + student.email, + student.phone + ) + ) + conn.commit() # 提交事务 + return True + except sqlite3.IntegrityError: # 违反唯一约束(身份证号或学号重复) + return False + + def update(self, student: Student) -> bool: + """更新学生信息 + :param student: 包含更新信息的Student对象 + :return: 更新成功返回True,失败返回False + 实现逻辑: + 1. 以身份证号作为唯一标识 + 2. 返回受影响的行数判断操作是否成功 + 3. 自动处理日期类型转换""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute( + """UPDATE students + SET name=?, gender=?, height=?, weight=?, enrollment_date=?, + class_name=?, major=?, email=?, phone=? + WHERE id_card=?""", + ( + student.name, + student.gender, + student.height, + student.weight, + str(student.enrollment_date) if student.enrollment_date else None, # 日期转字符串 + student.class_name, + student.major, + student.email, + student.phone, + student.id_card # 条件:根据身份证号更新 + ) + ) + conn.commit() + return cursor.rowcount > 0 # 判断是否有记录被更新 + + def delete_by_id(self, id_card: str) -> bool: + """根据身份证号删除学生信息 + :param id_card: 待删除学生的身份证号 + :return: 删除成功返回True,失败返回False + 实现逻辑: + - 通过受影响行数判断删除是否成功 + - 使用事务确保操作原子性""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM students WHERE id_card = ?", (id_card,)) + conn.commit() + return cursor.rowcount > 0 # 判断是否有记录被删除 + + def delete_by_stu_id(self, stu_id: str) -> bool: + """根据学号删除学生信息,逻辑同delete_by_id + :param stu_id: 待删除学生的学号 + :return: 删除成功返回True,失败返回False""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("DELETE FROM students WHERE stu_id = ?", (stu_id,)) + conn.commit() + return cursor.rowcount > 0 + + def search_by_name(self, name: str) -> List[Student]: + """根据姓名模糊查询学生信息 + :param name: 待查询的姓名片段 + :return: 包含匹配学生的列表 + 实现逻辑: + - 使用LIKE '%name%'实现模糊匹配 + - 支持空字符串查询(返回所有记录)""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM students WHERE name LIKE ?", ('%' + name + '%',)) + rows = cursor.fetchall() + return [self._row_to_student(row) for row in rows] + + def search_by_class(self, class_name: str) -> List[Student]: + """根据班级模糊查询学生信息,逻辑同search_by_name + :param class_name: 待查询的班级片段 + :return: 包含匹配学生的列表""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM students WHERE class_name LIKE ?", ('%' + class_name + '%',)) + rows = cursor.fetchall() + return [self._row_to_student(row) for row in rows] + + def search_by_major(self, major: str) -> List[Student]: + """根据专业模糊查询学生信息,逻辑同search_by_name + :param major: 待查询的专业片段 + :return: 包含匹配学生的列表""" + with sqlite3.connect(self.db_path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT * FROM students WHERE major LIKE ?", ('%' + major + '%',)) + rows = cursor.fetchall() + return [self._row_to_student(row) for row in rows] diff --git a/student/dal/student_dal.py b/student/dal/student_dal.py new file mode 100644 index 0000000..84f10ab --- /dev/null +++ b/student/dal/student_dal.py @@ -0,0 +1,57 @@ +from abc import ABC, abstractmethod +from typing import List, Optional +from ..model.student import Student + + +class IStudentDAL(ABC): + """学生信息数据访问层接口""" + + @abstractmethod + def get_by_id(self, id_card: str) -> Optional[Student]: + """根据身份证号获取学生信息,返回Student对象或None""" + pass + + @abstractmethod + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号获取学生信息,返回Student对象或None""" + pass + + @abstractmethod + def get_all(self) -> List[Student]: + """获取所有学生信息,返回Student对象列表""" + pass + + @abstractmethod + def add(self, student: Student) -> bool: + """添加学生信息,返回操作结果""" + pass + + @abstractmethod + def update(self, student: Student) -> bool: + """更新学生信息,返回操作结果""" + pass + + @abstractmethod + def delete_by_id(self, id_card: str) -> bool: + """根据身份证号删除学生信息,返回操作结果""" + pass + + @abstractmethod + def delete_by_stu_id(self, stu_id: str) -> bool: + """根据学号删除学生信息,返回操作结果""" + pass + + @abstractmethod + def search_by_name(self, name: str) -> List[Student]: + """根据姓名模糊查询学生信息""" + pass + + @abstractmethod + def search_by_class(self, class_name: str) -> List[Student]: + """根据班级模糊查询学生信息""" + pass + + @abstractmethod + def search_by_major(self, major: str) -> List[Student]: + """根据专业模糊查询学生信息""" + pass \ No newline at end of file diff --git a/student/data/scratch_1.json b/student/data/scratch_1.json new file mode 100644 index 0000000..e69de29 diff --git a/student/main.py b/student/main.py new file mode 100644 index 0000000..8fda195 --- /dev/null +++ b/student/main.py @@ -0,0 +1,58 @@ +# main.py +import tkinter as tk +from student.dal.csv_student_dal import CsvStudentDAL +from student.dal.json_student_dal import JsonStudentDAL +from student.dal.sqlite_student_dal import SQLiteStudentDAL +from student.bll.student_bll import StudentBLL +from student.ui.gui_ui import StudentGUI # 修改导入 +import sys +from pprint import pprint +pprint(sys.path) # 打印模块搜索路径 + + +def main(): + # 创建主窗口 + root = tk.Tk() + + # 选择数据存储方式 + storage_choice = tk.StringVar(value="3") + + # 创建选择对话框 + dialog = tk.Toplevel(root) + dialog.title("选择数据存储方式") + dialog.geometry("300x200") + dialog.transient(root) + dialog.grab_set() + + tk.Label(dialog, text="请选择数据存储方式:").pack(pady=10) + + tk.Radiobutton(dialog, text="CSV文件", variable=storage_choice, value="1").pack(anchor=tk.W, padx=20) + tk.Radiobutton(dialog, text="JSON文件", variable=storage_choice, value="2").pack(anchor=tk.W, padx=20) + tk.Radiobutton(dialog, text="SQLite数据库", variable=storage_choice, value="3").pack(anchor=tk.W, padx=20) + + def on_ok(): + dialog.destroy() + + tk.Button(dialog, text="确定", command=on_ok).pack(pady=20) + + # 等待对话框关闭 + root.wait_window(dialog) + + # 根据选择初始化数据访问层 + if storage_choice.get() == '1': + dal = CsvStudentDAL() + elif storage_choice.get() == '2': + dal = JsonStudentDAL() + else: + dal = SQLiteStudentDAL() + + # 初始化业务逻辑层和用户界面 + bll = StudentBLL(dal) + app = StudentGUI(root, bll) + + # 运行主循环 + root.mainloop() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/student/model/__init__.py b/student/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/student/model/student.py b/student/model/student.py new file mode 100644 index 0000000..d95d1db --- /dev/null +++ b/student/model/student.py @@ -0,0 +1,89 @@ +from datetime import date, datetime +from typing import Optional, Union + + +class Student: + """学生实体类,包含学生的基本信息和相关属性""" + + def __init__(self, + name: str, # 姓名,非空字符串 + id_card: str, # 身份证号, 唯一标识学生,非空字符串 + stu_id: str, # 学号, 唯一标识学生,非空字符串 + gender: Optional[bool] = None, # 性别,True为男,False为女 + height: Optional[int] = None, # 身高, 单位为cm + weight: Optional[float] = None, # 体重, 单位为kg,小数点后一位 + 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.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.email = email + self.phone = phone + + # 从身份证号码派生的属性 + self._age = None + self._birthday = None + self._parse_id_card() + + def _parse_id_card(self): + """从身份证号码中提取出生日期和计算年龄""" + if not self.id_card or len(self.id_card) != 18: + return + + try: + # 提取出生日期 + birth_date_str = self.id_card[6:14] + self._birthday = datetime.strptime(birth_date_str, '%Y%m%d').date() + + # 计算年龄 + today = date.today() + self._age = today.year - self._birthday.year - ( + (today.month, today.day) < (self._birthday.month, self._birthday.day)) + + # 从身份证第17位判断性别 + if self.gender is None: + gender_digit = int(self.id_card[16]) + self.gender = gender_digit % 2 == 1 # 奇数为男,偶数为女 + + except ValueError: + # 身份证号码格式错误 + pass + + @property + def age(self) -> int: + """获取计算得到的年龄""" + return self._age + + @property + def birthday(self) -> date: + """获取从身份证提取的出生日期""" + return self._birthday + + 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': str(self.enrollment_date) if self.enrollment_date else None, + 'class_name': self.class_name, + 'major': self.major, + 'email': self.email, + 'phone': self.phone, + 'age': self.age, + 'birthday': str(self.birthday) if self.birthday else None + } \ No newline at end of file diff --git a/student/students.csv b/student/students.csv new file mode 100644 index 0000000..82867a7 --- /dev/null +++ b/student/students.csv @@ -0,0 +1 @@ +name,id_card,stu_id,gender,height,weight,enrollment_date,class_name,major,email,phone diff --git a/student/students.db b/student/students.db new file mode 100644 index 0000000..6c63432 Binary files /dev/null and b/student/students.db differ diff --git a/student/students.json b/student/students.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/student/students.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/student/ui/__init__.py b/student/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/student/ui/console_ui.py b/student/ui/console_ui.py new file mode 100644 index 0000000..5a54031 --- /dev/null +++ b/student/ui/console_ui.py @@ -0,0 +1,350 @@ +from datetime import date, datetime +from typing import List, Optional +from ..bll.student_bll import StudentBLL +from ..model.student import Student + + +class StudentConsoleUI: + """学生信息管理系统的控制台用户界面""" + + def __init__(self, bll: StudentBLL): + self.bll = bll + + def display_menu(self): + """显示主菜单""" + print("\n" + "=" * 50) + print("学生信息管理系统".center(50)) + print("=" * 50) + print("1. 添加学生") + print("2. 删除学生") + print("3. 修改学生信息") + print("4. 查看学生详细信息") + print("5. 查询学生") + print("6. 统计功能") + print("7. 数据导入导出") + print("8. 退出系统") + print("=" * 50) + + def run(self): + """运行系统""" + while True: + self.display_menu() + choice = input("请输入您的选择: ") + + if choice == '1': + self.add_student() + elif choice == '2': + self.delete_student() + elif choice == '3': + self.update_student() + elif choice == '4': + self.view_student_details() + elif choice == '5': + self.search_students() + elif choice == '6': + self.statistics_menu() + elif choice == '7': + self.import_export_menu() + elif choice == '8': + print("感谢使用学生信息管理系统,再见!") + break + else: + print("无效的选择,请重新输入!") + + def add_student(self): + """添加学生界面""" + print("\n添加学生") + print("-" * 50) + + name = input("姓名: ") + id_card = input("身份证号: ") + stu_id = input("学号: ") + + gender_input = input("性别 (男/女,可选): ") + gender = None + if gender_input: + gender = gender_input.lower() == '男' + + height = input("身高 (cm,可选): ") + height = int(height) if height else None + + weight = input("体重 (kg,可选): ") + weight = float(weight) if weight else None + + enrollment_date = input("入学日期 (YYYY-MM-DD,可选): ") + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None + + class_name = input("班级名称 (可选): ") + major = input("专业名称 (可选): ") + email = input("电子邮箱 (可选): ") + phone = input("联系电话 (可选): ") + + 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 + ) + + success, message = self.bll.add_student(student) + if success: + print("添加成功!") + else: + print(f"添加失败: {message}") + + def delete_student(self): + """删除学生界面""" + print("\n删除学生") + print("-" * 50) + print("1. 根据身份证号删除") + print("2. 根据学号删除") + choice = input("请选择删除方式: ") + + if choice == '1': + id_card = input("请输入要删除的学生身份证号: ") + if self.bll.delete_student_by_id(id_card): + print("删除成功!") + else: + print("删除失败,未找到该学生!") + elif choice == '2': + stu_id = input("请输入要删除的学生学号: ") + if self.bll.delete_student_by_stu_id(stu_id): + print("删除成功!") + else: + print("删除失败,未找到该学生!") + else: + print("无效的选择!") + + def update_student(self): + """修改学生信息界面""" + print("\n修改学生信息") + print("-" * 50) + id_card = input("请输入要修改的学生身份证号: ") + + student = self.bll.get_student_by_id(id_card) + if not student: + print("未找到该学生!") + return + + print(f"当前学生信息 - 姓名: {student.name}, 学号: {student.stu_id}") + + name = input(f"姓名 ({student.name}): ") or student.name + stu_id = input(f"学号 ({student.stu_id}): ") or student.stu_id + + gender_input = input(f"性别 ({'男' if student.gender else '女' if student.gender is not None else '未设置'}): ") + gender = student.gender + if gender_input: + gender = gender_input.lower() == '男' + + height = input(f"身高 ({student.height} cm): ") + height = int(height) if height else student.height + + weight = input(f"体重 ({student.weight} kg): ") + weight = float(weight) if weight else student.weight + + enrollment_date = input(f"入学日期 ({student.enrollment_date}): ") + enrollment_date = datetime.strptime(enrollment_date, + '%Y-%m-%d').date() if enrollment_date else student.enrollment_date + + class_name = input(f"班级名称 ({student.class_name}): ") or student.class_name + major = input(f"专业名称 ({student.major}): ") or student.major + email = input(f"电子邮箱 ({student.email}): ") or student.email + phone = input(f"联系电话 ({student.phone}): ") or student.phone + + student.name = name + student.stu_id = stu_id + student.gender = gender + student.height = height + student.weight = weight + student.enrollment_date = enrollment_date + student.class_name = class_name + student.major = major + student.email = email + student.phone = phone + + success, message = self.bll.update_student(student) + if success: + print("修改成功!") + else: + print(f"修改失败: {message}") + + def view_student_details(self): + """查看学生详细信息界面""" + print("\n查看学生详细信息") + print("-" * 50) + print("1. 根据身份证号查询") + print("2. 根据学号查询") + choice = input("请选择查询方式: ") + + if choice == '1': + id_card = input("请输入学生身份证号: ") + student = self.bll.get_student_by_id(id_card) + elif choice == '2': + stu_id = input("请输入学生学号: ") + student = self.bll.get_student_by_stu_id(stu_id) + else: + print("无效的选择!") + return + + if not student: + print("未找到该学生!") + return + + self._print_student_details(student) + + def _print_student_details(self, student: Student): + """打印学生详细信息""" + print("\n" + "-" * 50) + print(f"姓名: {student.name}") + print(f"身份证号: {student.id_card}") + print(f"学号: {student.stu_id}") + print(f"性别: {'男' if student.gender else '女' if student.gender is not None else '未设置'}") + print(f"年龄: {student.age if student.age is not None else '未知'}") + print(f"出生日期: {student.birthday if student.birthday else '未知'}") + print(f"身高: {student.height} cm" if student.height else "身高: 未设置") + print(f"体重: {student.weight} kg" if student.weight else "体重: 未设置") + print(f"入学日期: {student.enrollment_date}" if student.enrollment_date else "入学日期: 未设置") + print(f"班级: {student.class_name if student.class_name else '未设置'}") + print(f"专业: {student.major if student.major else '未设置'}") + print(f"电子邮箱: {student.email if student.email else '未设置'}") + print(f"联系电话: {student.phone if student.phone else '未设置'}") + print("-" * 50) + + def search_students(self): + """查询学生界面""" + print("\n查询学生") + print("-" * 50) + print("1. 按姓名查询") + print("2. 按班级查询") + print("3. 按专业查询") + choice = input("请选择查询方式: ") + + if choice == '1': + keyword = input("请输入姓名关键词: ") + students = self.bll.search_students_by_name(keyword) + elif choice == '2': + keyword = input("请输入班级关键词: ") + students = self.bll.search_students_by_class(keyword) + elif choice == '3': + keyword = input("请输入专业关键词: ") + students = self.bll.search_students_by_major(keyword) + else: + print("无效的选择!") + return + + if not students: + print("未找到匹配的学生!") + return + + print(f"\n共找到 {len(students)} 名学生:") + for i, student in enumerate(students, 1): + print( + f"{i}. {student.name} - {student.stu_id} - {student.class_name if student.class_name else '未知班级'}") + + view_choice = input("是否查看详情?(y/n): ") + if view_choice.lower() == 'y': + detail_index = input("请输入要查看的学生序号: ") + try: + index = int(detail_index) - 1 + if 0 <= index < len(students): + self._print_student_details(students[index]) + else: + print("无效的序号!") + except ValueError: + print("请输入有效的数字!") + + def statistics_menu(self): + """统计功能菜单""" + print("\n统计功能") + print("-" * 50) + print("1. 学生总数") + print("2. 各专业学生人数") + print("3. 计算平均身高") + print("4. 计算平均体重") + print("5. 统计性别比例") + choice = input("请选择统计功能: ") + + if choice == '1': + total = self.bll.get_total_students() + print(f"学生总数: {total} 人") + elif choice == '2': + major_count = self.bll.get_students_by_major() + print("\n各专业学生人数:") + for major, count in major_count.items(): + print(f"{major}: {count} 人") + elif choice == '3': + print("1. 全部学生平均身高") + print("2. 按班级计算平均身高") + print("3. 按专业计算平均身高") + sub_choice = input("请选择计算方式: ") + + if sub_choice == '1': + avg_height = self.bll.calculate_average_height() + print(f"全部学生平均身高: {avg_height:.2f} cm") + elif sub_choice == '2': + class_name = input("请输入班级名称: ") + avg_height = self.bll.calculate_average_height('class', class_name) + print(f"{class_name} 班级平均身高: {avg_height:.2f} cm") + elif sub_choice == '3': + major = input("请输入专业名称: ") + avg_height = self.bll.calculate_average_height('major', major) + print(f"{major} 专业平均身高: {avg_height:.2f} cm") + else: + print("无效的选择!") + elif choice == '4': + print("1. 全部学生平均体重") + print("2. 按班级计算平均体重") + print("3. 按专业计算平均体重") + sub_choice = input("请选择计算方式: ") + + if sub_choice == '1': + avg_weight = self.bll.calculate_average_weight() + print(f"全部学生平均体重: {avg_weight:.2f} kg") + elif sub_choice == '2': + class_name = input("请输入班级名称: ") + avg_weight = self.bll.calculate_average_weight('class', class_name) + print(f"{class_name} 班级平均体重: {avg_weight:.2f} kg") + elif sub_choice == '3': + major = input("请输入专业名称: ") + avg_weight = self.bll.calculate_average_weight('major', major) + print(f"{major} 专业平均体重: {avg_weight:.2f} kg") + else: + print("无效的选择!") + elif choice == '5': + ratio = self.bll.get_gender_ratio() + print("\n性别比例统计:") + print(f"总人数: {ratio['total']}") + print(f"男生人数: {ratio['male']} ({ratio['male_ratio']:.2%})") + print(f"女生人数: {ratio['female']} ({ratio['female_ratio']:.2%})") + else: + print("无效的选择!") + + def import_export_menu(self): + """数据导入导出菜单""" + print("\n数据导入导出") + print("-" * 50) + print("1. 导出学生数据到CSV") + print("2. 导出学生数据到JSON") + print("3. 从CSV导入学生数据") + print("4. 从JSON导入学生数据") + choice = input("请选择操作: ") + + # 这里只是示例,实际实现需要根据具体的数据存储方式编写 + if choice in ['1', '2', '3', '4']: + print("功能开发中,暂未实现...") + else: + print("无效的选择!") +def _get_age(self, birthday: Optional[date]) -> Optional[int]: + """计算学生年龄""" + if not birthday: + return None + today = date.today() + age = today.year - birthday.year - ((today.month, today.day) < (birthday.month, birthday.day)) + return age \ No newline at end of file diff --git a/student/ui/gui_ui.py b/student/ui/gui_ui.py new file mode 100644 index 0000000..3398a60 --- /dev/null +++ b/student/ui/gui_ui.py @@ -0,0 +1,685 @@ +# student/ui/gui_ui.py +import tkinter as tk +from tkinter import ttk, messagebox +from datetime import datetime, date +import re +from student.model.student import Student +from typing import List, Optional +from ..bll.student_bll import StudentBLL +from ..model.student import Student +from ..util.validator import Validator + + +class StudentGUI: + """学生信息管理系统的图形用户界面""" + + def __init__(self, root: tk.Tk, bll: StudentBLL): + self.root = root + self.root.title("学生信息管理系统") + self.root.geometry("900x600") + self.root.minsize(800, 500) + + self.bll = bll + + # 设置中文字体 + self.style = ttk.Style() + self.style.configure("TLabel", font=("SimHei", 10)) + self.style.configure("TButton", font=("SimHei", 10)) + self.style.configure("TTreeview", font=("SimHei", 10)) + + # 创建主框架 + self.main_frame = ttk.Frame(self.root) + self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 创建顶部导航栏 + self.create_navigation_bar() + + # 创建主内容区域 + self.content_frame = ttk.Frame(self.main_frame) + self.content_frame.pack(fill=tk.BOTH, expand=True, pady=10) + + # 默认显示学生列表 + self.show_student_list() + + def create_navigation_bar(self): + """创建顶部导航栏""" + nav_frame = ttk.Frame(self.main_frame, height=40) + nav_frame.pack(fill=tk.X, side=tk.TOP) + + # 添加导航按钮 + buttons = [ + ("添加学生", self.show_add_student), + ("修改学生", self.show_update_student), + ("删除学生", self.show_delete_student), + ("查询学生", self.show_search_student), + ("统计信息", self.show_statistics), + ("数据导入导出", self.show_import_export) + ] + + for text, command in buttons: + btn = ttk.Button(nav_frame, text=text, command=command) + btn.pack(side=tk.LEFT, padx=5, pady=5) + + def clear_content_frame(self): + """清空内容区域""" + for widget in self.content_frame.winfo_children(): + widget.destroy() + + def show_student_list(self, students: Optional[List[Student]] = None): + """显示学生列表""" + self.clear_content_frame() + + if students is None: + students = self.bll.get_all_students() + + # 创建搜索框 + search_frame = ttk.Frame(self.content_frame) + search_frame.pack(fill=tk.X, pady=5) + + ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT, padx=5) + search_var = tk.StringVar() + search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30) + search_entry.pack(side=tk.LEFT, padx=5) + + def search_students(): + keyword = search_var.get() + if keyword: + search_results = [] + search_results.extend(self.bll.search_students_by_name(keyword)) + search_results.extend(self.bll.search_students_by_class(keyword)) + search_results.extend(self.bll.search_students_by_major(keyword)) + # 去重 + unique_students = [] + ids = set() + for s in search_results: + if s.id_card not in ids: + unique_students.append(s) + ids.add(s.id_card) + self.show_student_list(unique_students) + else: + self.show_student_list() + + ttk.Button(search_frame, text="搜索", command=search_students).pack(side=tk.LEFT, padx=5) + + # 创建表格 + columns = ("name", "id_card", "stu_id", "gender", "age", "class_name", "major") + tree = ttk.Treeview(self.content_frame, columns=columns, show="headings") + + # 设置列标题 + tree.heading("name", text="姓名") + tree.heading("id_card", text="身份证号") + tree.heading("stu_id", text="学号") + tree.heading("gender", text="性别") + tree.heading("age", text="年龄") + tree.heading("class_name", text="班级") + tree.heading("major", text="专业") + + # 设置列宽 + tree.column("name", width=80) + tree.column("id_card", width=150) + tree.column("stu_id", width=100) + tree.column("gender", width=50) + tree.column("age", width=50) + tree.column("class_name", width=100) + tree.column("major", width=100) + + # 添加数据 + for student in students: + gender_text = "男" if student.gender else "女" if student.gender is not None else "" + age_text = str(student.age) if student.age is not None else "" + tree.insert("", tk.END, values=( + student.name, + student.id_card, + student.stu_id, + gender_text, + age_text, + student.class_name or "", + student.major or "" + )) + + # 添加滚动条 + scrollbar = ttk.Scrollbar(self.content_frame, orient=tk.VERTICAL, command=tree.yview) + tree.configure(yscroll=scrollbar.set) + + # 布局 + scrollbar.pack(side=tk.RIGHT, fill=tk.Y) + tree.pack(fill=tk.BOTH, expand=True) + + # 双击查看详情 + def on_double_click(event): + item = tree.selection() + if item: + id_card = tree.item(item[0])["values"][1] # 身份证号在第二列 + student = self.bll.get_student_by_id(id_card) + if student: + self.show_student_details(student) + + tree.bind("", on_double_click) + + def show_student_details(self, student: Student): + """显示学生详情""" + self.clear_content_frame() + + # 创建详情表单 + detail_frame = ttk.LabelFrame(self.content_frame, text=f"学生详情 - {student.name}") + detail_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 左侧信息 + left_frame = ttk.Frame(detail_frame) + left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 右侧信息 + right_frame = ttk.Frame(detail_frame) + right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 基本信息 + info_items = [ + ("姓名:", student.name), + ("身份证号:", student.id_card), + ("学号:", student.stu_id), + ("性别:", "男" if student.gender else "女" if student.gender is not None else "未设置"), + ("年龄:", str(student.age) if student.age is not None else "未知"), + ("出生日期:", str(student.birthday) if student.birthday else "未知"), + ("身高:", f"{student.height} cm" if student.height else "未设置"), + ("体重:", f"{student.weight} kg" if student.weight else "未设置"), + ("入学日期:", str(student.enrollment_date) if student.enrollment_date else "未设置"), + ("班级:", student.class_name if student.class_name else "未设置"), + ("专业:", student.major if student.major else "未设置"), + ("电子邮箱:", student.email if student.email else "未设置"), + ("联系电话:", student.phone if student.phone else "未设置") + ] + + # 显示信息 + for i, (label, value) in enumerate(info_items): + if i < len(info_items) // 2: # 前半部分放左侧 + ttk.Label(left_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=5) + ttk.Label(left_frame, text=value).grid(row=i, column=1, sticky=tk.W, pady=5) + else: # 后半部分放右侧 + ttk.Label(right_frame, text=label).grid(row=i - len(info_items) // 2, column=0, sticky=tk.W, pady=5) + ttk.Label(right_frame, text=value).grid(row=i - len(info_items) // 2, column=1, sticky=tk.W, pady=5) + + # 返回按钮 + ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10) + + def show_add_student(self): + """显示添加学生表单""" + self.clear_content_frame() + + form_frame = ttk.LabelFrame(self.content_frame, text="添加学生") + form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 表单字段 + fields = { + "name": tk.StringVar(), + "id_card": tk.StringVar(), + "stu_id": tk.StringVar(), + "gender": tk.StringVar(value="未设置"), + "height": tk.StringVar(), + "weight": tk.StringVar(), + "enrollment_date": tk.StringVar(), + "class_name": tk.StringVar(), + "major": tk.StringVar(), + "email": tk.StringVar(), + "phone": tk.StringVar() + } + + # 错误提示标签和输入框引用 + error_labels = {} + entries = {} + + # 验证函数 + def validate_field(field_name, event=None): + value = fields[field_name].get() + error_msg = "" + # 根据字段类型进行验证 + if field_name == "name": + if not value: + error_msg = "姓名不能为空" + + elif field_name == "id_card": + if not value: + error_msg = "身份证号不能为空" + elif len(value) != 18: + error_msg = "身份证号必须为18位" + elif not value[:17].isdigit(): + error_msg = "前17位必须是数字" + elif not (value[17].isdigit() or value[17].upper() == 'X'): + error_msg = "最后一位必须是数字或X" + + elif field_name == "stu_id": + if not value: + error_msg = "学号不能为空" + + elif field_name == "height": + if value and not value.isdigit(): + error_msg = "请输入数字" + + elif field_name == "weight": + if value and not value.replace('.', '', 1).isdigit(): + error_msg = "请输入数字" + + elif field_name == "enrollment_date": + if value: + try: + datetime.strptime(value, '%Y-%m-%d') + except ValueError: + error_msg = "格式应为YYYY-MM-DD" + + elif field_name == "email": + if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value): + error_msg = "邮箱格式不正确" + + elif field_name == "phone": + if value and not re.match(r"^1[3-9]\d{9}$", value): + error_msg = "手机号格式不正确" + + # 更新错误标签和输入框样式 + if error_labels.get(field_name): + error_labels[field_name].config(text=error_msg, foreground="red") + + if entries.get(field_name): + if error_msg: + entries[field_name].configure(style="Error.TEntry") + else: + entries[field_name].configure(style="TEntry") + + return error_msg + + # 创建表单 + row = 0 + for field, var in fields.items(): + # 字段标签 + label_text = { + "name": "姓名:", + "id_card": "身份证号:", + "stu_id": "学号:", + "height": "身高 (cm):", + "weight": "体重 (kg):", + "enrollment_date": "入学日期 (YYYY-MM-DD):", + "class_name": "班级:", + "major": "专业:", + "email": "电子邮箱:", + "phone": "联系电话:" + }.get(field, f"{field}:") + + ttk.Label(form_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5) + + if field == "gender": + # 性别选择(单选按钮) + gender_frame = ttk.Frame(form_frame) + gender_frame.grid(row=row, column=1, sticky=tk.W, pady=5) + + for i, option in enumerate(["男", "女", "未设置"]): + ttk.Radiobutton(gender_frame, text=option, variable=var, value=option).pack(side=tk.LEFT, padx=5) + + # 性别不需要错误提示 + error_labels[field] = ttk.Label(form_frame, text="") + error_labels[field].grid(row=row, column=2, sticky=tk.W, pady=5) + + else: + # 创建输入框 + entry = ttk.Entry(form_frame, textvariable=var) + entry.grid(row=row, column=1, sticky=tk.EW, pady=5) + entries[field] = entry + + # 绑定验证事件 + var.trace_add("write", lambda name, index, mode, f=field: validate_field(f)) + entry.bind("", lambda event, f=field: validate_field(f, event)) + + # 添加错误提示标签 + error_labels[field] = ttk.Label(form_frame, text="", foreground="red") + error_labels[field].grid(row=row, column=2, sticky=tk.W, pady=5) + + # 添加标准说明 + standard_text = { + "id_card": "(18位数字,最后一位可为X)", + "enrollment_date": "(例如: 2023-09-01)", + "email": "(例如: example@mail.com)", + "phone": "(例如: 13800138000)" + }.get(field, "") + + if standard_text: + ttk.Label(form_frame, text=standard_text, foreground="gray").grid(row=row, column=3, sticky=tk.W, + pady=5) + + row += 1 + + # 设置列权重,使输入框可以拉伸 + form_frame.columnconfigure(1, weight=1) + + # 定义错误样式 + style = ttk.Style() + style.configure("Error.TEntry", fieldbackground="#ffe6e6", foreground="#000000") + + # 提交按钮 + def submit_form(): + # 验证所有字段 + errors = {} + for field in fields: + if field != "gender": # 性别不需要验证 + error = validate_field(field) + if error: + errors[field] = error + + if errors: + messagebox.showerror("输入错误", "请修正以下错误:\n\n" + "\n".join(errors.values())) + return + + # 获取表单数据 + name = fields["name"].get() + id_card = fields["id_card"].get() + stu_id = fields["stu_id"].get() + + gender_text = fields["gender"].get() + gender = None + if gender_text == "男": + gender = True + elif gender_text == "女": + gender = False + + height = fields["height"].get() + height = int(height) if height else None + + weight = fields["weight"].get() + weight = float(weight) if weight else None + + enrollment_date = fields["enrollment_date"].get() + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None + + class_name = fields["class_name"].get() + major = fields["major"].get() + email = fields["email"].get() + phone = fields["phone"].get() + + # 创建学生对象 + 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 + ) + + # 添加学生 + success, message = self.bll.add_student(student) + if success: + messagebox.showinfo("成功", "学生添加成功!") + self.show_student_list() + else: + messagebox.showerror("错误", f"添加失败: {message}") + + # 创建按钮 + button_frame = ttk.Frame(self.content_frame) + button_frame.pack(fill=tk.X, pady=10) + + ttk.Button(button_frame, text="提交", command=submit_form).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5) + + def show_update_student(self): + """显示修改学生表单""" + self.clear_content_frame() + + ttk.Label(self.content_frame, text="请输入要修改的学生身份证号:").pack(pady=10) + + id_card_var = tk.StringVar() + ttk.Entry(self.content_frame, textvariable=id_card_var).pack(fill=tk.X, padx=20, pady=5) + + def search_student(): + id_card = id_card_var.get() + student = self.bll.get_student_by_id(id_card) + + if student: + self._show_update_form(student) + else: + messagebox.showerror("错误", "未找到该学生!") + + ttk.Button(self.content_frame, text="查找学生", command=search_student).pack(pady=10) + ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=5) + + def _show_update_form(self, student: Student): + """显示实际的修改表单""" + self.clear_content_frame() + + form_frame = ttk.LabelFrame(self.content_frame, text=f"修改学生信息 - {student.name}") + form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 表单字段 + fields = { + "name": tk.StringVar(value=student.name), + "id_card": tk.StringVar(value=student.id_card), + "stu_id": tk.StringVar(value=student.stu_id), + "gender": tk.StringVar(value="男" if student.gender else "女" if student.gender is not None else "未设置"), + "height": tk.StringVar(value=str(student.height) if student.height is not None else ""), + "weight": tk.StringVar(value=str(student.weight) if student.weight is not None else ""), + "enrollment_date": tk.StringVar(value=str(student.enrollment_date) if student.enrollment_date else ""), + "class_name": tk.StringVar(value=student.class_name or ""), + "major": tk.StringVar(value=student.major or ""), + "email": tk.StringVar(value=student.email or ""), + "phone": tk.StringVar(value=student.phone or "") + } + + # 性别选项 + gender_options = ["男", "女", "未设置"] + + # 创建表单 + row = 0 + for field, var in fields.items(): + if field == "id_card": # 身份证号不可修改 + ttk.Label(form_frame, text="身份证号:").grid(row=row, column=0, sticky=tk.W, pady=5) + ttk.Label(form_frame, text=student.id_card).grid(row=row, column=1, sticky=tk.W, pady=5) + elif field == "gender": + ttk.Label(form_frame, text="性别:").grid(row=row, column=0, sticky=tk.W, pady=5) + gender_frame = ttk.Frame(form_frame) + gender_frame.grid(row=row, column=1, sticky=tk.W, pady=5) + + for i, option in enumerate(gender_options): + ttk.Radiobutton(gender_frame, text=option, variable=var, value=option).pack(side=tk.LEFT, padx=5) + else: + label_text = { + "name": "姓名:", + "stu_id": "学号:", + "height": "身高 (cm):", + "weight": "体重 (kg):", + "enrollment_date": "入学日期 (YYYY-MM-DD):", + "class_name": "班级:", + "major": "专业:", + "email": "电子邮箱:", + "phone": "联系电话:" + }.get(field, f"{field}:") + + ttk.Label(form_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5) + ttk.Entry(form_frame, textvariable=var).grid(row=row, column=1, sticky=tk.EW, pady=5) + + row += 1 + + # 设置列权重,使输入框可以拉伸 + form_frame.columnconfigure(1, weight=1) + + # 提交按钮 + def submit_form(): + # 获取表单数据 + name = fields["name"].get() + stu_id = fields["stu_id"].get() + + gender_text = fields["gender"].get() + gender = None + if gender_text == "男": + gender = True + elif gender_text == "女": + gender = False + + height = fields["height"].get() + height = int(height) if height else None + + weight = fields["weight"].get() + weight = float(weight) if weight else None + + enrollment_date = fields["enrollment_date"].get() + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None + + class_name = fields["class_name"].get() + major = fields["major"].get() + email = fields["email"].get() + phone = fields["phone"].get() + + # 更新学生对象 + student.name = name + student.stu_id = stu_id + student.gender = gender + student.height = height + student.weight = weight + student.enrollment_date = enrollment_date + student.class_name = class_name + student.major = major + student.email = email + student.phone = phone + + # 更新学生 + success, message = self.bll.update_student(student) + if success: + messagebox.showinfo("成功", "学生信息更新成功!") + self.show_student_list() + else: + messagebox.showerror("错误", f"更新失败: {message}") + + button_frame = ttk.Frame(self.content_frame) + button_frame.pack(fill=tk.X, pady=10) + + ttk.Button(button_frame, text="提交", command=submit_form).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5) + + def show_delete_student(self): + """显示删除学生界面""" + self.clear_content_frame() + + ttk.Label(self.content_frame, text="请输入要删除的学生身份证号:").pack(pady=10) + + id_card_var = tk.StringVar() + ttk.Entry(self.content_frame, textvariable=id_card_var).pack(fill=tk.X, padx=20, pady=5) + + def delete_student(): + id_card = id_card_var.get() + student = self.bll.get_student_by_id(id_card) + + if not student: + messagebox.showerror("错误", "未找到该学生!") + return + + confirm = messagebox.askyesno("确认", f"确定要删除学生 {student.name} 吗?") + if confirm: + if self.bll.delete_student_by_id(id_card): + messagebox.showinfo("成功", "学生删除成功!") + self.show_student_list() + else: + messagebox.showerror("错误", "删除失败!") + + button_frame = ttk.Frame(self.content_frame) + button_frame.pack(fill=tk.X, pady=10) + + ttk.Button(button_frame, text="删除", command=delete_student).pack(side=tk.LEFT, padx=5) + ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5) + + def show_search_student(self): + """显示查询学生界面""" + self.clear_content_frame() + + search_frame = ttk.LabelFrame(self.content_frame, text="查询学生") + search_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 查询方式 + search_type = tk.StringVar(value="name") + + ttk.Radiobutton(search_frame, text="按姓名", variable=search_type, value="name").grid(row=0, column=0, + sticky=tk.W, pady=5) + ttk.Radiobutton(search_frame, text="按班级", variable=search_type, value="class").grid(row=0, column=1, + sticky=tk.W, pady=5) + ttk.Radiobutton(search_frame, text="按专业", variable=search_type, value="major").grid(row=0, column=2, + sticky=tk.W, pady=5) + + # 查询关键词 + ttk.Label(search_frame, text="关键词:").grid(row=1, column=0, sticky=tk.W, pady=5) + keyword_var = tk.StringVar() + ttk.Entry(search_frame, textvariable=keyword_var).grid(row=1, column=1, sticky=tk.EW, pady=5) + + search_frame.columnconfigure(1, weight=1) + + def perform_search(): + keyword = keyword_var.get() + if not keyword: + messagebox.showwarning("警告", "请输入关键词!") + return + + search_method = search_type.get() + if search_method == "name": + students = self.bll.search_students_by_name(keyword) + elif search_method == "class": + students = self.bll.search_students_by_class(keyword) + else: # major + students = self.bll.search_students_by_major(keyword) + + if not students: + messagebox.showinfo("提示", "未找到匹配的学生!") + self.show_student_list() + else: + self.show_student_list(students) + + ttk.Button(search_frame, text="查询", command=perform_search).grid(row=1, column=2, padx=5, pady=5) + + ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10) + + def show_statistics(self): + """显示统计信息""" + self.clear_content_frame() + + stats_frame = ttk.LabelFrame(self.content_frame, text="统计信息") + stats_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10) + + # 学生总数 + total_students = self.bll.get_total_students() + ttk.Label(stats_frame, text=f"学生总数: {total_students}").grid(row=0, column=0, sticky=tk.W, pady=5) + + # 性别比例 + gender_ratio = self.bll.get_gender_ratio() + ttk.Label(stats_frame, text=f"男生人数: {gender_ratio['male']} ({gender_ratio['male_ratio']:.2%})").grid(row=1, + column=0, + sticky=tk.W, + pady=5) + ttk.Label(stats_frame, text=f"女生人数: {gender_ratio['female']} ({gender_ratio['female_ratio']:.2%})").grid( + row=2, column=0, sticky=tk.W, pady=5) + + # 各专业人数 + major_frame = ttk.LabelFrame(stats_frame, text="各专业人数") + major_frame.grid(row=3, column=0, sticky=tk.NSEW, pady=10) + + major_count = self.bll.get_students_by_major() + row = 0 + for major, count in major_count.items(): + ttk.Label(major_frame, text=f"{major}: {count} 人").grid(row=row, column=0, sticky=tk.W, pady=2) + row += 1 + + # 平均身高体重 + avg_height = self.bll.calculate_average_height() + avg_weight = self.bll.calculate_average_weight() + + ttk.Label(stats_frame, text=f"平均身高: {avg_height:.2f} cm").grid(row=0, column=1, sticky=tk.W, pady=5) + ttk.Label(stats_frame, text=f"平均体重: {avg_weight:.2f} kg").grid(row=1, column=1, sticky=tk.W, pady=5) + + # 设置列权重 + stats_frame.columnconfigure(0, weight=1) + stats_frame.columnconfigure(1, weight=1) + stats_frame.rowconfigure(3, weight=1) + + ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10) + + def show_import_export(self): + """显示数据导入导出界面""" + self.clear_content_frame() + + ttk.Label(self.content_frame, text="数据导入导出功能开发中...").pack(pady=20) + ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10) \ No newline at end of file diff --git a/student/util/__init__.py b/student/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/student/util/validator.py b/student/util/validator.py new file mode 100644 index 0000000..10df9b4 --- /dev/null +++ b/student/util/validator.py @@ -0,0 +1,85 @@ +from datetime import date +from typing import Union # 添加这行 +# student/util/validator.py +import re + + +class Validator: + @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 + + # 第18位可以是数字或X + check_char = id_card[17].upper() + if not (check_char.isdigit() or check_char == 'X'): + return False + + # 校验码验证 + factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + check_code_list = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2'] + + sum_val = sum(int(id_card[i]) * factors[i] for i in range(17)) + mod = sum_val % 11 + check_code = check_code_list[mod] + + return check_char == check_code + + @staticmethod + def validate_stu_id(stu_id: str) -> bool: + """验证学号是否有效,格式示例:20230001""" + pattern = r'^\d{8}$' # 假设学号为8位数字 + return bool(re.match(pattern, stu_id)) + + @staticmethod + def validate_name(name: str) -> bool: + """验证姓名是否有效,2-20个字符,只能包含中文""" + pattern = r'^[\u4e00-\u9fa5]{2,20}$' + return bool(re.match(pattern, name)) + + @staticmethod + def validate_height(height: int) -> bool: + """验证身高是否在合理范围""" + return 50 <= height <= 250 if height is not None else True + + @staticmethod + def validate_weight(weight: float) -> bool: + """验证体重是否在合理范围""" + return 5 <= weight <= 300 if weight is not None else True + + @staticmethod + def validate_enrollment_date(enrollment_date: Union[date, str], birthday: date) -> bool: + """验证入学日期是否晚于出生日期""" + if not enrollment_date: + return True + + if isinstance(enrollment_date, str): + try: + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() + except ValueError: + return False + + return enrollment_date >= birthday if birthday else True + + @staticmethod + def validate_email(email: str) -> bool: + """验证电子邮箱格式""" + if not email: + return True + + pattern = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$' + return bool(re.match(pattern, email)) + + @staticmethod + def validate_phone(phone: str) -> bool: + """验证手机号码格式""" + if not phone: + return True + + pattern = r'^1[3-9]\d{9}$' + return bool(re.match(pattern, phone)) \ No newline at end of file diff --git a/students.db b/students.db new file mode 100644 index 0000000..d5ae8f3 Binary files /dev/null and b/students.db differ