v0.2测试

This commit is contained in:
2026-04-10 14:18:07 +08:00
parent 6102774585
commit 9d89e62b63
19 changed files with 461 additions and 995 deletions

View File

@@ -57,7 +57,7 @@
|------|-----------|-----------|---------|-------------| |------|-----------|-----------|---------|-------------|
| 班主任 | 全班 | 无限制 | 可撤销任何记录 | 全班所有记录 | | 班主任 | 全班 | 无限制 | 可撤销任何记录 | 全班所有记录 |
| 班长 | 全班 | ±5分 | 可撤销任何记录 | 全班所有记录 | | 班长 | 全班 | ±5分 | 可撤销任何记录 | 全班所有记录 |
| 科代表 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 | | 学习委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 考勤委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 | | 考勤委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 劳动委员 | 全班 | 仅±1分卫生值日 | 不可撤销 | 仅自己提交的 | | 劳动委员 | 全班 | 仅±1分卫生值日 | 不可撤销 | 仅自己提交的 |
| 学生 | 自己 | 无 | 无 | 自己的历史 | | 学生 | 自己 | 无 | 无 | 自己的历史 |

View File

@@ -1,164 +1,111 @@
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================
# =========================================== # ===========================================
# FastAPI 应用配置 # FastAPI 应用配置
# =========================================== # ===========================================
# 应用名称 - 显示在API文档和日志中 # 应用名称
APP_NAME=班级操行分管理系统 APP_NAME=班级操行分管理系统
# 运行环境 - production(生产) / development(开发) / testing(测试) # 运行环境 - production / development / testing
# 生产环境会自动启用HTTPS重定向
APP_ENV=production APP_ENV=production
# 调试模式 - true开启详细错误信息生产环境必须为false # 调试模式 - true开启详细错误信息生产环境必须为false
DEBUG=False DEBUG=False
# 应用密钥 - 用于会话加密,必须32位以上随机字符串 # 应用密钥 - 必须32位以上随机字符串
# 生成方法: openssl rand -hex 32
SECRET_KEY=your-super-secret-key-min-32-characters-long SECRET_KEY=your-super-secret-key-min-32-characters-long
# API版本号 - 用于路由前缀,如 /api/v1 # API版本号
API_VERSION=v1 API_VERSION=v1
# =========================================== # ===========================================
# MySQL 数据库配置 # MySQL 数据库配置
# =========================================== # ===========================================
# 数据库主机地址 - 本地用127.0.0.1远程用实际IP
DB_HOST=127.0.0.1 DB_HOST=127.0.0.1
# 数据库端口 - MySQL默认3306
DB_PORT=3306 DB_PORT=3306
# 数据库用户名 - 建议创建专用账户不要用root
DB_USER=class_admin DB_USER=class_admin
# 数据库密码 - 使用强密码,包含大小写数字特殊字符
DB_PASSWORD=your-strong-db-password DB_PASSWORD=your-strong-db-password
# 数据库名称 - 固定为 classmanagerdb
DB_NAME=classmanagerdb DB_NAME=classmanagerdb
# 连接池大小 - 同时保持的数据库连接数,根据并发量调整
DB_POOL_SIZE=10 DB_POOL_SIZE=10
# 最大溢出连接 - 连接池满后最多额外创建的连接数
DB_MAX_OVERFLOW=20 DB_MAX_OVERFLOW=20
# =========================================== # ===========================================
# Redis 缓存配置 # Redis 缓存配置
# =========================================== # ===========================================
# Redis主机地址
REDIS_HOST=127.0.0.1 REDIS_HOST=127.0.0.1
# Redis端口 - 默认6379
REDIS_PORT=6379 REDIS_PORT=6379
# Redis密码 - 建议设置,防止未授权访问
REDIS_PASSWORD=your-redis-password REDIS_PASSWORD=your-redis-password
# Redis数据库编号 - 0-15建议使用独立数据库避免冲突
REDIS_DB=0 REDIS_DB=0
# 最大连接数 - 根据并发量调整建议50-200
REDIS_MAX_CONNECTIONS=50 REDIS_MAX_CONNECTIONS=50
# =========================================== # ===========================================
# JWT 认证配置 # JWT 认证配置
# =========================================== # ===========================================
# JWT密钥 - 用于签名Token必须32位以上随机字符串
# 生成方法: openssl rand -hex 32
JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars
# JWT签名算法 - HS256对称加密
JWT_ALGORITHM=HS256 JWT_ALGORITHM=HS256
# Token过期时间分钟- 30分钟无操作需重新登录
JWT_EXPIRE_MINUTES=30 JWT_EXPIRE_MINUTES=30
# =========================================== # ===========================================
# 密码加密配置 # 密码加密配置
# =========================================== # ===========================================
# 密码盐值 - 固定字符串用于SHA1+MD5双重加密
# 生成后不可更改,否则所有密码失效
# 生成方法: openssl rand -hex 16
PASSWORD_SALT=your-fixed-salt-string-for-password-hash PASSWORD_SALT=your-fixed-salt-string-for-password-hash
# =========================================== # ===========================================
# 调试入口配置 # 调试入口配置
# =========================================== # ===========================================
# 调试入口路径 - 随机字符串,用于添加第一批管理员
# 建议使用32位随机字符串只有开发人员知道此路径
# 添加完管理员后建议注释掉debug路由
# 生成方法: openssl rand -hex 16 | sed 's/\(..\)/\1/g' | cut -c1-32
DEBUG_PATH=/a7k9x2m4q8w1e3r5t6y7u8i9o0p1z2x3 DEBUG_PATH=/a7k9x2m4q8w1e3r5t6y7u8i9o0p1z2x3
# =========================================== # ===========================================
# 扣分规则配置 # 扣分规则配置
# =========================================== # ===========================================
# 注意:这些规则仅用于建议,实际操作时可选择是否应用扣分
# 作业未提交扣分 - 科代表标记未提交时建议扣分数值
DEDUCTION_HOMEWORK_NOT_SUBMIT=2 DEDUCTION_HOMEWORK_NOT_SUBMIT=2
# 作业迟交扣分 - 迟交作业建议扣分数值
DEDUCTION_HOMEWORK_LATE=1 DEDUCTION_HOMEWORK_LATE=1
# 缺勤扣分 - 缺勤建议扣分数值
DEDUCTION_ATTENDANCE_ABSENT=5 DEDUCTION_ATTENDANCE_ABSENT=5
# 迟到扣分 - 迟到建议扣分数值
DEDUCTION_ATTENDANCE_LATE=2 DEDUCTION_ATTENDANCE_LATE=2
# 请假扣分 - 请假建议扣分数值
DEDUCTION_ATTENDANCE_LEAVE=1 DEDUCTION_ATTENDANCE_LEAVE=1
# =========================================== # ===========================================
# 劳动委员固定分值配置 # 劳动委员固定分值配置
# =========================================== # ===========================================
# 劳动委员加分值 - 固定为1分
LABOR_POINTS_ADD=1 LABOR_POINTS_ADD=1
# 劳动委员扣分值 - 固定为-1分
LABOR_POINTS_SUBTRACT=-1 LABOR_POINTS_SUBTRACT=-1
# =========================================== # ===========================================
# 班长加减分限制配置 # 班长加减分限制配置
# =========================================== # ===========================================
# 班长最大单次加分值
MONITOR_MAX_ADD=5 MONITOR_MAX_ADD=5
# 班长最大单次扣分值(负数)
MONITOR_MAX_SUBTRACT=-5 MONITOR_MAX_SUBTRACT=-5
# =========================================== # ===========================================
# 日志配置 # 日志配置
# =========================================== # ===========================================
# 日志级别 - DEBUG/INFO/WARNING/ERROR
# 生产环境建议INFO开发环境可用DEBUG
LOG_LEVEL=INFO LOG_LEVEL=INFO
# 单日志文件最大大小(字节)- 100MB = 104857600
LOG_MAX_BYTES=104857600 LOG_MAX_BYTES=104857600
# 日志备份数量 - 保留30个历史日志文件
LOG_BACKUP_COUNT=30 LOG_BACKUP_COUNT=30
# 日志保留天数 - 操作日志保留1年访问日志保留90天
LOG_RETENTION_DAYS=365 LOG_RETENTION_DAYS=365
# =========================================== # ===========================================
# CORS 跨域配置 # CORS 跨域配置
# =========================================== # ===========================================
# 允许的跨域域名 - 多个域名用英文逗号分隔,不要有空格 # 允许的跨域域名 - 多个域名用英文逗号分隔
# 生产环境必须指定具体域名,不能用 * # 示例: https://example.com,https://api.example.com
CORS_ORIGINS=https://your-frontend-domain.com,http://localhost:8080 CORS_ORIGINS=https://your-frontend-domain.com,https://your-api-domain.com
# =========================================== # ===========================================
# 上传文件配置 # 上传文件配置
# =========================================== # ===========================================
# 最大上传文件大小(字节)- 5MB = 5242880
MAX_UPLOAD_SIZE=5242880 MAX_UPLOAD_SIZE=5242880
# 允许的文件扩展名 - 多个用英文逗号分隔
ALLOWED_EXTENSIONS=json ALLOWED_EXTENSIONS=json
# =========================================== # ===========================================
# 学生初始配置 # 学生初始配置
# =========================================== # ===========================================
# 学生初始操行分 - 新生导入时的默认分数默认60分
STUDENT_INITIAL_POINTS=60 STUDENT_INITIAL_POINTS=60

View File

