v2.2更新
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -52,5 +52,5 @@ docs/guide/student.pdf
|
|||||||
docs/guide/teacher.pdf
|
docs/guide/teacher.pdf
|
||||||
qrcode.png
|
qrcode.png
|
||||||
|
|
||||||
# 展示内容
|
# example
|
||||||
example
|
example
|
||||||
@@ -272,7 +272,7 @@ classmanager/
|
|||||||
| v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 |
|
| v1.7 | 2026.5.21 | 全量一致性审计:前后端配置统一(.env.example/config.py/config.php)、清理废弃全局变量、角色权限表精确化 |
|
||||||
| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 |
|
| v1.8 | 2026.5.22 | 科目管理融入作业管理页、科目删除数据依赖检查、加减分记录类型区分(manual/homework/attendance)、学生端作业详情优化 |
|
||||||
| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 |
|
| v2.0.1 | 2026.5.23 | 操作列折叠优化、扣分类型大类区分、科目选择修复、改名作业扣分、记录人优化、家长端优化、学期管理优化 |
|
||||||
| v2.1 | 2025.7.14 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 |
|
| v2.1 | 2026.5.26 | CSS变量化统一配色方案、简化按钮系统、操作列按钮风格统一、清理内联颜色、修复科目管理面板无法展开、数据库索引优化、清理init.sql冗余迁移代码、安全审计通过 |
|
||||||
|
|
||||||
## 许可证
|
## 许可证
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ from utils.logger import setup_logger, log_access
|
|||||||
from utils.database import init_db_pool, close_db_pool
|
from utils.database import init_db_pool, close_db_pool
|
||||||
from utils.redis_client import init_redis_pool, close_redis_pool
|
from utils.redis_client import init_redis_pool, close_redis_pool
|
||||||
from middleware.auth_middleware import AuthMiddleware
|
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
|
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(subject.router, prefix="/api/subject", tags=["科目管理"])
|
||||||
app.include_router(semester.router, prefix="/api/semester", tags=["学期管理"])
|
app.include_router(semester.router, prefix="/api/semester", tags=["学期管理"])
|
||||||
app.include_router(config_router, prefix="/api/config", 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=["调试"])
|
app.include_router(debug.router, tags=["调试"])
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ class ConductModel:
|
|||||||
student_id: int = None,
|
student_id: int = None,
|
||||||
start_date: str = None,
|
start_date: str = None,
|
||||||
end_date: str = None,
|
end_date: str = None,
|
||||||
|
related_type: str = None,
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 20
|
page_size: int = 20
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
@@ -217,6 +218,8 @@ class ConductModel:
|
|||||||
start_date = None
|
start_date = None
|
||||||
if end_date == "":
|
if end_date == "":
|
||||||
end_date = None
|
end_date = None
|
||||||
|
if related_type == "":
|
||||||
|
related_type = None
|
||||||
|
|
||||||
conditions = ["cr.is_revoked = 0"]
|
conditions = ["cr.is_revoked = 0"]
|
||||||
params = []
|
params = []
|
||||||
@@ -230,6 +233,9 @@ class ConductModel:
|
|||||||
if end_date:
|
if end_date:
|
||||||
conditions.append("cr.created_at <= %s")
|
conditions.append("cr.created_at <= %s")
|
||||||
params.append(end_date + ' 23:59:59')
|
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)
|
where_clause = " AND ".join(conditions)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -53,6 +53,18 @@ class StudentModel:
|
|||||||
sql += " ORDER BY student_no"
|
sql += " ORDER BY student_no"
|
||||||
return await execute_query(sql)
|
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
|
@staticmethod
|
||||||
async def create(
|
async def create(
|
||||||
student_no: str,
|
student_no: str,
|
||||||
|
|||||||
@@ -20,16 +20,15 @@ from middleware.permission import (
|
|||||||
)
|
)
|
||||||
from services.admin_service import AdminService
|
from services.admin_service import AdminService
|
||||||
from services.conduct_service import ConductService
|
from services.conduct_service import ConductService
|
||||||
from services.homework_service import HomeworkService
|
|
||||||
from services.attendance_service import AttendanceService
|
from services.attendance_service import AttendanceService
|
||||||
from services.log_service import LogService
|
from services.log_service import LogService
|
||||||
from utils.redis_client import RedisClient
|
from utils.redis_client import RedisClient
|
||||||
from schemas.admin import (
|
from schemas.admin import (
|
||||||
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
AddPointsRequest, RevokeRequest, AddAdminRequest,
|
||||||
AddStudentRequest, UpdateStudentRequest,
|
AddStudentRequest, UpdateStudentRequest,
|
||||||
UpdateHomeworkStatusRequest, AddAttendanceRequest,
|
AddAttendanceRequest,
|
||||||
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
|
UpdateAdminRequest, DeleteAdminRequest, ResetPasswordRequest,
|
||||||
CreateAssignmentRequest, UnlockUserRequest
|
UnlockUserRequest
|
||||||
)
|
)
|
||||||
from utils.response import success_response, error_response
|
from utils.response import success_response, error_response
|
||||||
from utils.logger import get_logger
|
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")
|
@router.get("/students")
|
||||||
async def get_students(
|
async def get_students(
|
||||||
request: Request,
|
request: Request,
|
||||||
page: int = Query(1, ge=1),
|
page: int = Query(1, ge=1),
|
||||||
page_size: int = Query(20, ge=1, le=1000),
|
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)
|
user = await get_current_user(request)
|
||||||
if user["user_type"] != "admin":
|
if user["user_type"] != "admin":
|
||||||
return error_response(message="仅管理员可查看学生列表", code=403)
|
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)
|
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(
|
result = await AdminService.update_student(
|
||||||
student_id=student_id,
|
student_id=student_id,
|
||||||
name=req.name,
|
name=req.name,
|
||||||
parent_phone=req.parent_phone
|
parent_phone=req.parent_phone,
|
||||||
|
dormitory_number=req.dormitory_number
|
||||||
)
|
)
|
||||||
if result["success"]:
|
if result["success"]:
|
||||||
await LogService.write_operation_log(
|
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):
|
async def add_conduct_points(request: Request, req: AddPointsRequest):
|
||||||
"""批量加减分"""
|
"""批量加减分"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
# 仅管理员(班主任/班干部)可操作
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="无权进行此操作", code=403)
|
||||||
result = await ConductService.add_points(
|
result = await ConductService.add_points(
|
||||||
student_ids=req.student_ids,
|
student_ids=req.student_ids,
|
||||||
points_change=req.points_change,
|
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):
|
async def revoke_conduct_record(request: Request, req: RevokeRequest):
|
||||||
"""撤销扣分记录"""
|
"""撤销扣分记录"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
# 仅管理员(班主任/班干部)可操作
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="无权进行此操作", code=403)
|
||||||
result = await ConductService.revoke_record(
|
result = await ConductService.revoke_record(
|
||||||
record_id=req.record_id,
|
record_id=req.record_id,
|
||||||
revoker_id=user["user_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):
|
async def restore_conduct_record(request: Request, req: RevokeRequest):
|
||||||
"""反撤销(恢复)已撤销的记录"""
|
"""反撤销(恢复)已撤销的记录"""
|
||||||
user = await get_current_user(request)
|
user = await get_current_user(request)
|
||||||
|
# 仅管理员(班主任/班干部)可操作
|
||||||
|
if user["user_type"] != "admin":
|
||||||
|
return error_response(message="无权进行此操作", code=403)
|
||||||
result = await ConductService.restore_record(
|
result = await ConductService.restore_record(
|
||||||
record_id=req.record_id,
|
record_id=req.record_id,
|
||||||
restorer_id=user["user_id"]
|
restorer_id=user["user_id"]
|
||||||
@@ -319,86 +341,6 @@ async def get_conduct_history(
|
|||||||
return error_response(message=f"获取历史记录失败: {str(e)}")
|
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"])
|
|
||||||
|
|
||||||
|
|
||||||
# ========== 考勤管理 ==========
|
# ========== 考勤管理 ==========
|
||||||
|
|
||||||
|
|||||||
@@ -36,21 +36,6 @@ async def get_child_conduct(request: Request):
|
|||||||
return success_response(data=result)
|
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")
|
@router.get("/child/attendance")
|
||||||
async def get_child_attendance(request: Request):
|
async def get_child_attendance(request: Request):
|
||||||
"""
|
"""
|
||||||
|
|||||||
231
backend/routes/upgrade.py
Normal file
231
backend/routes/upgrade.py
Normal file
@@ -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)
|
||||||
@@ -64,14 +64,6 @@ class AddAdminResponse(BaseModel):
|
|||||||
message: str
|
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):
|
class AddStudentRequest(BaseModel):
|
||||||
"""新增学生请求"""
|
"""新增学生请求"""
|
||||||
student_no: str = Field(..., min_length=1, max_length=20, pattern=r'^[a-zA-Z0-9]+$', description="学号")
|
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="宿舍号")
|
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):
|
class UnlockUserRequest(BaseModel):
|
||||||
"""解除用户登录锁定请求"""
|
"""解除用户登录锁定请求"""
|
||||||
username: str = Field(..., min_length=1, max_length=50, description="用户名")
|
username: str = Field(..., min_length=1, max_length=50, description="用户名")
|
||||||
@@ -47,18 +47,6 @@ class ConductHistoryResponse(BaseModel):
|
|||||||
records: List[ConductRecord]
|
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):
|
class AttendanceRecord(BaseModel):
|
||||||
"""考勤记录"""
|
"""考勤记录"""
|
||||||
attendance_id: int
|
attendance_id: int
|
||||||
|
|||||||
@@ -27,13 +27,14 @@ class AdminService:
|
|||||||
async def get_students(
|
async def get_students(
|
||||||
page: int = 1,
|
page: int = 1,
|
||||||
page_size: int = 20,
|
page_size: int = 20,
|
||||||
search: str = None
|
search: str = None,
|
||||||
|
dormitory_number: str = None
|
||||||
) -> Dict[str, Any]:
|
) -> Dict[str, Any]:
|
||||||
"""获取所有学生列表"""
|
"""获取所有学生列表"""
|
||||||
offset = (page - 1) * page_size
|
offset = (page - 1) * page_size
|
||||||
|
|
||||||
sql = """
|
sql = """
|
||||||
SELECT student_id, student_no, name, total_points, parent_phone, status
|
SELECT student_id, student_no, name, total_points, parent_phone, dormitory_number, status
|
||||||
FROM students
|
FROM students
|
||||||
WHERE status = 1
|
WHERE status = 1
|
||||||
"""
|
"""
|
||||||
@@ -43,6 +44,10 @@ class AdminService:
|
|||||||
sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
||||||
params.extend([f"%{search}%", f"%{search}%"])
|
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"
|
sql += " ORDER BY student_no LIMIT %s OFFSET %s"
|
||||||
params.extend([page_size, offset])
|
params.extend([page_size, offset])
|
||||||
|
|
||||||
@@ -50,12 +55,17 @@ class AdminService:
|
|||||||
|
|
||||||
# 获取总数
|
# 获取总数
|
||||||
count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1"
|
count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1"
|
||||||
|
count_params = []
|
||||||
if search:
|
if search:
|
||||||
count_sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
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:
|
else:
|
||||||
total_result = await execute_one(count_sql)
|
total_result = await execute_one(count_sql)
|
||||||
|
|
||||||
total = total_result["total"] if total_result else 0
|
total = total_result["total"] if total_result else 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -235,6 +235,7 @@ class ConductService:
|
|||||||
student_id=student_id,
|
student_id=student_id,
|
||||||
start_date=start_date,
|
start_date=start_date,
|
||||||
end_date=end_date,
|
end_date=end_date,
|
||||||
|
related_type=related_type,
|
||||||
page=page,
|
page=page,
|
||||||
page_size=page_size
|
page_size=page_size
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -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": "状态更新成功"}
|
|
||||||
@@ -15,7 +15,6 @@ from typing import Dict, Any, Optional, List
|
|||||||
from models.user import UserModel
|
from models.user import UserModel
|
||||||
from models.student import StudentModel
|
from models.student import StudentModel
|
||||||
from models.conduct import ConductModel
|
from models.conduct import ConductModel
|
||||||
from models.homework import HomeworkModel
|
|
||||||
from models.attendance import AttendanceModel
|
from models.attendance import AttendanceModel
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
||||||
@@ -46,24 +45,6 @@ class ParentService:
|
|||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@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]:
|
async def get_child_attendance(parent_id: int) -> Dict[str, Any]:
|
||||||
"""获取子女考勤记录"""
|
"""获取子女考勤记录"""
|
||||||
user = await UserModel.get_by_user_id(parent_id)
|
user = await UserModel.get_by_user_id(parent_id)
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ from datetime import datetime, timedelta
|
|||||||
|
|
||||||
from models.student import StudentModel
|
from models.student import StudentModel
|
||||||
from models.conduct import ConductModel
|
from models.conduct import ConductModel
|
||||||
from models.homework import HomeworkModel
|
|
||||||
from models.attendance import AttendanceModel
|
from models.attendance import AttendanceModel
|
||||||
from middleware.permission import PermissionChecker
|
from middleware.permission import PermissionChecker
|
||||||
|
from utils.database import execute_query
|
||||||
from utils.logger import get_logger
|
from utils.logger import get_logger
|
||||||
|
|
||||||
logger = get_logger(__name__)
|
logger = get_logger(__name__)
|
||||||
@@ -57,29 +57,33 @@ class StudentService:
|
|||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
async def get_homework_status(student_id: int) -> Dict[str, Any]:
|
async def get_homework_status(student_id: int) -> Dict[str, Any]:
|
||||||
"""获取学生作业情况"""
|
"""获取学生作业扣分记录"""
|
||||||
student = await StudentModel.get_by_id(student_id)
|
student = await StudentModel.get_by_id(student_id)
|
||||||
if not student:
|
if not student:
|
||||||
return {"error": "学生不存在"}
|
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)
|
total = len(records)
|
||||||
submitted = sum(1 for h in homework if h["status"] == "submitted")
|
deductions = sum(1 for r in records if r["points_change"] < 0)
|
||||||
not_submitted = sum(1 for h in homework if h["status"] == "not_submitted")
|
|
||||||
late = sum(1 for h in homework if h["status"] == "late")
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"student_id": student_id,
|
"student_id": student_id,
|
||||||
"student_name": student["name"],
|
"student_name": student["name"],
|
||||||
"statistics": {
|
"statistics": {
|
||||||
"total": total,
|
"total": total,
|
||||||
"submitted": submitted,
|
"deductions": deductions
|
||||||
"not_submitted": not_submitted,
|
|
||||||
"late": late
|
|
||||||
},
|
},
|
||||||
"homework": homework
|
"homework": records
|
||||||
}
|
}
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="action-bar">
|
<div class="action-bar">
|
||||||
<div class="action-buttons">
|
<div class="action-buttons">
|
||||||
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
|
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
|
||||||
|
<button class="btn btn-secondary" onclick="showDormitoryPointsModal()">宿舍加分</button>
|
||||||
<?php if ($role === '班主任'): ?>
|
<?php if ($role === '班主任'): ?>
|
||||||
<button class="btn btn-secondary" onclick="exportMoralityRecords()">导出德育分记录</button>
|
<button class="btn btn-secondary" onclick="exportMoralityRecords()">导出德育分记录</button>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
@@ -110,4 +111,40 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- 宿舍集体加分模态框 -->
|
||||||
|
<div id="dormitoryPointsModal" class="modal">
|
||||||
|
<div class="modal-content">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h3>宿舍集体加分</h3>
|
||||||
|
<button class="modal-close" onclick="closeModal('dormitoryPointsModal')">×</button>
|
||||||
|
</div>
|
||||||
|
<form onsubmit="event.preventDefault(); submitDormitoryPoints()">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>选择宿舍</label>
|
||||||
|
<select id="dormitorySelect" onchange="onDormitorySelected()" required>
|
||||||
|
<option value="">-- 请选择宿舍 --</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group" id="dormitoryStudentsGroup" style="display:none;">
|
||||||
|
<label>宿舍成员</label>
|
||||||
|
<div id="dormitoryStudentsList" style="max-height: 150px; overflow-y: auto; border: 1px solid var(--border-color); border-radius: 4px; padding: 8px;">
|
||||||
|
</div>
|
||||||
|
<small id="dormitoryStudentsCount"></small>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>分数变动</label>
|
||||||
|
<input type="number" id="dormitoryPointsChange" required placeholder="正数为加分,负数为扣分">
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>原因</label>
|
||||||
|
<textarea id="dormitoryPointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="submit" class="btn btn-primary">确认提交</button>
|
||||||
|
<button type="button" class="btn" onclick="closeModal('dormitoryPointsModal')">取消</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||||
@@ -154,7 +154,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
}
|
}
|
||||||
if (iconEl) iconEl.textContent = '⟳';
|
if (iconEl) iconEl.textContent = '⟳';
|
||||||
|
|
||||||
fetch('/upgrade.php?action=step&version=' + encodeURIComponent(step.version), { method: 'POST' })
|
fetch('/api/execute_upgrade.php?action=step&version=' + encodeURIComponent(step.version), { method: 'POST' })
|
||||||
.then(function(r) { return r.json(); })
|
.then(function(r) { return r.json(); })
|
||||||
.then(function(data) {
|
.then(function(data) {
|
||||||
if (data.success) {
|
if (data.success) {
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="filter-group" style="min-width:auto;">
|
<div class="filter-group" style="min-width:auto;">
|
||||||
<label> </label>
|
<label> </label>
|
||||||
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;">
|
<label style="display:flex;align-items:center;gap:4px;cursor:pointer;font-size:13px;">
|
||||||
<input type="checkbox" id="historyGrouped" onchange="loadHistory(1)"> 批次合并
|
<input type="checkbox" id="historyGrouped" checked onchange="loadHistory(1)"> 批次合并
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<?php if ($role === '班主任'): ?>
|
<?php if ($role === '班主任'): ?>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* 检查数据库版本是否需要升级
|
* 检查数据库版本是否需要升级(代理至后端 API)
|
||||||
* 返回 JSON: {needs_upgrade: bool, current: string, target: string}
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
require_once __DIR__ . '/../config.php';
|
require_once __DIR__ . '/../config.php';
|
||||||
@@ -20,84 +19,43 @@ if ($role !== '班主任') {
|
|||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
// 读取后端 .env 获取数据库配置
|
// 从 session 获取 JWT token
|
||||||
$envPath = __DIR__ . '/../../backend/.env';
|
$token = $_SESSION['jwt_token'] ?? '';
|
||||||
if (!file_exists($envPath)) {
|
if (empty($token)) {
|
||||||
echo json_encode(['error' => '数据库配置文件不存在']);
|
echo json_encode(['error' => '会话已过期,请重新登录']);
|
||||||
exit();
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$lines = file($envPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
// 调用后端 API
|
||||||
$dbConfig = [];
|
$apiUrl = API_BASE_URL . '/api/upgrade/check';
|
||||||
foreach ($lines as $line) {
|
|
||||||
$line = trim($line);
|
$ch = curl_init();
|
||||||
if ($line === '' || strpos($line, '#') === 0) {
|
curl_setopt_array($ch, [
|
||||||
continue;
|
CURLOPT_URL => $apiUrl,
|
||||||
}
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
if (strpos($line, '=') !== false) {
|
CURLOPT_TIMEOUT => API_TIMEOUT,
|
||||||
list($key, $value) = explode('=', $line, 2);
|
CURLOPT_HTTPHEADER => [
|
||||||
$dbConfig[trim($key)] = trim($value);
|
'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)) {
|
||||||
|
echo json_encode(['error' => '无法连接升级服务']);
|
||||||
|
exit();
|
||||||
}
|
}
|
||||||
|
|
||||||
$required = ['DB_HOST', 'DB_PORT', 'DB_USER', 'DB_PASSWORD', 'DB_NAME'];
|
$result = json_decode($apiResponse, true);
|
||||||
foreach ($required as $key) {
|
if (!$result || !isset($result['success']) || !$result['success']) {
|
||||||
if (!isset($dbConfig[$key]) || $dbConfig[$key] === '') {
|
echo json_encode(['error' => $result['message'] ?? '升级检查失败']);
|
||||||
echo json_encode(['error' => "缺少数据库配置: {$key}"]);
|
exit();
|
||||||
exit();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
// 转发后端返回的升级数据
|
||||||
$dsn = "mysql:host={$dbConfig['DB_HOST']};port={$dbConfig['DB_PORT']};dbname={$dbConfig['DB_NAME']};charset=utf8mb4";
|
echo json_encode($result['data']);
|
||||||
$pdo = new PDO($dsn, $dbConfig['DB_USER'], $dbConfig['DB_PASSWORD'], [
|
|
||||||
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 检测当前版本
|
|
||||||
$currentVersion = '0.0.0';
|
|
||||||
try {
|
|
||||||
$stmt = $pdo->query("SELECT setting_value FROM system_settings WHERE setting_key = 'db_version'");
|
|
||||||
$row = $stmt->fetch(PDO::FETCH_ASSOC);
|
|
||||||
if ($row) {
|
|
||||||
$currentVersion = $row['setting_value'];
|
|
||||||
}
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
// 表不存在,使用默认值
|
|
||||||
}
|
|
||||||
|
|
||||||
// 读取目标版本
|
|
||||||
$versionFile = __DIR__ . '/../../VERSION';
|
|
||||||
if (!file_exists($versionFile)) {
|
|
||||||
echo json_encode(['error' => 'VERSION 文件不存在']);
|
|
||||||
exit();
|
|
||||||
}
|
|
||||||
$targetVersion = trim(file_get_contents($versionFile));
|
|
||||||
|
|
||||||
$needsUpgrade = version_compare($targetVersion, $currentVersion, '>');
|
|
||||||
|
|
||||||
$allVersions = [
|
|
||||||
'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',
|
|
||||||
];
|
|
||||||
$steps = [];
|
|
||||||
foreach ($allVersions as $version => $file) {
|
|
||||||
if (version_compare($version, $currentVersion, '>') &&
|
|
||||||
version_compare($version, $targetVersion, '<=')) {
|
|
||||||
$steps[] = ['version' => $version, 'file' => $file];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
usort($steps, function($a, $b) { return version_compare($a['version'], $b['version']); });
|
|
||||||
|
|
||||||
echo json_encode([
|
|
||||||
'needs_upgrade' => $needsUpgrade,
|
|
||||||
'current' => $currentVersion,
|
|
||||||
'target' => $targetVersion,
|
|
||||||
'steps' => $steps
|
|
||||||
]);
|
|
||||||
} catch (PDOException $e) {
|
|
||||||
echo json_encode(['error' => '数据库连接失败: ' . $e->getMessage()]);
|
|
||||||
}
|
|
||||||
|
|||||||
89
frontend/api/execute_upgrade.php
Normal file
89
frontend/api/execute_upgrade.php
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 执行单个升级步骤(代理至后端 API)
|
||||||
|
*/
|
||||||
|
header('Content-Type: application/json; charset=utf-8');
|
||||||
|
|
||||||
|
require_once __DIR__ . '/../config.php';
|
||||||
|
|
||||||
|
// 验证登录和权限
|
||||||
|
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => '未授权']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$role = $_SESSION['role'] ?? '';
|
||||||
|
if ($role !== '班主任') {
|
||||||
|
http_response_code(403);
|
||||||
|
echo json_encode(['success' => false, 'error' => '权限不足']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 只接受 POST
|
||||||
|
if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => '无效请求']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$stepVersion = $_GET['version'] ?? '';
|
||||||
|
if (empty($stepVersion)) {
|
||||||
|
http_response_code(400);
|
||||||
|
echo json_encode(['success' => false, 'error' => '缺少版本号参数']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从 session 获取 JWT token
|
||||||
|
$token = $_SESSION['jwt_token'] ?? '';
|
||||||
|
if (empty($token)) {
|
||||||
|
http_response_code(401);
|
||||||
|
echo json_encode(['success' => false, 'error' => '会话已过期,请重新登录']);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 调用后端 API
|
||||||
|
$apiUrl = API_BASE_URL . '/api/upgrade/step';
|
||||||
|
|
||||||
|
$ch = curl_init();
|
||||||
|
curl_setopt_array($ch, [
|
||||||
|
CURLOPT_URL => $apiUrl,
|
||||||
|
CURLOPT_RETURNTRANSFER => true,
|
||||||
|
CURLOPT_POST => true,
|
||||||
|
CURLOPT_POSTFIELDS => json_encode(['version' => $stepVersion]),
|
||||||
|
CURLOPT_TIMEOUT => API_TIMEOUT,
|
||||||
|
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(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'version' => $stepVersion,
|
||||||
|
'error' => '无法连接升级服务'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = json_decode($apiResponse, true);
|
||||||
|
if (!$result || !isset($result['success']) || !$result['success']) {
|
||||||
|
http_response_code(500);
|
||||||
|
echo json_encode([
|
||||||
|
'success' => false,
|
||||||
|
'version' => $stepVersion,
|
||||||
|
'error' => $result['message'] ?? '升级失败'
|
||||||
|
]);
|
||||||
|
exit();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转发后端返回的数据
|
||||||
|
echo json_encode($result['data']);
|
||||||
@@ -160,6 +160,7 @@ $_SESSION['username'] = $data['username'];
|
|||||||
$_SESSION['real_name'] = $data['real_name'] ?? '';
|
$_SESSION['real_name'] = $data['real_name'] ?? '';
|
||||||
$_SESSION['role'] = $data['role'] ?? '';
|
$_SESSION['role'] = $data['role'] ?? '';
|
||||||
$_SESSION['login_time'] = time();
|
$_SESSION['login_time'] = time();
|
||||||
|
$_SESSION['jwt_token'] = $token;
|
||||||
|
|
||||||
// 如果是学生,额外设置 student_id
|
// 如果是学生,额外设置 student_id
|
||||||
if ($data['user_type'] === 'student') {
|
if ($data['user_type'] === 'student') {
|
||||||
|
|||||||
@@ -132,13 +132,8 @@ function formatDateTime(dateStr) {
|
|||||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStatusBadge(status, type = 'homework') {
|
function getStatusBadge(status, type = 'attendance') {
|
||||||
const statusMap = {
|
const statusMap = {
|
||||||
homework: {
|
|
||||||
'submitted': '已提交',
|
|
||||||
'not_submitted': '未提交',
|
|
||||||
'late': '迟交'
|
|
||||||
},
|
|
||||||
attendance: {
|
attendance: {
|
||||||
'present': '出勤',
|
'present': '出勤',
|
||||||
'absent': '缺勤',
|
'absent': '缺勤',
|
||||||
@@ -146,15 +141,13 @@ function getStatusBadge(status, type = 'homework') {
|
|||||||
'leave': '请假'
|
'leave': '请假'
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
const texts = statusMap[type] || statusMap.homework;
|
const texts = statusMap[type] || statusMap.attendance;
|
||||||
const text = texts[status] || status;
|
const text = texts[status] || status;
|
||||||
let className = 'status-badge ';
|
let className = 'status-badge ';
|
||||||
switch (status) {
|
switch (status) {
|
||||||
case 'submitted':
|
|
||||||
case 'present':
|
case 'present':
|
||||||
className += 'status-submitted';
|
className += 'status-submitted';
|
||||||
break;
|
break;
|
||||||
case 'not_submitted':
|
|
||||||
case 'absent':
|
case 'absent':
|
||||||
className += 'status-not_submitted';
|
className += 'status-not_submitted';
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -118,11 +118,113 @@ async function exportMoralityRecords() {
|
|||||||
console.error('导出失败:', err);
|
console.error('导出失败:', err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// 宿舍集体加分相关
|
||||||
|
var dormitoryStudentIds = [];
|
||||||
|
|
||||||
|
async function showDormitoryPointsModal() {
|
||||||
|
dormitoryStudentIds = [];
|
||||||
|
document.getElementById('dormitorySelect').innerHTML = '<option value="">-- 请选择宿舍 --</option>';
|
||||||
|
document.getElementById('dormitoryStudentsGroup').style.display = 'none';
|
||||||
|
document.getElementById('dormitoryStudentsList').innerHTML = '';
|
||||||
|
document.getElementById('dormitoryPointsChange').value = '';
|
||||||
|
document.getElementById('dormitoryPointsReason').value = '';
|
||||||
|
|
||||||
|
// 加载宿舍列表
|
||||||
|
const res = await apiGet('/api/admin/students/dormitories');
|
||||||
|
if (res && res.success && res.data.dormitories) {
|
||||||
|
const select = document.getElementById('dormitorySelect');
|
||||||
|
res.data.dormitories.forEach(d => {
|
||||||
|
const option = document.createElement('option');
|
||||||
|
option.value = d;
|
||||||
|
option.textContent = d;
|
||||||
|
select.appendChild(option);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById('dormitoryPointsModal').style.display = 'flex';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onDormitorySelected() {
|
||||||
|
const dormitory = document.getElementById('dormitorySelect').value;
|
||||||
|
const studentsGroup = document.getElementById('dormitoryStudentsGroup');
|
||||||
|
const studentsList = document.getElementById('dormitoryStudentsList');
|
||||||
|
const studentsCount = document.getElementById('dormitoryStudentsCount');
|
||||||
|
|
||||||
|
dormitoryStudentIds = [];
|
||||||
|
studentsList.innerHTML = '';
|
||||||
|
|
||||||
|
if (!dormitory) {
|
||||||
|
studentsGroup.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 加载该宿舍的学生
|
||||||
|
const res = await apiGet('/api/admin/students', { dormitory_number: dormitory, page_size: 1000 });
|
||||||
|
if (res && res.success && res.data.students) {
|
||||||
|
const students = res.data.students;
|
||||||
|
if (students.length === 0) {
|
||||||
|
studentsList.innerHTML = '<p style="color: var(--text-secondary);">该宿舍暂无学生</p>';
|
||||||
|
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 = `<span>${escapeHtml(s.name)}</span><span style="color: var(--text-secondary);">${escapeHtml(s.student_no)}</span>`;
|
||||||
|
studentsList.appendChild(div);
|
||||||
|
});
|
||||||
|
studentsCount.textContent = `共 ${students.length} 人`;
|
||||||
|
}
|
||||||
|
studentsGroup.style.display = 'block';
|
||||||
|
} else {
|
||||||
|
studentsList.innerHTML = '<p style="color: var(--text-secondary);">加载失败</p>';
|
||||||
|
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();
|
loadStudents();
|
||||||
|
|
||||||
window.loadStudents = loadStudents;
|
window.loadStudents = loadStudents;
|
||||||
window.showSinglePointsModal = showSinglePointsModal;
|
window.showSinglePointsModal = showSinglePointsModal;
|
||||||
window.exportMoralityRecords = exportMoralityRecords;
|
window.exportMoralityRecords = exportMoralityRecords;
|
||||||
|
window.showDormitoryPointsModal = showDormitoryPointsModal;
|
||||||
|
window.onDormitorySelected = onDormitorySelected;
|
||||||
|
window.submitDormitoryPoints = submitDormitoryPoints;
|
||||||
|
|
||||||
})();
|
})();
|
||||||
|
|||||||
@@ -25,9 +25,15 @@ async function loadDashboard() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let quickActions = '';
|
let quickActions = '';
|
||||||
if (role === '班主任' || role === '班长' || role === '劳动委员' || role === '志愿委员') {
|
if (role === '班主任' || role === '班长' || role === '学习委员' || role === '劳动委员' || role === '志愿委员') {
|
||||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||||
}
|
}
|
||||||
|
if (role === '班主任' || role === '学习委员') {
|
||||||
|
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/homework.php\'">作业扣分</button>';
|
||||||
|
}
|
||||||
|
if (role === '班主任' || role === '考勤委员') {
|
||||||
|
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/attendance.php\'">考勤管理</button>';
|
||||||
|
}
|
||||||
if (role === '班主任') {
|
if (role === '班主任') {
|
||||||
quickActions += '<button class="btn btn-outline" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
quickActions += '<button class="btn btn-outline" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
||||||
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';
|
quickActions += '<button class="btn btn-secondary" onclick="location.href=\'/admin/conduct.php\'">导出德育分记录</button>';
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ async function loadHistory(page = 1) {
|
|||||||
end_date: endDate
|
end_date: endDate
|
||||||
};
|
};
|
||||||
if (studentId) params.student_id = studentId;
|
if (studentId) params.student_id = studentId;
|
||||||
if (relatedType && !isGrouped) params.related_type = relatedType;
|
if (relatedType) params.related_type = relatedType;
|
||||||
if (isGrouped) params.grouped = true;
|
if (isGrouped) params.grouped = true;
|
||||||
|
|
||||||
const res = await apiGet('/api/admin/conduct/history', params);
|
const res = await apiGet('/api/admin/conduct/history', params);
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
const students = data.students || [];
|
const students = data.students || [];
|
||||||
|
|
||||||
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
||||||
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
|
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>宿舍号</th><th>初始密码</th>';
|
||||||
html += '</tr></thead><tbody>';
|
html += '</tr></thead><tbody>';
|
||||||
|
|
||||||
students.forEach(s => {
|
students.forEach(s => {
|
||||||
@@ -154,6 +154,7 @@
|
|||||||
<td>${escapeHtml(s.student_no || '')}</td>
|
<td>${escapeHtml(s.student_no || '')}</td>
|
||||||
<td>${escapeHtml(s.name || '')}</td>
|
<td>${escapeHtml(s.name || '')}</td>
|
||||||
<td>${escapeHtml(s.parent_phone || '')}</td>
|
<td>${escapeHtml(s.parent_phone || '')}</td>
|
||||||
|
<td>${escapeHtml(s.dormitory_number || '-')}</td>
|
||||||
<td>${escapeHtml(s.password || '123456')}</td>
|
<td>${escapeHtml(s.password || '123456')}</td>
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -16,25 +16,18 @@ async function loadHomework() {
|
|||||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
res.data.homework.forEach(hw => {
|
res.data.homework.forEach(record => {
|
||||||
// 提交状态
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
let statusDisplay = '-';
|
const pointsColor = record.points_change > 0 ? '#38a169' : '#e53e3e';
|
||||||
if (hw.status) {
|
|
||||||
statusDisplay = getStatusBadge(hw.status, 'homework');
|
|
||||||
}
|
|
||||||
// 扣分显示
|
|
||||||
const pointsDisplay = hw.points ? `<span class="text-danger">${hw.points}分</span>` : '-';
|
|
||||||
|
|
||||||
html += `<tr>
|
html += `<tr>
|
||||||
<td>${escapeHtml(hw.title)}</td>
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
<td>${escapeHtml(hw.subject_name)}</td>
|
<td style="color: ${pointsColor}; font-weight: bold;">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${hw.deadline || '-'}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${statusDisplay}</td>
|
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||||
<td>${pointsDisplay}</td>
|
|
||||||
</tr>`;
|
</tr>`;
|
||||||
});
|
});
|
||||||
if (res.data.homework.length === 0) {
|
if (res.data.homework.length === 0) {
|
||||||
html = '<tr><td colspan="5" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业记录</td></tr>';
|
html = '<tr><td colspan="4" style="text-align:center; padding: 40px; color: #999;">📝 暂无作业扣分记录</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('homeworkList').innerHTML = html;
|
document.getElementById('homeworkList').innerHTML = html;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,102 +0,0 @@
|
|||||||
/**
|
|
||||||
* 班级操行分管理系统 - 科目管理页JS
|
|
||||||
*
|
|
||||||
* 开发者: Canglan
|
|
||||||
* 版权归属: Sea Network Technology Studio
|
|
||||||
*
|
|
||||||
* 版权所有 © Sea Network Technology Studio
|
|
||||||
*/
|
|
||||||
|
|
||||||
(function() {
|
|
||||||
'use strict';
|
|
||||||
|
|
||||||
async function loadSubjects() {
|
|
||||||
const res = await apiGet('/api/subject/list');
|
|
||||||
if (res && res.success) {
|
|
||||||
let html = '';
|
|
||||||
res.data.subjects.forEach(sub => {
|
|
||||||
html += `
|
|
||||||
<div class="subject-item">
|
|
||||||
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
|
|
||||||
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
|
|
||||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
|
||||||
${sub.is_active ? '启用' : '禁用'}
|
|
||||||
</span>
|
|
||||||
<button class="btn btn-sm btn-outline" onclick="showEditSubjectModal(${sub.subject_id}, '${escapeHtml(sub.subject_name)}', '${escapeHtml(sub.subject_code || '')}', ${sub.sort_order || 0})">编辑</button>
|
|
||||||
<button class="btn btn-sm btn-ghost" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
|
|
||||||
${sub.is_active ? '禁用' : '启用'}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-sm btn-outline-danger" onclick="deleteSubject(${sub.subject_id})">删除</button>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
});
|
|
||||||
if (res.data.subjects.length === 0) {
|
|
||||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
|
||||||
}
|
|
||||||
document.getElementById('subjectList').innerHTML = html;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function toggleSubject(subjectId, enable) {
|
|
||||||
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
|
|
||||||
if (res && res.success) {
|
|
||||||
showToast(enable ? '科目已启用' : '科目已禁用');
|
|
||||||
loadSubjects();
|
|
||||||
} 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('科目删除成功');
|
|
||||||
loadSubjects();
|
|
||||||
} 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');
|
|
||||||
loadSubjects();
|
|
||||||
} else {
|
|
||||||
showToast(res?.message || '更新失败', 'error');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
loadSubjects();
|
|
||||||
|
|
||||||
window.loadSubjects = loadSubjects;
|
|
||||||
window.toggleSubject = toggleSubject;
|
|
||||||
window.deleteSubject = deleteSubject;
|
|
||||||
window.showEditSubjectModal = showEditSubjectModal;
|
|
||||||
window.submitEditSubject = submitEditSubject;
|
|
||||||
|
|
||||||
})();
|
|
||||||
@@ -7,37 +7,42 @@
|
|||||||
"_comment6": " student_no - 必填,学生学号,唯一标识",
|
"_comment6": " student_no - 必填,学生学号,唯一标识",
|
||||||
"_comment7": " name - 必填,学生姓名",
|
"_comment7": " name - 必填,学生姓名",
|
||||||
"_comment8": " parent_phone - 可选,家长手机号(11位手机号)",
|
"_comment8": " parent_phone - 可选,家长手机号(11位手机号)",
|
||||||
"_comment9": " password - 可选,初始密码,不填则默认 123456",
|
"_comment9": " dormitory_number - 可选,宿舍号(支持字母数字组合,如 301-A)",
|
||||||
"_comment10": "================================================",
|
"_comment10": " password - 可选,初始密码,不填则默认 123456",
|
||||||
"_comment11": "导入规则:",
|
"_comment11": "================================================",
|
||||||
"_comment12": " 1. 学生操行分初始值 = 60分",
|
"_comment12": "导入规则:",
|
||||||
"_comment13": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
|
"_comment13": " 1. 学生操行分初始值 = 60分",
|
||||||
"_comment14": " 3. 家长账号 = 手机号(若parent_phone有值),密码 = 指定的password或123456",
|
"_comment14": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
|
||||||
"_comment15": " 4. 家长姓名默认显示为 '学生姓名家长'",
|
"_comment15": " 3. 家长账号 = 手机号(若parent_phone有值),密码 = 指定的password或123456",
|
||||||
"_comment16": "================================================",
|
"_comment16": " 4. 家长姓名默认显示为 '学生姓名家长'",
|
||||||
|
"_comment17": "================================================",
|
||||||
"students": [
|
"students": [
|
||||||
{
|
{
|
||||||
"student_no": "20240001",
|
"student_no": "20240001",
|
||||||
"name": "张三",
|
"name": "张三",
|
||||||
"parent_phone": "13800138001",
|
"parent_phone": "13800138001",
|
||||||
|
"dormitory_number": "301-A",
|
||||||
"password": "123456"
|
"password": "123456"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"student_no": "20240002",
|
"student_no": "20240002",
|
||||||
"name": "李四",
|
"name": "李四",
|
||||||
"parent_phone": "13800138002",
|
"parent_phone": "13800138002",
|
||||||
|
"dormitory_number": "205",
|
||||||
"password": "123456"
|
"password": "123456"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"student_no": "20240003",
|
"student_no": "20240003",
|
||||||
"name": "王五",
|
"name": "王五",
|
||||||
"parent_phone": "",
|
"parent_phone": "",
|
||||||
|
"dormitory_number": "",
|
||||||
"password": ""
|
"password": ""
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"student_no": "20240004",
|
"student_no": "20240004",
|
||||||
"name": "赵六",
|
"name": "赵六",
|
||||||
"parent_phone": "13800138004",
|
"parent_phone": "13800138004",
|
||||||
|
"dormitory_number": "102-B",
|
||||||
"password": ""
|
"password": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<div class="nav">
|
<div class="nav">
|
||||||
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
|
<a href="/admin/dashboard.php" class="nav-item<?php echo $current_page === 'dashboard' ? ' active' : ''; ?>">首页</a>
|
||||||
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
|
<a href="/admin/students.php" class="nav-item<?php echo $current_page === 'students' ? ' active' : ''; ?>">学生管理</a>
|
||||||
<?php if ($role === '班主任' || $role === '班长' || $role === '考勤委员' || $role === '劳动委员' || $role === '志愿委员'): ?>
|
<?php if ($role === '班主任' || $role === '班长' || $role === '学习委员' || $role === '考勤委员' || $role === '劳动委员' || $role === '志愿委员'): ?>
|
||||||
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
<a href="/admin/conduct.php" class="nav-item<?php echo $current_page === 'conduct' ? ' active' : ''; ?>">操行分管理</a>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||||
|
|||||||
@@ -42,10 +42,6 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="stat-label">班级排名</div>
|
<div class="stat-label">班级排名</div>
|
||||||
<div class="stat-value" id="studentRank">--</div>
|
<div class="stat-value" id="studentRank">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
|
||||||
<div class="stat-label">缺交次数</div>
|
|
||||||
<div class="stat-value" id="homeworkMissing">--</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="initial-points-hint" id="initialPointsHint"></div>
|
<div class="initial-points-hint" id="initialPointsHint"></div>
|
||||||
</div>
|
</div>
|
||||||
@@ -93,15 +89,6 @@ async function loadDashboard() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 加载作业缺交次数
|
|
||||||
const hwRes = await apiGet('/api/parent/child/homework');
|
|
||||||
if (hwRes && hwRes.success && hwRes.data.homework) {
|
|
||||||
const homework = hwRes.data.homework;
|
|
||||||
const total = homework.length;
|
|
||||||
const notSubmitted = homework.filter(h => h.status === 'not_submitted' || h.status === 'late').length;
|
|
||||||
document.getElementById('homeworkMissing').textContent = `缺交 ${notSubmitted}/${total} 次`;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示初始分提示
|
// 显示初始分提示
|
||||||
const initialPoints = window.STUDENT_INITIAL_POINTS || 60;
|
const initialPoints = window.STUDENT_INITIAL_POINTS || 60;
|
||||||
document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`;
|
document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`;
|
||||||
|
|||||||
@@ -94,7 +94,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="stat-value" id="studentRank">--</div>
|
<div class="stat-value" id="studentRank">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
<div class="stat-label">缺交次数</div>
|
<div class="stat-label">作业扣分</div>
|
||||||
<div class="stat-value" id="homeworkRate">--</div>
|
<div class="stat-value" id="homeworkRate">--</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="stat-card">
|
<div class="stat-card">
|
||||||
@@ -140,11 +140,10 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<table>
|
<table>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>科目</th>
|
|
||||||
<th>时间</th>
|
<th>时间</th>
|
||||||
<th>分值</th>
|
<th>分值</th>
|
||||||
<th>备注</th>
|
<th>原因</th>
|
||||||
<th>作业</th>
|
<th>操作人</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="homeworkList"></tbody>
|
<tbody id="homeworkList"></tbody>
|
||||||
@@ -319,14 +318,13 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
document.getElementById('studentRank').textContent = '--';
|
document.getElementById('studentRank').textContent = '--';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// 获取作业统计 - 缺交次数
|
// 获取作业扣分统计
|
||||||
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||||
if (homeworkRes && homeworkRes.success) {
|
if (homeworkRes && homeworkRes.success) {
|
||||||
const stats = homeworkRes.data.statistics;
|
const stats = homeworkRes.data.statistics;
|
||||||
const notSubmitted = (stats.not_submitted || 0) + (stats.late || 0);
|
const deductions = stats.deductions || 0;
|
||||||
const total = stats.total || 0;
|
const total = stats.total || 0;
|
||||||
document.getElementById('homeworkRate').textContent = `缺交 ${notSubmitted}/${total} 次`;
|
document.getElementById('homeworkRate').textContent = `${deductions} 次扣分`;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取考勤统计
|
// 获取考勤统计
|
||||||
@@ -394,25 +392,24 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||||
if (res && res.success) {
|
if (res && res.success) {
|
||||||
let html = '';
|
let html = '';
|
||||||
res.data.homework.forEach(hw => {
|
res.data.homework.forEach(record => {
|
||||||
const pointsDisplay = hw.points ? hw.points + '分' : '-';
|
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||||
html += `
|
html += `
|
||||||
<tr>
|
<tr>
|
||||||
<td>${escapeHtml(hw.subject_name)}</td>
|
<td>${formatDateTime(record.created_at)}</td>
|
||||||
<td>${hw.deadline || hw.created_at}</td>
|
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||||
<td>${pointsDisplay}</td>
|
<td>${escapeHtml(record.reason)}</td>
|
||||||
<td>${escapeHtml(hw.comments || '-')}</td>
|
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||||
<td>${escapeHtml(hw.title)}</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
`;
|
`;
|
||||||
});
|
});
|
||||||
if (res.data.homework.length === 0) {
|
if (res.data.homework.length === 0) {
|
||||||
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
|
html = '<tr><td colspan="4" style="text-align:center;">暂无作业扣分记录</td></tr>';
|
||||||
}
|
}
|
||||||
document.getElementById('homeworkList').innerHTML = html;
|
document.getElementById('homeworkList').innerHTML = html;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('加载作业失败:', error);
|
console.error('加载作业记录失败:', error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ include __DIR__ . '/../includes/header.php';
|
|||||||
<div class="table-wrapper">
|
<div class="table-wrapper">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>作业名称</th><th>科目</th><th>截止时间</th><th>提交状态</th><th>扣分</th></tr>
|
<tr><th>时间</th><th>分值</th><th>原因</th><th>操作人</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody id="homeworkList"></tbody>
|
<tbody id="homeworkList"></tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
28
sql/init.sql
28
sql/init.sql
@@ -202,6 +202,26 @@ CREATE TABLE IF NOT EXISTS `semester_archives` (
|
|||||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
|
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
|
||||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 系统设置表
|
||||||
|
CREATE TABLE IF NOT EXISTS `system_settings` (
|
||||||
|
`setting_key` VARCHAR(50) PRIMARY KEY,
|
||||||
|
`setting_value` VARCHAR(255) NOT NULL,
|
||||||
|
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||||
|
|
||||||
|
-- 性能索引(v2.0)
|
||||||
|
CREATE INDEX `idx_conduct_semester` ON `conduct_records`(`semester_id`);
|
||||||
|
CREATE INDEX `idx_attendance_semester` ON `attendance_records`(`semester_id`);
|
||||||
|
CREATE INDEX `idx_conduct_student` ON `conduct_records`(`student_id`);
|
||||||
|
|
||||||
|
-- 性能索引(v2.1)
|
||||||
|
CREATE INDEX `idx_student_created` ON `conduct_records`(`student_id`, `created_at`);
|
||||||
|
CREATE INDEX `idx_recorder_id` ON `conduct_records`(`recorder_id`);
|
||||||
|
CREATE INDEX `idx_date` ON `attendance_records`(`date`);
|
||||||
|
CREATE INDEX `idx_username_created` ON `login_logs`(`username`, `created_at`);
|
||||||
|
CREATE INDEX `idx_operator_created` ON `operation_logs`(`operator_id`, `created_at`);
|
||||||
|
CREATE INDEX `idx_semester_id` ON `semester_archives`(`semester_id`);
|
||||||
|
|
||||||
SET FOREIGN_KEY_CHECKS = 1;
|
SET FOREIGN_KEY_CHECKS = 1;
|
||||||
|
|
||||||
-- 插入初始科目(仅语数英,如不存在)
|
-- 插入初始科目(仅语数英,如不存在)
|
||||||
@@ -210,4 +230,10 @@ INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VAL
|
|||||||
('数学', 'MATH', 2),
|
('数学', 'MATH', 2),
|
||||||
('英语', 'ENG', 3);
|
('英语', 'ENG', 3);
|
||||||
|
|
||||||
SELECT '数据库初始化完成!' AS message;
|
-- 初始化系统版本号
|
||||||
|
INSERT INTO `system_settings` (`setting_key`, `setting_value`)
|
||||||
|
VALUES ('db_version', '2.2')
|
||||||
|
ON DUPLICATE KEY UPDATE `setting_value` = '2.2';
|
||||||
|
|
||||||
|
-- 控制台输出初始化结果(含版本号)
|
||||||
|
SELECT CONCAT('数据库初始化完成!版本: v', (SELECT setting_value FROM system_settings WHERE setting_key = 'db_version')) AS message;
|
||||||
|
|||||||
11
sql/upgrades/v2.2.sql
Normal file
11
sql/upgrades/v2.2.sql
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
-- ===========================================
|
||||||
|
-- 班级操行分管理系统 - v2.1 → v2.2 升级脚本
|
||||||
|
-- 字符集: utf8mb4
|
||||||
|
--
|
||||||
|
-- 说明: v2.2 为安全修复与功能增强版本,无数据库 schema 变更。
|
||||||
|
-- 主要变更:
|
||||||
|
-- 1. 修复管理员操作越权漏洞(加分/撤销/恢复操作增加权限校验)
|
||||||
|
-- 2. 新增宿舍集体加分功能(前端+后端)
|
||||||
|
-- 3. 学生导入支持宿舍号字段
|
||||||
|
-- 4. 导入预览表格显示宿舍号列
|
||||||
|
-- ===========================================
|
||||||
41
upgrade.php
41
upgrade.php
@@ -10,6 +10,16 @@
|
|||||||
// 辅助函数
|
// 辅助函数
|
||||||
// ===========================================
|
// ===========================================
|
||||||
|
|
||||||
|
// 版本升级列表(唯一数据源)
|
||||||
|
$UPGRADE_VERSIONS = [
|
||||||
|
'1.7' => __DIR__ . '/sql/upgrades/v1.7.sql',
|
||||||
|
'1.8' => __DIR__ . '/sql/upgrades/v1.8.sql',
|
||||||
|
'2.0' => __DIR__ . '/sql/upgrades/v2.0.sql',
|
||||||
|
'2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql',
|
||||||
|
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
||||||
|
'2.2' => __DIR__ . '/sql/upgrades/v2.2.sql',
|
||||||
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 读取 backend/.env 文件并解析数据库配置
|
* 读取 backend/.env 文件并解析数据库配置
|
||||||
*/
|
*/
|
||||||
@@ -59,16 +69,10 @@ function detectCurrentVersion($pdo) {
|
|||||||
* 获取需要执行的升级步骤
|
* 获取需要执行的升级步骤
|
||||||
*/
|
*/
|
||||||
function getUpgradeSteps($currentVersion, $targetVersion) {
|
function getUpgradeSteps($currentVersion, $targetVersion) {
|
||||||
$allVersions = [
|
global $UPGRADE_VERSIONS;
|
||||||
'1.7' => __DIR__ . '/sql/upgrades/v1.7.sql',
|
|
||||||
'1.8' => __DIR__ . '/sql/upgrades/v1.8.sql',
|
|
||||||
'2.0' => __DIR__ . '/sql/upgrades/v2.0.sql',
|
|
||||||
'2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql',
|
|
||||||
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
|
||||||
];
|
|
||||||
|
|
||||||
$steps = [];
|
$steps = [];
|
||||||
foreach ($allVersions as $version => $sqlFile) {
|
foreach ($UPGRADE_VERSIONS as $version => $sqlFile) {
|
||||||
if (version_compare($version, $currentVersion, '>') &&
|
if (version_compare($version, $currentVersion, '>') &&
|
||||||
version_compare($version, $targetVersion, '<=')) {
|
version_compare($version, $targetVersion, '<=')) {
|
||||||
$steps[$version] = $sqlFile;
|
$steps[$version] = $sqlFile;
|
||||||
@@ -128,19 +132,11 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST' && ($_GET['action'] ?? '') === 'step')
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// 获取该版本对应的 SQL 文件
|
// 获取该版本对应的 SQL 文件
|
||||||
$allVersions = [
|
if (!isset($UPGRADE_VERSIONS[$stepVersion])) {
|
||||||
'1.7' => __DIR__ . '/sql/upgrades/v1.7.sql',
|
|
||||||
'1.8' => __DIR__ . '/sql/upgrades/v1.8.sql',
|
|
||||||
'2.0' => __DIR__ . '/sql/upgrades/v2.0.sql',
|
|
||||||
'2.0.1' => __DIR__ . '/sql/upgrades/v2.0.1.sql',
|
|
||||||
'2.1' => __DIR__ . '/sql/upgrades/v2.1.sql',
|
|
||||||
];
|
|
||||||
|
|
||||||
if (!isset($allVersions[$stepVersion])) {
|
|
||||||
throw new RuntimeException("未知版本: {$stepVersion}");
|
throw new RuntimeException("未知版本: {$stepVersion}");
|
||||||
}
|
}
|
||||||
|
|
||||||
$sqlFile = $allVersions[$stepVersion];
|
$sqlFile = $UPGRADE_VERSIONS[$stepVersion];
|
||||||
$shortFile = basename($sqlFile);
|
$shortFile = basename($sqlFile);
|
||||||
|
|
||||||
executeUpgrade($pdo, $stepVersion, $sqlFile);
|
executeUpgrade($pdo, $stepVersion, $sqlFile);
|
||||||
@@ -432,6 +428,15 @@ try {
|
|||||||
<div class="error-box">
|
<div class="error-box">
|
||||||
<strong>错误:</strong><?php echo htmlspecialchars($errorMessage); ?>
|
<strong>错误:</strong><?php echo htmlspecialchars($errorMessage); ?>
|
||||||
</div>
|
</div>
|
||||||
|
<?php if ($hasError && strpos($errorMessage, '配置文件不存在') !== false): ?>
|
||||||
|
<div class="warning-box">
|
||||||
|
<strong>💡 解决方法:</strong><br>
|
||||||
|
1. 进入 <code>backend/</code> 目录<br>
|
||||||
|
2. 复制配置模板:<code>cp .env.example .env</code><br>
|
||||||
|
3. 编辑 <code>.env</code> 文件,填入实际的数据库连接信息<br>
|
||||||
|
4. 刷新此页面
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<?php elseif ($isUpToDate): ?>
|
<?php elseif ($isUpToDate): ?>
|
||||||
<div class="success-box">
|
<div class="success-box">
|
||||||
✓ 数据库已是最新版本,无需升级。
|
✓ 数据库已是最新版本,无需升级。
|
||||||
|
|||||||
Reference in New Issue
Block a user