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';
暂无科目,请点击"添加科目"
'; + } + 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 += `| 时间 | 分数变动 | 原因 | 操作人 | |
|---|---|---|---|---|
| ${formatDateTime(record.created_at)} | ${record.points_change > 0 ? '+' : ''}${record.points_change} | ${escapeHtml(record.reason)} | -${escapeHtml(record.recorder_name)} | +${recorderDisplay} |
| 科目 | 时间 | 分值 | 备注 | 作业 |
|---|---|---|---|---|
| 作业名称 | 科目 | 截止时间 | 提交状态 | 扣分 |