修复考勤管理bug并加强了信息保护
This commit is contained in:
@@ -44,7 +44,8 @@ class Settings:
|
||||
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "")
|
||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30"))
|
||||
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "60"))
|
||||
JWT_IDLE_TIMEOUT_MINUTES: int = int(os.getenv("JWT_IDLE_TIMEOUT_MINUTES", "10"))
|
||||
|
||||
PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "")
|
||||
DEBUG_ENABLED: bool = os.getenv("DEBUG_ENABLED", "False").lower() == "true"
|
||||
@@ -63,7 +64,7 @@ class Settings:
|
||||
MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5"))
|
||||
|
||||
STUDY_COMMISSIONER_MAX_POINTS: int = int(os.getenv("STUDY_COMMISSIONER_MAX_POINTS", "5"))
|
||||
ATTENDANCE_REP_MAX_POINTS: int = int(os.getenv("ATTENDANCE_REP_MAX_POINTS", "5"))
|
||||
ATTENDANCE_REP_MAX_POINTS: int = int(os.getenv("ATTENDANCE_REP_MAX_POINTS", "8"))
|
||||
LABOR_REP_MAX_POINTS: int = int(os.getenv("LABOR_REP_MAX_POINTS", "1"))
|
||||
VOLUNTEER_REP_MAX_POINTS: int = int(os.getenv("VOLUNTEER_REP_MAX_POINTS", "5"))
|
||||
|
||||
|
||||
@@ -92,15 +92,16 @@ class AuthMiddleware(BaseHTTPMiddleware):
|
||||
logger.warning(f"[Auth] {path} - Redis Token不匹配, user_id={user_id}, stored={'有' if stored_token else '无'}")
|
||||
return self._cors_response(request, 401, "令牌已失效,请重新登录")
|
||||
|
||||
# 将用户信息存储到request.state
|
||||
# 将用户信息存储到request.state
|
||||
request.state.user_id = payload.get("user_id")
|
||||
request.state.username = payload.get("username")
|
||||
request.state.real_name = payload.get("real_name") or payload.get("username")
|
||||
request.state.user_type = payload.get("user_type")
|
||||
request.state.student_id = payload.get("student_id")
|
||||
request.state.role = payload.get("role")
|
||||
|
||||
# 刷新Token过期时间
|
||||
await RedisClient.expire(f"user_token:{user_id}", settings.JWT_EXPIRE_MINUTES * 60)
|
||||
# 刷新Token过期时间(空闲超时:10分钟无操作则需重新登录)
|
||||
await RedisClient.expire(f"user_token:{user_id}", settings.JWT_IDLE_TIMEOUT_MINUTES * 60)
|
||||
|
||||
logger.debug(f"[Auth] {path} - 认证成功, user_id={user_id}, username={payload.get('username')}")
|
||||
|
||||
@@ -148,6 +149,7 @@ async def get_current_user(request: Request) -> Dict[str, Any]:
|
||||
return {
|
||||
"user_id": request.state.user_id,
|
||||
"username": request.state.username,
|
||||
"real_name": getattr(request.state, 'real_name', None) or request.state.username,
|
||||
"user_type": request.state.user_type,
|
||||
"student_id": request.state.student_id,
|
||||
"role": request.state.role
|
||||
|
||||
@@ -126,6 +126,7 @@ class PermissionChecker:
|
||||
检查是否可以撤销扣分记录
|
||||
班主任:可以撤销任何记录
|
||||
班长:可以撤销任何记录
|
||||
考勤委员:可以撤销自己的记录
|
||||
其他:只能撤销自己的记录
|
||||
"""
|
||||
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
|
||||
|
||||
@@ -83,20 +83,24 @@ class ConductModel:
|
||||
start_date: str = None,
|
||||
end_date: str = None,
|
||||
student_id: int = None,
|
||||
include_revoked: bool = True
|
||||
include_revoked: bool = True,
|
||||
related_type: str = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取所有记录(班主任/班长专用)"""
|
||||
"""获取所有记录(班主任/班长专用)"""
|
||||
# 空字符串转为None
|
||||
if start_date == "":
|
||||
start_date = None
|
||||
if end_date == "":
|
||||
end_date = None
|
||||
|
||||
if related_type == "":
|
||||
related_type = None
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name
|
||||
SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name,
|
||||
ru.real_name as revoker_name
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
JOIN users u ON cr.recorder_id = u.user_id
|
||||
LEFT JOIN users ru ON cr.revoked_by = ru.user_id
|
||||
WHERE 1=1
|
||||
"""
|
||||
if not include_revoked:
|
||||
@@ -115,6 +119,10 @@ class ConductModel:
|
||||
sql += " AND DATE(cr.created_at) <= %s"
|
||||
params.append(end_date)
|
||||
|
||||
if related_type:
|
||||
sql += " AND cr.related_type = %s"
|
||||
params.append(related_type)
|
||||
|
||||
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
||||
params.extend([limit, offset])
|
||||
|
||||
@@ -128,7 +136,7 @@ class ConductModel:
|
||||
page: int = 1,
|
||||
page_size: int = 20
|
||||
) -> Dict[str, Any]:
|
||||
"""获取分组后的操行分记录(同批次合并)"""
|
||||
"""获取分组后的操行分记录(同批次合并)"""
|
||||
if start_date == "":
|
||||
start_date = None
|
||||
if end_date == "":
|
||||
@@ -215,7 +223,7 @@ class ConductModel:
|
||||
|
||||
@staticmethod
|
||||
async def restore_record(record_id: int, restorer_id: int) -> bool:
|
||||
"""反撤销(恢复)已撤销的记录"""
|
||||
"""反撤销(恢复)已撤销的记录"""
|
||||
try:
|
||||
sql = """
|
||||
UPDATE conduct_records
|
||||
|
||||
@@ -105,4 +105,11 @@ class UserModel:
|
||||
"""更新用户状态(0=禁用,1=启用)"""
|
||||
sql = "UPDATE users SET status = %s WHERE user_id = %s"
|
||||
result = await execute_update(sql, (status, user_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def update_real_name(user_id: int, real_name: str) -> bool:
|
||||
"""更新用户真实姓名"""
|
||||
sql = "UPDATE users SET real_name = %s WHERE user_id = %s"
|
||||
result = await execute_update(sql, (real_name, user_id))
|
||||
return result > 0
|
||||
@@ -87,7 +87,7 @@ async def import_students(request: Request, file: UploadFile = File(...)):
|
||||
initial_points=60
|
||||
)
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="import_students",
|
||||
target_type="student",
|
||||
details=f"批量导入: 成功{result['success_count']}人, 失败{result['failed_count']}人",
|
||||
@@ -113,7 +113,7 @@ async def add_student(request: Request, req: AddStudentRequest):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="add_student",
|
||||
target_type="student", target_id=result.get("student_id"),
|
||||
details=f"新增学生: {req.name}({req.student_no})",
|
||||
@@ -192,7 +192,7 @@ async def add_conduct_points(request: Request, req: AddPointsRequest):
|
||||
if result["success"]:
|
||||
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role=role, operation_type="add_points",
|
||||
target_type="conduct",
|
||||
details=f"批量加减分: {req.points_change}分, 原因: {req.reason}, 对象: {req.student_ids}",
|
||||
@@ -215,7 +215,7 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
|
||||
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||
record = result.get("record", {})
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role=role, operation_type="revoke_record",
|
||||
target_type="conduct", target_id=req.record_id,
|
||||
details=(
|
||||
@@ -242,7 +242,7 @@ async def restore_conduct_record(request: Request, req: RevokeRequest):
|
||||
if result["success"]:
|
||||
record = result.get("record", {})
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="restore_record",
|
||||
target_type="conduct", target_id=req.record_id,
|
||||
details=(
|
||||
@@ -266,7 +266,8 @@ async def get_conduct_history(
|
||||
page_size: int = Query(20, ge=1, le=1000),
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
grouped: bool = Query(False)
|
||||
grouped: bool = Query(False),
|
||||
related_type: Optional[str] = None
|
||||
):
|
||||
"""获取操行分历史记录"""
|
||||
try:
|
||||
@@ -278,7 +279,8 @@ async def get_conduct_history(
|
||||
page_size=page_size,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
grouped=grouped
|
||||
grouped=grouped,
|
||||
related_type=related_type
|
||||
)
|
||||
return success_response(data=result)
|
||||
except Exception as e:
|
||||
@@ -335,7 +337,7 @@ async def create_assignment(
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="create_assignment",
|
||||
target_type="homework",
|
||||
details=f"发布作业: {title}",
|
||||
@@ -362,7 +364,7 @@ async def update_submission_status(request: Request, req: UpdateHomeworkStatusRe
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role=role, operation_type="update_submission",
|
||||
target_type="homework", target_id=req.submission_id,
|
||||
details=f"状态: {req.status}",
|
||||
@@ -393,7 +395,7 @@ async def add_attendance(request: Request, req: AddAttendanceRequest):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role=role, operation_type="add_attendance",
|
||||
target_type="attendance",
|
||||
details=f"学生ID: {req.student_id}, 日期: {req.date}, 状态: {req.status}",
|
||||
@@ -440,7 +442,7 @@ async def add_admin(request: Request, req: AddAdminRequest):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="add_admin",
|
||||
target_type="admin",
|
||||
details=f"新增管理员: {req.real_name}({req.username}), 角色: {req.role_type}",
|
||||
@@ -477,16 +479,24 @@ async def update_admin(request: Request, user_id: int, req: UpdateAdminRequest):
|
||||
return error_response(message="无效的角色类型", code=400)
|
||||
|
||||
from models.admin_role import AdminRoleModel
|
||||
from models.user import UserModel
|
||||
|
||||
# 更新角色
|
||||
result = await AdminRoleModel.update_role(
|
||||
user_id=user_id,
|
||||
role_type=req.role_type
|
||||
)
|
||||
|
||||
# 更新姓名
|
||||
if req.real_name:
|
||||
await UserModel.update_real_name(user_id, req.real_name)
|
||||
|
||||
if result:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="update_admin",
|
||||
target_type="admin", target_id=user_id,
|
||||
details=f"更新管理员角色为: {req.role_type}",
|
||||
details=f"更新管理员角色为: {req.role_type}, 姓名: {req.real_name}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message="管理员更新成功")
|
||||
@@ -515,7 +525,7 @@ async def delete_admin(request: Request, user_id: int):
|
||||
# 再删除用户账号(软删除,将状态设为禁用)
|
||||
await UserModel.update_status(user_id, 0)
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="delete_admin",
|
||||
target_type="admin", target_id=user_id,
|
||||
details=f"删除管理员: ID={user_id}",
|
||||
@@ -548,7 +558,7 @@ async def reset_admin_password(request: Request, user_id: int, req: ResetPasswor
|
||||
updated = await UserModel.update_password(user_id, req.new_password)
|
||||
if updated:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="reset_password",
|
||||
target_type="admin", target_id=user_id,
|
||||
details=f"重置管理员密码: {target_user['real_name']}({target_user['username']})",
|
||||
|
||||
@@ -67,7 +67,7 @@ async def create_semester(request: Request, req: CreateSemesterRequest):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="create_semester",
|
||||
target_type="semester", target_id=result.get("semester_id"),
|
||||
details=f"创建学期: {req.semester_name}",
|
||||
@@ -92,7 +92,7 @@ async def activate_semester(request: Request, semester_id: int):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="activate_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"激活学期ID: {semester_id}",
|
||||
@@ -120,7 +120,7 @@ async def update_semester(request: Request, semester_id: int, req: UpdateSemeste
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="update_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"编辑学期ID: {semester_id}",
|
||||
@@ -145,7 +145,7 @@ async def delete_semester(request: Request, semester_id: int):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="delete_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"删除学期ID: {semester_id}",
|
||||
@@ -170,7 +170,7 @@ async def associate_records(request: Request, semester_id: int):
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="associate_records",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"关联数据到学期ID: {semester_id}, 结果: {result.get('data', {})}",
|
||||
@@ -203,7 +203,7 @@ async def archive_semester(
|
||||
if reset_scores:
|
||||
log_detail += " 并重置学生操行分"
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_id=user["user_id"], operator_name=user["real_name"],
|
||||
operator_role="班主任", operation_type="archive_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=log_detail,
|
||||
|
||||
@@ -82,11 +82,12 @@ class AttendanceService:
|
||||
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}",
|
||||
reason=f"考勤:{status_text}",
|
||||
recorder_id=recorder_id,
|
||||
recorder_name=recorder_name,
|
||||
related_type="attendance",
|
||||
@@ -98,8 +99,6 @@ class AttendanceService:
|
||||
|
||||
# 标记已应用扣分
|
||||
await AttendanceModel.mark_deduction_applied(attendance_id)
|
||||
await AttendanceModel.mark_deduction_applied(attendance_id)
|
||||
|
||||
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status}")
|
||||
|
||||
return {"success": True, "message": "考勤记录添加成功"}
|
||||
|
||||
@@ -76,7 +76,8 @@ class AuthService:
|
||||
username=user["username"],
|
||||
user_type=user["user_type"],
|
||||
student_id=user["student_id"],
|
||||
role=role
|
||||
role=role,
|
||||
real_name=user["real_name"]
|
||||
)
|
||||
|
||||
# 存储Token到Redis
|
||||
|
||||
@@ -196,7 +196,8 @@ class ConductService:
|
||||
page_size: int = 20,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
grouped: bool = False
|
||||
grouped: bool = False,
|
||||
related_type: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取历史记录"""
|
||||
# 空字符串转为None
|
||||
@@ -204,6 +205,8 @@ class ConductService:
|
||||
start_date = None
|
||||
if end_date == "":
|
||||
end_date = None
|
||||
if related_type == "":
|
||||
related_type = None
|
||||
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
offset = (page - 1) * page_size
|
||||
@@ -224,7 +227,8 @@ class ConductService:
|
||||
offset=offset,
|
||||
start_date=start_date,
|
||||
end_date=end_date,
|
||||
student_id=student_id
|
||||
student_id=student_id,
|
||||
related_type=related_type
|
||||
)
|
||||
|
||||
# 获取总数
|
||||
@@ -240,6 +244,9 @@ class ConductService:
|
||||
if end_date:
|
||||
count_conditions.append("DATE(cr.created_at) <= %s")
|
||||
count_params.append(end_date)
|
||||
if related_type:
|
||||
count_conditions.append("cr.related_type = %s")
|
||||
count_params.append(related_type)
|
||||
count_where = " AND ".join(count_conditions)
|
||||
count_sql = f"""
|
||||
SELECT COUNT(*) as total FROM conduct_records cr
|
||||
|
||||
@@ -23,7 +23,7 @@ class JWTHandler:
|
||||
"""JWT Token处理类"""
|
||||
|
||||
@staticmethod
|
||||
def create_token(user_id: int, username: str, user_type: str, student_id: int = None, role: str = None) -> str:
|
||||
def create_token(user_id: int, username: str, user_type: str, student_id: int = None, role: str = None, real_name: str = None) -> str:
|
||||
"""
|
||||
创建JWT Token
|
||||
"""
|
||||
@@ -33,6 +33,7 @@ class JWTHandler:
|
||||
'user_type': user_type,
|
||||
'student_id': student_id,
|
||||
'role': role,
|
||||
'real_name': real_name,
|
||||
'exp': datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES),
|
||||
'iat': datetime.utcnow(),
|
||||
'iss': settings.APP_NAME
|
||||
|
||||
@@ -100,7 +100,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>姓名</label>
|
||||
<input type="text" id="editAdminRealName" disabled>
|
||||
<input type="text" id="editAdminRealName" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>角色</label>
|
||||
|
||||
@@ -82,6 +82,11 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.student-cell { display: flex; flex-direction: column; align-items: center; }
|
||||
.student-cell-name { font-size: 14px; font-weight: 500; }
|
||||
.student-cell-no { font-size: 11px; color: #999; }
|
||||
</style>
|
||||
<script>
|
||||
let currentStatus = 'absent';
|
||||
let studentsData = [];
|
||||
@@ -116,14 +121,16 @@ async function loadStudents() {
|
||||
|
||||
// 渲染学生方格
|
||||
function renderStudentGrid() {
|
||||
const currentSlot = document.getElementById('attendanceSlot').value;
|
||||
let html = '';
|
||||
studentsData.forEach(student => {
|
||||
const hasRecord = existingRecords.some(r => r.student_id === student.student_id);
|
||||
html += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||||
data-id="${student.student_id}"
|
||||
const hasRecord = existingRecords.some(r => r.student_id === student.student_id && (!r.slot || r.slot === currentSlot));
|
||||
html += `<div class="student-cell${hasRecord ? ' has-record' : ''}"
|
||||
data-id="${student.student_id}"
|
||||
data-name="${escapeHtml(student.name)}"
|
||||
onclick="toggleStudent(this)">
|
||||
${escapeHtml(student.name)}
|
||||
<span class="student-cell-name">${escapeHtml(student.name)}</span>
|
||||
<span class="student-cell-no">${escapeHtml(student.student_no)}</span>
|
||||
</div>`;
|
||||
});
|
||||
if (studentsData.length === 0) {
|
||||
@@ -154,7 +161,8 @@ function deselectAllStudents() {
|
||||
// 加载当天已有考勤记录(用于标记 .has-record)
|
||||
async function loadExistingRecords() {
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||||
const slot = document.getElementById('attendanceSlot').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date, slot });
|
||||
if (res && res.success) {
|
||||
existingRecords = res.data.records || [];
|
||||
renderStudentGrid(); // 重新渲染以标记 has-record
|
||||
@@ -232,11 +240,14 @@ async function loadAttendanceRecords() {
|
||||
}
|
||||
}
|
||||
|
||||
// 日期变化时重新加载
|
||||
// 日期或时段变化时重新加载
|
||||
document.getElementById('attendanceDate').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
loadAttendanceRecords();
|
||||
});
|
||||
document.getElementById('attendanceSlot').addEventListener('change', function() {
|
||||
loadExistingRecords();
|
||||
});
|
||||
|
||||
// 页面初始化
|
||||
loadStudents();
|
||||
|
||||
@@ -41,6 +41,15 @@ include __DIR__ . '/../includes/header.php';
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>扣分类型</label>
|
||||
<select id="historyRelatedType">
|
||||
<option value="">全部</option>
|
||||
<option value="manual">手动加减分</option>
|
||||
<option value="homework">作业</option>
|
||||
<option value="attendance">考勤</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<button class="btn btn-secondary" onclick="exportHistoryRecords()">导出历史记录</button>
|
||||
@@ -88,12 +97,15 @@ async function loadHistory(page = 1) {
|
||||
const endDate = document.getElementById('historyEndDate').value;
|
||||
const studentId = document.getElementById('historyStudentId').value;
|
||||
|
||||
const relatedType = document.getElementById('historyRelatedType').value;
|
||||
|
||||
const params = {
|
||||
page, page_size: 20,
|
||||
start_date: startDate,
|
||||
end_date: endDate
|
||||
};
|
||||
if (studentId) params.student_id = studentId;
|
||||
if (relatedType) params.related_type = relatedType;
|
||||
|
||||
const res = await apiGet('/api/admin/conduct/history', params);
|
||||
|
||||
@@ -110,13 +122,15 @@ async function loadHistory(page = 1) {
|
||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||
<?php if ($role === '班主任'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
html += `<td><span class="text-muted" style="margin-right:4px;">已撤销</span><button class="btn btn-sm btn-secondary" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted" style="margin-right:4px;">${revokerInfo}</span><button class="btn btn-sm btn-secondary" onclick="restoreRecord(${record.record_id})">反撤销</button></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
<?php elseif ($role === '班长'): ?>
|
||||
if (record.is_revoked == 1) {
|
||||
html += `<td><span class="text-muted">已撤销</span></td>`;
|
||||
const revokerInfo = record.revoker_name ? `由 ${escapeHtml(record.revoker_name)} 撤销` : '已撤销';
|
||||
html += `<td><span class="text-muted">${revokerInfo}</span></td>`;
|
||||
} else {
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
}
|
||||
@@ -164,10 +178,12 @@ async function exportHistoryRecords() {
|
||||
showToast('正在导出历史记录...', 'info');
|
||||
|
||||
try {
|
||||
const relatedType = document.getElementById('historyRelatedType').value;
|
||||
const params = { page: 1, page_size: 1000 };
|
||||
if (startDate) params.start_date = startDate;
|
||||
if (endDate) params.end_date = endDate;
|
||||
if (studentId) params.student_id = studentId;
|
||||
if (relatedType) params.related_type = relatedType;
|
||||
|
||||
const res = await apiGet('/api/admin/conduct/history', params);
|
||||
if (res && res.success && res.data.records) {
|
||||
|
||||
@@ -179,7 +179,7 @@ async function loadStudents(page = 1) {
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td><a href="/admin/history.php?student_id=${student.student_id}" style="color: #3498db; text-decoration: none;">${escapeHtml(student.name)}</a></td>
|
||||
<td>${student.total_points}</td>
|
||||
${userRole === '班主任' ? `<td>${student.parent_phone || '-'}</td>` : ''}
|
||||
${userRole === '班主任' ? `<td>${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}</td>` : ''}
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
${userRole === '班主任' ? `<button class="btn btn-sm btn-secondary" onclick="showEditStudentModal(${student.student_id}, '${escapeHtml(student.student_no)}', '${escapeHtml(student.name)}', '${escapeHtml(student.parent_phone || '')}')">编辑</button>
|
||||
|
||||
@@ -63,6 +63,35 @@ include __DIR__ . '/../includes/header.php';
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 编辑科目模态框 -->
|
||||
<div id="editSubjectModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>编辑科目</h3>
|
||||
<button class="modal-close" onclick="closeModal('editSubjectModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitEditSubject()">
|
||||
<input type="hidden" id="editSubjectId">
|
||||
<div class="form-group">
|
||||
<label>科目名称</label>
|
||||
<input type="text" id="editSubjectName" required placeholder="例如:语文、数学">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>科目代码</label>
|
||||
<input type="text" id="editSubjectCode" placeholder="例如:CHI、MATH">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>排序序号</label>
|
||||
<input type="number" id="editSubjectSortOrder" placeholder="数字越小越靠前" min="0">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">保存</button>
|
||||
<button type="button" class="btn" onclick="closeModal('editSubjectModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.subject-list {
|
||||
display: flex;
|
||||
@@ -113,6 +142,7 @@ async function loadSubjects() {
|
||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||
${sub.is_active ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button class="btn btn-sm btn-primary" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
|
||||
<button class="btn btn-sm" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
|
||||
${sub.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
@@ -161,6 +191,39 @@ async function submitAddSubject() {
|
||||
}
|
||||
}
|
||||
|
||||
function showEditSubjectModal(subjectId, name, code, sortOrder) {
|
||||
document.getElementById('editSubjectId').value = subjectId;
|
||||
document.getElementById('editSubjectName').value = name;
|
||||
document.getElementById('editSubjectCode').value = code;
|
||||
document.getElementById('editSubjectSortOrder').value = sortOrder;
|
||||
document.getElementById('editSubjectModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitEditSubject() {
|
||||
const subjectId = document.getElementById('editSubjectId').value;
|
||||
const subjectName = document.getElementById('editSubjectName').value.trim();
|
||||
const subjectCode = document.getElementById('editSubjectCode').value.trim();
|
||||
const sortOrder = document.getElementById('editSubjectSortOrder').value;
|
||||
|
||||
if (!subjectName) {
|
||||
showToast('请填写科目名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = { subject_name: subjectName };
|
||||
if (subjectCode) data.subject_code = subjectCode;
|
||||
if (sortOrder !== '') data.sort_order = parseInt(sortOrder);
|
||||
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, data);
|
||||
if (res && res.success) {
|
||||
showToast('科目更新成功');
|
||||
closeModal('editSubjectModal');
|
||||
loadSubjects();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
|
||||
Reference in New Issue
Block a user