@@ -1,5 +1,5 @@
# =========================================== # ===========================================
# 班级操行分管理系统 - 后端服务 # 班级操行分管理系统 - 配置管理
# #
# 开发者: Canglan # 开发者: Canglan
# 联系方式: admin@sea-studio.top # 联系方式: admin@sea-studio.top
@@ -13,21 +13,15 @@ import os
from dotenv import load_dotenv from dotenv import load_dotenv
from typing import List from typing import List
# 加载环境变量
load_dotenv() load_dotenv()
class Settings: class Settings:
"""应用配置类"""
# ========== 应用配置 ==========
APP_NAME: str = os.getenv("APP_NAME", "班级操行分管理系统") APP_NAME: str = os.getenv("APP_NAME", "班级操行分管理系统")
APP_ENV: str = os.getenv("APP_ENV", "production") APP_ENV: str = os.getenv("APP_ENV", "production")
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true" DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
SECRET_KEY: str = os.getenv("SECRET_KEY", "") SECRET_KEY: str = os.getenv("SECRET_KEY", "")
API_VERSION: str = os.getenv("API_VERSION", "v1") API_VERSION: str = os.getenv("API_VERSION", "v1")
# ========== 数据库配置 ==========
DB_HOST: str = os.getenv("DB_HOST", "127.0.0.1") DB_HOST: str = os.getenv("DB_HOST", "127.0.0.1")
DB_PORT: int = int(os.getenv("DB_PORT", "3306")) DB_PORT: int = int(os.getenv("DB_PORT", "3306"))
DB_USER: str = os.getenv("DB_USER", "root") DB_USER: str = os.getenv("DB_USER", "root")
@@ -36,7 +30,6 @@ class Settings:
DB_POOL_SIZE: int = int(os.getenv("DB_POOL_SIZE", "10")) DB_POOL_SIZE: int = int(os.getenv("DB_POOL_SIZE", "10"))
DB_MAX_OVERFLOW: int = int(os.getenv("DB_MAX_OVERFLOW", "20")) DB_MAX_OVERFLOW: int = int(os.getenv("DB_MAX_OVERFLOW", "20"))
# ========== Redis配置 ==========
REDIS_HOST: str = os.getenv("REDIS_HOST", "127.0.0.1") REDIS_HOST: str = os.getenv("REDIS_HOST", "127.0.0.1")
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379")) REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "") REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
@@ -45,63 +38,47 @@ class Settings:
@property @property
def REDIS_URL(self) -> str: def REDIS_URL(self) -> str:
"""获取Redis连接URL"""
if self.REDIS_PASSWORD: if self.REDIS_PASSWORD:
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}" return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
# ========== JWT配置 ==========
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "") JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "")
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256") JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30")) JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30"))
# ========== 密码加密 ==========
PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "") PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "")
# ========== 调试入口 ==========
DEBUG_PATH: str = os.getenv("DEBUG_PATH", "/debug_add_admin") DEBUG_PATH: str = os.getenv("DEBUG_PATH", "/debug_add_admin")
# ========== 扣分规则 ==========
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2")) DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1")) DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5")) DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5"))
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2")) DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2"))
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1")) DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1"))
# ========== 劳动委员固定分值 ==========
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1")) LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1"))
LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1")) LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1"))
# ========== 班长加减分限制 ==========
MONITOR_MAX_ADD: int = int(os.getenv("MONITOR_MAX_ADD", "5")) MONITOR_MAX_ADD: int = int(os.getenv("MONITOR_MAX_ADD", "5"))
MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5")) MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5"))
# ========== 日志配置 ==========
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO") LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
LOG_MAX_BYTES: int = int(os.getenv("LOG_MAX_BYTES", "104857600")) LOG_MAX_BYTES: int = int(os.getenv("LOG_MAX_BYTES", "104857600"))
LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "30")) LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "30"))
LOG_RETENTION_DAYS: int = int(os.getenv("LOG_RETENTION_DAYS", "365")) LOG_RETENTION_DAYS: int = int(os.getenv("LOG_RETENTION_DAYS", "365"))
# ========== CORS配置 ========== @property
CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "http://localhost:8080").split(",") def CORS_ORIGINS(self) -> List[str]:
origins = os.getenv("CORS_ORIGINS", "")
return [origin.strip() for origin in origins.split(",") if origin.strip()]
# ========== 上传配置 ==========
MAX_UPLOAD_SIZE: int = int(os.getenv("MAX_UPLOAD_SIZE", "5242880")) MAX_UPLOAD_SIZE: int = int(os.getenv("MAX_UPLOAD_SIZE", "5242880"))
ALLOWED_EXTENSIONS: set = set(os.getenv("ALLOWED_EXTENSIONS", "json").split(",")) ALLOWED_EXTENSIONS: set = set(os.getenv("ALLOWED_EXTENSIONS", "json").split(","))
# ========== 学生初始配置 ==========
STUDENT_INITIAL_POINTS: int = int(os.getenv("STUDENT_INITIAL_POINTS", "60")) STUDENT_INITIAL_POINTS: int = int(os.getenv("STUDENT_INITIAL_POINTS", "60"))
def validate(self) -> None: def validate(self) -> None:
"""验证必要配置是否存在""" required = ["SECRET_KEY", "JWT_SECRET_KEY", "PASSWORD_SALT"]
required_configs = [ for name in required:
("SECRET_KEY", self.SECRET_KEY), if not getattr(self, name):
("JWT_SECRET_KEY", self.JWT_SECRET_KEY),
("PASSWORD_SALT", self.PASSWORD_SALT),
]
for name, value in required_configs:
if not value:
raise ValueError(f"配置 {name} 不能为空") raise ValueError(f"配置 {name} 不能为空")
settings = Settings() settings = Settings()

View File

@@ -1,11 +1,12 @@
# =========================================== # ===========================================
# 班级操行分管理系统 - FastAPI 主入口 # 班级操行分管理系统 - 主入口
# =========================================== #
# 开发者: Canglan # 开发者: Canglan
# 联系方式: admin@sea-studio.top # 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio # 版权归属: Sea Network Technology Studio
# 许可证: MIT License # 许可证: MIT License
# 版权所有: Copyright (c) 2024 Sea Network Technology Studio #
# 版权所有 © Sea Network Technology Studio
# =========================================== # ===========================================
from fastapi import FastAPI, Request from fastapi import FastAPI, Request
@@ -17,6 +18,7 @@ from config import settings
from utils.logger import setup_logger, log_access from utils.logger import setup_logger, log_access
from utils.database import init_db_pool, close_db_pool from utils.database import init_db_pool, close_db_pool
from utils.redis_client import init_redis_pool, close_redis_pool from utils.redis_client import init_redis_pool, close_redis_pool
from middleware.auth_middleware import AuthMiddleware
from routes import auth, student, parent, admin, subject, debug from routes import auth, student, parent, admin, subject, debug
@@ -57,13 +59,14 @@ async def access_log_middleware(request: Request, call_next):
return response return response
# CORS中间件 # CORS中间件 - 从环境变量读取允许的域名
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=settings.CORS_ORIGINS, allow_origins=settings.CORS_ORIGINS,
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
allow_headers=["*"], allow_headers=["*"],
expose_headers=["*"],
) )
@@ -78,13 +81,11 @@ app.include_router(debug.router, tags=["调试"])
@app.get("/") @app.get("/")
async def root(): async def root():
"""根路径健康检查"""
return {"status": "ok", "message": f"{settings.APP_NAME} API 运行中"} return {"status": "ok", "message": f"{settings.APP_NAME} API 运行中"}
@app.get("/health") @app.get("/health")
async def health_check(): async def health_check():
"""健康检查接口"""
return {"status": "healthy"} return {"status": "healthy"}

View File

