Files
SharedClassManager/backend/services/attendance_service.py

145 lines
5.5 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
from typing import Dict, Any, Optional
from datetime import datetime
from models.attendance import AttendanceModel
from models.student import StudentModel
from models.conduct import ConductModel
from models.user import UserModel
from middleware.permission import PermissionChecker
from config import settings
from utils.logger import get_logger
logger = get_logger(__name__)
# 考勤状态中文映射
ATTENDANCE_STATUS_MAP = {
"absent": "缺勤",
"late": "迟到",
"leave": "请假"
}
class AttendanceService:
"""考勤服务"""
@staticmethod
async def add_attendance(
student_id: int,
date: str,
status: str,
reason: Optional[str],
apply_deduction: bool,
recorder_id: int,
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(
student_id=student_id,
date=date,
status=status,
reason=reason,
recorder_id=recorder_id,
slot=slot
)
if not attendance_id:
return {"success": False, "message": "添加考勤记录失败"}
# 应用扣分
if apply_deduction and status in ["absent", "late", "leave"]:
# 确定扣分数值(优先使用自定义扣分)
if custom_deduction is not None:
points_change = -custom_deduction
elif status == "absent":
points_change = -settings.DEDUCTION_ATTENDANCE_ABSENT
elif status == "late":
points_change = -settings.DEDUCTION_ATTENDANCE_LATE
else:
points_change = -settings.DEDUCTION_ATTENDANCE_LEAVE
# 扣分为0时跳过如请假不扣分
if points_change == 0:
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status} (不扣分)")
return {"success": True, "message": "考勤记录添加成功(不扣分)"}
student = await StudentModel.get_by_id(student_id)
if student:
# 获取操作人姓名
user = await UserModel.get_by_user_id(recorder_id)
recorder_name = user.get("real_name", "班主任") if user else "班主任"
# 使用中文状态
status_text = ATTENDANCE_STATUS_MAP.get(status, status)
await ConductModel.create_record(
student_id=student_id,
points_change=points_change,
reason=f"考勤:{status_text}",
recorder_id=recorder_id,
recorder_name=recorder_name,
related_type="attendance",
related_id=attendance_id
)
# 更新学生总分
await StudentModel.update_total_points(student_id, points_change)
# 标记已应用扣分
await AttendanceModel.mark_deduction_applied(attendance_id)
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status}")
return {"success": True, "message": "考勤记录添加成功"}
@staticmethod
async def get_records(
user_id: int,
date: Optional[str] = None,
student_id: Optional[int] = None,
slot: Optional[str] = None
) -> Dict[str, Any]:
"""获取考勤记录"""
role = await PermissionChecker.get_user_role(user_id)
if role in ["班主任", "考勤委员"]:
records = await AttendanceModel.get_class_records(
date=date,
student_id=student_id,
slot=slot
)
elif student_id:
# 管理员可查看指定学生
records = await AttendanceModel.get_student_records(student_id)
else:
records = []
return {"records": records}