commit ef8573eb4215c9d1aa9c6574ebb0aca7c3f46431 Author: 19602 <1960219918@qq.com> Date: Wed Jun 25 10:06:26 2025 +0800 1 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/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..3e14072 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a82d671 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/.idea/python2.iml b/.idea/python2.iml new file mode 100644 index 0000000..5b13fa1 --- /dev/null +++ b/.idea/python2.iml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + \ No newline at end of file diff --git a/bll/StudentBLL.py b/bll/StudentBLL.py new file mode 100644 index 0000000..ec55c29 --- /dev/null +++ b/bll/StudentBLL.py @@ -0,0 +1,92 @@ +from datetime import date +from typing import List, Optional +from dal.StudentDAL import StudentDAL +from student.Student import Student + + +class StudentBLL: + def __init__(self, dal: StudentDAL): + self.dal = dal + + def add_student(self, student: Student): + if not student.is_valid: + errors = ", ".join(student.get_errors()) + raise ValueError(f"学生数据校验失败: {errors}") + + if not self.dal.add_student(student): + raise ValueError("添加失败,身份证号或学号可能已存在") + + def delete_student(self, stu_id: str): + if not self.dal.delete_student(stu_id): + raise ValueError(f"找不到学号为 {stu_id} 的学生") + + def update_student(self, stu_id: str, student: Student): + if not student.is_valid: + errors = ", ".join(student.get_errors()) + raise ValueError(f"学生数据校验失败: {errors}") + + if not self.dal.update_student(stu_id, student): + raise ValueError(f"找不到学号为 {stu_id} 的学生") + + def get_student_by_id_card(self, id_card: str) -> Optional[Student]: + return self.dal.get_by_id_card(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(self, search_type: str, keyword: str) -> List[Student]: + search_type = search_type.lower() + if search_type == 'name': + return self.dal.search_by_name(keyword) + elif search_type == 'class': + return self.dal.search_by_class(keyword) + elif search_type == 'major': + return self.dal.search_by_major(keyword) + else: + return [] + + def get_student_count(self) -> int: + return self.dal.get_student_count() + + def get_major_counts(self) -> dict: + return self.dal.get_major_counts() + + def calculate_avg_height(self) -> float: + students = self.dal.get_all() + heights = [s.height for s in students if s.height is not None] + if not heights: + return 0.0 + return sum(heights) / len(heights) + + def calculate_avg_weight(self) -> float: + students = self.dal.get_all() + weights = [s.weight for s in students if s.weight is not None] + if not weights: + return 0.0 + return sum(weights) / len(weights) + + def get_students_by_age(self, min_age: int, max_age: int) -> List[Student]: + today = date.today() + students = self.dal.get_all() + result = [] + + for student in students: + if student.age is None: + continue + + if min_age <= student.age <= max_age: + result.append(student) + + return result + + def export_data(self, file_path: str) -> bool: + return self.dal.export_data(file_path) + + def import_data(self, file_path: str) -> bool: + return self.dal.import_data(file_path) + + def clear_all(self) -> bool: + return self.dal.clear_all() \ No newline at end of file diff --git a/bll/__init__.py b/bll/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dal/StudentDAL.py b/dal/StudentDAL.py new file mode 100644 index 0000000..b8c8481 --- /dev/null +++ b/dal/StudentDAL.py @@ -0,0 +1,157 @@ +import os +import json +from datetime import date +from typing import List, Optional +from student.Student import Student + + +class StudentDAL: + def __init__(self, file_path: str): + self.file_path = file_path + self._ensure_file_exists() + + def _ensure_file_exists(self): + """确保文件存在""" + if not os.path.exists(self.file_path): + os.makedirs(os.path.dirname(self.file_path), exist_ok=True) + with open(self.file_path, 'w', encoding='utf-8') as f: + json.dump([], f) + + def _load_data(self) -> List[dict]: + """加载数据""" + try: + with open(self.file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except (FileNotFoundError, json.JSONDecodeError): + return [] + + def _save_data(self, data: List[dict]): + """保存数据""" + with open(self.file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + + def add_student(self, student: Student) -> bool: + data = self._load_data() + + # 检查身份证号是否唯一 + if any(s['id_card'] == student.id_card for s in data): + return False + + # 检查学号是否唯一 + if any(s['stu_id'] == student.stu_id for s in data): + return False + + data.append(student.to_dict()) + self._save_data(data) + return True + + def get_by_id_card(self, id_card: str) -> Optional[Student]: + data = self._load_data() + for student_dict in data: + if student_dict['id_card'] == id_card: + return Student.from_dict(student_dict) + return None + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + data = self._load_data() + for student_dict in data: + if student_dict['stu_id'] == stu_id: + return Student.from_dict(student_dict) + return None + + def get_all(self) -> List[Student]: + return [Student.from_dict(d) for d in self._load_data()] + + def update_student(self, stu_id: str, student: Student) -> bool: + data = self._load_data() + updated = False + + for i, student_dict in enumerate(data): + if student_dict['stu_id'] == stu_id: + # 保留原始身份证号 + new_data = student.to_dict() + new_data['id_card'] = student_dict['id_card'] + data[i] = new_data + updated = True + break + + if updated: + self._save_data(data) + return updated + + def delete_student(self, stu_id: str) -> bool: + data = self._load_data() + original_count = len(data) + data = [s for s in data if s['stu_id'] != stu_id] + + if len(data) < original_count: + self._save_data(data) + return True + return False + + def search_by_name(self, name: str) -> List[Student]: + data = self._load_data() + name_lower = name.lower() + return [ + Student.from_dict(d) + for d in data + if name_lower in d['name'].lower() + ] + + def search_by_class(self, class_name: str) -> List[Student]: + data = self._load_data() + class_lower = class_name.lower() + return [ + Student.from_dict(d) + for d in data + if d.get('class_name') and class_lower in d['class_name'].lower() + ] + + def search_by_major(self, major: str) -> List[Student]: + data = self._load_data() + major_lower = major.lower() + return [ + Student.from_dict(d) + for d in data + if d.get('major') and major_lower in d['major'].lower() + ] + + def get_student_count(self) -> int: + return len(self._load_data()) + + def get_major_counts(self) -> dict: + data = self._load_data() + counts = {} + for student_dict in data: + major = student_dict.get('major', '未指定') + counts[major] = counts.get(major, 0) + 1 + return counts + + def clear_all(self) -> bool: + self._save_data([]) + return True + + def export_data(self, file_path: str) -> bool: + data = self._load_data() + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + return True + except Exception: + return False + + def import_data(self, file_path: str) -> bool: + try: + with open(file_path, 'r', encoding='utf-8') as f: + new_data = json.load(f) + + current_data = self._load_data() + current_ids = {s['id_card'] for s in current_data} + + # 只导入不重复的学生 + to_import = [s for s in new_data if s['id_card'] not in current_ids] + + self._save_data(current_data + to_import) + return True + except Exception: + return False \ No newline at end of file diff --git a/dal/__init__.py b/dal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/data/students.json b/data/students.json new file mode 100644 index 0000000..e69de29 diff --git a/date/__init__.py b/date/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/date/student.json.py b/date/student.json.py new file mode 100644 index 0000000..e69de29 diff --git a/main.py b/main.py new file mode 100644 index 0000000..fcc3d52 --- /dev/null +++ b/main.py @@ -0,0 +1,21 @@ +from dal.StudentDAL import StudentDAL +from bll.StudentBLL import StudentBLL +from tui.StudentTUI import StudentTUI + + +def main(): + # 初始化数据访问层 + dal = StudentDAL("data/students.json") + + # 初始化业务逻辑层 + bll = StudentBLL(dal) + + # 初始化用户界面 + tui = StudentTUI(bll) + + # 启动系统 + tui.run() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/student/Student.py b/student/Student.py new file mode 100644 index 0000000..35aa224 --- /dev/null +++ b/student/Student.py @@ -0,0 +1,179 @@ +import re +from datetime import date +from typing import Optional, Union + + +class Student: + def __init__(self, + name: str, + id_card: str, + stu_id: str, + gender: Optional[bool] = None, + height: Optional[int] = None, + weight: Optional[float] = None, + enrollment_date: Optional[Union[date, str]] = None, + class_name: Optional[str] = None, + major: Optional[str] = None): + + # 基本信息 + self.name = name.strip() + self.id_card = id_card.strip() + self.stu_id = stu_id.strip() + self.gender = gender + self.height = height + self.weight = weight + self.class_name = class_name.strip() if class_name else None + self.major = major.strip() if major else None + + # 处理日期类型 - 将字符串转换为 date 对象 + self.enrollment_date = self._parse_date(enrollment_date) + + # 从身份证生成字段 + self.birthday = self._extract_birthday_from_id_card() + self.age = self._calculate_age() + + # 验证错误信息 + self._validation_errors = [] + self._validate() + + def _parse_date(self, date_value: Optional[Union[date, str]]) -> Optional[date]: + """将日期值解析为 date 对象""" + if date_value is None: + return None + if isinstance(date_value, date): + return date_value + try: + return date.fromisoformat(date_value) + except (ValueError, TypeError): + return None + + def _extract_birthday_from_id_card(self) -> Optional[date]: + """从身份证号提取出生日期""" + if len(self.id_card) != 18: + return None + + try: + birth_str = self.id_card[6:14] + return date(int(birth_str[0:4]), int(birth_str[4:6]), int(birth_str[6:8])) + except (ValueError, IndexError): + return None + + def _calculate_age(self) -> Optional[int]: + """计算年龄""" + if not self.birthday: + return None + + today = date.today() + age = today.year - self.birthday.year + # 如果生日还没过,年龄减1 + if (today.month, today.day) < (self.birthday.month, self.birthday.day): + age -= 1 + return age + + def _validate(self): + """执行所有验证""" + self._validate_id_card() + self._validate_stu_id() + self._validate_name() + self._validate_height() + self._validate_weight() + self._validate_dates() + + def _validate_id_card(self): + """验证身份证号:18位,校验位正确""" + if len(self.id_card) != 18: + self._validation_errors.append("身份证号必须是18位") + return + + # 校验位验证 + factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + check_codes = '10X98765432' + + try: + total = sum(int(self.id_card[i]) * factors[i] for i in range(17)) + if check_codes[total % 11] != self.id_card[17].upper(): + self._validation_errors.append("身份证号校验位不正确") + except ValueError: + self._validation_errors.append("身份证号包含无效字符") + + def _validate_stu_id(self): + """验证学号:格式规则(简单示例)""" + if not re.match(r'^[A-Za-z0-9]{8,12}$', self.stu_id): + self._validation_errors.append("学号格式不正确,应为8-12位字母数字组合") + + def _validate_name(self): + """验证姓名:2-20个字符,不能包含数字和特殊符号""" + if not (2 <= len(self.name) <= 20): + self._validation_errors.append("姓名长度需在2-20个字符之间") + + if not re.match(r'^[\u4e00-\u9fa5a-zA-Z·\s]+$', self.name): + self._validation_errors.append("姓名不能包含数字和特殊符号") + + def _validate_height(self): + """验证身高:50-250cm之间""" + if self.height is not None and not (50 <= self.height <= 250): + self._validation_errors.append(f"身高{self.height}cm超出合理范围(50-250厘米)") + + def _validate_weight(self): + """验证体重:5-300kg之间""" + if self.weight is not None and not (5 <= self.weight <= 300): + self._validation_errors.append(f"体重{self.weight}kg超出合理范围(5-300千克)") + + def _validate_dates(self): + """验证日期:入学日期不能早于出生日期""" + if self.birthday and self.enrollment_date: + if self.enrollment_date < self.birthday: + self._validation_errors.append("入学日期不能早于出生日期") + + if self.enrollment_date and self.enrollment_date > date.today(): + self._validation_errors.append("入学日期不能在未来") + + @property + def is_valid(self) -> bool: + return len(self._validation_errors) == 0 + + def get_errors(self) -> list: + return self._validation_errors.copy() + + def to_dict(self) -> dict: + """将对象序列化为字典""" + return { + 'name': self.name, + 'id_card': self.id_card, + 'stu_id': self.stu_id, + 'gender': self.gender, + 'height': self.height, + 'weight': self.weight, + 'enrollment_date': self.enrollment_date.isoformat() if self.enrollment_date else None, + 'class_name': self.class_name, + 'major': self.major, + 'birthday': self.birthday.isoformat() if self.birthday else None, + 'age': self.age + } + + @classmethod + def from_dict(cls, data: dict) -> 'Student': + """从字典创建对象""" + # 解析日期字符串为 date 对象 + enrollment_date = data.get('enrollment_date') + birthday = data.get('birthday') + + return cls( + name=data.get('name', ''), + id_card=data.get('id_card', ''), + stu_id=data.get('stu_id', ''), + gender=data.get('gender'), + height=data.get('height'), + weight=data.get('weight'), + enrollment_date=enrollment_date, + class_name=data.get('class_name'), + major=data.get('major') + ) + + def __repr__(self) -> str: + gender_str = "男" if self.gender is True else "女" if self.gender is False else "未知" + return ( + f"Student(name='{self.name}', id_card='{self.id_card}', stu_id='{self.stu_id}', " + f"gender={gender_str}, height={self.height}, weight={self.weight}, " + f"class_name='{self.class_name}', major='{self.major}')" + ) \ 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/tui/StudentTUI.py b/tui/StudentTUI.py new file mode 100644 index 0000000..788702f --- /dev/null +++ b/tui/StudentTUI.py @@ -0,0 +1,292 @@ +from datetime import date +from bll.StudentBLL import StudentBLL +from dal.StudentDAL import StudentDAL +from student.Student import Student + + +class StudentTUI: + def __init__(self, bll: StudentBLL): + self.bll = bll + + def display_menu(self): + print("\n===== 学生信息管理系统 =====") + print("1. 添加学生") + print("2. 删除学生") + print("3. 更新学生") + print("4. 查询学生") + print("5. 统计分析") + print("6. 导入导出") + print("7. 清空数据") + print("0. 退出系统") + print("==========================") + + def run(self): + while True: + self.display_menu() + choice = input("请选择操作: ").strip() + + if choice == "0": + print("再见!") + break + elif choice == "1": + self.add_student() + elif choice == "2": + self.delete_student() + elif choice == "3": + self.update_student() + elif choice == "4": + self.query_students() + elif choice == "5": + self.show_stats() + elif choice == "6": + self.import_export() + elif choice == "7": + self.clear_data() + else: + print("无效选择,请重试") + + def add_student(self): + print("\n--- 添加学生 ---") + name = input("姓名: ").strip() + id_card = input("身份证号: ").strip() + stu_id = input("学号: ").strip() + gender = self.input_gender() + height = self.input_int("身高(cm): ") + weight = self.input_float("体重(kg): ") + enrollment_date = self.input_date("入学日期(YYYY-MM-DD): ") + class_name = input("班级: ").strip() + major = input("专业: ").strip() + + try: + student = Student( + name=name, + id_card=id_card, + stu_id=stu_id, + gender=gender, + height=height, + weight=weight, + enrollment_date=enrollment_date, + class_name=class_name, + major=major + ) + self.bll.add_student(student) + print("添加成功!") + except ValueError as e: + print(f"添加失败: {e}") + + def input_gender(self): + """输入性别""" + while True: + gender = input("性别(1.男 2.女 3.跳过): ").strip() + if gender == '1': + return True + elif gender == '2': + return False + elif gender == '3': + return None + else: + print("无效选择,请重新输入") + + def input_int(self, prompt: str): + """输入整数""" + while True: + value = input(prompt).strip() + try: + return int(value) if value else None + except ValueError: + print("请输入有效的整数") + + def input_float(self, prompt: str): + """输入浮点数""" + while True: + value = input(prompt).strip() + try: + return float(value) if value else None + except ValueError: + print("请输入有效的数字") + + def input_date(self, prompt: str): + """输入日期""" + while True: + try: + date_str = input(prompt).strip() + if not date_str: + return None + return date.fromisoformat(date_str) + except ValueError: + print("日期格式应为YYYY-MM-DD") + + def delete_student(self): + print("\n--- 删除学生 ---") + stu_id = input("请输入学号: ").strip() + try: + self.bll.delete_student(stu_id) + print("删除成功!") + except ValueError as e: + print(f"删除失败: {e}") + + def update_student(self): + print("\n--- 更新学生 ---") + stu_id = input("请输入学号: ").strip() + student = self.bll.get_student_by_stu_id(stu_id) + + if not student: + print("学生不存在") + return + + print(f"当前信息: {student}") + name = input(f"新姓名({student.name}): ").strip() or student.name + id_card = input(f"新身份证号({student.id_card}): ").strip() or student.id_card + new_stu_id = input(f"新学号({student.stu_id}): ").strip() or student.stu_id + gender = self.input_gender_update(student.gender) + height = self.input_int(f"新身高({student.height}): ") or student.height + weight = self.input_float(f"新体重({student.weight}): ") or student.weight + enrollment_date = self.input_date_update(student.enrollment_date) + class_name = input(f"新班级({student.class_name}): ").strip() or student.class_name + major = input(f"新专业({student.major}): ").strip() or student.major + + try: + updated_student = Student( + name=name, + id_card=id_card, + stu_id=new_stu_id, + gender=gender, + height=height, + weight=weight, + enrollment_date=enrollment_date, + class_name=class_name, + major=major + ) + + # 如果学号改变了,需要先删除旧记录 + if new_stu_id != stu_id: + self.bll.delete_student(stu_id) + + self.bll.add_student(updated_student) + print("更新成功!") + except ValueError as e: + print(f"更新失败: {e}") + + def input_gender_update(self, current_gender): + current = "男" if current_gender is True else "女" if current_gender is False else "未指定" + print(f"当前性别: {current}") + return self.input_gender() + + def input_date_update(self, current_date): + """输入日期(更新时)""" + if current_date: + print(f"当前日期: {current_date.isoformat()}") + return self.input_date("新的日期(YYYY-MM-DD): ") or current_date + + def query_students(self): + print("\n--- 查询学生 ---") + print("1. 按身份证号查询") + print("2. 按学号查询") + print("3. 按姓名查询") + print("4. 按班级查询") + print("5. 按专业查询") + print("6. 所有学生") + choice = input("请选择查询方式: ").strip() + + students = [] + if choice == "1": + id_card = input("请输入身份证号: ").strip() + student = self.bll.get_student_by_id_card(id_card) + if student: + students = [student] + elif choice == "2": + stu_id = input("请输入学号: ").strip() + student = self.bll.get_student_by_stu_id(stu_id) + if student: + students = [student] + elif choice == "3": + name = input("请输入姓名: ").strip() + students = self.bll.search_students('name', name) + elif choice == "4": + class_name = input("请输入班级: ").strip() + students = self.bll.search_students('class', class_name) + elif choice == "5": + major = input("请输入专业: ").strip() + students = self.bll.search_students('major', major) + elif choice == "6": + students = self.bll.get_all_students() + else: + print("无效选择") + return + + self.print_students(students) + + def print_students(self, students): + if not students: + print("没有找到学生") + return + + print("\n身份证号\t学号\t姓名\t性别\t身高\t体重\t班级\t专业\t年龄") + print("=" * 100) + for s in students: + gender = "男" if s.gender is True else "女" if s.gender is False else "未知" + print(f"{s.id_card}\t{s.stu_id}\t{s.name}\t{gender}\t" + f"{s.height or '--'}\t{s.weight or '--'}\t" + f"{s.class_name or '--'}\t{s.major or '--'}\t" + f"{s.age or '--'}") + print(f"共找到 {len(students)} 名学生") + + def show_stats(self): + print("\n--- 统计分析 ---") + print("1. 学生总数") + print("2. 专业分布") + print("3. 平均身高") + print("4. 平均体重") + print("5. 按年龄范围查询") + choice = input("请选择统计项目: ").strip() + + if choice == "1": + count = self.bll.get_student_count() + print(f"学生总数: {count}") + elif choice == "2": + major_counts = self.bll.get_major_counts() + print("\n专业分布:") + for major, count in major_counts.items(): + print(f" {major}: {count}人") + elif choice == "3": + avg_height = self.bll.calculate_avg_height() + print(f"平均身高: {avg_height:.1f} cm") + elif choice == "4": + avg_weight = self.bll.calculate_avg_weight() + print(f"平均体重: {avg_weight:.1f} kg") + elif choice == "5": + min_age = int(input("最小年龄: ")) + max_age = int(input("最大年龄: ")) + students = self.bll.get_students_by_age(min_age, max_age) + self.print_students(students) + else: + print("无效选择") + + def import_export(self): + print("\n--- 导入导出 ---") + print("1. 导出数据") + print("2. 导入数据") + choice = input("请选择操作: ").strip() + + if choice == "1": + file_path = input("导出文件路径: ").strip() + if self.bll.export_data(file_path): + print("导出成功!") + else: + print("导出失败") + elif choice == "2": + file_path = input("导入文件路径: ").strip() + if self.bll.import_data(file_path): + print("导入成功!") + else: + print("导入失败") + else: + print("无效选择") + + def clear_data(self): + confirm = input("确定要清空所有数据吗?(y/n): ").strip().lower() + if confirm == 'y': + if self.bll.clear_all(): + print("数据已清空") + else: + print("清空失败") \ No newline at end of file diff --git a/tui/__init__.py b/tui/__init__.py new file mode 100644 index 0000000..e69de29