forked from p3qolzsn9/stu1
Compare commits
No commits in common. 'master' and 'main' have entirely different histories.
@ -1,3 +0,0 @@
|
|||||||
# 默认忽略的文件
|
|
||||||
/shelf/
|
|
||||||
/workspace.xml
|
|
@ -1,12 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<profile version="1.0">
|
|
||||||
<option name="myName" value="Project Default" />
|
|
||||||
<inspection_tool class="PyUnresolvedReferencesInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
|
||||||
<option name="ignoredIdentifiers">
|
|
||||||
<list>
|
|
||||||
<option value="面向对象学生系统.dal.idal.*" />
|
|
||||||
</list>
|
|
||||||
</option>
|
|
||||||
</inspection_tool>
|
|
||||||
</profile>
|
|
||||||
</component>
|
|
@ -1,6 +0,0 @@
|
|||||||
<component name="InspectionProjectProfileManager">
|
|
||||||
<settings>
|
|
||||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
|
||||||
<version value="1.0" />
|
|
||||||
</settings>
|
|
||||||
</component>
|
|
@ -1,7 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="Black">
|
|
||||||
<option name="sdkName" value="Python 3.11" />
|
|
||||||
</component>
|
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
|
|
||||||
</project>
|
|
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="ProjectModuleManager">
|
|
||||||
<modules>
|
|
||||||
<module fileurl="file://$PROJECT_DIR$/.idea/学生系统AA版本.iml" filepath="$PROJECT_DIR$/.idea/学生系统AA版本.iml" />
|
|
||||||
</modules>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="VcsDirectoryMappings">
|
|
||||||
<mapping directory="$PROJECT_DIR$/.." vcs="Git" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<module type="PYTHON_MODULE" version="4">
|
|
||||||
<component name="NewModuleRootManager">
|
|
||||||
<content url="file://$MODULE_DIR$" />
|
|
||||||
<orderEntry type="jdk" jdkName="Python 3.11" jdkType="Python SDK" />
|
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
|
||||||
</module>
|
|
@ -1,80 +0,0 @@
|
|||||||
from typing import List, Optional
|
|
||||||
from models.student import Student
|
|
||||||
from dal.interfaces import IStudentDAL
|
|
||||||
from bll.validators import validate_student
|
|
||||||
|
|
||||||
|
|
||||||
class StudentService:
|
|
||||||
def __init__(self, dal: IStudentDAL):
|
|
||||||
self.dal = dal
|
|
||||||
|
|
||||||
def add_student(self, student: Student) -> bool:
|
|
||||||
"""添加学生"""
|
|
||||||
if not validate_student(student):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查学号和身份证号是否已存在
|
|
||||||
if self.dal.get_by_id(student.id_card) or self.dal.get_by_stu_id(student.stu_id):
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.dal.add_student(student)
|
|
||||||
|
|
||||||
def delete_student(self, id_card: str) -> bool:
|
|
||||||
"""删除学生"""
|
|
||||||
return self.dal.delete_student(id_card)
|
|
||||||
|
|
||||||
def update_student(self, student: Student) -> bool:
|
|
||||||
"""更新学生信息"""
|
|
||||||
if not validate_student(student):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 检查学生是否存在
|
|
||||||
existing = self.dal.get_by_id(student.id_card)
|
|
||||||
if not existing:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return self.dal.update_student(student)
|
|
||||||
|
|
||||||
def get_student_by_id(self, id_card: str) -> Optional[Student]:
|
|
||||||
"""根据身份证号查询学生"""
|
|
||||||
return self.dal.get_by_id(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_by_name(self, name: str) -> List[Student]:
|
|
||||||
"""按姓名模糊查询"""
|
|
||||||
return self.dal.get_by_name(name)
|
|
||||||
|
|
||||||
def search_by_class(self, class_name: str) -> List[Student]:
|
|
||||||
"""按班级模糊查询"""
|
|
||||||
return self.dal.get_by_class(class_name)
|
|
||||||
|
|
||||||
def search_by_major(self, major: str) -> List[Student]:
|
|
||||||
"""按专业模糊查询"""
|
|
||||||
return self.dal.get_by_major(major)
|
|
||||||
|
|
||||||
def count_total_students(self) -> int:
|
|
||||||
"""统计学生总数"""
|
|
||||||
return self.dal.count_students()
|
|
||||||
|
|
||||||
def count_students_by_major(self) -> dict:
|
|
||||||
"""按专业统计学生人数"""
|
|
||||||
return self.dal.count_by_major()
|
|
||||||
|
|
||||||
def calculate_average_height(self, group_by: str = None) -> dict:
|
|
||||||
"""计算平均身高"""
|
|
||||||
return self.dal.calculate_avg_height(group_by)
|
|
||||||
|
|
||||||
def calculate_average_weight(self, group_by: str = None) -> dict:
|
|
||||||
"""计算平均体重"""
|
|
||||||
return self.dal.calculate_avg_weight(group_by)
|
|
||||||
|
|
||||||
def clear_all_data(self) -> bool:
|
|
||||||
"""清空所有数据"""
|
|
||||||
return self.dal.clear_all()
|
|
@ -1,59 +0,0 @@
|
|||||||
|
|
||||||
import re
|
|
||||||
from datetime import date
|
|
||||||
from typing import Optional
|
|
||||||
from models.student import Student
|
|
||||||
|
|
||||||
|
|
||||||
def validate_id_card(id_card: str) -> bool:
|
|
||||||
"""验证身份证号有效性"""
|
|
||||||
if len(id_card) != 18:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# 校验位计算
|
|
||||||
factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
|
||||||
checksum_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
|
||||||
|
|
||||||
total = 0
|
|
||||||
for i in range(17):
|
|
||||||
total += int(id_card[i]) * factors[i]
|
|
||||||
|
|
||||||
return checksum_map[total % 11] == id_card[-1].upper()
|
|
||||||
|
|
||||||
|
|
||||||
def validate_stu_id(stu_id: str) -> bool:
|
|
||||||
"""验证学号有效性"""
|
|
||||||
# 假设学号格式为: 15位数的那个学号
|
|
||||||
return len(stu_id) == 15 and stu_id.isdigit()
|
|
||||||
|
|
||||||
|
|
||||||
def validate_name(name: str) -> bool:
|
|
||||||
"""验证姓名有效性"""
|
|
||||||
return 2 <= len(name) <= 20 and re.match(r'^[\u4e00-\u9fa5a-zA-Z·]+$', name)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_height(height: Optional[int]) -> bool:
|
|
||||||
"""验证身高有效性"""
|
|
||||||
return height is None or (50 <= height <= 250)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_weight(weight: Optional[float]) -> bool:
|
|
||||||
"""验证体重有效性"""
|
|
||||||
return weight is None or (5 <= weight <= 300)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_enrollment_date(enrollment_date: Optional[date], birthday: date) -> bool:
|
|
||||||
"""验证入学日期有效性"""
|
|
||||||
return enrollment_date is None or enrollment_date > birthday
|
|
||||||
|
|
||||||
|
|
||||||
def validate_student(student: Student) -> bool:
|
|
||||||
"""验证学生对象所有字段"""
|
|
||||||
return all([
|
|
||||||
validate_id_card(student.id_card),
|
|
||||||
validate_stu_id(student.stu_id),
|
|
||||||
validate_name(student.name),
|
|
||||||
validate_height(student.height),
|
|
||||||
validate_weight(student.weight),
|
|
||||||
validate_enrollment_date(student.enrollment_date, student.birthday)
|
|
||||||
])
|
|
@ -1,129 +0,0 @@
|
|||||||
import csv
|
|
||||||
from typing import List, Optional
|
|
||||||
from pathlib import Path
|
|
||||||
from models.student import Student
|
|
||||||
from dal.interfaces import IStudentDAL
|
|
||||||
|
|
||||||
|
|
||||||
class CSVStudentDAL(IStudentDAL):
|
|
||||||
def clear_all(self) -> bool:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def calculate_avg_weight(self, group_by: str = None) -> dict:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def calculate_avg_height(self, group_by: str = None) -> dict:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def count_by_major(self) -> dict:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def count_students(self) -> int:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_by_major(self, major: str) -> List[Student]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_by_class(self, class_name: str) -> List[Student]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def get_by_name(self, name: str) -> List[Student]:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __init__(self, file_path: str = "students.csv"):
|
|
||||||
self.file_path = Path(file_path)
|
|
||||||
self._ensure_file_exists()
|
|
||||||
|
|
||||||
def _ensure_file_exists(self):
|
|
||||||
"""确保CSV文件存在"""
|
|
||||||
if not self.file_path.exists():
|
|
||||||
with open(self.file_path, 'w', newline='', encoding='utf-8') as f:
|
|
||||||
writer = csv.writer(f)
|
|
||||||
writer.writerow([
|
|
||||||
'name', 'id_card', 'stu_id', 'gender',
|
|
||||||
'height', 'weight', 'enrollment_date',
|
|
||||||
'class_name', 'major', 'birthday', 'age'
|
|
||||||
])
|
|
||||||
|
|
||||||
def add_student(self, student: Student) -> bool:
|
|
||||||
"""添加学生到CSV文件"""
|
|
||||||
students = self.get_all()
|
|
||||||
|
|
||||||
# 检查是否已存在
|
|
||||||
if any(s.id_card == student.id_card or s.stu_id == student.stu_id for s in students):
|
|
||||||
return False
|
|
||||||
|
|
||||||
students.append(student)
|
|
||||||
return self._save_all(students)
|
|
||||||
|
|
||||||
def delete_student(self, id_card: str) -> bool:
|
|
||||||
"""从CSV文件删除学生"""
|
|
||||||
students = self.get_all()
|
|
||||||
original_count = len(students)
|
|
||||||
|
|
||||||
students = [s for s in students if s.id_card != id_card]
|
|
||||||
|
|
||||||
if len(students) < original_count:
|
|
||||||
return self._save_all(students)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_student(self, student: Student) -> bool:
|
|
||||||
"""更新CSV文件中的学生信息"""
|
|
||||||
students = self.get_all()
|
|
||||||
updated = False
|
|
||||||
|
|
||||||
for i, s in enumerate(students):
|
|
||||||
if s.id_card == student.id_card:
|
|
||||||
students[i] = student
|
|
||||||
updated = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if updated:
|
|
||||||
return self._save_all(students)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_by_id(self, id_card: str) -> Optional[Student]:
|
|
||||||
"""根据身份证号获取学生"""
|
|
||||||
students = self.get_all()
|
|
||||||
for s in students:
|
|
||||||
if s.id_card == id_card:
|
|
||||||
return s
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
||||||
"""根据学号获取学生"""
|
|
||||||
students = self.get_all()
|
|
||||||
for s in students:
|
|
||||||
if s.stu_id == stu_id:
|
|
||||||
return s
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all(self) -> List[Student]:
|
|
||||||
"""获取所有学生"""
|
|
||||||
students = []
|
|
||||||
|
|
||||||
with open(self.file_path, 'r', newline='', encoding='utf-8') as f:
|
|
||||||
reader = csv.DictReader(f)
|
|
||||||
for row in reader:
|
|
||||||
students.append(Student.from_dict(row))
|
|
||||||
|
|
||||||
return students
|
|
||||||
|
|
||||||
# 其他方法实现类似,限于篇幅省略...
|
|
||||||
|
|
||||||
def _save_all(self, students: List[Student]) -> bool:
|
|
||||||
"""保存所有学生到CSV文件"""
|
|
||||||
try:
|
|
||||||
with open(self.file_path, 'w', newline='', encoding='utf-8') as f:
|
|
||||||
writer = csv.DictWriter(f, fieldnames=[
|
|
||||||
'name', 'id_card', 'stu_id', 'gender',
|
|
||||||
'height', 'weight', 'enrollment_date',
|
|
||||||
'class_name', 'major', 'birthday', 'age'
|
|
||||||
])
|
|
||||||
writer.writeheader()
|
|
||||||
for student in students:
|
|
||||||
writer.writerow(student.to_dict())
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"保存数据时出错: {e}")
|
|
||||||
return False
|
|
@ -1,77 +0,0 @@
|
|||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import List, Optional
|
|
||||||
from models.student import Student
|
|
||||||
|
|
||||||
|
|
||||||
class IStudentDAL(ABC):
|
|
||||||
"""学生信息数据访问层接口"""
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def add_student(self, student: Student) -> bool:
|
|
||||||
"""添加学生信息"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def delete_student(self, id_card: str) -> bool:
|
|
||||||
"""根据身份证号删除学生信息"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def update_student(self, student: Student) -> bool:
|
|
||||||
"""更新学生信息"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_by_id(self, id_card: str) -> Optional[Student]:
|
|
||||||
"""根据身份证号获取学生信息"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
||||||
"""根据学号获取学生信息"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_all(self) -> List[Student]:
|
|
||||||
"""获取所有学生信息"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_by_name(self, name: str) -> List[Student]:
|
|
||||||
"""根据姓名查询学生信息(模糊查询)"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_by_class(self, class_name: str) -> List[Student]:
|
|
||||||
"""根据班级查询学生信息(模糊查询)"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_by_major(self, major: str) -> List[Student]:
|
|
||||||
"""根据专业查询学生信息(模糊查询)"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def count_students(self) -> int:
|
|
||||||
"""统计学生总数"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def count_by_major(self) -> dict:
|
|
||||||
"""按专业统计学生人数"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def calculate_avg_height(self, group_by: str = None) -> dict:
|
|
||||||
"""计算平均身高"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def calculate_avg_weight(self, group_by: str = None) -> dict:
|
|
||||||
"""计算平均体重"""
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def clear_all(self) -> bool:
|
|
||||||
"""清空所有学生数据"""
|
|
||||||
pass
|
|
@ -1,175 +0,0 @@
|
|||||||
import json
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import List, Optional, Dict, Any
|
|
||||||
|
|
||||||
from dal.interfaces import IStudentDAL
|
|
||||||
from models.student import Student
|
|
||||||
|
|
||||||
|
|
||||||
class JSONStudentDAL(IStudentDAL):
|
|
||||||
def __init__(self, file_path: str = "students.json"):
|
|
||||||
self.file_path = Path(file_path)
|
|
||||||
self._ensure_file_exists()
|
|
||||||
|
|
||||||
def _ensure_file_exists(self):
|
|
||||||
"""确保JSON文件存在"""
|
|
||||||
if not self.file_path.exists():
|
|
||||||
with open(self.file_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump([], f, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
def _load_data(self) -> List[Dict[str, Any]]:
|
|
||||||
"""从JSON文件加载所有数据"""
|
|
||||||
try:
|
|
||||||
with open(self.file_path, 'r', encoding='utf-8') as f:
|
|
||||||
return json.load(f)
|
|
||||||
except (json.JSONDecodeError, FileNotFoundError):
|
|
||||||
return []
|
|
||||||
|
|
||||||
def _save_data(self, data: List[Dict[str, Any]]) -> bool:
|
|
||||||
"""保存数据到JSON文件"""
|
|
||||||
try:
|
|
||||||
with open(self.file_path, 'w', encoding='utf-8') as f:
|
|
||||||
json.dump(data, f, ensure_ascii=False, indent=2)
|
|
||||||
return True
|
|
||||||
except Exception as e:
|
|
||||||
print(f"保存数据时出错: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
def add_student(self, student: Student) -> bool:
|
|
||||||
"""添加学生到JSON文件"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
|
|
||||||
# 检查是否已存在
|
|
||||||
if any(s['id_card'] == student.id_card or s['stu_id'] == student.stu_id
|
|
||||||
for s in students_data):
|
|
||||||
return False
|
|
||||||
|
|
||||||
students_data.append(student.to_dict())
|
|
||||||
return self._save_data(students_data)
|
|
||||||
|
|
||||||
def delete_student(self, id_card: str) -> bool:
|
|
||||||
"""从JSON文件删除学生"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
original_count = len(students_data)
|
|
||||||
|
|
||||||
students_data = [s for s in students_data if s['id_card'] != id_card]
|
|
||||||
|
|
||||||
if len(students_data) < original_count:
|
|
||||||
return self._save_data(students_data)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def update_student(self, student: Student) -> bool:
|
|
||||||
"""更新JSON文件中的学生信息"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
updated = False
|
|
||||||
|
|
||||||
for i, s in enumerate(students_data):
|
|
||||||
if s['id_card'] == student.id_card:
|
|
||||||
students_data[i] = student.to_dict()
|
|
||||||
updated = True
|
|
||||||
break
|
|
||||||
|
|
||||||
if updated:
|
|
||||||
return self._save_data(students_data)
|
|
||||||
return False
|
|
||||||
|
|
||||||
def get_by_id(self, id_card: str) -> Optional[Student]:
|
|
||||||
"""根据身份证号获取学生"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
for s in students_data:
|
|
||||||
if s['id_card'] == id_card:
|
|
||||||
return Student.from_dict(s)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
|
|
||||||
"""根据学号获取学生"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
for s in students_data:
|
|
||||||
if s['stu_id'] == stu_id:
|
|
||||||
return Student.from_dict(s)
|
|
||||||
return None
|
|
||||||
|
|
||||||
def get_all(self) -> List[Student]:
|
|
||||||
"""获取所有学生"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
return [Student.from_dict(s) for s in students_data]
|
|
||||||
|
|
||||||
def get_by_name(self, name: str) -> List[Student]:
|
|
||||||
"""根据姓名查询学生信息(模糊查询)"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
return [Student.from_dict(s) for s in students_data
|
|
||||||
if name.lower() in s['name'].lower()]
|
|
||||||
|
|
||||||
def get_by_class(self, class_name: str) -> List[Student]:
|
|
||||||
"""根据班级查询学生信息(模糊查询)"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
return [Student.from_dict(s) for s in students_data
|
|
||||||
if s['class_name'] and class_name.lower() in s['class_name'].lower()]
|
|
||||||
|
|
||||||
def get_by_major(self, major: str) -> List[Student]:
|
|
||||||
"""根据专业查询学生信息(模糊查询)"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
return [Student.from_dict(s) for s in students_data
|
|
||||||
if s['major'] and major.lower() in s['major'].lower()]
|
|
||||||
|
|
||||||
def count_students(self) -> int:
|
|
||||||
"""统计学生总数"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
return len(students_data)
|
|
||||||
|
|
||||||
def count_by_major(self) -> Dict[str, int]:
|
|
||||||
"""按专业统计学生人数"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
result = {}
|
|
||||||
|
|
||||||
for s in students_data:
|
|
||||||
major = s.get('major', '未指定')
|
|
||||||
result[major] = result.get(major, 0) + 1
|
|
||||||
|
|
||||||
return result
|
|
||||||
|
|
||||||
def calculate_avg_height(self, group_by: str = None) -> Dict[str, float]:
|
|
||||||
"""计算平均身高"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
|
|
||||||
if not group_by:
|
|
||||||
heights = [s['height'] for s in students_data if s.get('height') is not None]
|
|
||||||
return {'all': sum(heights) / len(heights)} if heights else {}
|
|
||||||
|
|
||||||
# 按指定字段分组计算
|
|
||||||
groups = {}
|
|
||||||
for s in students_data:
|
|
||||||
if s.get('height') is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = s.get(group_by, '未指定')
|
|
||||||
if key not in groups:
|
|
||||||
groups[key] = []
|
|
||||||
groups[key].append(s['height'])
|
|
||||||
|
|
||||||
return {k: sum(v) / len(v) for k, v in groups.items()}
|
|
||||||
|
|
||||||
def calculate_avg_weight(self, group_by: str = None) -> Dict[str, float]:
|
|
||||||
"""计算平均体重"""
|
|
||||||
students_data = self._load_data()
|
|
||||||
|
|
||||||
if not group_by:
|
|
||||||
weights = [s['weight'] for s in students_data if s.get('weight') is not None]
|
|
||||||
return {'all': sum(weights) / len(weights)} if weights else {}
|
|
||||||
|
|
||||||
# 按指定字段分组计算
|
|
||||||
groups = {}
|
|
||||||
for s in students_data:
|
|
||||||
if s.get('weight') is None:
|
|
||||||
continue
|
|
||||||
|
|
||||||
key = s.get(group_by, '未指定')
|
|
||||||
if key not in groups:
|
|
||||||
groups[key] = []
|
|
||||||
groups[key].append(s['weight'])
|
|
||||||
|
|
||||||
return {k: sum(v) / len(v) for k, v in groups.items()}
|
|
||||||
|
|
||||||
def clear_all(self) -> bool:
|
|
||||||
"""清空所有学生数据"""
|
|
||||||
return self._save_data([])
|
|
@ -1,66 +0,0 @@
|
|||||||
from datetime import date
|
|
||||||
from typing import Optional, Union
|
|
||||||
from utils.id_card import extract_birthday_from_id_card, calculate_age
|
|
||||||
|
|
||||||
|
|
||||||
class Student:
|
|
||||||
def __init__(self,
|
|
||||||
name: str,
|
|
||||||
id_card: str,
|
|
||||||
stu_id: str,
|
|
||||||
gender: Optional[bool] = None,
|
|
||||||
height: Optional[int] = None,
|
|
||||||
weight: Optional[float] = None,
|
|
||||||
enrollment_date: Optional[Union[date, str]] = None,
|
|
||||||
class_name: Optional[str] = None,
|
|
||||||
major: Optional[str] = None):
|
|
||||||
|
|
||||||
self.name = name
|
|
||||||
self.id_card = id_card
|
|
||||||
self.stu_id = stu_id
|
|
||||||
self.gender = gender # True: 男, False: 女
|
|
||||||
self.height = height
|
|
||||||
self.weight = weight
|
|
||||||
self.class_name = class_name
|
|
||||||
self.major = major
|
|
||||||
|
|
||||||
# 处理日期类型
|
|
||||||
if isinstance(enrollment_date, str):
|
|
||||||
self.enrollment_date = date.fromisoformat(enrollment_date)
|
|
||||||
else:
|
|
||||||
self.enrollment_date = enrollment_date
|
|
||||||
|
|
||||||
# 从身份证生成字段
|
|
||||||
self.birthday = extract_birthday_from_id_card(id_card)
|
|
||||||
self.age = calculate_age(self.birthday)
|
|
||||||
|
|
||||||
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,
|
|
||||||
'birthday': self.birthday.isoformat(),
|
|
||||||
'age': self.age
|
|
||||||
}
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_dict(cls, data: dict):
|
|
||||||
"""从字典创建学生对象"""
|
|
||||||
return cls(
|
|
||||||
name=data['name'],
|
|
||||||
id_card=data['id_card'],
|
|
||||||
stu_id=data['stu_id'],
|
|
||||||
gender=data.get('gender'),
|
|
||||||
height=data.get('height'),
|
|
||||||
weight=data.get('weight'),
|
|
||||||
enrollment_date=data.get('enrollment_date'),
|
|
||||||
class_name=data.get('class_name'),
|
|
||||||
major=data.get('major')
|
|
||||||
)
|
|
|
@ -1,15 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"name": "德国",
|
|
||||||
"id_card": "510703200411220010",
|
|
||||||
"stu_id": "110309230907004",
|
|
||||||
"gender": true,
|
|
||||||
"height": 177,
|
|
||||||
"weight": 60.0,
|
|
||||||
"enrollment_date": "2023-09-01",
|
|
||||||
"class_name": "困",
|
|
||||||
"major": "碎觉",
|
|
||||||
"birthday": "2004-11-22",
|
|
||||||
"age": 20
|
|
||||||
}
|
|
||||||
]
|
|
@ -1,403 +0,0 @@
|
|||||||
import sys
|
|
||||||
from datetime import date
|
|
||||||
from typing import Optional
|
|
||||||
from models.student import Student
|
|
||||||
from bll.student_service import StudentService
|
|
||||||
|
|
||||||
|
|
||||||
def _show_main_menu():
|
|
||||||
"""显示主菜单"""
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("学生信息管理系统".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
print("1. 添加学生信息")
|
|
||||||
print("2. 删除学生信息")
|
|
||||||
print("3. 更新学生信息")
|
|
||||||
print("4. 查询学生信息")
|
|
||||||
print("5. 统计分析")
|
|
||||||
print("6. 数据导入导出")
|
|
||||||
print("7. 清空所有学生信息")
|
|
||||||
print("0. 退出系统")
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
|
|
||||||
def _data_import_export():
|
|
||||||
"""数据导入导出功能"""
|
|
||||||
while True:
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("数据导入导出".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
print("1. 导出数据到CSV")
|
|
||||||
print("2. 从CSV导入数据")
|
|
||||||
print("3. 导出数据到JSON")
|
|
||||||
print("4. 从JSON导入数据")
|
|
||||||
print("5. 返回上一级")
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
choice = input("请选择操作: ").strip()
|
|
||||||
|
|
||||||
if choice in ("1", "2", "3", "4"):
|
|
||||||
print("\n此功能暂未实现")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "5":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("无效的选择,请重新输入!")
|
|
||||||
input("按任意键继续...")
|
|
||||||
|
|
||||||
|
|
||||||
def _input_student_info(update_mode=False) -> Optional[Student]:
|
|
||||||
"""输入学生信息"""
|
|
||||||
try:
|
|
||||||
print("\n请输入学生信息:")
|
|
||||||
name = input("姓名: ").strip()
|
|
||||||
id_card = input("身份证号: ").strip() if not update_mode else None
|
|
||||||
stu_id = input("学号: ").strip() if not update_mode else None
|
|
||||||
|
|
||||||
gender_input = input("性别(男/女): ").strip().lower()
|
|
||||||
gender = None
|
|
||||||
if gender_input == '男':
|
|
||||||
gender = True
|
|
||||||
elif gender_input == '女':
|
|
||||||
gender = False
|
|
||||||
|
|
||||||
height_input = input("身高(cm): ").strip()
|
|
||||||
height = int(height_input) if height_input else None
|
|
||||||
|
|
||||||
weight_input = input("体重(kg): ").strip()
|
|
||||||
weight = float(weight_input) if weight_input else None
|
|
||||||
|
|
||||||
enrollment_date_input = input("入学日期(YYYY-MM-DD): ").strip()
|
|
||||||
enrollment_date = date.fromisoformat(enrollment_date_input) if enrollment_date_input else None
|
|
||||||
|
|
||||||
class_name = input("班级名称: ").strip() or None
|
|
||||||
major = input("专业: ").strip() or None
|
|
||||||
|
|
||||||
if update_mode:
|
|
||||||
return Student(
|
|
||||||
name=name or None,
|
|
||||||
id_card=None,
|
|
||||||
stu_id=None,
|
|
||||||
gender=gender,
|
|
||||||
height=height,
|
|
||||||
weight=weight,
|
|
||||||
enrollment_date=enrollment_date,
|
|
||||||
class_name=class_name,
|
|
||||||
major=major
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
return 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
|
|
||||||
)
|
|
||||||
except ValueError as e:
|
|
||||||
print(f"\n输入格式错误: {e}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _display_students_list(students: list[Student]):
|
|
||||||
"""显示学生列表"""
|
|
||||||
if not students:
|
|
||||||
print("\n没有找到符合条件的学生")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("\n" + "=" * 90)
|
|
||||||
print(f"{'学号':<12}{'姓名':<10}{'性别':<6}{'年龄':<6}{'班级':<15}{'专业':<20}{'入学日期':<12}")
|
|
||||||
print("=" * 90)
|
|
||||||
|
|
||||||
for student in students:
|
|
||||||
gender = '男' if student.gender else '女' if student.gender is not None else '未知'
|
|
||||||
print(
|
|
||||||
f"{student.stu_id:<16}"
|
|
||||||
f"{student.name:<10}"
|
|
||||||
f"{gender:<6}"
|
|
||||||
f"{student.age:<6}"
|
|
||||||
f"{student.class_name or '未指定':<15}"
|
|
||||||
f"{student.major or '未指定':<20}"
|
|
||||||
f"{student.enrollment_date.isoformat() if student.enrollment_date else '未指定':<12}"
|
|
||||||
)
|
|
||||||
print("=" * 90)
|
|
||||||
print(f"共找到 {len(students)} 名学生")
|
|
||||||
|
|
||||||
|
|
||||||
def _display_student_details(student: Student):
|
|
||||||
"""显示学生详细信息"""
|
|
||||||
print("\n" + "=" * 40)
|
|
||||||
print("学生详细信息".center(36))
|
|
||||||
print("=" * 40)
|
|
||||||
print(f"学号: {student.stu_id}")
|
|
||||||
print(f"姓名: {student.name}")
|
|
||||||
print(f"性别: {'男' if student.gender else '女' if student.gender is not None else '未指定'}")
|
|
||||||
print(f"年龄: {student.age}")
|
|
||||||
print(f"出生日期: {student.birthday.isoformat()}")
|
|
||||||
print(f"身份证号: {student.id_card}")
|
|
||||||
print(f"身高: {student.height}cm" if student.height else "身高: 未指定")
|
|
||||||
print(f"体重: {student.weight}kg" if student.weight else "体重: 未指定")
|
|
||||||
print(f"班级: {student.class_name or '未指定'}")
|
|
||||||
print(f"专业: {student.major or '未指定'}")
|
|
||||||
print(f"入学日期: {student.enrollment_date.isoformat() if student.enrollment_date else '未指定'}")
|
|
||||||
print("=" * 40)
|
|
||||||
|
|
||||||
|
|
||||||
class ConsoleUI:
|
|
||||||
def __init__(self, service: StudentService):
|
|
||||||
self.service = service
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
"""运行控制台界面"""
|
|
||||||
while True:
|
|
||||||
_show_main_menu()
|
|
||||||
choice = input("请选择操作: ").strip()
|
|
||||||
|
|
||||||
if choice == "1":
|
|
||||||
self._add_student()
|
|
||||||
elif choice == "2":
|
|
||||||
self._delete_student()
|
|
||||||
elif choice == "3":
|
|
||||||
self._update_student()
|
|
||||||
elif choice == "4":
|
|
||||||
self._search_student()
|
|
||||||
elif choice == "5":
|
|
||||||
self._statistics()
|
|
||||||
elif choice == "6":
|
|
||||||
_data_import_export()
|
|
||||||
elif choice == "7":
|
|
||||||
self._clear_all_data()
|
|
||||||
elif choice == "0":
|
|
||||||
print("感谢使用学生信息管理系统,再见!")
|
|
||||||
sys.exit(0)
|
|
||||||
else:
|
|
||||||
print("无效的选择,请重新输入!")
|
|
||||||
input("按任意键继续...")
|
|
||||||
|
|
||||||
def _add_student(self):
|
|
||||||
"""添加学生信息"""
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("添加学生信息".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
student = _input_student_info()
|
|
||||||
if student:
|
|
||||||
if self.service.add_student(student):
|
|
||||||
print("\n学生信息添加成功!")
|
|
||||||
else:
|
|
||||||
print("\n添加失败,可能是学号或身份证号已存在,或数据验证失败!")
|
|
||||||
input("\n按任意键返回主菜单...")
|
|
||||||
|
|
||||||
def _delete_student(self):
|
|
||||||
"""删除学生信息"""
|
|
||||||
while True:
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("删除学生信息".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
print("1. 按身份证号删除")
|
|
||||||
print("2. 按学号删除")
|
|
||||||
print("3. 返回上一级")
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
choice = input("请选择操作: ").strip()
|
|
||||||
|
|
||||||
if choice == "1":
|
|
||||||
id_card = input("请输入身份证号: ").strip()
|
|
||||||
if self.service.delete_student(id_card):
|
|
||||||
print("\n删除成功!")
|
|
||||||
else:
|
|
||||||
print("\n删除失败,未找到该学生!")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "2":
|
|
||||||
stu_id = input("请输入学号: ").strip()
|
|
||||||
student = self.service.get_student_by_stu_id(stu_id)
|
|
||||||
if student and self.service.delete_student(student.id_card):
|
|
||||||
print("\n删除成功!")
|
|
||||||
else:
|
|
||||||
print("\n删除失败,未找到该学生!")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "3":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("无效的选择,请重新输入!")
|
|
||||||
input("按任意键继续...")
|
|
||||||
|
|
||||||
def _update_student(self):
|
|
||||||
"""更新学生信息"""
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("更新学生信息".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
stu_id = input("请输入要更新的学生学号: ").strip()
|
|
||||||
student = self.service.get_student_by_stu_id(stu_id)
|
|
||||||
|
|
||||||
if not student:
|
|
||||||
print("\n未找到该学生!")
|
|
||||||
input("\n按任意键返回主菜单...")
|
|
||||||
return
|
|
||||||
|
|
||||||
print("\n当前学生信息:")
|
|
||||||
_display_student_details(student)
|
|
||||||
|
|
||||||
print("\n请输入新的学生信息(留空保持不变):")
|
|
||||||
new_student = _input_student_info(update_mode=True)
|
|
||||||
|
|
||||||
# 更新字段
|
|
||||||
for attr, value in new_student.__dict__.items():
|
|
||||||
if value is not None:
|
|
||||||
setattr(student, attr, value)
|
|
||||||
|
|
||||||
if self.service.update_student(student):
|
|
||||||
print("\n学生信息更新成功!")
|
|
||||||
else:
|
|
||||||
print("\n更新失败,数据验证未通过!")
|
|
||||||
input("\n按任意键返回主菜单...")
|
|
||||||
|
|
||||||
def _search_student(self):
|
|
||||||
"""查询学生信息"""
|
|
||||||
while True:
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("查询学生信息".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
print("1. 查询所有学生")
|
|
||||||
print("2. 按身份证号查询")
|
|
||||||
print("3. 按学号查询")
|
|
||||||
print("4. 按姓名查询")
|
|
||||||
print("5. 按班级查询")
|
|
||||||
print("6. 按专业查询")
|
|
||||||
print("7. 返回上一级")
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
choice = input("请选择操作: ").strip()
|
|
||||||
|
|
||||||
if choice == "1":
|
|
||||||
students = self.service.get_all_students()
|
|
||||||
_display_students_list(students)
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "2":
|
|
||||||
id_card = input("请输入身份证号: ").strip()
|
|
||||||
student = self.service.get_student_by_id(id_card)
|
|
||||||
if student:
|
|
||||||
_display_student_details(student)
|
|
||||||
else:
|
|
||||||
print("\n未找到该学生!")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "3":
|
|
||||||
stu_id = input("请输入学号: ").strip()
|
|
||||||
student = self.service.get_student_by_stu_id(stu_id)
|
|
||||||
if student:
|
|
||||||
_display_student_details(student)
|
|
||||||
else:
|
|
||||||
print("\n未找到该学生!")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "4":
|
|
||||||
name = input("请输入姓名(支持模糊查询): ").strip()
|
|
||||||
students = self.service.search_by_name(name)
|
|
||||||
_display_students_list(students)
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "5":
|
|
||||||
class_name = input("请输入班级名称(支持模糊查询): ").strip()
|
|
||||||
students = self.service.search_by_class(class_name)
|
|
||||||
_display_students_list(students)
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "6":
|
|
||||||
major = input("请输入专业名称(支持模糊查询): ").strip()
|
|
||||||
students = self.service.search_by_major(major)
|
|
||||||
_display_students_list(students)
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "7":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("无效的选择,请重新输入!")
|
|
||||||
input("按任意键继续...")
|
|
||||||
|
|
||||||
def _statistics(self):
|
|
||||||
"""统计分析功能"""
|
|
||||||
while True:
|
|
||||||
print("\n" + "=" * 30)
|
|
||||||
print("统计分析".center(24))
|
|
||||||
print("=" * 30)
|
|
||||||
print("1. 学生总数")
|
|
||||||
print("2. 按专业统计人数")
|
|
||||||
print("3. 平均身高")
|
|
||||||
print("4. 平均体重")
|
|
||||||
print("5. 返回上一级")
|
|
||||||
print("=" * 30)
|
|
||||||
|
|
||||||
choice = input("请选择操作: ").strip()
|
|
||||||
|
|
||||||
if choice == "1":
|
|
||||||
count = self.service.count_total_students()
|
|
||||||
print(f"\n学生总数: {count}")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "2":
|
|
||||||
stats = self.service.count_students_by_major()
|
|
||||||
print("\n各专业学生人数统计:")
|
|
||||||
for major, count in stats.items():
|
|
||||||
print(f"{major}: {count}人")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "3":
|
|
||||||
print("\n1. 全体平均身高")
|
|
||||||
print("2. 按班级统计平均身高")
|
|
||||||
print("3. 按专业统计平均身高")
|
|
||||||
sub_choice = input("请选择统计方式: ").strip()
|
|
||||||
|
|
||||||
if sub_choice == "1":
|
|
||||||
avg = self.service.calculate_average_height()
|
|
||||||
print(f"\n全体平均身高: {avg.get('all', 0):.1f}cm")
|
|
||||||
elif sub_choice == "2":
|
|
||||||
avg = self.service.calculate_average_height("class_name")
|
|
||||||
print("\n各班级平均身高:")
|
|
||||||
for class_name, height in avg.items():
|
|
||||||
print(f"{class_name}: {height:.1f}cm")
|
|
||||||
elif sub_choice == "3":
|
|
||||||
avg = self.service.calculate_average_height("major")
|
|
||||||
print("\n各专业平均身高:")
|
|
||||||
for major, height in avg.items():
|
|
||||||
print(f"{major}: {height:.1f}cm")
|
|
||||||
else:
|
|
||||||
print("无效的选择!")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "4":
|
|
||||||
print("\n1. 全体平均体重")
|
|
||||||
print("2. 按班级统计平均体重")
|
|
||||||
print("3. 按专业统计平均体重")
|
|
||||||
sub_choice = input("请选择统计方式: ").strip()
|
|
||||||
|
|
||||||
if sub_choice == "1":
|
|
||||||
avg = self.service.calculate_average_weight()
|
|
||||||
print(f"\n全体平均体重: {avg.get('all', 0):.1f}kg")
|
|
||||||
elif sub_choice == "2":
|
|
||||||
avg = self.service.calculate_average_weight("class_name")
|
|
||||||
print("\n各班级平均体重:")
|
|
||||||
for class_name, weight in avg.items():
|
|
||||||
print(f"{class_name}: {weight:.1f}kg")
|
|
||||||
elif sub_choice == "3":
|
|
||||||
avg = self.service.calculate_average_weight("major")
|
|
||||||
print("\n各专业平均体重:")
|
|
||||||
for major, weight in avg.items():
|
|
||||||
print(f"{major}: {weight:.1f}kg")
|
|
||||||
else:
|
|
||||||
print("无效的选择!")
|
|
||||||
input("\n按任意键继续...")
|
|
||||||
elif choice == "5":
|
|
||||||
break
|
|
||||||
else:
|
|
||||||
print("无效的选择,请重新输入!")
|
|
||||||
input("按任意键继续...")
|
|
||||||
|
|
||||||
def _clear_all_data(self):
|
|
||||||
"""清空所有学生数据"""
|
|
||||||
confirm = input("\n警告: 这将删除所有学生数据!确定要继续吗?(y/n): ").strip().lower()
|
|
||||||
if confirm == 'y':
|
|
||||||
if self.service.clear_all_data():
|
|
||||||
print("\n所有学生数据已清空!")
|
|
||||||
else:
|
|
||||||
print("\n清空数据失败!")
|
|
||||||
else:
|
|
||||||
print("\n操作已取消")
|
|
||||||
input("\n按任意键返回主菜单...")
|
|
||||||
|
|
@ -1,202 +0,0 @@
|
|||||||
import re
|
|
||||||
from datetime import date
|
|
||||||
from typing import Optional, Tuple
|
|
||||||
|
|
||||||
|
|
||||||
def validate_id_card(id_card: str) -> bool:
|
|
||||||
"""
|
|
||||||
Validate a Chinese ID card number (18 digits)
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id_card: ID card number string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
bool: True if valid, False otherwise
|
|
||||||
"""
|
|
||||||
if not isinstance(id_card, str) or len(id_card) != 18:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Check basic format (first 17 digits, last digit can be digit or X)
|
|
||||||
if not re.match(r'^\d{17}[\dXx]$', id_card):
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Calculate checksum
|
|
||||||
factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
|
||||||
checksum_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
|
||||||
|
|
||||||
total = 0
|
|
||||||
for i in range(17):
|
|
||||||
try:
|
|
||||||
total += int(id_card[i]) * factors[i]
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
calculated_checksum = checksum_map[total % 11]
|
|
||||||
if id_card[-1].upper() != calculated_checksum:
|
|
||||||
return False
|
|
||||||
|
|
||||||
# Validate birthdate
|
|
||||||
try:
|
|
||||||
birth_date = extract_birthday_from_id_card(id_card)
|
|
||||||
# Validate the date is reasonable
|
|
||||||
if birth_date.year < 1900 or birth_date > date.today():
|
|
||||||
return False
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def extract_birthday_from_id_card(id_card: str) -> date:
|
|
||||||
"""
|
|
||||||
Extract birthdate from ID card number
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id_card: Valid 18-digit ID card number
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
date: Birth date as date object
|
|
||||||
|
|
||||||
Raises:
|
|
||||||
ValueError: If ID card format is invalid
|
|
||||||
"""
|
|
||||||
if len(id_card) != 18:
|
|
||||||
raise ValueError("ID card number must be 18 digits")
|
|
||||||
|
|
||||||
birth_str = id_card[6:14] # YYYYMMDD format
|
|
||||||
|
|
||||||
try:
|
|
||||||
year = int(birth_str[0:4])
|
|
||||||
month = int(birth_str[4:6])
|
|
||||||
day = int(birth_str[6:8])
|
|
||||||
|
|
||||||
# Validate month and day
|
|
||||||
if month < 1 or month > 12 or day < 1 or day > 31:
|
|
||||||
raise ValueError("Invalid date components")
|
|
||||||
|
|
||||||
return date(year, month, day)
|
|
||||||
except (ValueError, IndexError) as e:
|
|
||||||
raise ValueError(f"Invalid birth date format in ID card: {birth_str}") from e
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_age(birth_date: date) -> int:
|
|
||||||
"""
|
|
||||||
Calculate current age based on birthdate
|
|
||||||
|
|
||||||
Args:
|
|
||||||
birth_date: Date of birth
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
int: Current age in years
|
|
||||||
"""
|
|
||||||
today = date.today()
|
|
||||||
age = today.year - birth_date.year
|
|
||||||
|
|
||||||
# Adjust if birthday hasn't occurred yet this year
|
|
||||||
if (today.month, today.day) < (birth_date.month, birth_date.day):
|
|
||||||
age -= 1
|
|
||||||
|
|
||||||
return age
|
|
||||||
|
|
||||||
|
|
||||||
def get_gender_from_id_card(id_card: str) -> Optional[bool]:
|
|
||||||
"""
|
|
||||||
Get gender from ID card number
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id_card: ID card number string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Optional[bool]:
|
|
||||||
True for male,
|
|
||||||
False for female,
|
|
||||||
None if cannot determine
|
|
||||||
"""
|
|
||||||
if len(id_card) not in (15, 18):
|
|
||||||
return None
|
|
||||||
|
|
||||||
# For 18-digit ID: gender is 17th digit (index 16)
|
|
||||||
# For 15-digit ID: gender is last digit
|
|
||||||
gender_digit_pos = 16 if len(id_card) == 18 else -1
|
|
||||||
|
|
||||||
try:
|
|
||||||
gender_digit = int(id_card[gender_digit_pos])
|
|
||||||
return gender_digit % 2 == 1 # Odd for male, even for female
|
|
||||||
except (ValueError, IndexError):
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def get_region_from_id_card(id_card: str) -> Tuple[str, str]:
|
|
||||||
"""
|
|
||||||
Get region information from ID card number
|
|
||||||
|
|
||||||
Args:
|
|
||||||
id_card: ID card number string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
Tuple[str, str]: (province, city) names
|
|
||||||
"""
|
|
||||||
if len(id_card) not in (15, 18):
|
|
||||||
return "未知", "未知"
|
|
||||||
|
|
||||||
# First 6 digits represent region code
|
|
||||||
region_code = id_card[:6]
|
|
||||||
|
|
||||||
# Simplified region mapping (in a real app, use a complete database)
|
|
||||||
region_map = {
|
|
||||||
'110000': ('北京市', '北京市'),
|
|
||||||
'110100': ('北京市', '北京市'),
|
|
||||||
'310000': ('上海市', '上海市'),
|
|
||||||
'310100': ('上海市', '上海市'),
|
|
||||||
'440000': ('广东省', ''),
|
|
||||||
'510000': ('四川省', ''),
|
|
||||||
'510700': ('四川省', '绵阳市'),
|
|
||||||
'510703': ('四川省', '绵阳市'),
|
|
||||||
'510704': ('四川省', '绵阳市'),
|
|
||||||
'510705': ('四川省', '绵阳市'),
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# Add more regions as needed...
|
|
||||||
}
|
|
||||||
|
|
||||||
return region_map.get(region_code, ("未知", "未知"))
|
|
||||||
|
|
||||||
|
|
||||||
def generate_test_id_card() -> str:
|
|
||||||
"""
|
|
||||||
Generate a valid format ID card number for testing
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
str: A syntactically valid but fake ID card number
|
|
||||||
"""
|
|
||||||
import random
|
|
||||||
|
|
||||||
# Region code (random Beijing district)
|
|
||||||
region = '110' + str(random.randint(100, 199))[:3]
|
|
||||||
|
|
||||||
# Birth_date (18-60 years old)
|
|
||||||
birth_year = date.today().year - random.randint(18, 60)
|
|
||||||
birth_date = f"{birth_year}{random.randint(1, 12):02d}{random.randint(1, 28):02d}"
|
|
||||||
|
|
||||||
# Sequence number
|
|
||||||
seq_num = f"{random.randint(0, 999):03d}"
|
|
||||||
|
|
||||||
# First 17 digits
|
|
||||||
first_17 = region + birth_date + seq_num
|
|
||||||
|
|
||||||
# Calculate checksum
|
|
||||||
factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
|
|
||||||
checksum_map = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
|
|
||||||
|
|
||||||
total = sum(int(first_17[i]) * factors[i] for i in range(17))
|
|
||||||
checksum = checksum_map[total % 11]
|
|
||||||
|
|
||||||
return first_17 + checksum
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in new issue