v0.1测试
This commit is contained in:
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