学生信息管理系统 #1

Open
ptuaqkmj6 wants to merge 0 commits from master into main

3
.idea/.gitignore vendored

@ -0,0 +1,3 @@
# 默认忽略的文件
/shelf/
/workspace.xml

@ -0,0 +1,8 @@
<?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>

@ -0,0 +1,12 @@
<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="s.args" />
</list>
</option>
</inspection_tool>
</profile>
</component>

@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.11" />
</component>
</project>

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/PythonProject1.iml" filepath="$PROJECT_DIR$/.idea/PythonProject1.iml" />
</modules>
</component>
</project>

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component>
</project>

@ -0,0 +1,304 @@
from typing import List, Optional
from ..dal.student_dal import IStudentDAL
from ..model.student import Student
from ..util.validator import Validator
class StudentBLL:
"""学生信息管理系统的业务逻辑层,处理学生信息的业务逻辑和数据验证"""
def __init__(self, dal: IStudentDAL):
"""
初始化业务逻辑层
Args:
dal: 数据访问层对象实现IStudentDAL接口
"""
self.dal = dal
def add_student(self, student: Student) -> (bool, str):
"""
添加学生信息包含完整的数据验证流程
Args:
student: 要添加的学生对象
Returns:
元组(操作是否成功, 操作结果信息)
"""
# 验证学生基本信息格式
if not Validator.validate_id_card(student.id_card):
return False, "身份证号码格式不正确"
if not Validator.validate_stu_id(student.stu_id):
return False, "学号格式不正确"
if not Validator.validate_name(student.name):
return False, "姓名格式不正确必须为2-20个中文字符"
if student.height is not None and not Validator.validate_height(student.height):
return False, "身高必须在50-250cm之间"
if student.weight is not None and not Validator.validate_weight(student.weight):
return False, "体重必须在5-300kg之间"
if student.email and not Validator.validate_email(student.email):
return False, "电子邮箱格式不正确"
if student.phone and not Validator.validate_phone(student.phone):
return False, "手机号码格式不正确"
# 验证入学日期与出生日期的逻辑关系
if student.enrollment_date and student.birthday:
if not Validator.validate_enrollment_date(student.enrollment_date, student.birthday):
return False, "入学日期不能早于出生日期"
# 检查数据唯一性
if self.dal.get_by_id(student.id_card):
return False, "该身份证号已存在"
if self.dal.get_by_stu_id(student.stu_id):
return False, "该学号已存在"
# 执行数据添加操作
if self.dal.add(student):
return True, "学生添加成功"
else:
return False, "学生添加失败"
def update_student(self, student: Student) -> (bool, str):
"""
更新学生信息包含与添加操作相同的数据验证流程
Args:
student: 包含更新信息的学生对象通过id_card匹配记录
Returns:
元组(操作是否成功, 操作结果信息)
"""
# 重复添加操作中的数据验证逻辑
if not Validator.validate_id_card(student.id_card):
return False, "身份证号码格式不正确"
if not Validator.validate_stu_id(student.stu_id):
return False, "学号格式不正确"
if not Validator.validate_name(student.name):
return False, "姓名格式不正确必须为2-20个中文字符"
if student.height is not None and not Validator.validate_height(student.height):
return False, "身高必须在50-250cm之间"
if student.weight is not None and not Validator.validate_weight(student.weight):
return False, "体重必须在5-300kg之间"
if student.email and not Validator.validate_email(student.email):
return False, "电子邮箱格式不正确"
if student.phone and not Validator.validate_phone(student.phone):
return False, "手机号码格式不正确"
if student.enrollment_date and student.birthday:
if not Validator.validate_enrollment_date(student.enrollment_date, student.birthday):
return False, "入学日期不能早于出生日期"
# 执行数据更新操作
if self.dal.update(student):
return True, "学生信息更新成功"
else:
return False, "学生信息更新失败,未找到该学生"
def delete_student_by_id(self, id_card: str) -> bool:
"""
根据身份证号删除学生记录
Args:
id_card: 要删除学生的身份证号
Returns:
操作是否成功
"""
return self.dal.delete_by_id(id_card)
def delete_student_by_stu_id(self, stu_id: str) -> bool:
"""
根据学号删除学生记录
Args:
stu_id: 要删除学生的学号
Returns:
操作是否成功
"""
return self.dal.delete_by_stu_id(stu_id)
def get_student_by_id(self, id_card: str) -> Optional[Student]:
"""
根据身份证号查询学生信息
Args:
id_card: 要查询学生的身份证号
Returns:
匹配的学生对象未找到返回None
"""
return self.dal.get_by_id(id_card)
def get_student_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""
根据学号查询学生信息
Args:
stu_id: 要查询学生的学号
Returns:
匹配的学生对象未找到返回None
"""
return self.dal.get_by_stu_id(stu_id)
def get_all_students(self) -> List[Student]:
"""
获取系统中所有学生记录
Returns:
包含所有学生对象的列表
"""
return self.dal.get_all()
def search_students_by_name(self, name: str) -> List[Student]:
"""
根据姓名模糊查询学生信息
Args:
name: 要查询的姓名关键词
Returns:
包含匹配学生对象的列表
"""
return self.dal.search_by_name(name)
def search_students_by_class(self, class_name: str) -> List[Student]:
"""
根据班级模糊查询学生信息
Args:
class_name: 要查询的班级关键词
Returns:
包含匹配学生对象的列表
"""
return self.dal.search_by_class(class_name)
def search_students_by_major(self, major: str) -> List[Student]:
"""
根据专业模糊查询学生信息
Args:
major: 要查询的专业关键词
Returns:
包含匹配学生对象的列表
"""
return self.dal.search_by_major(major)
def get_total_students(self) -> int:
"""
获取系统中学生总数
Returns:
学生总人数
"""
return len(self.get_all_students())
def get_students_by_major(self) -> dict:
"""
统计各专业学生分布情况
Returns:
字典键为专业名称值为该专业学生人数
"""
major_count = {}
for student in self.get_all_students():
if student.major:
major_count[student.major] = major_count.get(student.major, 0) + 1
return major_count
def calculate_average_height(self, group_by: str = None, group_value: str = None) -> float:
"""
计算学生平均身高支持按班级或专业分组计算
Args:
group_by: 分组依据可选'class''major'不分组时为None
group_value: 分组值如具体班级名称或专业名称
Returns:
平均身高单位厘米无有效数据时返回0
"""
students = self.get_all_students()
# 按指定条件筛选学生群体
if group_by == 'class':
students = [s for s in students if s.class_name == group_value]
elif group_by == 'major':
students = [s for s in students if s.major == group_value]
# 过滤有效身高数据并计算平均值
heights = [s.height for s in students if s.height is not None]
return sum(heights) / len(heights) if heights else 0
def calculate_average_weight(self, group_by: str = None, group_value: str = None) -> float:
"""
计算学生平均体重支持按班级或专业分组计算
Args:
group_by: 分组依据可选'class''major'不分组时为None
group_value: 分组值如具体班级名称或专业名称
Returns:
平均体重单位千克无有效数据时返回0
"""
students = self.get_all_students()
# 按指定条件筛选学生群体
if group_by == 'class':
students = [s for s in students if s.class_name == group_value]
elif group_by == 'major':
students = [s for s in students if s.major == group_value]
# 过滤有效体重数据并计算平均值
weights = [s.weight for s in students if s.weight is not None]
return sum(weights) / len(weights) if weights else 0
def get_gender_ratio(self) -> dict:
"""
统计学生性别比例
Returns:
包含以下键的字典:
- total: 总人数
- male: 男生人数
- female: 女生人数
- male_ratio: 男生比例
- female_ratio: 女生比例
"""
total = 0
male_count = 0
female_count = 0
# 遍历统计性别分布
for student in self.get_all_students():
if student.gender is not None:
total += 1
if student.gender:
male_count += 1
else:
female_count += 1
# 计算比例并返回结果
return {
'total': total,
'male': male_count,
'female': female_count,
'male_ratio': male_count / total if total > 0 else 0,
'female_ratio': female_count / total if total > 0 else 0
}

@ -0,0 +1,192 @@
import csv
from typing import List, Optional
from .student_dal import IStudentDAL
from ..model.student import Student
from datetime import datetime
class CsvStudentDAL(IStudentDAL):
"""学生信息的CSV文件存储实现"""
def __init__(self, file_path: str = 'students.csv'):
self.file_path = file_path
self.headers = ['name', 'id_card', 'stu_id', 'gender', 'height', 'weight',
'enrollment_date', 'class_name', 'major', 'email', 'phone']
# 确保文件存在
self._ensure_file_exists()
def _ensure_file_exists(self):
"""确保CSV文件存在并包含表头"""
try:
with open(self.file_path, 'r', encoding='utf-8') as file:
reader = csv.reader(file)
if not any(reader): # 文件为空
self._write_headers()
except FileNotFoundError:
self._write_headers()
def _write_headers(self):
"""写入CSV文件表头"""
with open(self.file_path, 'w', encoding='utf-8', newline='') as file:
writer = csv.DictWriter(file, fieldnames=self.headers)
writer.writeheader()
def _student_to_row(self, student: Student) -> dict:
"""将Student对象转换为CSV行数据"""
return {
'name': student.name,
'id_card': student.id_card,
'stu_id': student.stu_id,
'gender': student.gender,
'height': student.height,
'weight': student.weight,
'enrollment_date': str(student.enrollment_date) if student.enrollment_date else '',
'class_name': student.class_name,
'major': student.major,
'email': student.email,
'phone': student.phone
}
def _row_to_student(self, row: dict) -> Student:
"""将CSV行数据转换为Student对象"""
# 处理日期类型将字符串转换为datetime.date对象
enrollment_date = row['enrollment_date']
if enrollment_date:
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date()
# 处理布尔类型,将字符串转换为布尔值
gender = row['gender']
if gender:
gender = gender.lower() == 'true'
# 处理数值类型,将字符串转换为数值
height = int(row['height']) if row['height'] else None
weight = float(row['weight']) if row['weight'] else None
# 创建并返回学生对象
return Student(
name=row['name'],
id_card=row['id_card'],
stu_id=row['stu_id'],
gender=gender,
height=height,
weight=weight,
enrollment_date=enrollment_date,
class_name=row['class_name'],
major=row['major'],
email=row['email'],
phone=row['phone']
)
def get_by_id(self, id_card: str) -> Optional[Student]:
"""根据身份证号获取学生信息"""
with open(self.file_path, 'r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
if row['id_card'] == id_card:
return self._row_to_student(row)
return None
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""根据学号获取学生信息"""
with open(self.file_path, 'r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
if row['stu_id'] == stu_id:
return self._row_to_student(row)
return None
def get_all(self) -> List[Student]:
"""获取所有学生信息"""
students = []
with open(self.file_path, 'r', encoding='utf-8') as file:
reader = csv.DictReader(file)
for row in reader:
students.append(self._row_to_student(row))
return students
def add(self, student: Student) -> bool:
"""添加学生信息"""
# 检查学号和身份证号是否已存在
if self.get_by_id(student.id_card) or self.get_by_stu_id(student.stu_id):
return False
# 将学生信息追加到CSV文件
with open(self.file_path, 'a', encoding='utf-8', newline='') as file:
writer = csv.DictWriter(file, fieldnames=self.headers)
writer.writerow(self._student_to_row(student))
return True
def update(self, student: Student) -> bool:
"""
更新学生信息
通过身份证号匹配要更新的记录
"""
students = self.get_all() # 获取所有学生信息
updated = False
# 重写整个CSV文件替换匹配的学生记录
with open(self.file_path, 'w', encoding='utf-8', newline='') as file:
writer = csv.DictWriter(file, fieldnames=self.headers)
writer.writeheader()# 写入表头
for s in students:
if s.id_card == student.id_card: # 找到匹配的学生记录
writer.writerow(self._student_to_row(student))
updated = True
else:
writer.writerow(self._student_to_row(s)) # 保持原有记录不变
return updated
def delete_by_id(self, id_card: str) -> bool:
"""
根据身份证号删除学生信息
通过重写整个文件排除要删除的记录
"""
students = self.get_all() # 获取所有学生信息
deleted = False
# 重写整个CSV文件排除要删除的学生记录
with open(self.file_path, 'w', encoding='utf-8', newline='') as file:
writer = csv.DictWriter(file, fieldnames=self.headers)
writer.writeheader() # 写入表头
for s in students:
if s.id_card == id_card: # 找到匹配的学生记录
deleted = True # 标记已删除
else:
writer.writerow(self._student_to_row(s)) # 保留其他记录
return deleted
def delete_by_stu_id(self, stu_id: str) -> bool:
"""
根据学号删除学生信息
通过重写整个文件排除要删除的记录
"""
students = self.get_all() # 获取所有学生信息
deleted = False
# 重写整个CSV文件排除要删除的学生记录
with open(self.file_path, 'w', encoding='utf-8', newline='') as file:
writer = csv.DictWriter(file, fieldnames=self.headers)
writer.writeheader() # 写入表头
for s in students:
if s.stu_id == stu_id: # 找到匹配的学生记录
deleted = True # 标记已删除
else:
writer.writerow(self._student_to_row(s)) # 保留其他记录
return deleted
def search_by_name(self, name: str) -> List[Student]:
"""根据姓名模糊查询学生信息"""
return [s for s in self.get_all() if name in s.name]
def search_by_class(self, class_name: str) -> List[Student]:
"""根据班级模糊查询学生信息"""
return [s for s in self.get_all() if class_name in (s.class_name or '')]
def search_by_major(self, major: str) -> List[Student]:
"""根据专业模糊查询学生信息"""
return [s for s in self.get_all() if major in (s.major or '')]

@ -0,0 +1,226 @@
import json
from typing import List, Optional
from .student_dal import IStudentDAL
from ..model.student import Student
from datetime import datetime
class JsonStudentDAL(IStudentDAL):
"""学生信息的JSON文件存储实现实现了IStudentDAL接口提供基于JSON文件的学生数据增删改查功能"""
def __init__(self, file_path: str = 'students.json'):
"""初始化JSON数据访问层
:param file_path: JSON数据文件路径默认使用当前目录下的students.json"""
self.file_path = file_path
self._ensure_file_exists() # 调用私有方法确保数据文件存在,避免后续操作报错
def _ensure_file_exists(self):
"""确保JSON数据文件存在且格式正确
处理两种异常情况
1. 文件不存在时创建空JSON数组文件
2. 文件存在但非JSON格式时如损坏重建空文件"""
try:
with open(self.file_path, 'r', encoding='utf-8') as file:
json.load(file) # 尝试解析文件验证是否为合法JSON
except (FileNotFoundError, json.JSONDecodeError):
# 异常处理:文件不存在或解析失败时,创建包含空数组的新文件
with open(self.file_path, 'w', encoding='utf-8') as file:
json.dump([], file) # 写入空数组,确保文件格式正确
def _student_to_dict(self, student: Student) -> dict:
"""将Student实体对象转换为JSON兼容的字典格式
:param student: 学生实体对象
:return: 可序列化的字典日期类型转换为字符串
处理逻辑
- 日期字段enrollment_date转为%Y-%m-%d格式字符串
- None值保持为None确保JSON序列化合法"""
return {
'name': student.name, # 直接存储字符串属性
'id_card': student.id_card, # 身份证号(字符串)
'stu_id': student.stu_id, # 学号(字符串)
'gender': student.gender, # 性别(布尔值)
'height': student.height, # 身高(整数)
'weight': student.weight, # 体重(整数)
# 日期转字符串:若存在日期则格式化为%Y-%m-%d否则保持None
'enrollment_date': str(student.enrollment_date) if student.enrollment_date else None,
'class_name': student.class_name, # 班级(字符串)
'major': student.major, # 专业(字符串)
'email': student.email, # 邮箱(字符串)
'phone': student.phone # 电话(字符串)
}
def _dict_to_student(self, data: dict) -> Student:
"""将JSON字典转换为Student实体对象
:param data: 从JSON文件读取的字典数据
:return: 初始化的Student对象字符串日期转换为date对象
处理逻辑
- enrollment_date字段从字符串解析为datetime.date对象
- 缺失字段使用None或默认值初始化"""
enrollment_date = data.get('enrollment_date')
if enrollment_date:
# 字符串转日期使用strptime解析为datetime对象再提取date部分
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date()
return Student(
name=data.get('name'), # 获取姓名缺失时为None
id_card=data.get('id_card'), # 获取身份证号
stu_id=data.get('stu_id'), # 获取学号
gender=data.get('gender'), # 获取性别
height=data.get('height'), # 获取身高
weight=data.get('weight'), # 获取体重
enrollment_date=enrollment_date, # 转换后的入学日期
class_name=data.get('class_name'), # 获取班级
major=data.get('major'), # 获取专业
email=data.get('email'), # 获取邮箱
phone=data.get('phone') # 获取电话
)
def _load_data(self) -> List[dict]:
"""从JSON文件加载学生数据
:return: 包含学生字典的列表
封装文件读取逻辑统一处理编码和JSON解析"""
with open(self.file_path, 'r', encoding='utf-8') as file:
return json.load(file) # 读取文件并解析为Python列表
def _save_data(self, data: List[dict]):
"""将学生数据保存至JSON文件
:param data: 包含学生字典的列表
保存逻辑
- ensure_ascii=False确保中文正常存储
- indent=2添加缩进提升文件可读性"""
with open(self.file_path, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=2) # 写入文件禁用ASCII转义
def get_by_id(self, id_card: str) -> Optional[Student]:
"""根据身份证号查询学生信息
:param id_card: 待查询的身份证号
:return: 匹配的Student对象或None
实现逻辑
- 加载全部数据遍历匹配
- 找到后转换为Student对象返回"""
for data in self._load_data():
if data.get('id_card') == id_card: # 精确匹配身份证号
return self._dict_to_student(data) # 转换为实体对象
return None # 未找到返回None
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""根据学号查询学生信息逻辑同get_by_id
:param stu_id: 待查询的学号
:return: 匹配的Student对象或None"""
for data in self._load_data():
if data.get('stu_id') == stu_id: # 精确匹配学号
return self._dict_to_student(data)
return None
def get_all(self) -> List[Student]:
"""获取所有学生信息
:return: 包含所有Student对象的列表
实现逻辑
- 加载全部字典数据
- 逐个转换为Student对象"""
return [self._dict_to_student(data) for data in self._load_data()] # 列表推导式批量转换
def add(self, student: Student) -> bool:
"""添加新学生信息
:param student: 待添加的Student对象
:return: 添加成功返回True失败返回False
业务逻辑
1. 先校验身份证号和学号的唯一性
2. 校验通过后追加数据并保存
3. 唯一性冲突时返回False"""
# 检查身份证号或学号是否已存在
if self.get_by_id(student.id_card) or self.get_by_stu_id(student.stu_id):
return False # 已存在相同标识,添加失败
data = self._load_data() # 加载当前数据
data.append(self._student_to_dict(student)) # 转换并追加
self._save_data(data) # 保存更新后的数据
return True # 添加成功
def update(self, student: Student) -> bool:
"""更新学生信息
:param student: 包含更新后信息的Student对象
:return: 更新成功返回True失败返回False
实现逻辑
1. 按身份证号定位记录
2. 找到后替换为新数据
3. 未找到或无变更时返回False"""
data = self._load_data() # 加载当前数据
updated = False # 标记是否更新
for i, item in enumerate(data):
if item.get('id_card') == student.id_card: # 匹配身份证号
data[i] = self._student_to_dict(student) # 替换为新数据
updated = True
break
if updated:
self._save_data(data) # 保存更新后的数据
return updated # 返回是否更新成功
def delete_by_id(self, id_card: str) -> bool:
"""根据身份证号删除学生信息
:param id_card: 待删除学生的身份证号
:return: 删除成功返回True失败返回False
实现逻辑
1. 过滤掉目标身份证号的记录
2. 比较删除前后数据长度
3. 长度变化时保存并返回True"""
data = self._load_data() # 加载当前数据
original_length = len(data) # 记录原始长度
# 过滤掉目标身份证号的记录
data = [item for item in data if item.get('id_card') != id_card]
if len(data) < original_length: # 长度减少表示删除成功
self._save_data(data) # 保存更新后的数据
return True
return False # 未找到目标记录
def delete_by_stu_id(self, stu_id: str) -> bool:
"""根据学号删除学生信息逻辑同delete_by_id
:param stu_id: 待删除学生的学号
:return: 删除成功返回True失败返回False"""
data = self._load_data()
original_length = len(data)
data = [item for item in data if item.get('stu_id') != stu_id]
if len(data) < original_length:
self._save_data(data)
return True
return False
def search_by_name(self, name: str) -> List[Student]:
"""根据姓名模糊查询学生
:param name: 待查询的姓名片段
:return: 包含匹配学生的列表
实现逻辑
- 遍历所有数据
- 使用in操作符实现包含匹配
- 处理name字段为None的情况"""
return [
self._dict_to_student(data) for data in self._load_data()
if name in data.get('name', '') # data.get('name', '')避免KeyError空字符串确保in操作合法
]
def search_by_class(self, class_name: str) -> List[Student]:
"""根据班级模糊查询学生
:param class_name: 待查询的班级片段
:return: 包含匹配学生的列表
实现逻辑
- 处理class_name为None的情况替换为空字符串
- 使用in操作符实现包含匹配"""
return [
self._dict_to_student(data) for data in self._load_data()
if class_name in (data.get('class_name') or '') # data.get('class_name')为None时替换为空字符串
]
def search_by_major(self, major: str) -> List[Student]:
"""根据专业模糊查询学生逻辑同search_by_class
:param major: 待查询的专业片段
:return: 包含匹配学生的列表"""
return [
self._dict_to_student(data) for data in self._load_data()
if major in (data.get('major') or '') # 处理major为None的情况
]

@ -0,0 +1,234 @@
import sqlite3
from typing import List, Optional
from .student_dal import IStudentDAL
from ..model.student import Student
from datetime import datetime
class SQLiteStudentDAL(IStudentDAL):
"""学生信息的SQLite数据库存储实现实现了IStudentDAL接口提供基于SQLite的学生数据持久化功能"""
def __init__(self, db_path: str = 'students.db'):
"""初始化SQLite数据访问层
:param db_path: 数据库文件路径默认使用当前目录下的students.db"""
self.db_path = db_path
self._create_table() # 确保学生表存在,不存在则创建
def _create_table(self):
"""创建学生信息表(如果不存在)
表结构设计
- id自增主键
- id_card/stu_id设置唯一约束确保数据唯一性
- enrollment_date存储为YYYY-MM-DD格式字符串
- gender布尔值存储为整数0/1"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS students (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL, -- 学生姓名非空
id_card TEXT UNIQUE NOT NULL, -- 身份证号唯一且非空
stu_id TEXT UNIQUE NOT NULL, -- 学号唯一且非空
gender INTEGER, -- 性别0/1
height INTEGER, -- 身高厘米
weight REAL, -- 体重公斤
enrollment_date TEXT, -- 入学日期YYYY-MM-DD
class_name TEXT, -- 班级名称
major TEXT, -- 专业
email TEXT, -- 邮箱
phone TEXT -- 电话
)
''')
conn.commit() # 提交DDL语句
def _row_to_student(self, row: tuple) -> Optional[Student]:
"""将数据库查询结果行转换为Student对象
:param row: 查询结果元组
:return: 转换后的Student对象或None
处理逻辑
- 索引映射将数据库列索引映射到Student属性
- 类型转换日期字符串转date对象整数转布尔值"""
if not row: # 处理空结果
return None
# 处理日期类型从字符串转换为datetime.date
enrollment_date = row[7] # 注意索引7对应enrollment_date列
if enrollment_date:
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date()
# 处理布尔类型数据库中存储为0/1转换为True/False
gender = row[4] # 注意索引4对应gender列
if gender is not None:
gender = bool(gender)
# 构建并返回Student对象
return Student(
name=row[1], # 姓名
id_card=row[2], # 身份证号
stu_id=row[3], # 学号
gender=gender, # 性别(布尔值)
height=row[4], # 身高
weight=row[5], # 体重
enrollment_date=enrollment_date, # 入学日期
class_name=row[8], # 班级
major=row[9], # 专业
email=row[10], # 邮箱
phone=row[11] # 电话
)
def get_by_id(self, id_card: str) -> Optional[Student]:
"""根据身份证号查询学生信息
:param id_card: 待查询的身份证号
:return: 匹配的Student对象或None
实现逻辑
- 使用参数化查询防止SQL注入
- 通过id_card唯一索引快速定位记录"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM students WHERE id_card = ?", (id_card,))
row = cursor.fetchone() # 获取单条记录
return self._row_to_student(row) # 转换为Student对象
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""根据学号查询学生信息逻辑同get_by_id
:param stu_id: 待查询的学号
:return: 匹配的Student对象或None"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM students WHERE stu_id = ?", (stu_id,))
row = cursor.fetchone()
return self._row_to_student(row)
def get_all(self) -> List[Student]:
"""获取所有学生信息
:return: 包含所有学生的列表
实现逻辑
- 查询全量数据
- 批量转换为Student对象"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM students")
rows = cursor.fetchall() # 获取所有记录
return [self._row_to_student(row) for row in rows] # 列表推导式批量转换
def add(self, student: Student) -> bool:
"""添加学生信息
:param student: 待添加的Student对象
:return: 添加成功返回True失败返回False
业务逻辑
1. 使用事务确保数据一致性
2. 自动处理日期类型转换
3. 利用数据库唯一约束防止重复数据
4. 异常处理IntegrityError表示违反唯一性约束"""
try:
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute(
"""INSERT INTO students
(name, id_card, stu_id, gender, height, weight, enrollment_date, class_name, major, email, phone)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
student.name,
student.id_card,
student.stu_id,
student.gender,
student.height,
student.weight,
str(student.enrollment_date) if student.enrollment_date else None, # 日期转字符串
student.class_name,
student.major,
student.email,
student.phone
)
)
conn.commit() # 提交事务
return True
except sqlite3.IntegrityError: # 违反唯一约束(身份证号或学号重复)
return False
def update(self, student: Student) -> bool:
"""更新学生信息
:param student: 包含更新信息的Student对象
:return: 更新成功返回True失败返回False
实现逻辑
1. 以身份证号作为唯一标识
2. 返回受影响的行数判断操作是否成功
3. 自动处理日期类型转换"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute(
"""UPDATE students
SET name=?, gender=?, height=?, weight=?, enrollment_date=?,
class_name=?, major=?, email=?, phone=?
WHERE id_card=?""",
(
student.name,
student.gender,
student.height,
student.weight,
str(student.enrollment_date) if student.enrollment_date else None, # 日期转字符串
student.class_name,
student.major,
student.email,
student.phone,
student.id_card # 条件:根据身份证号更新
)
)
conn.commit()
return cursor.rowcount > 0 # 判断是否有记录被更新
def delete_by_id(self, id_card: str) -> bool:
"""根据身份证号删除学生信息
:param id_card: 待删除学生的身份证号
:return: 删除成功返回True失败返回False
实现逻辑
- 通过受影响行数判断删除是否成功
- 使用事务确保操作原子性"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM students WHERE id_card = ?", (id_card,))
conn.commit()
return cursor.rowcount > 0 # 判断是否有记录被删除
def delete_by_stu_id(self, stu_id: str) -> bool:
"""根据学号删除学生信息逻辑同delete_by_id
:param stu_id: 待删除学生的学号
:return: 删除成功返回True失败返回False"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("DELETE FROM students WHERE stu_id = ?", (stu_id,))
conn.commit()
return cursor.rowcount > 0
def search_by_name(self, name: str) -> List[Student]:
"""根据姓名模糊查询学生信息
:param name: 待查询的姓名片段
:return: 包含匹配学生的列表
实现逻辑
- 使用LIKE '%name%'实现模糊匹配
- 支持空字符串查询返回所有记录"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM students WHERE name LIKE ?", ('%' + name + '%',))
rows = cursor.fetchall()
return [self._row_to_student(row) for row in rows]
def search_by_class(self, class_name: str) -> List[Student]:
"""根据班级模糊查询学生信息逻辑同search_by_name
:param class_name: 待查询的班级片段
:return: 包含匹配学生的列表"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM students WHERE class_name LIKE ?", ('%' + class_name + '%',))
rows = cursor.fetchall()
return [self._row_to_student(row) for row in rows]
def search_by_major(self, major: str) -> List[Student]:
"""根据专业模糊查询学生信息逻辑同search_by_name
:param major: 待查询的专业片段
:return: 包含匹配学生的列表"""
with sqlite3.connect(self.db_path) as conn:
cursor = conn.cursor()
cursor.execute("SELECT * FROM students WHERE major LIKE ?", ('%' + major + '%',))
rows = cursor.fetchall()
return [self._row_to_student(row) for row in rows]

@ -0,0 +1,57 @@
from abc import ABC, abstractmethod
from typing import List, Optional
from ..model.student import Student
class IStudentDAL(ABC):
"""学生信息数据访问层接口"""
@abstractmethod
def get_by_id(self, id_card: str) -> Optional[Student]:
"""根据身份证号获取学生信息返回Student对象或None"""
pass
@abstractmethod
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""根据学号获取学生信息返回Student对象或None"""
pass
@abstractmethod
def get_all(self) -> List[Student]:
"""获取所有学生信息返回Student对象列表"""
pass
@abstractmethod
def add(self, student: Student) -> bool:
"""添加学生信息,返回操作结果"""
pass
@abstractmethod
def update(self, student: Student) -> bool:
"""更新学生信息,返回操作结果"""
pass
@abstractmethod
def delete_by_id(self, id_card: str) -> bool:
"""根据身份证号删除学生信息,返回操作结果"""
pass
@abstractmethod
def delete_by_stu_id(self, stu_id: str) -> bool:
"""根据学号删除学生信息,返回操作结果"""
pass
@abstractmethod
def search_by_name(self, name: str) -> List[Student]:
"""根据姓名模糊查询学生信息"""
pass
@abstractmethod
def search_by_class(self, class_name: str) -> List[Student]:
"""根据班级模糊查询学生信息"""
pass
@abstractmethod
def search_by_major(self, major: str) -> List[Student]:
"""根据专业模糊查询学生信息"""
pass

@ -0,0 +1,58 @@
# main.py
import tkinter as tk
from student.dal.csv_student_dal import CsvStudentDAL
from student.dal.json_student_dal import JsonStudentDAL
from student.dal.sqlite_student_dal import SQLiteStudentDAL
from student.bll.student_bll import StudentBLL
from student.ui.gui_ui import StudentGUI # 修改导入
import sys
from pprint import pprint
pprint(sys.path) # 打印模块搜索路径
def main():
# 创建主窗口
root = tk.Tk()
# 选择数据存储方式
storage_choice = tk.StringVar(value="3")
# 创建选择对话框
dialog = tk.Toplevel(root)
dialog.title("选择数据存储方式")
dialog.geometry("300x200")
dialog.transient(root)
dialog.grab_set()
tk.Label(dialog, text="请选择数据存储方式:").pack(pady=10)
tk.Radiobutton(dialog, text="CSV文件", variable=storage_choice, value="1").pack(anchor=tk.W, padx=20)
tk.Radiobutton(dialog, text="JSON文件", variable=storage_choice, value="2").pack(anchor=tk.W, padx=20)
tk.Radiobutton(dialog, text="SQLite数据库", variable=storage_choice, value="3").pack(anchor=tk.W, padx=20)
def on_ok():
dialog.destroy()
tk.Button(dialog, text="确定", command=on_ok).pack(pady=20)
# 等待对话框关闭
root.wait_window(dialog)
# 根据选择初始化数据访问层
if storage_choice.get() == '1':
dal = CsvStudentDAL()
elif storage_choice.get() == '2':
dal = JsonStudentDAL()
else:
dal = SQLiteStudentDAL()
# 初始化业务逻辑层和用户界面
bll = StudentBLL(dal)
app = StudentGUI(root, bll)
# 运行主循环
root.mainloop()
if __name__ == "__main__":
main()

@ -0,0 +1,89 @@
from datetime import date, datetime
from typing import Optional, Union
class Student:
"""学生实体类,包含学生的基本信息和相关属性"""
def __init__(self,
name: str, # 姓名,非空字符串
id_card: str, # 身份证号, 唯一标识学生,非空字符串
stu_id: str, # 学号, 唯一标识学生,非空字符串
gender: Optional[bool] = None, # 性别True为男False为女
height: Optional[int] = None, # 身高, 单位为cm
weight: Optional[float] = None, # 体重, 单位为kg,小数点后一位
enrollment_date: Optional[Union[date, str]] = None, # 入学日期
class_name: Optional[str] = None, # 班级名称
major: Optional[str] = None, # 专业名称
email: Optional[str] = None, # 电子邮箱
phone: 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
self.enrollment_date = enrollment_date
self.class_name = class_name
self.major = major
self.email = email
self.phone = phone
# 从身份证号码派生的属性
self._age = None
self._birthday = None
self._parse_id_card()
def _parse_id_card(self):
"""从身份证号码中提取出生日期和计算年龄"""
if not self.id_card or len(self.id_card) != 18:
return
try:
# 提取出生日期
birth_date_str = self.id_card[6:14]
self._birthday = datetime.strptime(birth_date_str, '%Y%m%d').date()
# 计算年龄
today = date.today()
self._age = today.year - self._birthday.year - (
(today.month, today.day) < (self._birthday.month, self._birthday.day))
# 从身份证第17位判断性别
if self.gender is None:
gender_digit = int(self.id_card[16])
self.gender = gender_digit % 2 == 1 # 奇数为男,偶数为女
except ValueError:
# 身份证号码格式错误
pass
@property
def age(self) -> int:
"""获取计算得到的年龄"""
return self._age
@property
def birthday(self) -> date:
"""获取从身份证提取的出生日期"""
return self._birthday
def to_dict(self) -> dict:
"""将学生对象转换为字典格式"""
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': str(self.enrollment_date) if self.enrollment_date else None,
'class_name': self.class_name,
'major': self.major,
'email': self.email,
'phone': self.phone,
'age': self.age,
'birthday': str(self.birthday) if self.birthday else None
}

@ -0,0 +1 @@
name,id_card,stu_id,gender,height,weight,enrollment_date,class_name,major,email,phone
1 name id_card stu_id gender height weight enrollment_date class_name major email phone

Binary file not shown.

@ -0,0 +1,350 @@
from datetime import date, datetime
from typing import List, Optional
from ..bll.student_bll import StudentBLL
from ..model.student import Student
class StudentConsoleUI:
"""学生信息管理系统的控制台用户界面"""
def __init__(self, bll: StudentBLL):
self.bll = bll
def display_menu(self):
"""显示主菜单"""
print("\n" + "=" * 50)
print("学生信息管理系统".center(50))
print("=" * 50)
print("1. 添加学生")
print("2. 删除学生")
print("3. 修改学生信息")
print("4. 查看学生详细信息")
print("5. 查询学生")
print("6. 统计功能")
print("7. 数据导入导出")
print("8. 退出系统")
print("=" * 50)
def run(self):
"""运行系统"""
while True:
self.display_menu()
choice = input("请输入您的选择: ")
if choice == '1':
self.add_student()
elif choice == '2':
self.delete_student()
elif choice == '3':
self.update_student()
elif choice == '4':
self.view_student_details()
elif choice == '5':
self.search_students()
elif choice == '6':
self.statistics_menu()
elif choice == '7':
self.import_export_menu()
elif choice == '8':
print("感谢使用学生信息管理系统,再见!")
break
else:
print("无效的选择,请重新输入!")
def add_student(self):
"""添加学生界面"""
print("\n添加学生")
print("-" * 50)
name = input("姓名: ")
id_card = input("身份证号: ")
stu_id = input("学号: ")
gender_input = input("性别 (男/女,可选): ")
gender = None
if gender_input:
gender = gender_input.lower() == ''
height = input("身高 (cm可选): ")
height = int(height) if height else None
weight = input("体重 (kg可选): ")
weight = float(weight) if weight else None
enrollment_date = input("入学日期 (YYYY-MM-DD可选): ")
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None
class_name = input("班级名称 (可选): ")
major = input("专业名称 (可选): ")
email = input("电子邮箱 (可选): ")
phone = input("联系电话 (可选): ")
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,
email=email,
phone=phone
)
success, message = self.bll.add_student(student)
if success:
print("添加成功!")
else:
print(f"添加失败: {message}")
def delete_student(self):
"""删除学生界面"""
print("\n删除学生")
print("-" * 50)
print("1. 根据身份证号删除")
print("2. 根据学号删除")
choice = input("请选择删除方式: ")
if choice == '1':
id_card = input("请输入要删除的学生身份证号: ")
if self.bll.delete_student_by_id(id_card):
print("删除成功!")
else:
print("删除失败,未找到该学生!")
elif choice == '2':
stu_id = input("请输入要删除的学生学号: ")
if self.bll.delete_student_by_stu_id(stu_id):
print("删除成功!")
else:
print("删除失败,未找到该学生!")
else:
print("无效的选择!")
def update_student(self):
"""修改学生信息界面"""
print("\n修改学生信息")
print("-" * 50)
id_card = input("请输入要修改的学生身份证号: ")
student = self.bll.get_student_by_id(id_card)
if not student:
print("未找到该学生!")
return
print(f"当前学生信息 - 姓名: {student.name}, 学号: {student.stu_id}")
name = input(f"姓名 ({student.name}): ") or student.name
stu_id = input(f"学号 ({student.stu_id}): ") or student.stu_id
gender_input = input(f"性别 ({'' if student.gender else '' if student.gender is not None else '未设置'}): ")
gender = student.gender
if gender_input:
gender = gender_input.lower() == ''
height = input(f"身高 ({student.height} cm): ")
height = int(height) if height else student.height
weight = input(f"体重 ({student.weight} kg): ")
weight = float(weight) if weight else student.weight
enrollment_date = input(f"入学日期 ({student.enrollment_date}): ")
enrollment_date = datetime.strptime(enrollment_date,
'%Y-%m-%d').date() if enrollment_date else student.enrollment_date
class_name = input(f"班级名称 ({student.class_name}): ") or student.class_name
major = input(f"专业名称 ({student.major}): ") or student.major
email = input(f"电子邮箱 ({student.email}): ") or student.email
phone = input(f"联系电话 ({student.phone}): ") or student.phone
student.name = name
student.stu_id = stu_id
student.gender = gender
student.height = height
student.weight = weight
student.enrollment_date = enrollment_date
student.class_name = class_name
student.major = major
student.email = email
student.phone = phone
success, message = self.bll.update_student(student)
if success:
print("修改成功!")
else:
print(f"修改失败: {message}")
def view_student_details(self):
"""查看学生详细信息界面"""
print("\n查看学生详细信息")
print("-" * 50)
print("1. 根据身份证号查询")
print("2. 根据学号查询")
choice = input("请选择查询方式: ")
if choice == '1':
id_card = input("请输入学生身份证号: ")
student = self.bll.get_student_by_id(id_card)
elif choice == '2':
stu_id = input("请输入学生学号: ")
student = self.bll.get_student_by_stu_id(stu_id)
else:
print("无效的选择!")
return
if not student:
print("未找到该学生!")
return
self._print_student_details(student)
def _print_student_details(self, student: Student):
"""打印学生详细信息"""
print("\n" + "-" * 50)
print(f"姓名: {student.name}")
print(f"身份证号: {student.id_card}")
print(f"学号: {student.stu_id}")
print(f"性别: {'' if student.gender else '' if student.gender is not None else '未设置'}")
print(f"年龄: {student.age if student.age is not None else '未知'}")
print(f"出生日期: {student.birthday if student.birthday else '未知'}")
print(f"身高: {student.height} cm" if student.height else "身高: 未设置")
print(f"体重: {student.weight} kg" if student.weight else "体重: 未设置")
print(f"入学日期: {student.enrollment_date}" if student.enrollment_date else "入学日期: 未设置")
print(f"班级: {student.class_name if student.class_name else '未设置'}")
print(f"专业: {student.major if student.major else '未设置'}")
print(f"电子邮箱: {student.email if student.email else '未设置'}")
print(f"联系电话: {student.phone if student.phone else '未设置'}")
print("-" * 50)
def search_students(self):
"""查询学生界面"""
print("\n查询学生")
print("-" * 50)
print("1. 按姓名查询")
print("2. 按班级查询")
print("3. 按专业查询")
choice = input("请选择查询方式: ")
if choice == '1':
keyword = input("请输入姓名关键词: ")
students = self.bll.search_students_by_name(keyword)
elif choice == '2':
keyword = input("请输入班级关键词: ")
students = self.bll.search_students_by_class(keyword)
elif choice == '3':
keyword = input("请输入专业关键词: ")
students = self.bll.search_students_by_major(keyword)
else:
print("无效的选择!")
return
if not students:
print("未找到匹配的学生!")
return
print(f"\n共找到 {len(students)} 名学生:")
for i, student in enumerate(students, 1):
print(
f"{i}. {student.name} - {student.stu_id} - {student.class_name if student.class_name else '未知班级'}")
view_choice = input("是否查看详情?(y/n): ")
if view_choice.lower() == 'y':
detail_index = input("请输入要查看的学生序号: ")
try:
index = int(detail_index) - 1
if 0 <= index < len(students):
self._print_student_details(students[index])
else:
print("无效的序号!")
except ValueError:
print("请输入有效的数字!")
def statistics_menu(self):
"""统计功能菜单"""
print("\n统计功能")
print("-" * 50)
print("1. 学生总数")
print("2. 各专业学生人数")
print("3. 计算平均身高")
print("4. 计算平均体重")
print("5. 统计性别比例")
choice = input("请选择统计功能: ")
if choice == '1':
total = self.bll.get_total_students()
print(f"学生总数: {total}")
elif choice == '2':
major_count = self.bll.get_students_by_major()
print("\n各专业学生人数:")
for major, count in major_count.items():
print(f"{major}: {count}")
elif choice == '3':
print("1. 全部学生平均身高")
print("2. 按班级计算平均身高")
print("3. 按专业计算平均身高")
sub_choice = input("请选择计算方式: ")
if sub_choice == '1':
avg_height = self.bll.calculate_average_height()
print(f"全部学生平均身高: {avg_height:.2f} cm")
elif sub_choice == '2':
class_name = input("请输入班级名称: ")
avg_height = self.bll.calculate_average_height('class', class_name)
print(f"{class_name} 班级平均身高: {avg_height:.2f} cm")
elif sub_choice == '3':
major = input("请输入专业名称: ")
avg_height = self.bll.calculate_average_height('major', major)
print(f"{major} 专业平均身高: {avg_height:.2f} cm")
else:
print("无效的选择!")
elif choice == '4':
print("1. 全部学生平均体重")
print("2. 按班级计算平均体重")
print("3. 按专业计算平均体重")
sub_choice = input("请选择计算方式: ")
if sub_choice == '1':
avg_weight = self.bll.calculate_average_weight()
print(f"全部学生平均体重: {avg_weight:.2f} kg")
elif sub_choice == '2':
class_name = input("请输入班级名称: ")
avg_weight = self.bll.calculate_average_weight('class', class_name)
print(f"{class_name} 班级平均体重: {avg_weight:.2f} kg")
elif sub_choice == '3':
major = input("请输入专业名称: ")
avg_weight = self.bll.calculate_average_weight('major', major)
print(f"{major} 专业平均体重: {avg_weight:.2f} kg")
else:
print("无效的选择!")
elif choice == '5':
ratio = self.bll.get_gender_ratio()
print("\n性别比例统计:")
print(f"总人数: {ratio['total']}")
print(f"男生人数: {ratio['male']} ({ratio['male_ratio']:.2%})")
print(f"女生人数: {ratio['female']} ({ratio['female_ratio']:.2%})")
else:
print("无效的选择!")
def import_export_menu(self):
"""数据导入导出菜单"""
print("\n数据导入导出")
print("-" * 50)
print("1. 导出学生数据到CSV")
print("2. 导出学生数据到JSON")
print("3. 从CSV导入学生数据")
print("4. 从JSON导入学生数据")
choice = input("请选择操作: ")
# 这里只是示例,实际实现需要根据具体的数据存储方式编写
if choice in ['1', '2', '3', '4']:
print("功能开发中,暂未实现...")
else:
print("无效的选择!")
def _get_age(self, birthday: Optional[date]) -> Optional[int]:
"""计算学生年龄"""
if not birthday:
return None
today = date.today()
age = today.year - birthday.year - ((today.month, today.day) < (birthday.month, birthday.day))
return age

@ -0,0 +1,685 @@
# student/ui/gui_ui.py
import tkinter as tk
from tkinter import ttk, messagebox
from datetime import datetime, date
import re
from student.model.student import Student
from typing import List, Optional
from ..bll.student_bll import StudentBLL
from ..model.student import Student
from ..util.validator import Validator
class StudentGUI:
"""学生信息管理系统的图形用户界面"""
def __init__(self, root: tk.Tk, bll: StudentBLL):
self.root = root
self.root.title("学生信息管理系统")
self.root.geometry("900x600")
self.root.minsize(800, 500)
self.bll = bll
# 设置中文字体
self.style = ttk.Style()
self.style.configure("TLabel", font=("SimHei", 10))
self.style.configure("TButton", font=("SimHei", 10))
self.style.configure("TTreeview", font=("SimHei", 10))
# 创建主框架
self.main_frame = ttk.Frame(self.root)
self.main_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 创建顶部导航栏
self.create_navigation_bar()
# 创建主内容区域
self.content_frame = ttk.Frame(self.main_frame)
self.content_frame.pack(fill=tk.BOTH, expand=True, pady=10)
# 默认显示学生列表
self.show_student_list()
def create_navigation_bar(self):
"""创建顶部导航栏"""
nav_frame = ttk.Frame(self.main_frame, height=40)
nav_frame.pack(fill=tk.X, side=tk.TOP)
# 添加导航按钮
buttons = [
("添加学生", self.show_add_student),
("修改学生", self.show_update_student),
("删除学生", self.show_delete_student),
("查询学生", self.show_search_student),
("统计信息", self.show_statistics),
("数据导入导出", self.show_import_export)
]
for text, command in buttons:
btn = ttk.Button(nav_frame, text=text, command=command)
btn.pack(side=tk.LEFT, padx=5, pady=5)
def clear_content_frame(self):
"""清空内容区域"""
for widget in self.content_frame.winfo_children():
widget.destroy()
def show_student_list(self, students: Optional[List[Student]] = None):
"""显示学生列表"""
self.clear_content_frame()
if students is None:
students = self.bll.get_all_students()
# 创建搜索框
search_frame = ttk.Frame(self.content_frame)
search_frame.pack(fill=tk.X, pady=5)
ttk.Label(search_frame, text="搜索:").pack(side=tk.LEFT, padx=5)
search_var = tk.StringVar()
search_entry = ttk.Entry(search_frame, textvariable=search_var, width=30)
search_entry.pack(side=tk.LEFT, padx=5)
def search_students():
keyword = search_var.get()
if keyword:
search_results = []
search_results.extend(self.bll.search_students_by_name(keyword))
search_results.extend(self.bll.search_students_by_class(keyword))
search_results.extend(self.bll.search_students_by_major(keyword))
# 去重
unique_students = []
ids = set()
for s in search_results:
if s.id_card not in ids:
unique_students.append(s)
ids.add(s.id_card)
self.show_student_list(unique_students)
else:
self.show_student_list()
ttk.Button(search_frame, text="搜索", command=search_students).pack(side=tk.LEFT, padx=5)
# 创建表格
columns = ("name", "id_card", "stu_id", "gender", "age", "class_name", "major")
tree = ttk.Treeview(self.content_frame, columns=columns, show="headings")
# 设置列标题
tree.heading("name", text="姓名")
tree.heading("id_card", text="身份证号")
tree.heading("stu_id", text="学号")
tree.heading("gender", text="性别")
tree.heading("age", text="年龄")
tree.heading("class_name", text="班级")
tree.heading("major", text="专业")
# 设置列宽
tree.column("name", width=80)
tree.column("id_card", width=150)
tree.column("stu_id", width=100)
tree.column("gender", width=50)
tree.column("age", width=50)
tree.column("class_name", width=100)
tree.column("major", width=100)
# 添加数据
for student in students:
gender_text = "" if student.gender else "" if student.gender is not None else ""
age_text = str(student.age) if student.age is not None else ""
tree.insert("", tk.END, values=(
student.name,
student.id_card,
student.stu_id,
gender_text,
age_text,
student.class_name or "",
student.major or ""
))
# 添加滚动条
scrollbar = ttk.Scrollbar(self.content_frame, orient=tk.VERTICAL, command=tree.yview)
tree.configure(yscroll=scrollbar.set)
# 布局
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
tree.pack(fill=tk.BOTH, expand=True)
# 双击查看详情
def on_double_click(event):
item = tree.selection()
if item:
id_card = tree.item(item[0])["values"][1] # 身份证号在第二列
student = self.bll.get_student_by_id(id_card)
if student:
self.show_student_details(student)
tree.bind("<Double-1>", on_double_click)
def show_student_details(self, student: Student):
"""显示学生详情"""
self.clear_content_frame()
# 创建详情表单
detail_frame = ttk.LabelFrame(self.content_frame, text=f"学生详情 - {student.name}")
detail_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 左侧信息
left_frame = ttk.Frame(detail_frame)
left_frame.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# 右侧信息
right_frame = ttk.Frame(detail_frame)
right_frame.pack(side=tk.RIGHT, fill=tk.BOTH, expand=True, padx=10, pady=10)
# 基本信息
info_items = [
("姓名:", student.name),
("身份证号:", student.id_card),
("学号:", student.stu_id),
("性别:", "" if student.gender else "" if student.gender is not None else "未设置"),
("年龄:", str(student.age) if student.age is not None else "未知"),
("出生日期:", str(student.birthday) if student.birthday else "未知"),
("身高:", f"{student.height} cm" if student.height else "未设置"),
("体重:", f"{student.weight} kg" if student.weight else "未设置"),
("入学日期:", str(student.enrollment_date) if student.enrollment_date else "未设置"),
("班级:", student.class_name if student.class_name else "未设置"),
("专业:", student.major if student.major else "未设置"),
("电子邮箱:", student.email if student.email else "未设置"),
("联系电话:", student.phone if student.phone else "未设置")
]
# 显示信息
for i, (label, value) in enumerate(info_items):
if i < len(info_items) // 2: # 前半部分放左侧
ttk.Label(left_frame, text=label).grid(row=i, column=0, sticky=tk.W, pady=5)
ttk.Label(left_frame, text=value).grid(row=i, column=1, sticky=tk.W, pady=5)
else: # 后半部分放右侧
ttk.Label(right_frame, text=label).grid(row=i - len(info_items) // 2, column=0, sticky=tk.W, pady=5)
ttk.Label(right_frame, text=value).grid(row=i - len(info_items) // 2, column=1, sticky=tk.W, pady=5)
# 返回按钮
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)
def show_add_student(self):
"""显示添加学生表单"""
self.clear_content_frame()
form_frame = ttk.LabelFrame(self.content_frame, text="添加学生")
form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表单字段
fields = {
"name": tk.StringVar(),
"id_card": tk.StringVar(),
"stu_id": tk.StringVar(),
"gender": tk.StringVar(value="未设置"),
"height": tk.StringVar(),
"weight": tk.StringVar(),
"enrollment_date": tk.StringVar(),
"class_name": tk.StringVar(),
"major": tk.StringVar(),
"email": tk.StringVar(),
"phone": tk.StringVar()
}
# 错误提示标签和输入框引用
error_labels = {}
entries = {}
# 验证函数
def validate_field(field_name, event=None):
value = fields[field_name].get()
error_msg = ""
# 根据字段类型进行验证
if field_name == "name":
if not value:
error_msg = "姓名不能为空"
elif field_name == "id_card":
if not value:
error_msg = "身份证号不能为空"
elif len(value) != 18:
error_msg = "身份证号必须为18位"
elif not value[:17].isdigit():
error_msg = "前17位必须是数字"
elif not (value[17].isdigit() or value[17].upper() == 'X'):
error_msg = "最后一位必须是数字或X"
elif field_name == "stu_id":
if not value:
error_msg = "学号不能为空"
elif field_name == "height":
if value and not value.isdigit():
error_msg = "请输入数字"
elif field_name == "weight":
if value and not value.replace('.', '', 1).isdigit():
error_msg = "请输入数字"
elif field_name == "enrollment_date":
if value:
try:
datetime.strptime(value, '%Y-%m-%d')
except ValueError:
error_msg = "格式应为YYYY-MM-DD"
elif field_name == "email":
if value and not re.match(r"[^@]+@[^@]+\.[^@]+", value):
error_msg = "邮箱格式不正确"
elif field_name == "phone":
if value and not re.match(r"^1[3-9]\d{9}$", value):
error_msg = "手机号格式不正确"
# 更新错误标签和输入框样式
if error_labels.get(field_name):
error_labels[field_name].config(text=error_msg, foreground="red")
if entries.get(field_name):
if error_msg:
entries[field_name].configure(style="Error.TEntry")
else:
entries[field_name].configure(style="TEntry")
return error_msg
# 创建表单
row = 0
for field, var in fields.items():
# 字段标签
label_text = {
"name": "姓名:",
"id_card": "身份证号:",
"stu_id": "学号:",
"height": "身高 (cm):",
"weight": "体重 (kg):",
"enrollment_date": "入学日期 (YYYY-MM-DD):",
"class_name": "班级:",
"major": "专业:",
"email": "电子邮箱:",
"phone": "联系电话:"
}.get(field, f"{field}:")
ttk.Label(form_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5)
if field == "gender":
# 性别选择(单选按钮)
gender_frame = ttk.Frame(form_frame)
gender_frame.grid(row=row, column=1, sticky=tk.W, pady=5)
for i, option in enumerate(["", "", "未设置"]):
ttk.Radiobutton(gender_frame, text=option, variable=var, value=option).pack(side=tk.LEFT, padx=5)
# 性别不需要错误提示
error_labels[field] = ttk.Label(form_frame, text="")
error_labels[field].grid(row=row, column=2, sticky=tk.W, pady=5)
else:
# 创建输入框
entry = ttk.Entry(form_frame, textvariable=var)
entry.grid(row=row, column=1, sticky=tk.EW, pady=5)
entries[field] = entry
# 绑定验证事件
var.trace_add("write", lambda name, index, mode, f=field: validate_field(f))
entry.bind("<FocusOut>", lambda event, f=field: validate_field(f, event))
# 添加错误提示标签
error_labels[field] = ttk.Label(form_frame, text="", foreground="red")
error_labels[field].grid(row=row, column=2, sticky=tk.W, pady=5)
# 添加标准说明
standard_text = {
"id_card": "(18位数字最后一位可为X)",
"enrollment_date": "(例如: 2023-09-01)",
"email": "(例如: example@mail.com)",
"phone": "(例如: 13800138000)"
}.get(field, "")
if standard_text:
ttk.Label(form_frame, text=standard_text, foreground="gray").grid(row=row, column=3, sticky=tk.W,
pady=5)
row += 1
# 设置列权重,使输入框可以拉伸
form_frame.columnconfigure(1, weight=1)
# 定义错误样式
style = ttk.Style()
style.configure("Error.TEntry", fieldbackground="#ffe6e6", foreground="#000000")
# 提交按钮
def submit_form():
# 验证所有字段
errors = {}
for field in fields:
if field != "gender": # 性别不需要验证
error = validate_field(field)
if error:
errors[field] = error
if errors:
messagebox.showerror("输入错误", "请修正以下错误:\n\n" + "\n".join(errors.values()))
return
# 获取表单数据
name = fields["name"].get()
id_card = fields["id_card"].get()
stu_id = fields["stu_id"].get()
gender_text = fields["gender"].get()
gender = None
if gender_text == "":
gender = True
elif gender_text == "":
gender = False
height = fields["height"].get()
height = int(height) if height else None
weight = fields["weight"].get()
weight = float(weight) if weight else None
enrollment_date = fields["enrollment_date"].get()
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None
class_name = fields["class_name"].get()
major = fields["major"].get()
email = fields["email"].get()
phone = fields["phone"].get()
# 创建学生对象
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,
email=email,
phone=phone
)
# 添加学生
success, message = self.bll.add_student(student)
if success:
messagebox.showinfo("成功", "学生添加成功!")
self.show_student_list()
else:
messagebox.showerror("错误", f"添加失败: {message}")
# 创建按钮
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="提交", command=submit_form).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5)
def show_update_student(self):
"""显示修改学生表单"""
self.clear_content_frame()
ttk.Label(self.content_frame, text="请输入要修改的学生身份证号:").pack(pady=10)
id_card_var = tk.StringVar()
ttk.Entry(self.content_frame, textvariable=id_card_var).pack(fill=tk.X, padx=20, pady=5)
def search_student():
id_card = id_card_var.get()
student = self.bll.get_student_by_id(id_card)
if student:
self._show_update_form(student)
else:
messagebox.showerror("错误", "未找到该学生!")
ttk.Button(self.content_frame, text="查找学生", command=search_student).pack(pady=10)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=5)
def _show_update_form(self, student: Student):
"""显示实际的修改表单"""
self.clear_content_frame()
form_frame = ttk.LabelFrame(self.content_frame, text=f"修改学生信息 - {student.name}")
form_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 表单字段
fields = {
"name": tk.StringVar(value=student.name),
"id_card": tk.StringVar(value=student.id_card),
"stu_id": tk.StringVar(value=student.stu_id),
"gender": tk.StringVar(value="" if student.gender else "" if student.gender is not None else "未设置"),
"height": tk.StringVar(value=str(student.height) if student.height is not None else ""),
"weight": tk.StringVar(value=str(student.weight) if student.weight is not None else ""),
"enrollment_date": tk.StringVar(value=str(student.enrollment_date) if student.enrollment_date else ""),
"class_name": tk.StringVar(value=student.class_name or ""),
"major": tk.StringVar(value=student.major or ""),
"email": tk.StringVar(value=student.email or ""),
"phone": tk.StringVar(value=student.phone or "")
}
# 性别选项
gender_options = ["", "", "未设置"]
# 创建表单
row = 0
for field, var in fields.items():
if field == "id_card": # 身份证号不可修改
ttk.Label(form_frame, text="身份证号:").grid(row=row, column=0, sticky=tk.W, pady=5)
ttk.Label(form_frame, text=student.id_card).grid(row=row, column=1, sticky=tk.W, pady=5)
elif field == "gender":
ttk.Label(form_frame, text="性别:").grid(row=row, column=0, sticky=tk.W, pady=5)
gender_frame = ttk.Frame(form_frame)
gender_frame.grid(row=row, column=1, sticky=tk.W, pady=5)
for i, option in enumerate(gender_options):
ttk.Radiobutton(gender_frame, text=option, variable=var, value=option).pack(side=tk.LEFT, padx=5)
else:
label_text = {
"name": "姓名:",
"stu_id": "学号:",
"height": "身高 (cm):",
"weight": "体重 (kg):",
"enrollment_date": "入学日期 (YYYY-MM-DD):",
"class_name": "班级:",
"major": "专业:",
"email": "电子邮箱:",
"phone": "联系电话:"
}.get(field, f"{field}:")
ttk.Label(form_frame, text=label_text).grid(row=row, column=0, sticky=tk.W, pady=5)
ttk.Entry(form_frame, textvariable=var).grid(row=row, column=1, sticky=tk.EW, pady=5)
row += 1
# 设置列权重,使输入框可以拉伸
form_frame.columnconfigure(1, weight=1)
# 提交按钮
def submit_form():
# 获取表单数据
name = fields["name"].get()
stu_id = fields["stu_id"].get()
gender_text = fields["gender"].get()
gender = None
if gender_text == "":
gender = True
elif gender_text == "":
gender = False
height = fields["height"].get()
height = int(height) if height else None
weight = fields["weight"].get()
weight = float(weight) if weight else None
enrollment_date = fields["enrollment_date"].get()
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date() if enrollment_date else None
class_name = fields["class_name"].get()
major = fields["major"].get()
email = fields["email"].get()
phone = fields["phone"].get()
# 更新学生对象
student.name = name
student.stu_id = stu_id
student.gender = gender
student.height = height
student.weight = weight
student.enrollment_date = enrollment_date
student.class_name = class_name
student.major = major
student.email = email
student.phone = phone
# 更新学生
success, message = self.bll.update_student(student)
if success:
messagebox.showinfo("成功", "学生信息更新成功!")
self.show_student_list()
else:
messagebox.showerror("错误", f"更新失败: {message}")
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="提交", command=submit_form).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5)
def show_delete_student(self):
"""显示删除学生界面"""
self.clear_content_frame()
ttk.Label(self.content_frame, text="请输入要删除的学生身份证号:").pack(pady=10)
id_card_var = tk.StringVar()
ttk.Entry(self.content_frame, textvariable=id_card_var).pack(fill=tk.X, padx=20, pady=5)
def delete_student():
id_card = id_card_var.get()
student = self.bll.get_student_by_id(id_card)
if not student:
messagebox.showerror("错误", "未找到该学生!")
return
confirm = messagebox.askyesno("确认", f"确定要删除学生 {student.name} 吗?")
if confirm:
if self.bll.delete_student_by_id(id_card):
messagebox.showinfo("成功", "学生删除成功!")
self.show_student_list()
else:
messagebox.showerror("错误", "删除失败!")
button_frame = ttk.Frame(self.content_frame)
button_frame.pack(fill=tk.X, pady=10)
ttk.Button(button_frame, text="删除", command=delete_student).pack(side=tk.LEFT, padx=5)
ttk.Button(button_frame, text="返回", command=self.show_student_list).pack(side=tk.LEFT, padx=5)
def show_search_student(self):
"""显示查询学生界面"""
self.clear_content_frame()
search_frame = ttk.LabelFrame(self.content_frame, text="查询学生")
search_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 查询方式
search_type = tk.StringVar(value="name")
ttk.Radiobutton(search_frame, text="按姓名", variable=search_type, value="name").grid(row=0, column=0,
sticky=tk.W, pady=5)
ttk.Radiobutton(search_frame, text="按班级", variable=search_type, value="class").grid(row=0, column=1,
sticky=tk.W, pady=5)
ttk.Radiobutton(search_frame, text="按专业", variable=search_type, value="major").grid(row=0, column=2,
sticky=tk.W, pady=5)
# 查询关键词
ttk.Label(search_frame, text="关键词:").grid(row=1, column=0, sticky=tk.W, pady=5)
keyword_var = tk.StringVar()
ttk.Entry(search_frame, textvariable=keyword_var).grid(row=1, column=1, sticky=tk.EW, pady=5)
search_frame.columnconfigure(1, weight=1)
def perform_search():
keyword = keyword_var.get()
if not keyword:
messagebox.showwarning("警告", "请输入关键词!")
return
search_method = search_type.get()
if search_method == "name":
students = self.bll.search_students_by_name(keyword)
elif search_method == "class":
students = self.bll.search_students_by_class(keyword)
else: # major
students = self.bll.search_students_by_major(keyword)
if not students:
messagebox.showinfo("提示", "未找到匹配的学生!")
self.show_student_list()
else:
self.show_student_list(students)
ttk.Button(search_frame, text="查询", command=perform_search).grid(row=1, column=2, padx=5, pady=5)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)
def show_statistics(self):
"""显示统计信息"""
self.clear_content_frame()
stats_frame = ttk.LabelFrame(self.content_frame, text="统计信息")
stats_frame.pack(fill=tk.BOTH, expand=True, padx=10, pady=10)
# 学生总数
total_students = self.bll.get_total_students()
ttk.Label(stats_frame, text=f"学生总数: {total_students}").grid(row=0, column=0, sticky=tk.W, pady=5)
# 性别比例
gender_ratio = self.bll.get_gender_ratio()
ttk.Label(stats_frame, text=f"男生人数: {gender_ratio['male']} ({gender_ratio['male_ratio']:.2%})").grid(row=1,
column=0,
sticky=tk.W,
pady=5)
ttk.Label(stats_frame, text=f"女生人数: {gender_ratio['female']} ({gender_ratio['female_ratio']:.2%})").grid(
row=2, column=0, sticky=tk.W, pady=5)
# 各专业人数
major_frame = ttk.LabelFrame(stats_frame, text="各专业人数")
major_frame.grid(row=3, column=0, sticky=tk.NSEW, pady=10)
major_count = self.bll.get_students_by_major()
row = 0
for major, count in major_count.items():
ttk.Label(major_frame, text=f"{major}: {count}").grid(row=row, column=0, sticky=tk.W, pady=2)
row += 1
# 平均身高体重
avg_height = self.bll.calculate_average_height()
avg_weight = self.bll.calculate_average_weight()
ttk.Label(stats_frame, text=f"平均身高: {avg_height:.2f} cm").grid(row=0, column=1, sticky=tk.W, pady=5)
ttk.Label(stats_frame, text=f"平均体重: {avg_weight:.2f} kg").grid(row=1, column=1, sticky=tk.W, pady=5)
# 设置列权重
stats_frame.columnconfigure(0, weight=1)
stats_frame.columnconfigure(1, weight=1)
stats_frame.rowconfigure(3, weight=1)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)
def show_import_export(self):
"""显示数据导入导出界面"""
self.clear_content_frame()
ttk.Label(self.content_frame, text="数据导入导出功能开发中...").pack(pady=20)
ttk.Button(self.content_frame, text="返回", command=self.show_student_list).pack(pady=10)

@ -0,0 +1,85 @@
from datetime import date
from typing import Union # 添加这行
# student/util/validator.py
import re
class Validator:
@staticmethod
def validate_id_card(id_card: str) -> bool:
"""验证身份证号码是否有效"""
if not id_card or len(id_card) != 18:
return False
# 前17位必须是数字
if not id_card[:17].isdigit():
return False
# 第18位可以是数字或X
check_char = id_card[17].upper()
if not (check_char.isdigit() or check_char == 'X'):
return False
# 校验码验证
factors = [7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2]
check_code_list = ['1', '0', 'X', '9', '8', '7', '6', '5', '4', '3', '2']
sum_val = sum(int(id_card[i]) * factors[i] for i in range(17))
mod = sum_val % 11
check_code = check_code_list[mod]
return check_char == check_code
@staticmethod
def validate_stu_id(stu_id: str) -> bool:
"""验证学号是否有效格式示例20230001"""
pattern = r'^\d{8}$' # 假设学号为8位数字
return bool(re.match(pattern, stu_id))
@staticmethod
def validate_name(name: str) -> bool:
"""验证姓名是否有效2-20个字符只能包含中文"""
pattern = r'^[\u4e00-\u9fa5]{2,20}$'
return bool(re.match(pattern, name))
@staticmethod
def validate_height(height: int) -> bool:
"""验证身高是否在合理范围"""
return 50 <= height <= 250 if height is not None else True
@staticmethod
def validate_weight(weight: float) -> bool:
"""验证体重是否在合理范围"""
return 5 <= weight <= 300 if weight is not None else True
@staticmethod
def validate_enrollment_date(enrollment_date: Union[date, str], birthday: date) -> bool:
"""验证入学日期是否晚于出生日期"""
if not enrollment_date:
return True
if isinstance(enrollment_date, str):
try:
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date()
except ValueError:
return False
return enrollment_date >= birthday if birthday else True
@staticmethod
def validate_email(email: str) -> bool:
"""验证电子邮箱格式"""
if not email:
return True
pattern = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
return bool(re.match(pattern, email))
@staticmethod
def validate_phone(phone: str) -> bool:
"""验证手机号码格式"""
if not phone:
return True
pattern = r'^1[3-9]\d{9}$'
return bool(re.match(pattern, phone))

Binary file not shown.
Loading…
Cancel
Save