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分 | 可撤销任何记录 | 全班所有记录 |
| 科代表 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 学习委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 考勤委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 劳动委员 | 全班 | 仅±1分卫生值日 | 不可撤销 | 仅自己提交的 |
| 学生 | 自己 | 无 | 无 | 自己的历史 |

View File

@@ -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

View File

@@ -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()

View File

@@ -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"}

View File

@@ -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

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)
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,9 +343,19 @@ 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,

View File

@@ -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 # 系统添加
)

View File

@@ -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"])

View File

@@ -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

View File

@@ -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>

View File

@@ -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>
<td>${index + 1}</td>
<td>${escapeHtml(student.student_no)}</td>
<td>${escapeHtml(student.name)}</td>
<td>${student.total_points}</td>
</tr>
`;
html += `<tr>
<td>${index + 1}</td>
<td>${escapeHtml(student.student_no)}</td>
<td>${escapeHtml(student.name)}</td>
<td>${student.total_points}</td>
</tr>`;
});
if (rankingRes.data.ranking.length === 0) {
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();
}
$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>

View File

@@ -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 '&amp;';
if (m === '<') return '&lt;';
if (m === '>') return '&gt;';
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();
}
});

View File

@@ -10,44 +10,72 @@
* 版权所有 © Sea Network Technology Studio
*/
// 加载环境变量
// 读取.env文件
$envFile = __DIR__ . '/.env';
if (file_exists($envFile)) {
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
foreach ($lines as $line) {
if (strpos(trim($line), '#') === 0) {
continue;
}
if (strpos($line, '=') !== false) {
list($key, $value) = explode('=', $line, 2);
putenv(trim($key) . '=' . trim($value));
}
$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) {
$line = trim($line);
// 跳过注释行
if (strpos($line, '#') === 0 || empty($line)) {
continue;
}
// 解析 KEY=VALUE
if (strpos($line, '=') !== false) {
$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);
}
?>
// 生产环境关闭错误显示
error_reporting(0);
ini_set('display_errors', 0);

View File

@@ -1,6 +1,6 @@
<?php
/**
* 班级操行分管理系统 - 家长端
* 班级操行分管理系统 - 家长端
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
@@ -12,261 +12,58 @@
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 class="nav">
<a href="/parent/dashboard.php" class="nav-item active">首页</a>
<a href="/parent/attendance.php" class="nav-item">考勤记录</a>
</div>
<div class="container">
<div class="child-info">
<div class="child-name" id="childName">--</div>
<div class="child-no" id="childNo">--</div>
</div>
<div class="card">
<div class="conduct-score">
<div class="score-number" id="totalPoints">--</div>
<div class="score-label">当前操行分</div>
</div>
</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>
</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>
<div class="container" id="pageContainer">
<!-- 首页内容 -->
<div id="page-dashboard" class="page-content">
<div class="child-info" id="childInfo">
<div class="child-name" id="childName">--</div>
<div class="child-no" id="childNo">--</div>
</div>
<div class="card">
<div class="conduct-score">
<div class="score-number" id="totalPoints">--</div>
<div class="score-label">当前操行分</div>
</div>
</div>
</div>
<script>
async function loadDashboard() {
const res = await apiGet('/api/parent/child/conduct');
if (res && res.success) {
document.getElementById('childName').textContent = res.data.student_name;
document.getElementById('childNo').textContent = res.data.student_no;
document.getElementById('totalPoints').textContent = res.data.total_points;
}
}
loadDashboard();
</script>
<script src="/assets/js/parent.js"></script>
<!-- 作业情况页 -->
<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>
<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) {
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);
}
}
// 加载考勤
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>
<?php include __DIR__ . '/../includes/footer.php'; ?>

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
-- 联系方式: 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;