From d128c94bb12a52b77801540f3f6e2b99205e6769 Mon Sep 17 00:00:00 2001 From: lou74 Date: Tue, 24 Jun 2025 11:10:50 +0800 Subject: [PATCH] 123 --- .idea/.gitignore | 3 + .idea/PythonProject1.iml | 8 + .idea/inspectionProfiles/Project_Default.xml | 12 + .../inspectionProfiles/profiles_settings.xml | 6 + .idea/misc.xml | 6 + .idea/modules.xml | 8 + .idea/vcs.xml | 6 + desktop.ini | 6 + .../inspectionProfiles/Project_Default.xml | 12 + .../inspectionProfiles/profiles_settings.xml | 6 + student/.idea/misc.xml | 4 + student/.idea/modules.xml | 8 + student/.idea/student.iml | 12 + student/.idea/workspace.xml | 42 ++ student/__init__.py | 0 student/bll/__init__.py | 0 student/bll/student_bll.py | 188 +++++ student/dal/__init__.py | 0 student/dal/csv_student_dal.py | 181 +++++ student/dal/json_student_dal.py | 150 ++++ student/dal/sqlite_student_dal.py | 181 +++++ student/dal/student_dal.py | 57 ++ student/data/scratch_1.json | 0 student/main.py | 58 ++ student/model/__init__.py | 0 student/model/student.py | 89 +++ student/students.csv | 1 + student/students.db | Bin 0 -> 20480 bytes student/students.json | 1 + student/tests/__init__.py | 0 student/tests/test_bll.py | 0 student/tests/test_dal.py | 0 student/tests/test_model.py | 0 student/ui/__init__.py | 0 student/ui/console_ui.py | 350 +++++++++ student/ui/gui_ui.py | 685 ++++++++++++++++++ student/util/__init__.py | 0 student/util/validator.py | 85 +++ students.db | Bin 0 -> 20480 bytes 39 files changed, 2165 insertions(+) create mode 100644 .idea/.gitignore create mode 100644 .idea/PythonProject1.iml create mode 100644 .idea/inspectionProfiles/Project_Default.xml create mode 100644 .idea/inspectionProfiles/profiles_settings.xml create mode 100644 .idea/misc.xml create mode 100644 .idea/modules.xml create mode 100644 .idea/vcs.xml create mode 100644 desktop.ini create mode 100644 student/.idea/inspectionProfiles/Project_Default.xml create mode 100644 student/.idea/inspectionProfiles/profiles_settings.xml create mode 100644 student/.idea/misc.xml create mode 100644 student/.idea/modules.xml create mode 100644 student/.idea/student.iml create mode 100644 student/.idea/workspace.xml create mode 100644 student/__init__.py create mode 100644 student/bll/__init__.py create mode 100644 student/bll/student_bll.py create mode 100644 student/dal/__init__.py create mode 100644 student/dal/csv_student_dal.py create mode 100644 student/dal/json_student_dal.py create mode 100644 student/dal/sqlite_student_dal.py create mode 100644 student/dal/student_dal.py create mode 100644 student/data/scratch_1.json create mode 100644 student/main.py create mode 100644 student/model/__init__.py create mode 100644 student/model/student.py create mode 100644 student/students.csv create mode 100644 student/students.db create mode 100644 student/students.json create mode 100644 student/tests/__init__.py create mode 100644 student/tests/test_bll.py create mode 100644 student/tests/test_dal.py create mode 100644 student/tests/test_model.py create mode 100644 student/ui/__init__.py create mode 100644 student/ui/console_ui.py create mode 100644 student/ui/gui_ui.py create mode 100644 student/util/__init__.py create mode 100644 student/util/validator.py create mode 100644 students.db 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/desktop.ini b/desktop.ini new file mode 100644 index 0000000..3634585 --- /dev/null +++ b/desktop.ini @@ -0,0 +1,6 @@ +[.ShellClassInfo] +IconResource=C:\WINDOWS\System32\SHELL32.dll,230 +[ViewState] +Mode= +Vid= +FolderType=Generic diff --git a/student/.idea/inspectionProfiles/Project_Default.xml b/student/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..811e3a4 --- /dev/null +++ b/student/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/student/.idea/inspectionProfiles/profiles_settings.xml b/student/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/student/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/student/.idea/misc.xml b/student/.idea/misc.xml new file mode 100644 index 0000000..a971a2c --- /dev/null +++ b/student/.idea/misc.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/student/.idea/modules.xml b/student/.idea/modules.xml new file mode 100644 index 0000000..a328533 --- /dev/null +++ b/student/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/student/.idea/student.iml b/student/.idea/student.iml new file mode 100644 index 0000000..b5ad51a --- /dev/null +++ b/student/.idea/student.iml @@ -0,0 +1,12 @@ + + + + + + + + + + \ No newline at end of file diff --git a/student/.idea/workspace.xml b/student/.idea/workspace.xml new file mode 100644 index 0000000..f47db9b --- /dev/null +++ b/student/.idea/workspace.xml @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + + + + 1750342565374 + + + + \ 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..8a48d3b --- /dev/null +++ b/student/bll/student_bll.py @@ -0,0 +1,188 @@ +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): + self.dal = dal + + def add_student(self, student: Student) -> (bool, str): + """添加学生,包含数据验证""" + # 验证学生数据 + 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): + """更新学生信息,包含数据验证""" + # 验证学生数据 + 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: + """根据身份证号删除学生""" + return self.dal.delete_by_id(id_card) + + def delete_student_by_stu_id(self, stu_id: str) -> bool: + """根据学号删除学生""" + return self.dal.delete_by_stu_id(stu_id) + + def get_student_by_id(self, id_card: str) -> Optional[Student]: + """根据身份证号获取学生""" + return self.dal.get_by_id(id_card) + + def get_student_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号获取学生""" + return self.dal.get_by_stu_id(stu_id) + + def get_all_students(self) -> List[Student]: + """获取所有学生""" + return self.dal.get_all() + + def search_students_by_name(self, name: str) -> List[Student]: + """根据姓名搜索学生(模糊查询)""" + return self.dal.search_by_name(name) + + def search_students_by_class(self, class_name: str) -> List[Student]: + """根据班级搜索学生(模糊查询)""" + return self.dal.search_by_class(class_name) + + def search_students_by_major(self, major: str) -> List[Student]: + """根据专业搜索学生(模糊查询)""" + return self.dal.search_by_major(major) + + def get_total_students(self) -> int: + """获取学生总数""" + return len(self.get_all_students()) + + def get_students_by_major(self) -> dict: + """统计各专业学生人数""" + 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: + """计算平均身高 + group_by: 分组依据,可选 'class' 或 'major' + group_value: 分组值,如班级名称或专业名称 + """ + 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: + """计算平均体重 + group_by: 分组依据,可选 'class' 或 'major' + group_value: 分组值,如班级名称或专业名称 + """ + 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: + """统计性别比例""" + 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..60655ec --- /dev/null +++ b/student/dal/csv_student_dal.py @@ -0,0 +1,181 @@ +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对象""" + # 处理日期类型 + 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 + + 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 + + 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 + + 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 + + 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..4de5744 --- /dev/null +++ b/student/dal/json_student_dal.py @@ -0,0 +1,150 @@ +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文件存储实现""" + + def __init__(self, file_path: str = 'students.json'): + self.file_path = file_path + self._ensure_file_exists() + + def _ensure_file_exists(self): + """确保JSON文件存在""" + try: + with open(self.file_path, 'r', encoding='utf-8') as file: + json.load(file) + 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对象转换为字典""" + 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 None, + 'class_name': student.class_name, + 'major': student.major, + 'email': student.email, + 'phone': student.phone + } + + def _dict_to_student(self, data: dict) -> Student: + """将字典转换为Student对象""" + # 处理日期类型 + enrollment_date = data.get('enrollment_date') + if enrollment_date: + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() + + return Student( + name=data.get('name'), + id_card=data.get('id_card'), + stu_id=data.get('stu_id'), + gender=data.get('gender'), + height=data.get('height'), + weight=data.get('weight'), + enrollment_date=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文件数据""" + with open(self.file_path, 'r', encoding='utf-8') as file: + return json.load(file) + + def _save_data(self, data: List[dict]): + """保存数据到JSON文件""" + with open(self.file_path, 'w', encoding='utf-8') as file: + json.dump(data, file, ensure_ascii=False, indent=2) + + def get_by_id(self, id_card: str) -> Optional[Student]: + """根据身份证号获取学生信息""" + for data in self._load_data(): + if data.get('id_card') == id_card: + return self._dict_to_student(data) + return None + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号获取学生信息""" + 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 [self._dict_to_student(data) for data in self._load_data()] + + 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 + + data = self._load_data() + data.append(self._student_to_dict(student)) + self._save_data(data) + return True + + def update(self, student: Student) -> bool: + """更新学生信息""" + 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: + """根据身份证号删除学生信息""" + 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: + """根据学号删除学生信息""" + 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]: + """根据姓名模糊查询学生信息""" + return [self._dict_to_student(data) for data in self._load_data() if name in data.get('name', '')] + + def search_by_class(self, class_name: str) -> List[Student]: + """根据班级模糊查询学生信息""" + return [self._dict_to_student(data) for data in self._load_data() if + class_name in (data.get('class_name') or '')] + + def search_by_major(self, major: str) -> List[Student]: + """根据专业模糊查询学生信息""" + return [self._dict_to_student(data) for data in self._load_data() if major in (data.get('major') or '')] \ No newline at end of file diff --git a/student/dal/sqlite_student_dal.py b/student/dal/sqlite_student_dal.py new file mode 100644 index 0000000..ba8679c --- /dev/null +++ b/student/dal/sqlite_student_dal.py @@ -0,0 +1,181 @@ +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数据库存储实现""" + + def __init__(self, db_path: str = 'students.db'): + self.db_path = db_path + self._create_table() + + def _create_table(self): + """创建学生表(如果不存在)""" + 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, + height INTEGER, + weight REAL, + enrollment_date TEXT, + class_name TEXT, + major TEXT, + email TEXT, + phone TEXT + ) + ''') + conn.commit() + + def _row_to_student(self, row: tuple) -> Optional[Student]: + """将数据库行转换为Student对象""" + if not row: + return None + + # 处理日期类型 + enrollment_date = row[7] + if enrollment_date: + enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() + + # 处理布尔类型 + gender = row[3] + if gender is not None: + gender = bool(gender) + + 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]: + """根据身份证号获取学生信息""" + 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) + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + """根据学号获取学生信息""" + 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]: + """获取所有学生信息""" + 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: + """添加学生信息""" + 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: + """更新学生信息""" + 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: + """根据身份证号删除学生信息""" + 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: + """根据学号删除学生信息""" + 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]: + """根据姓名模糊查询学生信息""" + 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]: + """根据班级模糊查询学生信息""" + 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]: + """根据专业模糊查询学生信息""" + 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] \ No newline at end of file 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 0000000000000000000000000000000000000000..6c63432d5553563dc4381f9176530677a062b963 GIT binary patch literal 20480 zcmeI)&1=(O90%|xZ8z;ki6Bxu6dnvr(y)1{HEwa%MVz7KiG^Yr;WPoC$K=J1rz-JPlxm~_wU zI(k55a*1#pS*Mf`5@UIWJ0uX=z z1Rwwb2tWV=5P-n{A+Vj8O)o8R%|O3zn|{Y;`zy^iJ3Z5FjaFwi>PlHvs9L^VRp?0U zf~z~`GV8hc#N{reGa2q#E*vxH8KxWf{_q?-JIe4=x-dsal4a0JO;zqFb-G=zY?kXy zdRJ-Eazov!)Yu@KN=;oJ>ou4LRh4@xt!=T~s8+{gEJJJQT_X%@)G9j- 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 0000000000000000000000000000000000000000..d5ae8f337f0a6066883d7d9545330fff40b40391 GIT binary patch literal 20480 zcmeI&&u-H&9KdmB>js)sb+?`>`M7Bl74QO-$wC!fwsomQPmyW7H7I{rk_zq|_6B}EUKmY**5I_I{1Q2M3z-4Q{dvfAT z3;i)N`CVksSAK5p7ABc(7WdA^YTzm94c?BF+=x9&bZky-%j0i}^Q_zJIrsflpT#0D zNs;I4`_AKD*1yW51G$mHKn`6`y;Ea(IUb%5##8xTP32(XT?}2@$+>d9(^{)?8q!lA zJn3H8JQus===P)Sj20Tf4j*mKPEN2q1s}0tg_0 z00IagfB*srG*#fBTo~v7rfx6mLI42-5I_I{1Q0*~0R#|0U@5@)pBDiE1Q0*~0R#|0 l009ILKmdW}3-JH{=I=2pL;wK<5I_I{1Q0*~0R#|0;5VTUzSaN$ literal 0 HcmV?d00001