Files
ClassManager/.cospec/plan/changes/fix-admin-multi-issues/task.md
2026-04-14 14:48:49 +08:00

225 lines
20 KiB
Markdown
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.
## 实施
### 阶段 1统一导航栏
- [x] 1.1 创建统一导航栏模板
【目标对象】`frontend/includes/nav.php`(新建)
【修改目的】将所有 admin 页面硬编码的导航栏抽取为共享模板,解决各页面导航不一致的问题;同时修复 dashboard.php 中密码链接 `passwork.php` 的拼写错误
【修改方式】新建 PHP 文件,定义导航栏 HTML 结构,直接读取 `$role``$current_page` 变量(已由 `header.php` 第 17-19 行定义)来动态生成导航项和 active 状态
【相关依赖】`frontend/includes/header.php`(第 17-19 行已定义 `$current_page``$user_type``$role` 变量,在 include nav.php 之前已可用)
【修改内容】
- 直接使用 `$role``$current_page` 变量(无需参数传递,因为 header.php 在 nav.php 之前被 include
- 导航项及角色条件统一为:
- 首页dashboard所有管理员可见
- 学生管理students所有管理员可见
- 操行分管理conduct$role==='班主任' || $role==='班长'
- 作业管理homework$role==='班主任' || $role==='学习委员'
- 考勤管理attendance$role==='班主任' || $role==='考勤委员'
- 科目管理subjects$role==='班主任' || $role==='学习委员'
- 管理员管理admins$role==='班主任'
- 历史记录history所有管理员可见
- 修改密码password所有管理员可见
- 根据 `$current_page`(值为不含 `.php` 后缀的文件名,如 `dashboard``students`)为当前页面对应的导航项添加 `active` class
- 密码链接统一写为 `password.php`(修复 dashboard 中 `passwork.php` 拼写错误)
- 导航栏外层容器沿用现有 `<div class="nav">` 结构
- [x] 1.2 各 admin 页面引入统一导航模板
【目标对象】所有 `frontend/admin/*.php` 页面
【修改目的】移除各页面硬编码的 `<div class="nav">...</div>` 块,替换为 `include nav.php`,统一导航栏
【修改方式】在以下每个页面中,找到 `<div class="nav">` 到对应的 `</div>` 之间的导航栏块,整块替换为 `<?php include __DIR__ . '/../includes/nav.php'; ?>`
【相关依赖】`frontend/includes/nav.php`(任务 1.1 创建)
【修改内容】
- `frontend/admin/dashboard.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
- `frontend/admin/students.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
- `frontend/admin/conduct.php`:替换第 31-47 行的 `<div class="nav">...</div>` 为 include nav.php
- `frontend/admin/homework.php`:替换第 31-47 行的 `<div class="nav">...</div>` 为 include nav.php注意此页面后续会在任务 2.2 完全重写,此处仅替换导航栏部分)
- `frontend/admin/attendance.php`:替换第 31-47 行的 `<div class="nav">...</div>` 为 include nav.php注意此页面后续会在任务 3.3 完全重写,此处仅替换导航栏部分)
- `frontend/admin/history.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
- `frontend/admin/subjects.php`:替换第 30-48 行的 `<div class="nav">...</div>` 为 include nav.php
- `frontend/admin/admins.php`:替换第 30-46 行的 `<div class="nav">...</div>` 为 include nav.php
- `frontend/admin/password.php`:替换第 25-43 行的 `<div class="nav">...</div>` 为 include nav.php
- 替换后需确认 include 语句位于 `include header.php;` 之后、页面主体内容之前
### 阶段 2作业管理改版
- [x] 2.1 清理 `admin.js` 中废弃的作业管理函数
【目标对象】`frontend/assets/js/admin.js`
【修改目的】移除作业管理改版后不再使用的旧作业管理函数,避免全局函数污染和潜在冲突
【修改方式】删除第 174-220 行的三个函数:`showAddAssignmentModal()``loadSubjectsForSelect()``submitAddAssignment()`
【相关依赖】无homework.php 将在任务 2.2 完全重写,不再依赖这些全局函数)
【修改内容】
- 删除第 174-178 行的 `showAddAssignmentModal()` 函数
- 删除第 180-192 行的 `loadSubjectsForSelect()` 函数
- 删除第 194-220 行的 `submitAddAssignment()` 函数
- 注意:`admin.js` 中其他通用函数(如 `submitBatchPoints()``closeModal()``escapeHtml()``toggleSelectAll()`)保持不变,仍被 conduct.php 和 students.php 使用
- [x] 2.2 重写作业管理前端页面
【目标对象】`frontend/admin/homework.php`
【修改目的】将作业发布/提交管理模式改为单纯的加减操行分模式,交互方式参照 `conduct.php`
【修改方式】完全重写页面 HTML 内容和 `<script>` 中的 JavaScript 逻辑(保留文件头部的 PHP 鉴权部分和 include 结构)
【相关依赖】
- `frontend/includes/nav.php`(任务 1.1,导航栏引入)
- `frontend/assets/js/admin.js`(提供 `submitBatchPoints()``showBatchPointsModal()``closeModal()``escapeHtml()``toggleSelectAll()` 等全局复用函数)
- 后端 API `GET /api/admin/students`(加载学生列表,参见 `conduct.php` 第 77-95 行的 `loadStudents()` 调用方式)
- 后端 API `POST /api/admin/conduct/add`(加减分接口,请求体:`{ student_ids: number[], points_change: number, reason: string }`
【修改内容】
- 角色权限判断(第 23 行)统一为 `in_array($role, ['班主任', '学习委员'])`(之前是 `科代表`,与后端 `admin.py` 第 228 行的角色检查一致)
- 移除发布作业按钮和发布作业模态框
- 移除作业列表和提交记录展示
- 新增学生列表表格(列:复选框、学号、姓名、当前操行分、操作-加减分按钮),参照 `conduct.php` 第 57-71 行的表格结构
- 新增批量加减分模态框(复用 `conduct.php` 第 116-143 行和 `students.php` 第 214-241 行的 `batchPointsModal` 模态框结构),包含:
- 已选学生数量显示
- 扣分类型快捷选择:未交作业(-2分)、迟交作业(-1分)、自定义(选择后自动填入分数变动输入框)
- 分数变动输入框(支持正数加分、负数扣分)
- 原因输入框textarea
- 确认提交按钮
- 页面级 JS 中定义 `let selectedStudentIds = [];``loadStudents()``showSinglePointsModal()` 函数(与 conduct.php 和 students.php 的页面级变量风格一致)
- 提交时复用 `admin.js` 中已有的 `submitBatchPoints()` 函数(该函数调用 `POST /api/admin/conduct/add``reason` 字段格式如 `"[作业扣分] 未交作业 - 原因内容"` 以区分来源
- 错误处理遵循仓库既有风格:调用 `apiPost()` 后检查 `res.success`,失败时使用 `showToast(res.message || '操作失败', 'error')` 提示(参见 `admin.js` 第 57-64 行的 `submitBatchPoints` 错误处理模式)
- 加载学生列表失败时,在表格区域显示错误提示文本
### 阶段 3考勤管理改版
- [x] 3.1 清理 `admin.js` 中废弃的考勤管理函数
【目标对象】`frontend/assets/js/admin.js`
【修改目的】移除考勤管理改版后不再使用的旧考勤添加函数,避免全局函数污染和潜在冲突
【修改方式】删除原始文件第 222-268 行的三个函数(因任务 2.1 已删除第 174-220 行,实际操作时行号已前移约 47 行,请按函数名定位):`showAddAttendanceModal()``loadStudentsForSelect()``submitAddAttendance()`
【相关依赖】任务 2.1(先删除作业相关函数,行号会偏移)
【修改内容】
- 删除 `showAddAttendanceModal()` 函数(原始第 222-227 行)
- 删除 `loadStudentsForSelect()` 函数(原始第 229-238 行)
- 删除 `submitAddAttendance()` 函数(原始第 241-268 行)
- 建议按函数名搜索定位删除,而非依赖行号
- 注意attendance.php 页面重写后,所有考勤相关 JS 逻辑将在页面级 `<script>` 中定义,不依赖 `admin.js` 中的旧函数
- [x] 3.2 新增考勤方格网格 CSS 样式
【目标对象】`frontend/assets/css/admin.css`
【修改目的】为考勤页面的学生方格网格提供样式
【修改方式】在文件末尾追加新样式规则
【相关依赖】无
【修改内容】
- `.student-grid` 容器:`display: flex; flex-wrap: wrap; gap: 10px;` 布局
- `.student-cell` 方格:`width: calc(100% / 7 - 10px);`(每行 7 个带边框、圆角、居中文字、内边距、cursor: pointer
- `.student-cell.selected` 选中状态:红色/粉色背景高亮(如 `background: #fee2e2; border-color: #ef4444;`
- `.student-cell.has-record` 已有考勤记录标记:灰色虚线边框(如 `border: 2px dashed #9ca3af;`),用于标识当天已有考勤记录的学生
- `.student-cell:hover` 悬停效果:浅色背景变化
- `.attendance-toolbar` 工具栏样式flex 布局,按钮间距
- 移动端响应式:小屏幕下 `.student-cell` 宽度调整为 `calc(100% / 4 - 10px)` 或更宽
- [x] 3.3 重写考勤管理前端页面
【目标对象】`frontend/admin/attendance.php`
【修改目的】将单个学生下拉选择模式改为学生方格网格模式(一行 7 个),选中扣分制
【修改方式】完全重写页面 UI 和 `<script>` 中的 JavaScript 逻辑(保留文件头部的 PHP 鉴权部分和 include 结构)
【相关依赖】
- `frontend/includes/nav.php`(任务 1.1,导航栏引入)
- `frontend/assets/css/admin.css`(任务 3.2,方格网格样式)
- 后端 API `GET /api/admin/students`(加载学生列表)
- 后端 API `POST /api/admin/attendance`(添加考勤记录,请求体:`{ student_id: number, date: string, status: string, reason?: string, apply_deduction: boolean }`
- 后端 API `GET /api/admin/attendance/records`(查询考勤记录,参数:`{ date: string }`
【修改内容】
- 页面顶部工具栏:日期选择器(默认当天)+ 考勤状态选择(缺勤/迟到/请假单选按钮组)+ 原因输入框
- 主体区域:加载所有学生,以方格网格展示(使用 `.student-grid` 容器和 `.student-cell` 方格)
- 每个方格显示学生姓名,点击切换选中/取消状态(添加/移除 `.selected` class
- 底部工具栏:全选按钮 + 取消全选按钮 + 提交按钮
- 提交逻辑:遍历所有选中的学生,对每个学生调用 `POST /api/admin/attendance`,参数为 `{ student_id, date, status, reason, apply_deduction: true }`
- 提交过程中的错误处理:使用 `Promise.allSettled()` 并发提交,提交完成后汇总成功/失败数量,使用 `showToast()` 提示结果(与仓库既有风格一致)
- 重复考勤记录边界处理:后端 `AttendanceService.add_attendance` 不检查同一学生同一天是否已有考勤记录,允许重复提交。前端在提交前先调用 `GET /api/admin/attendance/records` 获取当天已有记录,对已有考勤记录的学生在方格上添加视觉标记(如 `.has-record` 样式,灰色虚线边框),并在提交时 `confirm()` 提示"以下学生已有考勤记录,是否继续提交?"
- 下方保留历史考勤记录表格:调用 `GET /api/admin/attendance/records` 查询指定日期的记录并渲染(参照现有 `attendance.php` 第 112-131 行的渲染逻辑)
- 移除原有的单个学生下拉选择添加考勤模态框
- 加载学生列表失败时,在网格区域显示错误提示文本
- 错误提示风格遵循仓库既有模式:使用 `showToast(message, 'error')` 函数
### 阶段 4家长手机号权限控制
- [x] 4.1 家长手机号权限控制
### 阶段 5扣分规则配置化
- [x] 5.1 作业和考勤扣分规则配置化
【目标对象】`frontend/.env.example``frontend/config.php``frontend/includes/header.php``frontend/admin/homework.php``frontend/admin/attendance.php`
【修改目的】将作业和考勤的默认扣分量从 .env 文件配置,前端快捷按钮动态读取配置值,作业加减分有上限限制
【修改方式】
- .env.example 新增6个扣分配置项有默认值
- config.php 读取并 define 为常量
- header.php 注入到 JS 全局变量
- homework.php 快捷按钮使用配置值 + 自定义输入 + ±HOMEWORK_MAX_POINTS 限制
- attendance.php 状态按钮使用配置值
### 阶段 6CORS 跨域拦截修复
- [x] 6.1 注册 AuthMiddleware 为全局中间件并修复 CORS 执行顺序
【目标对象】`backend/main.py``backend/middleware/auth_middleware.py`
【修改目的】修复 CORS 跨域拦截问题AuthMiddleware 未注册导致 request.state 属性缺失,路由层 500 错误被浏览器误报为 CORS 错误
【修改方式】
- auth_middleware.py: dispatch 方法顶部添加 OPTIONS 请求跳过逻辑
- main.py: 注册 AuthMiddleware 为全局中间件先注册后执行CORS 在 Auth 之后注册(后注册先执行)
- main.py: 添加 CORS 配置启动日志和空值警告
【中间件执行顺序】CORS → Auth → access_log → 路由
【后续修复】将 AuthMiddleware 从 BaseHTTPMiddleware 改为纯 ASGI 中间件,解决 BaseHTTPMiddleware 提前返回响应时 CORS 头丢失的问题
【目标对象】`frontend/admin/students.php`
【修改目的】除班主任角色外,隐藏家长手机号列的显示内容,保护隐私
【修改方式】在表头 HTML 和 JS 渲染处添加 `$role` 判断
【相关依赖】`$_SESSION['role']`(已存储在 `$role` 变量中,由各页面顶部从 session 读取)
【修改内容】
- 第 68 行表头处:`<th>家长手机号</th>` 改为 `<?php if ($role === '班主任'): ?><th>家长手机号</th><?php endif; ?>`(表头列有条件显示)
- 在页面 `<script>` 标签开头注入角色信息:`const userRole = '<?php echo $role; ?>';`
- 第 148 行 JS 渲染处:`<td>${student.parent_phone || '-'}</td>` 改为根据 `userRole` 判断渲染内容:班主任显示真实手机号,其他角色显示 `***`
- 第 156 行空数据提示处:`colspan="6"` 需根据角色动态调整——班主任 6 列,非班主任 5 列(可用 PHP 输出:`colspan="<?php echo $role === '班主任' ? '6' : '5'; ?>"`
- 新增学生表单(第 101-129 行)和导入学生功能中的手机号字段保持不变,任何有学生管理权限的角色都能录入(与 proposal 一致)
### 阶段 7修复 AuthMiddleware 401 无限循环
- [x] 7.1 AuthMiddleware 添加调试日志和修复潜在问题
【目标对象】`backend/middleware/auth_middleware.py`
【修改目的】修复 401 无限循环问题。当前 AuthMiddleware 注册为全局中间件后,所有非公开路径的请求都需要 JWT 认证,但缺少详细日志无法定位 Token 验证失败的具体原因。需要添加详细日志并修复 OPEN_PATHS 未被检查的 Bug。
【修改方式】在 dispatch 方法中添加每个认证步骤的调试日志,修复 OPEN_PATHS 逻辑,添加更多公开路径
【修改内容】
- 在 dispatch 方法中每个关键检查点添加 logger.debug 日志:
1. 请求路径和方法
2. Authorization header 是否存在
3. JWT 验证结果(成功时记录 user_id失败时记录原因
4. Redis Token 查询结果stored_token 是否存在、是否匹配)
- 修复 OPEN_PATHS 未被检查的 Bug在 is_public_path 检查后增加 OPEN_PATHS 检查OPEN_PATHS 中的路径跳过 Token 验证但仍继续到路由层
-`/api/auth/me``/api/auth/change-password` 添加到 PUBLIC_PATHS目前 OPEN_PATHS 逻辑未被 dispatch 使用,导致这些路径被拦截)
- `_cors_response` 方法中的 allowed_origins 改为从 config.settings 读取,而非硬编码
- [x] 7.2 前端 common.js 添加 401 防循环机制
【目标对象】`frontend/assets/js/common.js`
【修改目的】防止 401 响应导致的无限刷新循环。当前 apiRequest 在收到 401 时直接 clearAuth + redirect如果用户重新登录后 Token 仍然无效(如 Redis 重启),会形成 401 → 登录 → 401 → 登录的无限循环。
【修改方式】在 401 处理逻辑中添加防循环机制
【修改内容】
- 在 401 处理中添加路径判断:如果当前已在 `/index.php`(登录页),不执行重定向
- 添加重定向频率限制:使用 sessionStorage 记录最近一次 401 重定向时间,如果 5 秒内重复 401停止重定向并在控制台输出警告
- 清除认证信息后,在跳转前添加延迟或在 URL 中添加标记参数,防止浏览器缓存导致的循环
### 阶段 8修复 302 循环 - 401 时同步清除 PHP Session
- [x] 8.1 修复 302 循环 - 401 时同步清除 PHP Session
【目标对象】`frontend/assets/js/common.js`
【修改目的】修复 PHP Session 与 JWT Token 不同步导致的 302 无限重定向循环。当 JWT Token 无效(如后端重启后 Redis 清空)导致 API 返回 401 时,`clearAuth()` 只清除了 localStorage 中的 JWT Token但 PHP Session 仍然有效,导致 `index.php` 第 15-23 行检测到有效 Session 后又 302 重定向到 dashboard形成循环。
【修改方式】在 `apiRequest` 函数的 401 处理逻辑中,`clearAuth()` 之后增加对 `/api/clear_session.php` 的 POST 调用,以同步清除 PHP Session
【修改内容】
-`clearAuth()` 调用后、路径检查之前,添加 `fetch('/api/clear_session.php', { method: 'POST', headers: { 'Content-Type': 'application/json' } })` 调用
- 使用 try-catch 包裹,失败时仅输出 console.warn 警告,不阻塞后续重定向逻辑
- `/api/clear_session.php` 是同源路径(由 Nginx 直接处理,不经过后端 FastAPI不需要 Authorization header也不会触发跨域问题
### 阶段 9修复 500 响应 CORS 头丢失 + admin.js 变量重复声明
- [x] 9.1 修复 500 响应 CORS 头丢失
【目标对象】`backend/middleware/auth_middleware.py`
【修改目的】当路由层抛出异常导致 500 时BaseHTTPMiddleware 的 call_next 返回的 500 响应不会经过 CORSMiddleware导致 CORS 头缺失。浏览器报告"CORS Missing Allow Origin",前端无法读取错误信息。
【修改方式】在 dispatch 方法中将 `return await call_next(request)` 改为 try-except 包裹,确保所有响应都有 CORS 头
【修改内容】
- 用 try-except 包裹 `call_next(request)` 调用
- 在 try 中获取 response 后检查是否已有 CORS 头,若无则补充
- 在 except 中捕获路由层异常,返回带 CORS 头的 500 响应
- [x] 9.2 修复 admin.js selectedStudentIds 重复声明
【目标对象】`frontend/assets/js/admin.js``frontend/admin/conduct.php``frontend/admin/homework.php`
【修改目的】admin.js 通过 footer.php 在所有 admin 页面加载,其 `let selectedStudentIds = []` 与 conduct.php 和 homework.php 页面级 `<script>` 中的同名声明冲突,导致 SyntaxError
【修改方式】将 `let` 改为 `var``var` 允许重复声明
【修改内容】
- admin.js 第12-16行`let``var`4个变量声明
- conduct.php 第59行`let selectedStudentIds = []``var selectedStudentIds = []`
- homework.php 第94行`let selectedStudentIds = []``var selectedStudentIds = []`