diff --git a/.gitignore b/.gitignore index 0e10557..eca4a9a 100644 --- a/.gitignore +++ b/.gitignore @@ -52,5 +52,5 @@ docs/guide/student.pdf docs/guide/teacher.pdf qrcode.png -# 展示内容 +# example example \ No newline at end of file diff --git a/README.md b/README.md index cfd8f91..de0e323 100644 --- a/README.md +++ b/README.md @@ -272,7 +272,7 @@ classmanager/ | 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 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 | -| v2.1 | 2025.7.14 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 | +| v2.1 | 2026.5.26 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 | ## 许可证 diff --git a/VERSION b/VERSION index 879b416..8bbe6cf 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -2.1 +2.2 diff --git a/backend/main.py b/backend/main.py index d23f649..2b7f9bc 100644 --- a/backend/main.py +++ b/backend/main.py @@ -21,7 +21,7 @@ from utils.logger import setup_logger, log_access from utils.database import init_db_pool, close_db_pool from utils.redis_client import init_redis_pool, close_redis_pool from middleware.auth_middleware import AuthMiddleware -from routes import auth, student, parent, admin, subject, semester, debug +from routes import auth, student, parent, admin, subject, semester, debug, upgrade from routes.config import router as config_router @@ -119,6 +119,7 @@ app.include_router(admin.router, prefix="/api/admin", tags=["管理端"]) app.include_router(subject.router, prefix="/api/subject", tags=["科目管理"]) app.include_router(semester.router, prefix="/api/semester", tags=["学期管理"]) app.include_router(config_router, prefix="/api/config", tags=["配置"]) +app.include_router(upgrade.router, prefix="/api/upgrade", tags=["升级管理"]) app.include_router(debug.router, tags=["调试"]) diff --git a/backend/models/conduct.py b/backend/models/conduct.py index 5725de1..8c6140d 100644 --- a/backend/models/conduct.py +++ b/backend/models/conduct.py @@ -209,6 +209,7 @@ class ConductModel: student_id: int = None, start_date: str = None, end_date: str = None, + related_type: str = None, page: int = 1, page_size: int = 20 ) -> Dict[str, Any]: @@ -217,6 +218,8 @@ class ConductModel: start_date = None if end_date == "": end_date = None + if related_type == "": + related_type = None conditions = ["cr.is_revoked = 0"] params = [] @@ -230,6 +233,9 @@ class ConductModel: if end_date: conditions.append("cr.created_at <= %s") params.append(end_date + ' 23:59:59') + if related_type: + conditions.append("cr.related_type = %s") + params.append(related_type) where_clause = " AND ".join(conditions) diff --git a/backend/models/homework.py b/backend/models/homework.py deleted file mode 100644 index f69584a..0000000 --- a/backend/models/homework.py +++ /dev/null @@ -1,117 +0,0 @@ -# =========================================== -# 班级操行分管理系统 - 后端服务 -# -# 开发者: Canglan -# 联系方式: admin@sea-studio.top -# 版权归属: Sea Network Technology Studio -# 许可证: MIT License -# -# 版权所有 © Sea Network Technology Studio -# =========================================== - -from typing import Optional, Dict, Any, List -from utils.database import execute_one, execute_query, execute_insert, execute_update - - -class HomeworkModel: - """作业数据模型""" - - @staticmethod - async def get_all_assignments() -> List[Dict[str, Any]]: - sql = """ - SELECT a.*, s.subject_name, u.real_name as created_by_name - FROM assignments a - JOIN subjects s ON a.subject_id = s.subject_id - JOIN users u ON a.created_by = u.user_id - ORDER BY a.deadline ASC, a.created_at DESC - """ - return await execute_query(sql) - - @staticmethod - async def get_assignments_by_subjects(subject_ids: List[int]) -> List[Dict[str, Any]]: - if not subject_ids: - return [] - placeholders = ','.join(['%s'] * len(subject_ids)) - sql = f""" - SELECT a.*, s.subject_name, u.real_name as created_by_name - FROM assignments a - JOIN subjects s ON a.subject_id = s.subject_id - JOIN users u ON a.created_by = u.user_id - WHERE a.subject_id IN ({placeholders}) - ORDER BY a.deadline ASC, a.created_at DESC - """ - return await execute_query(sql, tuple(subject_ids)) - - @staticmethod - async def get_student_homework(student_id: int) -> List[Dict[str, Any]]: - sql = """ - SELECT a.assignment_id, a.title, a.description, a.deadline, a.created_at, - s.subject_name, hs.status, hs.submit_time, hs.comments, hs.deduction_applied, - cr.points_change AS points - FROM assignments a - JOIN subjects s ON a.subject_id = s.subject_id - LEFT JOIN homework_submissions hs ON a.assignment_id = hs.assignment_id AND hs.student_id = %s - LEFT JOIN conduct_records cr ON cr.related_type = 'homework' - AND cr.related_id = a.assignment_id AND cr.student_id = %s AND cr.is_revoked = 0 - GROUP BY a.assignment_id - ORDER BY a.deadline ASC, a.created_at DESC - """ - return await execute_query(sql, (student_id, student_id)) - @staticmethod - async def get_submission(submission_id: int) -> Optional[Dict[str, Any]]: - sql = """ - SELECT hs.*, a.title, a.subject_id, a.assignment_id, s.name as student_name - FROM homework_submissions hs - JOIN assignments a ON hs.assignment_id = a.assignment_id - JOIN students s ON hs.student_id = s.student_id - WHERE hs.submission_id = %s - """ - return await execute_one(sql, (submission_id,)) - - @staticmethod - async def create_assignment( - subject_id: int, - title: str, - description: str, - deadline: str, - created_by: int - ) -> int: - sql = """ - INSERT INTO assignments (subject_id, title, description, deadline, created_by) - VALUES (%s, %s, %s, %s, %s) - """ - assignment_id = await execute_insert(sql, (subject_id, title, description, deadline, created_by)) - - # 为所有学生创建提交记录 - from models.student import StudentModel - students = await StudentModel.get_all(include_disabled=False) - - for student in students: - sql_sub = """ - INSERT INTO homework_submissions (assignment_id, student_id, status) - VALUES (%s, %s, 'not_submitted') - """ - await execute_insert(sql_sub, (assignment_id, student["student_id"])) - - return assignment_id - - @staticmethod - async def update_submission( - submission_id: int, - status: str, - comments: str = None, - updated_by: int = None - ) -> bool: - sql = """ - UPDATE homework_submissions - SET status = %s, comments = %s, updated_by = %s, updated_at = NOW() - WHERE submission_id = %s - """ - result = await execute_update(sql, (status, comments, updated_by, submission_id)) - return result > 0 - - @staticmethod - async def mark_deduction_applied(submission_id: int) -> bool: - sql = "UPDATE homework_submissions SET deduction_applied = 1 WHERE submission_id = %s" - result = await execute_update(sql, (submission_id,)) - return result > 0 \ No newline at end of file diff --git a/backend/models/student.py b/backend/models/student.py index befbbac..26fd61a 100644 --- a/backend/models/student.py +++ b/backend/models/student.py @@ -53,6 +53,18 @@ class StudentModel: sql += " ORDER BY student_no" return await execute_query(sql) + @staticmethod + async def get_dormitory_list() -> List[str]: + """获取所有不重复的宿舍号列表""" + sql = """ + SELECT DISTINCT dormitory_number + FROM students + WHERE status = 1 AND dormitory_number IS NOT NULL AND dormitory_number != '' + ORDER BY dormitory_number + """ + rows = await execute_query(sql) + return [row["dormitory_number"] for row in rows] + @staticmethod async def create( student_no: str, diff --git a/backend/routes/admin.py b/backend/routes/admin.py index 2a21980..8a19f9a 100644 --- a/backend/routes/admin.py +++ b/backend/routes/admin.py @@ -20,16 +20,15 @@ from middleware.permission import ( ) from services.admin_service import AdminService from services.conduct_service import ConductService -from services.homework_service import HomeworkService from services.attendance_service import AttendanceService from services.log_service import LogService from utils.redis_client import RedisClient from schemas.admin import ( AddPointsRequest, RevokeRequest, AddAdminRequest, AddStudentRequest, UpdateStudentRequest, - UpdateHomeworkStatusRequest, AddAttendanceRequest, + AddAttendanceRequest, UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest, - CreateAssignmentRequest, UnlockUserRequest + UnlockUserRequest ) from utils.response import success_response, error_response from utils.logger import get_logger @@ -41,18 +40,31 @@ logger = get_logger(__name__) # ========== 学生管理 ========== +@router.get("/students/dormitories") +async def get_dormitory_list(request: Request): + """获取宿舍号列表""" + user = await get_current_user(request) + if user["user_type"] != "admin": + return error_response(message="仅管理员可查看", code=403) + + from models.student import StudentModel + dormitories = await StudentModel.get_dormitory_list() + return success_response(data={"dormitories": dormitories}) + + @router.get("/students") async def get_students( request: Request, page: int = Query(1, ge=1), page_size: int = Query(20, ge=1, le=1000), - search: Optional[str] = None + search: Optional[str] = None, + dormitory_number: Optional[str] = None ): """获取所有学生列表(单班级)""" user = await get_current_user(request) if user["user_type"] != "admin": return error_response(message="仅管理员可查看学生列表", code=403) - result = await AdminService.get_students(page=page, page_size=page_size, search=search) + result = await AdminService.get_students(page=page, page_size=page_size, search=search, dormitory_number=dormitory_number) return success_response(data=result) @@ -139,7 +151,8 @@ async def update_student(request: Request, student_id: int, req: UpdateStudentRe result = await AdminService.update_student( student_id=student_id, name=req.name, - parent_phone=req.parent_phone + parent_phone=req.parent_phone, + dormitory_number=req.dormitory_number ) if result["success"]: await LogService.write_operation_log( @@ -207,6 +220,9 @@ async def reset_student_password(request: Request, student_id: int, req: ResetPa async def add_conduct_points(request: Request, req: AddPointsRequest): """批量加减分""" user = await get_current_user(request) + # 仅管理员(班主任/班干部)可操作 + if user["user_type"] != "admin": + return error_response(message="无权进行此操作", code=403) result = await ConductService.add_points( student_ids=req.student_ids, points_change=req.points_change, @@ -236,6 +252,9 @@ async def add_conduct_points(request: Request, req: AddPointsRequest): async def revoke_conduct_record(request: Request, req: RevokeRequest): """撤销扣分记录""" user = await get_current_user(request) + # 仅管理员(班主任/班干部)可操作 + if user["user_type"] != "admin": + return error_response(message="无权进行此操作", code=403) result = await ConductService.revoke_record( record_id=req.record_id, revoker_id=user["user_id"] @@ -264,6 +283,9 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest): async def restore_conduct_record(request: Request, req: RevokeRequest): """反撤销(恢复)已撤销的记录""" user = await get_current_user(request) + # 仅管理员(班主任/班干部)可操作 + if user["user_type"] != "admin": + return error_response(message="无权进行此操作", code=403) result = await ConductService.restore_record( record_id=req.record_id, restorer_id=user["user_id"] @@ -319,86 +341,6 @@ async def get_conduct_history( return error_response(message=f"获取历史记录失败: {str(e)}") -# ========== 作业管理 ========== - -@router.get("/homework/assignments") -async def get_assignments(request: Request): - """获取作业列表""" - user = await get_current_user(request) - role = await PermissionChecker.get_user_role(user["user_id"]) - if role not in ["班主任", "学习委员"]: - return error_response(message="无权限", code=403) - result = await HomeworkService.get_assignments(user["user_id"]) - return success_response(data=result) - - -@router.get("/homework/submissions/{assignment_id}") -async def get_submissions(request: Request, assignment_id: int): - """获取作业提交记录""" - user = await get_current_user(request) - role = await PermissionChecker.get_user_role(user["user_id"]) - if role not in ["班主任", "学习委员"]: - return error_response(message="无权限", code=403) - result = await HomeworkService.get_submissions( - assignment_id=assignment_id, - user_id=user["user_id"] - ) - return success_response(data=result) - - -@router.post("/homework/assignment") -async def create_assignment(request: Request, req: CreateAssignmentRequest): - """发布作业(班主任)""" - user = await get_current_user(request) - is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) - if not is_teacher: - return error_response(message="仅班主任可发布作业", code=403) - result = await HomeworkService.create_assignment( - subject_id=req.subject_id, - title=req.title, - description=req.description, - deadline=req.deadline, - created_by=user["user_id"] - ) - if result["success"]: - await LogService.write_operation_log( - operator_id=user["user_id"], operator_name=user["real_name"], - operator_role="班主任", operation_type="create_assignment", - target_type="homework", - details=f"发布作业: {title}", - ip=request.client.host - ) - return success_response(data=result, message="作业发布成功") - else: - return error_response(message=result["message"]) - - -@router.put("/homework/submission") -async def update_submission_status(request: Request, req: UpdateHomeworkStatusRequest): - """更新作业提交状态(班主任或学习委员)""" - user = await get_current_user(request) - role = await PermissionChecker.get_user_role(user["user_id"]) - if role not in ["班主任", "学习委员"]: - return error_response(message="无权进行此操作", code=403) - result = await HomeworkService.update_submission_status( - submission_id=req.submission_id, - status=req.status, - comments=req.comments, - apply_deduction=req.apply_deduction, - operator_id=user["user_id"] - ) - if result["success"]: - await LogService.write_operation_log( - 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}", - ip=request.client.host - ) - return success_response(message="状态更新成功") - else: - return error_response(message=result["message"]) - # ========== 考勤管理 ========== diff --git a/backend/routes/parent.py b/backend/routes/parent.py index f1598e0..ba214f1 100644 --- a/backend/routes/parent.py +++ b/backend/routes/parent.py @@ -36,21 +36,6 @@ async def get_child_conduct(request: Request): return success_response(data=result) -@router.get("/child/homework") -async def get_child_homework(request: Request): - """ - 获取子女作业情况 - """ - user = await get_current_user(request) - - if user["user_type"] != "parent": - return error_response(message="仅限家长访问", code=403) - - result = await ParentService.get_child_homework(user["user_id"]) - - return success_response(data=result) - - @router.get("/child/attendance") async def get_child_attendance(request: Request): """ diff --git a/backend/routes/upgrade.py b/backend/routes/upgrade.py new file mode 100644 index 0000000..5ef0760 --- /dev/null +++ b/backend/routes/upgrade.py @@ -0,0 +1,231 @@ +# =========================================== +# 班级操行分管理系统 - 升级管理路由 +# +# 开发者: Canglan +# 版权归属: Sea Network Technology Studio +# +# 版权所有 © Sea Network Technology Studio +# =========================================== + +from fastapi import APIRouter, Request +from utils.database import execute_query, execute_update, get_pool +from utils.response import success_response, error_response +from utils.logger import setup_logger +from middleware.permission import PermissionChecker +import os +import re + +logger = setup_logger() +router = APIRouter() + +# 版本列表(按顺序) +ALL_VERSIONS = { + '1.7': 'v1.7.sql', + '1.8': 'v1.8.sql', + '2.0': 'v2.0.sql', + '2.0.1': 'v2.0.1.sql', + '2.1': 'v2.1.sql', + '2.2': 'v2.2.sql', +} + + +@router.get("/check") +async def check_upgrade(request: Request): + """检查数据库版本是否需要升级""" + # 权限检查:仅班主任可执行升级操作 + user_type = getattr(request.state, 'user_type', None) + if user_type != 'admin': + return error_response(message="仅管理员可执行升级操作", code=403) + + is_teacher = await PermissionChecker.check_is_teacher( + getattr(request.state, 'user_id', 0) + ) + if not is_teacher: + return error_response(message="仅班主任可执行升级操作", code=403) + + user_id = request.state.user.get('user_id') if hasattr(request.state, 'user') else getattr(request.state, 'user_id', None) + + # 检测当前数据库版本 + current_version = '0.0.0' + try: + row = await execute_query( + "SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'" + ) + if row: + current_version = row[0]['setting_value'] + except Exception: + pass # 表不存在时使用默认值 + + # 读取目标版本(从 VERSION 文件) + version_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'VERSION') + version_file = os.path.normpath(version_file) + target_version = '0.0.0' + try: + if os.path.exists(version_file): + with open(version_file, 'r') as f: + target_version = f.read().strip() + except Exception: + pass + + # 计算需要升级的步骤 + needs_upgrade = _compare_versions(target_version, current_version) > 0 + + steps = [] + for version, file_name in sorted(ALL_VERSIONS.items(), key=lambda x: _version_tuple(x[0])): + if _compare_versions(version, current_version) > 0 and _compare_versions(version, target_version) <= 0: + steps.append({'version': version, 'file': file_name}) + + return success_response(data={ + 'needs_upgrade': needs_upgrade, + 'current': current_version, + 'target': target_version, + 'steps': steps + }) + + +@router.post("/step") +async def execute_upgrade_step(request: Request): + """执行单个升级步骤""" + # 权限检查:仅班主任可执行升级操作 + user_type = getattr(request.state, 'user_type', None) + if user_type != 'admin': + return error_response(message="仅管理员可执行升级操作", code=403) + + is_teacher = await PermissionChecker.check_is_teacher( + getattr(request.state, 'user_id', 0) + ) + if not is_teacher: + return error_response(message="仅班主任可执行升级操作", code=403) + + user_id = request.state.user.get('user_id') if hasattr(request.state, 'user') else getattr(request.state, 'user_id', None) + + body = await request.json() + version = body.get('version', '') + + if not version: + return error_response(message='缺少版本号参数', code=400) + + if version not in ALL_VERSIONS: + return error_response(message=f'未知版本: {version}', code=400) + + # SQL 文件路径 + sql_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), '..', 'sql', 'upgrades') + sql_file = os.path.normpath(os.path.join(sql_dir, ALL_VERSIONS[version])) + + if not os.path.exists(sql_file): + return error_response(message=f'SQL 文件不存在: {ALL_VERSIONS[version]}', code=500) + + try: + # 读取并执行 SQL + with open(sql_file, 'r', encoding='utf-8') as f: + sql_content = f.read().strip() + + if sql_content and sql_content != '--': + # 使用 aiomysql 直接执行多条 SQL + pool = get_pool() + async with pool.acquire() as conn: + async with conn.cursor() as cursor: + # 分割 SQL 语句(按 DELIMITER 处理存储过程) + await _execute_sql_content(cursor, sql_content) + await conn.commit() + + # 更新版本号 + await execute_update( + "INSERT INTO system_settings (setting_key, setting_value) VALUES ('db_version', %s) " + "ON DUPLICATE KEY UPDATE setting_value = %s", + (version, version) + ) + + # 重新检测版本 + new_version = '0.0.0' + try: + row = await execute_query( + "SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'" + ) + if row: + new_version = row[0]['setting_value'] + except Exception: + pass + + logger.info(f"数据库升级成功: v{version} ({ALL_VERSIONS[version]})") + + return success_response(data={ + 'success': True, + 'version': version, + 'message': f"升级至 v{version} 成功 ({ALL_VERSIONS[version]})", + 'current': new_version + }) + + except Exception as e: + logger.error(f"数据库升级失败: v{version} - {str(e)}") + return error_response(message=f"升级至 v{version} 失败: {str(e)}", code=500) + + +def _compare_versions(v1: str, v2: str) -> int: + """比较两个版本号,返回 1/0/-1""" + t1 = _version_tuple(v1) + t2 = _version_tuple(v2) + if t1 > t2: + return 1 + elif t1 < t2: + return -1 + return 0 + + +def _version_tuple(v: str) -> tuple: + """将版本字符串转为可比较的元组""" + parts = [] + for p in v.split('.'): + try: + parts.append(int(p)) + except ValueError: + parts.append(0) + return tuple(parts) + + +async def _execute_sql_content(cursor, sql_content: str): + """执行 SQL 内容,处理存储过程中的 DELIMITER""" + # 如果包含 DELIMITER,需要特殊处理 + if 'DELIMITER' in sql_content: + # 移除 DELIMITER 行,按 $$ 分割存储过程 + lines = sql_content.split('\n') + current_block = [] + in_procedure = False + + for line in lines: + stripped = line.strip() + if stripped.upper().startswith('DELIMITER $$'): + in_procedure = True + current_block = [] + continue + elif stripped.upper() == 'DELIMITER ;': + # 执行累积的存储过程块 + if current_block: + proc_sql = '\n'.join(current_block).strip() + if proc_sql: + await cursor.execute(proc_sql) + in_procedure = False + current_block = [] + continue + elif stripped.upper().startswith('DELIMITER'): + continue + + if in_procedure: + current_block.append(line) + else: + # 普通SQL,按分号分割执行 + if stripped and not stripped.startswith('--'): + # 简单的按分号分割 + for stmt in stripped.split(';'): + stmt = stmt.strip() + if stmt: + await cursor.execute(stmt) + else: + # 无 DELIMITER,简单执行 + # 按 CREATE 分割以支持多语句 + # 分割 SQL 语句 + statements = re.split(r';\s*\n', sql_content) + for stmt in statements: + stmt = stmt.strip() + if stmt and stmt != '--': + await cursor.execute(stmt) diff --git a/backend/schemas/admin.py b/backend/schemas/admin.py index 1814602..a50029a 100644 --- a/backend/schemas/admin.py +++ b/backend/schemas/admin.py @@ -64,14 +64,6 @@ class AddAdminResponse(BaseModel): message: str -class UpdateHomeworkStatusRequest(BaseModel): - """更新作业状态请求""" - submission_id: int = Field(..., gt=0, description="提交记录ID") - status: str = Field(..., pattern=r'^(submitted|not_submitted|late|excused)$', description="状态") - comments: Optional[str] = Field(None, max_length=500, description="评语") - apply_deduction: bool = False - - class AddStudentRequest(BaseModel): """新增学生请求""" student_no: str = Field(..., min_length=1, max_length=20, pattern=r'^[a-zA-Z0-9]+$', description="学号") @@ -114,14 +106,6 @@ class UpdateStudentRequest(BaseModel): dormitory_number: Optional[str] = Field(None, max_length=20, description="宿舍号") -class CreateAssignmentRequest(BaseModel): - """创建作业请求""" - subject_id: int = Field(..., gt=0, description="科目ID") - title: str = Field(..., min_length=1, max_length=200, description="作业标题") - description: Optional[str] = Field(None, max_length=1000, description="作业描述") - deadline: str = Field(..., min_length=1, max_length=20, description="截止日期") - - class UnlockUserRequest(BaseModel): """解除用户登录锁定请求""" username: str = Field(..., min_length=1, max_length=50, description="用户名") \ No newline at end of file diff --git a/backend/schemas/student.py b/backend/schemas/student.py index 1bfe1b4..ff4adb4 100644 --- a/backend/schemas/student.py +++ b/backend/schemas/student.py @@ -47,18 +47,6 @@ class ConductHistoryResponse(BaseModel): records: List[ConductRecord] -class HomeworkSubmission(BaseModel): - """作业提交情况""" - assignment_id: int - title: str - subject: str - deadline: date - status: str - submit_time: Optional[datetime] = None - comments: Optional[str] = None - deduction_applied: bool - - class AttendanceRecord(BaseModel): """考勤记录""" attendance_id: int diff --git a/backend/services/admin_service.py b/backend/services/admin_service.py index 2fbf183..6c5e3fc 100644 --- a/backend/services/admin_service.py +++ b/backend/services/admin_service.py @@ -27,14 +27,15 @@ class AdminService: async def get_students( page: int = 1, page_size: int = 20, - search: str = None + search: str = None, + dormitory_number: str = None ) -> Dict[str, Any]: """获取所有学生列表""" offset = (page - 1) * page_size sql = """ - SELECT student_id, student_no, name, total_points, parent_phone, status - FROM students + SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status + FROM students WHERE status = 1 """ params = [] @@ -43,6 +44,10 @@ class AdminService: sql += " AND (student_no LIKE %s OR name LIKE %s)" params.extend([f"%{search}%", f"%{search}%"]) + if dormitory_number: + sql += " AND dormitory_number = %s" + params.append(dormitory_number) + sql += " ORDER BY student_no LIMIT %s OFFSET %s" params.extend([page_size, offset]) @@ -50,12 +55,17 @@ class AdminService: # 获取总数 count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1" + count_params = [] if search: count_sql += " AND (student_no LIKE %s OR name LIKE %s)" - total_result = await execute_one(count_sql, (f"%{search}%", f"%{search}%")) + count_params.extend([f"%{search}%", f"%{search}%"]) + if dormitory_number: + count_sql += " AND dormitory_number = %s" + count_params.append(dormitory_number) + if count_params: + total_result = await execute_one(count_sql, tuple(count_params)) else: total_result = await execute_one(count_sql) - total = total_result["total"] if total_result else 0 return { diff --git a/backend/services/conduct_service.py b/backend/services/conduct_service.py index c4da850..105464e 100644 --- a/backend/services/conduct_service.py +++ b/backend/services/conduct_service.py @@ -235,6 +235,7 @@ class ConductService: student_id=student_id, start_date=start_date, end_date=end_date, + related_type=related_type, page=page, page_size=page_size ) diff --git a/backend/services/homework_service.py b/backend/services/homework_service.py deleted file mode 100644 index 788b825..0000000 --- a/backend/services/homework_service.py +++ /dev/null @@ -1,135 +0,0 @@ -# =========================================== -# 班级操行分管理系统 - 后端服务 -# -# 开发者: Canglan -# 联系方式: admin@sea-studio.top -# 版权归属: Sea Network Technology Studio -# 许可证: MIT License -# -# 版权所有 © Sea Network Technology Studio -# =========================================== - -from typing import Dict, Any, List, Optional -from datetime import datetime - -from models.homework import HomeworkModel -from models.student import StudentModel -from models.conduct import ConductModel -from middleware.permission import PermissionChecker -from config import settings -from utils.logger import get_logger - -logger = get_logger(__name__) - - -class HomeworkService: - """作业服务""" - - @staticmethod - async def get_assignments(user_id: int) -> Dict[str, Any]: - """获取作业列表""" - role = await PermissionChecker.get_user_role(user_id) - - if role == "班主任": - assignments = await HomeworkModel.get_all_assignments() - elif role == "学习委员": - subject_ids = await PermissionChecker.get_user_subject_ids(user_id) - assignments = await HomeworkModel.get_assignments_by_subjects(subject_ids) - else: - assignments = [] - - return {"assignments": assignments} - - @staticmethod - async def create_assignment( - subject_id: int, - title: str, - description: Optional[str], - deadline: str, - created_by: int - ) -> Dict[str, Any]: - """创建作业""" - assignment_id = await HomeworkModel.create_assignment( - subject_id=subject_id, - title=title, - description=description, - deadline=deadline, - created_by=created_by - ) - - if assignment_id: - logger.info(f"用户[{created_by}] 创建作业[{assignment_id}]: {title}") - return {"success": True, "assignment_id": assignment_id} - else: - return {"success": False, "message": "创建作业失败"} - - @staticmethod - async def update_submission_status( - submission_id: int, - status: str, - comments: Optional[str], - apply_deduction: bool, - operator_id: int - ) -> Dict[str, Any]: - """更新作业提交状态""" - # 获取提交记录信息 - submission = await HomeworkModel.get_submission(submission_id) - if not submission: - return {"success": False, "message": "提交记录不存在"} - - # 检查权限 - role = await PermissionChecker.get_user_role(operator_id) - if role == "学习委员": - # 检查是否管理该科目 - subject_ids = await PermissionChecker.get_user_subject_ids(operator_id) - if submission["subject_id"] not in subject_ids: - return {"success": False, "message": "无权操作此作业"} - elif role != "班主任": - return {"success": False, "message": "无权进行此操作"} - - # 更新状态 - result = await HomeworkModel.update_submission( - submission_id=submission_id, - status=status, - comments=comments, - updated_by=operator_id - ) - - if not result: - return {"success": False, "message": "更新失败"} - - # 应用扣分 - if apply_deduction and status in ["not_submitted", "late"]: - # 确定扣分数值 - if status == "not_submitted": - points_change = -settings.DEDUCTION_HOMEWORK_NOT_SUBMIT - else: - points_change = -settings.DEDUCTION_HOMEWORK_LATE - - # 创建扣分记录 - student = await StudentModel.get_by_id(submission["student_id"]) - if student: - # 获取操作人姓名 - from models.user import UserModel - user = await UserModel.get_by_user_id(operator_id) - recorder_name = user.get("real_name", "班主任") if user else "班主任" - - await ConductModel.create_record( - student_id=submission["student_id"], - points_change=points_change, - reason=f"作业未提交/迟交: {submission['title']}", - recorder_id=operator_id, - recorder_name=recorder_name, - related_type="homework", - related_id=submission["assignment_id"] - ) - - # 更新学生总分 - await StudentModel.update_total_points(submission["student_id"], points_change) - - # 标记已应用扣分 - await HomeworkModel.mark_deduction_applied(submission_id) - - logger.info(f"用户[{operator_id}] 更新作业提交状态[{submission_id}] -> {status}") - - return {"success": True, "message": "状态更新成功"} \ No newline at end of file diff --git a/backend/services/parent_service.py b/backend/services/parent_service.py index 2f8eec7..9a17ccc 100644 --- a/backend/services/parent_service.py +++ b/backend/services/parent_service.py @@ -15,7 +15,6 @@ from typing import Dict, Any, Optional, List from models.user import UserModel from models.student import StudentModel from models.conduct import ConductModel -from models.homework import HomeworkModel from models.attendance import AttendanceModel from utils.logger import get_logger @@ -46,24 +45,6 @@ class ParentService: } @staticmethod - async def get_child_homework(parent_id: int) -> Dict[str, Any]: - """获取子女作业情况""" - user = await UserModel.get_by_user_id(parent_id) - if not user or not user["student_id"]: - return {"error": "未关联学生"} - - student = await StudentModel.get_by_id(user["student_id"]) - if not student: - return {"error": "学生不存在"} - - homework = await HomeworkModel.get_student_homework(user["student_id"]) - - return { - "student_id": student["student_id"], - "student_name": student["name"], - "homework": homework - } - @staticmethod async def get_child_attendance(parent_id: int) -> Dict[str, Any]: """获取子女考勤记录""" user = await UserModel.get_by_user_id(parent_id) diff --git a/backend/services/student_service.py b/backend/services/student_service.py index 8ce50be..403f354 100644 --- a/backend/services/student_service.py +++ b/backend/services/student_service.py @@ -14,9 +14,9 @@ from datetime import datetime, timedelta from models.student import StudentModel from models.conduct import ConductModel -from models.homework import HomeworkModel from models.attendance import AttendanceModel from middleware.permission import PermissionChecker +from utils.database import execute_query from utils.logger import get_logger logger = get_logger(__name__) @@ -57,29 +57,33 @@ class StudentService: @staticmethod async def get_homework_status(student_id: int) -> Dict[str, Any]: - """获取学生作业情况""" + """获取学生作业扣分记录""" student = await StudentModel.get_by_id(student_id) if not student: return {"error": "学生不存在"} - homework = await HomeworkModel.get_student_homework(student_id) + # 查询作业相关的操行分记录 + sql = """ + SELECT cr.record_id, cr.points_change, cr.reason, cr.created_at, + cr.related_type, cr.recorder_name + FROM conduct_records cr + WHERE cr.student_id = %s AND cr.related_type = 'homework' AND cr.is_revoked = 0 + ORDER BY cr.created_at DESC + """ + records = await execute_query(sql, (student_id,)) # 统计 - total = len(homework) - submitted = sum(1 for h in homework if h["status"] == "submitted") - not_submitted = sum(1 for h in homework if h["status"] == "not_submitted") - late = sum(1 for h in homework if h["status"] == "late") + total = len(records) + deductions = sum(1 for r in records if r["points_change"] < 0) return { "student_id": student_id, "student_name": student["name"], "statistics": { "total": total, - "submitted": submitted, - "not_submitted": not_submitted, - "late": late + "deductions": deductions }, - "homework": homework + "homework": records } @staticmethod diff --git a/frontend/admin/conduct.php b/frontend/admin/conduct.php index c894432..633dd53 100644 --- a/frontend/admin/conduct.php +++ b/frontend/admin/conduct.php @@ -35,6 +35,7 @@ include __DIR__ . '/../includes/header.php';
+ +该宿舍暂无学生
'; + studentsCount.textContent = ''; + } else { + students.forEach(s => { + dormitoryStudentIds.push(s.student_id); + const div = document.createElement('div'); + div.style.cssText = 'display: flex; justify-content: space-between; padding: 4px 0; border-bottom: 1px solid var(--border-color);'; + div.innerHTML = `${escapeHtml(s.name)}${escapeHtml(s.student_no)}`; + studentsList.appendChild(div); + }); + studentsCount.textContent = `共 ${students.length} 人`; + } + studentsGroup.style.display = 'block'; + } else { + studentsList.innerHTML = '加载失败
'; + studentsGroup.style.display = 'block'; + } +} + +async function submitDormitoryPoints() { + if (dormitoryStudentIds.length === 0) { + showToast('该宿舍没有学生', 'warning'); + return; + } + + const pointsChange = parseInt(document.getElementById('dormitoryPointsChange').value); + const reason = document.getElementById('dormitoryPointsReason').value; + + if (isNaN(pointsChange) || pointsChange === 0) { + showToast('分值不能为0', 'error'); + return; + } + + if (!reason.trim()) { + showToast('请填写原因', 'error'); + return; + } + + const data = { + student_ids: dormitoryStudentIds, + points_change: pointsChange, + reason: reason + }; + + const res = await apiPost('/api/admin/conduct/add', data); + + if (res && res.success) { + showToast(`操作成功: ${res.data.success_count} 人成功`); + closeModal('dormitoryPointsModal'); + loadStudents(); + } else { + showToast(res?.message || '操作失败', 'error'); + } +} loadStudents(); window.loadStudents = loadStudents; window.showSinglePointsModal = showSinglePointsModal; window.exportMoralityRecords = exportMoralityRecords; +window.showDormitoryPointsModal = showDormitoryPointsModal; +window.onDormitorySelected = onDormitorySelected; +window.submitDormitoryPoints = submitDormitoryPoints; })(); diff --git a/frontend/assets/js/dashboard.js b/frontend/assets/js/dashboard.js index 7695740..fea9407 100644 --- a/frontend/assets/js/dashboard.js +++ b/frontend/assets/js/dashboard.js @@ -25,9 +25,15 @@ async function loadDashboard() { } let quickActions = ''; - if (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') { + if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') { quickActions += ''; } + if (role === '班主任' || role === '学习委员') { + quickActions += ''; + } + if (role === '班主任' || role === '考勤委员') { + quickActions += ''; + } if (role === '班主任') { quickActions += ''; quickActions += ''; diff --git a/frontend/assets/js/history.js b/frontend/assets/js/history.js index 939c043..bab68f9 100644 --- a/frontend/assets/js/history.js +++ b/frontend/assets/js/history.js @@ -40,7 +40,7 @@ async function loadHistory(page = 1) { end_date: endDate }; if (studentId) params.student_id = studentId; - if (relatedType && !isGrouped) params.related_type = relatedType; + if (relatedType) params.related_type = relatedType; if (isGrouped) params.grouped = true; const res = await apiGet('/api/admin/conduct/history', params); diff --git a/frontend/assets/js/modules/student-mgmt.js b/frontend/assets/js/modules/student-mgmt.js index a884e5e..91df684 100644 --- a/frontend/assets/js/modules/student-mgmt.js +++ b/frontend/assets/js/modules/student-mgmt.js @@ -146,7 +146,7 @@ const students = data.students || []; let html = '| 学号 | 姓名 | 家长手机号 | 初始密码 | '; + html += '学号 | 姓名 | 家长手机号 | 宿舍号 | 初始密码 | '; html += '${escapeHtml(s.student_no || '')} | ${escapeHtml(s.name || '')} | ${escapeHtml(s.parent_phone || '')} | +${escapeHtml(s.dormitory_number || '-')} | ${escapeHtml(s.password || '123456')} | `; }); diff --git a/frontend/assets/js/student-homework.js b/frontend/assets/js/student-homework.js index 1860f23..4ff9d5e 100644 --- a/frontend/assets/js/student-homework.js +++ b/frontend/assets/js/student-homework.js @@ -16,25 +16,18 @@ async function loadHomework() { const res = await apiGet(`/api/student/homework/${STUDENT_ID}`); if (res && res.success) { let html = ''; - res.data.homework.forEach(hw => { - // 提交状态 - let statusDisplay = '-'; - if (hw.status) { - statusDisplay = getStatusBadge(hw.status, 'homework'); - } - // 扣分显示 - const pointsDisplay = hw.points ? `${hw.points}分` : '-'; - + res.data.homework.forEach(record => { + const pointsClass = record.points_change > 0 ? 'plus' : 'minus'; + const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e'; html += `
|---|---|---|---|---|---|---|---|---|
| ${escapeHtml(hw.title)} | -${escapeHtml(hw.subject_name)} | -${hw.deadline || '-'} | -${statusDisplay} | -${pointsDisplay} | +${formatDateTime(record.created_at)} | +${record.points_change > 0 ? '+' : ''}${record.points_change} | +${escapeHtml(record.reason)} | +${escapeHtml(record.recorder_name || '-')} |
| 📝 暂无作业记录 | ||||||||
| 📝 暂无作业扣分记录 | ||||||||
| 科目 | 时间 | 分值 | -备注 | -作业 | +原因 | +操作人 | ||
|---|---|---|---|---|---|---|---|---|
| ${escapeHtml(hw.subject_name)} | -${hw.deadline || hw.created_at} | -${pointsDisplay} | -${escapeHtml(hw.comments || '-')} | -${escapeHtml(hw.title)} | +${formatDateTime(record.created_at)} | +${record.points_change > 0 ? '+' : ''}${record.points_change} | +${escapeHtml(record.reason)} | +${escapeHtml(record.recorder_name || '-')} |
| 暂无作业 | ||||||||
| 暂无作业扣分记录 | ||||||||
| 作业名称 | 科目 | 截止时间 | 提交状态 | 扣分 |
|---|---|---|---|---|
| 时间 | 分值 | 原因 | 操作人 |
backend/ 目录cp .env.example .env.env 文件,填入实际的数据库连接信息