lou74 2 months ago
commit d128c94bb1

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,6 @@
[.ShellClassInfo]
IconResource=C:\WINDOWS\System32\SHELL32.dll,230
[ViewState]
Mode=
Vid=
FolderType=Generic

@ -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,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.11" project-jdk-type="Python SDK" />
</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/student.iml" filepath="$PROJECT_DIR$/.idea/student.iml" />
</modules>
</component>
</project>

@ -0,0 +1,12 @@
<?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>
<component name="PyDocumentationSettings">
<option name="format" value="PLAIN" />
<option name="myDocStringFormat" value="Plain" />
</component>
</module>

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ChangeListManager">
<list default="true" id="34441950-3172-454a-9d4a-ff9ff755fcae" name="更改" comment="" />
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 5
}]]></component>
<component name="ProjectId" id="2yjKVGxqmCN66PqKmF6zTYHIQcf" />
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"Python.main.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true"
}
}]]></component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-python-sdk-fc1d50d2d915-aa17d162503b-com.jetbrains.pycharm.community.sharedIndexes.bundled-PC-243.24978.54" />
</set>
</attachedChunks>
</component>
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="应用程序级" UseSingleDictionary="true" transferred="true" />
<component name="TaskManager">
<task active="true" id="Default" summary="默认任务">
<changelist id="34441950-3172-454a-9d4a-ff9ff755fcae" name="更改" comment="" />
<created>1750342565374</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1750342565374</updated>
</task>
<servers />
</component>
</project>

@ -0,0 +1,188 @@
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):
self.dal = dal
def add_student(self, student: Student) -> (bool, str):
"""添加学生,包含数据验证"""
# 验证学生数据
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):
"""更新学生信息,包含数据验证"""
# 验证学生数据
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:
"""根据身份证号删除学生"""
return self.dal.delete_by_id(id_card)
def delete_student_by_stu_id(self, stu_id: str) -> bool:
"""根据学号删除学生"""
return self.dal.delete_by_stu_id(stu_id)
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_students_by_name(self, name: str) -> List[Student]:
"""根据姓名搜索学生(模糊查询)"""
return self.dal.search_by_name(name)
def search_students_by_class(self, class_name: str) -> List[Student]:
"""根据班级搜索学生(模糊查询)"""
return self.dal.search_by_class(class_name)
def search_students_by_major(self, major: str) -> List[Student]:
"""根据专业搜索学生(模糊查询)"""
return self.dal.search_by_major(major)
def get_total_students(self) -> int:
"""获取学生总数"""
return len(self.get_all_students())
def get_students_by_major(self) -> dict:
"""统计各专业学生人数"""
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:
"""计算平均身高
group_by: 分组依据可选 'class' 'major'
group_value: 分组值如班级名称或专业名称
"""
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:
"""计算平均体重
group_by: 分组依据可选 'class' 'major'
group_value: 分组值如班级名称或专业名称
"""
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:
"""统计性别比例"""
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,181 @@
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对象"""
# 处理日期类型
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
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
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
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
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,150 @@
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文件存储实现"""
def __init__(self, file_path: str = 'students.json'):
self.file_path = file_path
self._ensure_file_exists()
def _ensure_file_exists(self):
"""确保JSON文件存在"""
try:
with open(self.file_path, 'r', encoding='utf-8') as file:
json.load(file)
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对象转换为字典"""
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 None,
'class_name': student.class_name,
'major': student.major,
'email': student.email,
'phone': student.phone
}
def _dict_to_student(self, data: dict) -> Student:
"""将字典转换为Student对象"""
# 处理日期类型
enrollment_date = data.get('enrollment_date')
if enrollment_date:
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date()
return Student(
name=data.get('name'),
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文件数据"""
with open(self.file_path, 'r', encoding='utf-8') as file:
return json.load(file)
def _save_data(self, data: List[dict]):
"""保存数据到JSON文件"""
with open(self.file_path, 'w', encoding='utf-8') as file:
json.dump(data, file, ensure_ascii=False, indent=2)
def get_by_id(self, id_card: str) -> Optional[Student]:
"""根据身份证号获取学生信息"""
for data in self._load_data():
if data.get('id_card') == id_card:
return self._dict_to_student(data)
return None
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""根据学号获取学生信息"""
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 [self._dict_to_student(data) for data in self._load_data()]
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
data = self._load_data()
data.append(self._student_to_dict(student))
self._save_data(data)
return True
def update(self, student: Student) -> bool:
"""更新学生信息"""
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:
"""根据身份证号删除学生信息"""
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:
"""根据学号删除学生信息"""
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]:
"""根据姓名模糊查询学生信息"""
return [self._dict_to_student(data) for data in self._load_data() if name in data.get('name', '')]
def search_by_class(self, class_name: str) -> List[Student]:
"""根据班级模糊查询学生信息"""
return [self._dict_to_student(data) for data in self._load_data() if
class_name in (data.get('class_name') or '')]
def search_by_major(self, major: str) -> List[Student]:
"""根据专业模糊查询学生信息"""
return [self._dict_to_student(data) for data in self._load_data() if major in (data.get('major') or '')]

@ -0,0 +1,181 @@
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数据库存储实现"""
def __init__(self, db_path: str = 'students.db'):
self.db_path = db_path
self._create_table()
def _create_table(self):
"""创建学生表(如果不存在)"""
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,
height INTEGER,
weight REAL,
enrollment_date TEXT,
class_name TEXT,
major TEXT,
email TEXT,
phone TEXT
)
''')
conn.commit()
def _row_to_student(self, row: tuple) -> Optional[Student]:
"""将数据库行转换为Student对象"""
if not row:
return None
# 处理日期类型
enrollment_date = row[7]
if enrollment_date:
enrollment_date = datetime.strptime(enrollment_date, '%Y-%m-%d').date()
# 处理布尔类型
gender = row[3]
if gender is not None:
gender = bool(gender)
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]:
"""根据身份证号获取学生信息"""
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)
def get_by_stu_id(self, stu_id: str) -> Optional[Student]:
"""根据学号获取学生信息"""
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]:
"""获取所有学生信息"""
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:
"""添加学生信息"""
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:
"""更新学生信息"""
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:
"""根据身份证号删除学生信息"""
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:
"""根据学号删除学生信息"""
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]:
"""根据姓名模糊查询学生信息"""
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]:
"""根据班级模糊查询学生信息"""
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]:
"""根据专业模糊查询学生信息"""
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