diff --git a/backend/models/semester.py b/backend/models/semester.py index 1347526..b95ebf4 100644 --- a/backend/models/semester.py +++ b/backend/models/semester.py @@ -137,6 +137,73 @@ class SemesterModel: GROUP BY hs.student_id, hs.status """ return await execute_query(sql, (start_date, end_date)) + + @staticmethod + async def update( + semester_id: int, + semester_name: str = None, + start_date: str = None, + end_date: str = None + ) -> bool: + """编辑学期信息(仅未归档)""" + sets = [] + params = [] + if semester_name is not None: + sets.append("semester_name = %s") + params.append(semester_name) + if start_date is not None: + sets.append("start_date = %s") + params.append(start_date) + if end_date is not None: + sets.append("end_date = %s") + params.append(end_date) + if not sets: + return False + params.append(semester_id) + sql = f"UPDATE semesters SET {', '.join(sets)} WHERE semester_id = %s AND is_archived = 0" + result = await execute_update(sql, tuple(params)) + return result > 0 + + @staticmethod + async def delete(semester_id: int) -> bool: + """删除学期""" + sql = "DELETE FROM semesters WHERE semester_id = %s" + result = await execute_update(sql, (semester_id,)) + return result > 0 + + @staticmethod + async def count_archives(semester_id: int) -> int: + """统计学期的归档数据数量""" + sql = "SELECT COUNT(*) as cnt FROM semester_archives WHERE semester_id = %s" + result = await execute_one(sql, (semester_id,)) + return result['cnt'] if result else 0 + + @staticmethod + async def associate_records_by_date_range( + semester_id: int, + start_date: str, + end_date: str + ) -> Dict[str, int]: + """按日期范围关联记录到学期""" + # 关联操行分记录(created_at 为 TIMESTAMP,需包含 end_date 当天) + conduct_sql = """ + UPDATE conduct_records + SET semester_id = %s + WHERE semester_id IS NULL + AND created_at BETWEEN %s AND CONCAT(%s, ' 23:59:59') + """ + conduct_count = await execute_update(conduct_sql, (semester_id, start_date, end_date)) + + # 关联考勤记录 + attendance_sql = """ + UPDATE attendance_records + SET semester_id = %s + WHERE semester_id IS NULL + AND `date` BETWEEN %s AND %s + """ + attendance_count = await execute_update(attendance_sql, (semester_id, start_date, end_date)) + + return {"conduct": conduct_count, "attendance": attendance_count} class SemesterArchiveModel: diff --git a/backend/routes/semester.py b/backend/routes/semester.py index 2a7db42..e76b704 100644 --- a/backend/routes/semester.py +++ b/backend/routes/semester.py @@ -18,7 +18,7 @@ from middleware.permission import ( ) from services.semester_service import SemesterService from services.log_service import LogService -from schemas.semester import CreateSemesterRequest +from schemas.semester import CreateSemesterRequest, UpdateSemesterRequest from utils.response import success_response, error_response from utils.logger import get_logger @@ -103,8 +103,90 @@ async def activate_semester(request: Request, semester_id: int): return error_response(message=result["message"]) +@router.put("/update/{semester_id}") +async def update_semester(request: Request, semester_id: int, req: UpdateSemesterRequest): + """编辑学期(班主任)""" + user = await get_current_user(request) + is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) + if not is_teacher: + return error_response(message="仅班主任可编辑学期", code=403) + + result = await SemesterService.update_semester( + semester_id=semester_id, + semester_name=req.semester_name, + start_date=req.start_date, + end_date=req.end_date, + operator_id=user["user_id"] + ) + if result["success"]: + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["username"], + operator_role="班主任", operation_type="update_semester", + target_type="semester", target_id=semester_id, + details=f"编辑学期ID: {semester_id}", + ip=request.client.host + ) + return success_response(message=result["message"]) + else: + return error_response(message=result["message"]) + + +@router.delete("/delete/{semester_id}") +async def delete_semester(request: Request, semester_id: int): + """删除学期(班主任)""" + user = await get_current_user(request) + is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) + if not is_teacher: + return error_response(message="仅班主任可删除学期", code=403) + + result = await SemesterService.delete_semester( + semester_id=semester_id, + operator_id=user["user_id"] + ) + if result["success"]: + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["username"], + operator_role="班主任", operation_type="delete_semester", + target_type="semester", target_id=semester_id, + details=f"删除学期ID: {semester_id}", + ip=request.client.host + ) + return success_response(message=result["message"]) + else: + return error_response(message=result["message"]) + + +@router.post("/{semester_id}/associate") +async def associate_records(request: Request, semester_id: int): + """关联记录到学期(班主任)""" + user = await get_current_user(request) + is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) + if not is_teacher: + return error_response(message="仅班主任可关联数据", code=403) + + result = await SemesterService.associate_records( + semester_id=semester_id, + operator_id=user["user_id"] + ) + if result["success"]: + await LogService.write_operation_log( + operator_id=user["user_id"], operator_name=user["username"], + operator_role="班主任", operation_type="associate_records", + target_type="semester", target_id=semester_id, + details=f"关联数据到学期ID: {semester_id}, 结果: {result.get('data', {})}", + ip=request.client.host + ) + return success_response(data=result.get("data"), message=result["message"]) + else: + return error_response(message=result["message"]) + + @router.post("/archive/{semester_id}") -async def archive_semester(request: Request, semester_id: int): +async def archive_semester( + request: Request, + semester_id: int, + reset_scores: bool = Query(False) +): """归档学期(班主任)""" user = await get_current_user(request) is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) @@ -113,14 +195,18 @@ async def archive_semester(request: Request, semester_id: int): result = await SemesterService.archive_semester( semester_id=semester_id, - operator_id=user["user_id"] + operator_id=user["user_id"], + reset_scores=reset_scores ) if result["success"]: + log_detail = f"归档学期ID: {semester_id}" + if reset_scores: + log_detail += " 并重置学生操行分" await LogService.write_operation_log( operator_id=user["user_id"], operator_name=user["username"], operator_role="班主任", operation_type="archive_semester", target_type="semester", target_id=semester_id, - details=f"归档学期ID: {semester_id}", + details=log_detail, ip=request.client.host ) return success_response(message=result["message"]) diff --git a/backend/schemas/semester.py b/backend/schemas/semester.py index b87668e..cfb2b51 100644 --- a/backend/schemas/semester.py +++ b/backend/schemas/semester.py @@ -18,3 +18,10 @@ class CreateSemesterRequest(BaseModel): semester_name: str = Field(..., min_length=1, max_length=100, description="学期名称") start_date: Optional[str] = Field(None, description="学期开始日期 (YYYY-MM-DD)") end_date: Optional[str] = Field(None, description="学期结束日期 (YYYY-MM-DD)") + + +class UpdateSemesterRequest(BaseModel): + """编辑学期请求""" + semester_name: Optional[str] = Field(None, min_length=1, max_length=100, description="学期名称") + start_date: Optional[str] = Field(None, description="学期开始日期 (YYYY-MM-DD)") + end_date: Optional[str] = Field(None, description="学期结束日期 (YYYY-MM-DD)") diff --git a/backend/services/semester_service.py b/backend/services/semester_service.py index 9e446ed..5c9dce6 100644 --- a/backend/services/semester_service.py +++ b/backend/services/semester_service.py @@ -127,9 +127,110 @@ class SemesterService: return {"success": False, "message": f"激活学期失败: {str(e)}"} @staticmethod - async def archive_semester( + async def update_semester( + semester_id: int, + semester_name: str = None, + start_date: str = None, + end_date: str = None, + operator_id: int = None + ) -> Dict[str, Any]: + """编辑学期信息""" + try: + semester = await SemesterModel.get_by_id(semester_id) + if not semester: + return {"success": False, "message": "学期不存在"} + + if semester['is_archived']: + return {"success": False, "message": "已归档的学期不能编辑"} + + result = await SemesterModel.update( + semester_id=semester_id, + semester_name=semester_name, + start_date=start_date, + end_date=end_date + ) + + if result: + logger.info(f"用户[{operator_id}] 编辑了学期: {semester['semester_name']}") + return {"success": True, "message": "学期信息已更新"} + else: + return {"success": False, "message": "更新失败,请检查参数"} + except Exception as e: + logger.error(f"编辑学期失败: {e}") + return {"success": False, "message": f"编辑学期失败: {str(e)}"} + + @staticmethod + async def delete_semester( semester_id: int, operator_id: int = None + ) -> Dict[str, Any]: + """删除学期""" + try: + semester = await SemesterModel.get_by_id(semester_id) + if not semester: + return {"success": False, "message": "学期不存在"} + + # 检查是否有关联归档数据 + archive_count = await SemesterModel.count_archives(semester_id) + if archive_count > 0: + return {"success": False, "message": f"该学期有 {archive_count} 条归档数据,无法删除"} + + result = await SemesterModel.delete(semester_id) + + if result: + logger.info(f"用户[{operator_id}] 删除了学期: {semester['semester_name']}") + return {"success": True, "message": "学期已删除"} + else: + return {"success": False, "message": "删除失败"} + except Exception as e: + logger.error(f"删除学期失败: {e}") + return {"success": False, "message": f"删除学期失败: {str(e)}"} + + @staticmethod + async def associate_records( + semester_id: int, + operator_id: int = None + ) -> Dict[str, Any]: + """关联记录到学期""" + try: + semester = await SemesterModel.get_by_id(semester_id) + if not semester: + return {"success": False, "message": "学期不存在"} + + if semester['is_archived']: + return {"success": False, "message": "已归档的学期不能关联数据"} + + start_date = semester.get('start_date') + if not start_date: + return {"success": False, "message": "学期未设置开始日期,无法关联数据"} + + end_date = semester.get('end_date') or datetime.date.today().isoformat() + + counts = await SemesterModel.associate_records_by_date_range( + semester_id=semester_id, + start_date=start_date, + end_date=end_date + ) + + logger.info( + f"用户[{operator_id}] 关联数据到学期: {semester['semester_name']}, " + f"操行分 {counts['conduct']} 条, 考勤 {counts['attendance']} 条" + ) + + return { + "success": True, + "message": f"关联完成:操行分 {counts['conduct']} 条,考勤 {counts['attendance']} 条", + "data": counts + } + except Exception as e: + logger.error(f"关联记录失败: {e}") + return {"success": False, "message": f"关联记录失败: {str(e)}"} + + @staticmethod + async def archive_semester( + semester_id: int, + operator_id: int = None, + reset_scores: bool = False ) -> Dict[str, Any]: """归档学期""" try: @@ -231,6 +332,18 @@ class SemesterService: # 标记学期为已归档 await SemesterModel.archive(semester_id) + # 归档成功后按需重置学生操行分 + if reset_scores: + reset_result = await SemesterService.reset_student_points() + logger.info( + f"用户[{operator_id}] 归档学期: {semester['semester_name']} 并重置学生操行分, " + f"共 {total_students} 名学生" + ) + return { + "success": True, + "message": f"学期归档成功,共归档 {total_students} 名学生数据,已重置学生操行分" + } + logger.info( f"用户[{operator_id}] 归档了学期: {semester['semester_name']}, " f"共 {total_students} 名学生" diff --git a/frontend/admin/semesters.php b/frontend/admin/semesters.php index 08033f9..d1407d8 100644 --- a/frontend/admin/semesters.php +++ b/frontend/admin/semesters.php @@ -84,6 +84,54 @@ include __DIR__ . '/../includes/header.php'; + +
注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。
+归档会创建所有学生当前操行分的数据快照,原始数据不受影响。
+注意:归档前需确保学期已设置开始日期,否则无法归档。归档后该学期数据将变为只读,不可撤销。