v2.2更新
This commit is contained in:
@@ -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=["调试"])
|
||||
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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"
|
||||
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,
|
||||
|
||||
@@ -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"])
|
||||
|
||||
|
||||
# ========== 考勤管理 ==========
|
||||
|
||||
|
||||
@@ -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):
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
|
||||
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="用户名")
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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.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)
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user