v0.1测试
This commit is contained in:
164
backend/.env.example
Normal file
164
backend/.env.example
Normal file
@@ -0,0 +1,164 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# ===========================================
|
||||
# FastAPI 应用配置
|
||||
# ===========================================
|
||||
|
||||
# 应用名称 - 显示在API文档和日志中
|
||||
APP_NAME=班级操行分管理系统
|
||||
# 运行环境 - production(生产) / development(开发) / testing(测试)
|
||||
# 生产环境会自动启用HTTPS重定向
|
||||
APP_ENV=production
|
||||
# 调试模式 - true开启详细错误信息,生产环境必须为false
|
||||
DEBUG=False
|
||||
# 应用密钥 - 用于会话加密,必须32位以上随机字符串
|
||||
# 生成方法: openssl rand -hex 32
|
||||
SECRET_KEY=your-super-secret-key-min-32-characters-long
|
||||
# API版本号 - 用于路由前缀,如 /api/v1
|
||||
API_VERSION=v1
|
||||
|
||||
# ===========================================
|
||||
# MySQL 数据库配置
|
||||
# ===========================================
|
||||
|
||||
# 数据库主机地址 - 本地用127.0.0.1,远程用实际IP
|
||||
DB_HOST=127.0.0.1
|
||||
# 数据库端口 - MySQL默认3306
|
||||
DB_PORT=3306
|
||||
# 数据库用户名 - 建议创建专用账户,不要用root
|
||||
DB_USER=class_admin
|
||||
# 数据库密码 - 使用强密码,包含大小写数字特殊字符
|
||||
DB_PASSWORD=your-strong-db-password
|
||||
# 数据库名称 - 固定为 classmanagerdb
|
||||
DB_NAME=classmanagerdb
|
||||
# 连接池大小 - 同时保持的数据库连接数,根据并发量调整
|
||||
DB_POOL_SIZE=10
|
||||
# 最大溢出连接 - 连接池满后最多额外创建的连接数
|
||||
DB_MAX_OVERFLOW=20
|
||||
|
||||
# ===========================================
|
||||
# Redis 缓存配置
|
||||
# ===========================================
|
||||
|
||||
# Redis主机地址
|
||||
REDIS_HOST=127.0.0.1
|
||||
# Redis端口 - 默认6379
|
||||
REDIS_PORT=6379
|
||||
# Redis密码 - 建议设置,防止未授权访问
|
||||
REDIS_PASSWORD=your-redis-password
|
||||
# Redis数据库编号 - 0-15,建议使用独立数据库避免冲突
|
||||
REDIS_DB=0
|
||||
# 最大连接数 - 根据并发量调整,建议50-200
|
||||
REDIS_MAX_CONNECTIONS=50
|
||||
|
||||
# ===========================================
|
||||
# JWT 认证配置
|
||||
# ===========================================
|
||||
|
||||
# JWT密钥 - 用于签名Token,必须32位以上随机字符串
|
||||
# 生成方法: openssl rand -hex 32
|
||||
JWT_SECRET_KEY=your-jwt-secret-key-min-32-chars
|
||||
# JWT签名算法 - HS256对称加密
|
||||
JWT_ALGORITHM=HS256
|
||||
# Token过期时间(分钟)- 30分钟无操作需重新登录
|
||||
JWT_EXPIRE_MINUTES=30
|
||||
|
||||
# ===========================================
|
||||
# 密码加密配置
|
||||
# ===========================================
|
||||
|
||||
# 密码盐值 - 固定字符串,用于SHA1+MD5双重加密
|
||||
# 生成后不可更改,否则所有密码失效
|
||||
# 生成方法: openssl rand -hex 16
|
||||
PASSWORD_SALT=your-fixed-salt-string-for-password-hash
|
||||
|
||||
# ===========================================
|
||||
# 调试入口配置
|
||||
# ===========================================
|
||||
|
||||
# 调试入口路径 - 随机字符串,用于添加第一批管理员
|
||||
# 建议使用32位随机字符串,只有开发人员知道此路径
|
||||
# 添加完管理员后建议注释掉debug路由
|
||||
# 生成方法: openssl rand -hex 16 | sed 's/\(..\)/\1/g' | cut -c1-32
|
||||
DEBUG_PATH=/a7k9x2m4q8w1e3r5t6y7u8i9o0p1z2x3
|
||||
|
||||
# ===========================================
|
||||
# 扣分规则配置
|
||||
# ===========================================
|
||||
# 注意:这些规则仅用于建议,实际操作时可选择是否应用扣分
|
||||
|
||||
# 作业未提交扣分 - 科代表标记未提交时建议扣分数值
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT=2
|
||||
# 作业迟交扣分 - 迟交作业建议扣分数值
|
||||
DEDUCTION_HOMEWORK_LATE=1
|
||||
# 缺勤扣分 - 缺勤建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_ABSENT=5
|
||||
# 迟到扣分 - 迟到建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_LATE=2
|
||||
# 请假扣分 - 请假建议扣分数值
|
||||
DEDUCTION_ATTENDANCE_LEAVE=1
|
||||
|
||||
# ===========================================
|
||||
# 劳动委员固定分值配置
|
||||
# ===========================================
|
||||
|
||||
# 劳动委员加分值 - 固定为1分
|
||||
LABOR_POINTS_ADD=1
|
||||
# 劳动委员扣分值 - 固定为-1分
|
||||
LABOR_POINTS_SUBTRACT=-1
|
||||
|
||||
# ===========================================
|
||||
# 班长加减分限制配置
|
||||
# ===========================================
|
||||
|
||||
# 班长最大单次加分值
|
||||
MONITOR_MAX_ADD=5
|
||||
# 班长最大单次扣分值(负数)
|
||||
MONITOR_MAX_SUBTRACT=-5
|
||||
|
||||
# ===========================================
|
||||
# 日志配置
|
||||
# ===========================================
|
||||
|
||||
# 日志级别 - DEBUG/INFO/WARNING/ERROR
|
||||
# 生产环境建议INFO,开发环境可用DEBUG
|
||||
LOG_LEVEL=INFO
|
||||
# 单日志文件最大大小(字节)- 100MB = 104857600
|
||||
LOG_MAX_BYTES=104857600
|
||||
# 日志备份数量 - 保留30个历史日志文件
|
||||
LOG_BACKUP_COUNT=30
|
||||
# 日志保留天数 - 操作日志保留1年,访问日志保留90天
|
||||
LOG_RETENTION_DAYS=365
|
||||
|
||||
# ===========================================
|
||||
# CORS 跨域配置
|
||||
# ===========================================
|
||||
|
||||
# 允许的跨域域名 - 多个域名用英文逗号分隔,不要有空格
|
||||
# 生产环境必须指定具体域名,不能用 *
|
||||
CORS_ORIGINS=https://your-frontend-domain.com,http://localhost:8080
|
||||
|
||||
# ===========================================
|
||||
# 上传文件配置
|
||||
# ===========================================
|
||||
|
||||
# 最大上传文件大小(字节)- 5MB = 5242880
|
||||
MAX_UPLOAD_SIZE=5242880
|
||||
# 允许的文件扩展名 - 多个用英文逗号分隔
|
||||
ALLOWED_EXTENSIONS=json
|
||||
|
||||
# ===========================================
|
||||
# 学生初始配置
|
||||
# ===========================================
|
||||
|
||||
# 学生初始操行分 - 新生导入时的默认分数,默认60分
|
||||
STUDENT_INITIAL_POINTS=60
|
||||
107
backend/config.py
Normal file
107
backend/config.py
Normal file
@@ -0,0 +1,107 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
from typing import List
|
||||
|
||||
# 加载环境变量
|
||||
load_dotenv()
|
||||
|
||||
|
||||
class Settings:
|
||||
"""应用配置类"""
|
||||
|
||||
# ========== 应用配置 ==========
|
||||
APP_NAME: str = os.getenv("APP_NAME", "班级操行分管理系统")
|
||||
APP_ENV: str = os.getenv("APP_ENV", "production")
|
||||
DEBUG: bool = os.getenv("DEBUG", "False").lower() == "true"
|
||||
SECRET_KEY: str = os.getenv("SECRET_KEY", "")
|
||||
API_VERSION: str = os.getenv("API_VERSION", "v1")
|
||||
|
||||
# ========== 数据库配置 ==========
|
||||
DB_HOST: str = os.getenv("DB_HOST", "127.0.0.1")
|
||||
DB_PORT: int = int(os.getenv("DB_PORT", "3306"))
|
||||
DB_USER: str = os.getenv("DB_USER", "root")
|
||||
DB_PASSWORD: str = os.getenv("DB_PASSWORD", "")
|
||||
DB_NAME: str = os.getenv("DB_NAME", "classmanagerdb")
|
||||
DB_POOL_SIZE: int = int(os.getenv("DB_POOL_SIZE", "10"))
|
||||
DB_MAX_OVERFLOW: int = int(os.getenv("DB_MAX_OVERFLOW", "20"))
|
||||
|
||||
# ========== Redis配置 ==========
|
||||
REDIS_HOST: str = os.getenv("REDIS_HOST", "127.0.0.1")
|
||||
REDIS_PORT: int = int(os.getenv("REDIS_PORT", "6379"))
|
||||
REDIS_PASSWORD: str = os.getenv("REDIS_PASSWORD", "")
|
||||
REDIS_DB: int = int(os.getenv("REDIS_DB", "0"))
|
||||
REDIS_MAX_CONNECTIONS: int = int(os.getenv("REDIS_MAX_CONNECTIONS", "50"))
|
||||
|
||||
@property
|
||||
def REDIS_URL(self) -> str:
|
||||
"""获取Redis连接URL"""
|
||||
if self.REDIS_PASSWORD:
|
||||
return f"redis://:{self.REDIS_PASSWORD}@{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
return f"redis://{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB}"
|
||||
|
||||
# ========== JWT配置 ==========
|
||||
JWT_SECRET_KEY: str = os.getenv("JWT_SECRET_KEY", "")
|
||||
JWT_ALGORITHM: str = os.getenv("JWT_ALGORITHM", "HS256")
|
||||
JWT_EXPIRE_MINUTES: int = int(os.getenv("JWT_EXPIRE_MINUTES", "30"))
|
||||
|
||||
# ========== 密码加密 ==========
|
||||
PASSWORD_SALT: str = os.getenv("PASSWORD_SALT", "")
|
||||
|
||||
# ========== 调试入口 ==========
|
||||
DEBUG_PATH: str = os.getenv("DEBUG_PATH", "/debug_add_admin")
|
||||
|
||||
# ========== 扣分规则 ==========
|
||||
DEDUCTION_HOMEWORK_NOT_SUBMIT: int = int(os.getenv("DEDUCTION_HOMEWORK_NOT_SUBMIT", "2"))
|
||||
DEDUCTION_HOMEWORK_LATE: int = int(os.getenv("DEDUCTION_HOMEWORK_LATE", "1"))
|
||||
DEDUCTION_ATTENDANCE_ABSENT: int = int(os.getenv("DEDUCTION_ATTENDANCE_ABSENT", "5"))
|
||||
DEDUCTION_ATTENDANCE_LATE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LATE", "2"))
|
||||
DEDUCTION_ATTENDANCE_LEAVE: int = int(os.getenv("DEDUCTION_ATTENDANCE_LEAVE", "1"))
|
||||
|
||||
# ========== 劳动委员固定分值 ==========
|
||||
LABOR_POINTS_ADD: int = int(os.getenv("LABOR_POINTS_ADD", "1"))
|
||||
LABOR_POINTS_SUBTRACT: int = int(os.getenv("LABOR_POINTS_SUBTRACT", "-1"))
|
||||
|
||||
# ========== 班长加减分限制 ==========
|
||||
MONITOR_MAX_ADD: int = int(os.getenv("MONITOR_MAX_ADD", "5"))
|
||||
MONITOR_MAX_SUBTRACT: int = int(os.getenv("MONITOR_MAX_SUBTRACT", "-5"))
|
||||
|
||||
# ========== 日志配置 ==========
|
||||
LOG_LEVEL: str = os.getenv("LOG_LEVEL", "INFO")
|
||||
LOG_MAX_BYTES: int = int(os.getenv("LOG_MAX_BYTES", "104857600"))
|
||||
LOG_BACKUP_COUNT: int = int(os.getenv("LOG_BACKUP_COUNT", "30"))
|
||||
LOG_RETENTION_DAYS: int = int(os.getenv("LOG_RETENTION_DAYS", "365"))
|
||||
|
||||
# ========== CORS配置 ==========
|
||||
CORS_ORIGINS: List[str] = os.getenv("CORS_ORIGINS", "http://localhost:8080").split(",")
|
||||
|
||||
# ========== 上传配置 ==========
|
||||
MAX_UPLOAD_SIZE: int = int(os.getenv("MAX_UPLOAD_SIZE", "5242880"))
|
||||
ALLOWED_EXTENSIONS: set = set(os.getenv("ALLOWED_EXTENSIONS", "json").split(","))
|
||||
|
||||
# ========== 学生初始配置 ==========
|
||||
STUDENT_INITIAL_POINTS: int = int(os.getenv("STUDENT_INITIAL_POINTS", "60"))
|
||||
|
||||
def validate(self) -> None:
|
||||
"""验证必要配置是否存在"""
|
||||
required_configs = [
|
||||
("SECRET_KEY", self.SECRET_KEY),
|
||||
("JWT_SECRET_KEY", self.JWT_SECRET_KEY),
|
||||
("PASSWORD_SALT", self.PASSWORD_SALT),
|
||||
]
|
||||
for name, value in required_configs:
|
||||
if not value:
|
||||
raise ValueError(f"配置 {name} 不能为空")
|
||||
|
||||
|
||||
settings = Settings()
|
||||
97
backend/main.py
Normal file
97
backend/main.py
Normal file
@@ -0,0 +1,97 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - FastAPI 主入口
|
||||
# ===========================================
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
# 版权所有: Copyright (c) 2024 Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import FastAPI, Request
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from contextlib import asynccontextmanager
|
||||
import uvicorn
|
||||
|
||||
from config import settings
|
||||
from utils.logger import setup_logger, log_access
|
||||
from utils.database import init_db_pool, close_db_pool
|
||||
from utils.redis_client import init_redis_pool, close_redis_pool
|
||||
from routes import auth, student, parent, admin, subject, debug
|
||||
|
||||
|
||||
# 设置日志
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI):
|
||||
"""应用生命周期管理"""
|
||||
logger.info("正在启动应用...")
|
||||
await init_db_pool()
|
||||
await init_redis_pool()
|
||||
logger.info(f"{settings.APP_NAME} 启动完成")
|
||||
|
||||
yield
|
||||
|
||||
logger.info("正在关闭应用...")
|
||||
await close_db_pool()
|
||||
await close_redis_pool()
|
||||
logger.info("应用已关闭")
|
||||
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title=settings.APP_NAME,
|
||||
version=settings.API_VERSION,
|
||||
debug=settings.DEBUG,
|
||||
lifespan=lifespan
|
||||
)
|
||||
|
||||
|
||||
# 访问日志中间件
|
||||
@app.middleware("http")
|
||||
async def access_log_middleware(request: Request, call_next):
|
||||
log_access(request)
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
|
||||
# CORS中间件
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.CORS_ORIGINS,
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
|
||||
# 注册路由
|
||||
app.include_router(auth.router, prefix="/api/auth", tags=["认证"])
|
||||
app.include_router(student.router, prefix="/api/student", tags=["学生端"])
|
||||
app.include_router(parent.router, prefix="/api/parent", tags=["家长端"])
|
||||
app.include_router(admin.router, prefix="/api/admin", tags=["管理端"])
|
||||
app.include_router(subject.router, prefix="/api/subject", tags=["科目管理"])
|
||||
app.include_router(debug.router, tags=["调试"])
|
||||
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径健康检查"""
|
||||
return {"status": "ok", "message": f"{settings.APP_NAME} API 运行中"}
|
||||
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查接口"""
|
||||
return {"status": "healthy"}
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
uvicorn.run(
|
||||
"main:app",
|
||||
host="0.0.0.0",
|
||||
port=8000,
|
||||
reload=settings.DEBUG
|
||||
)
|
||||
11
backend/middleware/__init__.py
Normal file
11
backend/middleware/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
111
backend/middleware/auth_middleware.py
Normal file
111
backend/middleware/auth_middleware.py
Normal file
@@ -0,0 +1,111 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Optional, Dict, Any, Tuple
|
||||
import re
|
||||
|
||||
from utils.jwt_handler import jwt_handler
|
||||
from utils.redis_client import RedisClient
|
||||
from utils.response import unauthorized_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 不需要认证的路由
|
||||
PUBLIC_PATHS = [
|
||||
r'^/$',
|
||||
r'^/health$',
|
||||
r'^/api/auth/login$',
|
||||
r'^/api/auth/logout$',
|
||||
r'^/debug/.*$', # 调试入口
|
||||
]
|
||||
|
||||
# 不需要Token验证但需要记录访问的路由
|
||||
OPEN_PATHS = [
|
||||
r'^/api/auth/change-password$',
|
||||
]
|
||||
|
||||
|
||||
def is_public_path(path: str) -> bool:
|
||||
"""检查是否为公开路径"""
|
||||
for pattern in PUBLIC_PATHS:
|
||||
if re.match(pattern, path):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
class AuthMiddleware(BaseHTTPMiddleware):
|
||||
"""JWT认证中间件"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
path = request.url.path
|
||||
|
||||
# 公开路径跳过认证
|
||||
if is_public_path(path):
|
||||
return await call_next(request)
|
||||
|
||||
# 获取Authorization头
|
||||
auth_header = request.headers.get("Authorization")
|
||||
|
||||
if not auth_header:
|
||||
return unauthorized_response("缺少认证令牌")
|
||||
|
||||
# 解析Bearer Token
|
||||
try:
|
||||
scheme, token = auth_header.split()
|
||||
if scheme.lower() != "bearer":
|
||||
return unauthorized_response("认证格式错误")
|
||||
except ValueError:
|
||||
return unauthorized_response("认证格式错误")
|
||||
|
||||
# 验证Token
|
||||
payload = jwt_handler.verify_token(token)
|
||||
if not payload:
|
||||
return unauthorized_response("令牌无效或已过期")
|
||||
|
||||
# 验证Redis中的Token
|
||||
user_id = payload.get("user_id")
|
||||
stored_token = await RedisClient.get_user_token(user_id)
|
||||
|
||||
if not stored_token or stored_token != token:
|
||||
return unauthorized_response("令牌已失效,请重新登录")
|
||||
|
||||
# 将用户信息存储到request.state
|
||||
request.state.user_id = payload.get("user_id")
|
||||
request.state.username = payload.get("username")
|
||||
request.state.user_type = payload.get("user_type")
|
||||
request.state.student_id = payload.get("student_id")
|
||||
request.state.role = payload.get("role")
|
||||
|
||||
# 刷新Token过期时间
|
||||
from config import settings
|
||||
await RedisClient.expire(f"user_token:{user_id}", settings.JWT_EXPIRE_MINUTES * 60)
|
||||
|
||||
return await call_next(request)
|
||||
|
||||
|
||||
async def get_current_user(request: Request) -> Dict[str, Any]:
|
||||
"""获取当前登录用户信息"""
|
||||
return {
|
||||
"user_id": request.state.user_id,
|
||||
"username": request.state.username,
|
||||
"user_type": request.state.user_type,
|
||||
"student_id": request.state.student_id,
|
||||
"role": request.state.role
|
||||
}
|
||||
|
||||
|
||||
async def get_current_user_id(request: Request) -> int:
|
||||
"""获取当前用户ID"""
|
||||
return request.state.user_id
|
||||
197
backend/middleware/permission.py
Normal file
197
backend/middleware/permission.py
Normal file
@@ -0,0 +1,197 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request, HTTPException
|
||||
from typing import List, Optional, Callable
|
||||
from functools import wraps
|
||||
|
||||
from utils.response import forbidden_response
|
||||
from utils.database import execute_one
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class PermissionChecker:
|
||||
"""权限检查器"""
|
||||
|
||||
@staticmethod
|
||||
async def get_user_role(user_id: int) -> Optional[str]:
|
||||
"""获取用户的管理员角色"""
|
||||
sql = """
|
||||
SELECT role_type FROM admin_roles
|
||||
WHERE user_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
result = await execute_one(sql, (user_id,))
|
||||
return result["role_type"] if result else None
|
||||
|
||||
@staticmethod
|
||||
async def get_user_class_id(user_id: int) -> Optional[int]:
|
||||
"""获取用户管理的班级ID"""
|
||||
sql = """
|
||||
SELECT class_id FROM admin_roles
|
||||
WHERE user_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
result = await execute_one(sql, (user_id,))
|
||||
return result["class_id"] if result else None
|
||||
|
||||
@staticmethod
|
||||
async def get_user_subject_ids(user_id: int) -> List[int]:
|
||||
"""获取科代表管理的科目ID列表"""
|
||||
sql = """
|
||||
SELECT subject_id FROM admin_roles
|
||||
WHERE user_id = %s AND role_type = '科代表'
|
||||
"""
|
||||
results = await execute_one(sql, (user_id,))
|
||||
if results:
|
||||
return [r["subject_id"] for r in results] if isinstance(results, list) else [results["subject_id"]]
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
async def check_is_teacher(user_id: int) -> bool:
|
||||
"""检查是否为班主任"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "班主任"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_monitor(user_id: int) -> bool:
|
||||
"""检查是否为班长"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "班长"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_subject_rep(user_id: int, subject_id: int = None) -> bool:
|
||||
"""检查是否为科代表"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
if role != "科代表":
|
||||
return False
|
||||
if subject_id:
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
|
||||
return subject_id in subject_ids
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
async def check_is_attendance_rep(user_id: int) -> bool:
|
||||
"""检查是否为考勤委员"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "考勤委员"
|
||||
|
||||
@staticmethod
|
||||
async def check_is_labor_rep(user_id: int) -> bool:
|
||||
"""检查是否为劳动委员"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
return role == "劳动委员"
|
||||
|
||||
@staticmethod
|
||||
async def check_can_revoke(user_id: int, record_id: int) -> bool:
|
||||
"""
|
||||
检查是否可以撤销扣分记录
|
||||
班主任:可以撤销任何记录
|
||||
班长:可以撤销任何记录
|
||||
其他:只能撤销自己的记录
|
||||
"""
|
||||
# 获取记录信息
|
||||
sql = "SELECT recorder_id FROM conduct_records WHERE record_id = %s"
|
||||
record = await execute_one(sql, (record_id,))
|
||||
if not record:
|
||||
return False
|
||||
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
# 班主任或班长可以撤销任何记录
|
||||
if role in ["班主任", "班长"]:
|
||||
return True
|
||||
|
||||
# 其他人只能撤销自己的记录
|
||||
return record["recorder_id"] == user_id
|
||||
|
||||
@staticmethod
|
||||
async def check_can_manage_student(user_id: int, student_id: int) -> bool:
|
||||
"""检查是否可以管理该学生(同班级)"""
|
||||
# 获取学生班级
|
||||
sql = "SELECT class_id FROM students WHERE student_id = %s"
|
||||
student = await execute_one(sql, (student_id,))
|
||||
if not student:
|
||||
return False
|
||||
|
||||
# 获取管理员管理的班级
|
||||
admin_class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
|
||||
return admin_class_id == student["class_id"]
|
||||
|
||||
|
||||
def require_auth(func: Callable):
|
||||
"""需要认证的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state') or not hasattr(request.state, 'user_id'):
|
||||
return forbidden_response("请先登录")
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_role(roles: List[str]):
|
||||
"""需要特定角色的装饰器"""
|
||||
def decorator(func: Callable):
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state'):
|
||||
return forbidden_response("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
user_role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if user_role not in roles:
|
||||
return forbidden_response(f"需要{','.join(roles)}权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
return decorator
|
||||
|
||||
|
||||
def require_teacher(func: Callable):
|
||||
"""需要班主任权限的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state'):
|
||||
return forbidden_response("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user_id)
|
||||
|
||||
if not is_teacher:
|
||||
return forbidden_response("需要班主任权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
|
||||
|
||||
def require_monitor(func: Callable):
|
||||
"""需要班长权限的装饰器"""
|
||||
@wraps(func)
|
||||
async def wrapper(*args, **kwargs):
|
||||
request = kwargs.get('request')
|
||||
if not request or not hasattr(request, 'state'):
|
||||
return forbidden_response("请先登录")
|
||||
|
||||
user_id = request.state.user_id
|
||||
is_monitor = await PermissionChecker.check_is_monitor(user_id)
|
||||
|
||||
if not is_monitor:
|
||||
return forbidden_response("需要班长权限")
|
||||
|
||||
return await func(*args, **kwargs)
|
||||
return wrapper
|
||||
121
backend/middleware/sanitize.py
Normal file
121
backend/middleware/sanitize.py
Normal file
@@ -0,0 +1,121 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from typing import Dict, Any
|
||||
import re
|
||||
|
||||
|
||||
class SanitizeMiddleware(BaseHTTPMiddleware):
|
||||
"""输入过滤中间件"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
# 只处理POST、PUT、PATCH请求
|
||||
if request.method in ["POST", "PUT", "PATCH"]:
|
||||
# 获取请求体
|
||||
body = await request.body()
|
||||
if body:
|
||||
import json
|
||||
try:
|
||||
data = json.loads(body)
|
||||
# 清理数据
|
||||
cleaned_data = self._sanitize_data(data)
|
||||
# 替换请求体
|
||||
request._body = json.dumps(cleaned_data).encode()
|
||||
except:
|
||||
pass
|
||||
|
||||
response = await call_next(request)
|
||||
return response
|
||||
|
||||
def _sanitize_data(self, data: Any) -> Any:
|
||||
"""递归清理数据"""
|
||||
if isinstance(data, dict):
|
||||
return {k: self._sanitize_data(v) for k, v in data.items()}
|
||||
elif isinstance(data, list):
|
||||
return [self._sanitize_data(item) for item in data]
|
||||
elif isinstance(data, str):
|
||||
return self._sanitize_string(data)
|
||||
else:
|
||||
return data
|
||||
|
||||
def _sanitize_string(self, value: str) -> str:
|
||||
"""清理字符串"""
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
# 去除首尾空格
|
||||
value = value.strip()
|
||||
|
||||
# 限制长度
|
||||
if len(value) > 1000:
|
||||
value = value[:1000]
|
||||
|
||||
# 转义HTML特殊字符
|
||||
html_chars = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/'
|
||||
}
|
||||
for char, escape in html_chars.items():
|
||||
value = value.replace(char, escape)
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def sanitize_input(value: str, max_length: int = 255) -> str:
|
||||
"""清理单个输入值"""
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
value = value.strip()
|
||||
if len(value) > max_length:
|
||||
value = value[:max_length]
|
||||
|
||||
return value
|
||||
|
||||
|
||||
def validate_points(points: int, min_val: int = -100, max_val: int = 100) -> tuple:
|
||||
"""
|
||||
验证分值
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if points == 0:
|
||||
return False, "分值不能为0"
|
||||
if points < min_val or points > max_val:
|
||||
return False, f"分值必须在{min_val}到{max_val}之间"
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_reason(reason: str) -> tuple:
|
||||
"""
|
||||
验证原因
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if not reason or not reason.strip():
|
||||
return False, "原因不能为空"
|
||||
if len(reason) > 255:
|
||||
return False, "原因长度不能超过255个字符"
|
||||
return True, ""
|
||||
|
||||
|
||||
def validate_date(date_str: str) -> bool:
|
||||
"""验证日期格式 YYYY-MM-DD"""
|
||||
if not date_str:
|
||||
return False
|
||||
pattern = r'^\d{4}-\d{2}-\d{2}$'
|
||||
if not re.match(pattern, date_str):
|
||||
return False
|
||||
return True
|
||||
11
backend/models/__init__.py
Normal file
11
backend/models/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
60
backend/models/admin_role.py
Normal file
60
backend/models/admin_role.py
Normal file
@@ -0,0 +1,60 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update
|
||||
|
||||
|
||||
class AdminRoleModel:
|
||||
"""管理员角色数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_user_id(user_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT ar.*, c.class_name, s.subject_name
|
||||
FROM admin_roles ar
|
||||
LEFT JOIN classes c ON ar.class_id = c.class_id
|
||||
LEFT JOIN subjects s ON ar.subject_id = s.subject_id
|
||||
WHERE ar.user_id = %s
|
||||
LIMIT 1
|
||||
"""
|
||||
return await execute_one(sql, (user_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_class_id_by_user(user_id: int) -> Optional[int]:
|
||||
sql = "SELECT class_id FROM admin_roles WHERE user_id = %s LIMIT 1"
|
||||
result = await execute_one(sql, (user_id,))
|
||||
return result["class_id"] if result else None
|
||||
|
||||
@staticmethod
|
||||
async def get_by_class(class_id: int) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT ar.*, u.real_name, u.username
|
||||
FROM admin_roles ar
|
||||
JOIN users u ON ar.user_id = u.user_id
|
||||
WHERE ar.class_id = %s
|
||||
ORDER BY ar.role_type
|
||||
"""
|
||||
return await execute_query(sql, (class_id,))
|
||||
|
||||
@staticmethod
|
||||
async def create(user_id: int, role_type: str, class_id: int, subject_id: int = None) -> int:
|
||||
sql = """
|
||||
INSERT INTO admin_roles (user_id, role_type, class_id, subject_id)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (user_id, role_type, class_id, subject_id))
|
||||
|
||||
@staticmethod
|
||||
async def delete(user_id: int) -> bool:
|
||||
sql = "DELETE FROM admin_roles WHERE user_id = %s"
|
||||
result = await execute_update(sql, (user_id,))
|
||||
return result > 0
|
||||
98
backend/models/attenddance.py
Normal file
98
backend/models/attenddance.py
Normal file
@@ -0,0 +1,98 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from datetime import datetime
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update
|
||||
|
||||
|
||||
class AttendanceModel:
|
||||
"""考勤数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_student_records(student_id: int, month: str = None) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT attendance_id, date, status, reason, deduction_applied, created_at
|
||||
FROM attendance_records
|
||||
WHERE student_id = %s
|
||||
"""
|
||||
params = [student_id]
|
||||
|
||||
if month:
|
||||
sql += " AND DATE_FORMAT(date, '%%Y-%%m') = %s"
|
||||
params.append(month)
|
||||
|
||||
sql += " ORDER BY date DESC"
|
||||
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def get_class_records(
|
||||
class_id: int,
|
||||
date: str = None,
|
||||
student_id: int = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT ar.*, s.name as student_name, s.student_no
|
||||
FROM attendance_records ar
|
||||
JOIN students s ON ar.student_id = s.student_id
|
||||
WHERE s.class_id = %s
|
||||
"""
|
||||
params = [class_id]
|
||||
|
||||
if date:
|
||||
sql += " AND ar.date = %s"
|
||||
params.append(date)
|
||||
|
||||
if student_id:
|
||||
sql += " AND ar.student_id = %s"
|
||||
params.append(student_id)
|
||||
|
||||
sql += " ORDER BY ar.date DESC, s.student_no"
|
||||
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def create_record(
|
||||
student_id: int,
|
||||
date: str,
|
||||
status: str,
|
||||
reason: str = None,
|
||||
recorder_id: int = None
|
||||
) -> int:
|
||||
# 检查是否已存在当天记录
|
||||
existing = await execute_one(
|
||||
"SELECT attendance_id FROM attendance_records WHERE student_id = %s AND date = %s",
|
||||
(student_id, date)
|
||||
)
|
||||
|
||||
if existing:
|
||||
# 更新已有记录
|
||||
sql = """
|
||||
UPDATE attendance_records
|
||||
SET status = %s, reason = %s, recorder_id = %s
|
||||
WHERE student_id = %s AND date = %s
|
||||
"""
|
||||
await execute_update(sql, (status, reason, recorder_id, student_id, date))
|
||||
return existing["attendance_id"]
|
||||
else:
|
||||
# 插入新记录
|
||||
sql = """
|
||||
INSERT INTO attendance_records (student_id, date, status, reason, recorder_id)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (student_id, date, status, reason, recorder_id))
|
||||
|
||||
@staticmethod
|
||||
async def mark_deduction_applied(attendance_id: int) -> bool:
|
||||
sql = "UPDATE attendance_records SET deduction_applied = 1 WHERE attendance_id = %s"
|
||||
result = await execute_update(sql, (attendance_id,))
|
||||
return result > 0
|
||||
35
backend/models/class_model.py
Normal file
35
backend/models/class_model.py
Normal file
@@ -0,0 +1,35 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update
|
||||
|
||||
|
||||
class ClassModel:
|
||||
"""班级数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(class_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = "SELECT * FROM classes WHERE class_id = %s"
|
||||
return await execute_one(sql, (class_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_all() -> List[Dict[str, Any]]:
|
||||
sql = "SELECT * FROM classes ORDER BY class_id"
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def create(class_name: str, grade: str = None, academic_year: str = None) -> int:
|
||||
sql = """
|
||||
INSERT INTO classes (class_name, grade, academic_year)
|
||||
VALUES (%s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (class_name, grade, academic_year))
|
||||
166
backend/models/conduct.py
Normal file
166
backend/models/conduct.py
Normal file
@@ -0,0 +1,166 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from datetime import datetime
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update, call_procedure
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConductModel:
|
||||
"""操行分数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def create_record(
|
||||
student_id: int,
|
||||
points_change: int,
|
||||
reason: str,
|
||||
recorder_id: int,
|
||||
recorder_name: str = None,
|
||||
related_type: str = 'manual',
|
||||
related_id: int = None
|
||||
) -> int:
|
||||
"""创建操行分记录"""
|
||||
sql = """
|
||||
INSERT INTO conduct_records
|
||||
(student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (
|
||||
student_id, points_change, reason, recorder_id, recorder_name, related_type, related_id
|
||||
))
|
||||
|
||||
@staticmethod
|
||||
async def get_student_records(
|
||||
student_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
include_revoked: bool = False
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取学生操行分记录"""
|
||||
sql = """
|
||||
SELECT cr.*, u.real_name as recorder_name
|
||||
FROM conduct_records cr
|
||||
LEFT JOIN users u ON cr.recorder_id = u.user_id
|
||||
WHERE cr.student_id = %s
|
||||
"""
|
||||
if not include_revoked:
|
||||
sql += " AND cr.is_revoked = 0"
|
||||
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
||||
return await execute_query(sql, (student_id, limit, offset))
|
||||
|
||||
@staticmethod
|
||||
async def get_records_by_recorder(
|
||||
recorder_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取操作人提交的记录"""
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
WHERE cr.recorder_id = %s AND cr.is_revoked = 0
|
||||
ORDER BY cr.created_at DESC
|
||||
LIMIT %s OFFSET %s
|
||||
"""
|
||||
return await execute_query(sql, (recorder_id, limit, offset))
|
||||
|
||||
@staticmethod
|
||||
async def get_all_records(
|
||||
class_id: int = None,
|
||||
limit: int = 100,
|
||||
offset: int = 0,
|
||||
start_date: str = None,
|
||||
end_date: str = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""获取所有记录(班主任/班长专用)"""
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name, s.student_no, u.real_name as recorder_name
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
JOIN users u ON cr.recorder_id = u.user_id
|
||||
WHERE cr.is_revoked = 0
|
||||
"""
|
||||
params = []
|
||||
|
||||
if class_id:
|
||||
sql += " AND s.class_id = %s"
|
||||
params.append(class_id)
|
||||
|
||||
if start_date:
|
||||
sql += " AND DATE(cr.created_at) >= %s"
|
||||
params.append(start_date)
|
||||
|
||||
if end_date:
|
||||
sql += " AND DATE(cr.created_at) <= %s"
|
||||
params.append(end_date)
|
||||
|
||||
sql += " ORDER BY cr.created_at DESC LIMIT %s OFFSET %s"
|
||||
params.extend([limit, offset])
|
||||
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def get_record_by_id(record_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取记录"""
|
||||
sql = """
|
||||
SELECT cr.*, s.name as student_name, s.total_points
|
||||
FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
WHERE cr.record_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (record_id,))
|
||||
|
||||
@staticmethod
|
||||
async def revoke_record(record_id: int, revoker_id: int) -> bool:
|
||||
"""撤销记录"""
|
||||
try:
|
||||
await call_procedure('revoke_conduct_record', (record_id, revoker_id))
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"撤销记录失败: {e}")
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
async def batch_create_records(records_data: List[Dict]) -> List[Dict]:
|
||||
"""批量创建操行分记录"""
|
||||
results = []
|
||||
for record in records_data:
|
||||
try:
|
||||
record_id = await ConductModel.create_record(
|
||||
student_id=record.get('student_id'),
|
||||
points_change=record.get('points_change'),
|
||||
reason=record.get('reason'),
|
||||
recorder_id=record.get('recorder_id'),
|
||||
recorder_name=record.get('recorder_name')
|
||||
)
|
||||
results.append({
|
||||
'student_id': record.get('student_id'),
|
||||
'success': True,
|
||||
'record_id': record_id
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'student_id': record.get('student_id'),
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
return results
|
||||
|
||||
@staticmethod
|
||||
async def get_student_total_points(student_id: int) -> int:
|
||||
"""获取学生当前总分"""
|
||||
sql = "SELECT total_points FROM students WHERE student_id = %s"
|
||||
result = await execute_one(sql, (student_id,))
|
||||
return result['total_points'] if result else 100
|
||||
118
backend/models/homework.py
Normal file
118
backend/models/homework.py
Normal file
@@ -0,0 +1,118 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update
|
||||
|
||||
|
||||
class HomeworkModel:
|
||||
"""作业数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_assignments_by_class(class_id: int) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT a.*, s.subject_name, u.real_name as created_by_name
|
||||
FROM assignments a
|
||||
JOIN subjects s ON a.subject_id = s.subject_id
|
||||
JOIN users u ON a.created_by = u.user_id
|
||||
WHERE a.class_id = %s
|
||||
ORDER BY a.deadline ASC, a.created_at DESC
|
||||
"""
|
||||
return await execute_query(sql, (class_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_assignments_by_subjects(class_id: int, subject_ids: List[int]) -> List[Dict[str, Any]]:
|
||||
if not subject_ids:
|
||||
return []
|
||||
placeholders = ','.join(['%s'] * len(subject_ids))
|
||||
sql = f"""
|
||||
SELECT a.*, s.subject_name, u.real_name as created_by_name
|
||||
FROM assignments a
|
||||
JOIN subjects s ON a.subject_id = s.subject_id
|
||||
JOIN users u ON a.created_by = u.user_id
|
||||
WHERE a.class_id = %s AND a.subject_id IN ({placeholders})
|
||||
ORDER BY a.deadline ASC, a.created_at DESC
|
||||
"""
|
||||
params = [class_id] + subject_ids
|
||||
return await execute_query(sql, tuple(params))
|
||||
|
||||
@staticmethod
|
||||
async def get_student_homework(student_id: int) -> List[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT a.assignment_id, a.title, a.description, a.deadline,
|
||||
s.subject_name, hs.status, hs.submit_time, hs.comments, hs.deduction_applied
|
||||
FROM assignments a
|
||||
JOIN subjects s ON a.subject_id = s.subject_id
|
||||
LEFT JOIN homework_submissions hs ON a.assignment_id = hs.assignment_id AND hs.student_id = %s
|
||||
WHERE a.class_id = (SELECT class_id FROM students WHERE student_id = %s)
|
||||
ORDER BY a.deadline ASC, a.created_at DESC
|
||||
"""
|
||||
return await execute_query(sql, (student_id, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def get_submission(submission_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = """
|
||||
SELECT hs.*, a.title, a.subject_id, a.assignment_id, s.name as student_name
|
||||
FROM homework_submissions hs
|
||||
JOIN assignments a ON hs.assignment_id = a.assignment_id
|
||||
JOIN students s ON hs.student_id = s.student_id
|
||||
WHERE hs.submission_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (submission_id,))
|
||||
|
||||
@staticmethod
|
||||
async def create_assignment(
|
||||
class_id: int,
|
||||
subject_id: int,
|
||||
title: str,
|
||||
description: str,
|
||||
deadline: str,
|
||||
created_by: int
|
||||
) -> int:
|
||||
sql = """
|
||||
INSERT INTO assignments (class_id, subject_id, title, description, deadline, created_by)
|
||||
VALUES (%s, %s, %s, %s, %s, %s)
|
||||
"""
|
||||
assignment_id = await execute_insert(sql, (class_id, subject_id, title, description, deadline, created_by))
|
||||
|
||||
# 为班级所有学生创建提交记录
|
||||
from models.student import StudentModel
|
||||
students = await StudentModel.get_by_class(class_id)
|
||||
|
||||
for student in students:
|
||||
sql_sub = """
|
||||
INSERT INTO homework_submissions (assignment_id, student_id, status)
|
||||
VALUES (%s, %s, 'not_submitted')
|
||||
"""
|
||||
await execute_insert(sql_sub, (assignment_id, student["student_id"]))
|
||||
|
||||
return assignment_id
|
||||
|
||||
@staticmethod
|
||||
async def update_submission(
|
||||
submission_id: int,
|
||||
status: str,
|
||||
comments: str = None,
|
||||
updated_by: int = None
|
||||
) -> bool:
|
||||
sql = """
|
||||
UPDATE homework_submissions
|
||||
SET status = %s, comments = %s, updated_by = %s, updated_at = NOW()
|
||||
WHERE submission_id = %s
|
||||
"""
|
||||
result = await execute_update(sql, (status, comments, updated_by, submission_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def mark_deduction_applied(submission_id: int) -> bool:
|
||||
sql = "UPDATE homework_submissions SET deduction_applied = 1 WHERE submission_id = %s"
|
||||
result = await execute_update(sql, (submission_id,))
|
||||
return result > 0
|
||||
146
backend/models/student.py
Normal file
146
backend/models/student.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 学生数据模型
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, List, Dict, Any
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update, execute_many
|
||||
from utils.security import security
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class StudentModel:
|
||||
"""学生数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(student_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""根据ID获取学生信息"""
|
||||
sql = """
|
||||
SELECT s.*
|
||||
FROM students s
|
||||
WHERE s.student_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (student_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_student_no(student_no: str) -> Optional[Dict[str, Any]]:
|
||||
"""根据学号获取学生信息"""
|
||||
sql = """
|
||||
SELECT s.*
|
||||
FROM students s
|
||||
WHERE s.student_no = %s
|
||||
"""
|
||||
return await execute_one(sql, (student_no,))
|
||||
|
||||
@staticmethod
|
||||
async def get_all(include_disabled: bool = False) -> List[Dict[str, Any]]:
|
||||
"""获取所有学生列表(单班级)"""
|
||||
sql = """
|
||||
SELECT student_id, student_no, name, total_points, parent_phone, status
|
||||
FROM students
|
||||
WHERE 1=1
|
||||
"""
|
||||
if not include_disabled:
|
||||
sql += " AND status = 1"
|
||||
sql += " ORDER BY student_no"
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
student_no: str,
|
||||
name: str,
|
||||
class_id: int,
|
||||
parent_phone: str = None,
|
||||
initial_points: int = 60
|
||||
) -> int:
|
||||
"""创建学生(初始操行分默认60分)"""
|
||||
sql = """
|
||||
INSERT INTO students (student_no, name, class_id, parent_phone, total_points)
|
||||
VALUES (%s, %s, %s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (student_no, name, class_id, parent_phone, initial_points))
|
||||
|
||||
@staticmethod
|
||||
async def update(student_id: int, name: str = None, parent_phone: str = None, status: int = None) -> bool:
|
||||
"""更新学生信息"""
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if name is not None:
|
||||
updates.append("name = %s")
|
||||
params.append(name)
|
||||
if parent_phone is not None:
|
||||
updates.append("parent_phone = %s")
|
||||
params.append(parent_phone)
|
||||
if status is not None:
|
||||
updates.append("status = %s")
|
||||
params.append(status)
|
||||
|
||||
if not updates:
|
||||
return True
|
||||
|
||||
params.append(student_id)
|
||||
sql = f"UPDATE students SET {', '.join(updates)} WHERE student_id = %s"
|
||||
result = await execute_update(sql, tuple(params))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def delete(student_id: int) -> bool:
|
||||
"""删除学生(软删除)"""
|
||||
sql = "UPDATE students SET status = 0 WHERE student_id = %s"
|
||||
result = await execute_update(sql, (student_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def update_total_points(student_id: int, points_change: int) -> bool:
|
||||
"""更新学生总分"""
|
||||
sql = "UPDATE students SET total_points = total_points + %s WHERE student_id = %s"
|
||||
result = await execute_update(sql, (points_change, student_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def get_ranking(limit: int = 50) -> List[Dict[str, Any]]:
|
||||
"""获取学生排行(单班级)"""
|
||||
sql = """
|
||||
SELECT student_id, student_no, name, total_points,
|
||||
RANK() OVER (ORDER BY total_points DESC) as rank
|
||||
FROM students
|
||||
WHERE status = 1
|
||||
ORDER BY total_points DESC
|
||||
LIMIT %s
|
||||
"""
|
||||
return await execute_query(sql, (limit,))
|
||||
|
||||
@staticmethod
|
||||
async def batch_create(students_data: List[Dict], initial_points: int = 60) -> List[Dict]:
|
||||
"""批量创建学生"""
|
||||
results = []
|
||||
for student in students_data:
|
||||
try:
|
||||
student_id = await StudentModel.create(
|
||||
student_no=student.get('student_no'),
|
||||
name=student.get('name'),
|
||||
class_id=1, # 单班级,固定为1
|
||||
parent_phone=student.get('parent_phone'),
|
||||
initial_points=initial_points
|
||||
)
|
||||
results.append({
|
||||
'student_no': student.get('student_no'),
|
||||
'success': True,
|
||||
'student_id': student_id
|
||||
})
|
||||
except Exception as e:
|
||||
results.append({
|
||||
'student_no': student.get('student_no'),
|
||||
'success': False,
|
||||
'error': str(e)
|
||||
})
|
||||
return results
|
||||
82
backend/models/subject.py
Normal file
82
backend/models/subject.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Optional, Dict, Any, List
|
||||
from utils.database import execute_one, execute_query, execute_insert, execute_update
|
||||
|
||||
|
||||
class SubjectModel:
|
||||
"""科目数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_all(is_active: bool = None) -> List[Dict[str, Any]]:
|
||||
if is_active is not None:
|
||||
sql = "SELECT * FROM subjects WHERE is_active = %s ORDER BY sort_order, subject_id"
|
||||
return await execute_query(sql, (1 if is_active else 0,))
|
||||
else:
|
||||
sql = "SELECT * FROM subjects ORDER BY sort_order, subject_id"
|
||||
return await execute_query(sql)
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(subject_id: int) -> Optional[Dict[str, Any]]:
|
||||
sql = "SELECT * FROM subjects WHERE subject_id = %s"
|
||||
return await execute_one(sql, (subject_id,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_name(subject_name: str) -> Optional[Dict[str, Any]]:
|
||||
sql = "SELECT * FROM subjects WHERE subject_name = %s"
|
||||
return await execute_one(sql, (subject_name,))
|
||||
|
||||
@staticmethod
|
||||
async def create(subject_name: str, subject_code: str = None, sort_order: int = 0) -> int:
|
||||
sql = """
|
||||
INSERT INTO subjects (subject_name, subject_code, sort_order)
|
||||
VALUES (%s, %s, %s)
|
||||
"""
|
||||
return await execute_insert(sql, (subject_name, subject_code, sort_order))
|
||||
|
||||
@staticmethod
|
||||
async def update(subject_id: int, **kwargs) -> bool:
|
||||
updates = []
|
||||
params = []
|
||||
|
||||
if "subject_name" in kwargs:
|
||||
updates.append("subject_name = %s")
|
||||
params.append(kwargs["subject_name"])
|
||||
if "subject_code" in kwargs:
|
||||
updates.append("subject_code = %s")
|
||||
params.append(kwargs["subject_code"])
|
||||
if "is_active" in kwargs:
|
||||
updates.append("is_active = %s")
|
||||
params.append(1 if kwargs["is_active"] else 0)
|
||||
if "sort_order" in kwargs:
|
||||
updates.append("sort_order = %s")
|
||||
params.append(kwargs["sort_order"])
|
||||
|
||||
if not updates:
|
||||
return True
|
||||
|
||||
params.append(subject_id)
|
||||
sql = f"UPDATE subjects SET {', '.join(updates)} WHERE subject_id = %s"
|
||||
result = await execute_update(sql, tuple(params))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def delete(subject_id: int) -> bool:
|
||||
sql = "UPDATE subjects SET is_active = 0 WHERE subject_id = %s"
|
||||
result = await execute_update(sql, (subject_id,))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def activate(subject_id: int) -> bool:
|
||||
sql = "UPDATE subjects SET is_active = 1 WHERE subject_id = %s"
|
||||
result = await execute_update(sql, (subject_id,))
|
||||
return result > 0
|
||||
101
backend/models/user.py
Normal file
101
backend/models/user.py
Normal file
@@ -0,0 +1,101 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from utils.database import execute_one, execute_insert, execute_update
|
||||
from utils.security import security
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class UserModel:
|
||||
"""用户数据模型"""
|
||||
|
||||
@staticmethod
|
||||
async def get_by_username(username: str) -> dict:
|
||||
"""根据用户名获取用户"""
|
||||
sql = """
|
||||
SELECT user_id, username, password_hash, real_name, user_type,
|
||||
student_id, status, need_change_password, last_login_time, last_login_ip
|
||||
FROM users
|
||||
WHERE username = %s AND status = 1
|
||||
"""
|
||||
return await execute_one(sql, (username,))
|
||||
|
||||
@staticmethod
|
||||
async def get_by_user_id(user_id: int) -> dict:
|
||||
"""根据用户ID获取用户"""
|
||||
sql = """
|
||||
SELECT user_id, username, real_name, user_type, student_id,
|
||||
need_change_password, status
|
||||
FROM users
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
return await execute_one(sql, (user_id,))
|
||||
|
||||
@staticmethod
|
||||
async def create_student(username: str, password: str, real_name: str, student_id: int) -> int:
|
||||
"""创建学生账号"""
|
||||
password_hash = security.sha1_md5_password(password)
|
||||
sql = """
|
||||
INSERT INTO users (username, password_hash, real_name, user_type, student_id, need_change_password)
|
||||
VALUES (%s, %s, %s, 'student', %s, 1)
|
||||
"""
|
||||
return await execute_insert(sql, (username, password_hash, real_name, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def create_parent(username: str, password: str, real_name: str, student_id: int) -> int:
|
||||
"""创建家长账号"""
|
||||
password_hash = security.sha1_md5_password(password)
|
||||
sql = """
|
||||
INSERT INTO users (username, password_hash, real_name, user_type, student_id, need_change_password)
|
||||
VALUES (%s, %s, %s, 'parent', %s, 0)
|
||||
"""
|
||||
return await execute_insert(sql, (username, password_hash, real_name, student_id))
|
||||
|
||||
@staticmethod
|
||||
async def create_admin(username: str, password: str, real_name: str) -> int:
|
||||
"""创建管理员账号"""
|
||||
password_hash = security.sha1_md5_password(password)
|
||||
sql = """
|
||||
INSERT INTO users (username, password_hash, real_name, user_type, need_change_password)
|
||||
VALUES (%s, %s, %s, 'admin', 1)
|
||||
"""
|
||||
return await execute_insert(sql, (username, password_hash, real_name))
|
||||
|
||||
@staticmethod
|
||||
async def update_password(user_id: int, new_password: str) -> bool:
|
||||
"""更新密码"""
|
||||
password_hash = security.sha1_md5_password(new_password)
|
||||
sql = """
|
||||
UPDATE users
|
||||
SET password_hash = %s, need_change_password = 0
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
result = await execute_update(sql, (password_hash, user_id))
|
||||
return result > 0
|
||||
|
||||
@staticmethod
|
||||
async def update_last_login(user_id: int, ip: str) -> None:
|
||||
"""更新最后登录信息"""
|
||||
sql = """
|
||||
UPDATE users
|
||||
SET last_login_time = NOW(), last_login_ip = %s
|
||||
WHERE user_id = %s
|
||||
"""
|
||||
await execute_update(sql, (ip, user_id))
|
||||
|
||||
@staticmethod
|
||||
async def check_username_exists(username: str) -> bool:
|
||||
"""检查用户名是否存在"""
|
||||
sql = "SELECT 1 FROM users WHERE username = %s"
|
||||
result = await execute_one(sql, (username,))
|
||||
return result is not None
|
||||
11
backend/requirements.txt
Normal file
11
backend/requirements.txt
Normal file
@@ -0,0 +1,11 @@
|
||||
fastapi==0.104.1
|
||||
uvicorn[standard]==0.24.0
|
||||
python-dotenv==1.0.0
|
||||
aiomysql==0.2.0
|
||||
redis==5.0.1
|
||||
python-jose[cryptography]==3.3.0
|
||||
passlib[bcrypt]==1.7.4
|
||||
pydantic==2.5.0
|
||||
pydantic-settings==2.1.0
|
||||
python-multipart==0.0.6
|
||||
loguru==0.7.2
|
||||
11
backend/routes/__init__.py
Normal file
11
backend/routes/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
64
backend/routes/admin.py
Normal file
64
backend/routes/admin.py
Normal file
@@ -0,0 +1,64 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 管理端路由
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
# 在 admin.py 中修改导入接口
|
||||
|
||||
@router.post("/students/import")
|
||||
async def import_students(
|
||||
request: Request,
|
||||
file: UploadFile = File(...)
|
||||
):
|
||||
"""
|
||||
批量导入学生(JSON格式)
|
||||
初始操行分默认为60分
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 检查权限(仅班主任)
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可导入学生", code=403)
|
||||
|
||||
# 检查文件大小
|
||||
file_size = 0
|
||||
content = await file.read()
|
||||
file_size = len(content)
|
||||
|
||||
if file_size > settings.MAX_UPLOAD_SIZE:
|
||||
return error_response(message=f"文件大小不能超过{settings.MAX_UPLOAD_SIZE // 1024 // 1024}MB")
|
||||
|
||||
# 检查文件扩展名
|
||||
filename = file.filename or ""
|
||||
extension = filename.split('.')[-1].lower() if '.' in filename else ''
|
||||
if extension not in settings.ALLOWED_EXTENSIONS:
|
||||
return error_response(message=f"不支持的文件类型,仅支持 {', '.join(settings.ALLOWED_EXTENSIONS)}")
|
||||
|
||||
# 解析JSON
|
||||
try:
|
||||
import json
|
||||
data = json.loads(content.decode('utf-8'))
|
||||
students = data.get("students", [])
|
||||
except json.JSONDecodeError as e:
|
||||
return error_response(message=f"JSON格式错误: {str(e)}")
|
||||
except UnicodeDecodeError:
|
||||
return error_response(message="文件编码错误,请使用UTF-8编码")
|
||||
|
||||
if not students:
|
||||
return error_response(message="文件中没有学生数据")
|
||||
|
||||
# 导入学生(初始操行分60分)
|
||||
result = await AdminService.import_students(
|
||||
students=students,
|
||||
operator_id=user["user_id"],
|
||||
initial_points=60
|
||||
)
|
||||
|
||||
return success_response(data=result, message=f"导入完成: 成功{result['success_count']}人,失败{result['failed_count']}人")
|
||||
100
backend/routes/auth.py
Normal file
100
backend/routes/auth.py
Normal file
@@ -0,0 +1,100 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from typing import Dict, Any
|
||||
|
||||
from schemas.auth import LoginRequest, ChangePasswordRequest
|
||||
from services.auth_service import AuthService
|
||||
from middleware.permission import get_current_user
|
||||
from utils.response import success_response, error_response, unauthorized_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.post("/login")
|
||||
async def login(request: LoginRequest, http_request: Request):
|
||||
"""
|
||||
用户登录
|
||||
"""
|
||||
# 获取客户端IP
|
||||
client_ip = http_request.client.host
|
||||
|
||||
result = await AuthService.login(
|
||||
username=request.username,
|
||||
password=request.password,
|
||||
ip=client_ip
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(
|
||||
data={
|
||||
"token": result["token"],
|
||||
"user_id": result["user_id"],
|
||||
"username": result["username"],
|
||||
"real_name": result["real_name"],
|
||||
"user_type": result["user_type"],
|
||||
"need_change_password": result["need_change_password"],
|
||||
"redirect": result["redirect"]
|
||||
},
|
||||
message="登录成功"
|
||||
)
|
||||
else:
|
||||
return error_response(message=result["message"], code=401)
|
||||
|
||||
|
||||
@router.post("/logout")
|
||||
async def logout(request: Request):
|
||||
"""
|
||||
用户登出
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
result = await AuthService.logout(user["user_id"])
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="登出成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.post("/change-password")
|
||||
async def change_password(request: Request, req: ChangePasswordRequest):
|
||||
"""
|
||||
修改密码
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await AuthService.change_password(
|
||||
user_id=user["user_id"],
|
||||
old_password=req.old_password,
|
||||
new_password=req.new_password
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="密码修改成功,请重新登录")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.get("/me")
|
||||
async def get_current_user_info(request: Request):
|
||||
"""
|
||||
获取当前用户信息
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 获取用户详细信息
|
||||
from services.auth_service import AuthService
|
||||
user_info = await AuthService.get_user_info(user["user_id"])
|
||||
|
||||
return success_response(data=user_info)
|
||||
69
backend/routes/debug.py
Normal file
69
backend/routes/debug.py
Normal file
@@ -0,0 +1,69 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional
|
||||
|
||||
from config import settings
|
||||
from services.admin_service import AdminService
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AddAdminDebugRequest(BaseModel):
|
||||
"""添加管理员请求"""
|
||||
username: str
|
||||
password: str
|
||||
real_name: str
|
||||
role_type: str # 班主任/班长/科代表/考勤委员/劳动委员
|
||||
class_id: int
|
||||
subject_id: Optional[int] = None
|
||||
|
||||
|
||||
@router.post(settings.DEBUG_PATH)
|
||||
async def debug_add_admin(request: Request, req: AddAdminDebugRequest):
|
||||
"""
|
||||
调试入口 - 添加第一批管理员
|
||||
注意:此接口仅用于首次部署,使用后建议注释掉此路由
|
||||
"""
|
||||
# 检查是否已存在管理员
|
||||
from models.user import UserModel
|
||||
existing = await UserModel.get_by_username(req.username)
|
||||
if existing:
|
||||
return error_response(message="用户名已存在")
|
||||
|
||||
# 创建管理员账号
|
||||
result = await AdminService.add_admin(
|
||||
username=req.username,
|
||||
real_name=req.real_name,
|
||||
password=req.password,
|
||||
role_type=req.role_type,
|
||||
class_id=req.class_id,
|
||||
subject_id=req.subject_id,
|
||||
operator_id=0 # 系统添加
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
logger.info(f"调试入口创建管理员: {req.username} ({req.role_type})")
|
||||
return success_response(
|
||||
data={
|
||||
"username": req.username,
|
||||
"password": req.password,
|
||||
"role_type": req.role_type
|
||||
},
|
||||
message=f"管理员 {req.username} 创建成功"
|
||||
)
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
66
backend/routes/parent.py
Normal file
66
backend/routes/parent.py
Normal file
@@ -0,0 +1,66 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user
|
||||
from services.parent_service import ParentService
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/child/conduct")
|
||||
async def get_child_conduct(request: Request):
|
||||
"""
|
||||
获取子女操行分(仅总分)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "parent":
|
||||
return error_response(message="仅限家长访问", code=403)
|
||||
|
||||
result = await ParentService.get_child_conduct(user["user_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/child/homework")
|
||||
async def get_child_homework(request: Request):
|
||||
"""
|
||||
获取子女作业情况
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "parent":
|
||||
return error_response(message="仅限家长访问", code=403)
|
||||
|
||||
result = await ParentService.get_child_homework(user["user_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/child/attendance")
|
||||
async def get_child_attendance(request: Request):
|
||||
"""
|
||||
获取子女考勤记录
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "parent":
|
||||
return error_response(message="仅限家长访问", code=403)
|
||||
|
||||
result = await ParentService.get_child_attendance(user["user_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
120
backend/routes/student.py
Normal file
120
backend/routes/student.py
Normal file
@@ -0,0 +1,120 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user
|
||||
from services.student_service import StudentService
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/conduct/{student_id}")
|
||||
async def get_conduct_history(
|
||||
request: Request,
|
||||
student_id: int,
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
offset: int = Query(0, ge=0)
|
||||
):
|
||||
"""
|
||||
获取学生操行分历史
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 权限检查:只能查看自己的信息(学生)或同班(管理员)
|
||||
if user["user_type"] == "student" and user["student_id"] != student_id:
|
||||
return error_response(message="无权查看其他学生信息", code=403)
|
||||
|
||||
result = await StudentService.get_conduct_history(
|
||||
student_id=student_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/homework/{student_id}")
|
||||
async def get_homework_status(request: Request, student_id: int):
|
||||
"""
|
||||
获取学生作业情况
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 权限检查
|
||||
if user["user_type"] == "student" and user["student_id"] != student_id:
|
||||
return error_response(message="无权查看其他学生信息", code=403)
|
||||
|
||||
result = await StudentService.get_homework_status(student_id)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/attendance/{student_id}")
|
||||
async def get_attendance_records(
|
||||
request: Request,
|
||||
student_id: int,
|
||||
month: Optional[str] = None
|
||||
):
|
||||
"""
|
||||
获取学生考勤记录
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
# 权限检查
|
||||
if user["user_type"] == "student" and user["student_id"] != student_id:
|
||||
return error_response(message="无权查看其他学生信息", code=403)
|
||||
|
||||
result = await StudentService.get_attendance_records(
|
||||
student_id=student_id,
|
||||
month=month
|
||||
)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/ranking")
|
||||
async def get_ranking(
|
||||
request: Request,
|
||||
class_id: Optional[int] = None,
|
||||
limit: int = Query(50, ge=1, le=100)
|
||||
):
|
||||
"""
|
||||
获取操行分排行榜
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await StudentService.get_ranking(
|
||||
user_id=user["user_id"],
|
||||
class_id=class_id,
|
||||
limit=limit
|
||||
)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.get("/my-info")
|
||||
async def get_my_info(request: Request):
|
||||
"""
|
||||
获取当前学生个人信息
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
if user["user_type"] != "student":
|
||||
return error_response(message="仅限学生访问", code=403)
|
||||
|
||||
result = await StudentService.get_student_info(user["student_id"])
|
||||
|
||||
return success_response(data=result)
|
||||
105
backend/routes/subject.py
Normal file
105
backend/routes/subject.py
Normal file
@@ -0,0 +1,105 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from fastapi import APIRouter, Request, Query
|
||||
from typing import Optional
|
||||
|
||||
from middleware.permission import get_current_user, PermissionChecker
|
||||
from services.subject_service import SubjectService
|
||||
from schemas.subject import CreateSubjectRequest, UpdateSubjectRequest
|
||||
from utils.response import success_response, error_response
|
||||
from utils.logger import get_logger
|
||||
|
||||
router = APIRouter()
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
@router.get("/list")
|
||||
async def get_subjects(
|
||||
request: Request,
|
||||
is_active: Optional[bool] = None
|
||||
):
|
||||
"""
|
||||
获取科目列表
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
result = await SubjectService.get_subjects(is_active=is_active)
|
||||
|
||||
return success_response(data=result)
|
||||
|
||||
|
||||
@router.post("/create")
|
||||
async def create_subject(request: Request, req: CreateSubjectRequest):
|
||||
"""
|
||||
创建科目(班主任)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可创建科目", code=403)
|
||||
|
||||
result = await SubjectService.create_subject(
|
||||
subject_name=req.subject_name,
|
||||
subject_code=req.subject_code,
|
||||
sort_order=req.sort_order
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(data=result, message="科目创建成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.put("/update/{subject_id}")
|
||||
async def update_subject(
|
||||
request: Request,
|
||||
subject_id: int,
|
||||
req: UpdateSubjectRequest
|
||||
):
|
||||
"""
|
||||
更新科目(班主任)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可更新科目", code=403)
|
||||
|
||||
result = await SubjectService.update_subject(
|
||||
subject_id=subject_id,
|
||||
**req.dict(exclude_none=True)
|
||||
)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="科目更新成功")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
|
||||
|
||||
@router.delete("/delete/{subject_id}")
|
||||
async def delete_subject(request: Request, subject_id: int):
|
||||
"""
|
||||
删除科目(软删除,班主任)
|
||||
"""
|
||||
user = await get_current_user(request)
|
||||
|
||||
is_teacher = await PermissionChecker.check_is_teacher(user["user_id"])
|
||||
if not is_teacher:
|
||||
return error_response(message="仅班主任可删除科目", code=403)
|
||||
|
||||
result = await SubjectService.delete_subject(subject_id)
|
||||
|
||||
if result["success"]:
|
||||
return success_response(message="科目已禁用")
|
||||
else:
|
||||
return error_response(message=result["message"])
|
||||
11
backend/schemas/__init__.py
Normal file
11
backend/schemas/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
81
backend/schemas/admin.py
Normal file
81
backend/schemas/admin.py
Normal file
@@ -0,0 +1,81 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class AddPointsRequest(BaseModel):
|
||||
"""加减分请求"""
|
||||
student_ids: List[int] = Field(..., description="学生ID列表")
|
||||
points_change: int = Field(..., description="分数变动")
|
||||
reason: str = Field(..., min_length=1, max_length=255, description="原因")
|
||||
|
||||
|
||||
class AddPointsResponse(BaseModel):
|
||||
"""加减分响应"""
|
||||
success_count: int
|
||||
fail_count: int
|
||||
details: List[dict]
|
||||
|
||||
|
||||
class RevokeRequest(BaseModel):
|
||||
"""撤销请求"""
|
||||
record_id: int = Field(..., description="记录ID")
|
||||
|
||||
|
||||
class ImportStudentsRequest(BaseModel):
|
||||
"""导入学生请求"""
|
||||
students: List[dict] = Field(..., description="学生列表")
|
||||
|
||||
|
||||
class ImportResult(BaseModel):
|
||||
"""导入结果"""
|
||||
total: int
|
||||
success: int
|
||||
failed: int
|
||||
errors: List[str]
|
||||
|
||||
|
||||
class AddAdminRequest(BaseModel):
|
||||
"""添加管理员请求"""
|
||||
username: str = Field(..., min_length=1, max_length=50, description="登录账号")
|
||||
real_name: str = Field(..., min_length=1, max_length=50, description="真实姓名")
|
||||
password: Optional[str] = Field(None, description="密码(不填则自动生成)")
|
||||
role_type: str = Field(..., description="角色类型")
|
||||
class_id: int = Field(..., description="班级ID")
|
||||
subject_id: Optional[int] = Field(None, description="科目ID(科代表需要)")
|
||||
|
||||
|
||||
class AddAdminResponse(BaseModel):
|
||||
"""添加管理员响应"""
|
||||
success: bool
|
||||
username: str
|
||||
password: Optional[str] = None
|
||||
message: str
|
||||
|
||||
|
||||
class UpdateHomeworkStatusRequest(BaseModel):
|
||||
"""更新作业状态请求"""
|
||||
submission_id: int
|
||||
status: str
|
||||
comments: Optional[str] = None
|
||||
apply_deduction: bool = False
|
||||
|
||||
|
||||
class AddAttendanceRequest(BaseModel):
|
||||
"""添加考勤请求"""
|
||||
student_id: int
|
||||
date: date
|
||||
status: str
|
||||
reason: Optional[str] = None
|
||||
apply_deduction: bool = False
|
||||
54
backend/schemas/auth.py
Normal file
54
backend/schemas/auth.py
Normal file
@@ -0,0 +1,54 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class LoginRequest(BaseModel):
|
||||
"""登录请求"""
|
||||
username: str = Field(..., min_length=1, max_length=50, description="用户名")
|
||||
password: str = Field(..., min_length=1, max_length=50, description="密码")
|
||||
|
||||
|
||||
class LoginResponse(BaseModel):
|
||||
"""登录响应"""
|
||||
success: bool
|
||||
token: str
|
||||
user_id: int
|
||||
username: str
|
||||
real_name: str
|
||||
user_type: str
|
||||
need_change_password: bool
|
||||
redirect: str
|
||||
|
||||
|
||||
class ChangePasswordRequest(BaseModel):
|
||||
"""修改密码请求"""
|
||||
old_password: str = Field(..., min_length=1, max_length=50, description="原密码")
|
||||
new_password: str = Field(..., min_length=6, max_length=20, description="新密码")
|
||||
|
||||
|
||||
class ChangePasswordResponse(BaseModel):
|
||||
"""修改密码响应"""
|
||||
success: bool
|
||||
message: str
|
||||
|
||||
|
||||
class UserInfo(BaseModel):
|
||||
"""用户信息"""
|
||||
user_id: int
|
||||
username: str
|
||||
real_name: str
|
||||
user_type: str
|
||||
student_id: Optional[int] = None
|
||||
role: Optional[str] = None
|
||||
need_change_password: bool
|
||||
79
backend/schemas/student.py
Normal file
79
backend/schemas/student.py
Normal file
@@ -0,0 +1,79 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
from datetime import date, datetime
|
||||
|
||||
|
||||
class StudentInfo(BaseModel):
|
||||
"""学生信息"""
|
||||
student_id: int
|
||||
student_no: str
|
||||
name: str
|
||||
class_id: int
|
||||
class_name: Optional[str] = None
|
||||
total_points: int
|
||||
parent_phone: Optional[str] = None
|
||||
status: int
|
||||
|
||||
|
||||
class ConductRecord(BaseModel):
|
||||
"""操行分记录"""
|
||||
record_id: int
|
||||
student_id: int
|
||||
student_name: Optional[str] = None
|
||||
points_change: int
|
||||
reason: str
|
||||
recorder_id: int
|
||||
recorder_name: str
|
||||
related_type: str
|
||||
is_revoked: bool
|
||||
created_at: datetime
|
||||
|
||||
|
||||
class ConductHistoryResponse(BaseModel):
|
||||
"""操行分历史响应"""
|
||||
student_id: int
|
||||
student_name: str
|
||||
total_points: int
|
||||
records: List[ConductRecord]
|
||||
|
||||
|
||||
class HomeworkSubmission(BaseModel):
|
||||
"""作业提交情况"""
|
||||
assignment_id: int
|
||||
title: str
|
||||
subject: str
|
||||
deadline: date
|
||||
status: str
|
||||
submit_time: Optional[datetime] = None
|
||||
comments: Optional[str] = None
|
||||
deduction_applied: bool
|
||||
|
||||
|
||||
class AttendanceRecord(BaseModel):
|
||||
"""考勤记录"""
|
||||
attendance_id: int
|
||||
date: date
|
||||
status: str
|
||||
reason: Optional[str] = None
|
||||
deduction_applied: bool
|
||||
|
||||
|
||||
class StudentRanking(BaseModel):
|
||||
"""学生排行"""
|
||||
student_id: int
|
||||
student_no: str
|
||||
name: str
|
||||
class_name: str
|
||||
total_points: int
|
||||
rank_in_class: int
|
||||
43
backend/schemas/subject.py
Normal file
43
backend/schemas/subject.py
Normal file
@@ -0,0 +1,43 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Optional, List
|
||||
|
||||
|
||||
class SubjectInfo(BaseModel):
|
||||
"""科目信息"""
|
||||
subject_id: int
|
||||
subject_name: str
|
||||
subject_code: Optional[str] = None
|
||||
is_active: bool
|
||||
sort_order: int
|
||||
|
||||
|
||||
class CreateSubjectRequest(BaseModel):
|
||||
"""创建科目请求"""
|
||||
subject_name: str = Field(..., min_length=1, max_length=50, description="科目名称")
|
||||
subject_code: Optional[str] = Field(None, max_length=20, description="科目代码")
|
||||
sort_order: int = Field(0, description="排序序号")
|
||||
|
||||
|
||||
class UpdateSubjectRequest(BaseModel):
|
||||
"""更新科目请求"""
|
||||
subject_name: Optional[str] = Field(None, max_length=50, description="科目名称")
|
||||
subject_code: Optional[str] = Field(None, max_length=20, description="科目代码")
|
||||
is_active: Optional[bool] = Field(None, description="是否启用")
|
||||
sort_order: Optional[int] = Field(None, description="排序序号")
|
||||
|
||||
|
||||
class SubjectListResponse(BaseModel):
|
||||
"""科目列表响应"""
|
||||
subjects: List[SubjectInfo]
|
||||
total: int
|
||||
11
backend/services/__init__.py
Normal file
11
backend/services/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
298
backend/services/admin_service.py
Normal file
298
backend/services/admin_service.py
Normal file
@@ -0,0 +1,298 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 管理员服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.admin_role import AdminRoleModel
|
||||
from utils.security import security
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AdminService:
|
||||
"""管理员服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_students(
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
search: str = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取所有学生列表(单班级)"""
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
sql = """
|
||||
SELECT student_id, student_no, name, total_points, parent_phone, status
|
||||
FROM students
|
||||
WHERE status = 1
|
||||
"""
|
||||
params = []
|
||||
|
||||
if search:
|
||||
sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
||||
params.extend([f"%{search}%", f"%{search}%"])
|
||||
|
||||
sql += " ORDER BY student_no LIMIT %s OFFSET %s"
|
||||
params.extend([page_size, offset])
|
||||
|
||||
students = await execute_query(sql, tuple(params))
|
||||
|
||||
# 获取总数
|
||||
count_sql = "SELECT COUNT(*) as total FROM students WHERE status = 1"
|
||||
if search:
|
||||
count_sql += " AND (student_no LIKE %s OR name LIKE %s)"
|
||||
total_result = await execute_one(count_sql, (f"%{search}%", f"%{search}%"))
|
||||
else:
|
||||
total_result = await execute_one(count_sql)
|
||||
|
||||
total = total_result["total"] if total_result else 0
|
||||
|
||||
return {
|
||||
"students": students,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def import_students(
|
||||
students: List[Dict],
|
||||
operator_id: int,
|
||||
initial_points: int = 60
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量导入学生(单班级版本)
|
||||
初始操行分默认60分
|
||||
"""
|
||||
results = []
|
||||
success_count = 0
|
||||
|
||||
for student in students:
|
||||
student_no = student.get("student_no", "").strip()
|
||||
name = student.get("name", "").strip()
|
||||
parent_phone = student.get("parent_phone", "").strip()
|
||||
password = student.get("password", "").strip()
|
||||
|
||||
# 验证必填字段
|
||||
if not student_no or not name:
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "学号或姓名不能为空"
|
||||
})
|
||||
continue
|
||||
|
||||
# 验证学号格式
|
||||
if not security.validate_student_no(student_no):
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "学号格式错误(4-20位字母数字组合)"
|
||||
})
|
||||
continue
|
||||
|
||||
# 验证手机号格式(如果有)
|
||||
if parent_phone and not security.validate_phone(parent_phone):
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "手机号格式错误"
|
||||
})
|
||||
continue
|
||||
|
||||
# 检查学号是否已存在
|
||||
existing = await StudentModel.get_by_student_no(student_no)
|
||||
if existing:
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": "学号已存在"
|
||||
})
|
||||
continue
|
||||
|
||||
# 设置初始密码
|
||||
init_password = password if password else "123456"
|
||||
|
||||
# 验证密码强度(可选)
|
||||
is_valid, msg = security.validate_password_strength(init_password)
|
||||
if not is_valid:
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": False,
|
||||
"error": f"密码不符合要求: {msg}"
|
||||
})
|
||||
continue
|
||||
|
||||
# 创建学生(初始操行分60分)
|
||||
student_id = await StudentModel.create(
|
||||
student_no=student_no,
|
||||
name=name,
|
||||
class_id=1, # 单班级,固定为1
|
||||
parent_phone=parent_phone if parent_phone else None,
|
||||
initial_points=initial_points
|
||||
)
|
||||
|
||||
# 创建学生账号
|
||||
await UserModel.create_student(
|
||||
username=student_no,
|
||||
password=init_password,
|
||||
real_name=name,
|
||||
student_id=student_id
|
||||
)
|
||||
|
||||
# 创建家长账号(如果有手机号)
|
||||
if parent_phone:
|
||||
parent_exists = await UserModel.get_by_username(parent_phone)
|
||||
if not parent_exists:
|
||||
parent_name = f"{name}家长"
|
||||
await UserModel.create_parent(
|
||||
username=parent_phone,
|
||||
password=init_password,
|
||||
real_name=parent_name,
|
||||
student_id=student_id
|
||||
)
|
||||
else:
|
||||
# 手机号已被占用
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": True,
|
||||
"student_id": student_id,
|
||||
"warning": f"家长手机号 {parent_phone} 已被其他账号使用,未创建家长账号"
|
||||
})
|
||||
success_count += 1
|
||||
continue
|
||||
|
||||
results.append({
|
||||
"student_no": student_no,
|
||||
"success": True,
|
||||
"student_id": student_id,
|
||||
"parent_phone": parent_phone if parent_phone else None
|
||||
})
|
||||
success_count += 1
|
||||
|
||||
logger.info(f"用户[{operator_id}] 导入学生: {student_no} - {name} (初始分:{initial_points})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"total": len(students),
|
||||
"success_count": success_count,
|
||||
"failed_count": len(students) - success_count,
|
||||
"results": results
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def add_student(
|
||||
student_no: str,
|
||||
name: str,
|
||||
parent_phone: Optional[str],
|
||||
operator_id: int,
|
||||
initial_points: int = 60
|
||||
) -> Dict[str, Any]:
|
||||
"""新增学生"""
|
||||
# 验证学号格式
|
||||
if not security.validate_student_no(student_no):
|
||||
return {"success": False, "message": "学号格式错误(4-20位字母数字组合)"}
|
||||
|
||||
# 验证手机号格式(如果有)
|
||||
if parent_phone and not security.validate_phone(parent_phone):
|
||||
return {"success": False, "message": "手机号格式错误"}
|
||||
|
||||
# 检查学号是否已存在
|
||||
existing = await StudentModel.get_by_student_no(student_no)
|
||||
if existing:
|
||||
return {"success": False, "message": "学号已存在"}
|
||||
|
||||
# 创建学生(初始操行分60分)
|
||||
student_id = await StudentModel.create(
|
||||
student_no=student_no,
|
||||
name=name,
|
||||
class_id=1, # 单班级,固定为1
|
||||
parent_phone=parent_phone if parent_phone else None,
|
||||
initial_points=initial_points
|
||||
)
|
||||
|
||||
# 创建学生账号
|
||||
await UserModel.create_student(
|
||||
username=student_no,
|
||||
password="123456",
|
||||
real_name=name,
|
||||
student_id=student_id
|
||||
)
|
||||
|
||||
# 创建家长账号
|
||||
if parent_phone:
|
||||
parent_exists = await UserModel.get_by_username(parent_phone)
|
||||
if not parent_exists:
|
||||
await UserModel.create_parent(
|
||||
username=parent_phone,
|
||||
password="123456",
|
||||
real_name=f"{name}家长",
|
||||
student_id=student_id
|
||||
)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 新增学生: {student_no} - {name}")
|
||||
|
||||
return {"success": True, "student_id": student_id, "student_no": student_no, "name": name}
|
||||
|
||||
@staticmethod
|
||||
async def add_admin(
|
||||
username: str,
|
||||
real_name: str,
|
||||
password: Optional[str],
|
||||
role_type: str,
|
||||
operator_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""添加管理员(单班级)"""
|
||||
# 检查用户名是否已存在
|
||||
existing = await UserModel.get_by_username(username)
|
||||
if existing:
|
||||
return {"success": False, "message": "用户名已存在"}
|
||||
|
||||
# 生成随机密码(如果未提供)
|
||||
if not password:
|
||||
password = security.generate_random_password()
|
||||
|
||||
# 创建管理员账号
|
||||
user_id = await UserModel.create_admin(
|
||||
username=username,
|
||||
password=password,
|
||||
real_name=real_name
|
||||
)
|
||||
|
||||
# 分配角色(班级ID固定为1)
|
||||
await AdminRoleModel.create(
|
||||
user_id=user_id,
|
||||
role_type=role_type,
|
||||
class_id=1, # 单班级,固定为1
|
||||
subject_id=None # 单班级版本暂不支持科目关联
|
||||
)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 添加管理员: {username} ({role_type})")
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"user_id": user_id,
|
||||
"username": username,
|
||||
"password": password,
|
||||
"role_type": role_type
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_admins() -> Dict[str, Any]:
|
||||
"""获取管理员列表(单班级)"""
|
||||
admins = await AdminRoleModel.get_by_class(1) # 班级ID固定为1
|
||||
|
||||
return {"admins": admins}
|
||||
114
backend/services/attendance_service.py
Normal file
114
backend/services/attendance_service.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.attendance import AttendanceModel
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AttendanceService:
|
||||
"""考勤服务"""
|
||||
|
||||
@staticmethod
|
||||
async def add_attendance(
|
||||
student_id: int,
|
||||
date: str,
|
||||
status: str,
|
||||
reason: Optional[str],
|
||||
apply_deduction: bool,
|
||||
recorder_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""添加考勤记录"""
|
||||
# 检查权限
|
||||
role = await PermissionChecker.get_user_role(recorder_id)
|
||||
if role not in ["班主任", "考勤委员"]:
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
# 检查是否同班级
|
||||
can_manage = await PermissionChecker.check_can_manage_student(recorder_id, student_id)
|
||||
if not can_manage:
|
||||
return {"success": False, "message": "无权操作该学生"}
|
||||
|
||||
# 添加考勤记录
|
||||
attendance_id = await AttendanceModel.create_record(
|
||||
student_id=student_id,
|
||||
date=date,
|
||||
status=status,
|
||||
reason=reason,
|
||||
recorder_id=recorder_id
|
||||
)
|
||||
|
||||
if not attendance_id:
|
||||
return {"success": False, "message": "添加考勤记录失败"}
|
||||
|
||||
# 应用扣分
|
||||
if apply_deduction and status in ["absent", "late", "leave"]:
|
||||
# 确定扣分数值
|
||||
if status == "absent":
|
||||
points_change = -settings.DEDUCTION_ATTENDANCE_ABSENT
|
||||
elif status == "late":
|
||||
points_change = -settings.DEDUCTION_ATTENDANCE_LATE
|
||||
else:
|
||||
points_change = -settings.DEDUCTION_ATTENDANCE_LEAVE
|
||||
|
||||
# 创建扣分记录
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if student:
|
||||
await ConductModel.create_record(
|
||||
student_id=student_id,
|
||||
points_change=points_change,
|
||||
reason=f"考勤异常: {status}",
|
||||
recorder_id=recorder_id,
|
||||
related_type="attendance",
|
||||
related_id=attendance_id
|
||||
)
|
||||
|
||||
# 标记已应用扣分
|
||||
await AttendanceModel.mark_deduction_applied(attendance_id)
|
||||
|
||||
logger.info(f"用户[{recorder_id}] 添加考勤记录[{attendance_id}] -> {status}")
|
||||
|
||||
return {"success": True, "message": "考勤记录添加成功"}
|
||||
|
||||
@staticmethod
|
||||
async def get_records(
|
||||
user_id: int,
|
||||
date: Optional[str] = None,
|
||||
student_id: Optional[int] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取考勤记录"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if role in ["班主任", "考勤委员"]:
|
||||
class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
records = await AttendanceModel.get_class_records(
|
||||
class_id=class_id,
|
||||
date=date,
|
||||
student_id=student_id
|
||||
)
|
||||
elif student_id:
|
||||
# 查看指定学生
|
||||
can_manage = await PermissionChecker.check_can_manage_student(user_id, student_id)
|
||||
if not can_manage:
|
||||
return {"error": "无权查看该学生记录"}
|
||||
records = await AttendanceModel.get_student_records(student_id)
|
||||
else:
|
||||
records = []
|
||||
|
||||
return {"records": records}
|
||||
169
backend/services/auth_service.py
Normal file
169
backend/services/auth_service.py
Normal file
@@ -0,0 +1,169 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.admin_role import AdminRoleModel
|
||||
from utils.security import security
|
||||
from utils.jwt_handler import jwt_handler
|
||||
from utils.redis_client import RedisClient
|
||||
from utils.database import execute_update
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class AuthService:
|
||||
"""认证服务"""
|
||||
|
||||
@staticmethod
|
||||
async def login(username: str, password: str, ip: str) -> Dict[str, Any]:
|
||||
"""
|
||||
用户登录
|
||||
"""
|
||||
# 检查登录失败次数
|
||||
attempts = await RedisClient.get(f"login_attempts:{username}")
|
||||
if attempts and int(attempts) >= 5:
|
||||
return {"success": False, "message": "登录失败次数过多,请15分钟后重试"}
|
||||
|
||||
# 获取用户信息
|
||||
user = await UserModel.get_by_username(username)
|
||||
|
||||
if not user:
|
||||
await RedisClient.set_login_attempts(username)
|
||||
return {"success": False, "message": "用户名或密码错误"}
|
||||
|
||||
# 验证密码
|
||||
if not security.verify_password(password, user["password_hash"]):
|
||||
await RedisClient.set_login_attempts(username)
|
||||
return {"success": False, "message": "用户名或密码错误"}
|
||||
|
||||
# 检查账号状态
|
||||
if user["status"] != 1:
|
||||
return {"success": False, "message": "账号已被禁用"}
|
||||
|
||||
# 清除登录失败记录
|
||||
await RedisClient.clear_login_attempts(username)
|
||||
|
||||
# 更新最后登录信息
|
||||
await UserModel.update_last_login(user["user_id"], ip)
|
||||
|
||||
# 获取用户角色(如果是管理员)
|
||||
role = None
|
||||
if user["user_type"] == "admin":
|
||||
admin_role = await AdminRoleModel.get_by_user_id(user["user_id"])
|
||||
role = admin_role["role_type"] if admin_role else None
|
||||
|
||||
# 生成Token
|
||||
token = jwt_handler.create_token(
|
||||
user_id=user["user_id"],
|
||||
username=user["username"],
|
||||
user_type=user["user_type"],
|
||||
student_id=user["student_id"],
|
||||
role=role
|
||||
)
|
||||
|
||||
# 存储Token到Redis
|
||||
await RedisClient.set_user_token(user["user_id"], token)
|
||||
|
||||
# 确定跳转路径
|
||||
redirect = AuthService._get_redirect_path(user["user_type"], role)
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"token": token,
|
||||
"user_id": user["user_id"],
|
||||
"username": user["username"],
|
||||
"real_name": user["real_name"],
|
||||
"user_type": user["user_type"],
|
||||
"need_change_password": user["need_change_password"] == 1,
|
||||
"redirect": redirect
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def logout(user_id: int) -> Dict[str, Any]:
|
||||
"""用户登出"""
|
||||
await RedisClient.delete_user_token(user_id)
|
||||
return {"success": True, "message": "登出成功"}
|
||||
|
||||
@staticmethod
|
||||
async def change_password(user_id: int, old_password: str, new_password: str) -> Dict[str, Any]:
|
||||
"""修改密码"""
|
||||
# 获取用户信息
|
||||
user = await UserModel.get_by_user_id(user_id)
|
||||
if not user:
|
||||
return {"success": False, "message": "用户不存在"}
|
||||
|
||||
# 验证原密码
|
||||
if not security.verify_password(old_password, user["password_hash"]):
|
||||
return {"success": False, "message": "原密码错误"}
|
||||
|
||||
# 验证新密码强度
|
||||
is_valid, msg = security.validate_password_strength(new_password)
|
||||
if not is_valid:
|
||||
return {"success": False, "message": msg}
|
||||
|
||||
# 更新密码
|
||||
result = await UserModel.update_password(user_id, new_password)
|
||||
|
||||
if result:
|
||||
# 清除所有Token
|
||||
await RedisClient.delete_user_token(user_id)
|
||||
return {"success": True, "message": "密码修改成功"}
|
||||
else:
|
||||
return {"success": False, "message": "密码修改失败"}
|
||||
|
||||
@staticmethod
|
||||
async def get_user_info(user_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取用户信息"""
|
||||
user = await UserModel.get_by_user_id(user_id)
|
||||
if not user:
|
||||
return None
|
||||
|
||||
result = {
|
||||
"user_id": user["user_id"],
|
||||
"username": user["username"],
|
||||
"real_name": user["real_name"],
|
||||
"user_type": user["user_type"],
|
||||
"need_change_password": user["need_change_password"] == 1
|
||||
}
|
||||
|
||||
# 获取学生信息
|
||||
if user["student_id"]:
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if student:
|
||||
result["student_no"] = student["student_no"]
|
||||
result["student_name"] = student["name"]
|
||||
result["class_id"] = student["class_id"]
|
||||
result["class_name"] = student["class_name"]
|
||||
result["total_points"] = student["total_points"]
|
||||
|
||||
# 获取管理员角色
|
||||
if user["user_type"] == "admin":
|
||||
admin_role = await AdminRoleModel.get_by_user_id(user_id)
|
||||
if admin_role:
|
||||
result["role"] = admin_role["role_type"]
|
||||
result["class_id"] = admin_role["class_id"]
|
||||
|
||||
return result
|
||||
|
||||
@staticmethod
|
||||
def _get_redirect_path(user_type: str, role: str = None) -> str:
|
||||
"""获取跳转路径"""
|
||||
if user_type == "student":
|
||||
return "/student/dashboard.php"
|
||||
elif user_type == "parent":
|
||||
return "/parent/dashboard.php"
|
||||
else:
|
||||
return "/admin/dashboard.php"
|
||||
182
backend/services/conduct_service.py
Normal file
182
backend/services/conduct_service.py
Normal file
@@ -0,0 +1,182 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from models.user import UserModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ConductService:
|
||||
"""操行分服务"""
|
||||
|
||||
@staticmethod
|
||||
async def add_points(
|
||||
student_ids: List[int],
|
||||
points_change: int,
|
||||
reason: str,
|
||||
recorder_id: int,
|
||||
recorder_name: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
批量加减分
|
||||
"""
|
||||
# 验证分值
|
||||
if points_change == 0:
|
||||
return {"success": False, "message": "分值不能为0"}
|
||||
|
||||
# 获取操作人角色
|
||||
role = await PermissionChecker.get_user_role(recorder_id)
|
||||
|
||||
# 权限验证
|
||||
if role == "班主任":
|
||||
# 班主任无限制
|
||||
pass
|
||||
elif role == "班长":
|
||||
# 班长限制 ±5分
|
||||
if points_change > settings.MONITOR_MAX_ADD or points_change < settings.MONITOR_MAX_SUBTRACT:
|
||||
return {"success": False, "message": f"班长单次只能加减{settings.MONITOR_MAX_ADD}分以内"}
|
||||
elif role == "劳动委员":
|
||||
# 劳动委员固定 ±1分
|
||||
if points_change not in [settings.LABOR_POINTS_ADD, settings.LABOR_POINTS_SUBTRACT]:
|
||||
return {"success": False, "message": "劳动委员只能进行±1分操作"}
|
||||
elif role in ["科代表", "考勤委员"]:
|
||||
# 科代表和考勤委员只能扣分
|
||||
if points_change > 0:
|
||||
return {"success": False, "message": "该角色只能进行扣分操作"}
|
||||
else:
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
# 批量处理
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
details = []
|
||||
|
||||
for student_id in student_ids:
|
||||
try:
|
||||
# 检查学生是否存在
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
details.append({"student_id": student_id, "error": "学生不存在"})
|
||||
fail_count += 1
|
||||
continue
|
||||
|
||||
# 创建记录
|
||||
record_id = await ConductModel.create_record(
|
||||
student_id=student_id,
|
||||
points_change=points_change,
|
||||
reason=reason,
|
||||
recorder_id=recorder_id,
|
||||
recorder_name=recorder_name
|
||||
)
|
||||
|
||||
details.append({"student_id": student_id, "success": True, "record_id": record_id})
|
||||
success_count += 1
|
||||
|
||||
logger.info(f"用户[{recorder_id}] 对学生[{student_id}] 进行 {points_change} 分操作")
|
||||
|
||||
except Exception as e:
|
||||
details.append({"student_id": student_id, "error": str(e)})
|
||||
fail_count += 1
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
"success_count": success_count,
|
||||
"fail_count": fail_count,
|
||||
"details": details
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def revoke_record(record_id: int, revoker_id: int) -> Dict[str, Any]:
|
||||
"""撤销扣分记录"""
|
||||
# 检查权限
|
||||
can_revoke = await PermissionChecker.check_can_revoke(revoker_id, record_id)
|
||||
if not can_revoke:
|
||||
return {"success": False, "message": "无权撤销此记录"}
|
||||
|
||||
# 撤销记录
|
||||
result = await ConductModel.revoke_record(record_id, revoker_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"用户[{revoker_id}] 撤销了记录[{record_id}]")
|
||||
return {"success": True, "message": "撤销成功"}
|
||||
else:
|
||||
return {"success": False, "message": "撤销失败"}
|
||||
|
||||
@staticmethod
|
||||
async def get_history(
|
||||
user_id: int,
|
||||
student_id: Optional[int] = None,
|
||||
page: int = 1,
|
||||
page_size: int = 20,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取历史记录"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
offset = (page - 1) * page_size
|
||||
|
||||
# 班主任/班长可查看全班
|
||||
if role in ["班主任", "班长"]:
|
||||
user_class = await PermissionChecker.get_user_class_id(user_id)
|
||||
records = await ConductModel.get_all_records(
|
||||
class_id=user_class,
|
||||
limit=page_size,
|
||||
offset=offset,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
|
||||
# 获取总数
|
||||
from utils.database import execute_one
|
||||
count_sql = """
|
||||
SELECT COUNT(*) as total FROM conduct_records cr
|
||||
JOIN students s ON cr.student_id = s.student_id
|
||||
WHERE s.class_id = %s AND cr.is_revoked = 0
|
||||
"""
|
||||
total_result = await execute_one(count_sql, (user_class,))
|
||||
total = total_result["total"] if total_result else 0
|
||||
|
||||
elif student_id:
|
||||
# 查看指定学生(需权限验证)
|
||||
can_manage = await PermissionChecker.check_can_manage_student(user_id, student_id)
|
||||
if not can_manage:
|
||||
return {"error": "无权查看该学生记录"}
|
||||
|
||||
records = await ConductModel.get_student_records(
|
||||
student_id=student_id,
|
||||
limit=page_size,
|
||||
offset=offset
|
||||
)
|
||||
total = len(await ConductModel.get_student_records(student_id, limit=10000))
|
||||
else:
|
||||
# 查看自己提交的记录
|
||||
records = await ConductModel.get_records_by_recorder(
|
||||
recorder_id=user_id,
|
||||
limit=page_size,
|
||||
offset=offset
|
||||
)
|
||||
total = len(await ConductModel.get_records_by_recorder(user_id, limit=10000))
|
||||
|
||||
return {
|
||||
"records": records,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total": total,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
131
backend/services/homework_service.py
Normal file
131
backend/services/homework_service.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
|
||||
from models.homework import HomeworkModel
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class HomeworkService:
|
||||
"""作业服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_assignments(user_id: int) -> Dict[str, Any]:
|
||||
"""获取作业列表"""
|
||||
role = await PermissionChecker.get_user_role(user_id)
|
||||
|
||||
if role == "班主任":
|
||||
class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
assignments = await HomeworkModel.get_assignments_by_class(class_id)
|
||||
elif role == "科代表":
|
||||
class_id = await PermissionChecker.get_user_class_id(user_id)
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(user_id)
|
||||
assignments = await HomeworkModel.get_assignments_by_subjects(class_id, subject_ids)
|
||||
else:
|
||||
assignments = []
|
||||
|
||||
return {"assignments": assignments}
|
||||
|
||||
@staticmethod
|
||||
async def create_assignment(
|
||||
subject_id: int,
|
||||
title: str,
|
||||
description: Optional[str],
|
||||
deadline: str,
|
||||
created_by: int
|
||||
) -> Dict[str, Any]:
|
||||
"""创建作业"""
|
||||
class_id = await PermissionChecker.get_user_class_id(created_by)
|
||||
|
||||
assignment_id = await HomeworkModel.create_assignment(
|
||||
class_id=class_id,
|
||||
subject_id=subject_id,
|
||||
title=title,
|
||||
description=description,
|
||||
deadline=deadline,
|
||||
created_by=created_by
|
||||
)
|
||||
|
||||
if assignment_id:
|
||||
logger.info(f"用户[{created_by}] 创建作业[{assignment_id}]: {title}")
|
||||
return {"success": True, "assignment_id": assignment_id}
|
||||
else:
|
||||
return {"success": False, "message": "创建作业失败"}
|
||||
|
||||
@staticmethod
|
||||
async def update_submission_status(
|
||||
submission_id: int,
|
||||
status: str,
|
||||
comments: Optional[str],
|
||||
apply_deduction: bool,
|
||||
operator_id: int
|
||||
) -> Dict[str, Any]:
|
||||
"""更新作业提交状态"""
|
||||
# 获取提交记录信息
|
||||
submission = await HomeworkModel.get_submission(submission_id)
|
||||
if not submission:
|
||||
return {"success": False, "message": "提交记录不存在"}
|
||||
|
||||
# 检查权限
|
||||
role = await PermissionChecker.get_user_role(operator_id)
|
||||
if role == "科代表":
|
||||
# 检查是否管理该科目
|
||||
subject_ids = await PermissionChecker.get_user_subject_ids(operator_id)
|
||||
if submission["subject_id"] not in subject_ids:
|
||||
return {"success": False, "message": "无权操作此作业"}
|
||||
elif role != "班主任":
|
||||
return {"success": False, "message": "无权进行此操作"}
|
||||
|
||||
# 更新状态
|
||||
result = await HomeworkModel.update_submission(
|
||||
submission_id=submission_id,
|
||||
status=status,
|
||||
comments=comments,
|
||||
updated_by=operator_id
|
||||
)
|
||||
|
||||
if not result:
|
||||
return {"success": False, "message": "更新失败"}
|
||||
|
||||
# 应用扣分
|
||||
if apply_deduction and status in ["not_submitted", "late"]:
|
||||
# 确定扣分数值
|
||||
if status == "not_submitted":
|
||||
points_change = -settings.DEDUCTION_HOMEWORK_NOT_SUBMIT
|
||||
else:
|
||||
points_change = -settings.DEDUCTION_HOMEWORK_LATE
|
||||
|
||||
# 创建扣分记录
|
||||
student = await StudentModel.get_by_id(submission["student_id"])
|
||||
if student:
|
||||
await ConductModel.create_record(
|
||||
student_id=submission["student_id"],
|
||||
points_change=points_change,
|
||||
reason=f"作业未提交/迟交: {submission['title']}",
|
||||
recorder_id=operator_id,
|
||||
related_type="homework",
|
||||
related_id=submission["assignment_id"]
|
||||
)
|
||||
|
||||
# 标记已应用扣分
|
||||
await HomeworkModel.mark_deduction_applied(submission_id)
|
||||
|
||||
logger.info(f"用户[{operator_id}] 更新作业提交状态[{submission_id}] -> {status}")
|
||||
|
||||
return {"success": True, "message": "状态更新成功"}
|
||||
82
backend/services/parent_service.py
Normal file
82
backend/services/parent_service.py
Normal file
@@ -0,0 +1,82 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, Optional
|
||||
|
||||
from models.user import UserModel
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from models.homework import HomeworkModel
|
||||
from models.attendance import AttendanceModel
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class ParentService:
|
||||
"""家长服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_child_conduct(parent_id: int) -> Dict[str, Any]:
|
||||
"""获取子女操行分(仅总分,家长端不显示详细记录)"""
|
||||
# 获取家长关联的学生
|
||||
user = await UserModel.get_by_user_id(parent_id)
|
||||
if not user or not user["student_id"]:
|
||||
return {"error": "未关联学生"}
|
||||
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"student_no": student["student_no"],
|
||||
"total_points": student["total_points"]
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_child_homework(parent_id: int) -> Dict[str, Any]:
|
||||
"""获取子女作业情况"""
|
||||
user = await UserModel.get_by_user_id(parent_id)
|
||||
if not user or not user["student_id"]:
|
||||
return {"error": "未关联学生"}
|
||||
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
homework = await HomeworkModel.get_student_homework(user["student_id"])
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"homework": homework
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_child_attendance(parent_id: int) -> Dict[str, Any]:
|
||||
"""获取子女考勤记录"""
|
||||
user = await UserModel.get_by_user_id(parent_id)
|
||||
if not user or not user["student_id"]:
|
||||
return {"error": "未关联学生"}
|
||||
|
||||
student = await StudentModel.get_by_id(user["student_id"])
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
records = await AttendanceModel.get_student_records(user["student_id"])
|
||||
|
||||
return {
|
||||
"student_id": student["student_id"],
|
||||
"student_name": student["name"],
|
||||
"records": records
|
||||
}
|
||||
146
backend/services/student_service.py
Normal file
146
backend/services/student_service.py
Normal file
@@ -0,0 +1,146 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from models.student import StudentModel
|
||||
from models.conduct import ConductModel
|
||||
from models.homework import HomeworkModel
|
||||
from models.attendance import AttendanceModel
|
||||
from middleware.permission import PermissionChecker
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class StudentService:
|
||||
"""学生服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_conduct_history(
|
||||
student_id: int,
|
||||
limit: int = 50,
|
||||
offset: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""获取学生操行分历史(学生端显示,扣分项操作人显示为班主任)"""
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
records = await ConductModel.get_student_records(
|
||||
student_id=student_id,
|
||||
limit=limit,
|
||||
offset=offset
|
||||
)
|
||||
|
||||
# 处理记录:扣分项的操作人统一显示为"班主任"
|
||||
for record in records:
|
||||
if record["points_change"] < 0: # 扣分项
|
||||
record["recorder_name"] = "班主任"
|
||||
# 加分项保持原操作人不变
|
||||
|
||||
return {
|
||||
"student_id": student_id,
|
||||
"student_name": student["name"],
|
||||
"total_points": student["total_points"],
|
||||
"records": records
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_homework_status(student_id: int) -> Dict[str, Any]:
|
||||
"""获取学生作业情况"""
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
homework = await HomeworkModel.get_student_homework(student_id)
|
||||
|
||||
# 统计
|
||||
total = len(homework)
|
||||
submitted = sum(1 for h in homework if h["status"] == "submitted")
|
||||
not_submitted = sum(1 for h in homework if h["status"] == "not_submitted")
|
||||
late = sum(1 for h in homework if h["status"] == "late")
|
||||
|
||||
return {
|
||||
"student_id": student_id,
|
||||
"student_name": student["name"],
|
||||
"statistics": {
|
||||
"total": total,
|
||||
"submitted": submitted,
|
||||
"not_submitted": not_submitted,
|
||||
"late": late
|
||||
},
|
||||
"homework": homework
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_attendance_records(
|
||||
student_id: int,
|
||||
month: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""获取学生考勤记录"""
|
||||
student = await StudentModel.get_by_id(student_id)
|
||||
if not student:
|
||||
return {"error": "学生不存在"}
|
||||
|
||||
records = await AttendanceModel.get_student_records(
|
||||
student_id=student_id,
|
||||
month=month
|
||||
)
|
||||
|
||||
# 统计
|
||||
present = sum(1 for r in records if r["status"] == "present")
|
||||
absent = sum(1 for r in records if r["status"] == "absent")
|
||||
late = sum(1 for r in records if r["status"] == "late")
|
||||
leave = sum(1 for r in records if r["status"] == "leave")
|
||||
|
||||
return {
|
||||
"student_id": student_id,
|
||||
"student_name": student["name"],
|
||||
"statistics": {
|
||||
"present": present,
|
||||
"absent": absent,
|
||||
"late": late,
|
||||
"leave": leave,
|
||||
"total": len(records)
|
||||
},
|
||||
"records": records
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_ranking(
|
||||
user_id: int,
|
||||
class_id: Optional[int] = None,
|
||||
limit: int = 50
|
||||
) -> Dict[str, Any]:
|
||||
"""获取排行榜"""
|
||||
# 如果未指定班级,获取用户所在班级
|
||||
if not class_id:
|
||||
user = await StudentModel.get_by_id(user_id) if user_id else None
|
||||
if user:
|
||||
class_id = user["class_id"]
|
||||
else:
|
||||
admin_class = await PermissionChecker.get_user_class_id(user_id)
|
||||
if admin_class:
|
||||
class_id = admin_class
|
||||
|
||||
ranking = await StudentModel.get_ranking(class_id=class_id, limit=limit)
|
||||
|
||||
return {
|
||||
"class_id": class_id,
|
||||
"ranking": ranking
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def get_student_info(student_id: int) -> Optional[Dict[str, Any]]:
|
||||
"""获取学生个人信息"""
|
||||
return await StudentModel.get_by_id(student_id)
|
||||
77
backend/services/subject_service.py
Normal file
77
backend/services/subject_service.py
Normal file
@@ -0,0 +1,77 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Dict, Any, List, Optional
|
||||
|
||||
from models.subject import SubjectModel
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class SubjectService:
|
||||
"""科目服务"""
|
||||
|
||||
@staticmethod
|
||||
async def get_subjects(is_active: Optional[bool] = None) -> Dict[str, Any]:
|
||||
"""获取科目列表"""
|
||||
subjects = await SubjectModel.get_all(is_active=is_active)
|
||||
|
||||
return {
|
||||
"subjects": subjects,
|
||||
"total": len(subjects)
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
async def create_subject(
|
||||
subject_name: str,
|
||||
subject_code: Optional[str],
|
||||
sort_order: int = 0
|
||||
) -> Dict[str, Any]:
|
||||
"""创建科目"""
|
||||
# 检查是否已存在
|
||||
existing = await SubjectModel.get_by_name(subject_name)
|
||||
if existing:
|
||||
return {"success": False, "message": "科目名称已存在"}
|
||||
|
||||
subject_id = await SubjectModel.create(
|
||||
subject_name=subject_name,
|
||||
subject_code=subject_code,
|
||||
sort_order=sort_order
|
||||
)
|
||||
|
||||
if subject_id:
|
||||
logger.info(f"创建科目: {subject_name}")
|
||||
return {"success": True, "subject_id": subject_id}
|
||||
else:
|
||||
return {"success": False, "message": "创建科目失败"}
|
||||
|
||||
@staticmethod
|
||||
async def update_subject(subject_id: int, **kwargs) -> Dict[str, Any]:
|
||||
"""更新科目"""
|
||||
result = await SubjectModel.update(subject_id, **kwargs)
|
||||
|
||||
if result:
|
||||
logger.info(f"更新科目: {subject_id}")
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": "更新科目失败"}
|
||||
|
||||
@staticmethod
|
||||
async def delete_subject(subject_id: int) -> Dict[str, Any]:
|
||||
"""删除科目(软删除)"""
|
||||
result = await SubjectModel.delete(subject_id)
|
||||
|
||||
if result:
|
||||
logger.info(f"禁用科目: {subject_id}")
|
||||
return {"success": True}
|
||||
else:
|
||||
return {"success": False, "message": "禁用科目失败"}
|
||||
11
backend/utils/__init__.py
Normal file
11
backend/utils/__init__.py
Normal file
@@ -0,0 +1,11 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
136
backend/utils/database.py
Normal file
136
backend/utils/database.py
Normal file
@@ -0,0 +1,136 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import aiomysql
|
||||
from typing import Optional, Dict, Any, List
|
||||
from contextlib import asynccontextmanager
|
||||
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# 连接池实例
|
||||
_pool: Optional[aiomysql.Pool] = None
|
||||
|
||||
|
||||
async def init_db_pool() -> None:
|
||||
"""初始化数据库连接池"""
|
||||
global _pool
|
||||
try:
|
||||
_pool = await aiomysql.create_pool(
|
||||
host=settings.DB_HOST,
|
||||
port=settings.DB_PORT,
|
||||
user=settings.DB_USER,
|
||||
password=settings.DB_PASSWORD,
|
||||
db=settings.DB_NAME,
|
||||
minsize=1,
|
||||
maxsize=settings.DB_POOL_SIZE,
|
||||
maxsize=settings.DB_MAX_OVERFLOW,
|
||||
autocommit=False,
|
||||
charset='utf8mb4',
|
||||
cursorclass=aiomysql.DictCursor
|
||||
)
|
||||
logger.info("数据库连接池初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"数据库连接池初始化失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def close_db_pool() -> None:
|
||||
"""关闭数据库连接池"""
|
||||
global _pool
|
||||
if _pool:
|
||||
_pool.close()
|
||||
await _pool.wait_closed()
|
||||
logger.info("数据库连接池已关闭")
|
||||
|
||||
|
||||
def get_pool() -> aiomysql.Pool:
|
||||
"""获取连接池实例"""
|
||||
if _pool is None:
|
||||
raise RuntimeError("数据库连接池未初始化")
|
||||
return _pool
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_connection():
|
||||
"""获取数据库连接(上下文管理器)"""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
yield cursor
|
||||
await conn.commit()
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
async def get_transaction():
|
||||
"""获取事务连接"""
|
||||
pool = get_pool()
|
||||
async with pool.acquire() as conn:
|
||||
async with conn.cursor() as cursor:
|
||||
try:
|
||||
yield cursor
|
||||
await conn.commit()
|
||||
except Exception:
|
||||
await conn.rollback()
|
||||
raise
|
||||
|
||||
|
||||
async def execute_query(sql: str, params: tuple = None) -> List[Dict[str, Any]]:
|
||||
"""执行查询SQL"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.execute(sql, params)
|
||||
return await cursor.fetchall()
|
||||
|
||||
|
||||
async def execute_one(sql: str, params: tuple = None) -> Optional[Dict[str, Any]]:
|
||||
"""执行查询SQL(单条)"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.execute(sql, params)
|
||||
return await cursor.fetchone()
|
||||
|
||||
|
||||
async def execute_insert(sql: str, params: tuple = None) -> int:
|
||||
"""执行插入SQL,返回自增ID"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.execute(sql, params)
|
||||
return cursor.lastrowid
|
||||
|
||||
|
||||
async def execute_update(sql: str, params: tuple = None) -> int:
|
||||
"""执行更新SQL,返回影响行数"""
|
||||
async with get_connection() as cursor:
|
||||
result = await cursor.execute(sql, params)
|
||||
return result
|
||||
|
||||
|
||||
async def execute_many(sql: str, params_list: list) -> int:
|
||||
"""批量执行SQL"""
|
||||
async with get_connection() as cursor:
|
||||
await cursor.executemany(sql, params_list)
|
||||
return cursor.rowcount
|
||||
|
||||
|
||||
async def call_procedure(proc_name: str, args: tuple = None) -> List[Dict[str, Any]]:
|
||||
"""调用存储过程"""
|
||||
async with get_connection() as cursor:
|
||||
if args:
|
||||
await cursor.callproc(proc_name, args)
|
||||
else:
|
||||
await cursor.callproc(proc_name)
|
||||
|
||||
# 获取结果
|
||||
result = []
|
||||
for result_set in cursor.fetchall():
|
||||
if result_set:
|
||||
result.extend(result_set)
|
||||
return result
|
||||
86
backend/utils/jwt_handler.py
Normal file
86
backend/utils/jwt_handler.py
Normal file
@@ -0,0 +1,86 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from jose import jwt, JWTError
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Dict, Any
|
||||
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
|
||||
class JWTHandler:
|
||||
"""JWT Token处理类"""
|
||||
|
||||
@staticmethod
|
||||
def create_token(user_id: int, username: str, user_type: str, student_id: int = None, role: str = None) -> str:
|
||||
"""
|
||||
创建JWT Token
|
||||
"""
|
||||
payload = {
|
||||
'user_id': user_id,
|
||||
'username': username,
|
||||
'user_type': user_type,
|
||||
'student_id': student_id,
|
||||
'role': role,
|
||||
'exp': datetime.utcnow() + timedelta(minutes=settings.JWT_EXPIRE_MINUTES),
|
||||
'iat': datetime.utcnow(),
|
||||
'iss': settings.APP_NAME
|
||||
}
|
||||
|
||||
token = jwt.encode(
|
||||
payload,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithm=settings.JWT_ALGORITHM
|
||||
)
|
||||
return token
|
||||
|
||||
@staticmethod
|
||||
def verify_token(token: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
验证JWT Token
|
||||
返回: 解码后的payload,失败返回None
|
||||
"""
|
||||
try:
|
||||
payload = jwt.decode(
|
||||
token,
|
||||
settings.JWT_SECRET_KEY,
|
||||
algorithms=[settings.JWT_ALGORITHM],
|
||||
options={'verify_exp': True}
|
||||
)
|
||||
return payload
|
||||
except jwt.ExpiredSignatureError:
|
||||
logger.warning("JWT Token已过期")
|
||||
return None
|
||||
except jwt.JWTError as e:
|
||||
logger.warning(f"JWT Token验证失败: {e}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_user_id_from_token(token: str) -> Optional[int]:
|
||||
"""从Token中获取用户ID"""
|
||||
payload = JWTHandler.verify_token(token)
|
||||
if payload:
|
||||
return payload.get('user_id')
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def get_user_type_from_token(token: str) -> Optional[str]:
|
||||
"""从Token中获取用户类型"""
|
||||
payload = JWTHandler.verify_token(token)
|
||||
if payload:
|
||||
return payload.get('user_type')
|
||||
return None
|
||||
|
||||
|
||||
jwt_handler = JWTHandler()
|
||||
102
backend/utils/logger.py
Normal file
102
backend/utils/logger.py
Normal file
@@ -0,0 +1,102 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
from fastapi import Request
|
||||
|
||||
from config import settings
|
||||
|
||||
|
||||
# 日志目录
|
||||
LOG_DIR = Path(__file__).parent.parent / "logs"
|
||||
LOG_DIR.mkdir(exist_ok=True)
|
||||
|
||||
|
||||
def setup_logger():
|
||||
"""配置日志系统"""
|
||||
|
||||
# 移除默认处理器
|
||||
logger.remove()
|
||||
|
||||
# 控制台输出(仅INFO及以上)
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan> - <level>{message}</level>",
|
||||
level=settings.LOG_LEVEL,
|
||||
colorize=True
|
||||
)
|
||||
|
||||
# 应用日志(轮转)
|
||||
logger.add(
|
||||
LOG_DIR / "app.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name} | {message}",
|
||||
rotation=settings.LOG_MAX_BYTES,
|
||||
retention=settings.LOG_RETENTION_DAYS,
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
level="DEBUG"
|
||||
)
|
||||
|
||||
# 错误日志(单独记录)
|
||||
logger.add(
|
||||
LOG_DIR / "error.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name} | {message}",
|
||||
rotation=settings.LOG_MAX_BYTES,
|
||||
retention=settings.LOG_RETENTION_DAYS * 2,
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
level="ERROR"
|
||||
)
|
||||
|
||||
# 访问日志
|
||||
logger.add(
|
||||
LOG_DIR / "access.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {message}",
|
||||
rotation="1 day",
|
||||
retention="90 days",
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
filter=lambda record: record["extra"].get("type") == "access"
|
||||
)
|
||||
|
||||
# 操作日志
|
||||
logger.add(
|
||||
LOG_DIR / "operation.log",
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {message}",
|
||||
rotation=settings.LOG_MAX_BYTES,
|
||||
retention=settings.LOG_RETENTION_DAYS,
|
||||
compression="gz",
|
||||
encoding="utf-8",
|
||||
filter=lambda record: record["extra"].get("type") == "operation"
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
def get_logger(name: str):
|
||||
"""获取日志记录器"""
|
||||
return logger.bind(name=name)
|
||||
|
||||
|
||||
def log_access(request: Request):
|
||||
"""记录访问日志"""
|
||||
logger.bind(type="access").info(f"{request.method} {request.url.path} - {request.client.host}")
|
||||
|
||||
|
||||
def log_operation(operator_id: int, operator_name: str, action: str, details: str = ""):
|
||||
"""记录操作日志"""
|
||||
logger.bind(type="operation").info(f"用户[{operator_id}:{operator_name}] 执行 {action} - {details}")
|
||||
|
||||
|
||||
# 导出logger
|
||||
__all__ = ["setup_logger", "get_logger", "log_access", "log_operation", "logger"]
|
||||
140
backend/utils/redus_client.py
Normal file
140
backend/utils/redus_client.py
Normal file
@@ -0,0 +1,140 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import redis.asyncio as redis
|
||||
from typing import Optional, Any
|
||||
import json
|
||||
|
||||
from config import settings
|
||||
from utils.logger import get_logger
|
||||
|
||||
logger = get_logger(__name__)
|
||||
|
||||
# Redis客户端实例
|
||||
_redis_client: Optional[redis.Redis] = None
|
||||
|
||||
|
||||
async def init_redis_pool() -> None:
|
||||
"""初始化Redis连接池"""
|
||||
global _redis_client
|
||||
try:
|
||||
_redis_client = redis.from_url(
|
||||
settings.REDIS_URL,
|
||||
max_connections=settings.REDIS_MAX_CONNECTIONS,
|
||||
decode_responses=True
|
||||
)
|
||||
# 测试连接
|
||||
await _redis_client.ping()
|
||||
logger.info("Redis连接池初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"Redis连接池初始化失败: {e}")
|
||||
raise
|
||||
|
||||
|
||||
async def close_redis_pool() -> None:
|
||||
"""关闭Redis连接池"""
|
||||
global _redis_client
|
||||
if _redis_client:
|
||||
await _redis_client.close()
|
||||
logger.info("Redis连接池已关闭")
|
||||
|
||||
|
||||
def get_redis() -> redis.Redis:
|
||||
"""获取Redis客户端"""
|
||||
if _redis_client is None:
|
||||
raise RuntimeError("Redis客户端未初始化")
|
||||
return _redis_client
|
||||
|
||||
|
||||
class RedisClient:
|
||||
"""Redis操作封装类"""
|
||||
|
||||
@staticmethod
|
||||
async def set(key: str, value: Any, expire: int = None) -> bool:
|
||||
"""设置缓存"""
|
||||
client = get_redis()
|
||||
if isinstance(value, (dict, list)):
|
||||
value = json.dumps(value, ensure_ascii=False)
|
||||
else:
|
||||
value = str(value)
|
||||
|
||||
if expire:
|
||||
return await client.setex(key, expire, value)
|
||||
return await client.set(key, value)
|
||||
|
||||
@staticmethod
|
||||
async def get(key: str) -> Optional[str]:
|
||||
"""获取缓存"""
|
||||
client = get_redis()
|
||||
return await client.get(key)
|
||||
|
||||
@staticmethod
|
||||
async def get_json(key: str) -> Optional[Any]:
|
||||
"""获取JSON格式缓存"""
|
||||
value = await RedisClient.get(key)
|
||||
if value:
|
||||
try:
|
||||
return json.loads(value)
|
||||
except json.JSONDecodeError:
|
||||
return value
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
async def delete(key: str) -> int:
|
||||
"""删除缓存"""
|
||||
client = get_redis()
|
||||
return await client.delete(key)
|
||||
|
||||
@staticmethod
|
||||
async def exists(key: str) -> bool:
|
||||
"""检查key是否存在"""
|
||||
client = get_redis()
|
||||
return await client.exists(key) > 0
|
||||
|
||||
@staticmethod
|
||||
async def expire(key: str, seconds: int) -> bool:
|
||||
"""设置过期时间"""
|
||||
client = get_redis()
|
||||
return await client.expire(key, seconds)
|
||||
|
||||
@staticmethod
|
||||
async def set_user_token(user_id: int, token: str, expire: int = None) -> bool:
|
||||
"""设置用户Token缓存"""
|
||||
key = f"user_token:{user_id}"
|
||||
expire = expire or settings.JWT_EXPIRE_MINUTES * 60
|
||||
return await RedisClient.set(key, token, expire)
|
||||
|
||||
@staticmethod
|
||||
async def get_user_token(user_id: int) -> Optional[str]:
|
||||
"""获取用户Token"""
|
||||
key = f"user_token:{user_id}"
|
||||
return await RedisClient.get(key)
|
||||
|
||||
@staticmethod
|
||||
async def delete_user_token(user_id: int) -> int:
|
||||
"""删除用户Token"""
|
||||
key = f"user_token:{user_id}"
|
||||
return await RedisClient.delete(key)
|
||||
|
||||
@staticmethod
|
||||
async def set_login_attempts(username: str) -> int:
|
||||
"""记录登录失败次数"""
|
||||
key = f"login_attempts:{username}"
|
||||
attempts = await RedisClient.get(key)
|
||||
attempts = int(attempts) + 1 if attempts else 1
|
||||
await RedisClient.set(key, attempts, 900) # 15分钟锁定
|
||||
return attempts
|
||||
|
||||
@staticmethod
|
||||
async def clear_login_attempts(username: str) -> None:
|
||||
"""清除登录失败记录"""
|
||||
key = f"login_attempts:{username}"
|
||||
await RedisClient.delete(key)
|
||||
106
backend/utils/response.py
Normal file
106
backend/utils/response.py
Normal file
@@ -0,0 +1,106 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
from typing import Any, Optional, Dict, List
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
|
||||
class ResponseCode:
|
||||
"""响应状态码"""
|
||||
SUCCESS = 200
|
||||
CREATED = 201
|
||||
BAD_REQUEST = 400
|
||||
UNAUTHORIZED = 401
|
||||
FORBIDDEN = 403
|
||||
NOT_FOUND = 404
|
||||
CONFLICT = 409
|
||||
UNPROCESSABLE = 422
|
||||
INTERNAL_ERROR = 500
|
||||
|
||||
|
||||
def success_response(data: Any = None, message: str = "操作成功") -> JSONResponse:
|
||||
"""成功响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.SUCCESS,
|
||||
content={
|
||||
"success": True,
|
||||
"code": ResponseCode.SUCCESS,
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def error_response(
|
||||
message: str = "操作失败",
|
||||
code: int = ResponseCode.BAD_REQUEST,
|
||||
data: Any = None
|
||||
) -> JSONResponse:
|
||||
"""错误响应"""
|
||||
return JSONResponse(
|
||||
status_code=code,
|
||||
content={
|
||||
"success": False,
|
||||
"code": code,
|
||||
"message": message,
|
||||
"data": data
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def unauthorized_response(message: str = "未授权,请重新登录") -> JSONResponse:
|
||||
"""未授权响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.UNAUTHORIZED,
|
||||
content={
|
||||
"success": False,
|
||||
"code": ResponseCode.UNAUTHORIZED,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def forbidden_response(message: str = "权限不足") -> JSONResponse:
|
||||
"""禁止访问响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.FORBIDDEN,
|
||||
content={
|
||||
"success": False,
|
||||
"code": ResponseCode.FORBIDDEN,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def not_found_response(message: str = "资源不存在") -> JSONResponse:
|
||||
"""资源不存在响应"""
|
||||
return JSONResponse(
|
||||
status_code=ResponseCode.NOT_FOUND,
|
||||
content={
|
||||
"success": False,
|
||||
"code": ResponseCode.NOT_FOUND,
|
||||
"message": message,
|
||||
"data": None
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def paginated_response(items: List[Any], total: int, page: int, page_size: int) -> Dict:
|
||||
"""分页响应数据"""
|
||||
return {
|
||||
"items": items,
|
||||
"total": total,
|
||||
"page": page,
|
||||
"page_size": page_size,
|
||||
"total_pages": (total + page_size - 1) // page_size
|
||||
}
|
||||
131
backend/utils/security.py
Normal file
131
backend/utils/security.py
Normal file
@@ -0,0 +1,131 @@
|
||||
# ===========================================
|
||||
# 班级操行分管理系统 - 后端服务
|
||||
#
|
||||
# 开发者: Canglan
|
||||
# 联系方式: admin@sea-studio.top
|
||||
# 版权归属: Sea Network Technology Studio
|
||||
# 许可证: MIT License
|
||||
#
|
||||
# 版权所有 © Sea Network Technology Studio
|
||||
# ===========================================
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import re
|
||||
from config import settings
|
||||
|
||||
|
||||
class SecurityUtils:
|
||||
"""安全工具类"""
|
||||
|
||||
@staticmethod
|
||||
def sha1_md5_password(password: str) -> str:
|
||||
"""
|
||||
双重加密:sha1 + md5
|
||||
流程:原始密码 -> sha1 -> 加盐 -> md5
|
||||
"""
|
||||
# 第一层:SHA1
|
||||
sha1_hash = hashlib.sha1(password.encode('utf-8')).hexdigest()
|
||||
# 加盐
|
||||
salted = sha1_hash + settings.PASSWORD_SALT
|
||||
# 第二层:MD5
|
||||
md5_hash = hashlib.md5(salted.encode('utf-8')).hexdigest()
|
||||
return md5_hash
|
||||
|
||||
@staticmethod
|
||||
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
||||
"""验证密码"""
|
||||
return SecurityUtils.sha1_md5_password(plain_password) == hashed_password
|
||||
|
||||
@staticmethod
|
||||
def generate_random_password(length: int = 8) -> str:
|
||||
"""生成随机密码"""
|
||||
alphabet = 'abcdefghijkmnpqrstuvwxyzABCDEFGHJKLMNPQRSTUVWXYZ23456789'
|
||||
return ''.join(secrets.choice(alphabet) for _ in range(length))
|
||||
|
||||
@staticmethod
|
||||
def validate_password_strength(password: str) -> tuple:
|
||||
"""
|
||||
验证密码强度
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if len(password) < 6:
|
||||
return False, "密码长度至少6位"
|
||||
if len(password) > 20:
|
||||
return False, "密码长度不能超过20位"
|
||||
|
||||
# 检查是否包含至少一个数字
|
||||
if not any(c.isdigit() for c in password):
|
||||
return False, "密码必须包含至少一个数字"
|
||||
|
||||
# 检查是否包含至少一个字母
|
||||
if not any(c.isalpha() for c in password):
|
||||
return False, "密码必须包含至少一个字母"
|
||||
|
||||
return True, ""
|
||||
|
||||
@staticmethod
|
||||
def sanitize_string(value: str, max_length: int = 255) -> str:
|
||||
"""
|
||||
清理字符串输入
|
||||
- 去除首尾空格
|
||||
- 限制长度
|
||||
- 转义特殊字符
|
||||
"""
|
||||
if not value:
|
||||
return ""
|
||||
|
||||
# 去除首尾空格
|
||||
value = value.strip()
|
||||
|
||||
# 限制长度
|
||||
if len(value) > max_length:
|
||||
value = value[:max_length]
|
||||
|
||||
# 转义HTML特殊字符(防止XSS)
|
||||
html_chars = {
|
||||
'&': '&',
|
||||
'<': '<',
|
||||
'>': '>',
|
||||
'"': '"',
|
||||
"'": ''',
|
||||
'/': '/'
|
||||
}
|
||||
for char, escape in html_chars.items():
|
||||
value = value.replace(char, escape)
|
||||
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def validate_student_no(student_no: str) -> bool:
|
||||
"""验证学号格式(数字+字母,长度4-20)"""
|
||||
if not student_no:
|
||||
return False
|
||||
if len(student_no) < 4 or len(student_no) > 20:
|
||||
return False
|
||||
# 字母数字组合
|
||||
return student_no.isalnum()
|
||||
|
||||
@staticmethod
|
||||
def validate_phone(phone: str) -> bool:
|
||||
"""验证手机号格式(中国手机号)"""
|
||||
if not phone:
|
||||
return False
|
||||
pattern = r'^1[3-9]\d{9}$'
|
||||
return bool(re.match(pattern, phone))
|
||||
|
||||
@staticmethod
|
||||
def validate_points_change(points: int, max_abs: int = 100) -> tuple:
|
||||
"""
|
||||
验证分值变动
|
||||
返回: (是否有效, 错误信息)
|
||||
"""
|
||||
if points == 0:
|
||||
return False, "分值不能为0"
|
||||
if abs(points) > max_abs:
|
||||
return f"单次分值变动不能超过{max_abs}分"
|
||||
return True, ""
|
||||
|
||||
|
||||
# 单例导出
|
||||
security = SecurityUtils()
|
||||
Reference in New Issue
Block a user