diff --git a/backend/models/attendance.py b/backend/models/attendance.py index 7aedeff..76c4c59 100644 --- a/backend/models/attendance.py +++ b/backend/models/attendance.py @@ -20,7 +20,7 @@ class AttendanceModel: @staticmethod async def get_student_records(student_id: int, month: str = None) -> List[Dict[str, Any]]: sql = """ - SELECT attendance_id, date, status, reason, deduction_applied, created_at + SELECT attendance_id, date, slot, status, reason, deduction_applied, created_at FROM attendance_records WHERE student_id = %s """ @@ -37,7 +37,8 @@ class AttendanceModel: @staticmethod async def get_class_records( date: str = None, - student_id: int = None + student_id: int = None, + slot: str = None ) -> List[Dict[str, Any]]: sql = """ SELECT ar.*, s.name as student_name, s.student_no @@ -55,6 +56,10 @@ class AttendanceModel: sql += " AND ar.student_id = %s" params.append(student_id) + if slot: + sql += " AND ar.slot = %s" + params.append(slot) + sql += " ORDER BY ar.date DESC, s.student_no" return await execute_query(sql, tuple(params)) @@ -65,30 +70,31 @@ class AttendanceModel: date: str, status: str, reason: str = None, - recorder_id: int = None + recorder_id: int = None, + slot: str = 'morning' ) -> int: - # 检查是否已存在当天记录 + # 检查是否已存在当天同时段记录 existing = await execute_one( - "SELECT attendance_id FROM attendance_records WHERE student_id = %s AND date = %s", - (student_id, date) + "SELECT attendance_id FROM attendance_records WHERE student_id = %s AND date = %s AND slot = %s", + (student_id, date, slot) ) if existing: # 更新已有记录 sql = """ - UPDATE attendance_records + UPDATE attendance_records SET status = %s, reason = %s, recorder_id = %s - WHERE student_id = %s AND date = %s + WHERE student_id = %s AND date = %s AND slot = %s """ - await execute_update(sql, (status, reason, recorder_id, student_id, date)) + await execute_update(sql, (status, reason, recorder_id, student_id, date, slot)) return existing["attendance_id"] else: # 插入新记录 sql = """ - INSERT INTO attendance_records (student_id, date, status, reason, recorder_id) - VALUES (%s, %s, %s, %s, %s) + INSERT INTO attendance_records (student_id, date, slot, status, reason, recorder_id) + VALUES (%s, %s, %s, %s, %s, %s) """ - return await execute_insert(sql, (student_id, date, status, reason, recorder_id)) + return await execute_insert(sql, (student_id, date, slot, status, reason, recorder_id)) @staticmethod async def mark_deduction_applied(attendance_id: int) -> bool: diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 4338f46..367eb46 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -391,7 +391,8 @@ async def add_attendance(request: Request, req: AddAttendanceRequest): reason=req.reason, apply_deduction=req.apply_deduction, recorder_id=user["user_id"], - custom_deduction=req.custom_deduction + custom_deduction=req.custom_deduction, + slot=req.slot ) if result["success"]: await LogService.write_operation_log( @@ -410,14 +411,16 @@ async def add_attendance(request: Request, req: AddAttendanceRequest): async def get_attendance_records( request: Request, date: Optional[str] = None, - student_id: Optional[int] = None + student_id: Optional[int] = None, + slot: Optional[str] = None ): """获取考勤记录""" user = await get_current_user(request) result = await AttendanceService.get_records( user_id=user["user_id"], date=date, - student_id=student_id + student_id=student_id, + slot=slot ) return success_response(data=result) diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index 6902bf4..a82fd10 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -16,8 +16,8 @@ from datetime import date, datetime class AddPointsRequest(BaseModel): """加减分请求""" - student_ids: List[int] = Field(..., description="学生ID列表") - points_change: int = Field(..., description="分数变动") + student_ids: List[int] = Field(..., min_length=1, max_length=200, description="学生ID列表") + points_change: int = Field(..., gt=-100, lt=100, description="分数变动") reason: str = Field(..., min_length=1, max_length=255, description="原因") @@ -48,11 +48,11 @@ class ImportResult(BaseModel): class AddAdminRequest(BaseModel): """添加管理员请求""" - username: str = Field(..., min_length=1, max_length=50, description="登录账号") + username: str = Field(..., min_length=2, max_length=50, pattern=r'^[a-zA-Z0-9_\u4e00-\u9fa]+$', description="登录账号") real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名") - password: Optional[str] = Field(None, description="密码(不填则自动生成)") - role_type: str = Field(..., description="角色类型") - subject_id: Optional[int] = Field(None, description="科目ID(科代表需要)") + password: Optional[str] = Field(None, min_length=6, max_length=50, description="密码(不填则自动生成)") + role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型") + subject_id: Optional[int] = Field(None, gt=0, description="科目ID(科代表需要)") class AddAdminResponse(BaseModel): @@ -65,34 +65,35 @@ class AddAdminResponse(BaseModel): class UpdateHomeworkStatusRequest(BaseModel): """更新作业状态请求""" - submission_id: int - status: str - comments: Optional[str] = None + submission_id: int = Field(..., gt=0, description="提交记录ID") + status: str = Field(..., pattern=r'^(submitted|not_submitted|late|excused)$', description="状态") + comments: Optional[str] = Field(None, max_length=500, description="评语") apply_deduction: bool = False class AddStudentRequest(BaseModel): """新增学生请求""" - student_no: str = Field(..., min_length=1, max_length=20, description="学号") + student_no: str = Field(..., min_length=1, max_length=20, pattern=r'^[a-zA-Z0-9]+$', description="学号") name: str = Field(..., min_length=1, max_length=50, description="姓名") - parent_phone: Optional[str] = Field(None, max_length=11, description="家长手机号") + parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号") class AddAttendanceRequest(BaseModel): """添加考勤请求""" - student_id: int + student_id: int = Field(..., gt=0, description="学生ID") date: date - status: str - reason: Optional[str] = None + slot: str = Field(default="morning", pattern=r'^(morning|afternoon|evening)$', description="时段") + status: str = Field(..., pattern=r'^(present|absent|late|leave)$', description="考勤状态") + reason: Optional[str] = Field(None, max_length=255, description="原因") apply_deduction: bool = True - custom_deduction: Optional[int] = Field(default=None, gt=0, description="自定义扣分值") + custom_deduction: Optional[int] = Field(default=None, gt=0, le=20, description="自定义扣分值") class UpdateAdminRequest(BaseModel): """更新管理员请求""" - user_id: int = Field(..., description="用户ID") + user_id: int = Field(..., gt=0, description="用户ID") real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名") - role_type: str = Field(..., description="角色类型") + role_type: str = Field(..., pattern=r'^(班长|学习委员|考勤委员|劳动委员|志愿委员)$', description="角色类型") class DeleteAdminRequest(BaseModel): @@ -108,4 +109,4 @@ class ResetPasswordRequest(BaseModel): class UpdateStudentRequest(BaseModel): """更新学生请求""" name: Optional[str] = Field(None, min_length=1, max_length=50, description="姓名") - parent_phone: Optional[str] = Field(None, max_length=20, description="家长手机号") \ No newline at end of file + parent_phone: Optional[str] = Field(None, max_length=11, pattern=r'^\d{0,11}$', description="家长手机号") \ No newline at end of file diff --git a/backend/services/attendance_service.py b/backend/services/attendance_service.py index 79e327d..07e7107 100644 --- a/backend/services/attendance_service.py +++ b/backend/services/attendance_service.py @@ -41,16 +41,29 @@ class AttendanceService: reason: Optional[str], apply_deduction: bool, recorder_id: int, - custom_deduction: Optional[int] = None + custom_deduction: Optional[int] = None, + slot: str = 'morning' ) -> Dict[str, Any]: """添加考勤记录""" + # 校验时段 + if slot not in ('morning', 'afternoon', 'evening'): + return {"success": False, "message": "无效的考勤时段"} + # 校验状态 + if status not in ('present', 'absent', 'late', 'leave'): + return {"success": False, "message": "无效的考勤状态"} + # 校验自定义扣分范围 + if custom_deduction is not None and (custom_deduction < 1 or custom_deduction > 20): + return {"success": False, "message": "自定义扣分必须在1-20之间"} + # 检查权限 role = await PermissionChecker.get_user_role(recorder_id) if role not in ["班主任", "考勤委员"]: return {"success": False, "message": "无权进行此操作"} - # 检查是否同班级 - # 单班级系统,管理员均可操作 + # 考勤委员扣分上限 + if role == "考勤委员" and apply_deduction and status in ["absent", "late"]: + if custom_deduction is not None and custom_deduction > settings.ATTENDANCE_REP_MAX_POINTS: + return {"success": False, "message": f"考勤委员单次扣分上限为{settings.ATTENDANCE_REP_MAX_POINTS}分"} # 添加考勤记录 attendance_id = await AttendanceModel.create_record( @@ -58,7 +71,8 @@ class AttendanceService: date=date, status=status, reason=reason, - recorder_id=recorder_id + recorder_id=recorder_id, + slot=slot ) if not attendance_id: @@ -107,7 +121,8 @@ class AttendanceService: async def get_records( user_id: int, date: Optional[str] = None, - student_id: Optional[int] = None + student_id: Optional[int] = None, + slot: Optional[str] = None ) -> Dict[str, Any]: """获取考勤记录""" role = await PermissionChecker.get_user_role(user_id) @@ -115,7 +130,8 @@ class AttendanceService: if role in ["班主任", "考勤委员"]: records = await AttendanceModel.get_class_records( date=date, - student_id=student_id + student_id=student_id, + slot=slot ) elif student_id: # 管理员可查看指定学生 diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index 2f01739..2cf2627 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -33,12 +33,18 @@ class ConductService: recorder_id: int, recorder_name: str ) -> Dict[str, Any]: - """ - 批量加减分 - """ + """批量加减分""" + # 输入校验 + if not student_ids or len(student_ids) > 200: + return {"success": False, "message": "学生数量需在1-200之间"} + if not reason or not reason.strip() or len(reason) > 255: + return {"success": False, "message": "原因不能为空且不超过255字符"} + # 验证分值 if points_change == 0: return {"success": False, "message": "分值不能为0"} + if abs(points_change) > 100: + return {"success": False, "message": "单次加减分不能超过100分"} # 获取操作人角色 role = await PermissionChecker.get_user_role(recorder_id) @@ -119,6 +125,9 @@ class ConductService: @staticmethod async def revoke_record(record_id: int, revoker_id: int) -> Dict[str, Any]: """撤销扣分记录""" + if not record_id or record_id <= 0: + return {"success": False, "message": "无效的记录ID"} + # 检查权限 can_revoke = await PermissionChecker.check_can_revoke(revoker_id, record_id) if not can_revoke: @@ -155,6 +164,9 @@ class ConductService: @staticmethod async def restore_record(record_id: int, restorer_id: int) -> Dict[str, Any]: """反撤销(恢复)已撤销的记录""" + if not record_id or record_id <= 0: + return {"success": False, "message": "无效的记录ID"} + # 检查权限:只有班主任可以反撤销 role = await PermissionChecker.get_user_role(restorer_id) if role != "班主任": @@ -207,6 +219,8 @@ class ConductService: end_date = None if related_type == "": related_type = None + if related_type and related_type not in ('manual', 'homework', 'attendance'): + return {"records": [], "page": page, "page_size": page_size, "total": 0, "total_pages": 0} role = await PermissionChecker.get_user_role(user_id) offset = (page - 1) * page_size diff --git a/frontend/admin/attendance.php b/frontend/admin/attendance.php index 7c54fab..d1593a3 100644 --- a/frontend/admin/attendance.php +++ b/frontend/admin/attendance.php @@ -41,9 +41,9 @@ include __DIR__ . '/../includes/header.php';