parent
cf3adab7eb
commit
88e34462b1
@ -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()
|
@ -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
|
|
@ -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
|
@ -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
|
|
@ -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']
|
||||||
|
)
|
||||||
|
|
@ -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
|
|
@ -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("清空失败")
|
Loading…
Reference in new issue