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