@@ -9,19 +9,13 @@
# 版权所有 © Sea Network Technology Studio # 版权所有 © Sea Network Technology Studio
# =========================================== # ===========================================
from fastapi import Request, HTTPException from fastapi import Request
from typing import List, Optional, Callable, Dict, Any from typing import List, Optional, Callable, Dict, Any
from functools import wraps from functools import wraps
from utils.response import forbidden_response from utils.response import forbidden_response
from utils.database import execute_one from utils.database import execute_one
from utils.logger import get_logger
logger = get_logger(__name__)
async def get_current_user(request: Request) -> Dict[str, Any]: async def get_current_user(request: Request) -> Dict[str, Any]:
"""获取当前登录用户信息"""
return { return {
"user_id": getattr(request.state, 'user_id', None), "user_id": getattr(request.state, 'user_id', None),
"username": getattr(request.state, 'username', None), "username": getattr(request.state, 'username', None),
@@ -30,184 +24,99 @@ async def get_current_user(request: Request) -> Dict[str, Any]:
"role": getattr(request.state, 'role', None) "role": getattr(request.state, 'role', None)
} }
async def get_current_user_id(request: Request) -> int: async def get_current_user_id(request: Request) -> int:
"""获取当前用户ID"""
return getattr(request.state, 'user_id', None) return getattr(request.state, 'user_id', None)
class PermissionChecker: class PermissionChecker:
"""权限检查器"""
@staticmethod @staticmethod
async def get_user_role(user_id: int) -> Optional[str]: async def get_user_role(user_id: int) -> Optional[str]:
"""获取用户的管理员角色""" sql = "SELECT role_type FROM admin_roles WHERE user_id = %s LIMIT 1"
sql = """
SELECT role_type FROM admin_roles
WHERE user_id = %s
LIMIT 1
"""
result = await execute_one(sql, (user_id,)) result = await execute_one(sql, (user_id,))
return result["role_type"] if result else None return result["role_type"] if result else None
@staticmethod
async def get_user_class_id(user_id: int) -> Optional[int]:
"""获取用户管理的班级ID"""
sql = """
SELECT class_id FROM admin_roles
WHERE user_id = %s
LIMIT 1
"""
result = await execute_one(sql, (user_id,))
return result["class_id"] if result else None
@staticmethod
async def get_user_subject_ids(user_id: int) -> List[int]:
"""获取科代表管理的科目ID列表"""
sql = """
SELECT subject_id FROM admin_roles
WHERE user_id = %s AND role_type = '科代表'
"""
results = await execute_one(sql, (user_id,))
if results:
return [r["subject_id"] for r in results] if isinstance(results, list) else [results["subject_id"]]
return []
@staticmethod @staticmethod
async def check_is_teacher(user_id: int) -> bool: async def check_is_teacher(user_id: int) -> bool:
"""检查是否为班主任"""
role = await PermissionChecker.get_user_role(user_id) role = await PermissionChecker.get_user_role(user_id)
return role == "班主任" return role == "班主任"
@staticmethod @staticmethod
async def check_is_monitor(user_id: int) -> bool: async def check_is_monitor(user_id: int) -> bool:
"""检查是否为班长"""
role = await PermissionChecker.get_user_role(user_id) role = await PermissionChecker.get_user_role(user_id)
return role == "班长" return role == "班长"
@staticmethod @staticmethod
async def check_is_subject_rep(user_id: int, subject_id: int = None) -> bool: async def check_is_study_commissioner(user_id: int) -> bool:
"""检查是否为科代表"""
role = await PermissionChecker.get_user_role(user_id) role = await PermissionChecker.get_user_role(user_id)
if role != "科代表": return role == "学习委员"
return False
if subject_id:
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
return subject_id in subject_ids
return True
@staticmethod @staticmethod
async def check_is_attendance_rep(user_id: int) -> bool: async def check_is_attendance_rep(user_id: int) -> bool:
"""检查是否为考勤委员"""
role = await PermissionChecker.get_user_role(user_id) role = await PermissionChecker.get_user_role(user_id)
return role == "考勤委员" return role == "考勤委员"
@staticmethod @staticmethod
async def check_is_labor_rep(user_id: int) -> bool: async def check_is_labor_rep(user_id: int) -> bool:
"""检查是否为劳动委员"""
role = await PermissionChecker.get_user_role(user_id) role = await PermissionChecker.get_user_role(user_id)
return role == "劳动委员" return role == "劳动委员"
@staticmethod
async def check_can_manage_subjects(user_id: int) -> bool:
role = await PermissionChecker.get_user_role(user_id)
return role in ["班主任", "学习委员"]
@staticmethod @staticmethod
async def check_can_revoke(user_id: int, record_id: int) -> bool: async def check_can_revoke(user_id: int, record_id: int) -> bool:
"""
检查是否可以撤销扣分记录
班主任:可以撤销任何记录
班长:可以撤销任何记录
其他:只能撤销自己的记录
"""
# 获取记录信息
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s" sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
record = await execute_one(sql, (record_id,)) record = await execute_one(sql, (record_id,))
if not record: if not record:
return False return False
role = await PermissionChecker.get_user_role(user_id) role = await PermissionChecker.get_user_role(user_id)
# 班主任或班长可以撤销任何记录
if role in ["班主任", "班长"]: if role in ["班主任", "班长"]:
return True return True
# 其他人只能撤销自己的记录
return record["recorder_id"] == user_id return record["recorder_id"] == user_id
@staticmethod
async def check_can_manage_student(user_id: int, student_id: int) -> bool:
"""检查是否可以管理该学生(同班级)"""
# 获取学生班级
sql = "SELECT class_id FROM students WHERE student_id = %s"
student = await execute_one(sql, (student_id,))
if not student:
return False
# 获取管理员管理的班级
admin_class_id = await PermissionChecker.get_user_class_id(user_id)
return admin_class_id == student["class_id"]
def require_auth(func: Callable): def require_auth(func: Callable):
"""需要认证的装饰器"""
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
request = kwargs.get('request') request = kwargs.get('request')
if not request or not hasattr(request, 'state') or not hasattr(request.state, 'user_id'): if not request or not hasattr(request.state, 'user_id'):
return forbidden_response("请先登录") return forbidden_response("请先登录")
return await func(*args, **kwargs) return await func(*args, **kwargs)
return wrapper return wrapper
def require_role(roles: List[str]): def require_role(roles: List[str]):
"""需要特定角色的装饰器"""
def decorator(func: Callable): def decorator(func: Callable):
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
request = kwargs.get('request') request = kwargs.get('request')
if not request or not hasattr(request, 'state'): if not request or not hasattr(request.state, 'user_id'):
return forbidden_response("请先登录") return forbidden_response("请先登录")
user_id = request.state.user_id user_id = request.state.user_id
user_role = await PermissionChecker.get_user_role(user_id) user_role = await PermissionChecker.get_user_role(user_id)
if user_role not in roles: if user_role not in roles:
return forbidden_response(f"需要{','.join(roles)}权限") return forbidden_response(f"需要{','.join(roles)}权限")
return await func(*args, **kwargs) return await func(*args, **kwargs)
return wrapper return wrapper
return decorator return decorator
def require_teacher(func: Callable): def require_teacher(func: Callable):
"""需要班主任权限的装饰器"""
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
request = kwargs.get('request') request = kwargs.get('request')
if not request or not hasattr(request, 'state'): if not request or not hasattr(request.state, 'user_id'):
return forbidden_response("请先登录") return forbidden_response("请先登录")
if not await PermissionChecker.check_is_teacher(request.state.user_id):
user_id = request.state.user_id
is_teacher = await PermissionChecker.check_is_teacher(user_id)
if not is_teacher:
return forbidden_response("需要班主任权限") return forbidden_response("需要班主任权限")
return await func(*args, **kwargs) return await func(*args, **kwargs)
return wrapper return wrapper
def require_study_commissioner(func: Callable):
def require_monitor(func: Callable):
"""需要班长权限的装饰器"""
@wraps(func) @wraps(func)
async def wrapper(*args, **kwargs): async def wrapper(*args, **kwargs):
request = kwargs.get('request') request = kwargs.get('request')
if not request or not hasattr(request, 'state'): if not request or not hasattr(request.state, 'user_id'):
return forbidden_response("请先登录") return forbidden_response("请先登录")
if not await PermissionChecker.check_is_study_commissioner(request.state.user_id):
user_id = request.state.user_id return forbidden_response("需要学习委员权限")
is_monitor = await PermissionChecker.check_is_monitor(user_id)
if not is_monitor:
return forbidden_response("需要班长权限")
return await func(*args, **kwargs) return await func(*args, **kwargs)
return wrapper return wrapper

View File

@@ -1,35 +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 ClassModel:
"""班级数据模型"""
@staticmethod
async def get_by_id(class_id: int) -> Optional[Dict[str, Any]]:
sql = "SELECT * FROM classes WHERE class_id = %s"
return await execute_one(sql, (class_id,))
@staticmethod
async def get_all() -> List[Dict[str, Any]]:
sql = "SELECT * FROM classes ORDER BY class_id"
return await execute_query(sql)
@staticmethod
async def create(class_name: str, grade: str = None, academic_year: str = None) -> int:
sql = """
INSERT INTO classes (class_name, grade, academic_year)
VALUES (%s, %s, %s)
"""
return await execute_insert(sql, (class_name, grade, academic_year))

View File

