v0.1测试

This commit is contained in:
2026-04-07 17:07:13 +08:00
parent 593973f598
commit 6b1b586fe3
80 changed files with 9073 additions and 32 deletions

View File

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

View File

@@ -1,32 +1,67 @@
# 班级操行分管理系统 # 班级操行分管理系统
基于 Python FastAPI 开发的班级操行分管理系统,支持学生端、管理端、家长端三端访问,实现操行分的加减记录与查询功能。 基于 Python FastAPI 开发的班级操行分管理系统,支持学生端、管理端、家长端三端访问,实现操行分管理、作业提交跟踪、考勤记录等功能。
## 主要功能 ## 主要功能
### 学生端 ### 学生端
- 查询个人当前操行总分 - 查询个人当前操行总分
- 查看个人加减分历史明细(时间、分数变化、原因) - 查看个人加减分历史明细(时间、分数变化、原因、操作人
- 修改个人登录密码 - 查看个人作业提交情况
- 查看全班学生操行分排行榜 - 查看个人考勤记录
- 查看任意学生的加减分历史记录 - 修改个人登录密码(首次登录强制修改)
### 管理端(班主任/班委)
- 对学生进行加分/减分操作(需填写原因)
- 导出操行分数据Excel
- 批量导入学生
### 家长端 ### 家长端
- 通过学生学号查询子女操行分 - 查询子女当前操行
- 查看子女加减分历史明细 - 查看子女作业提交情况
- 查看子女考勤记录
### 管理端
**班主任权限:**
- 学生管理:新增/编辑/删除学生、批量导入学生JSON
- 操行分管理:对学生进行加减分、撤销任何扣分记录、查看全班历史记录
- 作业管理:发布作业、查看提交情况
- 考勤管理:查看全班考勤记录
- 科目管理:动态增删学科
- 管理员管理:添加班长/科代表/考勤委员/劳动委员
**班长权限:**
- 操行分管理对学生进行加减分±5分以内、撤销任何人的扣分记录、查看全班历史记录
**科代表权限:**
- 作业管理:更新作业提交状态、关联扣分(仅扣分,按规则)
- 历史记录:仅查看自己提交的操作记录
**考勤委员权限:**
- 考勤管理:记录考勤状态、关联扣分(仅扣分,按规则)
- 历史记录:仅查看自己提交的操作记录
**劳动委员权限:**
- 操行分管理:以卫生值日为理由进行加减分(固定 ±1 分)
- 历史记录:仅查看自己提交的操作记录
## 技术栈 ## 技术栈
- **后端框架**: FastAPI | 层级 | 技术 | 版本 |
- **数据库**: MySQL |------|------|------|
- **认证**: JWT | 后端框架 | FastAPI | 0.104+ |
- **配置管理**: python-dotenv.env | 数据库 | MySQL | 5.7 |
| 缓存 | Redis | 7.x |
| 前端 | PHP | 8.0 |
| Web服务器 | Nginx | 1.28+ |
## 角色权限一览表
| 角色 | 操行分查看 | 操行分加减 | 撤销扣分 | 历史记录查看 |
|------|-----------|-----------|---------|-------------|
| 班主任 | 全班 | 无限制 | 可撤销任何记录 | 全班所有记录 |
| 班长 | 全班 | ±5分 | 可撤销任何记录 | 全班所有记录 |
| 科代表 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 考勤委员 | 全班 | 仅扣分(按规则) | 不可撤销 | 仅自己提交的 |
| 劳动委员 | 全班 | 仅±1分卫生值日 | 不可撤销 | 仅自己提交的 |
| 学生 | 自己 | 无 | 无 | 自己的历史 |
| 家长 | 子女总分 | 无 | 无 | 不可见详情 |
## 安装部署 ## 安装部署
@@ -38,4 +73,4 @@
## 许可证 ## 许可证
本项目使用[MIT License](LICENSE)许可证 本项目使用 [MIT License](LICENSE) 许可证

164
backend/.env.example Normal file
View 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
View 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
View 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
)

View File

@@ -0,0 +1,11 @@
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================

View 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

View 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

View 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 = {
'&': '&',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
}
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

View File

@@ -0,0 +1,11 @@
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================

View 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

View 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

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

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

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

View 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

View 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

View File

@@ -0,0 +1,11 @@
# ===========================================
# 班级操行分管理系统 - 后端服务
#
# 开发者: Canglan
# 联系方式: admin@sea-studio.top
# 版权归属: Sea Network Technology Studio
# 许可证: MIT License
#
# 版权所有 © Sea Network Technology Studio
# ===========================================

View 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}

View 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}

View 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"

View 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
}

View 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": "状态更新成功"}

View 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
}

View 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)

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

View 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
View 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"]

View 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
View 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
View 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 = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#x27;',
'/': '&#x2F;'
}
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
View File

0
docs/parent.md Normal file
View File

0
docs/student.md Normal file
View File

28
frontend/.env.example Normal file
View 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
View 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')">&times;</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'; ?>

View 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')">&times;</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
View 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')">&times;</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'; ?>

View 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
View 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
View 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')">&times;</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
View 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
View 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')">&times;</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')">&times;</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')">&times;</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
View 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')">&times;</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'; ?>

View 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;
}

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

View 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();
}
});

View File

@@ -0,0 +1,13 @@
/**
* 班级操行分管理系统 - 家长端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// 家长端专用功能
console.log('家长端已加载');

View File

@@ -0,0 +1,13 @@
/**
* 班级操行分管理系统 - 学生端JS
*
* 开发者: Canglan
* 联系方式: admin@sea-studio.top
* 版权归属: Sea Network Technology Studio
* 许可证: MIT License
*
* 版权所有 © Sea Network Technology Studio
*/
// 学生端专用功能
console.log('学生端已加载');

View 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
View 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);
}
?>

View 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>&copy; <?php echo date('Y'); ?> Sea Network Technology Studio</p>
</div>
</body>
</html>

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

View 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'; ?>

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

View 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'; ?>

View 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'; ?>

View File

View 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;">查看更多 &gt;</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>

View 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'; ?>

View 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
View 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;