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