@@ -214,6 +214,9 @@ async def get_assignments(request: Request):
获取作业列表 获取作业列表
""" """
user = await get_current_user(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"]) result = await HomeworkService.get_assignments(user["user_id"])
@@ -268,10 +271,10 @@ async def create_assignment(
@router.put("/homework/submission") @router.put("/homework/submission")
async def update_submission_status(request: Request, req: UpdateHomeworkStatusRequest): async def update_submission_status(request: Request, req: UpdateHomeworkStatusRequest):
"""
更新作业提交状态(科代表)
"""
user = await get_current_user(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.update_submission_status( result = await HomeworkService.update_submission_status(
submission_id=req.submission_id, submission_id=req.submission_id,
@@ -340,10 +343,20 @@ async def add_admin(request: Request, req: AddAdminRequest):
""" """
user = await get_current_user(request) user = await get_current_user(request)
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) if not await PermissionChecker.check_is_teacher(user["user_id"]):
if not is_teacher:
return error_response(message="仅班主任可添加管理员", code=403) return error_response(message="仅班主任可添加管理员", code=403)
# 验证角色类型是否合法
if req.role_type not in ["班长", "学习委员", "考勤委员", "劳动委员"]:
return error_response(message="无效的角色类型", code=400)
result = await AdminService.add_admin(
username=req.username,
real_name=req.real_name,
password=req.password,
role_type=req.role_type,
operator_id=user["user_id"]
)
result = await AdminService.add_admin( result = await AdminService.add_admin(
username=req.username, username=req.username,
real_name=req.real_name, real_name=req.real_name,

View File

@@ -1,5 +1,5 @@
# =========================================== # ===========================================
# 班级操行分管理系统 - 后端服务 # 班级操行分管理系统 - 调试入口
# #
# 开发者: Canglan # 开发者: Canglan
# 联系方式: admin@sea-studio.top # 联系方式: admin@sea-studio.top
@@ -9,7 +9,7 @@
# 版权所有 © Sea Network Technology Studio # 版权所有 © Sea Network Technology Studio
# =========================================== # ===========================================
from fastapi import APIRouter, Request, HTTPException from fastapi import APIRouter, Request
from pydantic import BaseModel from pydantic import BaseModel
from typing import Optional from typing import Optional
@@ -23,12 +23,10 @@ logger = get_logger(__name__)
class AddAdminDebugRequest(BaseModel): class AddAdminDebugRequest(BaseModel):
"""添加管理员请求"""
username: str username: str
password: str password: str
real_name: str real_name: str
role_type: str # 班主任/班长/科代表/考勤委员/劳动委员 role_type: str # 班主任/班长/学习委员/考勤委员/劳动委员
class_id: int
subject_id: Optional[int] = None subject_id: Optional[int] = None
@@ -38,20 +36,22 @@ async def debug_add_admin(request: Request, req: AddAdminDebugRequest):
调试入口 - 添加第一批管理员 调试入口 - 添加第一批管理员
注意:此接口仅用于首次部署,使用后建议注释掉此路由 注意:此接口仅用于首次部署,使用后建议注释掉此路由
""" """
# 检查是否已存在管理员
from models.user import UserModel from models.user import UserModel
existing = await UserModel.get_by_username(req.username) existing = await UserModel.get_by_username(req.username)
if existing: if existing:
return error_response(message="用户名已存在") return error_response(message="用户名已存在")
# 验证角色类型
valid_roles = ["班主任", "班长", "学习委员", "考勤委员", "劳动委员"]
if req.role_type not in valid_roles:
return error_response(message=f"无效的角色类型,可选: {', '.join(valid_roles)}")
# 创建管理员账号 # 创建管理员账号
result = await AdminService.add_admin( result = await AdminService.add_admin(
username=req.username, username=req.username,
real_name=req.real_name, real_name=req.real_name,
password=req.password, password=req.password,
role_type=req.role_type, role_type=req.role_type,
class_id=req.class_id,
subject_id=req.subject_id,
operator_id=0 # 系统添加 operator_id=0 # 系统添加
) )

View File

@@ -9,97 +9,41 @@
# 版权所有 © Sea Network Technology Studio # 版权所有 © Sea Network Technology Studio
# =========================================== # ===========================================
from fastapi import APIRouter, Request, Query from fastapi import APIRouter, Request
from typing import Optional from typing import Optional
from middleware.permission import get_current_user, PermissionChecker from middleware.permission import get_current_user, PermissionChecker
from services.subject_service import SubjectService from services.subject_service import SubjectService
from schemas.subject import CreateSubjectRequest, UpdateSubjectRequest from schemas.subject import CreateSubjectRequest, UpdateSubjectRequest
from utils.response import success_response, error_response from utils.response import success_response, error_response
from utils.logger import get_logger
router = APIRouter() router = APIRouter()
logger = get_logger(__name__)
@router.get("/list") @router.get("/list")
async def get_subjects( async def get_subjects(request: Request, is_active: Optional[bool] = None):
request: Request,
is_active: Optional[bool] = None
):
"""
获取科目列表
"""
user = await get_current_user(request) user = await get_current_user(request)
result = await SubjectService.get_subjects(is_active=is_active) result = await SubjectService.get_subjects(is_active=is_active)
return success_response(data=result) return success_response(data=result)
@router.post("/create") @router.post("/create")
async def create_subject(request: Request, req: CreateSubjectRequest): async def create_subject(request: Request, req: CreateSubjectRequest):
"""
创建科目(班主任)
"""
user = await get_current_user(request) user = await get_current_user(request)
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) return error_response(message="无权限", code=403)
if not is_teacher: result = await SubjectService.create_subject(req.subject_name, req.subject_code, req.sort_order)
return error_response(message="仅班主任可创建科目", code=403) return success_response(data=result, message="科目创建成功") if result["success"] else error_response(message=result["message"])
result = await SubjectService.create_subject(
subject_name=req.subject_name,
subject_code=req.subject_code,
sort_order=req.sort_order
)
if result["success"]:
return success_response(data=result, message="科目创建成功")
else:
return error_response(message=result["message"])
@router.put("/update/{subject_id}") @router.put("/update/{subject_id}")
async def update_subject( async def update_subject(request: Request, subject_id: int, req: UpdateSubjectRequest):
request: Request,
subject_id: int,
req: UpdateSubjectRequest
):
"""
更新科目(班主任)
"""
user = await get_current_user(request) user = await get_current_user(request)
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) return error_response(message="无权限", code=403)
if not is_teacher: result = await SubjectService.update_subject(subject_id, **req.dict(exclude_none=True))
return error_response(message="仅班主任可更新科目", code=403) return success_response(message="科目更新成功") if result["success"] else error_response(message=result["message"])
result = await SubjectService.update_subject(
subject_id=subject_id,
**req.dict(exclude_none=True)
)
if result["success"]:
return success_response(message="科目更新成功")
else:
return error_response(message=result["message"])
@router.delete("/delete/{subject_id}") @router.delete("/delete/{subject_id}")
async def delete_subject(request: Request, subject_id: int): async def delete_subject(request: Request, subject_id: int):
"""
删除科目(软删除,班主任)
"""
user = await get_current_user(request) user = await get_current_user(request)
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"]) return error_response(message="无权限", code=403)
if not is_teacher:
return error_response(message="仅班主任可删除科目", code=403)
result = await SubjectService.delete_subject(subject_id) result = await SubjectService.delete_subject(subject_id)
return success_response(message="科目已禁用") if result["success"] else error_response(message=result["message"])
if result["success"]:
return success_response(message="科目已禁用")
else:
return error_response(message=result["message"])

View File

@@ -9,8 +9,8 @@
# 版权所有 © Sea Network Technology Studio # 版权所有 © Sea Network Technology Studio
# =========================================== # ===========================================
# 后端API地址 # 后端API地址,修改为实际地址
API_BASE_URL=https://api.your-domain.com API_BASE_URL=https://your-api-domain.com
# API超时时间 # API超时时间
API_TIMEOUT=30 API_TIMEOUT=30

View File

@@ -17,24 +17,29 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
exit(); exit();
} }
$page_title = '管理员管理';
$role = $_SESSION['role'] ?? ''; $role = $_SESSION['role'] ?? '';
if ($role !== '班主任') { if ($role !== '班主任') {
header('Location: /admin/dashboard.php'); header('Location: /admin/dashboard.php');
exit(); exit();
} }
$page_title = '管理员管理';
include __DIR__ . '/../includes/header.php'; include __DIR__ . '/../includes/header.php';
?> ?>
<div class="nav"> <div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a> <a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a> <a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a> <a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '学习委员'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a> <a href="/admin/homework.php" class="nav-item">作业管理</a>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<a href="/admin/subjects.php" class="nav-item">科目管理</a> <a href="/admin/subjects.php" class="nav-item">科目管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<a href="/admin/admins.php" class="nav-item active">管理员管理</a> <a href="/admin/admins.php" class="nav-item active">管理员管理</a>
<a href="/admin/history.php" class="nav-item">历史记录</a> <a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a> <a href="/admin/password.php" class="nav-item">修改密码</a>
@@ -45,15 +50,10 @@ include __DIR__ . '/../includes/header.php';
<div class="action-bar"> <div class="action-bar">
<button class="btn btn-primary" onclick="showAddAdminModal()">添加管理员</button> <button class="btn btn-primary" onclick="showAddAdminModal()">添加管理员</button>
</div> </div>
<div class="table-wrapper"> <div class="table-wrapper">
<table class="table"> <table class="table">
<thead> <thead>
<tr> <tr><th>用户名</th><th>姓名</th><th>角色</th><th>关联科目</th></tr>
<th>用户名</th>
<th>姓名</th>
<th>角色</th>
</tr>
</thead> </thead>
<tbody id="adminList"></tbody> <tbody id="adminList"></tbody>
</table> </table>
@@ -87,7 +87,7 @@ include __DIR__ . '/../includes/header.php';
<select id="adminRole" required> <select id="adminRole" required>
<option value="">请选择角色</option> <option value="">请选择角色</option>
<option value="班长">班长</option> <option value="班长">班长</option>
<option value="科代表">科代表</option> <option value="学习委员">学习委员</option>
<option value="考勤委员">考勤委员</option> <option value="考勤委员">考勤委员</option>
<option value="劳动委员">劳动委员</option> <option value="劳动委员">劳动委员</option>
</select> </select>
@@ -110,15 +110,52 @@ async function loadAdmins() {
<td>${escapeHtml(admin.username)}</td> <td>${escapeHtml(admin.username)}</td>
<td>${escapeHtml(admin.real_name)}</td> <td>${escapeHtml(admin.real_name)}</td>
<td>${escapeHtml(admin.role_type)}</td> <td>${escapeHtml(admin.role_type)}</td>
<td>${admin.subject_name || '-'}</td>
</tr>`; </tr>`;
}); });
if (res.data.admins.length === 0) { if (res.data.admins.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无管理员</td></tr>'; html = '<tr><td colspan="4" style="text-align:center;">暂无管理员</td></tr>';
} }
document.getElementById('adminList').innerHTML = html; document.getElementById('adminList').innerHTML = html;
} }
} }
function showAddAdminModal() {
document.getElementById('addAdminModal').style.display = 'flex';
document.getElementById('addAdminForm')?.reset();
}
async function submitAddAdmin() {
const username = document.getElementById('adminUsername').value.trim();
const realName = document.getElementById('adminRealName').value.trim();
const password = document.getElementById('adminPassword').value;
const roleType = document.getElementById('adminRole').value;
if (!username || !realName || !roleType) {
showToast('请填写完整信息', 'warning');
return;
}
const res = await apiPost('/api/admin/add', {
username: username,
real_name: realName,
password: password || undefined,
role_type: roleType
});
if (res && res.success) {
let msg = `管理员 ${res.data.username} 添加成功`;
if (res.data.password) msg += `,密码: ${res.data.password}`;
showToast(msg);
closeModal('addAdminModal');
loadAdmins();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.style.display = 'none';
}
loadAdmins(); loadAdmins();
</script> </script>
<script src="/assets/js/admin.js"></script> <script src="/assets/js/admin.js"></script>

View File

@@ -28,14 +28,14 @@ include __DIR__ . '/../includes/header.php';
<?php if ($role === '班主任' || $role === '班长'): ?> <?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a> <a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?> <?php endif; ?>
<?php if ($role === '班主任' || $role === '科代表'): ?> <?php if ($role === '班主任' || $role === '学习委员'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a> <a href="/admin/homework.php" class="nav-item">作业管理</a>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<?php endif; ?> <?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?> <?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a> <a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?> <?php endif; ?>
<?php if ($role === '班主任'): ?> <?php if ($role === '班主任'): ?>
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
<a href="/admin/admins.php" class="nav-item">管理员管理</a> <a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?> <?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a> <a href="/admin/history.php" class="nav-item">历史记录</a>
@@ -65,18 +65,16 @@ include __DIR__ . '/../includes/header.php';
<script> <script>
async function loadDashboard() { async function loadDashboard() {
// 加载学生统计
const studentsRes = await apiGet('/api/admin/students'); const studentsRes = await apiGet('/api/admin/students');
if (studentsRes && studentsRes.success) { if (studentsRes && studentsRes.success) {
document.getElementById('dashboardStats').innerHTML = ` document.getElementById('dashboardStats').innerHTML = `
<div class="stat-card"> <div class="stat-card">
<div class="stat-label">班级学生数</div> <div class="stat-label">学生数</div>
<div class="stat-value">${studentsRes.data.total || 0}</div> <div class="stat-value">${studentsRes.data.total || 0}</div>
</div> </div>
`; `;
} }
// 快捷操作按钮
let quickActions = ''; let quickActions = '';
if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长') { if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长') {
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>'; quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
@@ -86,19 +84,16 @@ async function loadDashboard() {
} }
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>'; document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
// 加载排行榜
const rankingRes = await apiGet('/api/student/ranking', { limit: 10 }); const rankingRes = await apiGet('/api/student/ranking', { limit: 10 });
if (rankingRes && rankingRes.success) { if (rankingRes && rankingRes.success) {
let html = ''; let html = '';
rankingRes.data.ranking.forEach((student, index) => { rankingRes.data.ranking.forEach((student, index) => {
html += ` html += `<tr>
<tr>
<td>${index + 1}</td> <td>${index + 1}</td>
<td>${escapeHtml(student.student_no)}</td> <td>${escapeHtml(student.student_no)}</td>
<td>${escapeHtml(student.name)}</td> <td>${escapeHtml(student.name)}</td>
<td>${student.total_points}</td> <td>${student.total_points}</td>
</tr> </tr>`;
`;
}); });
if (rankingRes.data.ranking.length === 0) { if (rankingRes.data.ranking.length === 0) {
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>'; html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';

View File

@@ -17,25 +17,32 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
exit(); exit();
} }
$page_title = '科目管理';
$role = $_SESSION['role'] ?? ''; $role = $_SESSION['role'] ?? '';
if (!in_array($role, ['班主任', '学习委员'])) {
if ($role !== '班主任') {
header('Location: /admin/dashboard.php'); header('Location: /admin/dashboard.php');
exit(); exit();
} }
$page_title = '科目管理';
include __DIR__ . '/../includes/header.php'; include __DIR__ . '/../includes/header.php';
?> ?>
<div class="nav"> <div class="nav">
<a href="/admin/dashboard.php" class="nav-item">首页</a> <a href="/admin/dashboard.php" class="nav-item">首页</a>
<a href="/admin/students.php" class="nav-item">学生管理</a> <a href="/admin/students.php" class="nav-item">学生管理</a>
<?php if ($role === '班主任' || $role === '班长'): ?>
<a href="/admin/conduct.php" class="nav-item">操行分管理</a> <a href="/admin/conduct.php" class="nav-item">操行分管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '学习委员'): ?>
<a href="/admin/homework.php" class="nav-item">作业管理</a> <a href="/admin/homework.php" class="nav-item">作业管理</a>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<a href="/admin/subjects.php" class="nav-item active">科目管理</a> <a href="/admin/subjects.php" class="nav-item active">科目管理</a>
<?php endif; ?>
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
<?php endif; ?>
<?php if ($role === '班主任'): ?>
<a href="/admin/admins.php" class="nav-item">管理员管理</a> <a href="/admin/admins.php" class="nav-item">管理员管理</a>
<?php endif; ?>
<a href="/admin/history.php" class="nav-item">历史记录</a> <a href="/admin/history.php" class="nav-item">历史记录</a>
<a href="/admin/password.php" class="nav-item">修改密码</a> <a href="/admin/password.php" class="nav-item">修改密码</a>
</div> </div>
@@ -138,10 +145,7 @@ async function loadSubjects() {
} }
async function toggleSubject(subjectId, enable) { async function toggleSubject(subjectId, enable) {
const res = await apiPut(`/api/subject/update/${subjectId}`, { const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
is_active: enable
});
if (res && res.success) { if (res && res.success) {
showToast(enable ? '科目已启用' : '科目已禁用'); showToast(enable ? '科目已启用' : '科目已禁用');
loadSubjects(); loadSubjects();
@@ -150,6 +154,36 @@ async function toggleSubject(subjectId, enable) {
} }
} }
function showAddSubjectModal() {
document.getElementById('addSubjectModal').style.display = 'flex';
document.getElementById('addSubjectForm')?.reset();
}
async function submitAddSubject() {
const subjectName = document.getElementById('subjectName').value.trim();
const subjectCode = document.getElementById('subjectCode').value.trim();
if (!subjectName) {
showToast('请填写科目名称', 'warning');
return;
}
const res = await apiPost('/api/subject/create', {
subject_name: subjectName,
subject_code: subjectCode
});
if (res && res.success) {
showToast('科目添加成功');
closeModal('addSubjectModal');
loadSubjects();
} else {
showToast(res?.message || '添加失败', 'error');
}
}
function closeModal(modalId) {
const modal = document.getElementById(modalId);
if (modal) modal.style.display = 'none';
}
loadSubjects(); loadSubjects();
</script> </script>
<script src="/assets/js/admin.js"></script> <script src="/assets/js/admin.js"></script>

View File

@@ -9,8 +9,8 @@
* 版权所有 © Sea Network Technology Studio * 版权所有 © Sea Network Technology Studio
*/ */
// API基础地址 // API 使用相对路径,由 Nginx 反向代理 /api/ 到后端
const API_BASE_URL = window.API_BASE_URL || 'http://localhost:8000'; const API_BASE_URL = '';
const JWT_STORAGE_KEY = 'class_system_token'; const JWT_STORAGE_KEY = 'class_system_token';
const USER_STORAGE_KEY = 'class_system_user'; const USER_STORAGE_KEY = 'class_system_user';
@@ -54,23 +54,24 @@ function checkAuth() {
// API请求封装 // API请求封装
async function apiRequest(url, options = {}) { async function apiRequest(url, options = {}) {
const token = getToken(); const token = getToken();
const headers = { const headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options.headers ...options.headers
}; };
if (token) { if (token) {
headers['Authorization'] = `Bearer ${token}`; headers['Authorization'] = `Bearer ${token}`;
} }
// 确保 url 以 /api/ 开头
const fullUrl = url.startsWith('/api/') ? url : `/api${url}`;
const config = { const config = {
...options, ...options,
headers headers
}; };
try { try {
const response = await fetch(`${API_BASE_URL}${url}`, config); const response = await fetch(fullUrl, config);
const data = await response.json(); const data = await response.json();
if (response.status === 401) { if (response.status === 401) {
@@ -78,7 +79,6 @@ async function apiRequest(url, options = {}) {
window.location.href = '/index.php'; window.location.href = '/index.php';
return null; return null;
} }
return data; return data;
} catch (error) { } catch (error) {
console.error('API请求错误:', error); console.error('API请求错误:', error);
@@ -121,7 +121,6 @@ function showToast(message, type = 'success') {
toast.className = `toast toast-${type}`; toast.className = `toast toast-${type}`;
toast.textContent = message; toast.textContent = message;
document.body.appendChild(toast); document.body.appendChild(toast);
setTimeout(() => { setTimeout(() => {
toast.remove(); toast.remove();
}, 3000); }, 3000);
@@ -156,10 +155,8 @@ function getStatusBadge(status, type = 'homework') {
'leave': '请假' 'leave': '请假'
} }
}; };
const texts = statusMap[type] || statusMap.homework; const texts = statusMap[type] || statusMap.homework;
const text = texts[status] || status; const text = texts[status] || status;
let className = 'status-badge '; let className = 'status-badge ';
switch (status) { switch (status) {
case 'submitted': case 'submitted':
@@ -179,7 +176,6 @@ function getStatusBadge(status, type = 'homework') {
default: default:
className += 'status-not_submitted'; className += 'status-not_submitted';
} }
return `<span class="${className}">${text}</span>`; return `<span class="${className}">${text}</span>`;
} }
@@ -216,7 +212,6 @@ async function changePassword(newPassword) {
old_password: newPassword, old_password: newPassword,
new_password: newPassword new_password: newPassword
}); });
if (res && res.success) { if (res && res.success) {
showToast('密码修改成功,请重新登录'); showToast('密码修改成功,请重新登录');
setTimeout(() => logout(), 1500); setTimeout(() => logout(), 1500);
@@ -226,17 +221,25 @@ async function changePassword(newPassword) {
} }
} }
// HTML转义
function escapeHtml(str) {
if (!str) return '';
return str.replace(/[&<>]/g, function(m) {
if (m === '&') return '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
return m;
});
}
// 页面加载时初始化 // 页面加载时初始化
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
loadUserInfo(); loadUserInfo();
const logoutBtn = document.getElementById('logoutBtn'); const logoutBtn = document.getElementById('logoutBtn');
if (logoutBtn) { if (logoutBtn) {
logoutBtn.addEventListener('click', logout); logoutBtn.addEventListener('click', logout);
} }
if (window.location.pathname.includes('/student/') || window.location.pathname.includes('/parent/')) {
// 学生端检查强制修改密码
if (window.location.pathname.includes('/student/')) {
checkNeedChangePassword(); checkNeedChangePassword();
} }
}); });

