From c575d711eea50407d8674c10d6979efbae108b69 Mon Sep 17 00:00:00 2001 From: canglan Date: Tue, 26 May 2026 08:39:12 +0800 Subject: [PATCH] =?UTF-8?q?v2.0.1=E6=9B=B4=E6=96=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + backend/models/homework.py | 1 - backend/models/semester.py | 12 ++ backend/models/subject.py | 7 + backend/routes/admin.py | 3 +- backend/routes/debug.py | 4 + backend/schemas/admin.py | 1 + backend/services/conduct_service.py | 7 +- backend/services/semester_service.py | 4 + backend/services/subject_service.py | 5 + docs/cadre.md | 4 +- docs/guide/cadre.md | 2 +- docs/guide/teacher.md | 2 +- docs/teacher.md | 4 +- frontend/admin/conduct.php | 9 ++ frontend/admin/homework.php | 133 ++++++++++++++++++- frontend/admin/semesters.php | 1 + frontend/admin/subjects.php | 128 +----------------- frontend/api/clear_session.php | 3 +- frontend/api/save_session.php | 66 +++++++++- frontend/assets/css/style.css | 85 ++++++++++++ frontend/assets/js/admins.js | 13 +- frontend/assets/js/common.js | 68 +++++++++- frontend/assets/js/homework-manage.js | 154 +++++++++++++++++++--- frontend/assets/js/modules/points-mgmt.js | 10 +- frontend/assets/js/semesters.js | 25 +++- frontend/assets/js/student-homework.js | 19 ++- frontend/assets/js/students-manage.js | 15 ++- frontend/includes/nav.php | 5 +- frontend/index.php | 5 +- frontend/parent/history.php | 8 +- frontend/student/dashboard.php | 3 +- frontend/student/homework.php | 2 +- sql/upgrade_v2.0.sql | 143 ++++++++++++++++++++ 34 files changed, 750 insertions(+), 204 deletions(-) create mode 100644 sql/upgrade_v2.0.sql diff --git a/README.md b/README.md index 4ec343c..21fed3b 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ | 层级 | 技术 | 版本 | |------|------|------| +| 后端 | Python | 3.13.x | | 后端框架 | FastAPI | 0.104+ | | 数据库 | MySQL | 5.7 | | 缓存 | Redis | 7.x | @@ -269,6 +270,8 @@ classmanager/ | v1.5 | 2026.4.29 | 登录错误封禁5分钟+手动解锁、加减分回显修复、权限限制修复、按钮样式补全 | | v1.6 | 2026.4.29 | 权限修复:考勤委员提示遗漏、历史记录权限泄露、时间筛选失效、作业页分数限制与后端同步 | | v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 | +| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 | +| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 | ## 许可证 diff --git a/backend/models/homework.py b/backend/models/homework.py index ccddc47..f69584a 100644 --- a/backend/models/homework.py +++ b/backend/models/homework.py @@ -42,7 +42,6 @@ class HomeworkModel: """ return await execute_query(sql, tuple(subject_ids)) - @staticmethod @staticmethod async def get_student_homework(student_id: int) -> List[Dict[str, Any]]: sql = """ diff --git a/backend/models/semester.py b/backend/models/semester.py index b95ebf4..0feff9d 100644 --- a/backend/models/semester.py +++ b/backend/models/semester.py @@ -126,6 +126,18 @@ class SemesterModel: """ return await execute_query(sql, (semester_id, start_date, end_date)) + @staticmethod + async def count_records_by_semester(semester_id: int) -> Dict[str, int]: + """统计学期关联的记录数""" + conduct_sql = "SELECT COUNT(*) as cnt FROM conduct_records WHERE semester_id = %s" + attendance_sql = "SELECT COUNT(*) as cnt FROM attendance_records WHERE semester_id = %s" + conduct_result = await execute_one(conduct_sql, (semester_id,)) + attendance_result = await execute_one(attendance_sql, (semester_id,)) + return { + "conduct_count": conduct_result['cnt'] if conduct_result else 0, + "attendance_count": attendance_result['cnt'] if attendance_result else 0 + } + @staticmethod async def get_homework_stats_by_date_range(start_date: str, end_date: str) -> List[Dict]: """通过作业截止日期范围查询所有学生的作业提交统计""" diff --git a/backend/models/subject.py b/backend/models/subject.py index 2159b0e..eb3eff8 100644 --- a/backend/models/subject.py +++ b/backend/models/subject.py @@ -69,6 +69,13 @@ class SubjectModel: result = await execute_update(sql, tuple(params)) return result > 0 + @staticmethod + async def has_related_data(subject_id: int) -> bool: + """检查科目是否有关联数据(assignments表)""" + sql = "SELECT COUNT(*) as cnt FROM assignments WHERE subject_id = %s" + result = await execute_one(sql, (subject_id,)) + return result['cnt'] > 0 + @staticmethod async def delete(subject_id: int) -> bool: sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s" diff --git a/backend/routes/admin.py b/backend/routes/admin.py index bee850d..2a21980 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -212,7 +212,8 @@ async def add_conduct_points(request: Request, req: AddPointsRequest): points_change=req.points_change, reason=req.reason, recorder_id=user["user_id"], - recorder_name=user["username"] + recorder_name=user["real_name"], + related_type=req.related_type ) if result["success"]: try: diff --git a/backend/routes/debug.py b/backend/routes/debug.py index 45eb778..8592f56 100644 --- a/backend/routes/debug.py +++ b/backend/routes/debug.py @@ -37,6 +37,10 @@ async def debug_add_admin(request: Request, req: AddAdminDebugRequest): from fastapi.responses import JSONResponse return JSONResponse(status_code=404, content={"detail": "Not Found"}) + # 生产环境警告 + if settings.APP_ENV == "production": + logger.warning(f"调试入口在生产环境中被调用!路径: {settings.DEBUG_PATH}, 来源IP: {request.client.host}") + from models.user import UserModel valid_roles = ["班主任", "班长", "学习委员", "考勤委员", "劳动委员", "志愿委员"] diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index cfb58ff..1814602 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -19,6 +19,7 @@ class AddPointsRequest(BaseModel): 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="原因") + related_type: Optional[str] = Field(default='manual', pattern=r'^(manual|homework|attendance)$', description="关联类型: manual/homework/attendance") class AddPointsResponse(BaseModel): diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index ba08092..c4da850 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -31,7 +31,8 @@ class ConductService: points_change: int, reason: str, recorder_id: int, - recorder_name: str + recorder_name: str, + related_type: str = 'manual' ) -> Dict[str, Any]: """批量加减分""" # 输入校验 @@ -94,13 +95,13 @@ class ConductService: fail_count += 1 continue - # 创建记录 record_id = await ConductModel.create_record( student_id=student_id, points_change=points_change, reason=reason, recorder_id=recorder_id, - recorder_name=recorder_name + recorder_name=recorder_name, + related_type=related_type ) # 更新学生总分 diff --git a/backend/services/semester_service.py b/backend/services/semester_service.py index 5c9dce6..82ea56d 100644 --- a/backend/services/semester_service.py +++ b/backend/services/semester_service.py @@ -30,6 +30,10 @@ class SemesterService: """获取学期列表""" try: semesters = await SemesterModel.get_all() + for sem in semesters: + counts = await SemesterModel.count_records_by_semester(sem['semester_id']) + sem['conduct_count'] = counts['conduct_count'] + sem['attendance_count'] = counts['attendance_count'] return { "success": True, "semesters": semesters diff --git a/backend/services/subject_service.py b/backend/services/subject_service.py index 67cde98..2ab56cd 100644 --- a/backend/services/subject_service.py +++ b/backend/services/subject_service.py @@ -68,6 +68,11 @@ class SubjectService: @staticmethod async def delete_subject(subject_id: int) -> Dict[str, Any]: """删除科目(软删除)""" + # 检查科目是否有关联数据 + has_data = await SubjectModel.has_related_data(subject_id) + if has_data: + return {"success": False, "message": "该科目下已有作业数据,无法删除"} + result = await SubjectModel.delete(subject_id) if result: diff --git a/docs/cadre.md b/docs/cadre.md index 99dae92..378bb6f 100644 --- a/docs/cadre.md +++ b/docs/cadre.md @@ -13,7 +13,7 @@ ## 角色权限一览 -| 角色 | 操行分管理 | 历史记录 | 作业管理 | 考勤管理 | 科目管理 | +| 角色 | 操行分管理 | 历史记录 | 作业扣分 | 考勤管理 | 科目管理 | |------|-----------|---------|---------|---------|---------| | 班长 | ±5分以内 | 全部(可撤销) | - | - | - | | 学习委员 | ±5分以内 | 自己的 | ✓ | - | ✓ | @@ -47,7 +47,7 @@ ### 学习委员 -#### 作业管理 (homework.php) +#### 作业扣分 (homework.php) **批量扣分**: 1. 在学生列表中勾选目标学生 diff --git a/docs/guide/cadre.md b/docs/guide/cadre.md index d17b6b7..88d7bdf 100644 --- a/docs/guide/cadre.md +++ b/docs/guide/cadre.md @@ -14,7 +14,7 @@ ## 角色权限 -| 角色 | 操行分管理 | 历史记录 | 作业管理 | 考勤管理 | 科目管理 | +| 角色 | 操行分管理 | 历史记录 | 作业扣分 | 考勤管理 | 科目管理 | |------|-----------|---------|---------|---------|---------| | 班长 | ±5分以内 | 全部(可撤销) | - | - | - | | 学习委员 | ±5分以内 | 自己的 | ✓ | - | ✓ | diff --git a/docs/guide/teacher.md b/docs/guide/teacher.md index 746672f..8bb36e0 100644 --- a/docs/guide/teacher.md +++ b/docs/guide/teacher.md @@ -21,7 +21,7 @@ | 首页 | 查看学生总数、排行榜、快捷入口 | | 操行分管理 | 对学生加减分(无限制)、导出德育分记录 | | 历史记录 | 查看/导出/撤销全班记录,支持按扣分类型筛选 | -| 作业管理 | 发布缺交作业记录、关联扣分 | +| 作业扣分 | 发布缺交作业记录、关联扣分 | | 考勤管理 | 按时段(早上/中午/晚修)记录考勤、自定义扣分值 | | 学生管理 | 新增/编辑/删除/批量导入学生 | | 科目管理 | 增删改科目信息 | diff --git a/docs/teacher.md b/docs/teacher.md index 107d73a..a2e49a0 100644 --- a/docs/teacher.md +++ b/docs/teacher.md @@ -76,9 +76,9 @@ --- -### 4. 作业管理 (homework.php) +### 4. 作业扣分 (homework.php) -本模块用于管理学生作业缺交情况。 +本模块用于管理学生扣分记录。 #### 查看学生列表 - 展示所有学生的学号、姓名、当前操行分 diff --git a/frontend/admin/conduct.php b/frontend/admin/conduct.php index 8f3d780..c894432 100644 --- a/frontend/admin/conduct.php +++ b/frontend/admin/conduct.php @@ -75,6 +75,15 @@ include __DIR__ . '/../includes/header.php';
0 人
+
+ +
+ + + + +
+
diff --git a/frontend/admin/homework.php b/frontend/admin/homework.php index f7847e3..eaa6972 100644 --- a/frontend/admin/homework.php +++ b/frontend/admin/homework.php @@ -1,6 +1,6 @@
+ +
+
+

