v0.1测试
This commit is contained in:
15
.env.example
15
.env.example
@@ -1,15 +0,0 @@
|
||||
# MySQL 数据库配置
|
||||
DB_HOST=localhost
|
||||
DB_PORT=3306
|
||||
DB_USER=ClassManagerUser
|
||||
DB_PASSWORD=your_secure_password
|
||||
DB_NAME=ClassManagerDB
|
||||
|
||||
# JWT 密钥(32 bytes)
|
||||
JWT_SECRET_KEY=your_32_byte_jwt_secret_key
|
||||
|
||||
# aliyun 短信配置
|
||||
ALIYUN_SMS_ACCESS_KEY_ID=your_aliyun_sms_access_key_id
|
||||
ALIYUN_SMS_ACCESS_KEY_SECRET=your_aliyun_sms_access_key_secret
|
||||
ALIYUN_SMS_SIGN_NAME=your_aliyun_sms_sign_name
|
||||
ALIYUN_SMS_TEMPLATE_CODE=your_aliyun_sms_template_code
|
||||
67
README.md
67
README.md
@@ -1,32 +1,67 @@
|
||||
# 班级操行分管理系统
|
||||
|
||||
基于 Python FastAPI 开发的班级操行分管理系统,支持学生端、管理端、家长端三端访问,实现操行分的加减记录与查询功能。
|
||||
基于 Python FastAPI 开发的班级操行分管理系统,支持学生端、管理端、家长端三端访问,实现操行分管理、作业提交跟踪、考勤记录等功能。
|
||||
|
||||
## 主要功能
|
||||
|
||||
### 学生端
|
||||
- 查询个人当前操行总分
|
||||
- 查看个人加减分历史明细(时间、分数变化、原因)
|
||||
- 修改个人登录密码
|
||||
- 查看全班学生操行分排行榜
|
||||
- 查看任意学生的加减分历史记录
|
||||
|
||||
### 管理端(班主任/班委)
|
||||
- 对学生进行加分/减分操作(需填写原因)
|
||||
- 导出操行分数据(Excel)
|
||||
- 批量导入学生
|
||||
- 查看个人加减分历史明细(时间、分数变化、原因、操作人)
|
||||
- 查看个人作业提交情况
|
||||
- 查看个人考勤记录
|
||||
- 修改个人登录密码(首次登录强制修改)
|
||||
|
||||
### 家长端
|
||||
- 通过学生学号查询子女操行分
|
||||
- 查看子女加减分历史明细
|
||||
- 查询子女当前操行总分
|
||||
- 查看子女作业提交情况
|
||||
- 查看子女考勤记录
|
||||
|
||||
### 管理端
|
||||
|
||||
**班主任权限:**
|
||||
- 学生管理:新增/编辑/删除学生、批量导入学生(JSON)
|
||||
- 操行分管理:对学生进行加减分、撤销任何扣分记录、查看全班历史记录
|
||||
- 作业管理:发布作业、查看提交情况
|
||||
- 考勤管理:查看全班考勤记录
|
||||
- 科目管理:动态增删学科
|
||||
- 管理员管理:添加班长/科代表/考勤委员/劳动委员
|
||||
|
||||
**班长权限:**
|
||||
- 操行分管理:对学生进行加减分(±5分以内)、撤销任何人的扣分记录、查看全班历史记录
|
||||
|
||||
**科代表权限:**
|
||||
- 作业管理:更新作业提交状态、关联扣分(仅扣分,按规则)
|
||||
- 历史记录:仅查看自己提交的操作记录
|
||||
|
||||
**考勤委员权限:**
|
||||
- 考勤管理:记录考勤状态、关联扣分(仅扣分,按规则)
|
||||
- 历史记录:仅查看自己提交的操作记录
|
||||
|
||||
**劳动委员权限:**
|
||||
- 操行分管理:以卫生值日为理由进行加减分(固定 ±1 分)
|
||||
- 历史记录:仅查看自己提交的操作记录
|
||||
|
||||
## 技术栈
|
||||
|
||||
- **后端框架**: FastAPI
|
||||
- **数据库**: MySQL
|
||||
- **认证**: JWT
|
||||
- **配置管理**: python-dotenv(.env)
|
||||
| 层级 | 技术 | 版本 |
|
||||
|------|------|------|
|
||||
| 后端框架 | FastAPI | 0.104+ |
|
||||
| 数据库 | MySQL | 5.7 |
|
||||
| 缓存 | Redis | 7.x |
|
||||
| 前端 | PHP | 8.0 |
|
||||
| Web服务器 | Nginx | 1.28+ |
|
||||
|
||||
## 角色权限一览表
|
||||
|
||||
| 角色 | 操行分查看 | 操行分加减 | 撤销扣分 | 历史记录查看 |
|
||||
|------|-----------|-----------|---------|-------------|
|
||||
| 班主任 | 全班 | 无限制 | 可撤销任何记录 | 全班所有记录 |
|
||||
| 班长 | 全班 | ±5分 | 可撤销任何记录 | 全班所有记录 |
|
||||
| 科代表 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
|
||||
| 考勤委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
|
||||
| 劳动委员 | 全班 | 仅±1分(卫生值日) | 不可撤销 | 仅自己提交的 |
|
||||
| 学生 | 自己 | 无 | 无 | 自己的历史 |
|
||||
| 家长 | 子女总分 | 无 | 无 | 不可见详情 |
|
||||
|
||||
## 安装部署
|
||||
|
||||
|
||||
164
backend/.env.example
Normal file
164
backend/.env.example
Normal file
@@ -0,0 +1,164 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# ===========================================
|
||||
# FastAPI 应用配置
|
||||
# ===========================================
|
||||
|
||||
# 应用名称 - 显示在API文档和日志中
|
||||
APP_NAME=班级操行分管理系统
|
||||
# 运行环境 - production(生产) / development(开发) / testing(测试)
|
||||
# 生产环境会自动启用HTTPS重定向
|
||||
APP_ENV=production
|
||||
# 调试模式 - true开启详细错误信息,生产环境必须为false
|
||||
DEBUG=False
|
||||
# 应用密钥 - 用于会话加密,必须32位以上随机字符串
|
||||
# 生成方法: openssl rand -hex 32
|
||||
SECRET_KEY=your-super-secret-key-min-32-characters-long
|
||||
# API版本号 - 用于路由前缀,如 /api/v1
|
||||
API_VERSION=v1
|
||||
|
||||
# ===========================================
|
||||
# MySQL 数据库配置
|
||||
# ===========================================
|
||||
|
||||
# 数据库主机地址 - 本地用127.0.0.1,远程用实际IP
|
||||
DB_HOST=127.0.0.1
|
||||
# 数据库端口 - MySQL默认3306
|
||||
DB_PORT=3306
|
||||
# 数据库用户名 - 建议创建专用账户,不要用root
|
||||
DB_USER=class_admin
|
||||
# 数据库密码 - 使用强密码,包含大小写数字特殊字符
|
||||
DB_PASSWORD=your-strong-db-password
|
||||
# 数据库名称 - 固定为 classmanagerdb
|
||||
DB_NAME=classmanagerdb
|
||||
# 连接池大小 - 同时保持的数据库连接数,根据并发量调整
|
||||
DB_POOL_SIZE=10
|
||||
# 最大溢出连接 - 连接池满后最多额外创建的连接数
|
||||
DB_MAX_OVERFLOW=20
|
||||
|
||||
# ===========================================
|
||||
# Redis 缓存配置
|
||||
# ===========================================
|
||||
|
||||
# Redis主机地址
|
||||
REDIS_HOST=127.0.0.1
|
||||
# Redis端口 - 默认6379
|
||||
REDIS_PORT=6379
|
||||
# Redis密码 - 建议设置,防止未授权访问
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
# Redis数据库编号 - 0-15,建议使用独立数据库避免冲突
|
||||
REDIS_DB=0
|
||||
# 最大连接数 - 根据并发量调整,建议50-200
|
||||
REDIS_MAX_CONNECTIONS=50
|
||||
|
||||
# ===========================================
|
||||
# JWT 认证配置
|
||||
# ===========================================
|
||||
|
||||
# JWT密钥 - 用于签名Token,必须32位以上随机字符串
|
||||
# 生成方法: openssl rand -hex 32
|
||||
JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars
|
||||
# JWT签名算法 - HS256对称加密
|
||||
JWT_ALGORITHM=HS256
|
||||
# Token过期时间(分钟)- 30分钟无操作需重新登录
|
||||
JWT_EXPIRE_MINUTES=30
|
||||
|
||||
# ===========================================
|
||||
# 密码加密配置
|
||||
# ===========================================
|
||||
|
||||
# 密码盐值 - 固定字符串,用于SHA1+MD5双重加密
|
||||
# 生成后不可更改,否则所有密码失效
|
||||
# 生成方法: openssl rand -hex 16
|
||||
PASSWORD_SALT=your-fixed-salt-string-for-password-hash
|
||||
|
||||
# ===========================================
|
||||
# 调试入口配置
|
||||
# ===========================================
|
||||
|
||||
# 调试入口路径 - 随机字符串,用于添加第一批管理员
|
||||
# 建议使用32位随机字符串,只有开发人员知道此路径
|
||||
# 添加完管理员后建议注释掉debug路由
|
||||
# 生成方法: openssl rand -hex 16 | sed 's/\(..\)/\1/g' | cut -c1-32
|
||||
DEBUG_PATH=/a7k9x2m4q8w1e3r5t6y7u8i9o0p1z2x3
|
||||
|
||||
# ===========================================
|
||||
# 扣分规则配置
|
||||
# ===========================================
|
||||
# 注意:这些规则仅用于建议,实际操作时可选择是否应用扣分
|
||||
|
||||
# 作业未提交扣分 - 科代表标记未提交时建议扣分数值
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT=2
|
||||
# 作业迟交扣分 - 迟交作业建议扣分数值
|
||||
DEDUCTION_HOMEWORK_LATE=1
|
||||
# 缺勤扣分 - 缺勤建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
||||
# 迟到扣分 - 迟到建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_LATE=2
|
||||
# 请假扣分 - 请假建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_LEAVE=1
|
||||
|
||||
# ===========================================
|
||||
# 劳动委员固定分值配置
|
||||
# ===========================================
|
||||
|
||||
# 劳动委员加分值 - 固定为1分
|
||||
LABOR_POINTS_ADD=1
|
||||
# 劳动委员扣分值 - 固定为-1分
|
||||
LABOR_POINTS_SUBTRACT=-1
|
||||
|
||||
# ===========================================
|
||||
# 班长加减分限制配置
|
||||
# ===========================================
|
||||
|
||||
# 班长最大单次加分值
|
||||
MONITOR_MAX_ADD=5
|
||||
# 班长最大单次扣分值(负数)
|
||||
MONITOR_MAX_SUBTRACT=-5
|
||||
|
||||
# ===========================================
|
||||
# 日志配置
|
||||
# ===========================================
|
||||
|
||||
# 日志级别 - DEBUG/INFO/WARNING/ERROR
|
||||
# 生产环境建议INFO,开发环境可用DEBUG
|
||||
LOG_LEVEL=INFO
|
||||
# 单日志文件最大大小(字节)- 100MB = 104857600
|
||||
LOG_MAX_BYTES=104857600
|
||||
# 日志备份数量 - 保留30个历史日志文件
|
||||
LOG_BACKUP_COUNT=30
|
||||
# 日志保留天数 - 操作日志保留1年,访问日志保留90天
|
||||
LOG_RETENTION_DAYS=365
|
||||
|
||||
# ===========================================
|
||||
# CORS 跨域配置
|
||||
# ===========================================
|
||||
|
||||
# 允许的跨域域名 - 多个域名用英文逗号分隔,不要有空格
|
||||
# 生产环境必须指定具体域名,不能用 *
|
||||
CORS_ORIGINS=https://your-frontend-domain.com,http://localhost:8080
|
||||
|
||||
# ===========================================
|
||||
# 上传文件配置
|
||||
# ===========================================
|
||||
|
||||
# 最大上传文件大小(字节)- 5MB = 5242880
|
||||
MAX_UPLOAD_SIZE=5242880
|
||||
# 允许的文件扩展名 - 多个用英文逗号分隔
|
||||
ALLOWED_EXTENSIONS=json
|
||||
|
||||
# ===========================================
|
||||
# 学生初始配置
|
||||
# ===========================================
|
||||
|
||||
# 学生初始操行分 - 新生导入时的默认分数,默认60分
|
||||
STUDENT_INITIAL_POINTS=60
|
||||
107
backend/config.py
Normal file
107
backend/config.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from typing import List
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Settings:
|
||||
"""应用配置类"""
|
||||
|
||||
# ========== 应用配置 ==========
|
||||
APP_NAME: str = os.getenv("APP_NAME", "班级操行分管理系统")
|
||||
APP_ENV: str = os.getenv("APP_ENV", "production")
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
|
||||
API_VERSION: str = os.getenv("API_VERSION", "v1")
|
||||
|
||||
# ========== 数据库配置 ==========
|
||||
DB_HOST: str = os.getenv("DB_HOST", "127.0.0.1")
|
||||
DB_PORT: int = int(os.getenv("DB_PORT", "3306"))
|
||||
DB_USER: str = os.getenv("DB_USER", "root")
|
||||
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
|
||||
DB_NAME: str = os.getenv("DB_NAME", "classmanagerdb")
|
||||
DB_POOL_SIZE: int = int(os.getenv("DB_POOL_SIZE", "10"))
|
||||
DB_MAX_OVERFLOW: int = int(os.getenv("DB_MAX_OVERFLOW", "20"))
|
||||
|
||||
# ========== Redis配置 ==========
|
||||
REDIS_HOST: str = os.getenv("REDIS_HOST", "127.0.0.1")
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
|
||||
REDIS_MAX_CONNECTIONS: int = int(os.getenv("REDIS_MAX_CONNECTIONS", "50"))
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
"""获取Redis连接URL"""
|
||||
if self.REDIS_PASSWORD:
|
||||
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
|
||||
# ========== JWT配置 ==========
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "")
|
||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30"))
|
||||
|
||||
# ========== 密码加密 ==========
|
||||
PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "")
|
||||
|
||||
# ========== 调试入口 ==========
|
||||
DEBUG_PATH: str = os.getenv("DEBUG_PATH", "/debug_add_admin")
|
||||
|
||||
# ========== 扣分规则 ==========
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
|
||||
DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
|
||||
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5"))
|
||||
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2"))
|
||||
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1"))
|
||||
|
||||
# ========== 劳动委员固定分值 ==========
|
||||
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1"))
|
||||
LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1"))
|
||||
|
||||
# ========== 班长加减分限制 ==========
|
||||
MONITOR_MAX_ADD: int = int(os.getenv("MONITOR_MAX_ADD", "5"))
|
||||
MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5"))
|
||||
|
||||
# ========== 日志配置 ==========
|
||||
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
||||
LOG_MAX_BYTES: int = int(os.getenv("LOG_MAX_BYTES", "104857600"))
|
||||
LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "30"))
|
||||
LOG_RETENTION_DAYS: int = int(os.getenv("LOG_RETENTION_DAYS", "365"))
|
||||
|
||||
# ========== CORS配置 ==========
|
||||
CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "http://localhost:8080").split(",")
|
||||
|
||||
# ========== 上传配置 ==========
|
||||
MAX_UPLOAD_SIZE: int = int(os.getenv("MAX_UPLOAD_SIZE", "5242880"))
|
||||
ALLOWED_EXTENSIONS: set = set(os.getenv("ALLOWED_EXTENSIONS", "json").split(","))
|
||||
|
||||
# ========== 学生初始配置 ==========
|
||||
STUDENT_INITIAL_POINTS: int = int(os.getenv("STUDENT_INITIAL_POINTS", "60"))
|
||||
|
||||
def validate(self) -> None:
|
||||
"""验证必要配置是否存在"""
|
||||
required_configs = [
|
||||
("SECRET_KEY", self.SECRET_KEY),
|
||||
("JWT_SECRET_KEY", self.JWT_SECRET_KEY),
|
||||
("PASSWORD_SALT", self.PASSWORD_SALT),
|
||||
]
|
||||
for name, value in required_configs:
|
||||
if not value:
|
||||
raise ValueError(f"配置 {name} 不能为空")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
97
backend/main.py
Normal file
97
backend/main.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - FastAPI 主入口
|
||||
# ===========================================
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
# 版权所有: Copyright (c) 2024 Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import uvicorn
|
||||
|
||||
from config import settings
|
||||
from utils.logger import setup_logger, log_access
|
||||
from utils.database import init_db_pool, close_db_pool
|
||||
from utils.redis_client import init_redis_pool, close_redis_pool
|
||||
from routes import auth, student, parent, admin, subject, debug
|
||||
|
||||
|
||||
# 设置日志
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
logger.info("正在启动应用...")
|
||||
await init_db_pool()
|
||||
await init_redis_pool()
|
||||
logger.info(f"{settings.APP_NAME} 启动完成")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("正在关闭应用...")
|
||||
await close_db_pool()
|
||||
await close_redis_pool()
|
||||
logger.info("应用已关闭")
|
||||
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.API_VERSION,
|
||||
debug=settings.DEBUG,
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
|
||||
# 访问日志中间件
|
||||
@app.middleware("http")
|
||||
async def access_log_middleware(request: Request, call_next):
|
||||
log_access(request)
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
# CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# 注册路由
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["认证"])
|
||||
app.include_router(student.router, prefix="/api/student", tags=["学生端"])
|
||||
app.include_router(parent.router, prefix="/api/parent", tags=["家长端"])
|
||||
app.include_router(admin.router, prefix="/api/admin", tags=["管理端"])
|
||||
app.include_router(subject.router, prefix="/api/subject", tags=["科目管理"])
|
||||
app.include_router(debug.router, tags=["调试"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径健康检查"""
|
||||
return {"status": "ok", "message": f"{settings.APP_NAME} API 运行中"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
11
backend/middleware/__init__.py
Normal file
11
backend/middleware/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
111
backend/middleware/auth_middleware.py
Normal file
111
backend/middleware/auth_middleware.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import re
|
||||
|
||||
from utils.jwt_handler import jwt_handler
|
||||
from utils.redis_client import RedisClient
|
||||
from utils.response import unauthorized_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 不需要认证的路由
|
||||
PUBLIC_PATHS = [
|
||||
r'^/$',
|
||||
r'^/health$',
|
||||
r'^/api/auth/login$',
|
||||
r'^/api/auth/logout$',
|
||||
r'^/debug/.*$', # 调试入口
|
||||
]
|
||||
|
||||
# 不需要Token验证但需要记录访问的路由
|
||||
OPEN_PATHS = [
|
||||
r'^/api/auth/change-password$',
|
||||
]
|
||||
|
||||
|
||||
def is_public_path(path: str) -> bool:
|
||||
"""检查是否为公开路径"""
|
||||
for pattern in PUBLIC_PATHS:
|
||||
if re.match(pattern, path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AuthMiddleware(BaseHTTPMiddleware):
|
||||
"""JWT认证中间件"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
# 公开路径跳过认证
|
||||
if is_public_path(path):
|
||||
return await call_next(request)
|
||||
|
||||
# 获取Authorization头
|
||||
auth_header = request.headers.get("Authorization")
|
||||
|
||||
if not auth_header:
|
||||
return unauthorized_response("缺少认证令牌")
|
||||
|
||||
# 解析Bearer Token
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
return unauthorized_response("认证格式错误")
|
||||
except ValueError:
|
||||
return unauthorized_response("认证格式错误")
|
||||
|
||||
# 验证Token
|
||||
payload = jwt_handler.verify_token(token)
|
||||
if not payload:
|
||||
return unauthorized_response("令牌无效或已过期")
|
||||
|
||||
# 验证Redis中的Token
|
||||
user_id = payload.get("user_id")
|
||||
stored_token = await RedisClient.get_user_token(user_id)
|
||||
|
||||
if not stored_token or stored_token != token:
|
||||
return unauthorized_response("令牌已失效,请重新登录")
|
||||
|
||||
# 将用户信息存储到request.state
|
||||
request.state.user_id = payload.get("user_id")
|
||||
request.state.username = payload.get("username")
|
||||
request.state.user_type = payload.get("user_type")
|
||||
request.state.student_id = payload.get("student_id")
|
||||
request.state.role = payload.get("role")
|
||||
|
||||
# 刷新Token过期时间
|
||||
from config import settings
|
||||
await RedisClient.expire(f"user_token:{user_id}", settings.JWT_EXPIRE_MINUTES * 60)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
async def get_current_user(request: Request) -> Dict[str, Any]:
|
||||
"""获取当前登录用户信息"""
|
||||
return {
|
||||
"user_id": request.state.user_id,
|
||||
"username": request.state.username,
|
||||
"user_type": request.state.user_type,
|
||||
"student_id": request.state.student_id,
|
||||
"role": request.state.role
|
||||
}
|
||||
|
||||
|
||||
async def get_current_user_id(request: Request) -> int:
|
||||
"""获取当前用户ID"""
|
||||
return request.state.user_id
|
||||
197
backend/middleware/permission.py
Normal file
197
backend/middleware/permission.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from typing import List, Optional, Callable
|
||||
from functools import wraps
|
||||
|
||||
from utils.response import forbidden_response
|
||||
from utils.database import execute_one
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PermissionChecker:
|
||||
"""权限检查器"""
|
||||
|
||||
@staticmethod
|
||||
async def get_user_role(user_id: int) -> Optional[str]:
|
||||
"""获取用户的管理员角色"""
|
||||
sql = """
|
||||
SELECT role_type FROM admin_roles
|
||||
WHERE user_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
result = await execute_one(sql, (user_id,))
|
||||
return result["role_type"] if result else None
|
||||
|
||||
@staticmethod
|
||||
async def get_user_class_id(user_id: int) -> Optional[int]:
|
||||
"""获取用户管理的班级ID"""
|
||||
sql = """
|
||||
SELECT class_id FROM admin_roles
|
||||
WHERE user_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
result = await execute_one(sql, (user_id,))
|
||||
return result["class_id"] if result else None
|
||||
|
||||
@staticmethod
|
||||
async def get_user_subject_ids(user_id: int) -> List[int]:
|
||||
"""获取科代表管理的科目ID列表"""
|
||||
sql = """
|
||||
SELECT subject_id FROM admin_roles
|
||||
WHERE user_id = %s AND role_type = '科代表'
|
||||
"""
|
||||
results = await execute_one(sql, (user_id,))
|
||||
if results:
|
||||
return [r["subject_id"] for r in results] if isinstance(results, list) else [results["subject_id"]]
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
async def check_is_teacher(user_id: int) -> bool:
|
||||
"""检查是否为班主任"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "班主任"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_monitor(user_id: int) -> bool:
|
||||
"""检查是否为班长"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "班长"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_subject_rep(user_id: int, subject_id: int = None) -> bool:
|
||||
"""检查是否为科代表"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
if role != "科代表":
|
||||
return False
|
||||
if subject_id:
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
|
||||
return subject_id in subject_ids
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def check_is_attendance_rep(user_id: int) -> bool:
|
||||
"""检查是否为考勤委员"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "考勤委员"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_labor_rep(user_id: int) -> bool:
|
||||
"""检查是否为劳动委员"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "劳动委员"
|
||||
|
||||
@staticmethod
|
||||
async def check_can_revoke(user_id: int, record_id: int) -> bool:
|
||||
"""
|
||||
检查是否可以撤销扣分记录
|
||||
班主任:可以撤销任何记录
|
||||
班长:可以撤销任何记录
|
||||
其他:只能撤销自己的记录
|
||||
"""
|
||||
# 获取记录信息
|
||||
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
|
||||
record = await execute_one(sql, (record_id,))
|
||||
if not record:
|
||||
return False
|
||||
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
# 班主任或班长可以撤销任何记录
|
||||
if role in ["班主任", "班长"]:
|
||||
return True
|
||||
|
||||
# 其他人只能撤销自己的记录
|
||||
return record["recorder_id"] == user_id
|
||||
|
||||
@staticmethod
|
||||
async def check_can_manage_student(user_id: int, student_id: int) -> bool:
|
||||
"""检查是否可以管理该学生(同班级)"""
|
||||
# 获取学生班级
|
||||
sql = "SELECT class_id FROM students WHERE student_id = %s"
|
||||
student = await execute_one(sql, (student_id,))
|
||||
if not student:
|
||||
return False
|
||||
|
||||
# 获取管理员管理的班级
|
||||
admin_class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
|
||||
return admin_class_id == student["class_id"]
|
||||
|
||||
|
||||
def require_auth(func: Callable):
|
||||
"""需要认证的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state') or not hasattr(request.state, 'user_id'):
|
||||
return forbidden_response("请先登录")
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_role(roles: List[str]):
|
||||
"""需要特定角色的装饰器"""
|
||||
def decorator(func: Callable):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state'):
|
||||
return forbidden_response("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
user_role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if user_role not in roles:
|
||||
return forbidden_response(f"需要{','.join(roles)}权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def require_teacher(func: Callable):
|
||||
"""需要班主任权限的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state'):
|
||||
return forbidden_response("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user_id)
|
||||
|
||||
if not is_teacher:
|
||||
return forbidden_response("需要班主任权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_monitor(func: Callable):
|
||||
"""需要班长权限的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state'):
|
||||
return forbidden_response("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
is_monitor = await PermissionChecker.check_is_monitor(user_id)
|
||||
|
||||
if not is_monitor:
|
||||
return forbidden_response("需要班长权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
121
backend/middleware/sanitize.py
Normal file
121
backend/middleware/sanitize.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Dict, Any
|
||||
import re
|
||||
|
||||
|
||||
class SanitizeMiddleware(BaseHTTPMiddleware):
|
||||
"""输入过滤中间件"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# 只处理POST、PUT、PATCH请求
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
# 获取请求体
|
||||
body = await request.body()
|
||||
if body:
|
||||
import json
|
||||
try:
|
||||
data = json.loads(body)
|
||||
# 清理数据
|
||||
cleaned_data = self._sanitize_data(data)
|
||||
# 替换请求体
|
||||
request._body = json.dumps(cleaned_data).encode()
|
||||
except:
|
||||
pass
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
def _sanitize_data(self, data: Any) -> Any:
|
||||
"""递归清理数据"""
|
||||
if isinstance(data, dict):
|
||||
return {k: self._sanitize_data(v) for k, v in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [self._sanitize_data(item) for item in data]
|
||||
elif isinstance(data, str):
|
||||
return self._sanitize_string(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
def _sanitize_string(self, value: str) -> str:
|
||||
"""清理字符串"""
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
# 去除首尾空格
|
||||
value = value.strip()
|
||||
|
||||
# 限制长度
|
||||
if len(value) > 1000:
|
||||
value = value[:1000]
|
||||
|
||||
# 转义HTML特殊字符
|
||||
html_chars = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/'
|
||||
}
|
||||
for char, escape in html_chars.items():
|
||||
value = value.replace(char, escape)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def sanitize_input(value: str, max_length: int = 255) -> str:
|
||||
"""清理单个输入值"""
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
value = value.strip()
|
||||
if len(value) > max_length:
|
||||
value = value[:max_length]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def validate_points(points: int, min_val: int = -100, max_val: int = 100) -> tuple:
|
||||
"""
|
||||
验证分值
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if points == 0:
|
||||
return False, "分值不能为0"
|
||||
if points < min_val or points > max_val:
|
||||
return False, f"分值必须在{min_val}到{max_val}之间"
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_reason(reason: str) -> tuple:
|
||||
"""
|
||||
验证原因
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if not reason or not reason.strip():
|
||||
return False, "原因不能为空"
|
||||
if len(reason) > 255:
|
||||
return False, "原因长度不能超过255个字符"
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_date(date_str: str) -> bool:
|
||||
"""验证日期格式 YYYY-MM-DD"""
|
||||
if not date_str:
|
||||
return False
|
||||
pattern = r'^\d{4}-\d{2}-\d{2}$'
|
||||
if not re.match(pattern, date_str):
|
||||
return False
|
||||
return True
|
||||
11
backend/models/__init__.py
Normal file
11
backend/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
60
backend/models/admin_role.py
Normal file
60
backend/models/admin_role.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: 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 AdminRoleModel:
|
||||
"""管理员角色数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_user_id(user_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT ar.*, c.class_name, s.subject_name
|
||||
FROM admin_roles ar
|
||||
LEFT JOIN classes c ON ar.class_id = c.class_id
|
||||
LEFT JOIN subjects s ON ar.subject_id = s.subject_id
|
||||
WHERE ar.user_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
return await execute_one(sql, (user_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_class_id_by_user(user_id: int) -> Optional[int]:
|
||||
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_by_class(class_id: int) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT ar.*, u.real_name, u.username
|
||||
FROM admin_roles ar
|
||||
JOIN users u ON ar.user_id = u.user_id
|
||||
WHERE ar.class_id = %s
|
||||
ORDER BY ar.role_type
|
||||
"""
|
||||
return await execute_query(sql, (class_id,))
|
||||
|
||||
@staticmethod
|
||||
async def create(user_id: int, role_type: str, class_id: int, subject_id: int = None) -> int:
|
||||
sql = """
|
||||
INSERT INTO admin_roles (user_id, role_type, class_id, subject_id)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (user_id, role_type, class_id, subject_id))
|
||||
|
||||
@staticmethod
|
||||
async def delete(user_id: int) -> bool:
|
||||
sql = "DELETE FROM admin_roles WHERE user_id = %s"
|
||||
result = await execute_update(sql, (user_id,))
|
||||
return result > 0
|
||||
98
backend/models/attenddance.py
Normal file
98
backend/models/attenddance.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update
|
||||
|
||||
|
||||
class AttendanceModel:
|
||||
"""考勤数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_student_records(student_id: int, month: str = None) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT attendance_id, date, status, reason, deduction_applied, created_at
|
||||
FROM attendance_records
|
||||
WHERE student_id = %s
|
||||
"""
|
||||
params = [student_id]
|
||||
|
||||
if month:
|
||||
sql += " AND DATE_FORMAT(date, '%%Y-%%m') = %s"
|
||||
params.append(month)
|
||||
|
||||
sql += " ORDER BY date DESC"
|
||||
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def get_class_records(
|
||||
class_id: int,
|
||||
date: str = None,
|
||||
student_id: int = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT ar.*, s.name as student_name, s.student_no
|
||||
FROM attendance_records ar
|
||||
JOIN students s ON ar.student_id = s.student_id
|
||||
WHERE s.class_id = %s
|
||||
"""
|
||||
params = [class_id]
|
||||
|
||||
if date:
|
||||
sql += " AND ar.date = %s"
|
||||
params.append(date)
|
||||
|
||||
if student_id:
|
||||
sql += " AND ar.student_id = %s"
|
||||
params.append(student_id)
|
||||
|
||||
sql += " ORDER BY ar.date DESC, s.student_no"
|
||||
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def create_record(
|
||||
student_id: int,
|
||||
date: str,
|
||||
status: str,
|
||||
reason: str = None,
|
||||
recorder_id: int = None
|
||||
) -> int:
|
||||
# 检查是否已存在当天记录
|
||||
existing = await execute_one(
|
||||
"SELECT attendance_id FROM attendance_records WHERE student_id = %s AND date = %s",
|
||||
(student_id, date)
|
||||
)
|
||||
|
||||
if existing:
|
||||
# 更新已有记录
|
||||
sql = """
|
||||
UPDATE attendance_records
|
||||
SET status = %s, reason = %s, recorder_id = %s
|
||||
WHERE student_id = %s AND date = %s
|
||||
"""
|
||||
await execute_update(sql, (status, reason, recorder_id, student_id, date))
|
||||
return existing["attendance_id"]
|
||||
else:
|
||||
# 插入新记录
|
||||
sql = """
|
||||
INSERT INTO attendance_records (student_id, date, status, reason, recorder_id)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (student_id, date, status, reason, recorder_id))
|
||||
|
||||
@staticmethod
|
||||
async def mark_deduction_applied(attendance_id: int) -> bool:
|
||||
sql = "UPDATE attendance_records SET deduction_applied = 1 WHERE attendance_id = %s"
|
||||
result = await execute_update(sql, (attendance_id,))
|
||||
return result > 0
|
||||
35
backend/models/class_model.py
Normal file
35
backend/models/class_model.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: 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))
|
||||
166
backend/models/conduct.py
Normal file
166
backend/models/conduct.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update, call_procedure
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConductModel:
|
||||
"""操行分数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def create_record(
|
||||
student_id: int,
|
||||
points_change: int,
|
||||
reason: str,
|
||||
recorder_id: int,
|
||||
recorder_name: str = None,
|
||||
related_type: str = 'manual',
|
||||
related_id: int = None
|
||||
) -> int:
|
||||
"""创建操行分记录"""
|
||||
sql = """
|
||||
INSERT INTO conduct_records
|
||||
(student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (
|
||||
student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
async def get_student_records(
|
||||
student_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
include_revoked: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取学生操行分记录"""
|
||||
sql = """
|
||||
SELECT cr.*, u.real_name as recorder_name
|
||||
FROM conduct_records cr
|
||||
LEFT JOIN users u ON cr.recorder_id = u.user_id
|
||||
WHERE cr.student_id = %s
|
||||
"""
|
||||
if not include_revoked:
|
||||
sql += " AND cr.is_revoked = 0"
|
||||
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
||||
return await execute_query(sql, (student_id, limit, offset))
|
||||
|
||||
@staticmethod
|
||||
async def get_records_by_recorder(
|
||||
recorder_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取操作人提交的记录"""
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
WHERE cr.recorder_id = %s AND cr.is_revoked = 0
|
||||
ORDER BY cr.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
return await execute_query(sql, (recorder_id, limit, offset))
|
||||
|
||||
@staticmethod
|
||||
async def get_all_records(
|
||||
class_id: int = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
start_date: str = None,
|
||||
end_date: str = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取所有记录(班主任/班长专用)"""
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
JOIN users u ON cr.recorder_id = u.user_id
|
||||
WHERE cr.is_revoked = 0
|
||||
"""
|
||||
params = []
|
||||
|
||||
if class_id:
|
||||
sql += " AND s.class_id = %s"
|
||||
params.append(class_id)
|
||||
|
||||
if start_date:
|
||||
sql += " AND DATE(cr.created_at) >= %s"
|
||||
params.append(start_date)
|
||||
|
||||
if end_date:
|
||||
sql += " AND DATE(cr.created_at) <= %s"
|
||||
params.append(end_date)
|
||||
|
||||
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
||||
params.extend([limit, offset])
|
||||
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def get_record_by_id(record_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取记录"""
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name, s.total_points
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
WHERE cr.record_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (record_id,))
|
||||
|
||||
@staticmethod
|
||||
async def revoke_record(record_id: int, revoker_id: int) -> bool:
|
||||
"""撤销记录"""
|
||||
try:
|
||||
await call_procedure('revoke_conduct_record', (record_id, revoker_id))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"撤销记录失败: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def batch_create_records(records_data: List[Dict]) -> List[Dict]:
|
||||
"""批量创建操行分记录"""
|
||||
results = []
|
||||
for record in records_data:
|
||||
try:
|
||||
record_id = await ConductModel.create_record(
|
||||
student_id=record.get('student_id'),
|
||||
points_change=record.get('points_change'),
|
||||
reason=record.get('reason'),
|
||||
recorder_id=record.get('recorder_id'),
|
||||
recorder_name=record.get('recorder_name')
|
||||
)
|
||||
results.append({
|
||||
'student_id': record.get('student_id'),
|
||||
'success': True,
|
||||
'record_id': record_id
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'student_id': record.get('student_id'),
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
async def get_student_total_points(student_id: int) -> int:
|
||||
"""获取学生当前总分"""
|
||||
sql = "SELECT total_points FROM students WHERE student_id = %s"
|
||||
result = await execute_one(sql, (student_id,))
|
||||
return result['total_points'] if result else 100
|
||||
118
backend/models/homework.py
Normal file
118
backend/models/homework.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: 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 HomeworkModel:
|
||||
"""作业数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_assignments_by_class(class_id: int) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT a.*, s.subject_name, u.real_name as created_by_name
|
||||
FROM assignments a
|
||||
JOIN subjects s ON a.subject_id = s.subject_id
|
||||
JOIN users u ON a.created_by = u.user_id
|
||||
WHERE a.class_id = %s
|
||||
ORDER BY a.deadline ASC, a.created_at DESC
|
||||
"""
|
||||
return await execute_query(sql, (class_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_assignments_by_subjects(class_id: int, subject_ids: List[int]) -> List[Dict[str, Any]]:
|
||||
if not subject_ids:
|
||||
return []
|
||||
placeholders = ','.join(['%s'] * len(subject_ids))
|
||||
sql = f"""
|
||||
SELECT a.*, s.subject_name, u.real_name as created_by_name
|
||||
FROM assignments a
|
||||
JOIN subjects s ON a.subject_id = s.subject_id
|
||||
JOIN users u ON a.created_by = u.user_id
|
||||
WHERE a.class_id = %s AND a.subject_id IN ({placeholders})
|
||||
ORDER BY a.deadline ASC, a.created_at DESC
|
||||
"""
|
||||
params = [class_id] + subject_ids
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def get_student_homework(student_id: int) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT a.assignment_id, a.title, a.description, a.deadline,
|
||||
s.subject_name, hs.status, hs.submit_time, hs.comments, hs.deduction_applied
|
||||
FROM assignments a
|
||||
JOIN subjects s ON a.subject_id = s.subject_id
|
||||
LEFT JOIN homework_submissions hs ON a.assignment_id = hs.assignment_id AND hs.student_id = %s
|
||||
WHERE a.class_id = (SELECT class_id FROM students WHERE student_id = %s)
|
||||
ORDER BY a.deadline ASC, a.created_at DESC
|
||||
"""
|
||||
return await execute_query(sql, (student_id, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def get_submission(submission_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT hs.*, a.title, a.subject_id, a.assignment_id, s.name as student_name
|
||||
FROM homework_submissions hs
|
||||
JOIN assignments a ON hs.assignment_id = a.assignment_id
|
||||
JOIN students s ON hs.student_id = s.student_id
|
||||
WHERE hs.submission_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (submission_id,))
|
||||
|
||||
@staticmethod
|
||||
async def create_assignment(
|
||||
class_id: int,
|
||||
subject_id: int,
|
||||
title: str,
|
||||
description: str,
|
||||
deadline: str,
|
||||
created_by: int
|
||||
) -> int:
|
||||
sql = """
|
||||
INSERT INTO assignments (class_id, subject_id, title, description, deadline, created_by)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
assignment_id = await execute_insert(sql, (class_id, subject_id, title, description, deadline, created_by))
|
||||
|
||||
# 为班级所有学生创建提交记录
|
||||
from models.student import StudentModel
|
||||
students = await StudentModel.get_by_class(class_id)
|
||||
|
||||
for student in students:
|
||||
sql_sub = """
|
||||
INSERT INTO homework_submissions (assignment_id, student_id, status)
|
||||
VALUES (%s, %s, 'not_submitted')
|
||||
"""
|
||||
await execute_insert(sql_sub, (assignment_id, student["student_id"]))
|
||||
|
||||
return assignment_id
|
||||
|
||||
@staticmethod
|
||||
async def update_submission(
|
||||
submission_id: int,
|
||||
status: str,
|
||||
comments: str = None,
|
||||
updated_by: int = None
|
||||
) -> bool:
|
||||
sql = """
|
||||
UPDATE homework_submissions
|
||||
SET status = %s, comments = %s, updated_by = %s, updated_at = NOW()
|
||||
WHERE submission_id = %s
|
||||
"""
|
||||
result = await execute_update(sql, (status, comments, updated_by, submission_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def mark_deduction_applied(submission_id: int) -> bool:
|
||||
sql = "UPDATE homework_submissions SET deduction_applied = 1 WHERE submission_id = %s"
|
||||
result = await execute_update(sql, (submission_id,))
|
||||
return result > 0
|
||||
146
backend/models/student.py
Normal file
146
backend/models/student.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 学生数据模型
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update, execute_many
|
||||
from utils.security import security
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class StudentModel:
|
||||
"""学生数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(student_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取学生信息"""
|
||||
sql = """
|
||||
SELECT s.*
|
||||
FROM students s
|
||||
WHERE s.student_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (student_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_student_no(student_no: str) -> Optional[Dict[str, Any]]:
|
||||
"""根据学号获取学生信息"""
|
||||
sql = """
|
||||
SELECT s.*
|
||||
FROM students s
|
||||
WHERE s.student_no = %s
|
||||
"""
|
||||
return await execute_one(sql, (student_no,))
|
||||
|
||||
@staticmethod
|
||||
async def get_all(include_disabled: bool = False) -> List[Dict[str, Any]]:
|
||||
"""获取所有学生列表(单班级)"""
|
||||
sql = """
|
||||
SELECT student_id, student_no, name, total_points, parent_phone, status
|
||||
FROM students
|
||||
WHERE 1=1
|
||||
"""
|
||||
if not include_disabled:
|
||||
sql += " AND status = 1"
|
||||
sql += " ORDER BY student_no"
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
student_no: str,
|
||||
name: str,
|
||||
class_id: int,
|
||||
parent_phone: str = None,
|
||||
initial_points: int = 60
|
||||
) -> int:
|
||||
"""创建学生(初始操行分默认60分)"""
|
||||
sql = """
|
||||
INSERT INTO students (student_no, name, class_id, parent_phone, total_points)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (student_no, name, class_id, parent_phone, initial_points))
|
||||
|
||||
@staticmethod
|
||||
async def update(student_id: int, name: str = None, parent_phone: str = None, status: int = None) -> bool:
|
||||
"""更新学生信息"""
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if name is not None:
|
||||
updates.append("name = %s")
|
||||
params.append(name)
|
||||
if parent_phone is not None:
|
||||
updates.append("parent_phone = %s")
|
||||
params.append(parent_phone)
|
||||
if status is not None:
|
||||
updates.append("status = %s")
|
||||
params.append(status)
|
||||
|
||||
if not updates:
|
||||
return True
|
||||
|
||||
params.append(student_id)
|
||||
sql = f"UPDATE students SET {', '.join(updates)} WHERE student_id = %s"
|
||||
result = await execute_update(sql, tuple(params))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def delete(student_id: int) -> bool:
|
||||
"""删除学生(软删除)"""
|
||||
sql = "UPDATE students SET status = 0 WHERE student_id = %s"
|
||||
result = await execute_update(sql, (student_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def update_total_points(student_id: int, points_change: int) -> bool:
|
||||
"""更新学生总分"""
|
||||
sql = "UPDATE students SET total_points = total_points + %s WHERE student_id = %s"
|
||||
result = await execute_update(sql, (points_change, student_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def get_ranking(limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""获取学生排行(单班级)"""
|
||||
sql = """
|
||||
SELECT student_id, student_no, name, total_points,
|
||||
RANK() OVER (ORDER BY total_points DESC) as rank
|
||||
FROM students
|
||||
WHERE status = 1
|
||||
ORDER BY total_points DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
return await execute_query(sql, (limit,))
|
||||
|
||||
@staticmethod
|
||||
async def batch_create(students_data: List[Dict], initial_points: int = 60) -> List[Dict]:
|
||||
"""批量创建学生"""
|
||||
results = []
|
||||
for student in students_data:
|
||||
try:
|
||||
student_id = await StudentModel.create(
|
||||
student_no=student.get('student_no'),
|
||||
name=student.get('name'),
|
||||
class_id=1, # 单班级,固定为1
|
||||
parent_phone=student.get('parent_phone'),
|
||||
initial_points=initial_points
|
||||
)
|
||||
results.append({
|
||||
'student_no': student.get('student_no'),
|
||||
'success': True,
|
||||
'student_id': student_id
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'student_no': student.get('student_no'),
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
return results
|
||||
82
backend/models/subject.py
Normal file
82
backend/models/subject.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: 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 SubjectModel:
|
||||
"""科目数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_all(is_active: bool = None) -> List[Dict[str, Any]]:
|
||||
if is_active is not None:
|
||||
sql = "SELECT * FROM subjects WHERE is_active = %s ORDER BY sort_order, subject_id"
|
||||
return await execute_query(sql, (1 if is_active else 0,))
|
||||
else:
|
||||
sql = "SELECT * FROM subjects ORDER BY sort_order, subject_id"
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(subject_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = "SELECT * FROM subjects WHERE subject_id = %s"
|
||||
return await execute_one(sql, (subject_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_name(subject_name: str) -> Optional[Dict[str, Any]]:
|
||||
sql = "SELECT * FROM subjects WHERE subject_name = %s"
|
||||
return await execute_one(sql, (subject_name,))
|
||||
|
||||
@staticmethod
|
||||
async def create(subject_name: str, subject_code: str = None, sort_order: int = 0) -> int:
|
||||
sql = """
|
||||
INSERT INTO subjects (subject_name, subject_code, sort_order)
|
||||
VALUES (%s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (subject_name, subject_code, sort_order))
|
||||
|
||||
@staticmethod
|
||||
async def update(subject_id: int, **kwargs) -> bool:
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if "subject_name" in kwargs:
|
||||
updates.append("subject_name = %s")
|
||||
params.append(kwargs["subject_name"])
|
||||
if "subject_code" in kwargs:
|
||||
updates.append("subject_code = %s")
|
||||
params.append(kwargs["subject_code"])
|
||||
if "is_active" in kwargs:
|
||||
updates.append("is_active = %s")
|
||||
params.append(1 if kwargs["is_active"] else 0)
|
||||
if "sort_order" in kwargs:
|
||||
updates.append("sort_order = %s")
|
||||
params.append(kwargs["sort_order"])
|
||||
|
||||
if not updates:
|
||||
return True
|
||||
|
||||
params.append(subject_id)
|
||||
sql = f"UPDATE subjects SET {', '.join(updates)} WHERE subject_id = %s"
|
||||
result = await execute_update(sql, tuple(params))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def delete(subject_id: int) -> bool:
|
||||
sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s"
|
||||
result = await execute_update(sql, (subject_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def activate(subject_id: int) -> bool:
|
||||
sql = "UPDATE subjects SET is_active = 1 WHERE subject_id = %s"
|
||||
result = await execute_update(sql, (subject_id,))
|
||||
return result > 0
|
||||
101
backend/models/user.py
Normal file
101
backend/models/user.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from utils.database import execute_one, execute_insert, execute_update
|
||||
from utils.security import security
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UserModel:
|
||||
"""用户数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_username(username: str) -> dict:
|
||||
"""根据用户名获取用户"""
|
||||
sql = """
|
||||
SELECT user_id, username, password_hash, real_name, user_type,
|
||||
student_id, status, need_change_password, last_login_time, last_login_ip
|
||||
FROM users
|
||||
WHERE username = %s AND status = 1
|
||||
"""
|
||||
return await execute_one(sql, (username,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_user_id(user_id: int) -> dict:
|
||||
"""根据用户ID获取用户"""
|
||||
sql = """
|
||||
SELECT user_id, username, real_name, user_type, student_id,
|
||||
need_change_password, status
|
||||
FROM users
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (user_id,))
|
||||
|
||||
@staticmethod
|
||||
async def create_student(username: str, password: str, real_name: str, student_id: int) -> int:
|
||||
"""创建学生账号"""
|
||||
password_hash = security.sha1_md5_password(password)
|
||||
sql = """
|
||||
INSERT INTO users (username, password_hash, real_name, user_type, student_id, need_change_password)
|
||||
VALUES (%s, %s, %s, 'student', %s, 1)
|
||||
"""
|
||||
return await execute_insert(sql, (username, password_hash, real_name, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def create_parent(username: str, password: str, real_name: str, student_id: int) -> int:
|
||||
"""创建家长账号"""
|
||||
password_hash = security.sha1_md5_password(password)
|
||||
sql = """
|
||||
INSERT INTO users (username, password_hash, real_name, user_type, student_id, need_change_password)
|
||||
VALUES (%s, %s, %s, 'parent', %s, 0)
|
||||
"""
|
||||
return await execute_insert(sql, (username, password_hash, real_name, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def create_admin(username: str, password: str, real_name: str) -> int:
|
||||
"""创建管理员账号"""
|
||||
password_hash = security.sha1_md5_password(password)
|
||||
sql = """
|
||||
INSERT INTO users (username, password_hash, real_name, user_type, need_change_password)
|
||||
VALUES (%s, %s, %s, 'admin', 1)
|
||||
"""
|
||||
return await execute_insert(sql, (username, password_hash, real_name))
|
||||
|
||||
@staticmethod
|
||||
async def update_password(user_id: int, new_password: str) -> bool:
|
||||
"""更新密码"""
|
||||
password_hash = security.sha1_md5_password(new_password)
|
||||
sql = """
|
||||
UPDATE users
|
||||
SET password_hash = %s, need_change_password = 0
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
result = await execute_update(sql, (password_hash, user_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def update_last_login(user_id: int, ip: str) -> None:
|
||||
"""更新最后登录信息"""
|
||||
sql = """
|
||||
UPDATE users
|
||||
SET last_login_time = NOW(), last_login_ip = %s
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
await execute_update(sql, (ip, user_id))
|
||||
|
||||
@staticmethod
|
||||
async def check_username_exists(username: str) -> bool:
|
||||
"""检查用户名是否存在"""
|
||||
sql = "SELECT 1 FROM users WHERE username = %s"
|
||||
result = await execute_one(sql, (username,))
|
||||
return result is not None
|
||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
python-dotenv==1.0.0
|
||||
aiomysql==0.2.0
|
||||
redis==5.0.1
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
loguru==0.7.2
|
||||
11
backend/routes/__init__.py
Normal file
11
backend/routes/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
64
backend/routes/admin.py
Normal file
64
backend/routes/admin.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 管理端路由
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# 在 admin.py 中修改导入接口
|
||||
|
||||
@router.post("/students/import")
|
||||
async def import_students(
|
||||
request: Request,
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
"""
|
||||
批量导入学生(JSON格式)
|
||||
初始操行分默认为60分
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 检查权限(仅班主任)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可导入学生", code=403)
|
||||
|
||||
# 检查文件大小
|
||||
file_size = 0
|
||||
content = await file.read()
|
||||
file_size = len(content)
|
||||
|
||||
if file_size > settings.MAX_UPLOAD_SIZE:
|
||||
return error_response(message=f"文件大小不能超过{settings.MAX_UPLOAD_SIZE // 1024 // 1024}MB")
|
||||
|
||||
# 检查文件扩展名
|
||||
filename = file.filename or ""
|
||||
extension = filename.split('.')[-1].lower() if '.' in filename else ''
|
||||
if extension not in settings.ALLOWED_EXTENSIONS:
|
||||
return error_response(message=f"不支持的文件类型,仅支持 {', '.join(settings.ALLOWED_EXTENSIONS)}")
|
||||
|
||||
# 解析JSON
|
||||
try:
|
||||
import json
|
||||
data = json.loads(content.decode('utf-8'))
|
||||
students = data.get("students", [])
|
||||
except json.JSONDecodeError as e:
|
||||
return error_response(message=f"JSON格式错误: {str(e)}")
|
||||
except UnicodeDecodeError:
|
||||
return error_response(message="文件编码错误,请使用UTF-8编码")
|
||||
|
||||
if not students:
|
||||
return error_response(message="文件中没有学生数据")
|
||||
|
||||
# 导入学生(初始操行分60分)
|
||||
result = await AdminService.import_students(
|
||||
students=students,
|
||||
operator_id=user["user_id"],
|
||||
initial_points=60
|
||||
)
|
||||
|
||||
return success_response(data=result, message=f"导入完成: 成功{result['success_count']}人,失败{result['failed_count']}人")
|
||||
100
backend/routes/auth.py
Normal file
100
backend/routes/auth.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from typing import Dict, Any
|
||||
|
||||
from schemas.auth import LoginRequest, ChangePasswordRequest
|
||||
from services.auth_service import AuthService
|
||||
from middleware.permission import get_current_user
|
||||
from utils.response import success_response, error_response, unauthorized_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(request: LoginRequest, http_request: Request):
|
||||
"""
|
||||
用户登录
|
||||
"""
|
||||
# 获取客户端IP
|
||||
client_ip = http_request.client.host
|
||||
|
||||
result = await AuthService.login(
|
||||
username=request.username,
|
||||
password=request.password,
|
||||
ip=client_ip
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(
|
||||
data={
|
||||
"token": result["token"],
|
||||
"user_id": result["user_id"],
|
||||
"username": result["username"],
|
||||
"real_name": result["real_name"],
|
||||
"user_type": result["user_type"],
|
||||
"need_change_password": result["need_change_password"],
|
||||
"redirect": result["redirect"]
|
||||
},
|
||||
message="登录成功"
|
||||
)
|
||||
else:
|
||||
return error_response(message=result["message"], code=401)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
"""
|
||||
用户登出
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
result = await AuthService.logout(user["user_id"])
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="登出成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(request: Request, req: ChangePasswordRequest):
|
||||
"""
|
||||
修改密码
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await AuthService.change_password(
|
||||
user_id=user["user_id"],
|
||||
old_password=req.old_password,
|
||||
new_password=req.new_password
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="密码修改成功,请重新登录")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user_info(request: Request):
|
||||
"""
|
||||
获取当前用户信息
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 获取用户详细信息
|
||||
from services.auth_service import AuthService
|
||||
user_info = await AuthService.get_user_info(user["user_id"])
|
||||
|
||||
return success_response(data=user_info)
|
||||
69
backend/routes/debug.py
Normal file
69
backend/routes/debug.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from config import settings
|
||||
from services.admin_service import AdminService
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AddAdminDebugRequest(BaseModel):
|
||||
"""添加管理员请求"""
|
||||
username: str
|
||||
password: str
|
||||
real_name: str
|
||||
role_type: str # 班主任/班长/科代表/考勤委员/劳动委员
|
||||
class_id: int
|
||||
subject_id: Optional[int] = None
|
||||
|
||||
|
||||
@router.post(settings.DEBUG_PATH)
|
||||
async def debug_add_admin(request: Request, req: AddAdminDebugRequest):
|
||||
"""
|
||||
调试入口 - 添加第一批管理员
|
||||
注意:此接口仅用于首次部署,使用后建议注释掉此路由
|
||||
"""
|
||||
# 检查是否已存在管理员
|
||||
from models.user import UserModel
|
||||
existing = await UserModel.get_by_username(req.username)
|
||||
if existing:
|
||||
return error_response(message="用户名已存在")
|
||||
|
||||
# 创建管理员账号
|
||||
result = await AdminService.add_admin(
|
||||
username=req.username,
|
||||
real_name=req.real_name,
|
||||
password=req.password,
|
||||
role_type=req.role_type,
|
||||
class_id=req.class_id,
|
||||
subject_id=req.subject_id,
|
||||
operator_id=0 # 系统添加
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
logger.info(f"调试入口创建管理员: {req.username} ({req.role_type})")
|
||||
return success_response(
|
||||
data={
|
||||
"username": req.username,
|
||||
"password": req.password,
|
||||
"role_type": req.role_type
|
||||
},
|
||||
message=f"管理员 {req.username} 创建成功"
|
||||
)
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
66
backend/routes/parent.py
Normal file
66
backend/routes/parent.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user
|
||||
from services.parent_service import ParentService
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/child/conduct")
|
||||
async def get_child_conduct(request: Request):
|
||||
"""
|
||||
获取子女操行分(仅总分)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "parent":
|
||||
return error_response(message="仅限家长访问", code=403)
|
||||
|
||||
result = await ParentService.get_child_conduct(user["user_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/child/homework")
|
||||
async def get_child_homework(request: Request):
|
||||
"""
|
||||
获取子女作业情况
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "parent":
|
||||
return error_response(message="仅限家长访问", code=403)
|
||||
|
||||
result = await ParentService.get_child_homework(user["user_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/child/attendance")
|
||||
async def get_child_attendance(request: Request):
|
||||
"""
|
||||
获取子女考勤记录
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "parent":
|
||||
return error_response(message="仅限家长访问", code=403)
|
||||
|
||||
result = await ParentService.get_child_attendance(user["user_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
120
backend/routes/student.py
Normal file
120
backend/routes/student.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user
|
||||
from services.student_service import StudentService
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/conduct/{student_id}")
|
||||
async def get_conduct_history(
|
||||
request: Request,
|
||||
student_id: int,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
"""
|
||||
获取学生操行分历史
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 权限检查:只能查看自己的信息(学生)或同班(管理员)
|
||||
if user["user_type"] == "student" and user["student_id"] != student_id:
|
||||
return error_response(message="无权查看其他学生信息", code=403)
|
||||
|
||||
result = await StudentService.get_conduct_history(
|
||||
student_id=student_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/homework/{student_id}")
|
||||
async def get_homework_status(request: Request, student_id: int):
|
||||
"""
|
||||
获取学生作业情况
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 权限检查
|
||||
if user["user_type"] == "student" and user["student_id"] != student_id:
|
||||
return error_response(message="无权查看其他学生信息", code=403)
|
||||
|
||||
result = await StudentService.get_homework_status(student_id)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/attendance/{student_id}")
|
||||
async def get_attendance_records(
|
||||
request: Request,
|
||||
student_id: int,
|
||||
month: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
获取学生考勤记录
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 权限检查
|
||||
if user["user_type"] == "student" and user["student_id"] != student_id:
|
||||
return error_response(message="无权查看其他学生信息", code=403)
|
||||
|
||||
result = await StudentService.get_attendance_records(
|
||||
student_id=student_id,
|
||||
month=month
|
||||
)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/ranking")
|
||||
async def get_ranking(
|
||||
request: Request,
|
||||
class_id: Optional[int] = None,
|
||||
limit: int = Query(50, ge=1, le=100)
|
||||
):
|
||||
"""
|
||||
获取操行分排行榜
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await StudentService.get_ranking(
|
||||
user_id=user["user_id"],
|
||||
class_id=class_id,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/my-info")
|
||||
async def get_my_info(request: Request):
|
||||
"""
|
||||
获取当前学生个人信息
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "student":
|
||||
return error_response(message="仅限学生访问", code=403)
|
||||
|
||||
result = await StudentService.get_student_info(user["student_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
105
backend/routes/subject.py
Normal file
105
backend/routes/subject.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user, PermissionChecker
|
||||
from services.subject_service import SubjectService
|
||||
from schemas.subject import CreateSubjectRequest, UpdateSubjectRequest
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def get_subjects(
|
||||
request: Request,
|
||||
is_active: Optional[bool] = None
|
||||
):
|
||||
"""
|
||||
获取科目列表
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await SubjectService.get_subjects(is_active=is_active)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_subject(request: Request, req: CreateSubjectRequest):
|
||||
"""
|
||||
创建科目(班主任)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可创建科目", code=403)
|
||||
|
||||
result = await SubjectService.create_subject(
|
||||
subject_name=req.subject_name,
|
||||
subject_code=req.subject_code,
|
||||
sort_order=req.sort_order
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(data=result, message="科目创建成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.put("/update/{subject_id}")
|
||||
async def update_subject(
|
||||
request: Request,
|
||||
subject_id: int,
|
||||
req: UpdateSubjectRequest
|
||||
):
|
||||
"""
|
||||
更新科目(班主任)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可更新科目", code=403)
|
||||
|
||||
result = await SubjectService.update_subject(
|
||||
subject_id=subject_id,
|
||||
**req.dict(exclude_none=True)
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="科目更新成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.delete("/delete/{subject_id}")
|
||||
async def delete_subject(request: Request, subject_id: int):
|
||||
"""
|
||||
删除科目(软删除,班主任)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可删除科目", code=403)
|
||||
|
||||
result = await SubjectService.delete_subject(subject_id)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="科目已禁用")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
11
backend/schemas/__init__.py
Normal file
11
backend/schemas/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
81
backend/schemas/admin.py
Normal file
81
backend/schemas/admin.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class AddPointsRequest(BaseModel):
|
||||
"""加减分请求"""
|
||||
student_ids: List[int] = Field(..., description="学生ID列表")
|
||||
points_change: int = Field(..., description="分数变动")
|
||||
reason: str = Field(..., min_length=1, max_length=255, description="原因")
|
||||
|
||||
|
||||
class AddPointsResponse(BaseModel):
|
||||
"""加减分响应"""
|
||||
success_count: int
|
||||
fail_count: int
|
||||
details: List[dict]
|
||||
|
||||
|
||||
class RevokeRequest(BaseModel):
|
||||
"""撤销请求"""
|
||||
record_id: int = Field(..., description="记录ID")
|
||||
|
||||
|
||||
class ImportStudentsRequest(BaseModel):
|
||||
"""导入学生请求"""
|
||||
students: List[dict] = Field(..., description="学生列表")
|
||||
|
||||
|
||||
class ImportResult(BaseModel):
|
||||
"""导入结果"""
|
||||
total: int
|
||||
success: int
|
||||
failed: int
|
||||
errors: List[str]
|
||||
|
||||
|
||||
class AddAdminRequest(BaseModel):
|
||||
"""添加管理员请求"""
|
||||
username: str = Field(..., min_length=1, max_length=50, description="登录账号")
|
||||
real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名")
|
||||
password: Optional[str] = Field(None, description="密码(不填则自动生成)")
|
||||
role_type: str = Field(..., description="角色类型")
|
||||
class_id: int = Field(..., description="班级ID")
|
||||
subject_id: Optional[int] = Field(None, description="科目ID(科代表需要)")
|
||||
|
||||
|
||||
class AddAdminResponse(BaseModel):
|
||||
"""添加管理员响应"""
|
||||
success: bool
|
||||
username: str
|
||||
password: Optional[str] = None
|
||||
message: str
|
||||
|
||||
|
||||
class UpdateHomeworkStatusRequest(BaseModel):
|
||||
"""更新作业状态请求"""
|
||||
submission_id: int
|
||||
status: str
|
||||
comments: Optional[str] = None
|
||||
apply_deduction: bool = False
|
||||
|
||||
|
||||
class AddAttendanceRequest(BaseModel):
|
||||
"""添加考勤请求"""
|
||||
student_id: int
|
||||
date: date
|
||||
status: str
|
||||
reason: Optional[str] = None
|
||||
apply_deduction: bool = False
|
||||
54
backend/schemas/auth.py
Normal file
54
backend/schemas/auth.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""登录请求"""
|
||||
username: str = Field(..., min_length=1, max_length=50, description="用户名")
|
||||
password: str = Field(..., min_length=1, max_length=50, description="密码")
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""登录响应"""
|
||||
success: bool
|
||||
token: str
|
||||
user_id: int
|
||||
username: str
|
||||
real_name: str
|
||||
user_type: str
|
||||
need_change_password: bool
|
||||
redirect: str
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
"""修改密码请求"""
|
||||
old_password: str = Field(..., min_length=1, max_length=50, description="原密码")
|
||||
new_password: str = Field(..., min_length=6, max_length=20, description="新密码")
|
||||
|
||||
|
||||
class ChangePasswordResponse(BaseModel):
|
||||
"""修改密码响应"""
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""用户信息"""
|
||||
user_id: int
|
||||
username: str
|
||||
real_name: str
|
||||
user_type: str
|
||||
student_id: Optional[int] = None
|
||||
role: Optional[str] = None
|
||||
need_change_password: bool
|
||||
79
backend/schemas/student.py
Normal file
79
backend/schemas/student.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class StudentInfo(BaseModel):
|
||||
"""学生信息"""
|
||||
student_id: int
|
||||
student_no: str
|
||||
name: str
|
||||
class_id: int
|
||||
class_name: Optional[str] = None
|
||||
total_points: int
|
||||
parent_phone: Optional[str] = None
|
||||
status: int
|
||||
|
||||
|
||||
class ConductRecord(BaseModel):
|
||||
"""操行分记录"""
|
||||
record_id: int
|
||||
student_id: int
|
||||
student_name: Optional[str] = None
|
||||
points_change: int
|
||||
reason: str
|
||||
recorder_id: int
|
||||
recorder_name: str
|
||||
related_type: str
|
||||
is_revoked: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ConductHistoryResponse(BaseModel):
|
||||
"""操行分历史响应"""
|
||||
student_id: int
|
||||
student_name: str
|
||||
total_points: int
|
||||
records: List[ConductRecord]
|
||||
|
||||
|
||||
class HomeworkSubmission(BaseModel):
|
||||
"""作业提交情况"""
|
||||
assignment_id: int
|
||||
title: str
|
||||
subject: str
|
||||
deadline: date
|
||||
status: str
|
||||
submit_time: Optional[datetime] = None
|
||||
comments: Optional[str] = None
|
||||
deduction_applied: bool
|
||||
|
||||
|
||||
class AttendanceRecord(BaseModel):
|
||||
"""考勤记录"""
|
||||
attendance_id: int
|
||||
date: date
|
||||
status: str
|
||||
reason: Optional[str] = None
|
||||
deduction_applied: bool
|
||||
|
||||
|
||||
class StudentRanking(BaseModel):
|
||||
"""学生排行"""
|
||||
student_id: int
|
||||
student_no: str
|
||||
name: str
|
||||
class_name: str
|
||||
total_points: int
|
||||
rank_in_class: int
|
||||
43
backend/schemas/subject.py
Normal file
43
backend/schemas/subject.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class SubjectInfo(BaseModel):
|
||||
"""科目信息"""
|
||||
subject_id: int
|
||||
subject_name: str
|
||||
subject_code: Optional[str] = None
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
|
||||
|
||||
class CreateSubjectRequest(BaseModel):
|
||||
"""创建科目请求"""
|
||||
subject_name: str = Field(..., min_length=1, max_length=50, description="科目名称")
|
||||
subject_code: Optional[str] = Field(None, max_length=20, description="科目代码")
|
||||
sort_order: int = Field(0, description="排序序号")
|
||||
|
||||
|
||||
class UpdateSubjectRequest(BaseModel):
|
||||
"""更新科目请求"""
|
||||
subject_name: Optional[str] = Field(None, max_length=50, description="科目名称")
|
||||
subject_code: Optional[str] = Field(None, max_length=20, description="科目代码")
|
||||
is_active: Optional[bool] = Field(None, description="是否启用")
|
||||
sort_order: Optional[int] = Field(None, description="排序序号")
|
||||
|
||||
|
||||
class SubjectListResponse(BaseModel):
|
||||
"""科目列表响应"""
|
||||
subjects: List[SubjectInfo]
|
||||
total: int
|
||||
11
backend/services/__init__.py
Normal file
11
backend/services/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
298
backend/services/admin_service.py
Normal file
298
backend/services/admin_service.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 管理员服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.admin_role import AdminRoleModel
|
||||
from utils.security import security
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AdminService:
|
||||
"""管理员服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_students(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
search: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取所有学生列表(单班级)"""
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
sql = """
|
||||
SELECT student_id, student_no, name, total_points, parent_phone, status
|
||||
FROM students
|
||||
WHERE status = 1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if search:
|
||||
sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
||||
params.extend([f"%{search}%", f"%{search}%"])
|
||||
|
||||
sql += " ORDER BY student_no LIMIT %s OFFSET %s"
|
||||
params.extend([page_size, offset])
|
||||
|
||||
students = await execute_query(sql, tuple(params))
|
||||
|
||||
# 获取总数
|
||||
count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1"
|
||||
if search:
|
||||
count_sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
||||
total_result = await execute_one(count_sql, (f"%{search}%", f"%{search}%"))
|
||||
else:
|
||||
total_result = await execute_one(count_sql)
|
||||
|
||||
total = total_result["total"] if total_result else 0
|
||||
|
||||
return {
|
||||
"students": students,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def import_students(
|
||||
students: List[Dict],
|
||||
operator_id: int,
|
||||
initial_points: int = 60
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量导入学生(单班级版本)
|
||||
初始操行分默认60分
|
||||
"""
|
||||
results = []
|
||||
success_count = 0
|
||||
|
||||
for student in students:
|
||||
student_no = student.get("student_no", "").strip()
|
||||
name = student.get("name", "").strip()
|
||||
parent_phone = student.get("parent_phone", "").strip()
|
||||
password = student.get("password", "").strip()
|
||||
|
||||
# 验证必填字段
|
||||
if not student_no or not name:
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "学号或姓名不能为空"
|
||||
})
|
||||
continue
|
||||
|
||||
# 验证学号格式
|
||||
if not security.validate_student_no(student_no):
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "学号格式错误(4-20位字母数字组合)"
|
||||
})
|
||||
continue
|
||||
|
||||
# 验证手机号格式(如果有)
|
||||
if parent_phone and not security.validate_phone(parent_phone):
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "手机号格式错误"
|
||||
})
|
||||
continue
|
||||
|
||||
# 检查学号是否已存在
|
||||
existing = await StudentModel.get_by_student_no(student_no)
|
||||
if existing:
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "学号已存在"
|
||||
})
|
||||
continue
|
||||
|
||||
# 设置初始密码
|
||||
init_password = password if password else "123456"
|
||||
|
||||
# 验证密码强度(可选)
|
||||
is_valid, msg = security.validate_password_strength(init_password)
|
||||
if not is_valid:
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": f"密码不符合要求: {msg}"
|
||||
})
|
||||
continue
|
||||
|
||||
# 创建学生(初始操行分60分)
|
||||
student_id = await StudentModel.create(
|
||||
student_no=student_no,
|
||||
name=name,
|
||||
class_id=1, # 单班级,固定为1
|
||||
parent_phone=parent_phone if parent_phone else None,
|
||||
initial_points=initial_points
|
||||
)
|
||||
|
||||
# 创建学生账号
|
||||
await UserModel.create_student(
|
||||
username=student_no,
|
||||
password=init_password,
|
||||
real_name=name,
|
||||
student_id=student_id
|
||||
)
|
||||
|
||||
# 创建家长账号(如果有手机号)
|
||||
if parent_phone:
|
||||
parent_exists = await UserModel.get_by_username(parent_phone)
|
||||
if not parent_exists:
|
||||
parent_name = f"{name}家长"
|
||||
await UserModel.create_parent(
|
||||
username=parent_phone,
|
||||
password=init_password,
|
||||
real_name=parent_name,
|
||||
student_id=student_id
|
||||
)
|
||||
else:
|
||||
# 手机号已被占用
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": True,
|
||||
"student_id": student_id,
|
||||
"warning": f"家长手机号 {parent_phone} 已被其他账号使用,未创建家长账号"
|
||||
})
|
||||
success_count += 1
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": True,
|
||||
"student_id": student_id,
|
||||
"parent_phone": parent_phone if parent_phone else None
|
||||
})
|
||||
success_count += 1
|
||||
|
||||
logger.info(f"用户[{operator_id}] 导入学生: {student_no} - {name} (初始分:{initial_points})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"total": len(students),
|
||||
"success_count": success_count,
|
||||
"failed_count": len(students) - success_count,
|
||||
"results": results
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def add_student(
|
||||
student_no: str,
|
||||
name: str,
|
||||
parent_phone: Optional[str],
|
||||
operator_id: int,
|
||||
initial_points: int = 60
|
||||
) -> Dict[str, Any]:
|
||||
"""新增学生"""
|
||||
# 验证学号格式
|
||||
if not security.validate_student_no(student_no):
|
||||
return {"success": False, "message": "学号格式错误(4-20位字母数字组合)"}
|
||||
|
||||
# 验证手机号格式(如果有)
|
||||
if parent_phone and not security.validate_phone(parent_phone):
|
||||
return {"success": False, "message": "手机号格式错误"}
|
||||
|
||||
# 检查学号是否已存在
|
||||
existing = await StudentModel.get_by_student_no(student_no)
|
||||
if existing:
|
||||
return {"success": False, "message": "学号已存在"}
|
||||
|
||||
# 创建学生(初始操行分60分)
|
||||
student_id = await StudentModel.create(
|
||||
student_no=student_no,
|
||||
name=name,
|
||||
class_id=1, # 单班级,固定为1
|
||||
parent_phone=parent_phone if parent_phone else None,
|
||||
initial_points=initial_points
|
||||
)
|
||||
|
||||
# 创建学生账号
|
||||
await UserModel.create_student(
|
||||
username=student_no,
|
||||
password="123456",
|
||||
real_name=name,
|
||||
student_id=student_id
|
||||
)
|
||||
|
||||
# 创建家长账号
|
||||
if parent_phone:
|
||||
parent_exists = await UserModel.get_by_username(parent_phone)
|
||||
if not parent_exists:
|
||||
await UserModel.create_parent(
|
||||
username=parent_phone,
|
||||
password="123456",
|
||||
real_name=f"{name}家长",
|
||||
student_id=student_id
|
||||
)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 新增学生: {student_no} - {name}")
|
||||
|
||||
return {"success": True, "student_id": student_id, "student_no": student_no, "name": name}
|
||||
|
||||
@staticmethod
|
||||
async def add_admin(
|
||||
username: str,
|
||||
real_name: str,
|
||||
password: Optional[str],
|
||||
role_type: str,
|
||||
operator_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""添加管理员(单班级)"""
|
||||
# 检查用户名是否已存在
|
||||
existing = await UserModel.get_by_username(username)
|
||||
if existing:
|
||||
return {"success": False, "message": "用户名已存在"}
|
||||
|
||||
# 生成随机密码(如果未提供)
|
||||
if not password:
|
||||
password = security.generate_random_password()
|
||||
|
||||
# 创建管理员账号
|
||||
user_id = await UserModel.create_admin(
|
||||
username=username,
|
||||
password=password,
|
||||
real_name=real_name
|
||||
)
|
||||
|
||||
# 分配角色(班级ID固定为1)
|
||||
await AdminRoleModel.create(
|
||||
user_id=user_id,
|
||||
role_type=role_type,
|
||||
class_id=1, # 单班级,固定为1
|
||||
subject_id=None # 单班级版本暂不支持科目关联
|
||||
)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 添加管理员: {username} ({role_type})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"role_type": role_type
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_admins() -> Dict[str, Any]:
|
||||
"""获取管理员列表(单班级)"""
|
||||
admins = await AdminRoleModel.get_by_class(1) # 班级ID固定为1
|
||||
|
||||
return {"admins": admins}
|
||||
114
backend/services/attendance_service.py
Normal file
114
backend/services/attendance_service.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.attendance import AttendanceModel
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AttendanceService:
|
||||
"""考勤服务"""
|
||||
|
||||
@staticmethod
|
||||
async def add_attendance(
|
||||
student_id: int,
|
||||
date: str,
|
||||
status: str,
|
||||
reason: Optional[str],
|
||||
apply_deduction: bool,
|
||||
recorder_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""添加考勤记录"""
|
||||
# 检查权限
|
||||
role = await PermissionChecker.get_user_role(recorder_id)
|
||||
if role not in ["班主任", "考勤委员"]:
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
# 检查是否同班级
|
||||
can_manage = await PermissionChecker.check_can_manage_student(recorder_id, student_id)
|
||||
if not can_manage:
|
||||
return {"success": False, "message": "无权操作该学生"}
|
||||
|
||||
# 添加考勤记录
|
||||
attendance_id = await AttendanceModel.create_record(
|
||||
student_id=student_id,
|
||||
date=date,
|
||||
status=status,
|
||||
reason=reason,
|
||||
recorder_id=recorder_id
|
||||
)
|
||||
|
||||
if not attendance_id:
|
||||
return {"success": False, "message": "添加考勤记录失败"}
|
||||
|
||||
# 应用扣分
|
||||
if apply_deduction and status in ["absent", "late", "leave"]:
|
||||
# 确定扣分数值
|
||||
if status == "absent":
|
||||
points_change = -settings.DEDUCTION_ATTENDANCE_ABSENT
|
||||
elif status == "late":
|
||||
points_change = -settings.DEDUCTION_ATTENDANCE_LATE
|
||||
else:
|
||||
points_change = -settings.DEDUCTION_ATTENDANCE_LEAVE
|
||||
|
||||
# 创建扣分记录
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if student:
|
||||
await ConductModel.create_record(
|
||||
student_id=student_id,
|
||||
points_change=points_change,
|
||||
reason=f"考勤异常: {status}",
|
||||
recorder_id=recorder_id,
|
||||
related_type="attendance",
|
||||
related_id=attendance_id
|
||||
)
|
||||
|
||||
# 标记已应用扣分
|
||||
await AttendanceModel.mark_deduction_applied(attendance_id)
|
||||
|
||||
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status}")
|
||||
|
||||
return {"success": True, "message": "考勤记录添加成功"}
|
||||
|
||||
@staticmethod
|
||||
async def get_records(
|
||||
user_id: int,
|
||||
date: Optional[str] = None,
|
||||
student_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取考勤记录"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if role in ["班主任", "考勤委员"]:
|
||||
class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
records = await AttendanceModel.get_class_records(
|
||||
class_id=class_id,
|
||||
date=date,
|
||||
student_id=student_id
|
||||
)
|
||||
elif student_id:
|
||||
# 查看指定学生
|
||||
can_manage = await PermissionChecker.check_can_manage_student(user_id, student_id)
|
||||
if not can_manage:
|
||||
return {"error": "无权查看该学生记录"}
|
||||
records = await AttendanceModel.get_student_records(student_id)
|
||||
else:
|
||||
records = []
|
||||
|
||||
return {"records": records}
|
||||
169
backend/services/auth_service.py
Normal file
169
backend/services/auth_service.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.admin_role import AdminRoleModel
|
||||
from utils.security import security
|
||||
from utils.jwt_handler import jwt_handler
|
||||
from utils.redis_client import RedisClient
|
||||
from utils.database import execute_update
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""认证服务"""
|
||||
|
||||
@staticmethod
|
||||
async def login(username: str, password: str, ip: str) -> Dict[str, Any]:
|
||||
"""
|
||||
用户登录
|
||||
"""
|
||||
# 检查登录失败次数
|
||||
attempts = await RedisClient.get(f"login_attempts:{username}")
|
||||
if attempts and int(attempts) >= 5:
|
||||
return {"success": False, "message": "登录失败次数过多,请15分钟后重试"}
|
||||
|
||||
# 获取用户信息
|
||||
user = await UserModel.get_by_username(username)
|
||||
|
||||
if not user:
|
||||
await RedisClient.set_login_attempts(username)
|
||||
return {"success": False, "message": "用户名或密码错误"}
|
||||
|
||||
# 验证密码
|
||||
if not security.verify_password(password, user["password_hash"]):
|
||||
await RedisClient.set_login_attempts(username)
|
||||
return {"success": False, "message": "用户名或密码错误"}
|
||||
|
||||
# 检查账号状态
|
||||
if user["status"] != 1:
|
||||
return {"success": False, "message": "账号已被禁用"}
|
||||
|
||||
# 清除登录失败记录
|
||||
await RedisClient.clear_login_attempts(username)
|
||||
|
||||
# 更新最后登录信息
|
||||
await UserModel.update_last_login(user["user_id"], ip)
|
||||
|
||||
# 获取用户角色(如果是管理员)
|
||||
role = None
|
||||
if user["user_type"] == "admin":
|
||||
admin_role = await AdminRoleModel.get_by_user_id(user["user_id"])
|
||||
role = admin_role["role_type"] if admin_role else None
|
||||
|
||||
# 生成Token
|
||||
token = jwt_handler.create_token(
|
||||
user_id=user["user_id"],
|
||||
username=user["username"],
|
||||
user_type=user["user_type"],
|
||||
student_id=user["student_id"],
|
||||
role=role
|
||||
)
|
||||
|
||||
# 存储Token到Redis
|
||||
await RedisClient.set_user_token(user["user_id"], token)
|
||||
|
||||
# 确定跳转路径
|
||||
redirect = AuthService._get_redirect_path(user["user_type"], role)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user_id": user["user_id"],
|
||||
"username": user["username"],
|
||||
"real_name": user["real_name"],
|
||||
"user_type": user["user_type"],
|
||||
"need_change_password": user["need_change_password"] == 1,
|
||||
"redirect": redirect
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def logout(user_id: int) -> Dict[str, Any]:
|
||||
"""用户登出"""
|
||||
await RedisClient.delete_user_token(user_id)
|
||||
return {"success": True, "message": "登出成功"}
|
||||
|
||||
@staticmethod
|
||||
async def change_password(user_id: int, old_password: str, new_password: str) -> Dict[str, Any]:
|
||||
"""修改密码"""
|
||||
# 获取用户信息
|
||||
user = await UserModel.get_by_user_id(user_id)
|
||||
if not user:
|
||||
return {"success": False, "message": "用户不存在"}
|
||||
|
||||
# 验证原密码
|
||||
if not security.verify_password(old_password, user["password_hash"]):
|
||||
return {"success": False, "message": "原密码错误"}
|
||||
|
||||
# 验证新密码强度
|
||||
is_valid, msg = security.validate_password_strength(new_password)
|
||||
if not is_valid:
|
||||
return {"success": False, "message": msg}
|
||||
|
||||
# 更新密码
|
||||
result = await UserModel.update_password(user_id, new_password)
|
||||
|
||||
if result:
|
||||
# 清除所有Token
|
||||
await RedisClient.delete_user_token(user_id)
|
||||
return {"success": True, "message": "密码修改成功"}
|
||||
else:
|
||||
return {"success": False, "message": "密码修改失败"}
|
||||
|
||||
@staticmethod
|
||||
async def get_user_info(user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取用户信息"""
|
||||
user = await UserModel.get_by_user_id(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
result = {
|
||||
"user_id": user["user_id"],
|
||||
"username": user["username"],
|
||||
"real_name": user["real_name"],
|
||||
"user_type": user["user_type"],
|
||||
"need_change_password": user["need_change_password"] == 1
|
||||
}
|
||||
|
||||
# 获取学生信息
|
||||
if user["student_id"]:
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if student:
|
||||
result["student_no"] = student["student_no"]
|
||||
result["student_name"] = student["name"]
|
||||
result["class_id"] = student["class_id"]
|
||||
result["class_name"] = student["class_name"]
|
||||
result["total_points"] = student["total_points"]
|
||||
|
||||
# 获取管理员角色
|
||||
if user["user_type"] == "admin":
|
||||
admin_role = await AdminRoleModel.get_by_user_id(user_id)
|
||||
if admin_role:
|
||||
result["role"] = admin_role["role_type"]
|
||||
result["class_id"] = admin_role["class_id"]
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_redirect_path(user_type: str, role: str = None) -> str:
|
||||
"""获取跳转路径"""
|
||||
if user_type == "student":
|
||||
return "/student/dashboard.php"
|
||||
elif user_type == "parent":
|
||||
return "/parent/dashboard.php"
|
||||
else:
|
||||
return "/admin/dashboard.php"
|
||||
182
backend/services/conduct_service.py
Normal file
182
backend/services/conduct_service.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from models.user import UserModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConductService:
|
||||
"""操行分服务"""
|
||||
|
||||
@staticmethod
|
||||
async def add_points(
|
||||
student_ids: List[int],
|
||||
points_change: int,
|
||||
reason: str,
|
||||
recorder_id: int,
|
||||
recorder_name: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量加减分
|
||||
"""
|
||||
# 验证分值
|
||||
if points_change == 0:
|
||||
return {"success": False, "message": "分值不能为0"}
|
||||
|
||||
# 获取操作人角色
|
||||
role = await PermissionChecker.get_user_role(recorder_id)
|
||||
|
||||
# 权限验证
|
||||
if role == "班主任":
|
||||
# 班主任无限制
|
||||
pass
|
||||
elif role == "班长":
|
||||
# 班长限制 ±5分
|
||||
if points_change > settings.MONITOR_MAX_ADD or points_change < settings.MONITOR_MAX_SUBTRACT:
|
||||
return {"success": False, "message": f"班长单次只能加减{settings.MONITOR_MAX_ADD}分以内"}
|
||||
elif role == "劳动委员":
|
||||
# 劳动委员固定 ±1分
|
||||
if points_change not in [settings.LABOR_POINTS_ADD, settings.LABOR_POINTS_SUBTRACT]:
|
||||
return {"success": False, "message": "劳动委员只能进行±1分操作"}
|
||||
elif role in ["科代表", "考勤委员"]:
|
||||
# 科代表和考勤委员只能扣分
|
||||
if points_change > 0:
|
||||
return {"success": False, "message": "该角色只能进行扣分操作"}
|
||||
else:
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
# 批量处理
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
details = []
|
||||
|
||||
for student_id in student_ids:
|
||||
try:
|
||||
# 检查学生是否存在
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
details.append({"student_id": student_id, "error": "学生不存在"})
|
||||
fail_count += 1
|
||||
continue
|
||||
|
||||
# 创建记录
|
||||
record_id = await ConductModel.create_record(
|
||||
student_id=student_id,
|
||||
points_change=points_change,
|
||||
reason=reason,
|
||||
recorder_id=recorder_id,
|
||||
recorder_name=recorder_name
|
||||
)
|
||||
|
||||
details.append({"student_id": student_id, "success": True, "record_id": record_id})
|
||||
success_count += 1
|
||||
|
||||
logger.info(f"用户[{recorder_id}] 对学生[{student_id}] 进行 {points_change} 分操作")
|
||||
|
||||
except Exception as e:
|
||||
details.append({"student_id": student_id, "error": str(e)})
|
||||
fail_count += 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"success_count": success_count,
|
||||
"fail_count": fail_count,
|
||||
"details": details
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def revoke_record(record_id: int, revoker_id: int) -> Dict[str, Any]:
|
||||
"""撤销扣分记录"""
|
||||
# 检查权限
|
||||
can_revoke = await PermissionChecker.check_can_revoke(revoker_id, record_id)
|
||||
if not can_revoke:
|
||||
return {"success": False, "message": "无权撤销此记录"}
|
||||
|
||||
# 撤销记录
|
||||
result = await ConductModel.revoke_record(record_id, revoker_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"用户[{revoker_id}] 撤销了记录[{record_id}]")
|
||||
return {"success": True, "message": "撤销成功"}
|
||||
else:
|
||||
return {"success": False, "message": "撤销失败"}
|
||||
|
||||
@staticmethod
|
||||
async def get_history(
|
||||
user_id: int,
|
||||
student_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取历史记录"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 班主任/班长可查看全班
|
||||
if role in ["班主任", "班长"]:
|
||||
user_class = await PermissionChecker.get_user_class_id(user_id)
|
||||
records = await ConductModel.get_all_records(
|
||||
class_id=user_class,
|
||||
limit=page_size,
|
||||
offset=offset,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
# 获取总数
|
||||
from utils.database import execute_one
|
||||
count_sql = """
|
||||
SELECT COUNT(*) as total FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
WHERE s.class_id = %s AND cr.is_revoked = 0
|
||||
"""
|
||||
total_result = await execute_one(count_sql, (user_class,))
|
||||
total = total_result["total"] if total_result else 0
|
||||
|
||||
elif student_id:
|
||||
# 查看指定学生(需权限验证)
|
||||
can_manage = await PermissionChecker.check_can_manage_student(user_id, student_id)
|
||||
if not can_manage:
|
||||
return {"error": "无权查看该学生记录"}
|
||||
|
||||
records = await ConductModel.get_student_records(
|
||||
student_id=student_id,
|
||||
limit=page_size,
|
||||
offset=offset
|
||||
)
|
||||
total = len(await ConductModel.get_student_records(student_id, limit=10000))
|
||||
else:
|
||||
# 查看自己提交的记录
|
||||
records = await ConductModel.get_records_by_recorder(
|
||||
recorder_id=user_id,
|
||||
limit=page_size,
|
||||
offset=offset
|
||||
)
|
||||
total = len(await ConductModel.get_records_by_recorder(user_id, limit=10000))
|
||||
|
||||
return {
|
||||
"records": records,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
131
backend/services/homework_service.py
Normal file
131
backend/services/homework_service.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.homework import HomeworkModel
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HomeworkService:
|
||||
"""作业服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_assignments(user_id: int) -> Dict[str, Any]:
|
||||
"""获取作业列表"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if role == "班主任":
|
||||
class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
assignments = await HomeworkModel.get_assignments_by_class(class_id)
|
||||
elif role == "科代表":
|
||||
class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
|
||||
assignments = await HomeworkModel.get_assignments_by_subjects(class_id, subject_ids)
|
||||
else:
|
||||
assignments = []
|
||||
|
||||
return {"assignments": assignments}
|
||||
|
||||
@staticmethod
|
||||
async def create_assignment(
|
||||
subject_id: int,
|
||||
title: str,
|
||||
description: Optional[str],
|
||||
deadline: str,
|
||||
created_by: int
|
||||
) -> Dict[str, Any]:
|
||||
"""创建作业"""
|
||||
class_id = await PermissionChecker.get_user_class_id(created_by)
|
||||
|
||||
assignment_id = await HomeworkModel.create_assignment(
|
||||
class_id=class_id,
|
||||
subject_id=subject_id,
|
||||
title=title,
|
||||
description=description,
|
||||
deadline=deadline,
|
||||
created_by=created_by
|
||||
)
|
||||
|
||||
if assignment_id:
|
||||
logger.info(f"用户[{created_by}] 创建作业[{assignment_id}]: {title}")
|
||||
return {"success": True, "assignment_id": assignment_id}
|
||||
else:
|
||||
return {"success": False, "message": "创建作业失败"}
|
||||
|
||||
@staticmethod
|
||||
async def update_submission_status(
|
||||
submission_id: int,
|
||||
status: str,
|
||||
comments: Optional[str],
|
||||
apply_deduction: bool,
|
||||
operator_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""更新作业提交状态"""
|
||||
# 获取提交记录信息
|
||||
submission = await HomeworkModel.get_submission(submission_id)
|
||||
if not submission:
|
||||
return {"success": False, "message": "提交记录不存在"}
|
||||
|
||||
# 检查权限
|
||||
role = await PermissionChecker.get_user_role(operator_id)
|
||||
if role == "科代表":
|
||||
# 检查是否管理该科目
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(operator_id)
|
||||
if submission["subject_id"] not in subject_ids:
|
||||
return {"success": False, "message": "无权操作此作业"}
|
||||
elif role != "班主任":
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
# 更新状态
|
||||
result = await HomeworkModel.update_submission(
|
||||
submission_id=submission_id,
|
||||
status=status,
|
||||
comments=comments,
|
||||
updated_by=operator_id
|
||||
)
|
||||
|
||||
if not result:
|
||||
return {"success": False, "message": "更新失败"}
|
||||
|
||||
# 应用扣分
|
||||
if apply_deduction and status in ["not_submitted", "late"]:
|
||||
# 确定扣分数值
|
||||
if status == "not_submitted":
|
||||
points_change = -settings.DEDUCTION_HOMEWORK_NOT_SUBMIT
|
||||
else:
|
||||
points_change = -settings.DEDUCTION_HOMEWORK_LATE
|
||||
|
||||
# 创建扣分记录
|
||||
student = await StudentModel.get_by_id(submission["student_id"])
|
||||
if student:
|
||||
await ConductModel.create_record(
|
||||
student_id=submission["student_id"],
|
||||
points_change=points_change,
|
||||
reason=f"作业未提交/迟交: {submission['title']}",
|
||||
recorder_id=operator_id,
|
||||
related_type="homework",
|
||||
related_id=submission["assignment_id"]
|
||||
)
|
||||
|
||||
# 标记已应用扣分
|
||||
await HomeworkModel.mark_deduction_applied(submission_id)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 更新作业提交状态[{submission_id}] -> {status}")
|
||||
|
||||
return {"success": True, "message": "状态更新成功"}
|
||||
82
backend/services/parent_service.py
Normal file
82
backend/services/parent_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from models.homework import HomeworkModel
|
||||
from models.attendance import AttendanceModel
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ParentService:
|
||||
"""家长服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_child_conduct(parent_id: int) -> Dict[str, Any]:
|
||||
"""获取子女操行分(仅总分,家长端不显示详细记录)"""
|
||||
# 获取家长关联的学生
|
||||
user = await UserModel.get_by_user_id(parent_id)
|
||||
if not user or not user["student_id"]:
|
||||
return {"error": "未关联学生"}
|
||||
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"student_no": student["student_no"],
|
||||
"total_points": student["total_points"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_child_homework(parent_id: int) -> Dict[str, Any]:
|
||||
"""获取子女作业情况"""
|
||||
user = await UserModel.get_by_user_id(parent_id)
|
||||
if not user or not user["student_id"]:
|
||||
return {"error": "未关联学生"}
|
||||
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
homework = await HomeworkModel.get_student_homework(user["student_id"])
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"homework": homework
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_child_attendance(parent_id: int) -> Dict[str, Any]:
|
||||
"""获取子女考勤记录"""
|
||||
user = await UserModel.get_by_user_id(parent_id)
|
||||
if not user or not user["student_id"]:
|
||||
return {"error": "未关联学生"}
|
||||
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
records = await AttendanceModel.get_student_records(user["student_id"])
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"records": records
|
||||
}
|
||||
146
backend/services/student_service.py
Normal file
146
backend/services/student_service.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from models.homework import HomeworkModel
|
||||
from models.attendance import AttendanceModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class StudentService:
|
||||
"""学生服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_conduct_history(
|
||||
student_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""获取学生操行分历史(学生端显示,扣分项操作人显示为班主任)"""
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
records = await ConductModel.get_student_records(
|
||||
student_id=student_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
# 处理记录:扣分项的操作人统一显示为"班主任"
|
||||
for record in records:
|
||||
if record["points_change"] < 0: # 扣分项
|
||||
record["recorder_name"] = "班主任"
|
||||
# 加分项保持原操作人不变
|
||||
|
||||
return {
|
||||
"student_id": student_id,
|
||||
"student_name": student["name"],
|
||||
"total_points": student["total_points"],
|
||||
"records": records
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_homework_status(student_id: int) -> Dict[str, Any]:
|
||||
"""获取学生作业情况"""
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
homework = await HomeworkModel.get_student_homework(student_id)
|
||||
|
||||
# 统计
|
||||
total = len(homework)
|
||||
submitted = sum(1 for h in homework if h["status"] == "submitted")
|
||||
not_submitted = sum(1 for h in homework if h["status"] == "not_submitted")
|
||||
late = sum(1 for h in homework if h["status"] == "late")
|
||||
|
||||
return {
|
||||
"student_id": student_id,
|
||||
"student_name": student["name"],
|
||||
"statistics": {
|
||||
"total": total,
|
||||
"submitted": submitted,
|
||||
"not_submitted": not_submitted,
|
||||
"late": late
|
||||
},
|
||||
"homework": homework
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_attendance_records(
|
||||
student_id: int,
|
||||
month: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取学生考勤记录"""
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
records = await AttendanceModel.get_student_records(
|
||||
student_id=student_id,
|
||||
month=month
|
||||
)
|
||||
|
||||
# 统计
|
||||
present = sum(1 for r in records if r["status"] == "present")
|
||||
absent = sum(1 for r in records if r["status"] == "absent")
|
||||
late = sum(1 for r in records if r["status"] == "late")
|
||||
leave = sum(1 for r in records if r["status"] == "leave")
|
||||
|
||||
return {
|
||||
"student_id": student_id,
|
||||
"student_name": student["name"],
|
||||
"statistics": {
|
||||
"present": present,
|
||||
"absent": absent,
|
||||
"late": late,
|
||||
"leave": leave,
|
||||
"total": len(records)
|
||||
},
|
||||
"records": records
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_ranking(
|
||||
user_id: int,
|
||||
class_id: Optional[int] = None,
|
||||
limit: int = 50
|
||||
) -> Dict[str, Any]:
|
||||
"""获取排行榜"""
|
||||
# 如果未指定班级,获取用户所在班级
|
||||
if not class_id:
|
||||
user = await StudentModel.get_by_id(user_id) if user_id else None
|
||||
if user:
|
||||
class_id = user["class_id"]
|
||||
else:
|
||||
admin_class = await PermissionChecker.get_user_class_id(user_id)
|
||||
if admin_class:
|
||||
class_id = admin_class
|
||||
|
||||
ranking = await StudentModel.get_ranking(class_id=class_id, limit=limit)
|
||||
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"ranking": ranking
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_student_info(student_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取学生个人信息"""
|
||||
return await StudentModel.get_by_id(student_id)
|
||||
77
backend/services/subject_service.py
Normal file
77
backend/services/subject_service.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from models.subject import SubjectModel
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SubjectService:
|
||||
"""科目服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_subjects(is_active: Optional[bool] = None) -> Dict[str, Any]:
|
||||
"""获取科目列表"""
|
||||
subjects = await SubjectModel.get_all(is_active=is_active)
|
||||
|
||||
return {
|
||||
"subjects": subjects,
|
||||
"total": len(subjects)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def create_subject(
|
||||
subject_name: str,
|
||||
subject_code: Optional[str],
|
||||
sort_order: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""创建科目"""
|
||||
# 检查是否已存在
|
||||
existing = await SubjectModel.get_by_name(subject_name)
|
||||
if existing:
|
||||
return {"success": False, "message": "科目名称已存在"}
|
||||
|
||||
subject_id = await SubjectModel.create(
|
||||
subject_name=subject_name,
|
||||
subject_code=subject_code,
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
if subject_id:
|
||||
logger.info(f"创建科目: {subject_name}")
|
||||
return {"success": True, "subject_id": subject_id}
|
||||
else:
|
||||
return {"success": False, "message": "创建科目失败"}
|
||||
|
||||
@staticmethod
|
||||
async def update_subject(subject_id: int, **kwargs) -> Dict[str, Any]:
|
||||
"""更新科目"""
|
||||
result = await SubjectModel.update(subject_id, **kwargs)
|
||||
|
||||
if result:
|
||||
logger.info(f"更新科目: {subject_id}")
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": "更新科目失败"}
|
||||
|
||||
@staticmethod
|
||||
async def delete_subject(subject_id: int) -> Dict[str, Any]:
|
||||
"""删除科目(软删除)"""
|
||||
result = await SubjectModel.delete(subject_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"禁用科目: {subject_id}")
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": "禁用科目失败"}
|
||||
11
backend/utils/__init__.py
Normal file
11
backend/utils/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
136
backend/utils/database.py
Normal file
136
backend/utils/database.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import aiomysql
|
||||
from typing import Optional, Dict, Any, List
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 连接池实例
|
||||
_pool: Optional[aiomysql.Pool] = None
|
||||
|
||||
|
||||
async def init_db_pool() -> None:
|
||||
"""初始化数据库连接池"""
|
||||
global _pool
|
||||
try:
|
||||
_pool = await aiomysql.create_pool(
|
||||
host=settings.DB_HOST,
|
||||
port=settings.DB_PORT,
|
||||
user=settings.DB_USER,
|
||||
password=settings.DB_PASSWORD,
|
||||
db=settings.DB_NAME,
|
||||
minsize=1,
|
||||
maxsize=settings.DB_POOL_SIZE,
|
||||
maxsize=settings.DB_MAX_OVERFLOW,
|
||||
autocommit=False,
|
||||
charset='utf8mb4',
|
||||
cursorclass=aiomysql.DictCursor
|
||||
)
|
||||
logger.info("数据库连接池初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库连接池初始化失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def close_db_pool() -> None:
|
||||
"""关闭数据库连接池"""
|
||||
global _pool
|
||||
if _pool:
|
||||
_pool.close()
|
||||
await _pool.wait_closed()
|
||||
logger.info("数据库连接池已关闭")
|
||||
|
||||
|
||||
def get_pool() -> aiomysql.Pool:
|
||||
"""获取连接池实例"""
|
||||
if _pool is None:
|
||||
raise RuntimeError("数据库连接池未初始化")
|
||||
return _pool
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection():
|
||||
"""获取数据库连接(上下文管理器)"""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
yield cursor
|
||||
await conn.commit()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_transaction():
|
||||
"""获取事务连接"""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
try:
|
||||
yield cursor
|
||||
await conn.commit()
|
||||
except Exception:
|
||||
await conn.rollback()
|
||||
raise
|
||||
|
||||
|
||||
async def execute_query(sql: str, params: tuple = None) -> List[Dict[str, Any]]:
|
||||
"""执行查询SQL"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.execute(sql, params)
|
||||
return await cursor.fetchall()
|
||||
|
||||
|
||||
async def execute_one(sql: str, params: tuple = None) -> Optional[Dict[str, Any]]:
|
||||
"""执行查询SQL(单条)"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.execute(sql, params)
|
||||
return await cursor.fetchone()
|
||||
|
||||
|
||||
async def execute_insert(sql: str, params: tuple = None) -> int:
|
||||
"""执行插入SQL,返回自增ID"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.execute(sql, params)
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
async def execute_update(sql: str, params: tuple = None) -> int:
|
||||
"""执行更新SQL,返回影响行数"""
|
||||
async with get_connection() as cursor:
|
||||
result = await cursor.execute(sql, params)
|
||||
return result
|
||||
|
||||
|
||||
async def execute_many(sql: str, params_list: list) -> int:
|
||||
"""批量执行SQL"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.executemany(sql, params_list)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
async def call_procedure(proc_name: str, args: tuple = None) -> List[Dict[str, Any]]:
|
||||
"""调用存储过程"""
|
||||
async with get_connection() as cursor:
|
||||
if args:
|
||||
await cursor.callproc(proc_name, args)
|
||||
else:
|
||||
await cursor.callproc(proc_name)
|
||||
|
||||
# 获取结果
|
||||
result = []
|
||||
for result_set in cursor.fetchall():
|
||||
if result_set:
|
||||
result.extend(result_set)
|
||||
return result
|
||||
86
backend/utils/jwt_handler.py
Normal file
86
backend/utils/jwt_handler.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from jose import jwt, JWTError
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class JWTHandler:
|
||||
"""JWT Token处理类"""
|
||||
|
||||
@staticmethod
|
||||
def create_token(user_id: int, username: str, user_type: str, student_id: int = None, role: str = None) -> str:
|
||||
"""
|
||||
创建JWT Token
|
||||
"""
|
||||
payload = {
|
||||
'user_id': user_id,
|
||||
'username': username,
|
||||
'user_type': user_type,
|
||||
'student_id': student_id,
|
||||
'role': role,
|
||||
'exp': datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES),
|
||||
'iat': datetime.utcnow(),
|
||||
'iss': settings.APP_NAME
|
||||
}
|
||||
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithm=settings.JWT_ALGORITHM
|
||||
)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
验证JWT Token
|
||||
返回: 解码后的payload,失败返回None
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM],
|
||||
options={'verify_exp': True}
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("JWT Token已过期")
|
||||
return None
|
||||
except jwt.JWTError as e:
|
||||
logger.warning(f"JWT Token验证失败: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_user_id_from_token(token: str) -> Optional[int]:
|
||||
"""从Token中获取用户ID"""
|
||||
payload = JWTHandler.verify_token(token)
|
||||
if payload:
|
||||
return payload.get('user_id')
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_user_type_from_token(token: str) -> Optional[str]:
|
||||
"""从Token中获取用户类型"""
|
||||
payload = JWTHandler.verify_token(token)
|
||||
if payload:
|
||||
return payload.get('user_type')
|
||||
return None
|
||||
|
||||
|
||||
jwt_handler = JWTHandler()
|
||||
102
backend/utils/logger.py
Normal file
102
backend/utils/logger.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
from fastapi import Request
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR = Path(__file__).parent.parent / "logs"
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""配置日志系统"""
|
||||
|
||||
# 移除默认处理器
|
||||
logger.remove()
|
||||
|
||||
# 控制台输出(仅INFO及以上)
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan> - <level>{message}</level>",
|
||||
level=settings.LOG_LEVEL,
|
||||
colorize=True
|
||||
)
|
||||
|
||||
# 应用日志(轮转)
|
||||
logger.add(
|
||||
LOG_DIR / "app.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name} | {message}",
|
||||
rotation=settings.LOG_MAX_BYTES,
|
||||
retention=settings.LOG_RETENTION_DAYS,
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
level="DEBUG"
|
||||
)
|
||||
|
||||
# 错误日志(单独记录)
|
||||
logger.add(
|
||||
LOG_DIR / "error.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name} | {message}",
|
||||
rotation=settings.LOG_MAX_BYTES,
|
||||
retention=settings.LOG_RETENTION_DAYS * 2,
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
level="ERROR"
|
||||
)
|
||||
|
||||
# 访问日志
|
||||
logger.add(
|
||||
LOG_DIR / "access.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {message}",
|
||||
rotation="1 day",
|
||||
retention="90 days",
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
filter=lambda record: record["extra"].get("type") == "access"
|
||||
)
|
||||
|
||||
# 操作日志
|
||||
logger.add(
|
||||
LOG_DIR / "operation.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {message}",
|
||||
rotation=settings.LOG_MAX_BYTES,
|
||||
retention=settings.LOG_RETENTION_DAYS,
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
filter=lambda record: record["extra"].get("type") == "operation"
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str):
|
||||
"""获取日志记录器"""
|
||||
return logger.bind(name=name)
|
||||
|
||||
|
||||
def log_access(request: Request):
|
||||
"""记录访问日志"""
|
||||
logger.bind(type="access").info(f"{request.method} {request.url.path} - {request.client.host}")
|
||||
|
||||
|
||||
def log_operation(operator_id: int, operator_name: str, action: str, details: str = ""):
|
||||
"""记录操作日志"""
|
||||
logger.bind(type="operation").info(f"用户[{operator_id}:{operator_name}] 执行 {action} - {details}")
|
||||
|
||||
|
||||
# 导出logger
|
||||
__all__ = ["setup_logger", "get_logger", "log_access", "log_operation", "logger"]
|
||||
140
backend/utils/redus_client.py
Normal file
140
backend/utils/redus_client.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import redis.asyncio as redis
|
||||
from typing import Optional, Any
|
||||
import json
|
||||
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Redis客户端实例
|
||||
_redis_client: Optional[redis.Redis] = None
|
||||
|
||||
|
||||
async def init_redis_pool() -> None:
|
||||
"""初始化Redis连接池"""
|
||||
global _redis_client
|
||||
try:
|
||||
_redis_client = redis.from_url(
|
||||
settings.REDIS_URL,
|
||||
max_connections=settings.REDIS_MAX_CONNECTIONS,
|
||||
decode_responses=True
|
||||
)
|
||||
# 测试连接
|
||||
await _redis_client.ping()
|
||||
logger.info("Redis连接池初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"Redis连接池初始化失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def close_redis_pool() -> None:
|
||||
"""关闭Redis连接池"""
|
||||
global _redis_client
|
||||
if _redis_client:
|
||||
await _redis_client.close()
|
||||
logger.info("Redis连接池已关闭")
|
||||
|
||||
|
||||
def get_redis() -> redis.Redis:
|
||||
"""获取Redis客户端"""
|
||||
if _redis_client is None:
|
||||
raise RuntimeError("Redis客户端未初始化")
|
||||
return _redis_client
|
||||
|
||||
|
||||
class RedisClient:
|
||||
"""Redis操作封装类"""
|
||||
|
||||
@staticmethod
|
||||
async def set(key: str, value: Any, expire: int = None) -> bool:
|
||||
"""设置缓存"""
|
||||
client = get_redis()
|
||||
if isinstance(value, (dict, list)):
|
||||
value = json.dumps(value, ensure_ascii=False)
|
||||
else:
|
||||
value = str(value)
|
||||
|
||||
if expire:
|
||||
return await client.setex(key, expire, value)
|
||||
return await client.set(key, value)
|
||||
|
||||
@staticmethod
|
||||
async def get(key: str) -> Optional[str]:
|
||||
"""获取缓存"""
|
||||
client = get_redis()
|
||||
return await client.get(key)
|
||||
|
||||
@staticmethod
|
||||
async def get_json(key: str) -> Optional[Any]:
|
||||
"""获取JSON格式缓存"""
|
||||
value = await RedisClient.get(key)
|
||||
if value:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def delete(key: str) -> int:
|
||||
"""删除缓存"""
|
||||
client = get_redis()
|
||||
return await client.delete(key)
|
||||
|
||||
@staticmethod
|
||||
async def exists(key: str) -> bool:
|
||||
"""检查key是否存在"""
|
||||
client = get_redis()
|
||||
return await client.exists(key) > 0
|
||||
|
||||
@staticmethod
|
||||
async def expire(key: str, seconds: int) -> bool:
|
||||
"""设置过期时间"""
|
||||
client = get_redis()
|
||||
return await client.expire(key, seconds)
|
||||
|
||||
@staticmethod
|
||||
async def set_user_token(user_id: int, token: str, expire: int = None) -> bool:
|
||||
"""设置用户Token缓存"""
|
||||
key = f"user_token:{user_id}"
|
||||
expire = expire or settings.JWT_EXPIRE_MINUTES * 60
|
||||
return await RedisClient.set(key, token, expire)
|
||||
|
||||
@staticmethod
|
||||
async def get_user_token(user_id: int) -> Optional[str]:
|
||||
"""获取用户Token"""
|
||||
key = f"user_token:{user_id}"
|
||||
return await RedisClient.get(key)
|
||||
|
||||
@staticmethod
|
||||
async def delete_user_token(user_id: int) -> int:
|
||||
"""删除用户Token"""
|
||||
key = f"user_token:{user_id}"
|
||||
return await RedisClient.delete(key)
|
||||
|
||||
@staticmethod
|
||||
async def set_login_attempts(username: str) -> int:
|
||||
"""记录登录失败次数"""
|
||||
key = f"login_attempts:{username}"
|
||||
attempts = await RedisClient.get(key)
|
||||
attempts = int(attempts) + 1 if attempts else 1
|
||||
await RedisClient.set(key, attempts, 900) # 15分钟锁定
|
||||
return attempts
|
||||
|
||||
@staticmethod
|
||||
async def clear_login_attempts(username: str) -> None:
|
||||
"""清除登录失败记录"""
|
||||
key = f"login_attempts:{username}"
|
||||
await RedisClient.delete(key)
|
||||
106
backend/utils/response.py
Normal file
106
backend/utils/response.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Any, Optional, Dict, List
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
|
||||
class ResponseCode:
|
||||
"""响应状态码"""
|
||||
SUCCESS = 200
|
||||
CREATED = 201
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
CONFLICT = 409
|
||||
UNPROCESSABLE = 422
|
||||
INTERNAL_ERROR = 500
|
||||
|
||||
|
||||
def success_response(data: Any = None, message: str = "操作成功") -> JSONResponse:
|
||||
"""成功响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.SUCCESS,
|
||||
content={
|
||||
"success": True,
|
||||
"code": ResponseCode.SUCCESS,
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def error_response(
|
||||
message: str = "操作失败",
|
||||
code: int = ResponseCode.BAD_REQUEST,
|
||||
data: Any = None
|
||||
) -> JSONResponse:
|
||||
"""错误响应"""
|
||||
return JSONResponse(
|
||||
status_code=code,
|
||||
content={
|
||||
"success": False,
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def unauthorized_response(message: str = "未授权,请重新登录") -> JSONResponse:
|
||||
"""未授权响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.UNAUTHORIZED,
|
||||
content={
|
||||
"success": False,
|
||||
"code": ResponseCode.UNAUTHORIZED,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def forbidden_response(message: str = "权限不足") -> JSONResponse:
|
||||
"""禁止访问响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.FORBIDDEN,
|
||||
content={
|
||||
"success": False,
|
||||
"code": ResponseCode.FORBIDDEN,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def not_found_response(message: str = "资源不存在") -> JSONResponse:
|
||||
"""资源不存在响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.NOT_FOUND,
|
||||
content={
|
||||
"success": False,
|
||||
"code": ResponseCode.NOT_FOUND,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def paginated_response(items: List[Any], total: int, page: int, page_size: int) -> Dict:
|
||||
"""分页响应数据"""
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
131
backend/utils/security.py
Normal file
131
backend/utils/security.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import re
|
||||
from config import settings
|
||||
|
||||
|
||||
class SecurityUtils:
|
||||
"""安全工具类"""
|
||||
|
||||
@staticmethod
|
||||
def sha1_md5_password(password: str) -> str:
|
||||
"""
|
||||
双重加密:sha1 + md5
|
||||
流程:原始密码 -> sha1 -> 加盐 -> md5
|
||||
"""
|
||||
# 第一层:SHA1
|
||||
sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest()
|
||||
# 加盐
|
||||
salted = sha1_hash + settings.PASSWORD_SALT
|
||||
# 第二层:MD5
|
||||
md5_hash = hashlib.md5(salted.encode('utf-8')).hexdigest()
|
||||
return md5_hash
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return SecurityUtils.sha1_md5_password(plain_password) == hashed_password
|
||||
|
||||
@staticmethod
|
||||
def generate_random_password(length: int = 8) -> str:
|
||||
"""生成随机密码"""
|
||||
alphabet = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
@staticmethod
|
||||
def validate_password_strength(password: str) -> tuple:
|
||||
"""
|
||||
验证密码强度
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if len(password) < 6:
|
||||
return False, "密码长度至少6位"
|
||||
if len(password) > 20:
|
||||
return False, "密码长度不能超过20位"
|
||||
|
||||
# 检查是否包含至少一个数字
|
||||
if not any(c.isdigit() for c in password):
|
||||
return False, "密码必须包含至少一个数字"
|
||||
|
||||
# 检查是否包含至少一个字母
|
||||
if not any(c.isalpha() for c in password):
|
||||
return False, "密码必须包含至少一个字母"
|
||||
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def sanitize_string(value: str, max_length: int = 255) -> str:
|
||||
"""
|
||||
清理字符串输入
|
||||
- 去除首尾空格
|
||||
- 限制长度
|
||||
- 转义特殊字符
|
||||
"""
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
# 去除首尾空格
|
||||
value = value.strip()
|
||||
|
||||
# 限制长度
|
||||
if len(value) > max_length:
|
||||
value = value[:max_length]
|
||||
|
||||
# 转义HTML特殊字符(防止XSS)
|
||||
html_chars = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/'
|
||||
}
|
||||
for char, escape in html_chars.items():
|
||||
value = value.replace(char, escape)
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def validate_student_no(student_no: str) -> bool:
|
||||
"""验证学号格式(数字+字母,长度4-20)"""
|
||||
if not student_no:
|
||||
return False
|
||||
if len(student_no) < 4 or len(student_no) > 20:
|
||||
return False
|
||||
# 字母数字组合
|
||||
return student_no.isalnum()
|
||||
|
||||
@staticmethod
|
||||
def validate_phone(phone: str) -> bool:
|
||||
"""验证手机号格式(中国手机号)"""
|
||||
if not phone:
|
||||
return False
|
||||
pattern = r'^1[3-9]\d{9}$'
|
||||
return bool(re.match(pattern, phone))
|
||||
|
||||
@staticmethod
|
||||
def validate_points_change(points: int, max_abs: int = 100) -> tuple:
|
||||
"""
|
||||
验证分值变动
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if points == 0:
|
||||
return False, "分值不能为0"
|
||||
if abs(points) > max_abs:
|
||||
return f"单次分值变动不能超过{max_abs}分"
|
||||
return True, ""
|
||||
|
||||
|
||||
# 单例导出
|
||||
security = SecurityUtils()
|
||||
0
docs/admin.md
Normal file
0
docs/admin.md
Normal file
0
docs/parent.md
Normal file
0
docs/parent.md
Normal file
0
docs/student.md
Normal file
0
docs/student.md
Normal file
28
frontend/.env.example
Normal file
28
frontend/.env.example
Normal file
@@ -0,0 +1,28 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 前端配置
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# 后端API地址
|
||||
API_BASE_URL=https://api.your-domain.com
|
||||
|
||||
# API超时时间(秒)
|
||||
API_TIMEOUT=30
|
||||
|
||||
# JWT存储Key
|
||||
JWT_STORAGE_KEY=class_system_token
|
||||
|
||||
# 用户信息存储Key
|
||||
USER_STORAGE_KEY=class_system_user
|
||||
|
||||
# 站点名称
|
||||
SITE_NAME=班级操行分管理系统
|
||||
|
||||
# 会话超时时间(分钟)
|
||||
SESSION_TIMEOUT=30
|
||||
126
frontend/admin/admins.php
Normal file
126
frontend/admin/admins.php
Normal file
@@ -0,0 +1,126 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端管理员管理
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '管理员管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if ($role !== '班主任') {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<a href="/admin/conduct.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/admins.php" class="nav-item active">管理员管理</a>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary" onclick="showAddAdminModal()">添加管理员</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>用户名</th>
|
||||
<th>姓名</th>
|
||||
<th>角色</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="adminList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加管理员模态框 -->
|
||||
<div id="addAdminModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>添加管理员</h3>
|
||||
<button class="modal-close" onclick="closeModal('addAdminModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddAdmin()">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="adminUsername" required placeholder="登录账号">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>姓名</label>
|
||||
<input type="text" id="adminRealName" required placeholder="真实姓名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="text" id="adminPassword" placeholder="留空则自动生成">
|
||||
<small>自动生成8位随机密码</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>角色</label>
|
||||
<select id="adminRole" required>
|
||||
<option value="">请选择角色</option>
|
||||
<option value="班长">班长</option>
|
||||
<option value="科代表">科代表</option>
|
||||
<option value="考勤委员">考勤委员</option>
|
||||
<option value="劳动委员">劳动委员</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">添加</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addAdminModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadAdmins() {
|
||||
const res = await apiGet('/api/admin/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.admins.forEach(admin => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(admin.username)}</td>
|
||||
<td>${escapeHtml(admin.real_name)}</td>
|
||||
<td>${escapeHtml(admin.role_type)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.admins.length === 0) {
|
||||
html = '<tr><td colspan="3" style="text-align:center;">暂无管理员</td></tr>';
|
||||
}
|
||||
document.getElementById('adminList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadAdmins();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
184
frontend/admin/attendance.php
Normal file
184
frontend/admin/attendance.php
Normal file
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端考勤管理
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '考勤管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if (!in_array($role, ['班主任', '考勤委员'])) {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/attendance.php" class="nav-item active">考勤管理</a>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary" onclick="showAddAttendanceModal()">添加考勤</button>
|
||||
<div class="search-bar">
|
||||
<input type="date" id="attendanceDate" value="<?php echo date('Y-m-d'); ?>">
|
||||
<button class="btn btn-primary" onclick="loadAttendanceRecords()">查询</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>学号</th><th>姓名</th><th>状态</th><th>原因</th><th>记录人</th><th>扣分</th></tr>
|
||||
</thead>
|
||||
<tbody id="attendanceList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加考勤模态框 -->
|
||||
<div id="addAttendanceModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>添加考勤记录</h3>
|
||||
<button class="modal-close" onclick="closeModal('addAttendanceModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddAttendance()">
|
||||
<div class="form-group">
|
||||
<label>学生</label>
|
||||
<select id="attendanceStudentId" required></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>日期</label>
|
||||
<input type="date" id="attAttendanceDate" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>状态</label>
|
||||
<select id="attendanceStatus" required>
|
||||
<option value="present">出勤</option>
|
||||
<option value="absent">缺勤</option>
|
||||
<option value="late">迟到</option>
|
||||
<option value="leave">请假</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>原因</label>
|
||||
<input type="text" id="attendanceReason" placeholder="缺勤/迟到/请假原因">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label><input type="checkbox" id="attendanceDeduct"> 同时扣分</label>
|
||||
<small>扣分规则:缺勤-5分,迟到-2分,请假-1分</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">提交</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addAttendanceModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadAttendanceRecords() {
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const res = await apiGet('/api/admin/attendance/records', { date });
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(record.student_no)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||
<td>${escapeHtml(record.reason || '-')}</td>
|
||||
<td>${escapeHtml(record.recorder_name || '-')}</td>
|
||||
<td>${record.deduction_applied ? '已扣分' : '-'}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无考勤记录</td></tr>';
|
||||
}
|
||||
document.getElementById('attendanceList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">请选择学生</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
|
||||
});
|
||||
document.getElementById('attendanceStudentId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function showAddAttendanceModal() {
|
||||
await loadStudentsForSelect();
|
||||
document.getElementById('addAttendanceModal').style.display = 'flex';
|
||||
document.getElementById('attAttendanceDate').value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
async function submitAddAttendance() {
|
||||
const studentId = document.getElementById('attendanceStudentId').value;
|
||||
const date = document.getElementById('attAttendanceDate').value;
|
||||
const status = document.getElementById('attendanceStatus').value;
|
||||
const reason = document.getElementById('attendanceReason').value;
|
||||
const applyDeduction = document.getElementById('attendanceDeduct').checked;
|
||||
|
||||
if (!studentId || !date || !status) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/attendance', {
|
||||
student_id: parseInt(studentId),
|
||||
date: date,
|
||||
status: status,
|
||||
reason: reason,
|
||||
apply_deduction: applyDeduction
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('考勤记录添加成功');
|
||||
closeModal('addAttendanceModal');
|
||||
loadAttendanceRecords();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadAttendanceRecords();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
145
frontend/admin/conduct.php
Normal file
145
frontend/admin/conduct.php
Normal file
@@ -0,0 +1,145 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端操行分管理
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '操行分管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if (!in_array($role, ['班主任', '班长'])) {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<a href="/admin/conduct.php" class="nav-item active">操行分管理</a>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<button class="btn btn-primary" onclick="showBatchPointsModal()">批量加减分</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>当前操行分</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studentList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let selectedStudentIds = [];
|
||||
|
||||
async function loadStudents() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.students.forEach(student => {
|
||||
html += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td><button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button></td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="5" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
loadStudents();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<!-- 批量加减分模态框 -->
|
||||
<div id="batchPointsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>批量加减分</h3>
|
||||
<button class="modal-close" onclick="closeModal('batchPointsModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitBatchPoints()">
|
||||
<div class="form-group">
|
||||
<label>选中学生</label>
|
||||
<div id="selectedStudentsCount">0 人</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>分数变动</label>
|
||||
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
||||
<small><?php echo $role === '班长' ? '班长单次±5分以内' : '班主任无限制'; ?></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>原因</label>
|
||||
<textarea id="pointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">确认提交</button>
|
||||
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
114
frontend/admin/dashboard.php
Normal file
114
frontend/admin/dashboard.php
Normal file
@@ -0,0 +1,114 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端首页
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '首页';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item active">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="stats-grid" id="dashboardStats"></div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">快捷操作</div>
|
||||
<div class="action-buttons" id="quickActions"></div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">操行分排行榜 (Top 10)</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>排名</th><th>学号</th><th>姓名</th><th>操行分</th></tr>
|
||||
</thead>
|
||||
<tbody id="rankingList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadDashboard() {
|
||||
// 加载学生统计
|
||||
const studentsRes = await apiGet('/api/admin/students');
|
||||
if (studentsRes && studentsRes.success) {
|
||||
document.getElementById('dashboardStats').innerHTML = `
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">班级学生数</div>
|
||||
<div class="stat-value">${studentsRes.data.total || 0}</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
// 快捷操作按钮
|
||||
let quickActions = '';
|
||||
if ('<?php echo $role; ?>' === '班主任' || '<?php echo $role; ?>' === '班长') {
|
||||
quickActions += '<button class="btn btn-primary" onclick="location.href=\'/admin/conduct.php\'">操行分管理</button>';
|
||||
}
|
||||
if ('<?php echo $role; ?>' === '班主任') {
|
||||
quickActions += '<button class="btn btn-success" onclick="location.href=\'/admin/students.php\'">导入学生</button>';
|
||||
}
|
||||
document.getElementById('quickActions').innerHTML = quickActions || '<p>暂无快捷操作</p>';
|
||||
|
||||
// 加载排行榜
|
||||
const rankingRes = await apiGet('/api/student/ranking', { limit: 10 });
|
||||
if (rankingRes && rankingRes.success) {
|
||||
let html = '';
|
||||
rankingRes.data.ranking.forEach((student, index) => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${index + 1}</td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
if (rankingRes.data.ranking.length === 0) {
|
||||
html = '<tr><td colspan="4" style="text-align:center;">暂无数据</td></tr>';
|
||||
}
|
||||
document.getElementById('rankingList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadDashboard();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
166
frontend/admin/history.php
Normal file
166
frontend/admin/history.php
Normal file
@@ -0,0 +1,166 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端历史记录
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '历史记录';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item active">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="filter-bar">
|
||||
<div class="filter-group">
|
||||
<label>开始日期</label>
|
||||
<input type="date" id="historyStartDate">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>结束日期</label>
|
||||
<input type="date" id="historyEndDate">
|
||||
</div>
|
||||
<div class="filter-group">
|
||||
<label>学生</label>
|
||||
<select id="historyStudentId">
|
||||
<option value="">全部</option>
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-primary" onclick="loadHistory(1)">查询</button>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>时间</th>
|
||||
<th>学生</th>
|
||||
<th>分数变动</th>
|
||||
<th>原因</th>
|
||||
<th>操作人</th>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<th>操作</th>
|
||||
<?php endif; ?>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="historyList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination" id="historyPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentHistoryPage = 1;
|
||||
let totalHistoryPages = 1;
|
||||
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">全部</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${escapeHtml(s.student_no)} - ${escapeHtml(s.name)}</option>`;
|
||||
});
|
||||
document.getElementById('historyStudentId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadHistory(page = 1) {
|
||||
currentHistoryPage = page;
|
||||
const startDate = document.getElementById('historyStartDate').value;
|
||||
const endDate = document.getElementById('historyEndDate').value;
|
||||
const studentId = document.getElementById('historyStudentId').value;
|
||||
|
||||
const res = await apiGet('/api/admin/conduct/history', {
|
||||
page, page_size: 20,
|
||||
start_date: startDate,
|
||||
end_date: endDate,
|
||||
student_id: studentId
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
html += `<tr>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td>${escapeHtml(record.student_name)}</td>
|
||||
<td class="${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||
<td>${escapeHtml(record.reason)}</td>
|
||||
<td>${escapeHtml(record.recorder_name)}</td>`;
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
html += `<td><button class="btn btn-sm btn-danger" onclick="revokeRecord(${record.record_id})">撤销</button></td>`;
|
||||
<?php endif; ?>
|
||||
html += `</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.records.length === 0) {
|
||||
const colSpan = <?php echo ($role === '班主任' || $role === '班长') ? '6' : '5'; ?>;
|
||||
html = `<tr><td colspan="${colSpan}" style="text-align:center;">暂无记录</td></tr>`;
|
||||
}
|
||||
|
||||
document.getElementById('historyList').innerHTML = html;
|
||||
|
||||
totalHistoryPages = res.data.total_pages || 1;
|
||||
renderHistoryPagination();
|
||||
}
|
||||
}
|
||||
|
||||
function renderHistoryPagination() {
|
||||
const container = document.getElementById('historyPagination');
|
||||
if (!container) return;
|
||||
if (totalHistoryPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalHistoryPages; i++) {
|
||||
if (i === currentHistoryPage) {
|
||||
html += `<span class="active">${i}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" onclick="loadHistory(${i}); return false;">${i}</a>`;
|
||||
}
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
loadStudentsForSelect();
|
||||
loadHistory();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
170
frontend/admin/homework.php
Normal file
170
frontend/admin/homework.php
Normal file
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端作业管理
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '作业管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if (!in_array($role, ['班主任', '科代表'])) {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/homework.php" class="nav-item active">作业管理</a>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<button class="btn btn-primary" onclick="showAddAssignmentModal()">发布作业</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div id="assignmentsList"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 发布作业模态框 -->
|
||||
<div id="addAssignmentModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>发布作业</h3>
|
||||
<button class="modal-close" onclick="closeModal('addAssignmentModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddAssignment()">
|
||||
<div class="form-group">
|
||||
<label>科目</label>
|
||||
<select id="assignmentSubjectId" required></select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>作业标题</label>
|
||||
<input type="text" id="assignmentTitle" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>作业描述</label>
|
||||
<textarea id="assignmentDescription" rows="3"></textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>截止日期</label>
|
||||
<input type="date" id="assignmentDeadline" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">发布</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addAssignmentModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadAssignments() {
|
||||
const res = await apiGet('/api/admin/homework/assignments');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
for (const assignment of res.data.assignments) {
|
||||
html += `
|
||||
<div class="card assignment-card">
|
||||
<div class="assignment-header">
|
||||
<div><span class="assignment-title">${escapeHtml(assignment.title)}</span> <span class="assignment-meta">(${escapeHtml(assignment.subject_name)})</span></div>
|
||||
<div class="assignment-meta">截止: ${assignment.deadline}</div>
|
||||
</div>
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead><tr><th>学生</th><th>状态</th><th>备注</th><th>操作</th></tr></thead>
|
||||
<tbody id="submissions-${assignment.assignment_id}"><tr><td colspan="4" class="loading">加载中...</td></tr></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
if (res.data.assignments.length === 0) {
|
||||
html = '<div style="text-align:center;padding:40px;">暂无作业</div>';
|
||||
}
|
||||
document.getElementById('assignmentsList').innerHTML = html;
|
||||
|
||||
for (const assignment of res.data.assignments) {
|
||||
await loadSubmissions(assignment.assignment_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSubmissions(assignmentId) {
|
||||
const res = await apiGet(`/api/admin/homework/submissions/${assignmentId}`);
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.submissions.forEach(sub => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(sub.student_name)}</td>
|
||||
<td>${getStatusBadge(sub.status, 'homework')}</td>
|
||||
<td>${escapeHtml(sub.comments || '-')}</td>
|
||||
<td>
|
||||
<select id="status-${sub.submission_id}" class="status-select">
|
||||
<option value="submitted" ${sub.status === 'submitted' ? 'selected' : ''}>已提交</option>
|
||||
<option value="not_submitted" ${sub.status === 'not_submitted' ? 'selected' : ''}>未提交</option>
|
||||
<option value="late" ${sub.status === 'late' ? 'selected' : ''}>迟交</option>
|
||||
</select>
|
||||
<label><input type="checkbox" id="deduct-${sub.submission_id}"> 同时扣分</label>
|
||||
<button class="btn btn-sm btn-primary" onclick="updateSubmission(${sub.submission_id})">更新</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
document.getElementById(`submissions-${assignmentId}`).innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function updateSubmission(submissionId) {
|
||||
const status = document.getElementById(`status-${submissionId}`).value;
|
||||
const applyDeduction = document.getElementById(`deduct-${submissionId}`).checked;
|
||||
|
||||
const res = await apiPut('/api/admin/homework/submission', {
|
||||
submission_id: submissionId,
|
||||
status: status,
|
||||
apply_deduction: applyDeduction
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('更新成功');
|
||||
loadAssignments();
|
||||
} else {
|
||||
showToast(res?.message || '更新失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadAssignments();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
100
frontend/admin/passwork.php
Normal file
100
frontend/admin/passwork.php
Normal file
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端修改密码
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '修改密码';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item active">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-title">修改密码</div>
|
||||
<form id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label>原密码 <span style="color:red;">*</span></label>
|
||||
<input type="password" id="oldPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>新密码 <span style="color:red;">*</span></label>
|
||||
<input type="password" id="newPassword" required>
|
||||
<small>密码长度6-20位,需包含字母和数字</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认新密码 <span style="color:red;">*</span></label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const oldPassword = document.getElementById('oldPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast('两次输入的新密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (newPassword.length < 6 || newPassword.length > 20) {
|
||||
showToast('密码长度需为6-20位', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/auth/change-password', {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码修改成功,请重新登录');
|
||||
setTimeout(() => logout(), 1500);
|
||||
} else {
|
||||
showToast(res?.message || '密码修改失败', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
243
frontend/admin/students.php
Normal file
243
frontend/admin/students.php
Normal file
@@ -0,0 +1,243 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端学生管理
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '学生管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item active">学生管理</a>
|
||||
<?php if ($role === '班主任' || $role === '班长'): ?>
|
||||
<a href="/admin/conduct.php" class="nav-item">操行分管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '科代表'): ?>
|
||||
<a href="/admin/homework.php" class="nav-item">作业管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任' || $role === '考勤委员'): ?>
|
||||
<a href="/admin/attendance.php" class="nav-item">考勤管理</a>
|
||||
<?php endif; ?>
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<a href="/admin/subjects.php" class="nav-item">科目管理</a>
|
||||
<a href="/admin/admins.php" class="nav-item">管理员管理</a>
|
||||
<?php endif; ?>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<div class="action-buttons">
|
||||
<?php if ($role === '班主任'): ?>
|
||||
<button class="btn btn-primary" onclick="showImportModal()">导入学生</button>
|
||||
<button class="btn btn-success" onclick="showAddStudentModal()">新增学生</button>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<div class="search-bar">
|
||||
<input type="text" id="searchInput" placeholder="搜索姓名/学号">
|
||||
<button class="btn btn-primary" onclick="loadStudents(1)">搜索</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="table-wrapper">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th><input type="checkbox" id="selectAll" onclick="toggleSelectAll()"></th>
|
||||
<th>学号</th>
|
||||
<th>姓名</th>
|
||||
<th>操行分</th>
|
||||
<th>家长手机号</th>
|
||||
<th>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="studentList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="pagination" id="pagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 导入学生模态框 -->
|
||||
<div id="importModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>导入学生</h3>
|
||||
<button class="modal-close" onclick="closeModal('importModal')">×</button>
|
||||
</div>
|
||||
<div class="import-area" onclick="document.getElementById('importFile').click()">
|
||||
<p>点击选择JSON文件</p>
|
||||
<p class="import-label">或点击此处上传</p>
|
||||
<input type="file" id="importFile" accept=".json">
|
||||
<p style="margin-top: 10px; font-size: 12px; color: #999;">
|
||||
<a href="/assets/uploads/sample_import.json" download style="color: #667eea;">下载示例文件</a>
|
||||
</p>
|
||||
</div>
|
||||
<div id="importPreview" class="preview-table" style="display: none;"></div>
|
||||
<div class="modal-footer">
|
||||
<button class="btn btn-primary" onclick="doImport()" id="importBtn" style="display: none;">确认导入</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 新增学生模态框 -->
|
||||
<div id="addStudentModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>新增学生</h3>
|
||||
<button class="modal-close" onclick="closeModal('addStudentModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddStudent()">
|
||||
<div class="form-group">
|
||||
<label>学号 <span style="color:red;">*</span></label>
|
||||
<input type="text" id="studentNo" required placeholder="4-20位字母数字组合">
|
||||
<small>学号将作为学生登录账号</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>姓名 <span style="color:red;">*</span></label>
|
||||
<input type="text" id="studentName" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>家长手机号</label>
|
||||
<input type="tel" id="parentPhone" placeholder="11位手机号">
|
||||
<small>填写后将自动创建家长账号(密码同学生初始密码123456)</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">确认添加</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addStudentModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
|
||||
async function loadStudents(page = 1) {
|
||||
currentPage = page;
|
||||
const search = document.getElementById('searchInput').value;
|
||||
const res = await apiGet('/api/admin/students', { page, page_size: 20, search });
|
||||
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.students.forEach(student => {
|
||||
html += `<tr>
|
||||
<td><input type="checkbox" class="student-checkbox" data-id="${student.student_id}"></td>
|
||||
<td>${escapeHtml(student.student_no)}</td>
|
||||
<td>${escapeHtml(student.name)}</td>
|
||||
<td>${student.total_points}</td>
|
||||
<td>${student.parent_phone || '-'}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm btn-primary" onclick="showSinglePointsModal(${student.student_id}, '${escapeHtml(student.name)}')">加减分</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
if (res.data.students.length === 0) {
|
||||
html = '<tr><td colspan="6" style="text-align:center;">暂无学生数据</td></tr>';
|
||||
}
|
||||
|
||||
document.getElementById('studentList').innerHTML = html;
|
||||
|
||||
totalPages = res.data.total_pages || 1;
|
||||
renderPagination();
|
||||
}
|
||||
}
|
||||
|
||||
function renderPagination() {
|
||||
const container = document.getElementById('pagination');
|
||||
if (!container) return;
|
||||
if (totalPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
|
||||
let html = '';
|
||||
for (let i = 1; i <= totalPages; i++) {
|
||||
if (i === currentPage) {
|
||||
html += `<span class="active">${i}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" onclick="loadStudents(${i}); return false;">${i}</a>`;
|
||||
}
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
function showSinglePointsModal(studentId, studentName) {
|
||||
selectedStudentIds = [studentId];
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${studentName} (1人)`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载
|
||||
loadStudents();
|
||||
|
||||
// 搜索防抖
|
||||
let searchTimeout;
|
||||
document.getElementById('searchInput').addEventListener('input', () => {
|
||||
clearTimeout(searchTimeout);
|
||||
searchTimeout = setTimeout(() => loadStudents(1), 500);
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<!-- 批量加减分模态框(共用) -->
|
||||
<div id="batchPointsModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>批量加减分</h3>
|
||||
<button class="modal-close" onclick="closeModal('batchPointsModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitBatchPoints()">
|
||||
<div class="form-group">
|
||||
<label>选中学生</label>
|
||||
<div id="selectedStudentsCount">0 人</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>分数变动</label>
|
||||
<input type="number" id="pointsChange" required placeholder="正数为加分,负数为扣分">
|
||||
<small><?php echo $role === '班长' ? '班长单次±5分以内' : '班主任无限制'; ?></small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>原因</label>
|
||||
<textarea id="pointsReason" required rows="3" placeholder="请填写加减分原因"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">确认提交</button>
|
||||
<button type="button" class="btn" onclick="closeModal('batchPointsModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
157
frontend/admin/subjects.php
Normal file
157
frontend/admin/subjects.php
Normal file
@@ -0,0 +1,157 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端科目管理
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'admin') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '科目管理';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
|
||||
if ($role !== '班主任') {
|
||||
header('Location: /admin/dashboard.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/admin/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/admin/students.php" class="nav-item">学生管理</a>
|
||||
<a href="/admin/conduct.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/admins.php" class="nav-item">管理员管理</a>
|
||||
<a href="/admin/history.php" class="nav-item">历史记录</a>
|
||||
<a href="/admin/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="action-bar">
|
||||
<button class="btn btn-primary" onclick="showAddSubjectModal()">添加科目</button>
|
||||
</div>
|
||||
<div id="subjectList" class="subject-list"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 添加科目模态框 -->
|
||||
<div id="addSubjectModal" class="modal">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>添加科目</h3>
|
||||
<button class="modal-close" onclick="closeModal('addSubjectModal')">×</button>
|
||||
</div>
|
||||
<form onsubmit="event.preventDefault(); submitAddSubject()">
|
||||
<div class="form-group">
|
||||
<label>科目名称</label>
|
||||
<input type="text" id="subjectName" required placeholder="例如:语文、数学">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>科目代码</label>
|
||||
<input type="text" id="subjectCode" placeholder="例如:CHI、MATH">
|
||||
<small>可选,用于排序和标识</small>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">添加</button>
|
||||
<button type="button" class="btn" onclick="closeModal('addSubjectModal')">取消</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.subject-list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
.subject-item {
|
||||
background: #f8f9fa;
|
||||
padding: 12px 20px;
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 15px;
|
||||
}
|
||||
.subject-name {
|
||||
font-weight: 500;
|
||||
font-size: 16px;
|
||||
}
|
||||
.subject-code {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
.subject-status {
|
||||
font-size: 11px;
|
||||
padding: 2px 8px;
|
||||
border-radius: 12px;
|
||||
}
|
||||
.subject-status-active {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
.subject-status-inactive {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
</style>
|
||||
|
||||
<script>
|
||||
async function loadSubjects() {
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success) {
|
||||
let html = '';
|
||||
res.data.subjects.forEach(sub => {
|
||||
html += `
|
||||
<div class="subject-item">
|
||||
<span class="subject-name">${escapeHtml(sub.subject_name)}</span>
|
||||
<span class="subject-code">${escapeHtml(sub.subject_code || '')}</span>
|
||||
<span class="subject-status ${sub.is_active ? 'subject-status-active' : 'subject-status-inactive'}">
|
||||
${sub.is_active ? '启用' : '禁用'}
|
||||
</span>
|
||||
<button class="btn btn-sm" onclick="toggleSubject(${sub.subject_id}, ${!sub.is_active})">
|
||||
${sub.is_active ? '禁用' : '启用'}
|
||||
</button>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (res.data.subjects.length === 0) {
|
||||
html = '<p style="text-align:center;padding:40px;">暂无科目,请点击"添加科目"</p>';
|
||||
}
|
||||
document.getElementById('subjectList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
async function toggleSubject(subjectId, enable) {
|
||||
const res = await apiPut(`/api/subject/update/${subjectId}`, {
|
||||
is_active: enable
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(enable ? '科目已启用' : '科目已禁用');
|
||||
loadSubjects();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
loadSubjects();
|
||||
</script>
|
||||
<script src="/assets/js/admin.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
130
frontend/assets/css/admin.css
Normal file
130
frontend/assets/css/admin.css
Normal file
@@ -0,0 +1,130 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端样式
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
/* 批量操作栏 */
|
||||
.batch-bar {
|
||||
background: #f0f4ff;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 16px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.batch-info {
|
||||
color: #667eea;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* 导入区域 */
|
||||
.import-area {
|
||||
border: 2px dashed #ddd;
|
||||
border-radius: 12px;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
margin-bottom: 20px;
|
||||
transition: border-color 0.3s;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.import-area:hover {
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.import-area input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.import-label {
|
||||
color: #667eea;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/* 预览表格 */
|
||||
.preview-table {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
margin-top: 16px;
|
||||
}
|
||||
|
||||
/* 筛选栏 */
|
||||
.filter-bar {
|
||||
background: #f8f9fa;
|
||||
padding: 16px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 16px;
|
||||
align-items: flex-end;
|
||||
}
|
||||
|
||||
.filter-group {
|
||||
flex: 1;
|
||||
min-width: 150px;
|
||||
}
|
||||
|
||||
.filter-group label {
|
||||
display: block;
|
||||
margin-bottom: 4px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.filter-group input,
|
||||
.filter-group select {
|
||||
width: 100%;
|
||||
padding: 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
/* 作业卡片 */
|
||||
.assignment-card {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.assignment-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
flex-wrap: wrap;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.assignment-meta {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 状态选择器 */
|
||||
.status-select {
|
||||
padding: 4px 8px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* 复选框 */
|
||||
.student-checkbox {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
cursor: pointer;
|
||||
}
|
||||
666
frontend/assets/css/style.css
Normal file
666
frontend/assets/css/style.css
Normal file
@@ -0,0 +1,666 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 全局样式
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
background: #f5f7fb;
|
||||
min-height: 100vh;
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ========== 登录页面 ========== */
|
||||
.login-container {
|
||||
background: white;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.1);
|
||||
padding: 40px;
|
||||
width: 400px;
|
||||
max-width: 90%;
|
||||
margin: 100px auto;
|
||||
}
|
||||
|
||||
.login-header {
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
.login-header h1 {
|
||||
font-size: 24px;
|
||||
color: #333;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.login-header p {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-form .form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.login-form label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
color: #555;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.login-form input {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
.login-form input:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.btn-login {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
transition: opacity 0.3s;
|
||||
}
|
||||
|
||||
.btn-login:hover {
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.error-msg {
|
||||
background: #fee;
|
||||
color: #c33;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
margin-top: 15px;
|
||||
text-align: center;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.login-footer {
|
||||
text-align: center;
|
||||
margin-top: 30px;
|
||||
padding-top: 20px;
|
||||
border-top: 1px solid #eee;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ========== 公共头部 ========== */
|
||||
.header {
|
||||
background: white;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);
|
||||
padding: 12px 24px;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 100;
|
||||
}
|
||||
|
||||
.header h1 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.header-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.user-name {
|
||||
color: #666;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-role {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
padding: 2px 8px;
|
||||
border-radius: 20px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.btn-logout {
|
||||
background: #e53e3e;
|
||||
color: white;
|
||||
border: none;
|
||||
padding: 6px 16px;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: background 0.3s;
|
||||
}
|
||||
|
||||
.btn-logout:hover {
|
||||
background: #c53030;
|
||||
}
|
||||
|
||||
/* ========== 导航菜单 ========== */
|
||||
.nav {
|
||||
background: white;
|
||||
padding: 0 24px;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 12px 20px;
|
||||
background: none;
|
||||
border: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
font-size: 14px;
|
||||
transition: all 0.3s;
|
||||
border-bottom: 2px solid transparent;
|
||||
text-decoration: none;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
color: #667eea;
|
||||
border-bottom-color: #667eea;
|
||||
}
|
||||
|
||||
/* ========== 容器 ========== */
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 24px auto;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
/* ========== 卡片 ========== */
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.card-title {
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 16px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 2px solid #667eea;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* ========== 统计卡片网格 ========== */
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
|
||||
gap: 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.stat-value {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.stat-label {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* ========== 表格 ========== */
|
||||
.table-wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 12px;
|
||||
text-align: left;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
th {
|
||||
background: #f8f9fa;
|
||||
font-weight: 600;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
tr:hover {
|
||||
background: #f8f9fa;
|
||||
}
|
||||
|
||||
/* ========== 状态标签 ========== */
|
||||
.status-badge {
|
||||
display: inline-block;
|
||||
padding: 4px 10px;
|
||||
border-radius: 20px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.status-submitted {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
|
||||
.status-not_submitted {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
.status-late {
|
||||
background: #feebc8;
|
||||
color: #7c2d12;
|
||||
}
|
||||
|
||||
.status-present {
|
||||
background: #c6f6d5;
|
||||
color: #22543d;
|
||||
}
|
||||
|
||||
.status-absent {
|
||||
background: #fed7d7;
|
||||
color: #742a2a;
|
||||
}
|
||||
|
||||
.status-leave {
|
||||
background: #e9d8fd;
|
||||
color: #553c9a;
|
||||
}
|
||||
|
||||
/* ========== 按钮 ========== */
|
||||
.btn {
|
||||
padding: 8px 16px;
|
||||
border: none;
|
||||
border-radius: 6px;
|
||||
cursor: pointer;
|
||||
font-size: 13px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-primary:hover {
|
||||
background: #5a67d8;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background: #e53e3e;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-danger:hover {
|
||||
background: #c53030;
|
||||
}
|
||||
|
||||
.btn-success {
|
||||
background: #38a169;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.btn-success:hover {
|
||||
background: #2f855a;
|
||||
}
|
||||
|
||||
.btn-sm {
|
||||
padding: 4px 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ========== 模态框 ========== */
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: rgba(0, 0, 0, 0.5);
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 24px;
|
||||
width: 500px;
|
||||
max-width: 90%;
|
||||
max-height: 80vh;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
padding-bottom: 12px;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.modal-header h3 {
|
||||
font-size: 18px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.modal-close {
|
||||
background: none;
|
||||
border: none;
|
||||
font-size: 24px;
|
||||
cursor: pointer;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
margin-top: 20px;
|
||||
padding-top: 16px;
|
||||
border-top: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
/* ========== 表单 ========== */
|
||||
.form-group {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.form-group label {
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
font-weight: 500;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.form-group input,
|
||||
.form-group select,
|
||||
.form-group textarea {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.form-group input:focus,
|
||||
.form-group select:focus,
|
||||
.form-group textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
.form-group small {
|
||||
display: block;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
/* ========== 复选框组 ========== */
|
||||
.checkbox-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.checkbox-group input {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
/* ========== 操作栏 ========== */
|
||||
.action-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 20px;
|
||||
flex-wrap: wrap;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
display: flex;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
padding: 8px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 6px;
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
/* ========== 分页 ========== */
|
||||
.pagination {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.pagination a, .pagination span {
|
||||
padding: 6px 12px;
|
||||
border: 1px solid #ddd;
|
||||
border-radius: 4px;
|
||||
text-decoration: none;
|
||||
color: #666;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.pagination .active {
|
||||
background: #667eea;
|
||||
color: white;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
/* ========== 提示消息 ========== */
|
||||
.toast {
|
||||
position: fixed;
|
||||
bottom: 20px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
padding: 12px 24px;
|
||||
border-radius: 8px;
|
||||
color: white;
|
||||
font-size: 14px;
|
||||
z-index: 1100;
|
||||
animation: fadeInUp 0.3s ease;
|
||||
}
|
||||
|
||||
.toast-success {
|
||||
background: #38a169;
|
||||
}
|
||||
|
||||
.toast-error {
|
||||
background: #e53e3e;
|
||||
}
|
||||
|
||||
.toast-warning {
|
||||
background: #ed8936;
|
||||
}
|
||||
|
||||
@keyframes fadeInUp {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(20px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* ========== 加载动画 ========== */
|
||||
.loading {
|
||||
display: inline-block;
|
||||
width: 20px;
|
||||
height: 20px;
|
||||
border: 2px solid #ddd;
|
||||
border-top-color: #667eea;
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* ========== 底部 ========== */
|
||||
.footer {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ========== 记录项 ========== */
|
||||
.record-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.record-points {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.record-points.plus {
|
||||
color: #38a169;
|
||||
}
|
||||
|
||||
.record-points.minus {
|
||||
color: #e53e3e;
|
||||
}
|
||||
|
||||
.record-reason {
|
||||
flex: 1;
|
||||
margin: 0 15px;
|
||||
color: #555;
|
||||
}
|
||||
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.view-more {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.view-more a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.conduct-score {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.score-number {
|
||||
font-size: 64px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
/* ========== 响应式 ========== */
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.stats-grid {
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
th, td {
|
||||
padding: 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.nav {
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
padding: 10px 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.action-bar {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.search-bar {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.search-bar input {
|
||||
flex: 1;
|
||||
}
|
||||
}
|
||||
385
frontend/assets/js/admin.js
Normal file
385
frontend/assets/js/admin.js
Normal file
@@ -0,0 +1,385 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 管理端JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 全局变量
|
||||
let selectedStudentIds = [];
|
||||
let currentPage = 1;
|
||||
let totalPages = 1;
|
||||
let currentHistoryPage = 1;
|
||||
|
||||
// 显示批量加减分模态框
|
||||
function showBatchPointsModal() {
|
||||
selectedStudentIds = [];
|
||||
document.querySelectorAll('.student-checkbox:checked').forEach(cb => {
|
||||
selectedStudentIds.push(parseInt(cb.dataset.id));
|
||||
});
|
||||
|
||||
if (selectedStudentIds.length === 0) {
|
||||
showToast('请先选择学生', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
document.getElementById('selectedStudentsCount').innerHTML = `${selectedStudentIds.length} 人`;
|
||||
document.getElementById('pointsChange').value = '';
|
||||
document.getElementById('pointsReason').value = '';
|
||||
document.getElementById('batchPointsModal').style.display = 'flex';
|
||||
}
|
||||
|
||||
// 提交批量加减分
|
||||
async function submitBatchPoints() {
|
||||
const pointsChange = parseInt(document.getElementById('pointsChange').value);
|
||||
const reason = document.getElementById('pointsReason').value;
|
||||
|
||||
if (isNaN(pointsChange) || pointsChange === 0) {
|
||||
showToast('分值不能为0', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!reason.trim()) {
|
||||
showToast('请填写原因', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/add', {
|
||||
student_ids: selectedStudentIds,
|
||||
points_change: pointsChange,
|
||||
reason: reason
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast(`操作成功: ${res.data.success_count} 人成功`);
|
||||
closeModal('batchPointsModal');
|
||||
loadStudents();
|
||||
if (typeof loadConductStudents === 'function') loadConductStudents();
|
||||
} else {
|
||||
showToast(res?.message || '操作失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示导入模态框
|
||||
function showImportModal() {
|
||||
document.getElementById('importModal').style.display = 'flex';
|
||||
document.getElementById('importPreview').style.display = 'none';
|
||||
document.getElementById('importPreview').innerHTML = '';
|
||||
document.getElementById('importBtn').style.display = 'none';
|
||||
document.getElementById('importFile').value = '';
|
||||
}
|
||||
|
||||
// 预览导入文件
|
||||
function previewImportFile() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) return;
|
||||
|
||||
const reader = new FileReader();
|
||||
reader.onload = function(e) {
|
||||
try {
|
||||
const data = JSON.parse(e.target.result);
|
||||
const students = data.students || [];
|
||||
|
||||
let html = '<h4>预览数据</h4><div class="table-wrapper"><table><thead><tr>';
|
||||
html += '<th>学号</th><th>姓名</th><th>家长手机号</th><th>初始密码</th>';
|
||||
html += '</tr></thead><tbody>';
|
||||
|
||||
students.forEach(s => {
|
||||
html += `<tr>
|
||||
<td>${escapeHtml(s.student_no || '')}</td>
|
||||
<td>${escapeHtml(s.name || '')}</td>
|
||||
<td>${escapeHtml(s.parent_phone || '')}</td>
|
||||
<td>${escapeHtml(s.password || '123456')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
html += `</tbody></table></div><p>共 ${students.length} 条记录,初始操行分默认为60分</p>`;
|
||||
document.getElementById('importPreview').innerHTML = html;
|
||||
document.getElementById('importPreview').style.display = 'block';
|
||||
document.getElementById('importBtn').style.display = 'inline-block';
|
||||
} catch (error) {
|
||||
showToast('JSON格式错误', 'error');
|
||||
}
|
||||
};
|
||||
reader.readAsText(file);
|
||||
}
|
||||
|
||||
// 执行导入
|
||||
async function doImport() {
|
||||
const file = document.getElementById('importFile').files[0];
|
||||
if (!file) {
|
||||
showToast('请选择文件', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
|
||||
const token = getToken();
|
||||
const response = await fetch(`${API_BASE_URL}/api/admin/students/import`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Authorization': `Bearer ${token}`
|
||||
},
|
||||
body: formData
|
||||
});
|
||||
|
||||
const result = await response.json();
|
||||
|
||||
if (result.success) {
|
||||
showToast(result.message);
|
||||
closeModal('importModal');
|
||||
loadStudents();
|
||||
} else {
|
||||
showToast(result.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示新增学生模态框
|
||||
function showAddStudentModal() {
|
||||
document.getElementById('addStudentModal').style.display = 'flex';
|
||||
document.getElementById('addStudentForm').reset();
|
||||
}
|
||||
|
||||
// 提交新增学生
|
||||
async function submitAddStudent() {
|
||||
const studentNo = document.getElementById('studentNo').value.trim();
|
||||
const name = document.getElementById('studentName').value.trim();
|
||||
const parentPhone = document.getElementById('parentPhone').value.trim();
|
||||
|
||||
if (!studentNo || !name) {
|
||||
showToast('请填写学号和姓名', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/students', {
|
||||
student_no: studentNo,
|
||||
name: name,
|
||||
parent_phone: parentPhone
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('学生添加成功');
|
||||
closeModal('addStudentModal');
|
||||
loadStudents();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加作业模态框
|
||||
function showAddAssignmentModal() {
|
||||
document.getElementById('addAssignmentModal').style.display = 'flex';
|
||||
loadSubjectsForSelect();
|
||||
}
|
||||
|
||||
// 加载科目下拉框
|
||||
async function loadSubjectsForSelect() {
|
||||
const res = await apiGet('/api/subject/list');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">请选择科目</option>';
|
||||
res.data.subjects.forEach(s => {
|
||||
if (s.is_active) {
|
||||
html += `<option value="${s.subject_id}">${s.subject_name}</option>`;
|
||||
}
|
||||
});
|
||||
document.getElementById('assignmentSubjectId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交添加作业
|
||||
async function submitAddAssignment() {
|
||||
const subjectId = document.getElementById('assignmentSubjectId').value;
|
||||
const title = document.getElementById('assignmentTitle').value.trim();
|
||||
const description = document.getElementById('assignmentDescription').value;
|
||||
const deadline = document.getElementById('assignmentDeadline').value;
|
||||
|
||||
if (!subjectId || !title || !deadline) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/homework/assignment', {
|
||||
subject_id: parseInt(subjectId),
|
||||
title: title,
|
||||
description: description,
|
||||
deadline: deadline
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('作业发布成功');
|
||||
closeModal('addAssignmentModal');
|
||||
loadAssignments();
|
||||
} else {
|
||||
showToast(res?.message || '发布失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加考勤模态框
|
||||
async function showAddAttendanceModal() {
|
||||
await loadStudentsForSelect();
|
||||
document.getElementById('addAttendanceModal').style.display = 'flex';
|
||||
document.getElementById('attendanceDate').value = new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// 加载学生下拉框
|
||||
async function loadStudentsForSelect() {
|
||||
const res = await apiGet('/api/admin/students');
|
||||
if (res && res.success) {
|
||||
let html = '<option value="">请选择学生</option>';
|
||||
res.data.students.forEach(s => {
|
||||
html += `<option value="${s.student_id}">${s.student_no} - ${s.name}</option>`;
|
||||
});
|
||||
document.getElementById('attendanceStudentId').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
// 提交添加考勤
|
||||
async function submitAddAttendance() {
|
||||
const studentId = document.getElementById('attendanceStudentId').value;
|
||||
const date = document.getElementById('attendanceDate').value;
|
||||
const status = document.getElementById('attendanceStatus').value;
|
||||
const reason = document.getElementById('attendanceReason').value;
|
||||
const applyDeduction = document.getElementById('attendanceDeduct').checked;
|
||||
|
||||
if (!studentId || !date || !status) {
|
||||
showToast('请填写完整信息', 'warning');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/admin/attendance', {
|
||||
student_id: parseInt(studentId),
|
||||
date: date,
|
||||
status: status,
|
||||
reason: reason,
|
||||
apply_deduction: applyDeduction
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('考勤记录添加成功');
|
||||
closeModal('addAttendanceModal');
|
||||
loadAttendanceRecords();
|
||||
} else {
|
||||
showToast(res?.message || '添加失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 显示添加管理员模态框
|
||||
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 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');
|
||||
}
|
||||
}
|
||||
|
||||
// 撤销扣分记录
|
||||
async function revokeRecord(recordId) {
|
||||
if (!confirm('确定要撤销这条扣分记录吗?')) return;
|
||||
|
||||
const res = await apiPost('/api/admin/conduct/revoke', { record_id: recordId });
|
||||
if (res && res.success) {
|
||||
showToast('撤销成功');
|
||||
loadHistory(currentHistoryPage);
|
||||
} else {
|
||||
showToast(res?.message || '撤销失败', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
function closeModal(modalId) {
|
||||
const modal = document.getElementById(modalId);
|
||||
if (modal) {
|
||||
modal.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
// HTML转义
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return str.replace(/[&<>]/g, function(m) {
|
||||
if (m === '&') return '&';
|
||||
if (m === '<') return '<';
|
||||
if (m === '>') return '>';
|
||||
return m;
|
||||
});
|
||||
}
|
||||
|
||||
// 全选功能
|
||||
function toggleSelectAll() {
|
||||
const selectAll = document.getElementById('selectAll');
|
||||
if (selectAll) {
|
||||
document.querySelectorAll('.student-checkbox').forEach(cb => {
|
||||
cb.checked = selectAll.checked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 绑定文件选择事件
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
const fileInput = document.getElementById('importFile');
|
||||
if (fileInput) {
|
||||
fileInput.addEventListener('change', previewImportFile);
|
||||
}
|
||||
});
|
||||
242
frontend/assets/js/common.js
Normal file
242
frontend/assets/js/common.js
Normal file
@@ -0,0 +1,242 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 公共JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// API基础地址
|
||||
const API_BASE_URL = window.API_BASE_URL || 'http://localhost:8000';
|
||||
const JWT_STORAGE_KEY = 'class_system_token';
|
||||
const USER_STORAGE_KEY = 'class_system_user';
|
||||
|
||||
// 获取Token
|
||||
function getToken() {
|
||||
return localStorage.getItem(JWT_STORAGE_KEY);
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
function getUserInfo() {
|
||||
const userStr = localStorage.getItem(USER_STORAGE_KEY);
|
||||
if (!userStr) return null;
|
||||
try {
|
||||
return JSON.parse(userStr);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存用户信息
|
||||
function setUserInfo(user) {
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(user));
|
||||
}
|
||||
|
||||
// 清除登录信息
|
||||
function clearAuth() {
|
||||
localStorage.removeItem(JWT_STORAGE_KEY);
|
||||
localStorage.removeItem(USER_STORAGE_KEY);
|
||||
}
|
||||
|
||||
// 检查登录状态
|
||||
function checkAuth() {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
window.location.href = '/index.php';
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// API请求封装
|
||||
async function apiRequest(url, options = {}) {
|
||||
const token = getToken();
|
||||
|
||||
const headers = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers
|
||||
};
|
||||
|
||||
if (token) {
|
||||
headers['Authorization'] = `Bearer ${token}`;
|
||||
}
|
||||
|
||||
const config = {
|
||||
...options,
|
||||
headers
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}${url}`, config);
|
||||
const data = await response.json();
|
||||
|
||||
if (response.status === 401) {
|
||||
clearAuth();
|
||||
window.location.href = '/index.php';
|
||||
return null;
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('API请求错误:', error);
|
||||
showToast('网络错误,请稍后重试', 'error');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// GET请求
|
||||
async function apiGet(url, params = {}) {
|
||||
const queryString = new URLSearchParams(params).toString();
|
||||
const fullUrl = queryString ? `${url}?${queryString}` : url;
|
||||
return apiRequest(fullUrl, { method: 'GET' });
|
||||
}
|
||||
|
||||
// POST请求
|
||||
async function apiPost(url, data = {}) {
|
||||
return apiRequest(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
// PUT请求
|
||||
async function apiPut(url, data = {}) {
|
||||
return apiRequest(url, {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify(data)
|
||||
});
|
||||
}
|
||||
|
||||
// DELETE请求
|
||||
async function apiDelete(url) {
|
||||
return apiRequest(url, { method: 'DELETE' });
|
||||
}
|
||||
|
||||
// 显示提示消息
|
||||
function showToast(message, type = 'success') {
|
||||
const toast = document.createElement('div');
|
||||
toast.className = `toast toast-${type}`;
|
||||
toast.textContent = message;
|
||||
document.body.appendChild(toast);
|
||||
|
||||
setTimeout(() => {
|
||||
toast.remove();
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 格式化日期时间
|
||||
function formatDateTime(dateStr) {
|
||||
if (!dateStr) return '-';
|
||||
const date = new Date(dateStr);
|
||||
return `${date.getFullYear()}-${String(date.getMonth() + 1).padStart(2, '0')}-${String(date.getDate()).padStart(2, '0')} ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}`;
|
||||
}
|
||||
|
||||
// 获取状态标签HTML
|
||||
function getStatusBadge(status, type = 'homework') {
|
||||
const statusMap = {
|
||||
homework: {
|
||||
'submitted': '已提交',
|
||||
'not_submitted': '未提交',
|
||||
'late': '迟交'
|
||||
},
|
||||
attendance: {
|
||||
'present': '出勤',
|
||||
'absent': '缺勤',
|
||||
'late': '迟到',
|
||||
'leave': '请假'
|
||||
}
|
||||
};
|
||||
|
||||
const texts = statusMap[type] || statusMap.homework;
|
||||
const text = texts[status] || status;
|
||||
|
||||
let className = 'status-badge ';
|
||||
switch (status) {
|
||||
case 'submitted':
|
||||
case 'present':
|
||||
className += 'status-submitted';
|
||||
break;
|
||||
case 'not_submitted':
|
||||
case 'absent':
|
||||
className += 'status-not_submitted';
|
||||
break;
|
||||
case 'late':
|
||||
className += 'status-late';
|
||||
break;
|
||||
case 'leave':
|
||||
className += 'status-leave';
|
||||
break;
|
||||
default:
|
||||
className += 'status-not_submitted';
|
||||
}
|
||||
|
||||
return `<span class="${className}">${text}</span>`;
|
||||
}
|
||||
|
||||
// 退出登录
|
||||
async function logout() {
|
||||
await apiPost('/api/auth/logout');
|
||||
clearAuth();
|
||||
window.location.href = '/index.php';
|
||||
}
|
||||
|
||||
// 加载用户信息
|
||||
function loadUserInfo() {
|
||||
const user = getUserInfo();
|
||||
const userNameSpan = document.getElementById('userName');
|
||||
if (userNameSpan && user) {
|
||||
userNameSpan.textContent = user.real_name || user.username;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查是否需要修改密码
|
||||
function checkNeedChangePassword() {
|
||||
const user = getUserInfo();
|
||||
if (user && user.need_change_password) {
|
||||
const newPassword = prompt('首次登录,请设置新密码(6-20位,需包含字母和数字):');
|
||||
if (newPassword) {
|
||||
changePassword(newPassword);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 修改密码
|
||||
async function changePassword(newPassword) {
|
||||
const res = await apiPost('/api/auth/change-password', {
|
||||
old_password: newPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码修改成功,请重新登录');
|
||||
setTimeout(() => logout(), 1500);
|
||||
} else {
|
||||
showToast(res?.message || '密码修改失败', 'error');
|
||||
checkNeedChangePassword();
|
||||
}
|
||||
}
|
||||
|
||||
// 页面加载时初始化
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
loadUserInfo();
|
||||
|
||||
const logoutBtn = document.getElementById('logoutBtn');
|
||||
if (logoutBtn) {
|
||||
logoutBtn.addEventListener('click', logout);
|
||||
}
|
||||
|
||||
// 学生端检查强制修改密码
|
||||
if (window.location.pathname.includes('/student/')) {
|
||||
checkNeedChangePassword();
|
||||
}
|
||||
});
|
||||
13
frontend/assets/js/parent.js
Normal file
13
frontend/assets/js/parent.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 家长端JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 家长端专用功能
|
||||
console.log('家长端已加载');
|
||||
13
frontend/assets/js/student.js
Normal file
13
frontend/assets/js/student.js
Normal file
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端JS
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 学生端专用功能
|
||||
console.log('学生端已加载');
|
||||
44
frontend/assets/uploads/sample_import.json
Normal file
44
frontend/assets/uploads/sample_import.json
Normal file
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"_comment1": "================================================",
|
||||
"_comment2": "班级操行分管理系统 - 学生批量导入模板",
|
||||
"_comment3": "开发者: Canglan | 版权: Sea Network Technology Studio",
|
||||
"_comment4": "================================================",
|
||||
"_comment5": "字段说明:",
|
||||
"_comment6": " student_no - 必填,学生学号,唯一标识",
|
||||
"_comment7": " name - 必填,学生姓名",
|
||||
"_comment8": " parent_phone - 可选,家长手机号(11位手机号)",
|
||||
"_comment9": " password - 可选,初始密码,不填则默认 123456",
|
||||
"_comment10": "================================================",
|
||||
"_comment11": "导入规则:",
|
||||
"_comment12": " 1. 学生操行分初始值 = 60分",
|
||||
"_comment13": " 2. 学生账号 = 学号,密码 = 指定的password或123456",
|
||||
"_comment14": " 3. 家长账号 = 手机号(若parent_phone有值),密码 = 指定的password或123456",
|
||||
"_comment15": " 4. 家长姓名默认显示为 '学生姓名家长'",
|
||||
"_comment16": "================================================",
|
||||
"students": [
|
||||
{
|
||||
"student_no": "20240001",
|
||||
"name": "张三",
|
||||
"parent_phone": "13800138001",
|
||||
"password": "123456"
|
||||
},
|
||||
{
|
||||
"student_no": "20240002",
|
||||
"name": "李四",
|
||||
"parent_phone": "13800138002",
|
||||
"password": "123456"
|
||||
},
|
||||
{
|
||||
"student_no": "20240003",
|
||||
"name": "王五",
|
||||
"parent_phone": "",
|
||||
"password": ""
|
||||
},
|
||||
{
|
||||
"student_no": "20240004",
|
||||
"name": "赵六",
|
||||
"parent_phone": "13800138004",
|
||||
"password": ""
|
||||
}
|
||||
]
|
||||
}
|
||||
53
frontend/config.php
Normal file
53
frontend/config.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 前端配置
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
// 加载环境变量
|
||||
$envFile = __DIR__ . '/.env';
|
||||
if (file_exists($envFile)) {
|
||||
$lines = file($envFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
||||
foreach ($lines as $line) {
|
||||
if (strpos(trim($line), '#') === 0) {
|
||||
continue;
|
||||
}
|
||||
if (strpos($line, '=') !== false) {
|
||||
list($key, $value) = explode('=', $line, 2);
|
||||
putenv(trim($key) . '=' . trim($value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 定义常量
|
||||
define('API_BASE_URL', getenv('API_BASE_URL') ?: 'http://localhost:8000');
|
||||
define('API_TIMEOUT', (int)(getenv('API_TIMEOUT') ?: 30));
|
||||
define('JWT_STORAGE_KEY', getenv('JWT_STORAGE_KEY') ?: 'class_system_token');
|
||||
define('USER_STORAGE_KEY', getenv('USER_STORAGE_KEY') ?: 'class_system_user');
|
||||
define('SITE_NAME', getenv('SITE_NAME') ?: '班级操行分管理系统');
|
||||
define('SESSION_TIMEOUT', (int)(getenv('SESSION_TIMEOUT') ?: 30));
|
||||
|
||||
// 会话配置
|
||||
ini_set('session.cookie_httponly', 1);
|
||||
ini_set('session.use_only_cookies', 1);
|
||||
ini_set('session.cookie_secure', 1);
|
||||
session_start();
|
||||
|
||||
// 时区设置
|
||||
date_default_timezone_set('Asia/Shanghai');
|
||||
|
||||
// 错误报告(生产环境关闭)
|
||||
if (getenv('APP_ENV') === 'production') {
|
||||
error_reporting(0);
|
||||
ini_set('display_errors', 0);
|
||||
} else {
|
||||
error_reporting(E_ALL);
|
||||
ini_set('display_errors', 1);
|
||||
}
|
||||
?>
|
||||
17
frontend/includes/footer.php
Normal file
17
frontend/includes/footer.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 公共底部
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
?>
|
||||
<div class="footer">
|
||||
<p>© <?php echo date('Y'); ?> Sea Network Technology Studio</p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
49
frontend/includes/header.php
Normal file
49
frontend/includes/header.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 公共头部
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
if (!isset($_SESSION)) {
|
||||
session_start();
|
||||
}
|
||||
|
||||
$current_page = basename($_SERVER['PHP_SELF'], '.php');
|
||||
$user_type = $_SESSION['user_type'] ?? '';
|
||||
$role = $_SESSION['role'] ?? '';
|
||||
$page_title = $page_title ?? '首页';
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title><?php echo SITE_NAME; ?> - <?php echo $page_title; ?></title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<?php if ($user_type === 'admin'): ?>
|
||||
<link rel="stylesheet" href="/assets/css/admin.css">
|
||||
<?php endif; ?>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><?php echo SITE_NAME; ?></h1>
|
||||
<div class="header-info">
|
||||
<span class="user-name" id="userName"><?php echo htmlspecialchars($_SESSION['real_name'] ?? ''); ?></span>
|
||||
<?php if ($role): ?>
|
||||
<span class="user-role">(<?php echo htmlspecialchars($role); ?>)</span>
|
||||
<?php endif; ?>
|
||||
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
|
||||
const JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
|
||||
const USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
|
||||
</script>
|
||||
<script src="/assets/js/common.js"></script>
|
||||
112
frontend/index.php
Normal file
112
frontend/index.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?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']) && isset($_SESSION['user_type'])) {
|
||||
$redirect = [
|
||||
'student' => '/student/dashboard.php',
|
||||
'parent' => '/parent/dashboard.php',
|
||||
'admin' => '/admin/dashboard.php'
|
||||
];
|
||||
$target = $redirect[$_SESSION['user_type']] ?? '/index.php';
|
||||
header("Location: $target");
|
||||
exit();
|
||||
}
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
|
||||
<title><?php echo SITE_NAME; ?> - 登录</title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div class="login-container">
|
||||
<div class="login-header">
|
||||
<h1><?php echo SITE_NAME; ?></h1>
|
||||
<p>学生 / 家长 / 管理端 统一登录</p>
|
||||
</div>
|
||||
|
||||
<form id="loginForm" class="login-form">
|
||||
<div class="form-group">
|
||||
<label>用户名</label>
|
||||
<input type="text" id="username" name="username" required autocomplete="off" placeholder="请输入用户名">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>密码</label>
|
||||
<input type="password" id="password" name="password" required placeholder="请输入密码">
|
||||
</div>
|
||||
<button type="submit" class="btn-login">登 录</button>
|
||||
<div id="errorMsg" class="error-msg" style="display: none;"></div>
|
||||
</form>
|
||||
|
||||
<div class="login-footer">
|
||||
<p>© Sea Network Technology Studio</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
|
||||
const JWT_STORAGE_KEY = '<?php echo JWT_STORAGE_KEY; ?>';
|
||||
const USER_STORAGE_KEY = '<?php echo USER_STORAGE_KEY; ?>';
|
||||
|
||||
document.getElementById('loginForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const username = document.getElementById('username').value.trim();
|
||||
const password = document.getElementById('password').value;
|
||||
const errorMsg = document.getElementById('errorMsg');
|
||||
|
||||
if (!username || !password) {
|
||||
showError('请填写用户名和密码');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(`${API_BASE_URL}/api/auth/login`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ username, password })
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.data) {
|
||||
// 存储Token和用户信息
|
||||
localStorage.setItem(JWT_STORAGE_KEY, data.data.token);
|
||||
localStorage.setItem(USER_STORAGE_KEY, JSON.stringify(data.data));
|
||||
|
||||
// 跳转
|
||||
window.location.href = data.data.redirect;
|
||||
} else {
|
||||
showError(data.message || '登录失败');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('登录错误:', error);
|
||||
showError('网络错误,请检查后端服务是否启动');
|
||||
}
|
||||
});
|
||||
|
||||
function showError(msg) {
|
||||
const errorMsg = document.getElementById('errorMsg');
|
||||
errorMsg.textContent = msg;
|
||||
errorMsg.style.display = 'block';
|
||||
setTimeout(() => {
|
||||
errorMsg.style.display = 'none';
|
||||
}, 3000);
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
83
frontend/parent/attendance.php
Normal file
83
frontend/parent/attendance.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?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">作业情况</a>
|
||||
<a href="/parent/attendance.php" class="nav-item active">考勤记录</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<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 class="table">
|
||||
<thead><tr><th>日期</th><th>状态</th><th>原因</th></tr></thead>
|
||||
<tbody id="attendanceList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
async function loadAttendance() {
|
||||
const res = await apiGet('/api/parent/child/attendance');
|
||||
if (res && res.success) {
|
||||
let present = 0, absent = 0, late = 0, leave = 0;
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
html += `<tr>
|
||||
<td>${record.date}</td>
|
||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||
<td>${escapeHtml(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 (res.data.records.length === 0) {
|
||||
html = '<tr><td colspan="3" style="text-align:center;">暂无记录</td></tr>';
|
||||
}
|
||||
document.getElementById('attendanceList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
loadAttendance();
|
||||
</script>
|
||||
<script src="/assets/js/parent.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
272
frontend/parent/dashboard.php
Normal file
272
frontend/parent/dashboard.php
Normal file
@@ -0,0 +1,272 @@
|
||||
<?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();
|
||||
}
|
||||
|
||||
$student_id = $_SESSION['student_id'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo SITE_NAME; ?> - 家长端</title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<style>
|
||||
.child-info {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
border-radius: 12px;
|
||||
color: white;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.child-name {
|
||||
font-size: 24px;
|
||||
font-weight: bold;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.child-no {
|
||||
font-size: 14px;
|
||||
opacity: 0.9;
|
||||
}
|
||||
.conduct-score {
|
||||
text-align: center;
|
||||
padding: 30px;
|
||||
}
|
||||
.score-number {
|
||||
font-size: 72px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><?php echo SITE_NAME; ?> - 家长端</h1>
|
||||
<div class="header-info">
|
||||
<span class="user-name" id="userName"></span>
|
||||
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<button class="nav-item active" data-page="dashboard">首页</button>
|
||||
<button class="nav-item" data-page="homework">作业情况</button>
|
||||
<button class="nav-item" data-page="attendance">考勤记录</button>
|
||||
</div>
|
||||
|
||||
<div class="container" id="pageContainer">
|
||||
<!-- 首页内容 -->
|
||||
<div id="page-dashboard" class="page-content">
|
||||
<div class="child-info" id="childInfo">
|
||||
<div class="child-name" id="childName">--</div>
|
||||
<div class="child-no" id="childNo">--</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="conduct-score">
|
||||
<div class="score-number" id="totalPoints">--</div>
|
||||
<div class="score-label">当前操行分</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作业情况页 -->
|
||||
<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>
|
||||
66
frontend/parent/homework.php
Normal file
66
frontend/parent/homework.php
Normal file
@@ -0,0 +1,66 @@
|
||||
<?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'; ?>
|
||||
83
frontend/student/attendance.php
Normal file
83
frontend/student/attendance.php
Normal file
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端考勤记录
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '考勤记录';
|
||||
$student_id = $_SESSION['student_id'];
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/student/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/student/conduct.php" class="nav-item">操行分</a>
|
||||
<a href="/student/homework.php" class="nav-item">作业</a>
|
||||
<a href="/student/attendance.php" class="nav-item active">考勤</a>
|
||||
<a href="/student/password.php" class="nav-item">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<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 class="table">
|
||||
<thead><tr><th>日期</th><th>状态</th><th>原因</th></tr></thead>
|
||||
<tbody id="attendanceList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const STUDENT_ID = <?php echo $student_id; ?>;
|
||||
|
||||
async function loadAttendance() {
|
||||
const res = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
|
||||
if (res && res.success) {
|
||||
const stats = res.data.statistics;
|
||||
document.getElementById('attPresent').textContent = stats.present || 0;
|
||||
document.getElementById('attAbsent').textContent = stats.absent || 0;
|
||||
document.getElementById('attLate').textContent = stats.late || 0;
|
||||
document.getElementById('attLeave').textContent = stats.leave || 0;
|
||||
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
html += `<tr>
|
||||
<td>${record.date}</td>
|
||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||
<td>${escapeHtml(record.reason || '-')}</td>
|
||||
</tr>`;
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
html = '<tr><td colspan="3" style="text-align:center;">暂无记录</td></tr>';
|
||||
}
|
||||
document.getElementById('attendanceList').innerHTML = html;
|
||||
}
|
||||
}
|
||||
|
||||
loadAttendance();
|
||||
</script>
|
||||
<script src="/assets/js/student.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
0
frontend/student/conduct_history.php
Normal file
0
frontend/student/conduct_history.php
Normal file
513
frontend/student/dashboard.php
Normal file
513
frontend/student/dashboard.php
Normal file
@@ -0,0 +1,513 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端主页
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
// 检查登录状态
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$student_id = $_SESSION['student_id'];
|
||||
?>
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title><?php echo SITE_NAME; ?> - 学生端</title>
|
||||
<link rel="stylesheet" href="/assets/css/style.css">
|
||||
<style>
|
||||
.conduct-score {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
}
|
||||
.score-number {
|
||||
font-size: 64px;
|
||||
font-weight: bold;
|
||||
color: #667eea;
|
||||
}
|
||||
.score-label {
|
||||
color: #666;
|
||||
margin-top: 8px;
|
||||
}
|
||||
.record-item {
|
||||
padding: 12px 0;
|
||||
border-bottom: 1px solid #eee;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
.record-points {
|
||||
font-weight: bold;
|
||||
}
|
||||
.record-points.plus {
|
||||
color: #38a169;
|
||||
}
|
||||
.record-points.minus {
|
||||
color: #e53e3e;
|
||||
}
|
||||
.record-reason {
|
||||
flex: 1;
|
||||
margin: 0 15px;
|
||||
color: #555;
|
||||
}
|
||||
.record-time {
|
||||
font-size: 12px;
|
||||
color: #999;
|
||||
}
|
||||
.view-more {
|
||||
text-align: center;
|
||||
margin-top: 15px;
|
||||
}
|
||||
.view-more a {
|
||||
color: #667eea;
|
||||
text-decoration: none;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="header">
|
||||
<h1><?php echo SITE_NAME; ?> - 学生端</h1>
|
||||
<div class="header-info">
|
||||
<span class="user-name" id="userName"></span>
|
||||
<button class="btn-logout" id="logoutBtn">退出登录</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="nav">
|
||||
<button class="nav-item active" data-page="dashboard">首页</button>
|
||||
<button class="nav-item" data-page="conduct">操行分详情</button>
|
||||
<button class="nav-item" data-page="homework">作业情况</button>
|
||||
<button class="nav-item" data-page="attendance">考勤记录</button>
|
||||
<button class="nav-item" data-page="password">修改密码</button>
|
||||
</div>
|
||||
|
||||
<div class="container" id="pageContainer">
|
||||
<!-- 首页内容 -->
|
||||
<div id="page-dashboard" class="page-content">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">当前操行分</div>
|
||||
<div class="stat-value" id="totalPoints">--</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">作业完成率</div>
|
||||
<div class="stat-value" id="homeworkRate">--%</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">本月出勤率</div>
|
||||
<div class="stat-value" id="attendanceRate">--%</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<div class="card-title">最新操行分记录</div>
|
||||
<div id="recentRecords"></div>
|
||||
<div class="view-more">
|
||||
<a href="#" onclick="showPage('conduct'); return false;">查看更多 ></a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操行分详情页 -->
|
||||
<div id="page-conduct" class="page-content" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="conduct-score">
|
||||
<div class="score-number" id="conductTotalPoints">--</div>
|
||||
<div class="score-label">当前操行分</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">历史记录</div>
|
||||
<div id="conductRecords"></div>
|
||||
<div class="pagination" id="conductPagination"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 作业情况页 -->
|
||||
<div id="page-homework" class="page-content" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-title">作业列表</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>科目</th>
|
||||
<th>作业标题</th>
|
||||
<th>截止日期</th>
|
||||
<th>状态</th>
|
||||
<th>备注</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="homeworkList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 考勤记录页 -->
|
||||
<div id="page-attendance" class="page-content" style="display: none;">
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">出勤</div>
|
||||
<div class="stat-value" id="attPresent">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">缺勤</div>
|
||||
<div class="stat-value" id="attAbsent">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">迟到</div>
|
||||
<div class="stat-value" id="attLate">0</div>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<div class="stat-label">请假</div>
|
||||
<div class="stat-value" id="attLeave">0</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="card-title">考勤记录明细</div>
|
||||
<div class="table-wrapper">
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>日期</th>
|
||||
<th>状态</th>
|
||||
<th>原因</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="attendanceList"></tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码页 -->
|
||||
<div id="page-password" class="page-content" style="display: none;">
|
||||
<div class="card">
|
||||
<div class="card-title">修改密码</div>
|
||||
<form id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label>原密码</label>
|
||||
<input type="password" id="oldPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>新密码</label>
|
||||
<input type="password" id="newPassword" required>
|
||||
<small>密码长度6-20位,需包含字母和数字</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认新密码</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 修改密码模态框(首次登录强制) -->
|
||||
<div id="forceChangePasswordModal" class="modal" style="display: none;">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h3>首次登录,请修改密码</h3>
|
||||
</div>
|
||||
<form id="forcePasswordForm">
|
||||
<div class="form-group">
|
||||
<label>新密码</label>
|
||||
<input type="password" id="forceNewPassword" required>
|
||||
<small>密码长度6-20位,需包含字母和数字</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认新密码</label>
|
||||
<input type="password" id="forceConfirmPassword" required>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
const API_BASE_URL = '<?php echo API_BASE_URL; ?>';
|
||||
const STUDENT_ID = <?php echo $student_id; ?>;
|
||||
|
||||
let conductPage = 1;
|
||||
let conductTotalPages = 1;
|
||||
|
||||
// 页面切换
|
||||
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 'conduct':
|
||||
loadConductHistory();
|
||||
break;
|
||||
case 'homework':
|
||||
loadHomework();
|
||||
break;
|
||||
case 'attendance':
|
||||
loadAttendance();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// 加载首页
|
||||
async function loadDashboard() {
|
||||
try {
|
||||
// 获取操行分
|
||||
const conductRes = await apiGet(`/api/student/conduct/${STUDENT_ID}`);
|
||||
if (conductRes && conductRes.success) {
|
||||
document.getElementById('totalPoints').textContent = conductRes.data.total_points;
|
||||
|
||||
// 显示最近5条记录
|
||||
const records = conductRes.data.records.slice(0, 5);
|
||||
let html = '';
|
||||
records.forEach(record => {
|
||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
html += `
|
||||
<div class="record-item">
|
||||
<span class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</span>
|
||||
<span class="record-reason">${record.reason}</span>
|
||||
<span class="record-time">${formatDate(record.created_at)}</span>
|
||||
</div>
|
||||
`;
|
||||
});
|
||||
if (records.length === 0) {
|
||||
html = '<div style="text-align:center;padding:20px;color:#999;">暂无记录</div>';
|
||||
}
|
||||
document.getElementById('recentRecords').innerHTML = html;
|
||||
}
|
||||
|
||||
// 获取作业统计
|
||||
const homeworkRes = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||
if (homeworkRes && homeworkRes.success) {
|
||||
const stats = homeworkRes.data.statistics;
|
||||
const rate = stats.total > 0 ? Math.round(stats.submitted / stats.total * 100) : 0;
|
||||
document.getElementById('homeworkRate').textContent = `${rate}%`;
|
||||
}
|
||||
|
||||
// 获取考勤统计
|
||||
const attendanceRes = await apiGet(`/api/student/attendance/${STUDENT_ID}`);
|
||||
if (attendanceRes && attendanceRes.success) {
|
||||
const stats = attendanceRes.data.statistics;
|
||||
const total = stats.present + stats.absent + stats.late + stats.leave;
|
||||
const rate = total > 0 ? Math.round(stats.present / total * 100) : 100;
|
||||
document.getElementById('attendanceRate').textContent = `${rate}%`;
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载首页失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// 加载操行分历史
|
||||
async function loadConductHistory(page = 1) {
|
||||
conductPage = page;
|
||||
try {
|
||||
const res = await apiGet(`/api/student/conduct/${STUDENT_ID}`, {
|
||||
limit: 20,
|
||||
offset: (page - 1) * 20
|
||||
});
|
||||
if (res && res.success) {
|
||||
document.getElementById('conductTotalPoints').textContent = res.data.total_points;
|
||||
|
||||
let html = '<div class="table-wrapper"><table><thead><tr><th>时间</th><th>分数变动</th><th>原因</th><th>操作人</th></tr></thead><tbody>';
|
||||
res.data.records.forEach(record => {
|
||||
const pointsClass = record.points_change > 0 ? 'plus' : 'minus';
|
||||
html += `
|
||||
<tr>
|
||||
<td>${formatDateTime(record.created_at)}</td>
|
||||
<td class="record-points ${pointsClass}">${record.points_change > 0 ? '+' : ''}${record.points_change}</td>
|
||||
<td>${record.reason}</td>
|
||||
<td>${record.recorder_name}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
if (res.data.records.length === 0) {
|
||||
html += '<tr><td colspan="4" style="text-align:center;">暂无记录</td></tr>';
|
||||
}
|
||||
html += '</tbody></table></div>';
|
||||
document.getElementById('conductRecords').innerHTML = html;
|
||||
|
||||
// 分页
|
||||
const total = res.data.total || res.data.records.length;
|
||||
conductTotalPages = Math.ceil(total / 20);
|
||||
renderConductPagination();
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载操行分历史失败:', error);
|
||||
}
|
||||
}
|
||||
|
||||
function renderConductPagination() {
|
||||
const container = document.getElementById('conductPagination');
|
||||
if (!container) return;
|
||||
if (conductTotalPages <= 1) {
|
||||
container.innerHTML = '';
|
||||
return;
|
||||
}
|
||||
let html = '';
|
||||
for (let i = 1; i <= conductTotalPages; i++) {
|
||||
if (i === conductPage) {
|
||||
html += `<span class="active">${i}</span>`;
|
||||
} else {
|
||||
html += `<a href="#" onclick="loadConductHistory(${i}); return false;">${i}</a>`;
|
||||
}
|
||||
}
|
||||
container.innerHTML = html;
|
||||
}
|
||||
|
||||
// 加载作业
|
||||
async function loadHomework() {
|
||||
try {
|
||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||
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/student/attendance/${STUDENT_ID}`);
|
||||
if (res && res.success) {
|
||||
const stats = res.data.statistics;
|
||||
document.getElementById('attPresent').textContent = stats.present || 0;
|
||||
document.getElementById('attAbsent').textContent = stats.absent || 0;
|
||||
document.getElementById('attLate').textContent = stats.late || 0;
|
||||
document.getElementById('attLeave').textContent = stats.leave || 0;
|
||||
|
||||
let html = '';
|
||||
res.data.records.forEach(record => {
|
||||
html += `
|
||||
<tr>
|
||||
<td>${record.date}</td>
|
||||
<td>${getStatusBadge(record.status, 'attendance')}</td>
|
||||
<td>${record.reason || '-'}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
if (res.data.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.getElementById('passwordForm')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const oldPassword = document.getElementById('oldPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast('两次输入的新密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/auth/change-password', {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码修改成功,请重新登录');
|
||||
setTimeout(() => logout(), 1500);
|
||||
} else {
|
||||
showToast(res?.message || '密码修改失败', 'error');
|
||||
}
|
||||
});
|
||||
|
||||
// 强制修改密码
|
||||
document.getElementById('forcePasswordForm')?.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const newPassword = document.getElementById('forceNewPassword').value;
|
||||
const confirmPassword = document.getElementById('forceConfirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
alert('两次输入的新密码不一致');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/auth/change-password', {
|
||||
old_password: newPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码修改成功,请重新登录');
|
||||
setTimeout(() => logout(), 1500);
|
||||
} else {
|
||||
alert(res?.message || '密码修改失败');
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否需要强制修改密码
|
||||
function checkForceChangePassword() {
|
||||
const user = getUserInfo();
|
||||
if (user && user.need_change_password) {
|
||||
document.getElementById('forceChangePasswordModal').style.display = 'flex';
|
||||
}
|
||||
}
|
||||
|
||||
// 初始化
|
||||
document.querySelectorAll('.nav-item').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
showPage(btn.dataset.page);
|
||||
});
|
||||
});
|
||||
|
||||
loadDashboard();
|
||||
checkForceChangePassword();
|
||||
</script>
|
||||
<script src="/assets/js/common.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
74
frontend/student/homework.php
Normal file
74
frontend/student/homework.php
Normal file
@@ -0,0 +1,74 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端作业情况
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '作业情况';
|
||||
$student_id = $_SESSION['student_id'];
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/student/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/student/conduct.php" class="nav-item">操行分</a>
|
||||
<a href="/student/homework.php" class="nav-item active">作业</a>
|
||||
<a href="/student/attendance.php" class="nav-item">考勤</a>
|
||||
<a href="/student/password.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>
|
||||
const STUDENT_ID = <?php echo $student_id; ?>;
|
||||
|
||||
async function loadHomework() {
|
||||
const res = await apiGet(`/api/student/homework/${STUDENT_ID}`);
|
||||
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/student.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
81
frontend/student/password.php
Normal file
81
frontend/student/password.php
Normal file
@@ -0,0 +1,81 @@
|
||||
<?php
|
||||
/**
|
||||
* 班级操行分管理系统 - 学生端修改密码
|
||||
*
|
||||
* 开发者: Canglan
|
||||
* 联系方式: admin@sea-studio.top
|
||||
* 版权归属: Sea Network Technology Studio
|
||||
* 许可证: MIT License
|
||||
*
|
||||
* 版权所有 © Sea Network Technology Studio
|
||||
*/
|
||||
|
||||
require_once __DIR__ . '/../config.php';
|
||||
|
||||
if (!isset($_SESSION['user_id']) || $_SESSION['user_type'] !== 'student') {
|
||||
header('Location: /index.php');
|
||||
exit();
|
||||
}
|
||||
|
||||
$page_title = '修改密码';
|
||||
include __DIR__ . '/../includes/header.php';
|
||||
?>
|
||||
|
||||
<div class="nav">
|
||||
<a href="/student/dashboard.php" class="nav-item">首页</a>
|
||||
<a href="/student/conduct.php" class="nav-item">操行分</a>
|
||||
<a href="/student/homework.php" class="nav-item">作业</a>
|
||||
<a href="/student/attendance.php" class="nav-item">考勤</a>
|
||||
<a href="/student/password.php" class="nav-item active">修改密码</a>
|
||||
</div>
|
||||
|
||||
<div class="container">
|
||||
<div class="card">
|
||||
<div class="card-title">修改密码</div>
|
||||
<form id="passwordForm">
|
||||
<div class="form-group">
|
||||
<label>原密码</label>
|
||||
<input type="password" id="oldPassword" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>新密码</label>
|
||||
<input type="password" id="newPassword" required>
|
||||
<small>密码长度6-20位,需包含字母和数字</small>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label>确认新密码</label>
|
||||
<input type="password" id="confirmPassword" required>
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary">确认修改</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
document.getElementById('passwordForm').addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
const oldPassword = document.getElementById('oldPassword').value;
|
||||
const newPassword = document.getElementById('newPassword').value;
|
||||
const confirmPassword = document.getElementById('confirmPassword').value;
|
||||
|
||||
if (newPassword !== confirmPassword) {
|
||||
showToast('两次输入的新密码不一致', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const res = await apiPost('/api/auth/change-password', {
|
||||
old_password: oldPassword,
|
||||
new_password: newPassword
|
||||
});
|
||||
|
||||
if (res && res.success) {
|
||||
showToast('密码修改成功,请重新登录');
|
||||
setTimeout(() => logout(), 1500);
|
||||
} else {
|
||||
showToast(res?.message || '密码修改失败', 'error');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
<script src="/assets/js/student.js"></script>
|
||||
|
||||
<?php include __DIR__ . '/../includes/footer.php'; ?>
|
||||
331
sql/init.sql
Normal file
331
sql/init.sql
Normal file
@@ -0,0 +1,331 @@
|
||||
-- ===========================================
|
||||
-- 班级操行分管理系统 - 数据库初始化脚本
|
||||
-- ===========================================
|
||||
-- 开发者: Canglan
|
||||
-- 联系方式: admin@sea-studio.top
|
||||
-- 版权归属: Sea Network Technology Studio
|
||||
-- 许可证: MIT License
|
||||
-- 版权所有: Copyright (c) 2024 Sea Network Technology Studio
|
||||
-- ===========================================
|
||||
-- 数据库: classmanagerdb
|
||||
-- 字符集: utf8mb4
|
||||
-- ===========================================
|
||||
|
||||
-- 创建数据库(如果不存在)
|
||||
CREATE DATABASE IF NOT EXISTS `classmanagerdb`
|
||||
CHARACTER SET utf8mb4
|
||||
COLLATE utf8mb4_unicode_ci;
|
||||
|
||||
USE `classmanagerdb`;
|
||||
|
||||
-- ===========================================
|
||||
-- 1. 班级表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `classes`;
|
||||
CREATE TABLE `classes` (
|
||||
`class_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '班级ID',
|
||||
`class_name` VARCHAR(50) NOT NULL COMMENT '班级名称',
|
||||
`grade` VARCHAR(20) DEFAULT NULL COMMENT '年级',
|
||||
`academic_year` VARCHAR(20) DEFAULT NULL COMMENT '学年',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_class_name` (`class_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='班级表';
|
||||
|
||||
-- ===========================================
|
||||
-- 2. 科目表(支持动态增删)
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `subjects`;
|
||||
CREATE TABLE `subjects` (
|
||||
`subject_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '科目ID',
|
||||
`subject_name` VARCHAR(50) NOT NULL COMMENT '科目名称',
|
||||
`subject_code` VARCHAR(20) DEFAULT NULL COMMENT '科目代码',
|
||||
`is_active` TINYINT DEFAULT 1 COMMENT '是否启用(0禁用/1启用)',
|
||||
`sort_order` INT DEFAULT 0 COMMENT '排序序号',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_active` (`is_active`),
|
||||
UNIQUE KEY `uk_subject_name` (`subject_name`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='科目表';
|
||||
|
||||
-- ===========================================
|
||||
-- 3. 学生表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `students`;
|
||||
CREATE TABLE `students` (
|
||||
`student_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '学生ID',
|
||||
`student_no` VARCHAR(20) NOT NULL COMMENT '学号',
|
||||
`name` VARCHAR(50) NOT NULL COMMENT '学生姓名',
|
||||
`class_id` INT NOT NULL COMMENT '班级ID',
|
||||
`total_points` INT DEFAULT 100 COMMENT '操行分总分',
|
||||
`parent_phone` VARCHAR(20) DEFAULT NULL COMMENT '家长手机号',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '状态(0离校/1在校)',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE RESTRICT,
|
||||
UNIQUE KEY `uk_student_no` (`student_no`),
|
||||
INDEX `idx_class_id` (`class_id`),
|
||||
INDEX `idx_parent_phone` (`parent_phone`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='学生表';
|
||||
|
||||
-- ===========================================
|
||||
-- 4. 用户表(统一账号)
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `users`;
|
||||
CREATE TABLE `users` (
|
||||
`user_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '用户ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
|
||||
`password_hash` VARCHAR(64) NOT NULL COMMENT '密码哈希(SHA1+MD5)',
|
||||
`real_name` VARCHAR(50) NOT NULL COMMENT '真实姓名',
|
||||
`user_type` ENUM('student', 'parent', 'admin') NOT NULL COMMENT '用户类型',
|
||||
`student_id` INT DEFAULT NULL COMMENT '关联学生ID(学生/家长)',
|
||||
`status` TINYINT DEFAULT 1 COMMENT '状态(0禁用/1启用)',
|
||||
`need_change_password` TINYINT DEFAULT 1 COMMENT '是否需要修改密码(1需要/0不需要)',
|
||||
`last_login_time` DATETIME DEFAULT NULL COMMENT '最后登录时间',
|
||||
`last_login_ip` VARCHAR(45) DEFAULT NULL COMMENT '最后登录IP',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
INDEX `idx_user_type` (`user_type`),
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户表';
|
||||
|
||||
-- ===========================================
|
||||
-- 5. 管理员角色表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `admin_roles`;
|
||||
CREATE TABLE `admin_roles` (
|
||||
`admin_role_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '管理员角色ID',
|
||||
`user_id` INT NOT NULL COMMENT '用户ID',
|
||||
`role_type` ENUM('班主任', '班长', '科代表', '考勤委员', '劳动委员') NOT NULL COMMENT '角色类型',
|
||||
`class_id` INT NOT NULL COMMENT '管理的班级ID',
|
||||
`subject_id` INT DEFAULT NULL COMMENT '关联科目ID(科代表专用)',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE CASCADE,
|
||||
INDEX `idx_user_id` (`user_id`),
|
||||
INDEX `idx_role_type` (`role_type`),
|
||||
UNIQUE KEY `uk_user_class_subject` (`user_id`, `class_id`, `subject_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='管理员角色表';
|
||||
|
||||
-- ===========================================
|
||||
-- 6. 操行分记录表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `conduct_records`;
|
||||
CREATE TABLE `conduct_records` (
|
||||
`record_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '记录ID',
|
||||
`student_id` INT NOT NULL COMMENT '学生ID',
|
||||
`points_change` INT NOT NULL COMMENT '分数变动(正数加分,负数扣分)',
|
||||
`reason` VARCHAR(255) NOT NULL COMMENT '变动原因',
|
||||
`recorder_id` INT NOT NULL COMMENT '操作人ID',
|
||||
`recorder_name` VARCHAR(50) DEFAULT NULL COMMENT '操作人姓名(冗余字段)',
|
||||
`related_type` ENUM('manual', 'homework', 'attendance') DEFAULT 'manual' COMMENT '关联类型',
|
||||
`related_id` INT DEFAULT NULL COMMENT '关联ID(作业ID/考勤ID)',
|
||||
`is_revoked` TINYINT DEFAULT 0 COMMENT '是否已撤销(0未撤销/1已撤销)',
|
||||
`revoked_by` INT DEFAULT NULL COMMENT '撤销人ID',
|
||||
`revoked_at` DATETIME DEFAULT NULL COMMENT '撤销时间',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (`revoked_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
INDEX `idx_recorder_id` (`recorder_id`),
|
||||
INDEX `idx_created_at` (`created_at`),
|
||||
INDEX `idx_related` (`related_type`, `related_id`),
|
||||
INDEX `idx_is_revoked` (`is_revoked`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操行分记录表';
|
||||
|
||||
-- ===========================================
|
||||
-- 7. 作业表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `assignments`;
|
||||
CREATE TABLE `assignments` (
|
||||
`assignment_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '作业ID',
|
||||
`class_id` INT NOT NULL COMMENT '班级ID',
|
||||
`subject_id` INT NOT NULL COMMENT '科目ID',
|
||||
`title` VARCHAR(100) NOT NULL COMMENT '作业标题',
|
||||
`description` TEXT COMMENT '作业描述',
|
||||
`deadline` DATE NOT NULL COMMENT '截止日期',
|
||||
`created_by` INT NOT NULL COMMENT '发布人ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
FOREIGN KEY (`class_id`) REFERENCES `classes`(`class_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`subject_id`) REFERENCES `subjects`(`subject_id`) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (`created_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
|
||||
INDEX `idx_class_id` (`class_id`),
|
||||
INDEX `idx_deadline` (`deadline`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作业表';
|
||||
|
||||
-- ===========================================
|
||||
-- 8. 作业提交记录表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `homework_submissions`;
|
||||
CREATE TABLE `homework_submissions` (
|
||||
`submission_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '提交记录ID',
|
||||
`assignment_id` INT NOT NULL COMMENT '作业ID',
|
||||
`student_id` INT NOT NULL COMMENT '学生ID',
|
||||
`status` ENUM('submitted', 'not_submitted', 'late') DEFAULT 'not_submitted' COMMENT '提交状态',
|
||||
`submit_time` DATETIME DEFAULT NULL COMMENT '提交时间',
|
||||
`comments` TEXT COMMENT '备注',
|
||||
`deduction_applied` TINYINT DEFAULT 0 COMMENT '是否已应用扣分',
|
||||
`deduction_record_id` BIGINT DEFAULT NULL COMMENT '关联的扣分记录ID',
|
||||
`updated_by` INT DEFAULT NULL COMMENT '最后更新人ID',
|
||||
`updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
|
||||
FOREIGN KEY (`assignment_id`) REFERENCES `assignments`(`assignment_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`updated_by`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
|
||||
INDEX `idx_assignment_id` (`assignment_id`),
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
INDEX `idx_status` (`status`),
|
||||
UNIQUE KEY `uk_assignment_student` (`assignment_id`, `student_id`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='作业提交记录表';
|
||||
|
||||
-- ===========================================
|
||||
-- 9. 考勤记录表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `attendance_records`;
|
||||
CREATE TABLE `attendance_records` (
|
||||
`attendance_id` INT PRIMARY KEY AUTO_INCREMENT COMMENT '考勤记录ID',
|
||||
`student_id` INT NOT NULL COMMENT '学生ID',
|
||||
`date` DATE NOT NULL COMMENT '考勤日期',
|
||||
`status` ENUM('present', 'absent', 'late', 'leave') DEFAULT 'present' COMMENT '考勤状态',
|
||||
`reason` VARCHAR(255) DEFAULT NULL COMMENT '原因',
|
||||
`recorder_id` INT NOT NULL COMMENT '记录人ID',
|
||||
`deduction_applied` TINYINT DEFAULT 0 COMMENT '是否已应用扣分',
|
||||
`deduction_record_id` BIGINT DEFAULT NULL COMMENT '关联的扣分记录ID',
|
||||
`created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
FOREIGN KEY (`student_id`) REFERENCES `students`(`student_id`) ON DELETE CASCADE,
|
||||
FOREIGN KEY (`recorder_id`) REFERENCES `users`(`user_id`) ON DELETE RESTRICT,
|
||||
FOREIGN KEY (`deduction_record_id`) REFERENCES `conduct_records`(`record_id`) ON DELETE SET NULL,
|
||||
INDEX `idx_student_id` (`student_id`),
|
||||
INDEX `idx_date` (`date`),
|
||||
INDEX `idx_status` (`status`),
|
||||
UNIQUE KEY `uk_student_date` (`student_id`, `date`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='考勤记录表';
|
||||
|
||||
-- ===========================================
|
||||
-- 10. 操作日志表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `operation_logs`;
|
||||
CREATE TABLE `operation_logs` (
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
|
||||
`operator_id` INT NOT NULL COMMENT '操作人ID',
|
||||
`operator_name` VARCHAR(50) DEFAULT NULL COMMENT '操作人姓名',
|
||||
`operator_role` VARCHAR(50) DEFAULT NULL COMMENT '操作人角色',
|
||||
`operation_type` VARCHAR(50) NOT NULL COMMENT '操作类型',
|
||||
`target_type` VARCHAR(50) DEFAULT NULL COMMENT '目标类型',
|
||||
`target_id` INT DEFAULT NULL COMMENT '目标ID',
|
||||
`details` TEXT COMMENT '详细信息',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_operator_id` (`operator_id`),
|
||||
INDEX `idx_operation_type` (`operation_type`),
|
||||
INDEX `idx_created_at` (`created_at`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='操作日志表';
|
||||
|
||||
-- ===========================================
|
||||
-- 11. 登录日志表
|
||||
-- ===========================================
|
||||
DROP TABLE IF EXISTS `login_logs`;
|
||||
CREATE TABLE `login_logs` (
|
||||
`log_id` BIGINT PRIMARY KEY AUTO_INCREMENT COMMENT '日志ID',
|
||||
`username` VARCHAR(50) NOT NULL COMMENT '登录账号',
|
||||
`login_result` TINYINT NOT NULL COMMENT '登录结果(0失败/1成功)',
|
||||
`fail_reason` VARCHAR(100) DEFAULT NULL COMMENT '失败原因',
|
||||
`ip_address` VARCHAR(45) DEFAULT NULL COMMENT 'IP地址',
|
||||
`user_agent` VARCHAR(255) DEFAULT NULL COMMENT '用户代理',
|
||||
`created_at` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
|
||||
INDEX `idx_username` (`username`),
|
||||
INDEX `idx_created_at` (`created_at`),
|
||||
INDEX `idx_login_result` (`login_result`)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='登录日志表';
|
||||
|
||||
-- ===========================================
|
||||
-- 创建存储过程:撤销扣分记录
|
||||
-- ===========================================
|
||||
DROP PROCEDURE IF EXISTS `revoke_conduct_record`;
|
||||
DELIMITER //
|
||||
|
||||
CREATE PROCEDURE `revoke_conduct_record`(
|
||||
IN p_record_id BIGINT,
|
||||
IN p_revoker_id INT
|
||||
)
|
||||
BEGIN
|
||||
DECLARE v_student_id INT;
|
||||
DECLARE v_points_change INT;
|
||||
DECLARE v_is_revoked TINYINT;
|
||||
|
||||
SELECT `student_id`, `points_change`, `is_revoked`
|
||||
INTO v_student_id, v_points_change, v_is_revoked
|
||||
FROM `conduct_records`
|
||||
WHERE `record_id` = p_record_id;
|
||||
|
||||
IF v_is_revoked = 1 THEN
|
||||
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = '该记录已被撤销';
|
||||
END IF;
|
||||
|
||||
UPDATE `conduct_records`
|
||||
SET `is_revoked` = 1,
|
||||
`revoked_by` = p_revoker_id,
|
||||
`revoked_at` = NOW()
|
||||
WHERE `record_id` = p_record_id;
|
||||
|
||||
UPDATE `students`
|
||||
SET `total_points` = `total_points` - v_points_change
|
||||
WHERE `student_id` = v_student_id;
|
||||
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- ===========================================
|
||||
-- 创建触发器:更新学生总分
|
||||
-- ===========================================
|
||||
DROP TRIGGER IF EXISTS `update_student_points_on_insert`;
|
||||
DELIMITER //
|
||||
|
||||
CREATE TRIGGER `update_student_points_on_insert`
|
||||
AFTER INSERT ON `conduct_records`
|
||||
FOR EACH ROW
|
||||
BEGIN
|
||||
IF NEW.is_revoked = 0 THEN
|
||||
UPDATE `students`
|
||||
SET `total_points` = `total_points` + NEW.points_change
|
||||
WHERE `student_id` = NEW.student_id;
|
||||
END IF;
|
||||
END //
|
||||
|
||||
DELIMITER ;
|
||||
|
||||
-- ===========================================
|
||||
-- 创建视图:学生操行分排行榜
|
||||
-- ===========================================
|
||||
DROP VIEW IF EXISTS `v_student_ranking`;
|
||||
CREATE VIEW `v_student_ranking` AS
|
||||
SELECT
|
||||
s.student_id,
|
||||
s.student_no,
|
||||
s.name,
|
||||
c.class_name,
|
||||
s.total_points,
|
||||
RANK() OVER (PARTITION BY s.class_id ORDER BY s.total_points DESC) as rank_in_class
|
||||
FROM `students` s
|
||||
JOIN `classes` c ON s.class_id = c.class_id
|
||||
WHERE s.status = 1;
|
||||
|
||||
-- ===========================================
|
||||
-- 创建视图:今日考勤汇总
|
||||
-- ===========================================
|
||||
DROP VIEW IF EXISTS `v_today_attendance`;
|
||||
CREATE VIEW `v_today_attendance` AS
|
||||
SELECT
|
||||
c.class_name,
|
||||
COUNT(CASE WHEN a.status = 'present' THEN 1 END) as present_count,
|
||||
COUNT(CASE WHEN a.status = 'absent' THEN 1 END) as absent_count,
|
||||
COUNT(CASE WHEN a.status = 'late' THEN 1 END) as late_count,
|
||||
COUNT(CASE WHEN a.status = 'leave' THEN 1 END) as leave_count,
|
||||
COUNT(*) as total_count
|
||||
FROM `attendance_records` a
|
||||
JOIN `students` s ON a.student_id = s.student_id
|
||||
JOIN `classes` c ON s.class_id = c.class_id
|
||||
WHERE a.date = CURDATE()
|
||||
GROUP BY c.class_id;
|
||||
Reference in New Issue
Block a user