v1.2版本更新发布
This commit is contained in:
@@ -243,6 +243,7 @@ classmanager/
|
||||
|------|---------|------|
|
||||
| v1.0 | 2026.4.19 | 初始版本发布,包含基础功能 |
|
||||
| v1.1 | 2026.4.20 | 更新家长端查看加减分记录功能 |
|
||||
| v1.2 | 2026.4.22 | 新增学期功能 |
|
||||
|
||||
## 许可证
|
||||
|
||||
|
||||
@@ -54,6 +54,9 @@ PASSWORD_SALT=your-fixed-salt-string-for-password-hash
|
||||
# 调试入口配置
|
||||
# ===========================================
|
||||
|
||||
# 调试功能开关 - 设为 true 启用调试路由,生产环境必须为 false
|
||||
DEBUG_ENABLED=false
|
||||
# 调试入口路径 - 自定义随机路径增强安全性
|
||||
DEBUG_PATH=/a7k9x2m4q8w1e3r5t6y7u8i9o0p1z2x3
|
||||
|
||||
# ===========================================
|
||||
@@ -83,12 +86,26 @@ LABOR_POINTS_ADD=1
|
||||
LABOR_POINTS_SUBTRACT=-1
|
||||
|
||||
# ===========================================
|
||||
# 班长加减分限制配置
|
||||
# 各角色加减分限制配置
|
||||
# ===========================================
|
||||
|
||||
# 班长单次加分上限
|
||||
MONITOR_MAX_ADD=5
|
||||
# 班长单次扣分上限(负数)
|
||||
MONITOR_MAX_SUBTRACT=-5
|
||||
|
||||
# 学习委员单次加减分上限(绝对值)- 正负均不可超过此值
|
||||
STUDY_COMMISSIONER_MAX_POINTS=5
|
||||
|
||||
# 考勤委员单次扣分上限(绝对值)
|
||||
ATTENDANCE_REP_MAX_POINTS=5
|
||||
|
||||
# 劳动委员单次加减分上限(绝对值)
|
||||
LABOR_REP_MAX_POINTS=1
|
||||
|
||||
# 志愿委员单次加分上限
|
||||
VOLUNTEER_REP_MAX_POINTS=5
|
||||
|
||||
# ===========================================
|
||||
# 日志配置
|
||||
# ===========================================
|
||||
|
||||
@@ -47,6 +47,7 @@ class Settings:
|
||||
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30"))
|
||||
|
||||
PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "")
|
||||
DEBUG_ENABLED: bool = os.getenv("DEBUG_ENABLED", "False").lower() == "true"
|
||||
DEBUG_PATH: str = os.getenv("DEBUG_PATH", "/debug_add_admin")
|
||||
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
|
||||
@@ -61,6 +62,11 @@ class Settings:
|
||||
MONITOR_MAX_ADD: int = int(os.getenv("MONITOR_MAX_ADD", "5"))
|
||||
MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5"))
|
||||
|
||||
STUDY_COMMISSIONER_MAX_POINTS: int = int(os.getenv("STUDY_COMMISSIONER_MAX_POINTS", "5"))
|
||||
ATTENDANCE_REP_MAX_POINTS: int = int(os.getenv("ATTENDANCE_REP_MAX_POINTS", "5"))
|
||||
LABOR_REP_MAX_POINTS: int = int(os.getenv("LABOR_REP_MAX_POINTS", "1"))
|
||||
VOLUNTEER_REP_MAX_POINTS: int = int(os.getenv("VOLUNTEER_REP_MAX_POINTS", "5"))
|
||||
|
||||
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
||||
LOG_MAX_BYTES: int = int(os.getenv("LOG_MAX_BYTES", "104857600"))
|
||||
LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "30"))
|
||||
|
||||
@@ -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, debug
|
||||
from routes import auth, student, parent, admin, subject, semester, debug
|
||||
|
||||
|
||||
# 设置日志
|
||||
@@ -116,6 +116,7 @@ app.include_router(student.router, prefix="/api/student", tags=["学生端"])
|
||||
app.include_router(parent.router, prefix="/api/parent", tags=["家长端"])
|
||||
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(debug.router, tags=["调试"])
|
||||
|
||||
|
||||
|
||||
@@ -36,11 +36,10 @@ def is_public_path(path: str) -> bool:
|
||||
for pattern in PUBLIC_PATHS:
|
||||
if re.match(pattern, path):
|
||||
return True
|
||||
# 动态匹配调试入口路径
|
||||
if settings.DEBUG_PATH and path == settings.DEBUG_PATH:
|
||||
# 动态匹配调试入口路径(需同时启用调试功能)
|
||||
if settings.DEBUG_ENABLED and settings.DEBUG_PATH and path == settings.DEBUG_PATH:
|
||||
return True
|
||||
return False
|
||||
return False
|
||||
|
||||
|
||||
class AuthMiddleware(BaseHTTPMiddleware):
|
||||
|
||||
@@ -32,12 +32,11 @@ class AdminRoleModel:
|
||||
sql = """
|
||||
SELECT ar.*, u.real_name, u.username, s.subject_name
|
||||
FROM admin_roles ar
|
||||
JOIN users u ON ar.user_id = u.user_id
|
||||
JOIN users u ON ar.user_id = u.user_id AND u.status = 1
|
||||
LEFT JOIN subjects s ON ar.subject_id = s.subject_id
|
||||
ORDER BY ar.role_type
|
||||
"""
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def create(user_id: int, role_type: str, subject_id: int = None) -> int:
|
||||
sql = """
|
||||
|
||||
166
backend/models/semester.py
Normal file
166
backend/models/semester.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 学期数据模型
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update, execute_many
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SemesterModel:
|
||||
"""学期数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
semester_name: str,
|
||||
start_date: str = None,
|
||||
end_date: str = None
|
||||
) -> int:
|
||||
"""创建学期"""
|
||||
sql = """
|
||||
INSERT INTO semesters (semester_name, start_date, end_date)
|
||||
VALUES (%s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (semester_name, start_date, end_date))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(semester_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取学期信息"""
|
||||
sql = "SELECT * FROM semesters WHERE semester_id = %s"
|
||||
return await execute_one(sql, (semester_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> List[Dict[str, Any]]:
|
||||
"""获取所有学期列表"""
|
||||
sql = """
|
||||
SELECT semester_id, semester_name, start_date, end_date,
|
||||
is_active, is_archived, created_at
|
||||
FROM semesters
|
||||
ORDER BY created_at DESC
|
||||
"""
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def get_active() -> Optional[Dict[str, Any]]:
|
||||
"""获取当前活跃学期"""
|
||||
sql = """
|
||||
SELECT semester_id, semester_name, start_date, end_date,
|
||||
is_active, is_archived, created_at
|
||||
FROM semesters
|
||||
WHERE is_active = 1 AND is_archived = 0
|
||||
LIMIT 1
|
||||
"""
|
||||
return await execute_one(sql)
|
||||
|
||||
@staticmethod
|
||||
async def deactivate_all() -> int:
|
||||
"""将所有学期设为非活跃"""
|
||||
sql = "UPDATE semesters SET is_active = 0 WHERE is_active = 1"
|
||||
return await execute_update(sql)
|
||||
|
||||
@staticmethod
|
||||
async def activate(semester_id: int) -> bool:
|
||||
"""设为当前活跃学期"""
|
||||
sql = """
|
||||
UPDATE semesters SET is_active = 1
|
||||
WHERE semester_id = %s AND is_archived = 0
|
||||
"""
|
||||
result = await execute_update(sql, (semester_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def archive(semester_id: int) -> bool:
|
||||
"""归档学期"""
|
||||
sql = """
|
||||
UPDATE semesters SET is_archived = 1, is_active = 0
|
||||
WHERE semester_id = %s AND is_archived = 0
|
||||
"""
|
||||
result = await execute_update(sql, (semester_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def is_archived(semester_id: int) -> bool:
|
||||
"""检查学期是否已归档"""
|
||||
sql = "SELECT is_archived FROM semesters WHERE semester_id = %s"
|
||||
result = await execute_one(sql, (semester_id,))
|
||||
if not result:
|
||||
return False
|
||||
return bool(result['is_archived'])
|
||||
|
||||
@staticmethod
|
||||
async def get_record_semester_id(record_id: int) -> Optional[int]:
|
||||
"""获取操行分记录所属的学期ID"""
|
||||
sql = "SELECT semester_id FROM conduct_records WHERE record_id = %s"
|
||||
result = await execute_one(sql, (record_id,))
|
||||
return result['semester_id'] if result else None
|
||||
|
||||
|
||||
class SemesterArchiveModel:
|
||||
"""学期归档快照数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def batch_create(archives_data: List[Dict]) -> int:
|
||||
"""批量创建归档快照"""
|
||||
if not archives_data:
|
||||
return 0
|
||||
sql = """
|
||||
INSERT INTO semester_archives
|
||||
(semester_id, student_id, student_no, student_name, final_points, rank_position, total_students)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
params_list = [
|
||||
(
|
||||
a['semester_id'], a['student_id'], a['student_no'],
|
||||
a['student_name'], a['final_points'],
|
||||
a.get('rank_position'), a.get('total_students')
|
||||
)
|
||||
for a in archives_data
|
||||
]
|
||||
return await execute_many(sql, params_list)
|
||||
|
||||
@staticmethod
|
||||
async def get_by_semester(semester_id: int) -> List[Dict[str, Any]]:
|
||||
"""获取学期的归档数据"""
|
||||
sql = """
|
||||
SELECT archive_id, semester_id, student_id, student_no,
|
||||
student_name, final_points, rank_position, total_students, archived_at
|
||||
FROM semester_archives
|
||||
WHERE semester_id = %s
|
||||
ORDER BY rank_position ASC
|
||||
"""
|
||||
return await execute_query(sql, (semester_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_semester_and_student(semester_id: int, student_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取指定学期指定学生的归档数据"""
|
||||
sql = """
|
||||
SELECT archive_id, semester_id, student_id, student_no,
|
||||
student_name, final_points, rank_position, total_students, archived_at
|
||||
FROM semester_archives
|
||||
WHERE semester_id = %s AND student_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (semester_id, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_student(student_id: int) -> List[Dict[str, Any]]:
|
||||
"""获取学生在所有已归档学期的数据"""
|
||||
sql = """
|
||||
SELECT sa.archive_id, sa.semester_id, sa.student_id, sa.student_no,
|
||||
sa.student_name, sa.final_points, sa.rank_position,
|
||||
sa.total_students, sa.archived_at,
|
||||
s.semester_name, s.start_date, s.end_date
|
||||
FROM semester_archives sa
|
||||
JOIN semesters s ON sa.semester_id = s.semester_id
|
||||
WHERE sa.student_id = %s
|
||||
ORDER BY sa.archived_at DESC
|
||||
"""
|
||||
return await execute_query(sql, (student_id,))
|
||||
@@ -120,6 +120,13 @@ class StudentModel:
|
||||
row['rank'] = i + 1
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
async def get_total_count() -> int:
|
||||
"""获取活跃学生总数"""
|
||||
sql = "SELECT COUNT(*) as total FROM students WHERE status = 1"
|
||||
result = await execute_one(sql)
|
||||
return result["total"] if result else 0
|
||||
|
||||
@staticmethod
|
||||
async def batch_create(students_data: List[Dict], initial_points: int = 60) -> List[Dict]:
|
||||
"""批量创建学生"""
|
||||
|
||||
@@ -161,10 +161,17 @@ async def revoke_conduct_record(request: Request, req: RevokeRequest):
|
||||
)
|
||||
if result["success"]:
|
||||
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||
record = result.get("record", {})
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role=role, operation_type="revoke_record",
|
||||
target_type="conduct", target_id=req.record_id,
|
||||
details=(
|
||||
f"撤销记录ID: {req.record_id}, "
|
||||
f"原操作人: {record.get('recorder_name', '未知')}, "
|
||||
f"原分值变动: {'+' if record.get('points_change', 0) > 0 else ''}{record.get('points_change', 0)}分, "
|
||||
f"撤销操作人: {user['username']}"
|
||||
),
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message="撤销成功")
|
||||
@@ -447,7 +454,6 @@ async def reset_admin_password(request: Request, user_id: int, req: ResetPasswor
|
||||
return error_response(message="仅班主任可重置密码", code=403)
|
||||
|
||||
from models.user import UserModel
|
||||
from utils.security import SecurityUtils
|
||||
|
||||
# 获取管理员信息
|
||||
target_user = await UserModel.get_by_user_id(user_id)
|
||||
@@ -457,12 +463,8 @@ async def reset_admin_password(request: Request, user_id: int, req: ResetPasswor
|
||||
if target_user["user_type"] != "admin":
|
||||
return error_response(message="只能重置管理员密码", code=400)
|
||||
|
||||
# 使用传入的新密码
|
||||
new_password = req.new_password
|
||||
password_hash = SecurityUtils.sha1_md5_password(new_password)
|
||||
|
||||
# 更新密码
|
||||
updated = await UserModel.update_password(user_id, password_hash)
|
||||
# 使用传入的新密码(UserModel.update_password 内部会进行哈希)
|
||||
updated = await UserModel.update_password(user_id, req.new_password)
|
||||
if updated:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
|
||||
@@ -32,6 +32,11 @@ class AddAdminDebugRequest(BaseModel):
|
||||
|
||||
@router.post(settings.DEBUG_PATH)
|
||||
async def debug_add_admin(request: Request, req: AddAdminDebugRequest):
|
||||
# 检查调试功能是否启用
|
||||
if not settings.DEBUG_ENABLED:
|
||||
from fastapi.responses import JSONResponse
|
||||
return JSONResponse(status_code=404, content={"detail": "Not Found"})
|
||||
|
||||
from models.user import UserModel
|
||||
|
||||
valid_roles = ["班主任", "班长", "学习委员", "考勤委员", "劳动委员", "志愿委员"]
|
||||
|
||||
151
backend/routes/semester.py
Normal file
151
backend/routes/semester.py
Normal file
@@ -0,0 +1,151 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 学期管理路由
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import (
|
||||
get_current_user,
|
||||
PermissionChecker
|
||||
)
|
||||
from services.semester_service import SemesterService
|
||||
from services.log_service import LogService
|
||||
from schemas.semester import CreateSemesterRequest
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def list_semesters(request: Request):
|
||||
"""获取学期列表(仅班主任)"""
|
||||
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 SemesterService.list_semesters()
|
||||
if result["success"]:
|
||||
return success_response(data=result["semesters"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.get("/active")
|
||||
async def get_active_semester(request: Request):
|
||||
"""获取当前活跃学期"""
|
||||
user = await get_current_user(request)
|
||||
result = await SemesterService.get_active_semester()
|
||||
if result["success"]:
|
||||
return success_response(data=result.get("semester"))
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_semester(request: Request, req: CreateSemesterRequest):
|
||||
"""创建学期(班主任)"""
|
||||
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 SemesterService.create_semester(
|
||||
semester_name=req.semester_name,
|
||||
start_date=req.start_date,
|
||||
end_date=req.end_date,
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="create_semester",
|
||||
target_type="semester", target_id=result.get("semester_id"),
|
||||
details=f"创建学期: {req.semester_name}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(data=result, message="学期创建成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.put("/activate/{semester_id}")
|
||||
async def activate_semester(request: Request, semester_id: int):
|
||||
"""设为当前学期(班主任)"""
|
||||
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 SemesterService.activate_semester(
|
||||
semester_id=semester_id,
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="activate_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"激活学期ID: {semester_id}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message=result["message"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.post("/archive/{semester_id}")
|
||||
async def archive_semester(request: Request, semester_id: int):
|
||||
"""归档学期(班主任)"""
|
||||
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 SemesterService.archive_semester(
|
||||
semester_id=semester_id,
|
||||
operator_id=user["user_id"]
|
||||
)
|
||||
if result["success"]:
|
||||
await LogService.write_operation_log(
|
||||
operator_id=user["user_id"], operator_name=user["username"],
|
||||
operator_role="班主任", operation_type="archive_semester",
|
||||
target_type="semester", target_id=semester_id,
|
||||
details=f"归档学期ID: {semester_id}",
|
||||
ip=request.client.host
|
||||
)
|
||||
return success_response(message=result["message"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.get("/archive/{semester_id}/records")
|
||||
async def get_archive_records(
|
||||
request: Request,
|
||||
semester_id: int,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=200)
|
||||
):
|
||||
"""查看归档数据(仅班主任)"""
|
||||
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 SemesterService.get_archive_records(
|
||||
semester_id=semester_id,
|
||||
page=page,
|
||||
page_size=page_size
|
||||
)
|
||||
if result["success"]:
|
||||
return success_response(data=result["data"])
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
@@ -120,3 +120,19 @@ async def get_my_info(request: Request):
|
||||
result = await StudentService.get_student_info(user["student_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/semester-records")
|
||||
async def get_student_semester_records(request: Request):
|
||||
"""
|
||||
获取当前学生的历史学期归档记录
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "student":
|
||||
return error_response(message="仅限学生访问", code=403)
|
||||
|
||||
from models.semester import SemesterArchiveModel
|
||||
records = await SemesterArchiveModel.get_by_student(user["student_id"])
|
||||
|
||||
return success_response(data={"records": records})
|
||||
20
backend/schemas/semester.py
Normal file
20
backend/schemas/semester.py
Normal file
@@ -0,0 +1,20 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 学期请求模型
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class CreateSemesterRequest(BaseModel):
|
||||
"""创建学期请求"""
|
||||
semester_name: str = Field(..., min_length=1, max_length=100, description="学期名称")
|
||||
start_date: Optional[str] = Field(None, description="学期开始日期 (YYYY-MM-DD)")
|
||||
end_date: Optional[str] = Field(None, description="学期结束日期 (YYYY-MM-DD)")
|
||||
@@ -52,17 +52,25 @@ class ConductService:
|
||||
if points_change > settings.MONITOR_MAX_ADD or points_change < settings.MONITOR_MAX_SUBTRACT:
|
||||
return {"success": False, "message": f"班长单次只能加减{settings.MONITOR_MAX_ADD}分以内"}
|
||||
elif role == "劳动委员":
|
||||
# 劳动委员固定 ±1分
|
||||
if points_change not in [settings.LABOR_POINTS_ADD, settings.LABOR_POINTS_SUBTRACT]:
|
||||
return {"success": False, "message": "劳动委员只能进行±1分操作"}
|
||||
# 劳动委员可加减分,±LABOR_REP_MAX_POINTS以内
|
||||
if abs(points_change) > settings.LABOR_REP_MAX_POINTS:
|
||||
return {"success": False, "message": f"劳动委员单次只能加减{settings.LABOR_REP_MAX_POINTS}分以内"}
|
||||
elif role == "志愿委员":
|
||||
# 志愿委员只能加分,不限制正分上限
|
||||
# 志愿委员只能加分,上限VOLUNTEER_REP_MAX_POINTS
|
||||
if points_change < 0:
|
||||
return {"success": False, "message": "志愿委员只能加分"}
|
||||
elif role in ["学习委员", "考勤委员"]:
|
||||
# 学习委员和考勤委员只能扣分
|
||||
if points_change > settings.VOLUNTEER_REP_MAX_POINTS:
|
||||
return {"success": False, "message": f"志愿委员单次最多加{settings.VOLUNTEER_REP_MAX_POINTS}分"}
|
||||
elif role == "学习委员":
|
||||
# 学习委员可加减分,±STUDY_COMMISSIONER_MAX_POINTS以内
|
||||
if abs(points_change) > settings.STUDY_COMMISSIONER_MAX_POINTS:
|
||||
return {"success": False, "message": f"学习委员单次只能加减{settings.STUDY_COMMISSIONER_MAX_POINTS}分以内"}
|
||||
elif role == "考勤委员":
|
||||
# 考勤委员只能扣分,上限ATTENDANCE_REP_MAX_POINTS
|
||||
if points_change > 0:
|
||||
return {"success": False, "message": "该角色只能进行扣分操作"}
|
||||
return {"success": False, "message": "考勤委员只能进行扣分操作"}
|
||||
if abs(points_change) > settings.ATTENDANCE_REP_MAX_POINTS:
|
||||
return {"success": False, "message": f"考勤委员单次最多扣{settings.ATTENDANCE_REP_MAX_POINTS}分"}
|
||||
else:
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
@@ -121,6 +129,9 @@ class ConductService:
|
||||
if not record:
|
||||
return {"success": False, "message": "记录不存在"}
|
||||
|
||||
# 归档后班主任仍可撤销/修改记录(任务需求#8)
|
||||
# 归档操作本身不可逆,但归档数据可由班主任修改
|
||||
|
||||
# 撤销记录
|
||||
result = await ConductModel.revoke_record(record_id, revoker_id)
|
||||
|
||||
@@ -128,7 +139,16 @@ class ConductService:
|
||||
# 反向恢复学生总分
|
||||
await StudentModel.update_total_points(record["student_id"], -record["points_change"])
|
||||
logger.info(f"用户[{revoker_id}] 撤销了记录[{record_id}]")
|
||||
return {"success": True, "message": "撤销成功"}
|
||||
return {
|
||||
"success": True,
|
||||
"message": "撤销成功",
|
||||
"record": {
|
||||
"student_id": record["student_id"],
|
||||
"recorder_name": record.get("recorder_name", "未知"),
|
||||
"points_change": record["points_change"],
|
||||
"reason": record.get("reason", "")
|
||||
}
|
||||
}
|
||||
else:
|
||||
return {"success": False, "message": "撤销失败"}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import math
|
||||
from typing import Dict, Any, Optional, List
|
||||
|
||||
from models.user import UserModel
|
||||
@@ -93,22 +94,27 @@ class ParentService:
|
||||
|
||||
# 获取全班排名
|
||||
ranking = await StudentModel.get_ranking(limit=1000)
|
||||
total_students = await StudentModel.get_total_count()
|
||||
|
||||
# 查找当前学生排名
|
||||
student_rank = None
|
||||
total_students = 0
|
||||
for r in ranking:
|
||||
total_students += 1
|
||||
if r["student_id"] == user["student_id"]:
|
||||
student_rank = r["rank"]
|
||||
|
||||
# 计算百分比排名
|
||||
percentile = None
|
||||
if student_rank and total_students and total_students > 0:
|
||||
percentile = math.ceil(student_rank / total_students * 100)
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"student_no": student["student_no"],
|
||||
"total_points": student["total_points"],
|
||||
"rank": student_rank,
|
||||
"total_students": total_students
|
||||
"total_students": total_students,
|
||||
"percentile": percentile
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
244
backend/services/semester_service.py
Normal file
244
backend/services/semester_service.py
Normal file
@@ -0,0 +1,244 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 学期服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from models.semester import SemesterModel, SemesterArchiveModel
|
||||
from models.student import StudentModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
from utils.database import execute_query
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SemesterService:
|
||||
"""学期管理服务"""
|
||||
|
||||
@staticmethod
|
||||
async def list_semesters() -> Dict[str, Any]:
|
||||
"""获取学期列表"""
|
||||
try:
|
||||
semesters = await SemesterModel.get_all()
|
||||
return {
|
||||
"success": True,
|
||||
"semesters": semesters
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取学期列表失败: {e}")
|
||||
return {"success": False, "message": f"获取学期列表失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def create_semester(
|
||||
semester_name: str,
|
||||
start_date: str = None,
|
||||
end_date: str = None,
|
||||
operator_id: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""创建新学期"""
|
||||
if not semester_name or not semester_name.strip():
|
||||
return {"success": False, "message": "学期名称不能为空"}
|
||||
|
||||
try:
|
||||
# 自动将之前的活跃学期设为非活跃
|
||||
await SemesterModel.deactivate_all()
|
||||
|
||||
# 创建新学期并自动设为活跃
|
||||
semester_id = await SemesterModel.create(
|
||||
semester_name=semester_name.strip(),
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
# 设为活跃学期
|
||||
await SemesterModel.activate(semester_id)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 创建了新学期: {semester_name}")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": "学期创建成功",
|
||||
"semester_id": semester_id
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"创建学期失败: {e}")
|
||||
return {"success": False, "message": f"创建学期失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def activate_semester(
|
||||
semester_id: int,
|
||||
operator_id: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""设为当前活跃学期"""
|
||||
try:
|
||||
# 检查学期是否存在
|
||||
semester = await SemesterModel.get_by_id(semester_id)
|
||||
if not semester:
|
||||
return {"success": False, "message": "学期不存在"}
|
||||
|
||||
# 已归档的学期不能激活
|
||||
if semester['is_archived']:
|
||||
return {"success": False, "message": "已归档的学期不能设为当前学期"}
|
||||
|
||||
# 自动将之前的活跃学期设为非活跃
|
||||
await SemesterModel.deactivate_all()
|
||||
|
||||
# 设为活跃
|
||||
result = await SemesterModel.activate(semester_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"用户[{operator_id}] 激活了学期: {semester['semester_name']}")
|
||||
return {"success": True, "message": "已设为当前学期"}
|
||||
else:
|
||||
return {"success": False, "message": "激活失败"}
|
||||
except Exception as e:
|
||||
logger.error(f"激活学期失败: {e}")
|
||||
return {"success": False, "message": f"激活学期失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def archive_semester(
|
||||
semester_id: int,
|
||||
operator_id: int = None
|
||||
) -> Dict[str, Any]:
|
||||
"""归档学期"""
|
||||
try:
|
||||
# 检查学期是否存在
|
||||
semester = await SemesterModel.get_by_id(semester_id)
|
||||
if not semester:
|
||||
return {"success": False, "message": "学期不存在"}
|
||||
|
||||
# 已归档的不能重复归档
|
||||
if semester['is_archived']:
|
||||
return {"success": False, "message": "该学期已归档"}
|
||||
|
||||
# 获取所有活跃学生及其当前分数
|
||||
students = await StudentModel.get_all(include_disabled=False)
|
||||
if not students:
|
||||
return {"success": False, "message": "没有可归档的学生数据"}
|
||||
|
||||
total_students = len(students)
|
||||
|
||||
# 按分数降序排列以计算排名
|
||||
sorted_students = sorted(students, key=lambda s: s['total_points'], reverse=True)
|
||||
|
||||
# 构建归档快照数据
|
||||
archives_data = []
|
||||
for rank, student in enumerate(sorted_students, 1):
|
||||
archives_data.append({
|
||||
'semester_id': semester_id,
|
||||
'student_id': student['student_id'],
|
||||
'student_no': student['student_no'],
|
||||
'student_name': student['name'],
|
||||
'final_points': student['total_points'],
|
||||
'rank_position': rank,
|
||||
'total_students': total_students
|
||||
})
|
||||
|
||||
# 保存归档快照
|
||||
await SemesterArchiveModel.batch_create(archives_data)
|
||||
|
||||
# 标记学期为已归档
|
||||
await SemesterModel.archive(semester_id)
|
||||
|
||||
logger.info(
|
||||
f"用户[{operator_id}] 归档了学期: {semester['semester_name']}, "
|
||||
f"共 {total_students} 名学生"
|
||||
)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"学期归档成功,共归档 {total_students} 名学生数据"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"归档学期失败: {e}")
|
||||
return {"success": False, "message": f"归档学期失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def get_archive_records(
|
||||
semester_id: int,
|
||||
page: int = 1,
|
||||
page_size: int = 50
|
||||
) -> Dict[str, Any]:
|
||||
"""获取归档数据(只读)"""
|
||||
try:
|
||||
# 检查学期是否存在
|
||||
semester = await SemesterModel.get_by_id(semester_id)
|
||||
if not semester:
|
||||
return {"success": False, "message": "学期不存在"}
|
||||
|
||||
archives = await SemesterArchiveModel.get_by_semester(semester_id)
|
||||
|
||||
total = len(archives)
|
||||
offset = (page - 1) * page_size
|
||||
paged_archives = archives[offset:offset + page_size]
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"data": {
|
||||
"semester": {
|
||||
"semester_id": semester['semester_id'],
|
||||
"semester_name": semester['semester_name'],
|
||||
"start_date": semester.get('start_date'),
|
||||
"end_date": semester.get('end_date'),
|
||||
"is_archived": bool(semester['is_archived'])
|
||||
},
|
||||
"archives": paged_archives,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取归档数据失败: {e}")
|
||||
return {"success": False, "message": f"获取归档数据失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def reset_student_points(initial_points: int = None) -> Dict[str, Any]:
|
||||
"""重置所有学生的操行分为初始分"""
|
||||
if initial_points is None:
|
||||
initial_points = settings.STUDENT_INITIAL_POINTS
|
||||
|
||||
try:
|
||||
from utils.database import execute_update
|
||||
sql = "UPDATE students SET total_points = %s WHERE status = 1"
|
||||
affected = await execute_update(sql, (initial_points,))
|
||||
|
||||
logger.info(f"已重置 {affected} 名学生的操行分为 {initial_points}")
|
||||
return {
|
||||
"success": True,
|
||||
"message": f"已重置 {affected} 名学生的操行分为 {initial_points} 分",
|
||||
"affected": affected
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"重置学生分数失败: {e}")
|
||||
return {"success": False, "message": f"重置学生分数失败: {str(e)}"}
|
||||
|
||||
@staticmethod
|
||||
async def get_active_semester() -> Dict[str, Any]:
|
||||
"""获取当前活跃学期"""
|
||||
try:
|
||||
active = await SemesterModel.get_active()
|
||||
if active:
|
||||
return {
|
||||
"success": True,
|
||||
"semester": active
|
||||
}
|
||||
else:
|
||||
return {
|
||||
"success": True,
|
||||
"semester": None,
|
||||
"message": "当前没有活跃学期"
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取活跃学期失败: {e}")
|
||||
return {"success": False, "message": f"获取活跃学期失败: {str(e)}"}
|
||||
@@ -123,9 +123,11 @@ class StudentService:
|
||||
) -> Dict[str, Any]:
|
||||
"""获取排行榜(单班级系统)"""
|
||||
ranking = await StudentModel.get_ranking(limit=limit)
|
||||
total_students = await StudentModel.get_total_count()
|
||||
|
||||
return {
|
||||
"ranking": ranking
|
||||
"ranking": ranking,
|
||||
"total_students": total_students
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
|
||||
@@ -44,6 +44,9 @@ DEDUCTION_HOMEWORK_LATE=1
|
||||
# 作业-每次加减分上限(绝对值)
|
||||
HOMEWORK_MAX_POINTS=3
|
||||
|
||||
# 学习委员单次加减分上限(绝对值)
|
||||
STUDY_COMMISSIONER_MAX_POINTS=5
|
||||
|
||||
# 考勤-缺勤扣分
|
||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
||||
# 考勤-迟到扣分
|
||||
|
||||
@@ -20,7 +20,7 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
$page_title = '操行分管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if (!in_array($role, ['班主任', '班长', '劳动委员', '志愿委员'])) {
|
||||
if (!in_array($role, ['班主任', '班长', '学习委员', '劳动委员', '志愿委员'])) {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
@@ -209,6 +209,7 @@ loadStudents();
|
||||
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
||||
<small><?php
|
||||
if ($role === '班长') echo '班长单次±5分以内';
|
||||
elseif ($role === '学习委员') echo '学习委员单次±5分以内';
|
||||
elseif ($role === '劳动委员') echo '劳动委员仅限±1分';
|
||||
elseif ($role === '志愿委员') echo '志愿委员仅限加分';
|
||||
else echo '班主任无限制';
|
||||
|
||||
@@ -37,7 +37,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th></tr>
|
||||
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th><th>前%</th></tr>
|
||||
</thead>
|
||||
<tbody id="rankingList"></tbody>
|
||||
</table>
|
||||
@@ -69,17 +69,25 @@ async function loadDashboard() {
|
||||
|
||||
const rankingRes = await apiGet('/api/student/ranking', { limit: 100 });
|
||||
if (rankingRes && rankingRes.success) {
|
||||
const totalStudents = rankingRes.data.total_students || 0;
|
||||
let html = '';
|
||||
rankingRes.data.ranking.forEach((student, index) => {
|
||||
const rank = index + 1;
|
||||
let percentile = '--';
|
||||
if (totalStudents > 0) {
|
||||
const pct = Math.floor(rank / totalStudents * 100);
|
||||
percentile = (pct === 0 ? 1 : pct) + '%';
|
||||
}
|
||||
html += `<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${rank}</td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td>前${percentile}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (rankingRes.data.ranking.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无数据</td></tr>';
|
||||
}
|
||||
document.getElementById('rankingList').innerHTML = html;
|
||||
}
|
||||
|
||||
307
frontend/admin/semesters.php
Normal file
307
frontend/admin/semesters.php
Normal file
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 学期管理页面
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
if ($role !== '班主任') {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '学期管理';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<?php include __DIR__ . '/../includes/nav.php'; ?>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary" onclick="showCreateSemesterModal()">创建新学期</button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>学期名称</th>
|
||||
<th>开始日期</th>
|
||||
<th>结束日期</th>
|
||||
<th>状态</th>
|
||||
<th>创建时间</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="semesterList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 创建学期模态框 -->
|
||||
<div id="createSemesterModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>创建新学期</h3>
|
||||
<button class="modal-close" onclick="closeModal('createSemesterModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitCreateSemester()">
|
||||
<div class="form-group">
|
||||
<label>学期名称 <span style="color:red;">*</span></label>
|
||||
<input type="text" id="semesterName" required placeholder="如:2025春季学期" maxlength="100">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>开始日期</label>
|
||||
<input type="date" id="semesterStartDate">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>结束日期</label>
|
||||
<input type="date" id="semesterEndDate">
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">创建并激活</button>
|
||||
<button type="button" class="btn" onclick="closeModal('createSemesterModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 归档确认模态框 -->
|
||||
<div id="archiveConfirmModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>确认归档学期</h3>
|
||||
<button class="modal-close" onclick="closeModal('archiveConfirmModal')">×</button>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<p id="archiveConfirmText" style="margin: 10px 0;"></p>
|
||||
<p style="color: #e74c3c; font-size: 14px;">注意:归档后该学期的操行分记录将不可修改或撤销,但可以查看归档数据。</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" onclick="confirmArchive()">确认归档</button>
|
||||
<button type="button" class="btn" onclick="closeModal('archiveConfirmModal')">取消</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 归档数据查看模态框 -->
|
||||
<div id="archiveDataModal" class="modal">
|
||||
<div class="modal-content" style="max-width: 700px;">
|
||||
<div class="modal-header">
|
||||
<h3 id="archiveDataTitle">归档数据</h3>
|
||||
<button class="modal-close" onclick="closeModal('archiveDataModal')">×</button>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>排名</th>
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>最终操行分</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="archiveDataList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination" id="archivePagination"></div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" onclick="closeModal('archiveDataModal')">关闭</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
var archiveSemesterId = null;
|
||||
var archivePage = 1;
|
||||
var archiveTotalPages = 1;
|
||||
|
||||
async function loadSemesters() {
|
||||
const res = await apiGet('/api/semester/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
const semesters = res.data || [];
|
||||
semesters.forEach(sem => {
|
||||
let statusText = '';
|
||||
let statusClass = '';
|
||||
if (sem.is_archived) {
|
||||
statusText = '已归档';
|
||||
statusClass = 'status-badge status-not_submitted';
|
||||
} else if (sem.is_active) {
|
||||
statusText = '当前学期';
|
||||
statusClass = 'status-badge status-submitted';
|
||||
} else {
|
||||
statusText = '未激活';
|
||||
statusClass = 'status-badge status-late';
|
||||
}
|
||||
|
||||
let actions = '';
|
||||
if (!sem.is_archived) {
|
||||
if (!sem.is_active) {
|
||||
actions += `<button class="btn btn-sm btn-primary" onclick="activateSemester(${sem.semester_id})">激活</button> `;
|
||||
}
|
||||
actions += `<button class="btn btn-sm btn-warning" onclick="showArchiveConfirm(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">归档</button> `;
|
||||
}
|
||||
if (sem.is_archived) {
|
||||
actions += `<button class="btn btn-sm btn-secondary" onclick="viewArchiveData(${sem.semester_id}, '${escapeHtml(sem.semester_name)}')">查看归档</button>`;
|
||||
}
|
||||
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sem.semester_name)}</td>
|
||||
<td>${formatDate(sem.start_date)}</td>
|
||||
<td>${formatDate(sem.end_date)}</td>
|
||||
<td><span class="${statusClass}">${statusText}</span></td>
|
||||
<td>${formatDateTime(sem.created_at)}</td>
|
||||
<td>${actions}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (semesters.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学期,请点击上方按钮创建新学期</td></tr>';
|
||||
}
|
||||
document.getElementById('semesterList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function showCreateSemesterModal() {
|
||||
document.getElementById('semesterName').value = '';
|
||||
document.getElementById('semesterStartDate').value = '';
|
||||
document.getElementById('semesterEndDate').value = '';
|
||||
document.getElementById('createSemesterModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function submitCreateSemester() {
|
||||
const name = document.getElementById('semesterName').value.trim();
|
||||
const startDate = document.getElementById('semesterStartDate').value;
|
||||
const endDate = document.getElementById('semesterEndDate').value;
|
||||
|
||||
if (!name) {
|
||||
showToast('请输入学期名称', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/semester/create', {
|
||||
semester_name: name,
|
||||
start_date: startDate || null,
|
||||
end_date: endDate || null
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学期创建成功并已激活');
|
||||
closeModal('createSemesterModal');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '创建失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function activateSemester(semesterId) {
|
||||
if (!confirm('确认将此学期设为当前活跃学期?其他学期将被设为非活跃。')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPut(`/api/semester/activate/${semesterId}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '已设为当前学期');
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function showArchiveConfirm(semesterId, semesterName) {
|
||||
archiveSemesterId = semesterId;
|
||||
document.getElementById('archiveConfirmText').innerHTML =
|
||||
`确定要归档学期 "<strong>${semesterName}</strong>" 吗?<br>归档后将保存所有学生的当前操行分快照,该学期数据将变为只读。`;
|
||||
document.getElementById('archiveConfirmModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
async function confirmArchive() {
|
||||
if (!archiveSemesterId) return;
|
||||
|
||||
const res = await apiPost(`/api/semester/archive/${archiveSemesterId}`);
|
||||
if (res && res.success) {
|
||||
showToast(res.message || '归档成功');
|
||||
closeModal('archiveConfirmModal');
|
||||
archiveSemesterId = null;
|
||||
loadSemesters();
|
||||
} else {
|
||||
showToast(res?.message || '归档失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async function viewArchiveData(semesterId, semesterName, page) {
|
||||
page = page || 1;
|
||||
archivePage = page;
|
||||
document.getElementById('archiveDataTitle').textContent = `归档数据 - ${semesterName}`;
|
||||
|
||||
const res = await apiGet(`/api/semester/archive/${semesterId}/records`, {
|
||||
page: page, page_size: 50
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
const data = res.data || {};
|
||||
const archives = data.archives || [];
|
||||
let html = '';
|
||||
archives.forEach(a => {
|
||||
html += `<tr>
|
||||
<td>${a.rank_position || '-'}</td>
|
||||
<td>${escapeHtml(a.student_no)}</td>
|
||||
<td>${escapeHtml(a.student_name)}</td>
|
||||
<td>${a.final_points}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (archives.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无归档数据</td></tr>';
|
||||
}
|
||||
document.getElementById('archiveDataList').innerHTML = html;
|
||||
|
||||
archiveTotalPages = data.total_pages || 1;
|
||||
renderArchivePagination(semesterId, semesterName);
|
||||
document.getElementById('archiveDataModal').style.display = 'flex';
|
||||
} else {
|
||||
showToast(res?.message || '获取归档数据失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
function renderArchivePagination(semesterId, semesterName) {
|
||||
const container = document.getElementById('archivePagination');
|
||||
if (!container) return;
|
||||
if (archiveTotalPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (let i = 1; i <= archiveTotalPages; i++) {
|
||||
if (i === archivePage) {
|
||||
html += `<span class="active">${i}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" onclick="viewArchiveData(${semesterId}, '${escapeHtml(semesterName)}', ${i}); return false;">${i}</a>`;
|
||||
}
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) modal.style.display = 'none';
|
||||
}
|
||||
|
||||
loadSemesters();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -16,6 +16,9 @@
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/admins.php" class="nav-item<?php echo $current_page === 'admins' ? ' active' : ''; ?>">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/semesters.php" class="nav-item<?php echo $current_page === 'semesters' ? ' active' : ''; ?>">学期管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item<?php echo $current_page === 'history' ? ' active' : ''; ?>">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item<?php echo $current_page === 'password' ? ' active' : ''; ?>">修改密码</a>
|
||||
</div>
|
||||
|
||||
@@ -73,14 +73,17 @@ async function loadDashboard() {
|
||||
document.getElementById('totalPoints').textContent = res.data.total_points;
|
||||
}
|
||||
|
||||
// 加载排名信息
|
||||
// 加载排名信息
|
||||
const rankRes = await apiGet('/api/parent/child/ranking');
|
||||
if (rankRes && rankRes.success) {
|
||||
const rank = rankRes.data.rank;
|
||||
const total = rankRes.data.total_students;
|
||||
document.getElementById('studentRank').textContent = rank ? `第${rank}名` : '--';
|
||||
if (rank) {
|
||||
document.getElementById('studentRank').textContent = `第${rank}名`;
|
||||
} else {
|
||||
document.getElementById('studentRank').textContent = '--';
|
||||
}
|
||||
}
|
||||
|
||||
// 显示初始分提示
|
||||
const initialPoints = window.STUDENT_INITIAL_POINTS || 60;
|
||||
document.getElementById('initialPointsHint').textContent = `初始操行分为 ${initialPoints} 分`;
|
||||
|
||||
@@ -77,6 +77,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<button class="nav-item" data-page="conduct">操行分详情</button>
|
||||
<button class="nav-item" data-page="homework">作业情况</button>
|
||||
<button class="nav-item" data-page="attendance">考勤记录</button>
|
||||
<a href="/student/semester_history.php" class="nav-item">学期记录</a>
|
||||
<button class="nav-item" data-page="password">修改密码</button>
|
||||
</div>
|
||||
|
||||
@@ -301,7 +302,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
const ranking = rankingRes.data.ranking || [];
|
||||
const rank = ranking.find(s => s.student_id === parseInt(STUDENT_ID));
|
||||
if (rank) {
|
||||
document.getElementById('studentRank').textContent = rank.rank;
|
||||
document.getElementById('studentRank').textContent = `第${rank.rank}名`;
|
||||
} else {
|
||||
document.getElementById('studentRank').textContent = '--';
|
||||
}
|
||||
|
||||
189
frontend/student/semester_history.php
Normal file
189
frontend/student/semester_history.php
Normal file
@@ -0,0 +1,189 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端学期记录页面
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
// 检查登录状态
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '学期记录';
|
||||
$student_id = $_SESSION['student_id'];
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<style>
|
||||
.nav .nav-item {
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
margin: 0 4px;
|
||||
border: none;
|
||||
background: none;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
cursor: pointer;
|
||||
text-decoration: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
}
|
||||
.nav .nav-item.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
font-weight: bold;
|
||||
}
|
||||
.semester-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
}
|
||||
.semester-card-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 12px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
.semester-name {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
.semester-date {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.semester-stats {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
gap: 12px;
|
||||
text-align: center;
|
||||
}
|
||||
.semester-stat-item {
|
||||
padding: 8px;
|
||||
}
|
||||
.semester-stat-value {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
.semester-stat-label {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
margin-top: 4px;
|
||||
}
|
||||
.empty-state {
|
||||
text-align: center;
|
||||
padding: 60px 20px;
|
||||
color: #999;
|
||||
}
|
||||
.empty-state-icon {
|
||||
font-size: 48px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.archived-badge {
|
||||
display: inline-block;
|
||||
padding: 2px 8px;
|
||||
background: #e8f5e9;
|
||||
color: #388e3c;
|
||||
border-radius: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
</style>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/student/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/student/dashboard.php" class="nav-item">操行分详情</a>
|
||||
<a href="/student/dashboard.php" class="nav-item">作业情况</a>
|
||||
<a href="/student/dashboard.php" class="nav-item">考勤记录</a>
|
||||
<a href="/student/semester_history.php" class="nav-item active">学期记录</a>
|
||||
<a href="/student/dashboard.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-title">历史学期记录</div>
|
||||
<div id="semesterRecords"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadSemesterRecords() {
|
||||
try {
|
||||
const res = await apiGet('/api/student/semester-records');
|
||||
if (res && res.success) {
|
||||
const records = res.data.records || [];
|
||||
const container = document.getElementById('semesterRecords');
|
||||
|
||||
if (records.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">📋</div>
|
||||
<p>暂无历史学期记录</p>
|
||||
<p style="font-size: 13px; margin-top: 8px;">学期归档后,您的成绩记录将显示在这里</p>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
records.forEach(record => {
|
||||
const dateRange = record.start_date && record.end_date
|
||||
? `${record.start_date} ~ ${record.end_date}`
|
||||
: '';
|
||||
html += `
|
||||
<div class="semester-card">
|
||||
<div class="semester-card-header">
|
||||
<div>
|
||||
<div class="semester-name">${escapeHtml(record.semester_name)}</div>
|
||||
${dateRange ? `<div class="semester-date">${dateRange}</div>` : ''}
|
||||
</div>
|
||||
<span class="archived-badge">已归档</span>
|
||||
</div>
|
||||
<div class="semester-stats">
|
||||
<div class="semester-stat-item">
|
||||
<div class="semester-stat-value">${record.final_points}</div>
|
||||
<div class="semester-stat-label">最终操行分</div>
|
||||
</div>
|
||||
<div class="semester-stat-item">
|
||||
<div class="semester-stat-value">${record.rank_position || '--'}</div>
|
||||
<div class="semester-stat-label">班级排名</div>
|
||||
</div>
|
||||
<div class="semester-stat-item">
|
||||
<div class="semester-stat-value">${record.total_students || '--'}</div>
|
||||
<div class="semester-stat-label">班级总人数</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
container.innerHTML = html;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载学期记录失败:', error);
|
||||
document.getElementById('semesterRecords').innerHTML = `
|
||||
<div class="empty-state">
|
||||
<div class="empty-state-icon">⚠️</div>
|
||||
<p>加载失败,请稍后重试</p>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
}
|
||||
|
||||
loadSemesterRecords();
|
||||
</script>
|
||||
<script src="/assets/js/student.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
137
sql/init.sql
137
sql/init.sql
@@ -19,21 +19,19 @@ USE `classmanagerdb`;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
DROP TABLE IF EXISTS `conduct_records`;
|
||||
DROP TABLE IF EXISTS `homework_submissions`;
|
||||
DROP TABLE IF EXISTS `attendance_records`;
|
||||
DROP TABLE IF EXISTS `admin_roles`;
|
||||
DROP TABLE IF EXISTS `assignments`;
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
DROP TABLE IF EXISTS `students`;
|
||||
DROP TABLE IF EXISTS `subjects`;
|
||||
DROP TABLE IF EXISTS `operation_logs`;
|
||||
DROP TABLE IF EXISTS `login_logs`;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
-- 学期表
|
||||
CREATE TABLE IF NOT EXISTS `semesters` (
|
||||
`semester_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`semester_name` VARCHAR(100) NOT NULL COMMENT '学期名称,如 2025春季学期',
|
||||
`start_date` DATE DEFAULT NULL COMMENT '学期开始日期',
|
||||
`end_date` DATE DEFAULT NULL COMMENT '学期结束日期',
|
||||
`is_active` TINYINT DEFAULT 0 COMMENT '是否为当前活跃学期',
|
||||
`is_archived` TINYINT DEFAULT 0 COMMENT '是否已归档',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 科目表(仅保留语数英)
|
||||
CREATE TABLE `subjects` (
|
||||
CREATE TABLE IF NOT EXISTS `subjects` (
|
||||
`subject_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID',
|
||||
`subject_name` VARCHAR(50) NOT NULL COMMENT '科目名称',
|
||||
`subject_code` VARCHAR(20) DEFAULT NULL COMMENT '科目代码',
|
||||
@@ -44,7 +42,7 @@ CREATE TABLE `subjects` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 学生表(无班级ID)
|
||||
CREATE TABLE `students` (
|
||||
CREATE TABLE IF NOT EXISTS `students` (
|
||||
`student_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`student_no` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`name` VARCHAR(50) NOT NULL,
|
||||
@@ -56,7 +54,7 @@ CREATE TABLE `students` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 用户表
|
||||
CREATE TABLE `users` (
|
||||
CREATE TABLE IF NOT EXISTS `users` (
|
||||
`user_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`username` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`password_hash` VARCHAR(64) NOT NULL,
|
||||
@@ -72,7 +70,7 @@ CREATE TABLE `users` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 管理员角色表(无班级ID)
|
||||
CREATE TABLE `admin_roles` (
|
||||
CREATE TABLE IF NOT EXISTS `admin_roles` (
|
||||
`admin_role_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`role_type` ENUM('班主任', '班长', '学习委员', '考勤委员', '劳动委员', '志愿委员') NOT NULL,
|
||||
@@ -84,7 +82,7 @@ CREATE TABLE `admin_roles` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 操行分记录表
|
||||
CREATE TABLE `conduct_records` (
|
||||
CREATE TABLE IF NOT EXISTS `conduct_records` (
|
||||
`record_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`student_id` INT NOT NULL,
|
||||
`points_change` INT NOT NULL,
|
||||
@@ -96,14 +94,16 @@ CREATE TABLE `conduct_records` (
|
||||
`is_revoked` TINYINT DEFAULT 0,
|
||||
`revoked_by` INT DEFAULT NULL,
|
||||
`revoked_at` DATETIME DEFAULT NULL,
|
||||
`semester_id` INT DEFAULT NULL COMMENT '所属学期ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
|
||||
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`)
|
||||
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`),
|
||||
FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`) ON DELETE SET NULL
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 作业表(无班级ID)
|
||||
CREATE TABLE `assignments` (
|
||||
CREATE TABLE IF NOT EXISTS `assignments` (
|
||||
`assignment_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`subject_id` INT NOT NULL,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
@@ -116,7 +116,7 @@ CREATE TABLE `assignments` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 作业提交记录表
|
||||
CREATE TABLE `homework_submissions` (
|
||||
CREATE TABLE IF NOT EXISTS `homework_submissions` (
|
||||
`submission_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`assignment_id` INT NOT NULL,
|
||||
`student_id` INT NOT NULL,
|
||||
@@ -134,7 +134,7 @@ CREATE TABLE `homework_submissions` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 考勤记录表
|
||||
CREATE TABLE `attendance_records` (
|
||||
CREATE TABLE IF NOT EXISTS `attendance_records` (
|
||||
`attendance_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`student_id` INT NOT NULL,
|
||||
`date` DATE NOT NULL,
|
||||
@@ -143,15 +143,17 @@ CREATE TABLE `attendance_records` (
|
||||
`recorder_id` INT NOT NULL,
|
||||
`deduction_applied` TINYINT DEFAULT 0,
|
||||
`deduction_record_id` BIGINT DEFAULT NULL,
|
||||
`semester_id` INT DEFAULT NULL COMMENT '所属学期ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
|
||||
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
|
||||
FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`) ON DELETE SET NULL,
|
||||
UNIQUE KEY `uk_student_date` (`student_id`, `date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 操作日志表
|
||||
CREATE TABLE `operation_logs` (
|
||||
CREATE TABLE IF NOT EXISTS `operation_logs` (
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`operator_id` INT NOT NULL,
|
||||
`operator_name` VARCHAR(50) DEFAULT NULL,
|
||||
@@ -165,7 +167,7 @@ CREATE TABLE `operation_logs` (
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 登录日志表
|
||||
CREATE TABLE `login_logs` (
|
||||
CREATE TABLE IF NOT EXISTS `login_logs` (
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`username` VARCHAR(50) NOT NULL,
|
||||
`login_result` TINYINT NOT NULL,
|
||||
@@ -175,10 +177,93 @@ CREATE TABLE `login_logs` (
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- 插入初始科目(仅语数英)
|
||||
INSERT INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VALUES
|
||||
-- 学期归档快照表
|
||||
CREATE TABLE IF NOT EXISTS `semester_archives` (
|
||||
`archive_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`semester_id` INT NOT NULL,
|
||||
`student_id` INT NOT NULL,
|
||||
`student_no` VARCHAR(20) NOT NULL,
|
||||
`student_name` VARCHAR(50) NOT NULL,
|
||||
`final_points` INT NOT NULL COMMENT '学期最终操行分',
|
||||
`rank_position` INT DEFAULT NULL COMMENT '排名',
|
||||
`total_students` INT DEFAULT NULL COMMENT '班级总人数',
|
||||
`archived_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`),
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
SET FOREIGN_KEY_CHECKS = 1;
|
||||
|
||||
-- ===========================================
|
||||
-- 迁移语句:为现有表新增字段(仅在字段不存在时添加)
|
||||
-- ===========================================
|
||||
|
||||
-- conduct_records 表:添加 semester_id 字段(如不存在)
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||
AND TABLE_NAME = 'conduct_records'
|
||||
AND COLUMN_NAME = 'semester_id'
|
||||
);
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `conduct_records` ADD COLUMN `semester_id` INT DEFAULT NULL COMMENT ''所属学期ID'' AFTER `revoked_at`',
|
||||
'SELECT ''conduct_records.semester_id already exists'' AS message'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为新增的 semester_id 添加外键(如不存在)
|
||||
SET @fk_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||
AND TABLE_NAME = 'conduct_records'
|
||||
AND COLUMN_NAME = 'semester_id'
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
);
|
||||
SET @sql = IF(@fk_exists = 0,
|
||||
'ALTER TABLE `conduct_records` ADD FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`) ON DELETE SET NULL',
|
||||
'SELECT ''conduct_records semester_id FK already exists'' AS message'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- attendance_records 表:添加 semester_id 字段(如不存在)
|
||||
SET @column_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS
|
||||
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||
AND TABLE_NAME = 'attendance_records'
|
||||
AND COLUMN_NAME = 'semester_id'
|
||||
);
|
||||
SET @sql = IF(@column_exists = 0,
|
||||
'ALTER TABLE `attendance_records` ADD COLUMN `semester_id` INT DEFAULT NULL COMMENT ''所属学期ID'' AFTER `deduction_record_id`',
|
||||
'SELECT ''attendance_records.semester_id already exists'' AS message'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 为新增的 semester_id 添加外键(如不存在)
|
||||
SET @fk_exists = (
|
||||
SELECT COUNT(*) FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE
|
||||
WHERE TABLE_SCHEMA = 'classmanagerdb'
|
||||
AND TABLE_NAME = 'attendance_records'
|
||||
AND COLUMN_NAME = 'semester_id'
|
||||
AND REFERENCED_TABLE_NAME IS NOT NULL
|
||||
);
|
||||
SET @sql = IF(@fk_exists = 0,
|
||||
'ALTER TABLE `attendance_records` ADD FOREIGN KEY (`semester_id`) REFERENCES `semesters`(`semester_id`) ON DELETE SET NULL',
|
||||
'SELECT ''attendance_records semester_id FK already exists'' AS message'
|
||||
);
|
||||
PREPARE stmt FROM @sql;
|
||||
EXECUTE stmt;
|
||||
DEALLOCATE PREPARE stmt;
|
||||
|
||||
-- 插入初始科目(仅语数英,如不存在)
|
||||
INSERT IGNORE INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VALUES
|
||||
('语文', 'CHI', 1),
|
||||
('数学', 'MATH', 2),
|
||||
('英语', 'ENG', 3);
|
||||
|
||||
SELECT '数据库初始化完成!' AS message;
|
||||
SELECT '数据库初始化/迁移完成!' AS message;
|
||||
|
||||
Reference in New Issue
Block a user