diff --git a/stumis/bill/StudentBLL.py b/stumis/bill/StudentBLL.py new file mode 100644 index 0000000..afae75c --- /dev/null +++ b/stumis/bill/StudentBLL.py @@ -0,0 +1,92 @@ +time 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() diff --git a/stumis/bill/studentBLL.py b/stumis/bill/studentBLL.py deleted file mode 100644 index ea2b5a2..0000000 --- a/stumis/bill/studentBLL.py +++ /dev/null @@ -1,373 +0,0 @@ -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/StudentDAL.py b/stumis/dal/StudentDAL.py new file mode 100644 index 0000000..b4ab99d --- /dev/null +++ b/stumis/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 diff --git a/stumis/dal/studentDAL.py b/stumis/dal/studentDAL.py deleted file mode 100644 index f34f026..0000000 --- a/stumis/dal/studentDAL.py +++ /dev/null @@ -1,241 +0,0 @@ -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/main.py b/stumis/main.py index d9f4fc6..d47b7e5 100644 --- a/stumis/main.py +++ b/stumis/main.py @@ -1,438 +1,22 @@ -from datetime import date -import os -from typing import List, Dict, Optional, Any, Union +from dal.StudentDAL import StudentDAL +from bll.StudentBLL import StudentBLL +from tui.StudentTUI import StudentTUI -class StudentUI: - """学生信息管理系统表示层""" +def main(): + # 初始化数据访问层 + dal = StudentDAL("data/students.json") - def __init__(self, bll): - """初始化UI层,接收业务逻辑层实例""" - self.bll = bll + # 初始化业务逻辑层 + bll = StudentBLL(dal) - 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) + # 初始化用户界面 + tui = StudentTUI(bll) - 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按回车键继续...") + # 启动系统 + tui.run() if __name__ == "__main__": + 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/Student.py b/stumis/model/Student.py new file mode 100644 index 0000000..d247f2e --- /dev/null +++ b/stumis/model/Student.py @@ -0,0 +1,123 @@ +import csv +import json +import os +import sqlite3 +from abc import ABC, abstractmethod +from datetime import date +from typing import List, 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: 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 isinstance(enrollment_date, str): + self.enrollment_date = date.fromisoformat(enrollment_date) + else: + self.enrollment_date = enrollment_date + self.class_name = class_name + self.major = major + self.age, self.birthday = self._calculate_age_and_birthday() + self._validation_errors = [] + self._validate_all() + + def _calculate_age_and_birthday(self): + birth_year = int(self.id_card[6:10]) + birth_month = int(self.id_card[10:12]) + birth_day = int(self.id_card[12:14]) + birthday = date(birth_year, birth_month, birth_day) + today = date.today() + age = today.year - birth_year + if (today.month, today.day) < (birth_month, birth_day): + age -= 1 + return age, birthday + + def _validate_all(self): + self._validate_id_card() + self._validate_stu_id() + self._validate_name() + self._validate_enrollment_date() + self._validate_height() + self._validate_weight() + + def _validate_id_card(self): + if len(self.id_card) != 18: + self._validation_errors.append("身份证号必须为18位") + # 简单的校验位验证,可根据国家标准完善 + try: + int(self.id_card[:17]) + except ValueError: + self._validation_errors.append("身份证号前17位必须为数字") + + def _validate_stu_id(self): + if not self.stu_id: + self._validation_errors.append("学号不能为空") + + def _validate_name(self): + if not (2 <= len(self.name) <= 20): + self._validation_errors.append("姓名长度需在2-20个字符之间") + if any(char.isdigit() or not char.isalpha() for char in self.name): + self._validation_errors.append("姓名不能包含数字和特殊符号") + + def _validate_enrollment_date(self): + if self.enrollment_date and self.enrollment_date < self.birthday: + self._validation_errors.append("入学日期不能早于出生日期") + + def _validate_height(self): + if self.height is not None and not (50 <= self.height <= 250): + self._validation_errors.append("身高需在50-250cm之间") + + def _validate_weight(self): + if self.weight is not None and not (5 <= self.weight <= 300): + self._validation_errors.append("体重需在5-300kg之间") + + @property + def is_valid(self): + return len(self._validation_errors) == 0 + + def get_errors(self): + return self._validation_errors.copy() + + def to_dict(self): + 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, + 'age': self.age, + 'birthday': self.birthday.isoformat() + } + + @classmethod + def from_dict(cls, data): + return cls( + name=data['name'], + id_card=data['id_card'], + stu_id=data['stu_id'], + gender=data['gender'], + height=data['height'], + weight=data['weight'], + enrollment_date=data['enrollment_date'], + class_name=data['class_name'], + major=data['major'] + ) + diff --git a/stumis/model/student.py b/stumis/model/student.py deleted file mode 100644 index 1786c43..0000000 --- a/stumis/model/student.py +++ /dev/null @@ -1,112 +0,0 @@ -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/StudenTUI.py b/stumis/ui/StudenTUI.py new file mode 100644 index 0000000..788702f --- /dev/null +++ b/stumis/ui/StudenTUI.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/stumis/ui/studentUI.py b/stumis/ui/studentUI.py deleted file mode 100644 index d9f4fc6..0000000 --- a/stumis/ui/studentUI.py +++ /dev/null @@ -1,438 +0,0 @@ -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