From 8f7725191061c4e7d24045330533490dbd099e10 Mon Sep 17 00:00:00 2001 From: canglan Date: Wed, 22 Apr 2026 02:37:27 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AD=A6=E6=9C=9F=E5=8A=9F?= =?UTF-8?q?=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 2 + backend/.env.example | 13 ++- backend/models/semester.py | 70 +++++++++++++--- backend/services/semester_service.py | 102 ++++++++++++++++++++--- frontend/admin/dashboard.php | 48 ++++++++--- frontend/admin/semesters.php | 61 ++++++++++++-- frontend/student/semester_history.php | 17 ++++ sql/init.sql | 112 ++++++++++++++++++++++++++ 8 files changed, 388 insertions(+), 37 deletions(-) diff --git a/.gitignore b/.gitignore index f14b63f..8665ba3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ # 环境变量 .env +backend/.env +frontend/.env # Python __pycache__/ diff --git a/backend/.env.example b/backend/.env.example index 18bfb2c..a4e8a47 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -1,3 +1,14 @@ +# =========================================== +# 班级操行分管理系统 - 前端配置 +# +# 开发者: Canglan +# 联系方式: admin@sea-studio.top +# 版权归属: Sea Network Technology Studio +# 许可证: MIT License +# +# 版权所有 © Sea Network Technology Studio +# =========================================== + # =========================================== # FastAPI 应用配置 # =========================================== @@ -94,7 +105,7 @@ MONITOR_MAX_ADD=5 # 班长单次扣分上限(负数) MONITOR_MAX_SUBTRACT=-5 -# 学习委员单次加减分上限(绝对值)- 正负均不可超过此值 +# 学习委员单次加减分上限(绝对值) STUDY_COMMISSIONER_MAX_POINTS=5 # 考勤委员单次扣分上限(绝对值) diff --git a/backend/models/semester.py b/backend/models/semester.py index f2be317..1347526 100644 --- a/backend/models/semester.py +++ b/backend/models/semester.py @@ -51,14 +51,26 @@ class SemesterModel: @staticmethod async def get_active() -> Optional[Dict[str, Any]]: - """获取当前活跃学期""" - sql = """ - SELECT semester_id, semester_name, start_date, end_date, - is_active, is_archived, created_at + """获取当前活跃学期(优先 is_active 标记,降级为日期范围匹配)""" + fields = "semester_id, semester_name, start_date, end_date, is_active, is_archived, created_at" + # 第一优先级:is_active 标记 + sql = f""" + SELECT {fields} FROM semesters WHERE is_active = 1 AND is_archived = 0 LIMIT 1 """ + result = await execute_one(sql) + if result: + return result + # 第二优先级:日期范围匹配 + # 注:无日期的学期不会自动匹配为活跃学期(需手动激活) + sql = f""" + SELECT {fields} + FROM semesters + WHERE is_archived = 0 AND start_date <= CURDATE() AND (end_date IS NULL OR end_date >= CURDATE()) + LIMIT 1 + """ return await execute_one(sql) @staticmethod @@ -102,6 +114,29 @@ class SemesterModel: sql = "SELECT semester_id FROM conduct_records WHERE record_id = %s" result = await execute_one(sql, (record_id,)) return result['semester_id'] if result else None + + @staticmethod + async def get_attendance_stats_by_semester(semester_id: int, start_date: str, end_date: str) -> List[Dict]: + """批量查询学期内所有学生的考勤统计""" + sql = """ + SELECT student_id, status, COUNT(*) as cnt + FROM attendance_records + WHERE (semester_id = %s OR (semester_id IS NULL AND `date` BETWEEN %s AND %s)) + GROUP BY student_id, status + """ + return await execute_query(sql, (semester_id, start_date, end_date)) + + @staticmethod + async def get_homework_stats_by_date_range(start_date: str, end_date: str) -> List[Dict]: + """通过作业截止日期范围查询所有学生的作业提交统计""" + sql = """ + SELECT hs.student_id, hs.status, COUNT(*) as cnt + FROM homework_submissions hs + JOIN assignments a ON hs.assignment_id = a.assignment_id + WHERE a.deadline BETWEEN %s AND %s + GROUP BY hs.student_id, hs.status + """ + return await execute_query(sql, (start_date, end_date)) class SemesterArchiveModel: @@ -114,25 +149,39 @@ class SemesterArchiveModel: return 0 sql = """ INSERT INTO semester_archives - (semester_id, student_id, student_no, student_name, final_points, rank_position, total_students) - VALUES (%s, %s, %s, %s, %s, %s, %s) + (semester_id, student_id, student_no, student_name, final_points, rank_position, total_students, + attendance_present, attendance_absent, attendance_late, attendance_leave, + homework_submitted, homework_not_submitted, homework_late) + VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s) """ params_list = [ ( a['semester_id'], a['student_id'], a['student_no'], a['student_name'], a['final_points'], - a.get('rank_position'), a.get('total_students') + a.get('rank_position', 0), a.get('total_students', 0), + a.get('attendance_present', 0), a.get('attendance_absent', 0), + a.get('attendance_late', 0), a.get('attendance_leave', 0), + a.get('homework_submitted', 0), a.get('homework_not_submitted', 0), + a.get('homework_late', 0) ) for a in archives_data ] return await execute_many(sql, params_list) + @staticmethod + async def delete_by_semester(semester_id: int) -> int: + """删除指定学期的所有归档数据(用于归档操作的幂等性)""" + sql = "DELETE FROM semester_archives WHERE semester_id = %s" + return await execute_update(sql, (semester_id,)) + @staticmethod async def get_by_semester(semester_id: int) -> List[Dict[str, Any]]: """获取学期的归档数据""" sql = """ SELECT archive_id, semester_id, student_id, student_no, - student_name, final_points, rank_position, total_students, archived_at + student_name, final_points, rank_position, total_students, + attendance_present, attendance_absent, attendance_late, attendance_leave, + homework_submitted, homework_not_submitted, homework_late, archived_at FROM semester_archives WHERE semester_id = %s ORDER BY rank_position ASC @@ -156,7 +205,10 @@ class SemesterArchiveModel: sql = """ SELECT sa.archive_id, sa.semester_id, sa.student_id, sa.student_no, sa.student_name, sa.final_points, sa.rank_position, - sa.total_students, sa.archived_at, + sa.total_students, sa.attendance_present, sa.attendance_absent, + sa.attendance_late, sa.attendance_leave, + sa.homework_submitted, sa.homework_not_submitted, sa.homework_late, + sa.archived_at, s.semester_name, s.start_date, s.end_date FROM semester_archives sa JOIN semesters s ON sa.semester_id = s.semester_id diff --git a/backend/services/semester_service.py b/backend/services/semester_service.py index c538122..9e446ed 100644 --- a/backend/services/semester_service.py +++ b/backend/services/semester_service.py @@ -9,6 +9,7 @@ # 版权所有 © Sea Network Technology Studio # =========================================== +import datetime from typing import Dict, Any, List, Optional from models.semester import SemesterModel, SemesterArchiveModel @@ -49,20 +50,41 @@ class SemesterService: return {"success": False, "message": "学期名称不能为空"} try: - # 自动将之前的活跃学期设为非活跃 - await SemesterModel.deactivate_all() - - # 创建新学期并自动设为活跃 + # 创建学期(不预先 deactivate_all) semester_id = await SemesterModel.create( semester_name=semester_name.strip(), start_date=start_date, end_date=end_date ) - # 设为活跃学期 - await SemesterModel.activate(semester_id) + # 判断新学期的日期范围是否包含今天,决定是否自动激活 + should_activate = False + if start_date is not None: + try: + today = datetime.date.today() + s_date = datetime.datetime.strptime(start_date, "%Y-%m-%d").date() + e_date = ( + datetime.datetime.strptime(end_date, "%Y-%m-%d").date() + if end_date is not None + else None + ) + if s_date <= today and (e_date is None or e_date >= today): + should_activate = True + except (ValueError, TypeError): + should_activate = False - logger.info(f"用户[{operator_id}] 创建了新学期: {semester_name}") + if should_activate: + # 日期范围包含今天,自动激活 + await SemesterModel.deactivate_all() + await SemesterModel.activate(semester_id) + logger.info( + f"用户[{operator_id}] 创建并激活新学期: {semester_name}" + ) + else: + # 补录历史学期或未来学期,不激活 + logger.info( + f"用户[{operator_id}] 创建补录学期(未激活): {semester_name}" + ) return { "success": True, @@ -120,6 +142,11 @@ class SemesterService: if semester['is_archived']: return {"success": False, "message": "该学期已归档"} + # 校验开始日期:无 start_date 时作业统计会全部归零 + start_date = semester.get('start_date') + if not start_date: + return {"success": False, "message": "学期未设置开始日期,无法进行归档"} + # 获取所有活跃学生及其当前分数 students = await StudentModel.get_all(include_disabled=False) if not students: @@ -127,12 +154,60 @@ class SemesterService: total_students = len(students) + # 获取学期的日期范围,用于查询考勤和作业统计 + end_date = semester.get('end_date') or datetime.date.today().isoformat() + + # 批量查询考勤和作业统计 + attendance_stats = await SemesterModel.get_attendance_stats_by_semester( + semester_id, start_date, end_date + ) + homework_stats = await SemesterModel.get_homework_stats_by_date_range( + start_date, end_date + ) + + # 构建 attendance_map: {student_id: {status_field: cnt, ...}} + attendance_map = {} + for stat in attendance_stats: + sid = stat['student_id'] + if sid not in attendance_map: + attendance_map[sid] = { + 'attendance_present': 0, 'attendance_absent': 0, + 'attendance_late': 0, 'attendance_leave': 0 + } + status_key = { + 'present': 'attendance_present', + 'absent': 'attendance_absent', + 'late': 'attendance_late', + 'leave': 'attendance_leave' + }.get(stat['status']) + if status_key: + attendance_map[sid][status_key] = stat['cnt'] + + # 构建 homework_map: {student_id: {status_field: cnt, ...}} + homework_map = {} + for stat in homework_stats: + sid = stat['student_id'] + if sid not in homework_map: + homework_map[sid] = { + 'homework_submitted': 0, 'homework_not_submitted': 0, + 'homework_late': 0 + } + status_key = { + 'submitted': 'homework_submitted', + 'not_submitted': 'homework_not_submitted', + 'late': 'homework_late' + }.get(stat['status']) + if status_key: + homework_map[sid][status_key] = stat['cnt'] + # 按分数降序排列以计算排名 sorted_students = sorted(students, key=lambda s: s['total_points'], reverse=True) # 构建归档快照数据 archives_data = [] for rank, student in enumerate(sorted_students, 1): + att = attendance_map.get(student['student_id'], {}) + hw = homework_map.get(student['student_id'], {}) archives_data.append({ 'semester_id': semester_id, 'student_id': student['student_id'], @@ -140,10 +215,17 @@ class SemesterService: 'student_name': student['name'], 'final_points': student['total_points'], 'rank_position': rank, - 'total_students': total_students + 'total_students': total_students, + 'attendance_present': att.get('attendance_present', 0), + 'attendance_absent': att.get('attendance_absent', 0), + 'attendance_late': att.get('attendance_late', 0), + 'attendance_leave': att.get('attendance_leave', 0), + 'homework_submitted': hw.get('homework_submitted', 0), + 'homework_not_submitted': hw.get('homework_not_submitted', 0), + 'homework_late': hw.get('homework_late', 0), }) - - # 保存归档快照 + # 删除已有的归档数据以保证幂等性,再保存归档快照 + await SemesterArchiveModel.delete_by_semester(semester_id) await SemesterArchiveModel.batch_create(archives_data) # 标记学期为已归档 diff --git a/frontend/admin/dashboard.php b/frontend/admin/dashboard.php index 601402b..ea78013 100644 --- a/frontend/admin/dashboard.php +++ b/frontend/admin/dashboard.php @@ -35,9 +35,16 @@ include __DIR__ . '/../includes/header.php';
操行分排行榜
+
+ 显示前 + + % 的学生 + + +
- +
排名学号姓名操行分前%
排名学号姓名操行分
@@ -46,6 +53,8 @@ include __DIR__ . '/../includes/header.php';
diff --git a/frontend/admin/semesters.php b/frontend/admin/semesters.php index 35081f5..08033f9 100644 --- a/frontend/admin/semesters.php +++ b/frontend/admin/semesters.php @@ -64,16 +64,20 @@ include __DIR__ . '/../includes/header.php';
+
+ + +
- - + +
@@ -89,7 +93,7 @@ include __DIR__ . '/../includes/header.php';

-

注意:归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。

+

注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。