📚 科目管理

+ ▶ 展开 +
+ +
+
@@ -65,7 +79,7 @@ include __DIR__ . '/../includes/header.php';
未选择学生
- +
@@ -111,6 +128,114 @@ include __DIR__ . '/../includes/header.php';
+ + + + + + + + diff --git a/frontend/admin/semesters.php b/frontend/admin/semesters.php index 0416b2d..671f46b 100644 --- a/frontend/admin/semesters.php +++ b/frontend/admin/semesters.php @@ -42,6 +42,7 @@ include __DIR__ . '/../includes/header.php'; 开始日期 结束日期 状态 + 记录数 创建时间 操作 diff --git a/frontend/admin/subjects.php b/frontend/admin/subjects.php index 98e92cf..6fe47c4 100644 --- a/frontend/admin/subjects.php +++ b/frontend/admin/subjects.php @@ -1,6 +1,6 @@ - - - -
-
-
- -
-
-
-
- - - - - - - - - - - - - - \ No newline at end of file +header('Location: /admin/homework.php'); +exit(); diff --git a/frontend/api/clear_session.php b/frontend/api/clear_session.php index 48a2bea..13c84ed 100644 --- a/frontend/api/clear_session.php +++ b/frontend/api/clear_session.php @@ -18,8 +18,7 @@ require_once __DIR__ . '/../config.php'; // 设置响应头 header('Content-Type: application/json; charset=utf-8'); -// 允许跨域 -header('Access-Control-Allow-Origin: *'); +// 仅允许同源请求 header('Access-Control-Allow-Methods: POST, OPTIONS'); header('Access-Control-Allow-Headers: Content-Type'); diff --git a/frontend/api/save_session.php b/frontend/api/save_session.php index 5b6ab24..c1b3c6d 100644 --- a/frontend/api/save_session.php +++ b/frontend/api/save_session.php @@ -18,10 +18,9 @@ require_once __DIR__ . '/../config.php'; // 设置响应头 header('Content-Type: application/json; charset=utf-8'); -// 允许跨域(如果需要) -header('Access-Control-Allow-Origin: *'); +// 仅允许同源请求 header('Access-Control-Allow-Methods: POST, OPTIONS'); -header('Access-Control-Allow-Headers: Content-Type'); +header('Access-Control-Allow-Headers: Content-Type, Authorization'); // 处理预检请求 if ($_SERVER['REQUEST_METHOD'] === 'OPTIONS') { @@ -93,6 +92,67 @@ if (!in_array($data['user_type'], $validUserTypes)) { exit(); } +// 验证 JWT Token +$authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; +if (empty($authHeader) || !preg_match('/^Bearer\s+(.+)$/i', $authHeader, $matches)) { + http_response_code(401); + echo json_encode([ + 'success' => false, + 'message' => '缺少认证令牌' + ]); + exit(); +} + +$token = $matches[1]; +$apiUrl = API_BASE_URL . '/api/auth/me'; + +$ch = curl_init(); +curl_setopt_array($ch, [ + CURLOPT_URL => $apiUrl, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $token, + 'Content-Type: application/json' + ], + CURLOPT_SSL_VERIFYPEER => false, + CURLOPT_SSL_VERIFYHOST => 0 +]); + +$apiResponse = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +curl_close($ch); + +if ($httpCode !== 200 || empty($apiResponse)) { + http_response_code(401); + echo json_encode([ + 'success' => false, + 'message' => '认证令牌无效或已过期' + ]); + exit(); +} + +$tokenData = json_decode($apiResponse, true); +if (!$tokenData || !isset($tokenData['success']) || !$tokenData['success']) { + http_response_code(401); + echo json_encode([ + 'success' => false, + 'message' => '认证验证失败' + ]); + exit(); +} + +// 验证 token 中的 user_id 与请求数据中的 user_id 一致 +$tokenUserId = $tokenData['data']['user_id'] ?? null; +if ($tokenUserId === null || intval($tokenUserId) !== intval($data['user_id'])) { + http_response_code(403); + echo json_encode([ + 'success' => false, + 'message' => '身份验证不匹配' + ]); + exit(); +} + // 设置 Session 变量 $_SESSION['user_id'] = $data['user_id']; $_SESSION['user_type'] = $data['user_type']; diff --git a/frontend/assets/css/style.css b/frontend/assets/css/style.css index 95a001b..5035433 100644 --- a/frontend/assets/css/style.css +++ b/frontend/assets/css/style.css @@ -744,4 +744,89 @@ tr:hover { .search-bar input { flex: 1; } +} + +/* ========== 操作列下拉菜单 ========== */ +.action-dropdown { + position: relative; + display: inline-flex; + align-items: center; + gap: 4px; +} + +.action-dropdown-toggle { + background: #f7fafc; + color: #4a5568; + border: 1px solid #e2e8f0; + padding: 4px 10px; + font-size: 12px; + cursor: pointer; + border-radius: 6px; + transition: all 0.2s; + white-space: nowrap; +} + +.action-dropdown-toggle:hover { + background: #edf2f7; + border-color: #cbd5e0; +} + +.action-dropdown-toggle.open { + background: #edf2f7; + border-color: #a0aec0; +} + +.action-dropdown-menu { + display: none; + position: absolute; + top: 100%; + right: 0; + margin-top: 4px; + background: white; + border-radius: 8px; + box-shadow: 0 4px 16px rgba(0, 0, 0, 0.12); + border: 1px solid #e2e8f0; + min-width: 120px; + z-index: 200; + overflow: hidden; + padding: 4px 0; +} + +.action-dropdown-menu.show { + display: block; +} + +.action-dropdown-menu a { + display: block; + padding: 8px 14px; + color: #4a5568; + font-size: 13px; + cursor: pointer; + text-decoration: none; + transition: background 0.15s; + white-space: nowrap; +} + +.action-dropdown-menu a:hover { + background: #f7fafc; + color: #2d3748; +} + +.action-dropdown-menu a.danger { + color: #e53e3e; + border-top: 1px solid #edf2f7; + margin-top: 4px; + padding-top: 10px; +} + +.action-dropdown-menu a.danger:hover { + background: #fff5f5; + color: #c53030; +} + +@media (max-width: 768px) { + .action-dropdown-menu { + right: auto; + left: 0; + } } \ No newline at end of file diff --git a/frontend/assets/js/admins.js b/frontend/assets/js/admins.js index e18a5b4..c4e2f06 100644 --- a/frontend/assets/js/admins.js +++ b/frontend/assets/js/admins.js @@ -23,10 +23,15 @@ async function loadAdmins() { ${escapeHtml(admin.real_name)} ${escapeHtml(admin.role_type)} - - - - +
+ + +
`; }); diff --git a/frontend/assets/js/common.js b/frontend/assets/js/common.js index a20a5ed..c57cd0f 100644 --- a/frontend/assets/js/common.js +++ b/frontend/assets/js/common.js @@ -197,12 +197,12 @@ async function logout() { function escapeHtml(str) { if (!str) return ''; - return str.replace(/[&<>]/g, function(m) { - if (m === '&') return '&'; - if (m === '<') return '<'; - if (m === '>') return '>'; - return m; - }); + return String(str) + .replace(/&/g, '\x26amp;') + .replace(//g, '\x26gt;') + .replace(/"/g, '\x26quot;') + .replace(/'/g, '\x26#x27;'); } /** @@ -336,6 +336,36 @@ document.addEventListener('DOMContentLoaded', () => { } }); +function toggleActionDropdown(el) { + var dropdown = el.closest('.action-dropdown'); + if (!dropdown) return; + var menu = dropdown.querySelector('.action-dropdown-menu'); + if (!menu) return; + + var isOpen = menu.classList.contains('show'); + // 先关闭所有 + document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) { + m.classList.remove('show'); + var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle'); + if (toggle) toggle.classList.remove('open'); + }); + + if (!isOpen) { + menu.classList.add('show'); + el.classList.add('open'); + } +} + +document.addEventListener('click', function(e) { + if (!e.target.closest('.action-dropdown')) { + document.querySelectorAll('.action-dropdown-menu.show').forEach(function(m) { + m.classList.remove('show'); + var toggle = m.closest('.action-dropdown').querySelector('.action-dropdown-toggle'); + if (toggle) toggle.classList.remove('open'); + }); + } +}); + // 全局textarea键盘事件:Enter提交表单,Ctrl+Enter换行 document.addEventListener('keydown', function(e) { if (e.target.tagName !== 'TEXTAREA') return; @@ -351,4 +381,28 @@ document.addEventListener('keydown', function(e) { } } // Ctrl+Enter和Shift+Enter保持默认换行行为(不拦截) -}); \ No newline at end of file +}); + +window.selectDeductionType = function(points, reason) { + var pointsEl = document.getElementById('pointsChange'); + var reasonEl = document.getElementById('pointsReason'); + if (points === 0 && reason === '') { + // 自定义模式 - 清空分值和原因,聚焦原因输入框 + if (pointsEl) pointsEl.value = ''; + if (reasonEl) { + reasonEl.value = ''; + reasonEl.focus(); + } + } else if (points === null || points === undefined) { + // 类别模式 - 仅填充原因,聚焦分值输入框 + if (reasonEl) reasonEl.value = reason; + if (pointsEl) { + pointsEl.value = ''; + pointsEl.focus(); + } + } else { + // 预设模式 - 同时填充分值和原因 + if (pointsEl) pointsEl.value = points; + if (reasonEl) reasonEl.value = reason; + } +}; \ No newline at end of file diff --git a/frontend/assets/js/homework-manage.js b/frontend/assets/js/homework-manage.js index 247684d..c550650 100644 --- a/frontend/assets/js/homework-manage.js +++ b/frontend/assets/js/homework-manage.js @@ -1,5 +1,5 @@ /** - * 班级操行分管理系统 - 作业管理页JS + * 班级操行分管理系统 - 作业扣分页JS * * 开发者: Canglan * 版权归属: Sea Network Technology Studio @@ -28,7 +28,6 @@ document.getElementById('pointsChange').setAttribute('max', hwMaxPoints); // 加载科目列表(学习委员) async function loadSubjectsForHomework() { - if (hwRole !== '学习委员') return; const subjectSelect = document.getElementById('hwSubjectSelect'); if (!subjectSelect) return; const res = await apiGet('/api/subject/list'); @@ -69,16 +68,6 @@ function showSinglePointsModal(studentId, studentName) { document.getElementById('batchPointsModal').style.display = 'flex'; } -function selectDeductionType(points, reason) { - document.getElementById('pointsChange').value = points; - if (points !== 0) { - document.getElementById('pointsReason').value = reason; - } else { - document.getElementById('pointsReason').value = ''; - document.getElementById('pointsReason').focus(); - } -} - function handleSubmitPoints() { const pointsChange = parseInt(document.getElementById('pointsChange').value); if (isNaN(pointsChange) || pointsChange === 0) { @@ -91,7 +80,7 @@ function handleSubmitPoints() { } // 学习委员附加科目前缀、具体作业和缴交时间 - if (hwRole === '学习委员') { + if (hwRole === '学习委员' || hwRole === '班主任') { const subjectSelect = document.getElementById('hwSubjectSelect'); const subjectName = subjectSelect ? subjectSelect.value : ''; const hwTitle = document.getElementById('hwTitle').value.trim(); @@ -113,7 +102,136 @@ function handleSubmitPoints() { } } - submitBatchPoints(); + submitBatchPoints({ related_type: 'homework' }); +} + +// ========== 科目管理功能 ========== + +function toggleSubjectPanel() { + const content = document.getElementById('subjectPanelContent'); + const toggle = document.getElementById('subjectPanelToggle'); + if (content.style.display === 'none') { + content.style.display = 'block'; + toggle.textContent = '▼ 收起'; + loadSubjectList(); + } else { + content.style.display = 'none'; + toggle.textContent = '▶ 展开'; + } +} + +async function loadSubjectList() { + const res = await apiGet('/api/subject/list'); + if (res && res.success) { + let html = ''; + res.data.subjects.forEach(sub => { + html += ` +
+ ${escapeHtml(sub.subject_name)} + ${escapeHtml(sub.subject_code || '')} + + ${sub.is_active ? '启用' : '禁用'} + + + + +
+ `; + }); + if (res.data.subjects.length === 0) { + html = '

暂无科目,请点击"添加科目"

'; + } + document.getElementById('subjectList').innerHTML = html; + } +} + +function showAddSubjectModal() { + const form = document.getElementById('addSubjectFormInHw'); + if (form) form.reset(); + document.getElementById('addSubjectModal').style.display = 'flex'; +} + +async function submitAddSubject() { + const subjectName = document.getElementById('subjectName').value.trim(); + const subjectCode = document.getElementById('subjectCode').value.trim(); + + if (!subjectName) { + showToast('请填写科目名称', 'warning'); + return; + } + + const res = await apiPost('/api/subject/create', { + subject_name: subjectName, + subject_code: subjectCode + }); + + if (res && res.success) { + showToast('科目添加成功'); + closeModal('addSubjectModal'); + loadSubjectList(); + loadSubjectsForHomework(); + } else { + showToast(res?.message || '添加失败', 'error'); + } +} + +async function toggleSubjectStatus(subjectId, enable) { + const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable }); + if (res && res.success) { + showToast(enable ? '科目已启用' : '科目已禁用'); + loadSubjectList(); + loadSubjectsForHomework(); + } else { + showToast(res?.message || '操作失败', 'error'); + } +} + +async function deleteSubject(subjectId) { + if (!confirm('确定要删除该科目吗?如果科目下有作业数据将无法删除。')) return; + const res = await apiDelete('/api/subject/delete/' + subjectId); + if (res && res.success) { + showToast('科目删除成功'); + loadSubjectList(); + loadSubjectsForHomework(); + } else { + showToast(res?.message || '删除失败', 'error'); + } +} + +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'); + loadSubjectList(); + loadSubjectsForHomework(); + } else { + showToast(res?.message || '更新失败', 'error'); + } } loadStudents(); @@ -121,7 +239,13 @@ loadSubjectsForHomework(); window.loadStudents = loadStudents; window.showSinglePointsModal = showSinglePointsModal; -window.selectDeductionType = selectDeductionType; window.handleSubmitPoints = handleSubmitPoints; +window.toggleSubjectPanel = toggleSubjectPanel; +window.showAddSubjectModal = showAddSubjectModal; +window.submitAddSubject = submitAddSubject; +window.toggleSubjectStatus = toggleSubjectStatus; +window.deleteSubject = deleteSubject; +window.showEditSubjectModal = showEditSubjectModal; +window.submitEditSubject = submitEditSubject; })(); diff --git a/frontend/assets/js/modules/points-mgmt.js b/frontend/assets/js/modules/points-mgmt.js index 2edff8b..4451f27 100644 --- a/frontend/assets/js/modules/points-mgmt.js +++ b/frontend/assets/js/modules/points-mgmt.js @@ -35,7 +35,7 @@ } // 提交批量加减分 - async function submitBatchPoints() { + async function submitBatchPoints(options = {}) { const pointsChange = parseInt(document.getElementById('pointsChange').value); const reason = document.getElementById('pointsReason').value; @@ -49,11 +49,15 @@ return; } - const res = await apiPost('/api/admin/conduct/add', { + const data = { student_ids: selectedStudentIds, points_change: pointsChange, reason: reason - }); + }; + if (options.related_type) { + data.related_type = options.related_type; + } + const res = await apiPost('/api/admin/conduct/add', data); if (res && res.success) { showToast(`操作成功: ${res.data.success_count} 人成功`); diff --git a/frontend/assets/js/semesters.js b/frontend/assets/js/semesters.js index 832d427..bd58d18 100644 --- a/frontend/assets/js/semesters.js +++ b/frontend/assets/js/semesters.js @@ -60,28 +60,39 @@ async function loadSemesters() { const startDate = sem.start_date || ''; const endDate = sem.end_date || ''; if (!sem.is_archived) { - actions += ` `; - if (!sem.is_active) { - actions += ` `; - } - actions += ` `; - actions += ` `; + actions += `
+ +
+ 编辑 + ${!sem.is_active ? `激活` : ''} + 关联数据 + 归档 +
+
`; } if (sem.is_archived) { actions += ``; } + const conductCount = sem.conduct_count || 0; + const attendanceCount = sem.attendance_count || 0; + let recordText = '-'; + if (conductCount > 0 || attendanceCount > 0) { + recordText = `${conductCount}条操行分 / ${attendanceCount}条考勤`; + } + html += ` ${escapeHtml(sem.semester_name)} ${formatDate(sem.start_date)} ${formatDate(sem.end_date)} ${statusText} + ${recordText} ${formatDateTime(sem.created_at)} ${actions} `; }); if (semesters.length === 0) { - html = '暂无学期,请点击上方按钮创建新学期'; + html = '暂无学期,请点击上方按钮创建新学期'; } document.getElementById('semesterList').innerHTML = html; } diff --git a/frontend/assets/js/student-homework.js b/frontend/assets/js/student-homework.js index 0886a66..2b2a57c 100644 --- a/frontend/assets/js/student-homework.js +++ b/frontend/assets/js/student-homework.js @@ -17,17 +17,24 @@ async function loadHomework() { if (res && res.success) { let html = ''; res.data.homework.forEach(hw => { - const pointsDisplay = hw.points ? hw.points + '分' : '-'; + // 提交状态 + let statusDisplay = '-'; + if (hw.status) { + statusDisplay = getStatusBadge(hw.status, 'homework'); + } + // 扣分显示 + const pointsDisplay = hw.points ? `${hw.points}分` : '-'; + html += ` - ${escapeHtml(hw.subject_name)} - ${hw.deadline || hw.created_at} - ${pointsDisplay} - ${escapeHtml(hw.comments || '-')} ${escapeHtml(hw.title)} + ${escapeHtml(hw.subject_name)} + ${hw.deadline || '-'} + ${statusDisplay} + ${pointsDisplay} `; }); if (res.data.homework.length === 0) { - html = '暂无作业'; + html = '📝 暂无作业记录'; } document.getElementById('homeworkList').innerHTML = html; } diff --git a/frontend/assets/js/students-manage.js b/frontend/assets/js/students-manage.js index 759a26b..59f2522 100644 --- a/frontend/assets/js/students-manage.js +++ b/frontend/assets/js/students-manage.js @@ -30,11 +30,16 @@ async function loadStudents(page = 1) { ${student.total_points} ${userRole === '班主任' ? `${student.parent_phone ? student.parent_phone.slice(0,3) + '******' + student.parent_phone.slice(-2) : '-'}` : ''} - - ${userRole === '班主任' ? ` - - - ` : ''} +
+ + ${userRole === '班主任' ? ` + ` : ''} +
`; }); diff --git a/frontend/includes/nav.php b/frontend/includes/nav.php index 7e5fcfa..a3b8631 100644 --- a/frontend/includes/nav.php +++ b/frontend/includes/nav.php @@ -5,14 +5,11 @@ 操行分管理 - 作业管理 + 作业扣分 考勤管理 - - 科目管理 - 管理员管理 diff --git a/frontend/index.php b/frontend/index.php index 700f751..1b845f6 100644 --- a/frontend/index.php +++ b/frontend/index.php @@ -95,7 +95,10 @@ if (isset($_SESSION['user_id']) && isset($_SESSION['user_type'])) { try { const sessionResponse = await fetch('/api/save_session.php', { method: 'POST', - headers: { 'Content-Type': 'application/json' }, + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer ' + userData.token + }, body: JSON.stringify({ user_id: userData.user_id, user_type: userData.user_type, diff --git a/frontend/parent/history.php b/frontend/parent/history.php index b6c5538..58b6823 100644 --- a/frontend/parent/history.php +++ b/frontend/parent/history.php @@ -35,14 +35,13 @@ include __DIR__ . '/../includes/header.php'; 日期 - 类型 原因 分值 记录人 - 加载中... + 加载中...
@@ -76,17 +75,16 @@ async function loadHistory(page) { if (res && res.success) { let html = ''; if (res.data.records.length === 0) { - html = '暂无记录'; + html = '暂无记录'; } else { res.data.records.forEach(record => { const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; const pointsText = record.points_change > 0 ? `+${record.points_change}` : record.points_change; html += ` ${formatDateTime(record.created_at)} - ${escapeHtml(record.related_type || '手动')} ${escapeHtml(record.reason || '-')} ${pointsText} - ${escapeHtml(record.recorder_name || '-')} + 班主任 `; }); } diff --git a/frontend/student/dashboard.php b/frontend/student/dashboard.php index 40fc616..9e795d6 100644 --- a/frontend/student/dashboard.php +++ b/frontend/student/dashboard.php @@ -356,12 +356,13 @@ include __DIR__ . '/../includes/header.php'; let html = '
'; res.data.records.forEach(record => { const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; + const recorderDisplay = record.points_change < 0 ? '班主任' : escapeHtml(record.recorder_name || '班主任'); html += ` - + `; }); diff --git a/frontend/student/homework.php b/frontend/student/homework.php index 0c8b5a0..c951b77 100644 --- a/frontend/student/homework.php +++ b/frontend/student/homework.php @@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
时间分数变动原因操作人
${formatDateTime(record.created_at)} ${record.points_change > 0 ? '+' : ''}${record.points_change} ${escapeHtml(record.reason)}${escapeHtml(record.recorder_name)}${recorderDisplay}
- +
科目时间分值备注作业
作业名称科目截止时间提交状态扣分
diff --git a/sql/upgrade_v2.0.sql b/sql/upgrade_v2.0.sql new file mode 100644 index 0000000..3769b7f --- /dev/null +++ b/sql/upgrade_v2.0.sql @@ -0,0 +1,143 @@ +-- =========================================== +-- 班级操行分管理系统 - v2.0 数据库迁移脚本 +-- 适用版本: v1.8 → v2.0 +-- 字符集: utf8mb4 +-- +-- 说明: +-- v2.0 主要为应用层代码变更(UI折叠菜单、扣分类型扩展、 +-- 科目选择修复、页面改名、记录人优化、学期管理优化等), +-- 数据库 schema 变更较少。本脚本主要处理历史数据迁移 +-- 和索引优化。 +-- +-- 迁移内容: +-- 1. 将 conduct_records.recorder_name 从用户名更新为真实姓名 +-- 2. 将 attendance_records 相关记录中的 recorder_name 同步更新 +-- 3. 确保 semester_id 索引存在(学期记录数统计优化) +-- 4. 数据验证 +-- +-- 重要: 执行前请备份数据库! +-- =========================================== + +USE `classmanagerdb`; + +-- =========================================== +-- 1. 更新 conduct_records 中的 recorder_name +-- v2.0 将 recorder_name 从用户名(username)改为真实姓名(real_name) +-- 需要将历史记录中的用户名更新为对应的真实姓名 +-- =========================================== + +-- 通过 JOIN users 表将 recorder_name 从 username 更新为 real_name +UPDATE conduct_records cr +INNER JOIN users u ON cr.recorder_id = u.user_id +SET cr.recorder_name = u.real_name +WHERE cr.recorder_name != u.real_name + OR cr.recorder_name IS NULL; + +-- =========================================== +-- 2. 确保学期相关索引存在 +-- v2.0 新增学期记录数统计功能,需要 semester_id 索引优化查询 +-- =========================================== + +SET @dbname = DATABASE(); + +-- conduct_records 表 semester_id 索引 +SET @indexname = 'idx_conduct_semester_id'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'conduct_records' AND INDEX_NAME = @indexname) > 0, + 'SELECT 1', + 'ALTER TABLE conduct_records ADD INDEX idx_conduct_semester_id (semester_id)' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- attendance_records 表 semester_id 索引 +SET @indexname = 'idx_attendance_semester_id'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'attendance_records' AND INDEX_NAME = @indexname) > 0, + 'SELECT 1', + 'ALTER TABLE attendance_records ADD INDEX idx_attendance_semester_id (semester_id)' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- =========================================== +-- 3. 确保之前版本的索引也存在(幂等兼容) +-- =========================================== + +-- assignments 表 subject_id 索引(v1.8 科目删除数据检查) +SET @indexname = 'idx_assignments_subject_id'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'assignments' AND INDEX_NAME = @indexname) > 0, + 'SELECT 1', + 'ALTER TABLE assignments ADD INDEX idx_assignments_subject_id (subject_id)' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- conduct_records 表 student_id 索引 +SET @indexname = 'idx_conduct_student_id'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'conduct_records' AND INDEX_NAME = @indexname) > 0, + 'SELECT 1', + 'ALTER TABLE conduct_records ADD INDEX idx_conduct_student_id (student_id)' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- conduct_records 表 created_at 索引 +SET @indexname = 'idx_conduct_created_at'; +SET @preparedStatement = (SELECT IF( + (SELECT COUNT(*) FROM INFORMATION_SCHEMA.STATISTICS + WHERE TABLE_SCHEMA = @dbname AND TABLE_NAME = 'conduct_records' AND INDEX_NAME = @indexname) > 0, + 'SELECT 1', + 'ALTER TABLE conduct_records ADD INDEX idx_conduct_created_at (created_at)' +)); +PREPARE alterIfNotExists FROM @preparedStatement; +EXECUTE alterIfNotExists; +DEALLOCATE PREPARE alterIfNotExists; + +-- =========================================== +-- 4. 验证迁移结果 +-- =========================================== + +SELECT '===== v2.0 数据库迁移验证 =====' AS ''; + +-- 检查 recorder_name 是否已更新为真实姓名 +SELECT + 'recorder_name 迁移验证' AS `检查项`, + COUNT(*) AS `总记录数`, + SUM(CASE WHEN cr.recorder_name = u.real_name THEN 1 ELSE 0 END) AS `已匹配真实姓名`, + SUM(CASE WHEN cr.recorder_name != u.real_name AND u.real_name IS NOT NULL THEN 1 ELSE 0 END) AS `仍为用户名` +FROM conduct_records cr +INNER JOIN users u ON cr.recorder_id = u.user_id; + +-- 检查学期相关索引 +SELECT + '学期索引验证' AS `检查项`, + TABLE_NAME AS `表名`, + INDEX_NAME AS `索引名` +FROM INFORMATION_SCHEMA.STATISTICS +WHERE TABLE_SCHEMA = @dbname + AND INDEX_NAME IN ('idx_conduct_semester_id', 'idx_attendance_semester_id') +GROUP BY TABLE_NAME, INDEX_NAME; + +-- 检查学期记录数统计示例 +SELECT + '学期记录统计示例' AS `检查项`, + s.semester_name, + s.is_active, + (SELECT COUNT(*) FROM conduct_records WHERE semester_id = s.semester_id) AS `操行分记录数`, + (SELECT COUNT(*) FROM attendance_records WHERE semester_id = s.semester_id) AS `考勤记录数` +FROM semesters s +ORDER BY s.is_active DESC, s.created_at DESC +LIMIT 5; + +SELECT 'v2.0 数据库迁移完成!' AS message;