View File

@@ -10,44 +10,72 @@
* 版权所有 © Sea Network Technology Studio * 版权所有 © Sea Network Technology Studio
*/ */
// 加载环境变量 // 读取.env文件
$envFile = __DIR__ . '/.env'; $envFile = __DIR__ . '/.env';
if (file_exists($envFile)) { $config = [];
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) { if (!file_exists($envFile)) {
if (strpos(trim($line), '#') === 0) { die('错误: 配置文件 .env 不存在,请复制 .env.example 并修改配置');
}
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
if ($lines === false) {
die('错误: 无法读取配置文件 .env');
}
foreach ($lines as $line) {
$line = trim($line);
// 跳过注释行
if (strpos($line, '#') === 0 || empty($line)) {
continue; continue;
} }
// 解析 KEY=VALUE
if (strpos($line, '=') !== false) { if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2); $parts = explode('=', $line, 2);
putenv(trim($key) . '=' . trim($value)); $key = trim($parts[0]);
} $value = trim($parts[1]);
// 去除可能的引号
$value = trim($value, '"\'');
$config[$key] = $value;
} }
} }
// 检查必要配置是否存在
$requiredKeys = ['API_BASE_URL', 'API_TIMEOUT', 'JWT_STORAGE_KEY', 'USER_STORAGE_KEY', 'SITE_NAME', 'SESSION_TIMEOUT'];
$missingKeys = [];
foreach ($requiredKeys as $key) {
if (!isset($config[$key]) || $config[$key] === '') {
$missingKeys[] = $key;
}
}
if (!empty($missingKeys)) {
die('错误: 配置文件 .env 缺少必要配置项: ' . implode(', ', $missingKeys));
}
// 定义常量 // 定义常量
define('API_BASE_URL', getenv('API_BASE_URL') ?: 'http://localhost:8000'); define('API_BASE_URL', '''');
define('API_TIMEOUT', (int)(getenv('API_TIMEOUT') ?: 30)); define('API_TIMEOUT', (int)$config['API_TIMEOUT']);
define('JWT_STORAGE_KEY', getenv('JWT_STORAGE_KEY') ?: 'class_system_token'); define('JWT_STORAGE_KEY', $config['JWT_STORAGE_KEY']);
define('USER_STORAGE_KEY', getenv('USER_STORAGE_KEY') ?: 'class_system_user'); define('USER_STORAGE_KEY', $config['USER_STORAGE_KEY']);
define('SITE_NAME', getenv('SITE_NAME') ?: '班级操行分管理系统'); define('SITE_NAME', $config['SITE_NAME']);
define('SESSION_TIMEOUT', (int)(getenv('SESSION_TIMEOUT') ?: 30)); define('SESSION_TIMEOUT', (int)$config['SESSION_TIMEOUT']);
// 会话配置 // 会话配置
ini_set('session.cookie_httponly', 1); ini_set('session.cookie_httponly', 1);
ini_set('session.use_only_cookies', 1); ini_set('session.use_only_cookies', 1);
ini_set('session.cookie_secure', 1); ini_set('session.cookie_secure', 1);
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_domain', '.sea-studio.top');
ini_set('session.gc_maxlifetime', 7200);
session_name('CLASS_SESSION');
session_start(); session_start();
// 时区设置 // 时区设置
date_default_timezone_set('Asia/Shanghai'); date_default_timezone_set('Asia/Shanghai');
// 错误报告(生产环境关闭 // 生产环境关闭错误显示
if (getenv('APP_ENV') === 'production') { error_reporting(0);
error_reporting(0); ini_set('display_errors', 0);
ini_set('display_errors', 0);
} else {
error_reporting(E_ALL);
ini_set('display_errors', 1);
}
?>

View File

@@ -1,6 +1,6 @@
<?php <?php
/** /**
* 班级操行分管理系统 - 家长端 * 班级操行分管理系统 - 家长端
* *
* 开发者: Canglan * 开发者: Canglan
* 联系方式: admin@sea-studio.top * 联系方式: admin@sea-studio.top
@@ -12,69 +12,22 @@
require_once __DIR__ . '/../config.php'; require_once __DIR__ . '/../config.php';
// 检查登录状态
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') { if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
header('Location: /index.php'); header('Location: /index.php');
exit(); exit();
} }
$student_id = $_SESSION['student_id']; $page_title = '首页';
include __DIR__ . '/../includes/header.php';
?> ?>
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title><?php echo SITE_NAME; ?> - 家长端</title>
<link rel="stylesheet" href="/assets/css/style.css">
<style>
.child-info {
text-align: center;
padding: 20px;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 12px;
color: white;
margin-bottom: 20px;
}
.child-name {
font-size: 24px;
font-weight: bold;
margin-bottom: 8px;
}
.child-no {
font-size: 14px;
opacity: 0.9;
}
.conduct-score {
text-align: center;
padding: 30px;
}
.score-number {
font-size: 72px;
font-weight: bold;
color: #667eea;
}
</style>
</head>
<body>
<div class="header">
<h1><?php echo SITE_NAME; ?> - 家长端</h1>
<div class="header-info">
<span class="user-name" id="userName"></span>
<button class="btn-logout" id="logoutBtn">退出登录</button>
</div>
</div>
<div class="nav"> <div class="nav">
<button class="nav-item active" data-page="dashboard">首页</button> <a href="/parent/dashboard.php" class="nav-item active">首页</a>
<button class="nav-item" data-page="homework">作业情况</button> <a href="/parent/attendance.php" class="nav-item">考勤记录</a>
<button class="nav-item" data-page="attendance">考勤记录</button> </div>
</div>
<div class="container" id="pageContainer"> <div class="container">
<!-- 首页内容 --> <div class="child-info">
<div id="page-dashboard" class="page-content">
<div class="child-info" id="childInfo">
<div class="child-name" id="childName">--</div> <div class="child-name" id="childName">--</div>
<div class="child-no" id="childNo">--</div> <div class="child-no" id="childNo">--</div>
</div> </div>
@@ -84,189 +37,33 @@ $student_id = $_SESSION['student_id'];
<div class="score-label">当前操行分</div> <div class="score-label">当前操行分</div>
</div> </div>
</div> </div>
</div> </div>
<!-- 作业情况页 --> <style>
<div id="page-homework" class="page-content" style="display: none;"> .child-info {
<div class="card"> text-align: center;
<div class="card-title">作业列表</div> padding: 20px;
<div class="table-wrapper"> background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
<table> border-radius: 12px;
<thead> color: white;
<tr> margin-bottom: 20px;
<th>科目</th> }
<th>作业标题</th> .child-name { font-size: 24px; font-weight: bold; margin-bottom: 8px; }
<th>截止日期</th> .child-no { font-size: 14px; opacity: 0.9; }
<th>状态</th> .score-number { font-size: 72px; font-weight: bold; color: #667eea; text-align: center; }
<th>备注</th> </style>
</tr>
</thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<!-- 考勤记录页 --> <script>
<div id="page-attendance" class="page-content" style="display: none;"> async function loadDashboard() {
<div class="stats-grid"> const res = await apiGet('/api/parent/child/conduct');
<div class="stat-card">
<div class="stat-label">出勤</div>
<div class="stat-value" id="attPresent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">缺勤</div>
<div class="stat-value" id="attAbsent">0</div>
</div>
<div class="stat-card">
<div class="stat-label">迟到</div>
<div class="stat-value" id="attLate">0</div>
</div>
<div class="stat-card">
<div class="stat-label">请假</div>
<div class="stat-value" id="attLeave">0</div>
</div>
</div>
<div class="card">
<div class="card-title">考勤记录明细</div>
<div class="table-wrapper">
<table>
<thead>
<tr>
<th>日期</th>
<th>状态</th>
<th>原因</th>
</tr>
</thead>
<tbody id="attendanceList"></tbody>
</table>
</div>
</div>
</div>
</div>
<script>
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
const STUDENT_ID = <?php echo $student_id ?: 0; ?>;
// 页面切换
function showPage(pageName) {
document.querySelectorAll('.page-content').forEach(page => {
page.style.display = 'none';
});
document.getElementById(`page-${pageName}`).style.display = 'block';
document.querySelectorAll('.nav-item').forEach(item => {
item.classList.remove('active');
if (item.dataset.page === pageName) {
item.classList.add('active');
}
});
switch(pageName) {
case 'dashboard':
loadDashboard();
break;
case 'homework':
loadHomework();
break;
case 'attendance':
loadAttendance();
break;
}
}
// 加载首页
async function loadDashboard() {
try {
// 获取子女信息
const childRes = await apiGet(`/api/parent/child/conduct`);
if (childRes && childRes.success) {
document.getElementById('childName').textContent = childRes.data.student_name;
document.getElementById('childNo').textContent = childRes.data.student_no;
document.getElementById('totalPoints').textContent = childRes.data.total_points;
}
} catch (error) {
console.error('加载首页失败:', error);
}
}
// 加载作业
async function loadHomework() {
try {
const res = await apiGet(`/api/parent/child/homework`);
if (res && res.success) { if (res && res.success) {
let html = ''; document.getElementById('childName').textContent = res.data.student_name;
res.data.homework.forEach(hw => { document.getElementById('childNo').textContent = res.data.student_no;
html += ` document.getElementById('totalPoints').textContent = res.data.total_points;
<tr>
<td>${hw.subject}</td>
<td>${hw.title}</td>
<td>${hw.deadline}</td>
<td>${getStatusBadge(hw.status, 'homework')}</td>
<td>${hw.comments || '-'}</td>
</tr>
`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
} catch (error) {
console.error('加载作业失败:', error);
}
} }
}
loadDashboard();
</script>
<script src="/assets/js/parent.js"></script>
// 加载考勤 <?php include __DIR__ . '/../includes/footer.php'; ?>
async function loadAttendance() {
try {
const res = await apiGet(`/api/parent/child/attendance`);
if (res && res.success) {
const records = res.data.records;
let present = 0, absent = 0, late = 0, leave = 0;
let html = '';
records.forEach(record => {
html += `
<tr>
<td>${record.date}</td>
<td>${getStatusBadge(record.status, 'attendance')}</td>
<td>${record.reason || '-'}</td>
</tr>
`;
switch(record.status) {
case 'present': present++; break;
case 'absent': absent++; break;
case 'late': late++; break;
case 'leave': leave++; break;
}
});
document.getElementById('attPresent').textContent = present;
document.getElementById('attAbsent').textContent = absent;
document.getElementById('attLate').textContent = late;
document.getElementById('attLeave').textContent = leave;
if (records.length === 0) {
html = '<tr><td colspan="3" style="text-align:center;">暂无考勤记录</td></tr>';
}
document.getElementById('attendanceList').innerHTML = html;
}
} catch (error) {
console.error('加载考勤失败:', error);
}
}
// 初始化
document.querySelectorAll('.nav-item').forEach(btn => {
btn.addEventListener('click', () => {
showPage(btn.dataset.page);
});
});
loadDashboard();
</script>
<script src="/assets/js/common.js"></script>
</body>
</html>

View File

@@ -1,66 +0,0 @@
<?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'] !== 'parent') {
header('Location: /index.php');
exit();
}
$page_title = '作业情况';
include __DIR__ . '/../includes/header.php';
?>
<div class="nav">
<a href="/parent/dashboard.php" class="nav-item">首页</a>
<a href="/parent/homework.php" class="nav-item active">作业情况</a>
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
</div>
<div class="container">
<div class="card">
<div class="card-title">作业列表</div>
<div class="table-wrapper">
<table class="table">
<thead><tr><th>科目</th><th>作业标题</th><th>截止日期</th><th>状态</th><th>备注</th></tr></thead>
<tbody id="homeworkList"></tbody>
</table>
</div>
</div>
</div>
<script>
async function loadHomework() {
const res = await apiGet('/api/parent/child/homework');
if (res && res.success) {
let html = '';
res.data.homework.forEach(hw => {
html += `<tr>
<td>${escapeHtml(hw.subject)}</td>
<td>${escapeHtml(hw.title)}</td>
<td>${hw.deadline}</td>
<td>${getStatusBadge(hw.status, 'homework')}</td>
<td>${escapeHtml(hw.comments || '-')}</td>
</tr>`;
});
if (res.data.homework.length === 0) {
html = '<tr><td colspan="5" style="text-align:center;">暂无作业</td></tr>';
}
document.getElementById('homeworkList').innerHTML = html;
}
}
loadHomework();
</script>
<script src="/assets/js/parent.js"></script>
<?php include __DIR__ . '/../includes/footer.php'; ?>

29
nginx.ini Normal file
View File

@@ -0,0 +1,29 @@
# 前端站点配置
server {
listen 443 ssl http2;
server_name class.sea-studio.top;
root /www/wwwroot/ClassManager/frontend;
index index.php;
# 反向代理 /api/ 到后端
location /api/ {
proxy_pass https://classbackendapi.sea-studio.top/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_pass_request_headers on;
}
# PHP 处理
location ~ \.php$ {
fastcgi_pass unix:/tmp/php-cgi-80.sock;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}
# 静态资源缓存
location ~* \.(css|js|jpg|png|gif|ico)$ {
expires 30d;
}
}

View File

@@ -1,331 +1,184 @@
-- =========================================== -- ===========================================
-- 班级操行分管理系统 - 数据库初始化脚本 -- 班级操行分管理系统 - 数据库初始化脚本
-- =========================================== -- 数据库: classmanagerdb
-- 字符集: utf8mb4
--
-- 开发者: Canglan -- 开发者: Canglan
-- 联系方式: admin@sea-studio.top -- 联系方式: admin@sea-studio.top
-- 版权归属: Sea Network Technology Studio -- 版权归属: Sea Network Technology Studio
-- 许可证: MIT License -- 许可证: MIT License
-- 版权所有: Copyright (c) 2024 Sea Network Technology Studio --
-- =========================================== -- 版权所有 © Sea Network Technology Studio
-- 数据库: classmanagerdb
-- 字符集: utf8mb4
-- =========================================== -- ===========================================
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS `classmanagerdb` CREATE DATABASE IF NOT EXISTS `classmanagerdb`
CHARACTER SET utf8mb4 CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci; COLLATE utf8mb4_unicode_ci;
USE `classmanagerdb`; USE `classmanagerdb`;
-- =========================================== SET FOREIGN_KEY_CHECKS = 0;
-- 1. 班级表
-- ===========================================
DROP TABLE IF EXISTS `classes`;
CREATE TABLE `classes` (
`class_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID',
`class_name` VARCHAR(50) NOT NULL COMMENT '班级名称',
`grade` VARCHAR(20) DEFAULT NULL COMMENT '年级',
`academic_year` VARCHAR(20) DEFAULT NULL COMMENT '学年',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
INDEX `idx_class_name` (`class_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='班级表';
-- =========================================== DROP TABLE IF EXISTS `conduct_records`;
-- 2. 科目表(支持动态增删) 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 `subjects`;
DROP TABLE IF EXISTS `operation_logs`;
DROP TABLE IF EXISTS `login_logs`;
SET FOREIGN_KEY_CHECKS = 1;
-- 科目表(仅保留语数英)
CREATE TABLE `subjects` ( CREATE TABLE `subjects` (
`subject_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID', `subject_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID',
`subject_name` VARCHAR(50) NOT NULL COMMENT '科目名称', `subject_name` VARCHAR(50) NOT NULL COMMENT '科目名称',
`subject_code` VARCHAR(20) DEFAULT NULL COMMENT '科目代码', `subject_code` VARCHAR(20) DEFAULT NULL COMMENT '科目代码',
`is_active` TINYINT DEFAULT 1 COMMENT '是否启用0禁用/1启用', `is_active` TINYINT DEFAULT 1 COMMENT '是否启用',
`sort_order` INT DEFAULT 0 COMMENT '排序序号', `sort_order` INT DEFAULT 0 COMMENT '排序序号',
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX `idx_active` (`is_active`),
UNIQUE KEY `uk_subject_name` (`subject_name`) UNIQUE KEY `uk_subject_name` (`subject_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='科目表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =========================================== -- 学生表无班级ID
-- 3. 学生表
-- ===========================================
DROP TABLE IF EXISTS `students`;
CREATE TABLE `students` ( CREATE TABLE `students` (
`student_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID', `student_id` INT PRIMARY KEY AUTO_INCREMENT,
`student_no` VARCHAR(20) NOT NULL COMMENT '学号', `student_no` VARCHAR(20) NOT NULL UNIQUE,
`name` VARCHAR(50) NOT NULL COMMENT '学生姓名', `name` VARCHAR(50) NOT NULL,
`class_id` INT NOT NULL COMMENT '班级ID', `total_points` INT DEFAULT 60,
`total_points` INT DEFAULT 100 COMMENT '操行分总分', `parent_phone` VARCHAR(20) DEFAULT NULL,
`parent_phone` VARCHAR(20) DEFAULT NULL COMMENT '家长手机号', `status` TINYINT DEFAULT 1,
`status` TINYINT DEFAULT 1 COMMENT '状态0离校/1在校', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE RESTRICT,
UNIQUE KEY `uk_student_no` (`student_no`),
INDEX `idx_class_id` (`class_id`),
INDEX `idx_parent_phone` (`parent_phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生表';
-- =========================================== -- 用户表
-- 4. 用户表(统一账号)
-- ===========================================
DROP TABLE IF EXISTS `users`;
CREATE TABLE `users` ( CREATE TABLE `users` (
`user_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID', `user_id` INT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL COMMENT '登录账号', `username` VARCHAR(50) NOT NULL UNIQUE,
`password_hash` VARCHAR(64) NOT NULL COMMENT '密码哈希SHA1+MD5', `password_hash` VARCHAR(64) NOT NULL,
`real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名', `real_name` VARCHAR(50) NOT NULL,
`user_type` ENUM('student', 'parent', 'admin') NOT NULL COMMENT '用户类型', `user_type` ENUM('student', 'parent', 'admin') NOT NULL,
`student_id` INT DEFAULT NULL COMMENT '关联学生ID学生/家长)', `student_id` INT DEFAULT NULL,
`status` TINYINT DEFAULT 1 COMMENT '状态0禁用/1启用', `status` TINYINT DEFAULT 1,
`need_change_password` TINYINT DEFAULT 1 COMMENT '是否需要修改密码1需要/0不需要', `need_change_password` TINYINT DEFAULT 1,
`last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间', `last_login_time` DATETIME DEFAULT NULL,
`last_login_ip` VARCHAR(45) DEFAULT NULL COMMENT '最后登录IP', `last_login_ip` VARCHAR(45) DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY `uk_username` (`username`),
INDEX `idx_user_type` (`user_type`),
INDEX `idx_student_id` (`student_id`),
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =========================================== -- 管理员角色表无班级ID
-- 5. 管理员角色表
-- ===========================================
DROP TABLE IF EXISTS `admin_roles`;
CREATE TABLE `admin_roles` ( CREATE TABLE `admin_roles` (
`admin_role_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员角色ID', `admin_role_id` INT PRIMARY KEY AUTO_INCREMENT,
`user_id` INT NOT NULL COMMENT '用户ID', `user_id` INT NOT NULL,
`role_type` ENUM('班主任', '班长', '科代表', '考勤委员', '劳动委员') NOT NULL COMMENT '角色类型', `role_type` ENUM('班主任', '班长', '学习委员', '考勤委员', '劳动委员') NOT NULL,
`class_id` INT NOT NULL COMMENT '管理的班级ID', `subject_id` INT DEFAULT NULL,
`subject_id` INT DEFAULT NULL COMMENT '关联科目ID科代表专用', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE, FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE,
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE CASCADE,
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE CASCADE, FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE CASCADE,
INDEX `idx_user_id` (`user_id`), UNIQUE KEY `uk_user_subject` (`user_id`, `subject_id`)
INDEX `idx_role_type` (`role_type`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
UNIQUE KEY `uk_user_class_subject` (`user_id`, `class_id`, `subject_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员角色表';
-- =========================================== -- 操行分记录表
-- 6. 操行分记录表
-- ===========================================
DROP TABLE IF EXISTS `conduct_records`;
CREATE TABLE `conduct_records` ( CREATE TABLE `conduct_records` (
`record_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID', `record_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`student_id` INT NOT NULL COMMENT '学生ID', `student_id` INT NOT NULL,
`points_change` INT NOT NULL COMMENT '分数变动(正数加分,负数扣分)', `points_change` INT NOT NULL,
`reason` VARCHAR(255) NOT NULL COMMENT '变动原因', `reason` VARCHAR(255) NOT NULL,
`recorder_id` INT NOT NULL COMMENT '操作人ID', `recorder_id` INT NOT NULL,
`recorder_name` VARCHAR(50) DEFAULT NULL COMMENT '操作人姓名(冗余字段)', `recorder_name` VARCHAR(50) DEFAULT NULL,
`related_type` ENUM('manual', 'homework', 'attendance') DEFAULT 'manual' COMMENT '关联类型', `related_type` ENUM('manual', 'homework', 'attendance') DEFAULT 'manual',
`related_id` INT DEFAULT NULL COMMENT '关联ID作业ID/考勤ID', `related_id` INT DEFAULT NULL,
`is_revoked` TINYINT DEFAULT 0 COMMENT '是否已撤销0未撤销/1已撤销', `is_revoked` TINYINT DEFAULT 0,
`revoked_by` INT DEFAULT NULL COMMENT '撤销人ID', `revoked_by` INT DEFAULT NULL,
`revoked_at` DATETIME DEFAULT NULL COMMENT '撤销时间', `revoked_at` DATETIME DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE, FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT, FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT, FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`)
INDEX `idx_student_id` (`student_id`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INDEX `idx_recorder_id` (`recorder_id`),
INDEX `idx_created_at` (`created_at`),
INDEX `idx_related` (`related_type`, `related_id`),
INDEX `idx_is_revoked` (`is_revoked`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操行分记录表';
-- =========================================== -- 作业表无班级ID
-- 7. 作业表
-- ===========================================
DROP TABLE IF EXISTS `assignments`;
CREATE TABLE `assignments` ( CREATE TABLE `assignments` (
`assignment_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '作业ID', `assignment_id` INT PRIMARY KEY AUTO_INCREMENT,
`class_id` INT NOT NULL COMMENT '班级ID', `subject_id` INT NOT NULL,
`subject_id` INT NOT NULL COMMENT '科目ID', `title` VARCHAR(100) NOT NULL,
`title` VARCHAR(100) NOT NULL COMMENT '作业标题', `description` TEXT,
`description` TEXT COMMENT '作业描述', `deadline` DATE NOT NULL,
`deadline` DATE NOT NULL COMMENT '截止日期', `created_by` INT NOT NULL,
`created_by` INT NOT NULL COMMENT '发布人ID', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`),
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE CASCADE, FOREIGN KEY (`created_by`) REFERENCES `users`(`user_id`)
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE RESTRICT, ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
FOREIGN KEY (`created_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
INDEX `idx_class_id` (`class_id`),
INDEX `idx_deadline` (`deadline`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作业表';
-- =========================================== -- 作业提交记录表
-- 8. 作业提交记录表
-- ===========================================
DROP TABLE IF EXISTS `homework_submissions`;
CREATE TABLE `homework_submissions` ( CREATE TABLE `homework_submissions` (
`submission_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '提交记录ID', `submission_id` INT PRIMARY KEY AUTO_INCREMENT,
`assignment_id` INT NOT NULL COMMENT '作业ID', `assignment_id` INT NOT NULL,
`student_id` INT NOT NULL COMMENT '学生ID', `student_id` INT NOT NULL,
`status` ENUM('submitted', 'not_submitted', 'late') DEFAULT 'not_submitted' COMMENT '提交状态', `status` ENUM('submitted', 'not_submitted', 'late') DEFAULT 'not_submitted',
`submit_time` DATETIME DEFAULT NULL COMMENT '提交时间', `submit_time` DATETIME DEFAULT NULL,
`comments` TEXT COMMENT '备注', `comments` TEXT,
`deduction_applied` TINYINT DEFAULT 0 COMMENT '是否已应用扣分', `deduction_applied` TINYINT DEFAULT 0,
`deduction_record_id` BIGINT DEFAULT NULL COMMENT '关联的扣分记录ID', `deduction_record_id` BIGINT DEFAULT NULL,
`updated_by` INT DEFAULT NULL COMMENT '最后更新人ID', `updated_by` INT DEFAULT NULL,
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (`assignment_id`) REFERENCES `assignments`(`assignment_id`) ON DELETE CASCADE, FOREIGN KEY (`assignment_id`) REFERENCES `assignments`(`assignment_id`) ON DELETE CASCADE,
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE, FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
FOREIGN KEY (`updated_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL, FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
INDEX `idx_assignment_id` (`assignment_id`),
INDEX `idx_student_id` (`student_id`),
INDEX `idx_status` (`status`),
UNIQUE KEY `uk_assignment_student` (`assignment_id`, `student_id`) UNIQUE KEY `uk_assignment_student` (`assignment_id`, `student_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作业提交记录表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =========================================== -- 考勤记录表
-- 9. 考勤记录表
-- ===========================================
DROP TABLE IF EXISTS `attendance_records`;
CREATE TABLE `attendance_records` ( CREATE TABLE `attendance_records` (
`attendance_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '考勤记录ID', `attendance_id` INT PRIMARY KEY AUTO_INCREMENT,
`student_id` INT NOT NULL COMMENT '学生ID', `student_id` INT NOT NULL,
`date` DATE NOT NULL COMMENT '考勤日期', `date` DATE NOT NULL,
`status` ENUM('present', 'absent', 'late', 'leave') DEFAULT 'present' COMMENT '考勤状态', `status` ENUM('present', 'absent', 'late', 'leave') DEFAULT 'present',
`reason` VARCHAR(255) DEFAULT NULL COMMENT '原因', `reason` VARCHAR(255) DEFAULT NULL,
`recorder_id` INT NOT NULL COMMENT '记录人ID', `recorder_id` INT NOT NULL,
`deduction_applied` TINYINT DEFAULT 0 COMMENT '是否已应用扣分', `deduction_applied` TINYINT DEFAULT 0,
`deduction_record_id` BIGINT DEFAULT NULL COMMENT '关联的扣分记录ID', `deduction_record_id` BIGINT DEFAULT NULL,
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE, FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT, FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL, FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
INDEX `idx_student_id` (`student_id`),
INDEX `idx_date` (`date`),
INDEX `idx_status` (`status`),
UNIQUE KEY `uk_student_date` (`student_id`, `date`) UNIQUE KEY `uk_student_date` (`student_id`, `date`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='考勤记录表'; ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- =========================================== -- 操作日志表
-- 10. 操作日志表
-- ===========================================
DROP TABLE IF EXISTS `operation_logs`;
CREATE TABLE `operation_logs` ( CREATE TABLE `operation_logs` (
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', `log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`operator_id` INT NOT NULL COMMENT '操作人ID', `operator_id` INT NOT NULL,
`operator_name` VARCHAR(50) DEFAULT NULL COMMENT '操作人姓名', `operator_name` VARCHAR(50) DEFAULT NULL,
`operator_role` VARCHAR(50) DEFAULT NULL COMMENT '操作人角色', `operator_role` VARCHAR(50) DEFAULT NULL,
`operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型', `operation_type` VARCHAR(50) NOT NULL,
`target_type` VARCHAR(50) DEFAULT NULL COMMENT '目标类型', `target_type` VARCHAR(50) DEFAULT NULL,
`target_id` INT DEFAULT NULL COMMENT '目标ID', `target_id` INT DEFAULT NULL,
`details` TEXT COMMENT '详细信息', `details` TEXT,
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址', `ip_address` VARCHAR(45) DEFAULT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX `idx_operator_id` (`operator_id`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INDEX `idx_operation_type` (`operation_type`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
-- =========================================== -- 登录日志表
-- 11. 登录日志表
-- ===========================================
DROP TABLE IF EXISTS `login_logs`;
CREATE TABLE `login_logs` ( CREATE TABLE `login_logs` (
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID', `log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
`username` VARCHAR(50) NOT NULL COMMENT '登录账号', `username` VARCHAR(50) NOT NULL,
`login_result` TINYINT NOT NULL COMMENT '登录结果0失败/1成功', `login_result` TINYINT NOT NULL,
`fail_reason` VARCHAR(100) DEFAULT NULL COMMENT '失败原因', `fail_reason` VARCHAR(100) DEFAULT NULL,
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址', `ip_address` VARCHAR(45) DEFAULT NULL,
`user_agent` VARCHAR(255) DEFAULT NULL COMMENT '用户代理', `user_agent` VARCHAR(255) DEFAULT NULL,
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
INDEX `idx_username` (`username`), ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
INDEX `idx_created_at` (`created_at`),
INDEX `idx_login_result` (`login_result`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录日志表';
-- =========================================== -- 插入初始科目(仅语数英)
-- 创建存储过程:撤销扣分记录 INSERT INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VALUES
-- =========================================== ('语文', 'CHI', 1),
DROP PROCEDURE IF EXISTS `revoke_conduct_record`; ('数学', 'MATH', 2),
DELIMITER // ('英语', 'ENG', 3);
CREATE PROCEDURE `revoke_conduct_record`( SELECT '数据库初始化完成!' AS message;
IN p_record_id BIGINT,
IN p_revoker_id INT
)
BEGIN
DECLARE v_student_id INT;
DECLARE v_points_change INT;
DECLARE v_is_revoked TINYINT;
SELECT `student_id`, `points_change`, `is_revoked`
INTO v_student_id, v_points_change, v_is_revoked
FROM `conduct_records`
WHERE `record_id` = p_record_id;
IF v_is_revoked = 1 THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该记录已被撤销';
END IF;
UPDATE `conduct_records`
SET `is_revoked` = 1,
`revoked_by` = p_revoker_id,
`revoked_at` = NOW()
WHERE `record_id` = p_record_id;
UPDATE `students`
SET `total_points` = `total_points` - v_points_change
WHERE `student_id` = v_student_id;
END //
DELIMITER ;
-- ===========================================
-- 创建触发器:更新学生总分
-- ===========================================
DROP TRIGGER IF EXISTS `update_student_points_on_insert`;
DELIMITER //
CREATE TRIGGER `update_student_points_on_insert`
AFTER INSERT ON `conduct_records`
FOR EACH ROW
BEGIN
IF NEW.is_revoked = 0 THEN
UPDATE `students`
SET `total_points` = `total_points` + NEW.points_change
WHERE `student_id` = NEW.student_id;
END IF;
END //
DELIMITER ;
-- ===========================================
-- 创建视图:学生操行分排行榜
-- ===========================================
DROP VIEW IF EXISTS `v_student_ranking`;
CREATE VIEW `v_student_ranking` AS
SELECT
s.student_id,
s.student_no,
s.name,
c.class_name,
s.total_points,
RANK() OVER (PARTITION BY s.class_id ORDER BY s.total_points DESC) as rank_in_class
FROM `students` s
JOIN `classes` c ON s.class_id = c.class_id
WHERE s.status = 1;
-- ===========================================
-- 创建视图:今日考勤汇总
-- ===========================================
DROP VIEW IF EXISTS `v_today_attendance`;
CREATE VIEW `v_today_attendance` AS
SELECT
c.class_name,
COUNT(CASE WHEN a.status = 'present' THEN 1 END) as present_count,
COUNT(CASE WHEN a.status = 'absent' THEN 1 END) as absent_count,
COUNT(CASE WHEN a.status = 'late' THEN 1 END) as late_count,
COUNT(CASE WHEN a.status = 'leave' THEN 1 END) as leave_count,
COUNT(*) as total_count
FROM `attendance_records` a
JOIN `students` s ON a.student_id = s.student_id
JOIN `classes` c ON s.class_id = c.class_id
WHERE a.date = CURDATE()
GROUP BY c.class_id;