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