修复学期功能
This commit is contained in:
@@ -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
|
||||
|
||||
# 考勤委员单次扣分上限(绝对值)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
# 标记学期为已归档
|
||||
|
||||
Reference in New Issue
Block a user