v0.2测试
This commit is contained in:
@@ -57,7 +57,7 @@
|
||||
|------|-----------|-----------|---------|-------------|
|
||||
| 班主任 | 全班 | 无限制 | 可撤销任何记录 | 全班所有记录 |
|
||||
| 班长 | 全班 | ±5分 | 可撤销任何记录 | 全班所有记录 |
|
||||
| 科代表 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
|
||||
| 学习委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
|
||||
| 考勤委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
|
||||
| 劳动委员 | 全班 | 仅±1分(卫生值日) | 不可撤销 | 仅自己提交的 |
|
||||
| 学生 | 自己 | 无 | 无 | 自己的历史 |
|
||||
|
||||
@@ -1,164 +1,111 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# ===========================================
|
||||
# FastAPI 应用配置
|
||||
# ===========================================
|
||||
|
||||
# 应用名称 - 显示在API文档和日志中
|
||||
# 应用名称
|
||||
APP_NAME=班级操行分管理系统
|
||||
# 运行环境 - production(生产) / development(开发) / testing(测试)
|
||||
# 生产环境会自动启用HTTPS重定向
|
||||
# 运行环境 - production / development / testing
|
||||
APP_ENV=production
|
||||
# 调试模式 - true开启详细错误信息,生产环境必须为false
|
||||
DEBUG=False
|
||||
# 应用密钥 - 用于会话加密,必须32位以上随机字符串
|
||||
# 生成方法: openssl rand -hex 32
|
||||
# 应用密钥 - 必须32位以上随机字符串
|
||||
SECRET_KEY=your-super-secret-key-min-32-characters-long
|
||||
# API版本号 - 用于路由前缀,如 /api/v1
|
||||
# API版本号
|
||||
API_VERSION=v1
|
||||
|
||||
|
||||
# ===========================================
|
||||
# MySQL 数据库配置
|
||||
# ===========================================
|
||||
|
||||
# 数据库主机地址 - 本地用127.0.0.1,远程用实际IP
|
||||
DB_HOST=127.0.0.1
|
||||
# 数据库端口 - MySQL默认3306
|
||||
DB_PORT=3306
|
||||
# 数据库用户名 - 建议创建专用账户,不要用root
|
||||
DB_USER=class_admin
|
||||
# 数据库密码 - 使用强密码,包含大小写数字特殊字符
|
||||
DB_PASSWORD=your-strong-db-password
|
||||
# 数据库名称 - 固定为 classmanagerdb
|
||||
DB_NAME=classmanagerdb
|
||||
# 连接池大小 - 同时保持的数据库连接数,根据并发量调整
|
||||
DB_POOL_SIZE=10
|
||||
# 最大溢出连接 - 连接池满后最多额外创建的连接数
|
||||
DB_MAX_OVERFLOW=20
|
||||
|
||||
# ===========================================
|
||||
# Redis 缓存配置
|
||||
# ===========================================
|
||||
|
||||
# Redis主机地址
|
||||
REDIS_HOST=127.0.0.1
|
||||
# Redis端口 - 默认6379
|
||||
REDIS_PORT=6379
|
||||
# Redis密码 - 建议设置,防止未授权访问
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
# Redis数据库编号 - 0-15,建议使用独立数据库避免冲突
|
||||
REDIS_DB=0
|
||||
# 最大连接数 - 根据并发量调整,建议50-200
|
||||
REDIS_MAX_CONNECTIONS=50
|
||||
|
||||
# ===========================================
|
||||
# JWT 认证配置
|
||||
# ===========================================
|
||||
|
||||
# JWT密钥 - 用于签名Token,必须32位以上随机字符串
|
||||
# 生成方法: openssl rand -hex 32
|
||||
JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars
|
||||
# JWT签名算法 - HS256对称加密
|
||||
JWT_ALGORITHM=HS256
|
||||
# Token过期时间(分钟)- 30分钟无操作需重新登录
|
||||
JWT_EXPIRE_MINUTES=30
|
||||
|
||||
# ===========================================
|
||||
# 密码加密配置
|
||||
# ===========================================
|
||||
|
||||
# 密码盐值 - 固定字符串,用于SHA1+MD5双重加密
|
||||
# 生成后不可更改,否则所有密码失效
|
||||
# 生成方法: openssl rand -hex 16
|
||||
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
|
||||
|
||||
# ===========================================
|
||||
# 扣分规则配置
|
||||
# ===========================================
|
||||
# 注意:这些规则仅用于建议,实际操作时可选择是否应用扣分
|
||||
|
||||
# 作业未提交扣分 - 科代表标记未提交时建议扣分数值
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT=2
|
||||
# 作业迟交扣分 - 迟交作业建议扣分数值
|
||||
DEDUCTION_HOMEWORK_LATE=1
|
||||
# 缺勤扣分 - 缺勤建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
||||
# 迟到扣分 - 迟到建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_LATE=2
|
||||
# 请假扣分 - 请假建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_LEAVE=1
|
||||
|
||||
# ===========================================
|
||||
# 劳动委员固定分值配置
|
||||
# ===========================================
|
||||
|
||||
# 劳动委员加分值 - 固定为1分
|
||||
LABOR_POINTS_ADD=1
|
||||
# 劳动委员扣分值 - 固定为-1分
|
||||
LABOR_POINTS_SUBTRACT=-1
|
||||
|
||||
# ===========================================
|
||||
# 班长加减分限制配置
|
||||
# ===========================================
|
||||
|
||||
# 班长最大单次加分值
|
||||
MONITOR_MAX_ADD=5
|
||||
# 班长最大单次扣分值(负数)
|
||||
MONITOR_MAX_SUBTRACT=-5
|
||||
|
||||
# ===========================================
|
||||
# 日志配置
|
||||
# ===========================================
|
||||
|
||||
# 日志级别 - DEBUG/INFO/WARNING/ERROR
|
||||
# 生产环境建议INFO,开发环境可用DEBUG
|
||||
LOG_LEVEL=INFO
|
||||
# 单日志文件最大大小(字节)- 100MB = 104857600
|
||||
LOG_MAX_BYTES=104857600
|
||||
# 日志备份数量 - 保留30个历史日志文件
|
||||
LOG_BACKUP_COUNT=30
|
||||
# 日志保留天数 - 操作日志保留1年,访问日志保留90天
|
||||
LOG_RETENTION_DAYS=365
|
||||
|
||||
# ===========================================
|
||||
# CORS 跨域配置
|
||||
# ===========================================
|
||||
|
||||
# 允许的跨域域名 - 多个域名用英文逗号分隔,不要有空格
|
||||
# 生产环境必须指定具体域名,不能用 *
|
||||
CORS_ORIGINS=https://your-frontend-domain.com,http://localhost:8080
|
||||
# 允许的跨域域名 - 多个域名用英文逗号分隔
|
||||
# 示例: https://example.com,https://api.example.com
|
||||
CORS_ORIGINS=https://your-frontend-domain.com,https://your-api-domain.com
|
||||
|
||||
# ===========================================
|
||||
# 上传文件配置
|
||||
# ===========================================
|
||||
|
||||
# 最大上传文件大小(字节)- 5MB = 5242880
|
||||
MAX_UPLOAD_SIZE=5242880
|
||||
# 允许的文件扩展名 - 多个用英文逗号分隔
|
||||
ALLOWED_EXTENSIONS=json
|
||||
|
||||
# ===========================================
|
||||
# 学生初始配置
|
||||
# ===========================================
|
||||
|
||||
# 学生初始操行分 - 新生导入时的默认分数,默认60分
|
||||
STUDENT_INITIAL_POINTS=60
|
||||
@@ -1,5 +1,5 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
# 班级操行分管理系统 - 配置管理
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
@@ -13,21 +13,15 @@ import os
|
||||
from dotenv import load_dotenv
|
||||
from typing import List
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Settings:
|
||||
"""应用配置类"""
|
||||
|
||||
# ========== 应用配置 ==========
|
||||
APP_NAME: str = os.getenv("APP_NAME", "班级操行分管理系统")
|
||||
APP_ENV: str = os.getenv("APP_ENV", "production")
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
|
||||
API_VERSION: str = os.getenv("API_VERSION", "v1")
|
||||
|
||||
# ========== 数据库配置 ==========
|
||||
DB_HOST: str = os.getenv("DB_HOST", "127.0.0.1")
|
||||
DB_PORT: int = int(os.getenv("DB_PORT", "3306"))
|
||||
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_MAX_OVERFLOW: int = int(os.getenv("DB_MAX_OVERFLOW", "20"))
|
||||
|
||||
# ========== Redis配置 ==========
|
||||
REDIS_HOST: str = os.getenv("REDIS_HOST", "127.0.0.1")
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
|
||||
@@ -45,63 +38,47 @@ class Settings:
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
"""获取Redis连接URL"""
|
||||
if self.REDIS_PASSWORD:
|
||||
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}"
|
||||
|
||||
# ========== JWT配置 ==========
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "")
|
||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30"))
|
||||
|
||||
# ========== 密码加密 ==========
|
||||
PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "")
|
||||
|
||||
# ========== 调试入口 ==========
|
||||
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_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
|
||||
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5"))
|
||||
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2"))
|
||||
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1"))
|
||||
|
||||
# ========== 劳动委员固定分值 ==========
|
||||
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "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_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-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"))
|
||||
LOG_RETENTION_DAYS: int = int(os.getenv("LOG_RETENTION_DAYS", "365"))
|
||||
|
||||
# ========== CORS配置 ==========
|
||||
CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "http://localhost:8080").split(",")
|
||||
@property
|
||||
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"))
|
||||
ALLOWED_EXTENSIONS: set = set(os.getenv("ALLOWED_EXTENSIONS", "json").split(","))
|
||||
|
||||
# ========== 学生初始配置 ==========
|
||||
STUDENT_INITIAL_POINTS: int = int(os.getenv("STUDENT_INITIAL_POINTS", "60"))
|
||||
|
||||
def validate(self) -> None:
|
||||
"""验证必要配置是否存在"""
|
||||
required_configs = [
|
||||
("SECRET_KEY", self.SECRET_KEY),
|
||||
("JWT_SECRET_KEY", self.JWT_SECRET_KEY),
|
||||
("PASSWORD_SALT", self.PASSWORD_SALT),
|
||||
]
|
||||
for name, value in required_configs:
|
||||
if not value:
|
||||
required = ["SECRET_KEY", "JWT_SECRET_KEY", "PASSWORD_SALT"]
|
||||
for name in required:
|
||||
if not getattr(self, name):
|
||||
raise ValueError(f"配置 {name} 不能为空")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
@@ -1,11 +1,12 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - FastAPI 主入口
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 主入口
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
# 版权所有: Copyright (c) 2024 Sea Network Technology Studio
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
@@ -17,6 +18,7 @@ from config import settings
|
||||
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
|
||||
|
||||
|
||||
@@ -57,13 +59,14 @@ async def access_log_middleware(request: Request, call_next):
|
||||
return response
|
||||
|
||||
|
||||
# CORS中间件
|
||||
# CORS中间件 - 从环境变量读取允许的域名
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"],
|
||||
allow_headers=["*"],
|
||||
expose_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
@@ -78,13 +81,11 @@ app.include_router(debug.router, tags=["调试"])
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径健康检查"""
|
||||
return {"status": "ok", "message": f"{settings.APP_NAME} API 运行中"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
|
||||
@@ -9,19 +9,13 @@
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi import Request
|
||||
from typing import List, Optional, Callable, Dict, Any
|
||||
from functools import wraps
|
||||
|
||||
from utils.response import forbidden_response
|
||||
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]:
|
||||
"""获取当前登录用户信息"""
|
||||
return {
|
||||
"user_id": getattr(request.state, 'user_id', 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)
|
||||
}
|
||||
|
||||
|
||||
async def get_current_user_id(request: Request) -> int:
|
||||
"""获取当前用户ID"""
|
||||
return getattr(request.state, 'user_id', None)
|
||||
|
||||
|
||||
class PermissionChecker:
|
||||
"""权限检查器"""
|
||||
|
||||
@staticmethod
|
||||
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,))
|
||||
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
|
||||
async def check_is_teacher(user_id: int) -> bool:
|
||||
"""检查是否为班主任"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "班主任"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_monitor(user_id: int) -> bool:
|
||||
"""检查是否为班长"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "班长"
|
||||
|
||||
@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)
|
||||
if role != "科代表":
|
||||
return False
|
||||
if subject_id:
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
|
||||
return subject_id in subject_ids
|
||||
return True
|
||||
return role == "学习委员"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_attendance_rep(user_id: int) -> bool:
|
||||
"""检查是否为考勤委员"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "考勤委员"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_labor_rep(user_id: int) -> bool:
|
||||
"""检查是否为劳动委员"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
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
|
||||
async def check_can_revoke(user_id: int, record_id: int) -> bool:
|
||||
"""
|
||||
检查是否可以撤销扣分记录
|
||||
班主任:可以撤销任何记录
|
||||
班长:可以撤销任何记录
|
||||
其他:只能撤销自己的记录
|
||||
"""
|
||||
# 获取记录信息
|
||||
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
|
||||
record = await execute_one(sql, (record_id,))
|
||||
if not record:
|
||||
return False
|
||||
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
# 班主任或班长可以撤销任何记录
|
||||
if role in ["班主任", "班长"]:
|
||||
return True
|
||||
|
||||
# 其他人只能撤销自己的记录
|
||||
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):
|
||||
"""需要认证的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
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 await func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_role(roles: List[str]):
|
||||
"""需要特定角色的装饰器"""
|
||||
def decorator(func: Callable):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
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("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
user_role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if user_role not in roles:
|
||||
return forbidden_response(f"需要{','.join(roles)}权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def require_teacher(func: Callable):
|
||||
"""需要班主任权限的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
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("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user_id)
|
||||
|
||||
if not is_teacher:
|
||||
if not await PermissionChecker.check_is_teacher(request.state.user_id):
|
||||
return forbidden_response("需要班主任权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_monitor(func: Callable):
|
||||
"""需要班长权限的装饰器"""
|
||||
def require_study_commissioner(func: Callable):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
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("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
is_monitor = await PermissionChecker.check_is_monitor(user_id)
|
||||
|
||||
if not is_monitor:
|
||||
return forbidden_response("需要班长权限")
|
||||
|
||||
if not await PermissionChecker.check_is_study_commissioner(request.state.user_id):
|
||||
return forbidden_response("需要学习委员权限")
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
@@ -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))
|
||||
@@ -214,6 +214,9 @@ async def get_assignments(request: Request):
|
||||
获取作业列表
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||
if role not in ["班主任", "学习委员"]:
|
||||
return error_response(message="无权限", code=403)
|
||||
|
||||
result = await HomeworkService.get_assignments(user["user_id"])
|
||||
|
||||
@@ -268,10 +271,10 @@ async def create_assignment(
|
||||
|
||||
@router.put("/homework/submission")
|
||||
async def update_submission_status(request: Request, req: UpdateHomeworkStatusRequest):
|
||||
"""
|
||||
更新作业提交状态(科代表)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
role = await PermissionChecker.get_user_role(user["user_id"])
|
||||
if role not in ["班主任", "学习委员"]:
|
||||
return error_response(message="无权进行此操作", code=403)
|
||||
|
||||
result = await HomeworkService.update_submission_status(
|
||||
submission_id=req.submission_id,
|
||||
@@ -340,10 +343,20 @@ async def add_admin(request: Request, req: AddAdminRequest):
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
if not await PermissionChecker.check_is_teacher(user["user_id"]):
|
||||
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(
|
||||
username=req.username,
|
||||
real_name=req.real_name,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
# 班级操行分管理系统 - 调试入口
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
@@ -9,7 +9,7 @@
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from fastapi import APIRouter, Request
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
@@ -23,12 +23,10 @@ logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AddAdminDebugRequest(BaseModel):
|
||||
"""添加管理员请求"""
|
||||
username: str
|
||||
password: str
|
||||
real_name: str
|
||||
role_type: str # 班主任/班长/科代表/考勤委员/劳动委员
|
||||
class_id: int
|
||||
role_type: str # 班主任/班长/学习委员/考勤委员/劳动委员
|
||||
subject_id: Optional[int] = None
|
||||
|
||||
|
||||
@@ -38,20 +36,22 @@ async def debug_add_admin(request: Request, req: AddAdminDebugRequest):
|
||||
调试入口 - 添加第一批管理员
|
||||
注意:此接口仅用于首次部署,使用后建议注释掉此路由
|
||||
"""
|
||||
# 检查是否已存在管理员
|
||||
from models.user import UserModel
|
||||
existing = await UserModel.get_by_username(req.username)
|
||||
if existing:
|
||||
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(
|
||||
username=req.username,
|
||||
real_name=req.real_name,
|
||||
password=req.password,
|
||||
role_type=req.role_type,
|
||||
class_id=req.class_id,
|
||||
subject_id=req.subject_id,
|
||||
operator_id=0 # 系统添加
|
||||
)
|
||||
|
||||
|
||||
@@ -9,97 +9,41 @@
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from fastapi import APIRouter, Request
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user, PermissionChecker
|
||||
from services.subject_service import SubjectService
|
||||
from schemas.subject import CreateSubjectRequest, UpdateSubjectRequest
|
||||
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 get_subjects(
|
||||
request: Request,
|
||||
is_active: Optional[bool] = None
|
||||
):
|
||||
"""
|
||||
获取科目列表
|
||||
"""
|
||||
async def get_subjects(request: Request, is_active: Optional[bool] = None):
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await SubjectService.get_subjects(is_active=is_active)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_subject(request: Request, req: CreateSubjectRequest):
|
||||
"""
|
||||
创建科目(班主任)
|
||||
"""
|
||||
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 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"])
|
||||
|
||||
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
|
||||
return error_response(message="无权限", code=403)
|
||||
result = await SubjectService.create_subject(req.subject_name, req.subject_code, req.sort_order)
|
||||
return success_response(data=result, message="科目创建成功") if result["success"] else error_response(message=result["message"])
|
||||
|
||||
@router.put("/update/{subject_id}")
|
||||
async def update_subject(
|
||||
request: Request,
|
||||
subject_id: int,
|
||||
req: UpdateSubjectRequest
|
||||
):
|
||||
"""
|
||||
更新科目(班主任)
|
||||
"""
|
||||
async def update_subject(request: Request, subject_id: int, req: UpdateSubjectRequest):
|
||||
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 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"])
|
||||
|
||||
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
|
||||
return error_response(message="无权限", code=403)
|
||||
result = await SubjectService.update_subject(subject_id, **req.dict(exclude_none=True))
|
||||
return success_response(message="科目更新成功") if result["success"] else error_response(message=result["message"])
|
||||
|
||||
@router.delete("/delete/{subject_id}")
|
||||
async def delete_subject(request: Request, subject_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)
|
||||
|
||||
if not await PermissionChecker.check_can_manage_subjects(user["user_id"]):
|
||||
return error_response(message="无权限", code=403)
|
||||
result = await SubjectService.delete_subject(subject_id)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="科目已禁用")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
return success_response(message="科目已禁用") if result["success"] else error_response(message=result["message"])
|
||||
@@ -9,8 +9,8 @@
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# 后端API地址
|
||||
API_BASE_URL=https://api.your-domain.com
|
||||
# 后端API地址,修改为实际地址
|
||||
API_BASE_URL=https://your-api-domain.com
|
||||
|
||||
# API超时时间(秒)
|
||||
API_TIMEOUT=30
|
||||
|
||||
@@ -17,24 +17,29 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '管理员管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if ($role !== '班主任') {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '管理员管理';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.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>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<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>
|
||||
<?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/history.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">
|
||||
<button class="btn btn-primary" onclick="showAddAdminModal()">添加管理员</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>姓名</th>
|
||||
<th>角色</th>
|
||||
</tr>
|
||||
<tr><th>用户名</th><th>姓名</th><th>角色</th><th>关联科目</th></tr>
|
||||
</thead>
|
||||
<tbody id="adminList"></tbody>
|
||||
</table>
|
||||
@@ -87,7 +87,7 @@ include __DIR__ . '/../includes/header.php';
|
||||
<select id="adminRole" required>
|
||||
<option value="">请选择角色</option>
|
||||
<option value="班长">班长</option>
|
||||
<option value="科代表">科代表</option>
|
||||
<option value="学习委员">学习委员</option>
|
||||
<option value="考勤委员">考勤委员</option>
|
||||
<option value="劳动委员">劳动委员</option>
|
||||
</select>
|
||||
@@ -110,15 +110,52 @@ async function loadAdmins() {
|
||||
<td>${escapeHtml(admin.username)}</td>
|
||||
<td>${escapeHtml(admin.real_name)}</td>
|
||||
<td>${escapeHtml(admin.role_type)}</td>
|
||||
<td>${admin.subject_name || '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
@@ -28,14 +28,14 @@ include __DIR__ . '/../includes/header.php';
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<a href="/admin/homework.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; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.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>
|
||||
@@ -65,18 +65,16 @@ include __DIR__ . '/../includes/header.php';
|
||||
|
||||
<script>
|
||||
async function loadDashboard() {
|
||||
// 加载学生统计
|
||||
const studentsRes = await apiGet('/api/admin/students');
|
||||
if (studentsRes && studentsRes.success) {
|
||||
document.getElementById('dashboardStats').innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">班级学生数</div>
|
||||
<div class="stat-label">学生总数</div>
|
||||
<div class="stat-value">${studentsRes.data.total || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 快捷操作按钮
|
||||
let quickActions = '';
|
||||
if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长') {
|
||||
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>';
|
||||
|
||||
// 加载排行榜
|
||||
const rankingRes = await apiGet('/api/student/ranking', { limit: 10 });
|
||||
if (rankingRes && rankingRes.success) {
|
||||
let html = '';
|
||||
rankingRes.data.ranking.forEach((student, index) => {
|
||||
html += `
|
||||
<tr>
|
||||
html += `<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
</tr>
|
||||
`;
|
||||
</tr>`;
|
||||
});
|
||||
if (rankingRes.data.ranking.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||
|
||||
@@ -17,25 +17,32 @@ if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '科目管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if ($role !== '班主任') {
|
||||
if (!in_array($role, ['班主任', '学习委员'])) {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '科目管理';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.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>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '学习委员'): ?>
|
||||
<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>
|
||||
<?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>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
@@ -138,10 +145,7 @@ async function loadSubjects() {
|
||||
}
|
||||
|
||||
async function toggleSubject(subjectId, enable) {
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, {
|
||||
is_active: enable
|
||||
});
|
||||
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, { is_active: enable });
|
||||
if (res && res.success) {
|
||||
showToast(enable ? '科目已启用' : '科目已禁用');
|
||||
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();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
@@ -9,8 +9,8 @@
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// API基础地址
|
||||
const API_BASE_URL = window.API_BASE_URL || 'http://localhost:8000';
|
||||
// API 使用相对路径,由 Nginx 反向代理 /api/ 到后端
|
||||
const API_BASE_URL = '';
|
||||
const JWT_STORAGE_KEY = 'class_system_token';
|
||||
const USER_STORAGE_KEY = 'class_system_user';
|
||||
|
||||
@@ -54,23 +54,24 @@ function checkAuth() {
|
||||
// API请求封装
|
||||
async function apiRequest(url, options = {}) {
|
||||
const token = getToken();
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
// 确保 url 以 /api/ 开头
|
||||
const fullUrl = url.startsWith('/api/') ? url : `/api${url}`;
|
||||
|
||||
const config = {
|
||||
...options,
|
||||
headers
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, config);
|
||||
const response = await fetch(fullUrl, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 401) {
|
||||
@@ -78,7 +79,6 @@ async function apiRequest(url, options = {}) {
|
||||
window.location.href = '/index.php';
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
@@ -121,7 +121,6 @@ function showToast(message, type = 'success') {
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
@@ -156,10 +155,8 @@ function getStatusBadge(status, type = 'homework') {
|
||||
'leave': '请假'
|
||||
}
|
||||
};
|
||||
|
||||
const texts = statusMap[type] || statusMap.homework;
|
||||
const text = texts[status] || status;
|
||||
|
||||
let className = 'status-badge ';
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
@@ -179,7 +176,6 @@ function getStatusBadge(status, type = 'homework') {
|
||||
default:
|
||||
className += 'status-not_submitted';
|
||||
}
|
||||
|
||||
return `<span class="${className}">${text}</span>`;
|
||||
}
|
||||
|
||||
@@ -216,7 +212,6 @@ async function changePassword(newPassword) {
|
||||
old_password: newPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码修改成功,请重新登录');
|
||||
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 '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadUserInfo();
|
||||
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', logout);
|
||||
}
|
||||
|
||||
// 学生端检查强制修改密码
|
||||
if (window.location.pathname.includes('/student/')) {
|
||||
if (window.location.pathname.includes('/student/') || window.location.pathname.includes('/parent/')) {
|
||||
checkNeedChangePassword();
|
||||
}
|
||||
});
|
||||
@@ -10,44 +10,72 @@
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 加载环境变量
|
||||
// 读取.env文件
|
||||
$envFile = __DIR__ . '/.env';
|
||||
if (file_exists($envFile)) {
|
||||
$config = [];
|
||||
|
||||
if (!file_exists($envFile)) {
|
||||
die('错误: 配置文件 .env 不存在,请复制 .env.example 并修改配置');
|
||||
}
|
||||
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
if ($lines === false) {
|
||||
die('错误: 无法读取配置文件 .env');
|
||||
}
|
||||
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) {
|
||||
$line = trim($line);
|
||||
// 跳过注释行
|
||||
if (strpos($line, '#') === 0 || empty($line)) {
|
||||
continue;
|
||||
}
|
||||
// 解析 KEY=VALUE
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
putenv(trim($key) . '=' . trim($value));
|
||||
}
|
||||
$parts = explode('=', $line, 2);
|
||||
$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_TIMEOUT', (int)(getenv('API_TIMEOUT') ?: 30));
|
||||
define('JWT_STORAGE_KEY', getenv('JWT_STORAGE_KEY') ?: 'class_system_token');
|
||||
define('USER_STORAGE_KEY', getenv('USER_STORAGE_KEY') ?: 'class_system_user');
|
||||
define('SITE_NAME', getenv('SITE_NAME') ?: '班级操行分管理系统');
|
||||
define('SESSION_TIMEOUT', (int)(getenv('SESSION_TIMEOUT') ?: 30));
|
||||
define('API_BASE_URL', '''');
|
||||
define('API_TIMEOUT', (int)$config['API_TIMEOUT']);
|
||||
define('JWT_STORAGE_KEY', $config['JWT_STORAGE_KEY']);
|
||||
define('USER_STORAGE_KEY', $config['USER_STORAGE_KEY']);
|
||||
define('SITE_NAME', $config['SITE_NAME']);
|
||||
define('SESSION_TIMEOUT', (int)$config['SESSION_TIMEOUT']);
|
||||
|
||||
// 会话配置
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.use_only_cookies', 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();
|
||||
|
||||
// 时区设置
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
// 错误报告(生产环境关闭)
|
||||
if (getenv('APP_ENV') === 'production') {
|
||||
// 生产环境关闭错误显示
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
} else {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
}
|
||||
?>
|
||||
@@ -1,6 +1,6 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 家长端主页
|
||||
* 班级操行分管理系统 - 家长端首页
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
@@ -12,69 +12,22 @@
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
// 检查登录状态
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'parent') {
|
||||
header('Location: /index.php');
|
||||
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">
|
||||
<button class="nav-item active" data-page="dashboard">首页</button>
|
||||
<button class="nav-item" data-page="homework">作业情况</button>
|
||||
<button class="nav-item" data-page="attendance">考勤记录</button>
|
||||
<a href="/parent/dashboard.php" class="nav-item active">首页</a>
|
||||
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
|
||||
</div>
|
||||
|
||||
<div class="container" id="pageContainer">
|
||||
<!-- 首页内容 -->
|
||||
<div id="page-dashboard" class="page-content">
|
||||
<div class="child-info" id="childInfo">
|
||||
<div class="container">
|
||||
<div class="child-info">
|
||||
<div class="child-name" id="childName">--</div>
|
||||
<div class="child-no" id="childNo">--</div>
|
||||
</div>
|
||||
@@ -86,187 +39,31 @@ $student_id = $_SESSION['student_id'];
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作业情况页 -->
|
||||
<div id="page-homework" class="page-content" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-title">作业列表</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>科目</th>
|
||||
<th>作业标题</th>
|
||||
<th>截止日期</th>
|
||||
<th>状态</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="homeworkList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 考勤记录页 -->
|
||||
<div id="page-attendance" class="page-content" style="display: none;">
|
||||
<div class="stats-grid">
|
||||
<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>
|
||||
<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; }
|
||||
.score-number { font-size: 72px; font-weight: bold; color: #667eea; text-align: center; }
|
||||
</style>
|
||||
|
||||
<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`);
|
||||
const res = await apiGet('/api/parent/child/conduct');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.homework.forEach(hw => {
|
||||
html += `
|
||||
<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);
|
||||
document.getElementById('childName').textContent = res.data.student_name;
|
||||
document.getElementById('childNo').textContent = res.data.student_no;
|
||||
document.getElementById('totalPoints').textContent = res.data.total_points;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载考勤
|
||||
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>
|
||||
<script src="/assets/js/parent.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
@@ -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
29
nginx.ini
Normal 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;
|
||||
}
|
||||
}
|
||||
409
sql/init.sql
409
sql/init.sql
@@ -1,331 +1,184 @@
|
||||
-- ===========================================
|
||||
-- 班级操行分管理系统 - 数据库初始化脚本
|
||||
-- ===========================================
|
||||
-- 数据库: classmanagerdb
|
||||
-- 字符集: utf8mb4
|
||||
--
|
||||
-- 开发者: Canglan
|
||||
-- 联系方式: admin@sea-studio.top
|
||||
-- 版权归属: Sea Network Technology Studio
|
||||
-- 许可证: MIT License
|
||||
-- 版权所有: Copyright (c) 2024 Sea Network Technology Studio
|
||||
-- ===========================================
|
||||
-- 数据库: classmanagerdb
|
||||
-- 字符集: utf8mb4
|
||||
--
|
||||
-- 版权所有 © Sea Network Technology Studio
|
||||
-- ===========================================
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS `classmanagerdb`
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `classmanagerdb`;
|
||||
|
||||
-- ===========================================
|
||||
-- 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='班级表';
|
||||
SET FOREIGN_KEY_CHECKS = 0;
|
||||
|
||||
-- ===========================================
|
||||
-- 2. 科目表(支持动态增删)
|
||||
-- ===========================================
|
||||
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 `subjects` (
|
||||
`subject_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID',
|
||||
`subject_name` VARCHAR(50) NOT 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 '排序序号',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_active` (`is_active`),
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
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;
|
||||
|
||||
-- ===========================================
|
||||
-- 3. 学生表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `students`;
|
||||
-- 学生表(无班级ID)
|
||||
CREATE TABLE `students` (
|
||||
`student_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID',
|
||||
`student_no` VARCHAR(20) NOT NULL COMMENT '学号',
|
||||
`name` VARCHAR(50) NOT NULL COMMENT '学生姓名',
|
||||
`class_id` INT NOT NULL COMMENT '班级ID',
|
||||
`total_points` INT DEFAULT 100 COMMENT '操行分总分',
|
||||
`parent_phone` VARCHAR(20) DEFAULT NULL COMMENT '家长手机号',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '状态(0离校/1在校)',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
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='学生表';
|
||||
`student_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`student_no` VARCHAR(20) NOT NULL UNIQUE,
|
||||
`name` VARCHAR(50) NOT NULL,
|
||||
`total_points` INT DEFAULT 60,
|
||||
`parent_phone` VARCHAR(20) DEFAULT NULL,
|
||||
`status` TINYINT DEFAULT 1,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ===========================================
|
||||
-- 4. 用户表(统一账号)
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
-- 用户表
|
||||
CREATE TABLE `users` (
|
||||
`user_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
|
||||
`password_hash` VARCHAR(64) NOT NULL COMMENT '密码哈希(SHA1+MD5)',
|
||||
`real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名',
|
||||
`user_type` ENUM('student', 'parent', 'admin') NOT NULL COMMENT '用户类型',
|
||||
`student_id` INT DEFAULT NULL COMMENT '关联学生ID(学生/家长)',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '状态(0禁用/1启用)',
|
||||
`need_change_password` TINYINT DEFAULT 1 COMMENT '是否需要修改密码(1需要/0不需要)',
|
||||
`last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间',
|
||||
`last_login_ip` VARCHAR(45) DEFAULT NULL COMMENT '最后登录IP',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
INDEX `idx_user_type` (`user_type`),
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
`user_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`username` VARCHAR(50) NOT NULL UNIQUE,
|
||||
`password_hash` VARCHAR(64) NOT NULL,
|
||||
`real_name` VARCHAR(50) NOT NULL,
|
||||
`user_type` ENUM('student', 'parent', 'admin') NOT NULL,
|
||||
`student_id` INT DEFAULT NULL,
|
||||
`status` TINYINT DEFAULT 1,
|
||||
`need_change_password` TINYINT DEFAULT 1,
|
||||
`last_login_time` DATETIME DEFAULT NULL,
|
||||
`last_login_ip` VARCHAR(45) DEFAULT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
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;
|
||||
|
||||
-- ===========================================
|
||||
-- 5. 管理员角色表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `admin_roles`;
|
||||
-- 管理员角色表(无班级ID)
|
||||
CREATE TABLE `admin_roles` (
|
||||
`admin_role_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员角色ID',
|
||||
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||
`role_type` ENUM('班主任', '班长', '科代表', '考勤委员', '劳动委员') NOT NULL COMMENT '角色类型',
|
||||
`class_id` INT NOT NULL COMMENT '管理的班级ID',
|
||||
`subject_id` INT DEFAULT NULL COMMENT '关联科目ID(科代表专用)',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`admin_role_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`user_id` INT NOT NULL,
|
||||
`role_type` ENUM('班主任', '班长', '学习委员', '考勤委员', '劳动委员') NOT NULL,
|
||||
`subject_id` INT DEFAULT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
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,
|
||||
INDEX `idx_user_id` (`user_id`),
|
||||
INDEX `idx_role_type` (`role_type`),
|
||||
UNIQUE KEY `uk_user_class_subject` (`user_id`, `class_id`, `subject_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员角色表';
|
||||
UNIQUE KEY `uk_user_subject` (`user_id`, `subject_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ===========================================
|
||||
-- 6. 操行分记录表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `conduct_records`;
|
||||
-- 操行分记录表
|
||||
CREATE TABLE `conduct_records` (
|
||||
`record_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID',
|
||||
`student_id` INT NOT NULL COMMENT '学生ID',
|
||||
`points_change` INT NOT NULL COMMENT '分数变动(正数加分,负数扣分)',
|
||||
`reason` VARCHAR(255) NOT NULL COMMENT '变动原因',
|
||||
`recorder_id` INT NOT NULL COMMENT '操作人ID',
|
||||
`recorder_name` VARCHAR(50) DEFAULT NULL COMMENT '操作人姓名(冗余字段)',
|
||||
`related_type` ENUM('manual', 'homework', 'attendance') DEFAULT 'manual' COMMENT '关联类型',
|
||||
`related_id` INT DEFAULT NULL COMMENT '关联ID(作业ID/考勤ID)',
|
||||
`is_revoked` TINYINT DEFAULT 0 COMMENT '是否已撤销(0未撤销/1已撤销)',
|
||||
`revoked_by` INT DEFAULT NULL COMMENT '撤销人ID',
|
||||
`revoked_at` DATETIME DEFAULT NULL COMMENT '撤销时间',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`record_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`student_id` INT NOT NULL,
|
||||
`points_change` INT NOT NULL,
|
||||
`reason` VARCHAR(255) NOT NULL,
|
||||
`recorder_id` INT NOT NULL,
|
||||
`recorder_name` VARCHAR(50) DEFAULT NULL,
|
||||
`related_type` ENUM('manual', 'homework', 'attendance') DEFAULT 'manual',
|
||||
`related_id` INT DEFAULT NULL,
|
||||
`is_revoked` TINYINT DEFAULT 0,
|
||||
`revoked_by` INT DEFAULT NULL,
|
||||
`revoked_at` DATETIME DEFAULT NULL,
|
||||
`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`) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
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='操行分记录表';
|
||||
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`),
|
||||
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ===========================================
|
||||
-- 7. 作业表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `assignments`;
|
||||
-- 作业表(无班级ID)
|
||||
CREATE TABLE `assignments` (
|
||||
`assignment_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '作业ID',
|
||||
`class_id` INT NOT NULL COMMENT '班级ID',
|
||||
`subject_id` INT NOT NULL COMMENT '科目ID',
|
||||
`title` VARCHAR(100) NOT NULL COMMENT '作业标题',
|
||||
`description` TEXT COMMENT '作业描述',
|
||||
`deadline` DATE NOT NULL COMMENT '截止日期',
|
||||
`created_by` INT NOT NULL COMMENT '发布人ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE RESTRICT,
|
||||
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='作业表';
|
||||
`assignment_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`subject_id` INT NOT NULL,
|
||||
`title` VARCHAR(100) NOT NULL,
|
||||
`description` TEXT,
|
||||
`deadline` DATE NOT NULL,
|
||||
`created_by` INT NOT NULL,
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`),
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users`(`user_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ===========================================
|
||||
-- 8. 作业提交记录表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `homework_submissions`;
|
||||
-- 作业提交记录表
|
||||
CREATE TABLE `homework_submissions` (
|
||||
`submission_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '提交记录ID',
|
||||
`assignment_id` INT NOT NULL COMMENT '作业ID',
|
||||
`student_id` INT NOT NULL COMMENT '学生ID',
|
||||
`status` ENUM('submitted', 'not_submitted', 'late') DEFAULT 'not_submitted' COMMENT '提交状态',
|
||||
`submit_time` DATETIME DEFAULT NULL COMMENT '提交时间',
|
||||
`comments` TEXT COMMENT '备注',
|
||||
`deduction_applied` TINYINT DEFAULT 0 COMMENT '是否已应用扣分',
|
||||
`deduction_record_id` BIGINT DEFAULT NULL COMMENT '关联的扣分记录ID',
|
||||
`updated_by` INT DEFAULT NULL COMMENT '最后更新人ID',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
`submission_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`assignment_id` INT NOT NULL,
|
||||
`student_id` INT NOT NULL,
|
||||
`status` ENUM('submitted', 'not_submitted', 'late') DEFAULT 'not_submitted',
|
||||
`submit_time` DATETIME DEFAULT NULL,
|
||||
`comments` TEXT,
|
||||
`deduction_applied` TINYINT DEFAULT 0,
|
||||
`deduction_record_id` BIGINT DEFAULT NULL,
|
||||
`updated_by` INT DEFAULT NULL,
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (`assignment_id`) REFERENCES `assignments`(`assignment_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,
|
||||
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`)
|
||||
) 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` (
|
||||
`attendance_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '考勤记录ID',
|
||||
`student_id` INT NOT NULL COMMENT '学生ID',
|
||||
`date` DATE NOT NULL COMMENT '考勤日期',
|
||||
`status` ENUM('present', 'absent', 'late', 'leave') DEFAULT 'present' COMMENT '考勤状态',
|
||||
`reason` VARCHAR(255) DEFAULT NULL COMMENT '原因',
|
||||
`recorder_id` INT NOT NULL COMMENT '记录人ID',
|
||||
`deduction_applied` TINYINT DEFAULT 0 COMMENT '是否已应用扣分',
|
||||
`deduction_record_id` BIGINT DEFAULT NULL COMMENT '关联的扣分记录ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`attendance_id` INT PRIMARY KEY AUTO_INCREMENT,
|
||||
`student_id` INT NOT NULL,
|
||||
`date` DATE NOT NULL,
|
||||
`status` ENUM('present', 'absent', 'late', 'leave') DEFAULT 'present',
|
||||
`reason` VARCHAR(255) DEFAULT NULL,
|
||||
`recorder_id` INT NOT NULL,
|
||||
`deduction_applied` TINYINT DEFAULT 0,
|
||||
`deduction_record_id` BIGINT DEFAULT NULL,
|
||||
`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`) 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,
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
INDEX `idx_date` (`date`),
|
||||
INDEX `idx_status` (`status`),
|
||||
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` (
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
|
||||
`operator_id` INT NOT NULL COMMENT '操作人ID',
|
||||
`operator_name` VARCHAR(50) DEFAULT NULL COMMENT '操作人姓名',
|
||||
`operator_role` VARCHAR(50) DEFAULT NULL COMMENT '操作人角色',
|
||||
`operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型',
|
||||
`target_type` VARCHAR(50) DEFAULT NULL COMMENT '目标类型',
|
||||
`target_id` INT DEFAULT NULL COMMENT '目标ID',
|
||||
`details` TEXT COMMENT '详细信息',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_operator_id` (`operator_id`),
|
||||
INDEX `idx_operation_type` (`operation_type`),
|
||||
INDEX `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`operator_id` INT NOT NULL,
|
||||
`operator_name` VARCHAR(50) DEFAULT NULL,
|
||||
`operator_role` VARCHAR(50) DEFAULT NULL,
|
||||
`operation_type` VARCHAR(50) NOT NULL,
|
||||
`target_type` VARCHAR(50) DEFAULT NULL,
|
||||
`target_id` INT DEFAULT NULL,
|
||||
`details` TEXT,
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ===========================================
|
||||
-- 11. 登录日志表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `login_logs`;
|
||||
-- 登录日志表
|
||||
CREATE TABLE `login_logs` (
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
|
||||
`login_result` TINYINT NOT NULL COMMENT '登录结果(0失败/1成功)',
|
||||
`fail_reason` VARCHAR(100) DEFAULT NULL COMMENT '失败原因',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址',
|
||||
`user_agent` VARCHAR(255) DEFAULT NULL COMMENT '用户代理',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_username` (`username`),
|
||||
INDEX `idx_created_at` (`created_at`),
|
||||
INDEX `idx_login_result` (`login_result`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录日志表';
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT,
|
||||
`username` VARCHAR(50) NOT NULL,
|
||||
`login_result` TINYINT NOT NULL,
|
||||
`fail_reason` VARCHAR(100) DEFAULT NULL,
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL,
|
||||
`user_agent` VARCHAR(255) DEFAULT NULL,
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
|
||||
-- ===========================================
|
||||
-- 创建存储过程:撤销扣分记录
|
||||
-- ===========================================
|
||||
DROP PROCEDURE IF EXISTS `revoke_conduct_record`;
|
||||
DELIMITER //
|
||||
-- 插入初始科目(仅语数英)
|
||||
INSERT INTO `subjects` (`subject_name`, `subject_code`, `sort_order`) VALUES
|
||||
('语文', 'CHI', 1),
|
||||
('数学', 'MATH', 2),
|
||||
('英语', 'ENG', 3);
|
||||
|
||||
CREATE PROCEDURE `revoke_conduct_record`(
|
||||
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;
|
||||
SELECT '数据库初始化完成!' AS message;
|
||||
Reference in New Issue
Block a user