commit 4c1470a02844b90870e640a6167d608e0a32eec8 Author: 孟天翔 <3394736377@qq.com> Date: Tue Jun 24 11:52:03 2025 +0800 新建项目 diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..35410ca --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# 默认忽略的文件 +/shelf/ +/workspace.xml +# 基于编辑器的 HTTP 客户端请求 +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/PythonProject1.iml b/.idea/PythonProject1.iml new file mode 100644 index 0000000..59132df --- /dev/null +++ b/.idea/PythonProject1.iml @@ -0,0 +1,8 @@ + + + + + + + + \ 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/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/stumis/__init__.py b/stumis/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stumis/bill/__init__.py b/stumis/bill/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stumis/bill/studentBLL.py b/stumis/bill/studentBLL.py new file mode 100644 index 0000000..ea2b5a2 --- /dev/null +++ b/stumis/bill/studentBLL.py @@ -0,0 +1,373 @@ +from abc import ABC, abstractmethod +import json +import csv +import os +from typing import List, Dict, Optional, Any +from copy import deepcopy + + + +class Student: + def __init__(self, data: Dict[str, Any]): + self.__dict__.update(data) + + def to_dict(self) -> Dict[str, Any]: + return self.__dict__.copy() + + def validate(self) -> List[str]: + errors = [] + if not hasattr(self, 'id_number') or not self.id_number: + errors.append("身份证号不能为空") + if not hasattr(self, 'stu_id') or not self.stu_id: + errors.append("学号不能为空") + if hasattr(self, 'age') and (not isinstance(self.age, int) or self.age < 0): + errors.append("年龄必须为非负整数") + return errors + + +class IStudentDAL(ABC): + + @abstractmethod + def get_by_id(self, id_number: str) -> Optional[Dict[str, Any]]: + pass + + @abstractmethod + def get_by_stu_id(self, stu_id: str) -> Optional[Dict[str, Any]]: + pass + + @abstractmethod + def get_all(self) -> List[Dict[str, Any]]: + pass + + @abstractmethod + def add(self, student: Dict[str, Any]) -> bool: + pass + + def delete(self, id_number: str) -> bool: + pass + + def delete_by_stu_id(self, stu_id: str) -> bool: + pass + + def update(self, student: Dict[str, Any]) -> bool: + pass + + +class JsonStudentDAL(IStudentDAL): + + def __init__(self, file_path: str): + self.file_path = file_path + self.data = self._load() + + def _load(self) -> List[Dict[str, Any]]: + if not os.path.exists(self.file_path): + return [] + try: + with open(self.file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except: + return [] + + def _save(self) -> None: + with open(self.file_path, 'w', encoding='utf-8') as f: + json.dump(self.data, f, ensure_ascii=False, indent=4) + + def get_by_id(self, id_number: str) -> Optional[Dict[str, Any]]: + for s in self.data: + if s.get('id_number') == id_number: + return deepcopy(s) + return None + + def get_by_stu_id(self, stu_id: str) -> Optional[Dict[str, Any]]: + for s in self.data: + if s.get('stu_id') == stu_id: + return deepcopy(s) + return None + + def get_all(self) -> List[Dict[str, Any]]: + return [deepcopy(s) for s in self.data] + + def add(self, student: Dict[str, Any]) -> bool: + self.data.append(deepcopy(student)) + self._save() + return True + + def delete(self, id_number: str) -> bool: + for i, s in enumerate(self.data): + if s.get('id_number') == id_number: + del self.data[i] + self._save() + return True + return False + + def delete_by_stu_id(self, stu_id: str) -> bool: + for i, s in enumerate(self.data): + if s.get('stu_id') == stu_id: + del self.data[i] + self._save() + return True + return False + + def update(self, student: Dict[str, Any]) -> bool: + for i, s in enumerate(self.data): + if s.get('id_number') == student.get('id_number'): + self.data[i] = deepcopy(student) + self._save() + return True + return False + + +class CsvStudentDAL(IStudentDAL): + FIELD_TYPES = { + "id_number": str, + "stu_id": str, + "name": str, + "age": int, + "gender": str + } + + def __init__(self, file_path: str): + self.file_path = file_path + self.data = self._load() + + def _load(self) -> List[Dict[str, Any]]: + if not os.path.exists(self.file_path): + self._ensure_file_exists() + return [] + data = [] + try: + with open(self.file_path, 'r', newline='', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + for row in reader: + data.append(self._convert_row(row)) + except: + pass + return data + + def _ensure_file_exists(self) -> None: + if not os.path.exists(self.file_path): + with open(self.file_path, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.DictWriter(f, fieldnames=self.FIELD_TYPES.keys()) + writer.writeheader() + + def _convert_row(self, row: Dict[str, str]) -> Dict[str, Any]: + converted = {} + for k, v in row.items(): + if k in self.FIELD_TYPES: + try: + converted[k] = self.FIELD_TYPES[k](v) + except: + converted[k] = v + else: + converted[k] = v + return converted + + def _save(self) -> None: + with open(self.file_path, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.DictWriter(f, fieldnames=self.FIELD_TYPES.keys()) + writer.writeheader() + for s in self.data: + writer.writerow(s) + + def get_by_id(self, id_number: str) -> Optional[Dict[str, Any]]: + for s in self.data: + if s.get('id_number') == id_number: + return deepcopy(s) + return None + + def get_by_stu_id(self, stu_id: str) -> Optional[Dict[str, Any]]: + for s in self.data: + if s.get('stu_id') == stu_id: + return deepcopy(s) + return None + + def get_all(self) -> List[Dict[str, Any]]: + return [deepcopy(s) for s in self.data] + + def add(self, student: Dict[str, Any]) -> bool: + self.data.append(deepcopy(student)) + self._save() + return True + + def delete(self, id_number: str) -> bool: + for i, s in enumerate(self.data): + if s.get('id_number') == id_number: + del self.data[i] + self._save() + return True + return False + + def delete_by_stu_id(self, stu_id: str) -> bool: + for i, s in enumerate(self.data): + if s.get('stu_id') == stu_id: + del self.data[i] + self._save() + return True + return False + + def update(self, student: Dict[str, Any]) -> bool: + for i, s in enumerate(self.data): + if s.get('id_number') == student.get('id_number'): + self.data[i] = deepcopy(student) + self._save() + return True + return False + + +class StudentBLL: + def __init__(self, file_path: str): + self.dal = self._get_dal(file_path) + + def _get_dal(self, file_path: str) -> IStudentDAL: + ext = os.path.splitext(file_path)[1].lower() + if ext == '.json': + return JsonStudentDAL(file_path) + elif ext == '.csv': + return CsvStudentDAL(file_path) + else: + raise ValueError(f"不支持的文件类型: {ext},请使用.json或.csv") + + def _validate_student(self, student: Student) -> None: + errors = student.validate() + if errors: + raise ValueError(f"学生信息校验失败: {', '.join(errors)}") + + def _check_uniqueness(self, student: Student) -> None: + all_students = self.dal.get_all() + if any(s.get('id_number') == student.id_number for s in all_students): + raise ValueError(f"身份证号 {student.id_number} 已存在") + if any(s.get('stu_id') == student.stu_id for s in all_students): + raise ValueError(f"学号 {student.stu_id} 已存在") + + def add(self, student: Student) -> bool: + self._validate_student(student) + self._check_uniqueness(student) + return self.dal.add(student.to_dict()) + + def delete(self, id_number: str) -> bool: + if not self.dal.get_by_id(id_number): + raise ValueError(f"学生(身份证号: {id_number})不存在") + return self.dal.delete(id_number) + + def delete_by_stu_id(self, stu_id: str) -> bool: + if not self.dal.get_by_stu_id(stu_id): + raise ValueError(f"学生(学号: {stu_id})不存在") + return self.dal.delete_by_stu_id(stu_id) + + def update(self, student: Student) -> bool: + self._validate_student(student) + if not self.dal.get_by_id(student.id_number): + raise ValueError(f"学生(身份证号: {student.id_number})不存在") + all_students = self.dal.get_all() + if any(s.get('stu_id') == student.stu_id and s.get('id_number') != student.id_number for s in all_students): + raise ValueError(f"学号 {student.stu_id} 已被其他学生使用") + return self.dal.update(student.to_dict()) + + def update_partial(self, id_number: str, fields: Dict[str, Any]) -> bool: + student = self.dal.get_by_id(id_number) + if not student: + raise ValueError(f"学生(身份证号: {id_number})不存在") + updated = deepcopy(student) + for k, v in fields.items(): + if k == 'id_number': + raise ValueError("身份证号不允许直接修改") + updated[k] = v + if 'stu_id' in fields: + all_students = self.dal.get_all() + if any(s.get('stu_id') == fields['stu_id'] and s.get('id_number') != id_number for s in all_students): + raise ValueError(f"学号 {fields['stu_id']} 已被其他学生使用") + partial_student = Student(updated) + self._validate_student(partial_student) + return self.dal.update(partial_student.to_dict()) + + def get_by_id(self, id_number: str) -> Optional[Student]: + data = self.dal.get_by_id(id_number) + return Student(data) if data else None + + def get_by_stu_id(self, stu_id: str) -> Optional[Student]: + data = self.dal.get_by_stu_id(stu_id) + return Student(data) if data else None + + def get_all(self) -> List[Student]: + return [Student(deepcopy(s)) for s in self.dal.get_all()] + + def get_by_key(self, field: str, value: str, fuzzy: bool = False) -> List[Student]: + all_students = self.dal.get_all() + result = [] + for s in all_students: + if field not in s: + continue + s_value = str(s[field]) + if fuzzy: + if value in s_value: + result.append(Student(deepcopy(s))) + else: + if s_value == value: + result.append(Student(deepcopy(s))) + return result + + def get_by_range(self, field: str, min_val: Any, max_val: Any) -> List[Student]: + all_students = self.dal.get_all() + result = [] + for s in all_students: + if field not in s: + continue + try: + s_value = s[field] + if min_val <= s_value <= max_val: + result.append(Student(deepcopy(s))) + except (TypeError, ValueError): + continue + return result + + def export_to_json(self, file_path: str) -> bool: + all_data = [s.to_dict() for s in self.get_all()] + try: + dal = JsonStudentDAL(file_path) + return dal.export_to_json(all_data, file_path) + except: + return False + + def import_from_json(self, file_path: str) -> List[str]: + errors = [] + try: + dal = JsonStudentDAL(file_path) + data_list = dal.import_from_json(file_path) + for data in data_list: + try: + student = Student(data) + self._validate_student(student) + self._check_uniqueness(student) + self.dal.add(student.to_dict()) + except Exception as e: + errors.append(f"导入失败:{data.get('id_number', '无ID')} - {str(e)}") + return errors + except Exception as e: + errors.append(f"导入文件错误:{str(e)}") + return errors + + def export_to_csv(self, file_path: str) -> bool: + all_data = [s.to_dict() for s in self.get_all()] + try: + dal = CsvStudentDAL(file_path) + return dal.export_to_csv(all_data, file_path) + except: + return False + + def import_from_csv(self, file_path: str) -> List[str]: + errors = [] + try: + dal = CsvStudentDAL(file_path) + data_list = dal.import_from_csv(file_path) + for data in data_list: + try: + student = Student(data) + self._validate_student(student) + self._check_uniqueness(student) + self.dal.add(student.to_dict()) + except Exception as e: + errors.append(f"导入失败:{data.get('id_number', '无ID')} - {str(e)}") + return errors + except Exception as e: + errors.append(f"导入文件错误:{str(e)}") + return errors \ No newline at end of file diff --git a/stumis/dal/__init__.py b/stumis/dal/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stumis/dal/studentDAL.py b/stumis/dal/studentDAL.py new file mode 100644 index 0000000..f34f026 --- /dev/null +++ b/stumis/dal/studentDAL.py @@ -0,0 +1,241 @@ +from abc import ABC, abstractmethod +import json +import csv + + +class IStudentDAL(ABC): + @abstractmethod + def get_by_id(self, id_number): + pass + @abstractmethod + def get_by_stu_id(self, stu_id): + pass + @abstractmethod + def get_all(self): + pass + @abstractmethod + def add(self, student): + pass + @abstractmethod + def delete(self, id_number): + pass + @abstractmethod + def delete_by_stu_id(self, stu_id): + pass + @abstractmethod + def update(self, student): + pass + @abstractmethod + def is_exist(self, id_number): + pass + @abstractmethod + def is_exist_stu_id(self, stu_id): + pass + @abstractmethod + def import_from_json(self, file_path): + pass + @abstractmethod + def export_to_json(self, data, file_path): + pass + @abstractmethod + def import_from_csv(self, file_path): + pass + @abstractmethod + def export_to_csv(self, data, file_path): + pass + + +class JsonStudentDAL(IStudentDAL): + def __init__(self, file_path): + self.file_path = file_path + self._ensure_file_exists() + self.data = self._load() + def _ensure_file_exists(self): + try: + with open(self.file_path, 'x'): + pass + except FileExistsError: + pass + def _load(self): + try: + with open(self.file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return [] + def _save(self): + with open(self.file_path, 'w', encoding='utf-8') as f: + json.dump(self.data, f, ensure_ascii=False, indent=4) + def get_by_id(self, id_number): + for student in self.data: + if student['id_number'] == id_number: + return student + return None + def get_by_stu_id(self, stu_id): + for student in self.data: + if student['stu_id'] == stu_id: + return student + return None + def get_all(self): + return self.data + def add(self, student): + if self.is_exist(student['id_number']): + return False + self.data.append(student) + self._save() + return True + def delete(self, id_number): + for index, student in enumerate(self.data): + if student['id_number'] == id_number: + del self.data[index] + self._save() + return True + return False + def delete_by_stu_id(self, stu_id): + for index, student in enumerate(self.data): + if student['stu_id'] == stu_id: + del self.data[index] + self._save() + return True + return False + def update(self, student): + for index, existing_student in enumerate(self.data): + if existing_student['id_number'] == student['id_number']: + self.data[index] = student + self._save() + return True + return False + def is_exist(self, id_number): + for student in self.data: + if student['id_number'] == id_number: + return True + return False + def is_exist_stu_id(self, stu_id): + for student in self.data: + if student['stu_id'] == stu_id: + return True + return False + def import_from_json(self, file_path): + try: + with open(file_path, 'r', encoding='utf-8') as f: + return json.load(f) + except (json.JSONDecodeError, FileNotFoundError): + return [] + def export_to_json(self, data, file_path): + try: + with open(file_path, 'w', encoding='utf-8') as f: + json.dump(data, f, ensure_ascii=False, indent=4) + return True + except IOError: + return False +class CsvStudentDAL(IStudentDAL): + FIELD_TYPES = { + "id_number": str, + "stu_id": str, + "name": str, + "age": int, + "gender": str + } + def __init__(self, file_path): + self.file_path = file_path + self._ensure_file_exists() + self.data = self._load() + def _ensure_file_exists(self): + try: + with open(self.file_path, 'x', newline='', encoding='utf-8-sig') as f: + writer = csv.DictWriter(f, fieldnames=self.FIELD_TYPES.keys()) + writer.writeheader() + except FileExistsError: + pass + def _load(self): + data = [] + try: + with open(self.file_path, 'r', newline='', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + for row in reader: + data.append(self._convert_row(row)) + except FileNotFoundError: + pass + return data + def _save(self): + with open(self.file_path, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.DictWriter(f, fieldnames=self.FIELD_TYPES.keys()) + writer.writeheader() + for student in self.data: + writer.writerow(student) + def _convert_row(self, row): + converted_row = {} + for key, value in row.items(): + try: + converted_row[key] = self.FIELD_TYPES[key](value) + except ValueError: + converted_row[key] = value + return converted_row + def get_by_id(self, id_number): + for student in self.data: + if student['id_number'] == id_number: + return student + return None + def get_by_stu_id(self, stu_id): + for student in self.data: + if student['stu_id'] == stu_id: + return student + return None + def get_all(self): + return self.data + def add(self, student): + if self.is_exist(student['id_number']): + return False + self.data.append(student) + self._save() + return True + def delete(self, id_number): + for index, student in enumerate(self.data): + if student['id_number'] == id_number: + del self.data[index] + self._save() + return True + return False + def delete_by_stu_id(self, stu_id): + for index, student in enumerate(self.data): + if student['stu_id'] == stu_id: + del self.data[index] + self._save() + return True + return False + def update(self, student): + for index, existing_student in enumerate(self.data): + if existing_student['id_number'] == student['id_number']: + self.data[index] = student + self._save() + return True + return False + def is_exist(self, id_number): + for student in self.data: + if student['id_number'] == id_number: + return True + return False + def is_exist_stu_id(self, stu_id): + for student in self.data: + if student['stu_id'] == stu_id: + return True + return False + def import_from_csv(self, file_path): + data = [] + try: + with open(file_path, 'r', newline='', encoding='utf-8-sig') as f: + reader = csv.DictReader(f) + for row in reader: + data.append(self._convert_row(row)) + except FileNotFoundError: + pass + return data + def export_to_csv(self, data, file_path): + try: + with open(file_path, 'w', newline='', encoding='utf-8-sig') as f: + writer = csv.DictWriter(f, fieldnames=self.FIELD_TYPES.keys()) + writer.writeheader() + for student in data: + writer.writerow(student) + return True + except IOError: + return False diff --git a/stumis/data/student.csv b/stumis/data/student.csv new file mode 100644 index 0000000..e69de29 diff --git a/stumis/main.py b/stumis/main.py new file mode 100644 index 0000000..d9f4fc6 --- /dev/null +++ b/stumis/main.py @@ -0,0 +1,438 @@ +from datetime import date +import os +from typing import List, Dict, Optional, Any, Union + + +class StudentUI: + """学生信息管理系统表示层""" + + def __init__(self, bll): + """初始化UI层,接收业务逻辑层实例""" + self.bll = bll + + def display_menu(self) -> None: + """显示主菜单""" + print("\n" + "=" * 40) + print("学生信息管理系统 - 主菜单") + print("=" * 40) + print("1. 添加学生信息") + print("2. 删除学生信息") + print("3. 更新学生信息") + print("4. 查询学生信息") + print("5. 统计分析") + print("6. 数据导入导出") + print("7. 清空所有学生信息") + print("8. 退出系统") + print("=" * 40) + + def display_query_menu(self) -> None: + """显示查询子菜单""" + print("\n" + "=" * 40) + print("学生信息查询菜单") + print("=" * 40) + print("1. 查询所有学生") + print("2. 按身份证号查询") + print("3. 按学号查询") + print("4. 按姓名查询") + print("5. 按班级查询") + print("6. 返回上一级菜单") + print("=" * 40) + + def display_stats_menu(self) -> None: + """显示统计分析子菜单""" + print("\n" + "=" * 40) + print("学生信息统计分析菜单") + print("=" * 40) + print("1. 学生总数") + print("2. 平均身高") + print("3. 按身高范围统计") + print("4. 按入学年份统计") + print("5. 按年龄范围统计") + print("6. 返回上一级菜单") + print("=" * 40) + + def display_import_export_menu(self) -> None: + print("\n" + "=" * 40) + print("数据导入导出菜单") + print("=" * 40) + print("1. 导出数据到JSON") + print("2. 从JSON导入数据") + print("3. 导出数据到CSV") + print("4. 从CSV导入数据") + print("5. 返回上一级菜单") + print("=" * 40) + + def get_input(self, prompt: str, required: bool = True) -> Optional[str]: + while True: + value = input(prompt).strip() + if not value and required: + print("输入不能为空,请重新输入") + continue + return value if value else None + + def get_numeric_input(self, prompt: str, min_val: Optional[float] = None, max_val: Optional[float] = None) -> \ + Optional[float]: + while True: + value = self.get_input(prompt) + if value is None: + return None + try: + num = float(value) + if min_val is not None and num < min_val: + print(f"输入值必须大于等于{min_val}") + continue + if max_val is not None and num > max_val: + print(f"输入值必须小于等于{max_val}") + continue + return num + except ValueError: + print("请输入有效的数字") + + def get_date_input(self, prompt: str) -> Optional[date]: + while True: + value = self.get_input(prompt) + if value is None: + return None + try: + return date.fromisoformat(value) + except ValueError: + print("请输入有效的日期(YYYY-MM-DD格式)") + + def add_student(self) -> None: + print("\n" + "=" * 40) + print("添加学生信息") + print("=" * 40) + + sid = self.get_input("请输入学号: ") + name = self.get_input("请输入姓名: ") + height = self.get_numeric_input("请输入身高(cm): ", min_val=50, max_val=250) + birth_date = self.get_date_input("请输入出生日期(YYYY-MM-DD): ") + enrollment_date = self.get_date_input("请输入入学日期(YYYY-MM-DD): ") + class_name = self.get_input("请输入班级: ") + + student_data = { + 'sid': sid, + 'name': name, + 'height': int(height), + 'birth_date': birth_date, + 'enrollment_date': enrollment_date, + 'class_name': class_name + } + + try: + student = Student.from_dict(student_data) + success = self.bll.add(student) + if success: + print("✅ 学生信息添加成功") + else: + print("❌ 学生信息添加失败") + except Exception as e: + print(f"❌ 添加失败: {str(e)}") + + def delete_student(self) -> None: + print("\n" + "=" * 40) + print("删除学生信息") + print("=" * 40) + + id_number = self.get_input("请输入要删除的学生身份证号: ") + + try: + success = self.bll.delete(id_number) + if success: + print("✅ 学生信息删除成功") + else: + print("❌ 学生信息删除失败") + except Exception as e: + print(f"❌ 删除失败: {str(e)}") + + def update_student(self) -> None: + print("\n" + "=" * 40) + print("更新学生信息") + print("=" * 40) + + id_number = self.get_input("请输入要更新的学生身份证号: ") + + try: + student = self.bll.get_by_id(id_number) + if not student: + print(f"❌ 未找到身份证号为{id_number}的学生") + return + + print("\n当前学生信息:") + print(f"学号: {student.sid}") + print(f"姓名: {student.name}") + print(f"身高: {student.height}cm") + print(f"出生日期: {student.birth_date}") + print(f"入学日期: {student.enrollment_date}") + print(f"班级: {student.class_name}") + + print("\n请输入新信息(直接回车保持原值):") + sid = self.get_input(f"学号 [{student.sid}]: ", required=False) or student.sid + name = self.get_input(f"姓名 [{student.name}]: ", required=False) or student.name + height = self.get_numeric_input(f"身高 [{student.height}]: ", min_val=50, max_val=250) + height = int(height) if height is not None else student.height + birth_date = self.get_date_input(f"出生日期 [{student.birth_date}]: ") or student.birth_date + enrollment_date = self.get_date_input(f"入学日期 [{student.enrollment_date}]: ") or student.enrollment_date + class_name = self.get_input(f"班级 [{student.class_name}]: ", required=False) or student.class_name + + updated_data = { + 'id_number': id_number, + 'sid': sid, + 'name': name, + 'height': height, + 'birth_date': birth_date, + 'enrollment_date': enrollment_date, + 'class_name': class_name + } + + updated_student = Student.from_dict(updated_data) + success = self.bll.update(updated_student) + + if success: + print("✅ 学生信息更新成功") + else: + print("❌ 学生信息更新失败") + + except Exception as e: + print(f"❌ 更新失败: {str(e)}") + + def query_student(self) -> None: + while True: + self.display_query_menu() + choice = self.get_input("请选择操作(1-6): ") + + try: + if choice == "1": + students = self.bll.get_all() + self._display_students(students) + elif choice == "2": + id_number = self.get_input("请输入身份证号: ") + student = self.bll.get_by_id(id_number) + if student: + self._display_students([student]) + else: + print(f"❌ 未找到身份证号为{id_number}的学生") + elif choice == "3": + stu_id = self.get_input("请输入学号: ") + student = self.bll.get_by_stu_id(stu_id) + if student: + self._display_students([student]) + else: + print(f"❌ 未找到学号为{stu_id}的学生") + elif choice == "4": + name = self.get_input("请输入姓名: ") + students = self.bll.get_by_key("name", name, fuzzy=True) + self._display_students(students) + elif choice == "5": + class_name = self.get_input("请输入班级: ") + students = self.bll.get_by_key("class_name", class_name, fuzzy=True) + self._display_students(students) + elif choice == "6": + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 查询失败: {str(e)}") + + def _display_students(self, students: List[Student]) -> None: + if not students: + print("未找到符合条件的学生") + return + + print("\n" + "-" * 80) + print(f"{'学号':<12}{'姓名':<12}{'身高(cm)':<10}{'出生日期':<12}{'入学日期':<12}{'班级':<12}") + print("-" * 80) + for student in students: + print( + f"{student.sid:<12}{student.name:<12}{student.height:<10}{student.birth_date:<12}{student.enrollment_date:<12}{student.class_name:<12}") + print("-" * 80) + print(f"共找到 {len(students)} 条记录") + + def show_stats(self) -> None: + while True: + self.display_stats_menu() + choice = self.get_input("请选择操作(1-6): ") + + try: + if choice == "1": + students = self.bll.get_all() + print(f"学生总数: {len(students)}") + elif choice == "2": + students = self.bll.get_all() + if not students: + print("暂无学生数据") + continue + avg_height = sum(s.height for s in students) / len(students) + print(f"平均身高: {avg_height:.2f}cm") + elif choice == "3": + min_height = self.get_numeric_input("请输入最小身高(cm): ", min_val=50) + max_height = self.get_numeric_input("请输入最大身高(cm): ", min_val=min_height) + + students = self.bll.get_by_range("height", min_height, max_height) + print(f"身高在 {min_height}-{max_height}cm 之间的学生有 {len(students)} 人") + elif choice == "4": + students = self.bll.get_all() + if not students: + print("暂无学生数据") + continue + + year_count = {} + for s in students: + year = s.enrollment_date.year + year_count[year] = year_count.get(year, 0) + 1 + + print("\n按入学年份统计:") + for year, count in sorted(year_count.items()): + print(f"{year}年: {count}人") + elif choice == "5": + today = date.today() + min_age = int(self.get_numeric_input("请输入最小年龄: ", min_val=0)) + max_age = int(self.get_numeric_input("请输入最大年龄: ", min_val=min_age)) + + students = self.bll.get_all() + count = 0 + for s in students: + age = today.year - s.birth_date.year + if s.birth_date.month > today.month or ( + s.birth_date.month == today.month and s.birth_date.day > today.day): + age -= 1 + if min_age <= age <= max_age: + count += 1 + + print(f"年龄在 {min_age}-{max_age} 岁之间的学生有 {count} 人") + elif choice == "6": + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 统计失败: {str(e)}") + + def import_export_data(self) -> None: + while True: + self.display_import_export_menu() + choice = self.get_input("请选择操作(1-5): ") + + try: + if choice == "1": + file_path = self.get_input("请输入导出文件名: ", required=False) or "students.json" + if not file_path.endswith('.json'): + file_path += '.json' + + success = self.bll.export_to_json(file_path) + if success: + print(f"✅ 数据已成功导出到 {file_path}") + else: + print(f"❌ 数据导出失败") + elif choice == "2": + file_path = self.get_input("请输入导入文件名: ", required=False) or "students.json" + if not file_path.endswith('.json'): + file_path += '.json' + + if not os.path.exists(file_path): + print(f"❌ 文件 {file_path} 不存在") + continue + + errors = self.bll.import_from_json(file_path) + if not errors: + print(f"✅ 数据已成功导入") + else: + print(f"⚠️ 数据导入完成,但有 {len(errors)} 条记录失败:") + for error in errors: + print(f" - {error}") + elif choice == "3": + file_path = self.get_input("请输入导出文件名: ", required=False) or "students.csv" + if not file_path.endswith('.csv'): + file_path += '.csv' + + success = self.bll.export_to_csv(file_path) + if success: + print(f"✅ 数据已成功导出到 {file_path}") + else: + print(f"❌ 数据导出失败") + elif choice == "4": + file_path = self.get_input("请输入导入文件名: ", required=False) or "students.csv" + if not file_path.endswith('.csv'): + file_path += '.csv' + + if not os.path.exists(file_path): + print(f"❌ 文件 {file_path} 不存在") + continue + + errors = self.bll.import_from_csv(file_path) + if not errors: + print(f"✅ 数据已成功导入") + else: + print(f"⚠️ 数据导入完成,但有 {len(errors)} 条记录失败:") + for error in errors: + print(f" - {error}") + elif choice == "5": + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 操作失败: {str(e)}") + + def clear_students(self) -> None: + print("\n" + "=" * 40) + print("清空所有学生信息") + print("=" * 40) + + confirm = self.get_input("警告:此操作将删除所有学生信息,且无法恢复。是否继续?(y/n): ") + if confirm.lower() != 'y': + print("❌ 操作已取消") + return + + try: + success = self.bll.clear_all() + if success: + print("✅ 所有学生信息已清空") + else: + print("❌ 清空操作失败") + except Exception as e: + print(f"❌ 清空失败: {str(e)}") + + def run(self) -> None: + print("\n" + "=" * 40) + print("欢迎使用学生信息管理系统") + print("=" * 40) + + while True: + self.display_menu() + choice = self.get_input("请选择操作(1-8): ") + + try: + if choice == "1": + self.add_student() + elif choice == "2": + self.delete_student() + elif choice == "3": + self.update_student() + elif choice == "4": + self.query_student() + elif choice == "5": + self.show_stats() + elif choice == "6": + self.import_export_data() + elif choice == "7": + self.clear_students() + elif choice == "8": + confirm = self.get_input("确定要退出系统吗?(y/n): ") + if confirm.lower() == 'y': + print("感谢使用学生信息管理系统,再见!") + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 操作失败: {str(e)}") + finally: + input("\n按回车键继续...") + + +if __name__ == "__main__": + + data_file = input("请输入数据文件路径(默认students.json): ") or "students.json" + + bll = StudentBLL(data_file) + ui = StudentUI(bll) + + ui.run() \ No newline at end of file diff --git a/stumis/model/__init__.py b/stumis/model/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stumis/model/student.py b/stumis/model/student.py new file mode 100644 index 0000000..1786c43 --- /dev/null +++ b/stumis/model/student.py @@ -0,0 +1,112 @@ +from datetime import date +from typing import Optional, Dict, Any + + +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[str] = None, + class_name: Optional[str] = None, major: 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 + + if enrollment_date: + if isinstance(enrollment_date, str): + self.enrollment_date = date.fromisoformat(enrollment_date) + else: + self.enrollment_date = enrollment_date + else: + self.enrollment_date = None + + self.class_name = class_name + self.major = major + + @property + def birthday(self) -> Optional[date]: + if not self.id_card or len(self.id_card) != 18: + return None + try: + birth_str = self.id_card[6:14] + return date(int(birth_str[:4]), int(birth_str[4:6]), int(birth_str[6:8])) + except: + return None + + @property + def age(self) -> Optional[int]: + if not self.birthday: + return None + today = date.today() + age = today.year - self.birthday.year + if (today.month, today.day) < (self.birthday.month, self.birthday.day): + age -= 1 + return age + + @property + def errors(self) -> Dict[str, str]: + errors = {} + id_card_err = self.__validate_id_card(self.id_card) + if id_card_err: + errors["id_card"] = id_card_err + if len(self.name) < 2 or len(self.name) > 20: + errors["name"] = "姓名长度需在2-20字符之间" + if self.height and not (50 <= self.height <= 250): + errors["height"] = "身高需在50-250cm之间" + if self.weight and not (5.0 <= self.weight <= 300.0): + errors["weight"] = "体重需在5-300kg之间" + if self.enrollment_date and self.enrollment_date > date.today(): + errors["enrollment_date"] = "入学日期不能晚于当前日期" + return errors + + @property + def is_valid(self) -> bool: + return not bool(self.errors) + + def to_dict(self) -> Dict[str, Any]: + data = self.__dict__.copy() + if data.get("enrollment_date"): + data["enrollment_date"] = data["enrollment_date"].isoformat() + return data + + @classmethod + def from_dict(cls, data: Dict[str, Any]) -> 'Student': + if not isinstance(data, dict): + raise TypeError("输入必须为字典类型") + enrollment_date = data.get("enrollment_date") + if enrollment_date and isinstance(enrollment_date, str): + data["enrollment_date"] = date.fromisoformat(enrollment_date) + return cls(**data) + + def __repr__(self) -> str: + attrs = ", ".join([ + f"{k}='{v}'" if isinstance(v, str) else f"{k}={v}" + for k, v in self.__dict__.items() + ]) + return f"Student({attrs})" + + @staticmethod + def get_properties() -> list: + return [k for k, v in vars(Student).items() if isinstance(v, property)] + + @staticmethod + def __validate_id_card(id_card: str) -> Optional[str]: + if not id_card or len(id_card) != 18: + return "身份证号长度必须为18位" + if not id_card[:17].isdigit(): + return "身份证号前17位必须为数字" + weights = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2] + check_codes = "10X98765432" + sum_val = sum(int(id_card[i]) * weights[i] for i in range(17)) + if id_card[17] != check_codes[sum_val % 11]: + return "身份证号校验码错误" + try: + birth_str = id_card[6:14] + date(int(birth_str[:4]), int(birth_str[4:6]), int(birth_str[6:8])) + except: + return "身份证号中出生日期格式错误" + return None diff --git a/stumis/ui/__init__.py b/stumis/ui/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stumis/ui/studentUI.py b/stumis/ui/studentUI.py new file mode 100644 index 0000000..d9f4fc6 --- /dev/null +++ b/stumis/ui/studentUI.py @@ -0,0 +1,438 @@ +from datetime import date +import os +from typing import List, Dict, Optional, Any, Union + + +class StudentUI: + """学生信息管理系统表示层""" + + def __init__(self, bll): + """初始化UI层,接收业务逻辑层实例""" + self.bll = bll + + def display_menu(self) -> None: + """显示主菜单""" + print("\n" + "=" * 40) + print("学生信息管理系统 - 主菜单") + print("=" * 40) + print("1. 添加学生信息") + print("2. 删除学生信息") + print("3. 更新学生信息") + print("4. 查询学生信息") + print("5. 统计分析") + print("6. 数据导入导出") + print("7. 清空所有学生信息") + print("8. 退出系统") + print("=" * 40) + + def display_query_menu(self) -> None: + """显示查询子菜单""" + print("\n" + "=" * 40) + print("学生信息查询菜单") + print("=" * 40) + print("1. 查询所有学生") + print("2. 按身份证号查询") + print("3. 按学号查询") + print("4. 按姓名查询") + print("5. 按班级查询") + print("6. 返回上一级菜单") + print("=" * 40) + + def display_stats_menu(self) -> None: + """显示统计分析子菜单""" + print("\n" + "=" * 40) + print("学生信息统计分析菜单") + print("=" * 40) + print("1. 学生总数") + print("2. 平均身高") + print("3. 按身高范围统计") + print("4. 按入学年份统计") + print("5. 按年龄范围统计") + print("6. 返回上一级菜单") + print("=" * 40) + + def display_import_export_menu(self) -> None: + print("\n" + "=" * 40) + print("数据导入导出菜单") + print("=" * 40) + print("1. 导出数据到JSON") + print("2. 从JSON导入数据") + print("3. 导出数据到CSV") + print("4. 从CSV导入数据") + print("5. 返回上一级菜单") + print("=" * 40) + + def get_input(self, prompt: str, required: bool = True) -> Optional[str]: + while True: + value = input(prompt).strip() + if not value and required: + print("输入不能为空,请重新输入") + continue + return value if value else None + + def get_numeric_input(self, prompt: str, min_val: Optional[float] = None, max_val: Optional[float] = None) -> \ + Optional[float]: + while True: + value = self.get_input(prompt) + if value is None: + return None + try: + num = float(value) + if min_val is not None and num < min_val: + print(f"输入值必须大于等于{min_val}") + continue + if max_val is not None and num > max_val: + print(f"输入值必须小于等于{max_val}") + continue + return num + except ValueError: + print("请输入有效的数字") + + def get_date_input(self, prompt: str) -> Optional[date]: + while True: + value = self.get_input(prompt) + if value is None: + return None + try: + return date.fromisoformat(value) + except ValueError: + print("请输入有效的日期(YYYY-MM-DD格式)") + + def add_student(self) -> None: + print("\n" + "=" * 40) + print("添加学生信息") + print("=" * 40) + + sid = self.get_input("请输入学号: ") + name = self.get_input("请输入姓名: ") + height = self.get_numeric_input("请输入身高(cm): ", min_val=50, max_val=250) + birth_date = self.get_date_input("请输入出生日期(YYYY-MM-DD): ") + enrollment_date = self.get_date_input("请输入入学日期(YYYY-MM-DD): ") + class_name = self.get_input("请输入班级: ") + + student_data = { + 'sid': sid, + 'name': name, + 'height': int(height), + 'birth_date': birth_date, + 'enrollment_date': enrollment_date, + 'class_name': class_name + } + + try: + student = Student.from_dict(student_data) + success = self.bll.add(student) + if success: + print("✅ 学生信息添加成功") + else: + print("❌ 学生信息添加失败") + except Exception as e: + print(f"❌ 添加失败: {str(e)}") + + def delete_student(self) -> None: + print("\n" + "=" * 40) + print("删除学生信息") + print("=" * 40) + + id_number = self.get_input("请输入要删除的学生身份证号: ") + + try: + success = self.bll.delete(id_number) + if success: + print("✅ 学生信息删除成功") + else: + print("❌ 学生信息删除失败") + except Exception as e: + print(f"❌ 删除失败: {str(e)}") + + def update_student(self) -> None: + print("\n" + "=" * 40) + print("更新学生信息") + print("=" * 40) + + id_number = self.get_input("请输入要更新的学生身份证号: ") + + try: + student = self.bll.get_by_id(id_number) + if not student: + print(f"❌ 未找到身份证号为{id_number}的学生") + return + + print("\n当前学生信息:") + print(f"学号: {student.sid}") + print(f"姓名: {student.name}") + print(f"身高: {student.height}cm") + print(f"出生日期: {student.birth_date}") + print(f"入学日期: {student.enrollment_date}") + print(f"班级: {student.class_name}") + + print("\n请输入新信息(直接回车保持原值):") + sid = self.get_input(f"学号 [{student.sid}]: ", required=False) or student.sid + name = self.get_input(f"姓名 [{student.name}]: ", required=False) or student.name + height = self.get_numeric_input(f"身高 [{student.height}]: ", min_val=50, max_val=250) + height = int(height) if height is not None else student.height + birth_date = self.get_date_input(f"出生日期 [{student.birth_date}]: ") or student.birth_date + enrollment_date = self.get_date_input(f"入学日期 [{student.enrollment_date}]: ") or student.enrollment_date + class_name = self.get_input(f"班级 [{student.class_name}]: ", required=False) or student.class_name + + updated_data = { + 'id_number': id_number, + 'sid': sid, + 'name': name, + 'height': height, + 'birth_date': birth_date, + 'enrollment_date': enrollment_date, + 'class_name': class_name + } + + updated_student = Student.from_dict(updated_data) + success = self.bll.update(updated_student) + + if success: + print("✅ 学生信息更新成功") + else: + print("❌ 学生信息更新失败") + + except Exception as e: + print(f"❌ 更新失败: {str(e)}") + + def query_student(self) -> None: + while True: + self.display_query_menu() + choice = self.get_input("请选择操作(1-6): ") + + try: + if choice == "1": + students = self.bll.get_all() + self._display_students(students) + elif choice == "2": + id_number = self.get_input("请输入身份证号: ") + student = self.bll.get_by_id(id_number) + if student: + self._display_students([student]) + else: + print(f"❌ 未找到身份证号为{id_number}的学生") + elif choice == "3": + stu_id = self.get_input("请输入学号: ") + student = self.bll.get_by_stu_id(stu_id) + if student: + self._display_students([student]) + else: + print(f"❌ 未找到学号为{stu_id}的学生") + elif choice == "4": + name = self.get_input("请输入姓名: ") + students = self.bll.get_by_key("name", name, fuzzy=True) + self._display_students(students) + elif choice == "5": + class_name = self.get_input("请输入班级: ") + students = self.bll.get_by_key("class_name", class_name, fuzzy=True) + self._display_students(students) + elif choice == "6": + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 查询失败: {str(e)}") + + def _display_students(self, students: List[Student]) -> None: + if not students: + print("未找到符合条件的学生") + return + + print("\n" + "-" * 80) + print(f"{'学号':<12}{'姓名':<12}{'身高(cm)':<10}{'出生日期':<12}{'入学日期':<12}{'班级':<12}") + print("-" * 80) + for student in students: + print( + f"{student.sid:<12}{student.name:<12}{student.height:<10}{student.birth_date:<12}{student.enrollment_date:<12}{student.class_name:<12}") + print("-" * 80) + print(f"共找到 {len(students)} 条记录") + + def show_stats(self) -> None: + while True: + self.display_stats_menu() + choice = self.get_input("请选择操作(1-6): ") + + try: + if choice == "1": + students = self.bll.get_all() + print(f"学生总数: {len(students)}") + elif choice == "2": + students = self.bll.get_all() + if not students: + print("暂无学生数据") + continue + avg_height = sum(s.height for s in students) / len(students) + print(f"平均身高: {avg_height:.2f}cm") + elif choice == "3": + min_height = self.get_numeric_input("请输入最小身高(cm): ", min_val=50) + max_height = self.get_numeric_input("请输入最大身高(cm): ", min_val=min_height) + + students = self.bll.get_by_range("height", min_height, max_height) + print(f"身高在 {min_height}-{max_height}cm 之间的学生有 {len(students)} 人") + elif choice == "4": + students = self.bll.get_all() + if not students: + print("暂无学生数据") + continue + + year_count = {} + for s in students: + year = s.enrollment_date.year + year_count[year] = year_count.get(year, 0) + 1 + + print("\n按入学年份统计:") + for year, count in sorted(year_count.items()): + print(f"{year}年: {count}人") + elif choice == "5": + today = date.today() + min_age = int(self.get_numeric_input("请输入最小年龄: ", min_val=0)) + max_age = int(self.get_numeric_input("请输入最大年龄: ", min_val=min_age)) + + students = self.bll.get_all() + count = 0 + for s in students: + age = today.year - s.birth_date.year + if s.birth_date.month > today.month or ( + s.birth_date.month == today.month and s.birth_date.day > today.day): + age -= 1 + if min_age <= age <= max_age: + count += 1 + + print(f"年龄在 {min_age}-{max_age} 岁之间的学生有 {count} 人") + elif choice == "6": + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 统计失败: {str(e)}") + + def import_export_data(self) -> None: + while True: + self.display_import_export_menu() + choice = self.get_input("请选择操作(1-5): ") + + try: + if choice == "1": + file_path = self.get_input("请输入导出文件名: ", required=False) or "students.json" + if not file_path.endswith('.json'): + file_path += '.json' + + success = self.bll.export_to_json(file_path) + if success: + print(f"✅ 数据已成功导出到 {file_path}") + else: + print(f"❌ 数据导出失败") + elif choice == "2": + file_path = self.get_input("请输入导入文件名: ", required=False) or "students.json" + if not file_path.endswith('.json'): + file_path += '.json' + + if not os.path.exists(file_path): + print(f"❌ 文件 {file_path} 不存在") + continue + + errors = self.bll.import_from_json(file_path) + if not errors: + print(f"✅ 数据已成功导入") + else: + print(f"⚠️ 数据导入完成,但有 {len(errors)} 条记录失败:") + for error in errors: + print(f" - {error}") + elif choice == "3": + file_path = self.get_input("请输入导出文件名: ", required=False) or "students.csv" + if not file_path.endswith('.csv'): + file_path += '.csv' + + success = self.bll.export_to_csv(file_path) + if success: + print(f"✅ 数据已成功导出到 {file_path}") + else: + print(f"❌ 数据导出失败") + elif choice == "4": + file_path = self.get_input("请输入导入文件名: ", required=False) or "students.csv" + if not file_path.endswith('.csv'): + file_path += '.csv' + + if not os.path.exists(file_path): + print(f"❌ 文件 {file_path} 不存在") + continue + + errors = self.bll.import_from_csv(file_path) + if not errors: + print(f"✅ 数据已成功导入") + else: + print(f"⚠️ 数据导入完成,但有 {len(errors)} 条记录失败:") + for error in errors: + print(f" - {error}") + elif choice == "5": + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 操作失败: {str(e)}") + + def clear_students(self) -> None: + print("\n" + "=" * 40) + print("清空所有学生信息") + print("=" * 40) + + confirm = self.get_input("警告:此操作将删除所有学生信息,且无法恢复。是否继续?(y/n): ") + if confirm.lower() != 'y': + print("❌ 操作已取消") + return + + try: + success = self.bll.clear_all() + if success: + print("✅ 所有学生信息已清空") + else: + print("❌ 清空操作失败") + except Exception as e: + print(f"❌ 清空失败: {str(e)}") + + def run(self) -> None: + print("\n" + "=" * 40) + print("欢迎使用学生信息管理系统") + print("=" * 40) + + while True: + self.display_menu() + choice = self.get_input("请选择操作(1-8): ") + + try: + if choice == "1": + self.add_student() + elif choice == "2": + self.delete_student() + elif choice == "3": + self.update_student() + elif choice == "4": + self.query_student() + elif choice == "5": + self.show_stats() + elif choice == "6": + self.import_export_data() + elif choice == "7": + self.clear_students() + elif choice == "8": + confirm = self.get_input("确定要退出系统吗?(y/n): ") + if confirm.lower() == 'y': + print("感谢使用学生信息管理系统,再见!") + break + else: + print("❌ 无效选择,请重新输入") + except Exception as e: + print(f"❌ 操作失败: {str(e)}") + finally: + input("\n按回车键继续...") + + +if __name__ == "__main__": + + data_file = input("请输入数据文件路径(默认students.json): ") or "students.json" + + bll = StudentBLL(data_file) + ui = StudentUI(bll) + + ui.run() \ No newline at end of file