refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
"""
|
||||
工具模块
|
||||
"""
|
||||
|
||||
from utils.date_helper import DateHelper
|
||||
from utils.string_helper import StringHelper
|
||||
from utils.validator import Validator
|
||||
|
||||
__all__ = ['DateHelper', 'StringHelper', 'Validator']
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
断言工具
|
||||
"""
|
||||
|
||||
from typing import Any, Dict, List
|
||||
from httpx import Response
|
||||
|
||||
|
||||
class Assertions:
|
||||
"""断言工具类"""
|
||||
|
||||
@staticmethod
|
||||
def assert_status_code(response: Response, expected_status: int):
|
||||
"""断言状态码"""
|
||||
assert response.status_code == expected_status, \
|
||||
f"Expected status code {expected_status}, got {response.status_code}. Response: {response.text}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_contains(response: Response, key: str, value: Any = None):
|
||||
"""断言响应包含指定字段"""
|
||||
data = response.json()
|
||||
assert key in data, f"Response does not contain key '{key}'. Response: {data}"
|
||||
if value is not None:
|
||||
assert data[key] == value, \
|
||||
f"Expected {value} for key '{key}', got {data[key]}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_is_list(response: Response):
|
||||
"""断言响应是列表"""
|
||||
data = response.json()
|
||||
assert isinstance(data, list), f"Expected list, got {type(data)}. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_not_empty(response: Response):
|
||||
"""断言响应不为空"""
|
||||
data = response.json()
|
||||
assert data, f"Response is empty. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_field_type(response: Response, field: str, expected_type: type):
|
||||
"""断言响应字段类型"""
|
||||
data = response.json()
|
||||
assert field in data, f"Response does not contain field '{field}'"
|
||||
assert isinstance(data[field], expected_type), \
|
||||
f"Expected field '{field}' to be {expected_type}, got {type(data[field])}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_fields_present(response: Response, fields: List[str]):
|
||||
"""断言响应包含所有指定字段"""
|
||||
data = response.json()
|
||||
missing_fields = [field for field in fields if field not in data]
|
||||
assert not missing_fields, \
|
||||
f"Response is missing fields: {missing_fields}. Response: {data}"
|
||||
|
||||
@staticmethod
|
||||
def assert_response_field_length(response: Response, field: str, min_length: int = None, max_length: int = None):
|
||||
"""断言响应字段长度"""
|
||||
data = response.json()
|
||||
assert field in data, f"Response does not contain field '{field}'"
|
||||
field_value = data[field]
|
||||
|
||||
if isinstance(field_value, (str, list, dict)):
|
||||
length = len(field_value)
|
||||
if min_length is not None:
|
||||
assert length >= min_length, \
|
||||
f"Field '{field}' length {length} is less than minimum {min_length}"
|
||||
if max_length is not None:
|
||||
assert length <= max_length, \
|
||||
f"Field '{field}' length {length} is greater than maximum {max_length}"
|
||||
else:
|
||||
raise AssertionError(f"Field '{field}' is not a string, list, or dict")
|
||||
|
||||
@staticmethod
|
||||
def assert_error_response(response: Response, expected_message: str = None):
|
||||
"""断言错误响应"""
|
||||
Assertions.assert_status_code(response, 400)
|
||||
if expected_message:
|
||||
data = response.json()
|
||||
assert expected_message in str(data), \
|
||||
f"Expected error message '{expected_message}' not found in response: {data}"
|
||||
|
||||
|
||||
assertions = Assertions()
|
||||
@@ -0,0 +1,72 @@
|
||||
"""
|
||||
测试数据生成器
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
from faker import Faker
|
||||
|
||||
|
||||
class DataGenerator:
|
||||
"""测试数据生成器"""
|
||||
|
||||
def __init__(self, locale: str = "zh_CN"):
|
||||
self.faker = Faker(locale)
|
||||
|
||||
def generate_username(self) -> str:
|
||||
"""生成用户名"""
|
||||
return f"testuser_{''.join(random.choices(string.ascii_lowercase + string.digits, k=8))}"
|
||||
|
||||
def generate_password(self, length: int = 12) -> str:
|
||||
"""生成密码"""
|
||||
chars = string.ascii_letters + string.digits + "!@#$%^&*"
|
||||
return ''.join(random.choices(chars, k=length))
|
||||
|
||||
def generate_email(self) -> str:
|
||||
"""生成邮箱"""
|
||||
return self.faker.email()
|
||||
|
||||
def generate_phone(self) -> str:
|
||||
"""生成手机号"""
|
||||
return self.faker.phone_number()
|
||||
|
||||
def generate_name(self) -> str:
|
||||
"""生成姓名"""
|
||||
return self.faker.name()
|
||||
|
||||
def generate_role_name(self) -> str:
|
||||
"""生成角色名"""
|
||||
return f"ROLE_{''.join(random.choices(string.ascii_uppercase, k=6))}"
|
||||
|
||||
def generate_dict_type(self) -> str:
|
||||
"""生成字典类型"""
|
||||
return f"DICT_TYPE_{''.join(random.choices(string.ascii_uppercase, k=4))}"
|
||||
|
||||
def generate_dict_code(self) -> str:
|
||||
"""生成字典编码"""
|
||||
return f"CODE_{''.join(random.choices(string.ascii_uppercase + string.digits, k=6))}"
|
||||
|
||||
def generate_url(self) -> str:
|
||||
"""生成URL"""
|
||||
return self.faker.url()
|
||||
|
||||
def generate_company_name(self) -> str:
|
||||
"""生成公司名"""
|
||||
return self.faker.company()
|
||||
|
||||
def generate_address(self) -> str:
|
||||
"""生成地址"""
|
||||
return self.faker.address()
|
||||
|
||||
def generate_description(self) -> str:
|
||||
"""生成描述"""
|
||||
return self.faker.text(max_nb_chars=200)
|
||||
|
||||
def generate_permissions(self) -> str:
|
||||
"""生成权限字符串"""
|
||||
permissions = ["READ", "WRITE", "DELETE", "ADMIN", "MANAGE"]
|
||||
selected = random.sample(permissions, random.randint(1, len(permissions)))
|
||||
return ",".join(selected)
|
||||
|
||||
|
||||
data_generator = DataGenerator()
|
||||
@@ -0,0 +1,115 @@
|
||||
"""
|
||||
日期时间工具类
|
||||
|
||||
提供日期时间相关的工具方法
|
||||
|
||||
作者: 张翔
|
||||
日期: 2026-04-01
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional
|
||||
import pytz
|
||||
|
||||
|
||||
class DateHelper:
|
||||
"""日期时间工具类"""
|
||||
|
||||
@staticmethod
|
||||
def format_datetime(dt: datetime, fmt: str = "%Y-%m-%d %H:%M:%S") -> str:
|
||||
"""
|
||||
格式化日期时间
|
||||
|
||||
Args:
|
||||
dt: datetime对象
|
||||
fmt: 格式字符串
|
||||
|
||||
Returns:
|
||||
格式化后的字符串
|
||||
"""
|
||||
if dt is None:
|
||||
return ""
|
||||
return dt.strftime(fmt)
|
||||
|
||||
@staticmethod
|
||||
def parse_datetime(date_string: str, fmt: str = "%Y-%m-%d %H:%M:%S") -> Optional[datetime]:
|
||||
"""
|
||||
解析日期时间字符串
|
||||
|
||||
Args:
|
||||
date_string: 日期时间字符串
|
||||
fmt: 格式字符串
|
||||
|
||||
Returns:
|
||||
datetime对象,解析失败返回None
|
||||
"""
|
||||
try:
|
||||
return datetime.strptime(date_string, fmt)
|
||||
except (ValueError, TypeError):
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def days_between(start_date: datetime, end_date: datetime) -> int:
|
||||
"""
|
||||
计算两个日期之间的天数
|
||||
|
||||
Args:
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
|
||||
Returns:
|
||||
天数差
|
||||
"""
|
||||
if start_date is None or end_date is None:
|
||||
return 0
|
||||
delta = end_date - start_date
|
||||
return delta.days
|
||||
|
||||
@staticmethod
|
||||
def utc_to_local(utc_time: datetime, timezone: str = "Asia/Shanghai") -> datetime:
|
||||
"""
|
||||
UTC时间转本地时间
|
||||
|
||||
Args:
|
||||
utc_time: UTC时间
|
||||
timezone: 时区名称
|
||||
|
||||
Returns:
|
||||
本地时间
|
||||
"""
|
||||
if utc_time is None:
|
||||
return None
|
||||
|
||||
utc_tz = pytz.UTC
|
||||
local_tz = pytz.timezone(timezone)
|
||||
|
||||
if utc_time.tzinfo is None:
|
||||
utc_time = utc_tz.localize(utc_time)
|
||||
|
||||
return utc_time.astimezone(local_tz)
|
||||
|
||||
@staticmethod
|
||||
def get_current_timestamp() -> int:
|
||||
"""
|
||||
获取当前时间戳(秒)
|
||||
|
||||
Returns:
|
||||
当前时间戳
|
||||
"""
|
||||
return int(datetime.now().timestamp())
|
||||
|
||||
@staticmethod
|
||||
def add_days(dt: datetime, days: int) -> datetime:
|
||||
"""
|
||||
日期加减天数
|
||||
|
||||
Args:
|
||||
dt: 日期
|
||||
days: 天数(可为负数)
|
||||
|
||||
Returns:
|
||||
新日期
|
||||
"""
|
||||
if dt is None:
|
||||
return None
|
||||
return dt + timedelta(days=days)
|
||||
@@ -0,0 +1,33 @@
|
||||
"""
|
||||
日志工具
|
||||
"""
|
||||
|
||||
import sys
|
||||
from loguru import logger
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
def setup_logger(log_file: str = "e2e_tests.log", log_level: str = "INFO"):
|
||||
"""配置日志"""
|
||||
logger.remove()
|
||||
|
||||
logger.add(
|
||||
sys.stdout,
|
||||
format="<green>{time:YYYY-MM-DD HH:mm:ss}</green> | <level>{level: <8}</level> | <cyan>{name}</cyan>:<cyan>{function}</cyan>:<cyan>{line}</cyan> - <level>{message}</level>",
|
||||
level=log_level,
|
||||
colorize=True
|
||||
)
|
||||
|
||||
logger.add(
|
||||
log_file,
|
||||
format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}",
|
||||
level=log_level,
|
||||
rotation="10 MB",
|
||||
retention="7 days",
|
||||
compression="zip"
|
||||
)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
setup_logger()
|
||||
@@ -0,0 +1,173 @@
|
||||
"""
|
||||
字符串处理工具类
|
||||
|
||||
提供字符串相关的工具方法
|
||||
|
||||
作者: 张翔
|
||||
日期: 2026-04-01
|
||||
"""
|
||||
|
||||
import random
|
||||
import string
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class StringHelper:
|
||||
"""字符串处理工具类"""
|
||||
|
||||
@staticmethod
|
||||
def truncate(text: str, max_length: int = 100, suffix: str = "...") -> str:
|
||||
"""
|
||||
截断字符串
|
||||
|
||||
Args:
|
||||
text: 原始字符串
|
||||
max_length: 最大长度
|
||||
suffix: 后缀
|
||||
|
||||
Returns:
|
||||
截断后的字符串
|
||||
"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
if len(text) <= max_length:
|
||||
return text
|
||||
|
||||
return text[:max_length] + suffix
|
||||
|
||||
@staticmethod
|
||||
def mask_phone(phone: str) -> str:
|
||||
"""
|
||||
手机号脱敏
|
||||
|
||||
Args:
|
||||
phone: 手机号
|
||||
|
||||
Returns:
|
||||
脱敏后的手机号
|
||||
"""
|
||||
if not phone or len(phone) < 7:
|
||||
return phone
|
||||
|
||||
return phone[:3] + "****" + phone[-4:]
|
||||
|
||||
@staticmethod
|
||||
def mask_email(email: str) -> str:
|
||||
"""
|
||||
邮箱脱敏
|
||||
|
||||
Args:
|
||||
email: 邮箱
|
||||
|
||||
Returns:
|
||||
脱敏后的邮箱
|
||||
"""
|
||||
if not email or "@" not in email:
|
||||
return email
|
||||
|
||||
parts = email.split("@")
|
||||
username = parts[0]
|
||||
domain = parts[1]
|
||||
|
||||
if len(username) <= 2:
|
||||
masked_username = username[0] + "***"
|
||||
else:
|
||||
masked_username = username[:2] + "***"
|
||||
|
||||
return f"{masked_username}@{domain}"
|
||||
|
||||
@staticmethod
|
||||
def mask_id_card(id_card: str) -> str:
|
||||
"""
|
||||
身份证号脱敏
|
||||
|
||||
Args:
|
||||
id_card: 身份证号
|
||||
|
||||
Returns:
|
||||
脱敏后的身份证号
|
||||
"""
|
||||
if not id_card or len(id_card) < 8:
|
||||
return id_card
|
||||
|
||||
return id_card[:4] + "**********" + id_card[-4:]
|
||||
|
||||
@staticmethod
|
||||
def random_string(length: int = 16, chars: str = None) -> str:
|
||||
"""
|
||||
生成随机字符串
|
||||
|
||||
Args:
|
||||
length: 长度
|
||||
chars: 字符集,默认为字母数字
|
||||
|
||||
Returns:
|
||||
随机字符串
|
||||
"""
|
||||
if chars is None:
|
||||
chars = string.ascii_letters + string.digits
|
||||
|
||||
return ''.join(random.choice(chars) for _ in range(length))
|
||||
|
||||
@staticmethod
|
||||
def camel_to_snake(name: str) -> str:
|
||||
"""
|
||||
驼峰命名转下划线命名
|
||||
|
||||
Args:
|
||||
name: 驼峰命名字符串
|
||||
|
||||
Returns:
|
||||
下划线命名字符串
|
||||
"""
|
||||
if not name:
|
||||
return ""
|
||||
|
||||
s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name)
|
||||
return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower()
|
||||
|
||||
@staticmethod
|
||||
def snake_to_camel(name: str) -> str:
|
||||
"""
|
||||
下划线命名转驼峰命名
|
||||
|
||||
Args:
|
||||
name: 下划线命名字符串
|
||||
|
||||
Returns:
|
||||
驼峰命名字符串
|
||||
"""
|
||||
if not name:
|
||||
return ""
|
||||
|
||||
components = name.split('_')
|
||||
return components[0] + ''.join(x.title() for x in components[1:])
|
||||
|
||||
@staticmethod
|
||||
def is_empty(text: Optional[str]) -> bool:
|
||||
"""
|
||||
判断字符串是否为空
|
||||
|
||||
Args:
|
||||
text: 字符串
|
||||
|
||||
Returns:
|
||||
是否为空
|
||||
"""
|
||||
return text is None or text.strip() == ""
|
||||
|
||||
@staticmethod
|
||||
def default_if_empty(text: Optional[str], default: str) -> str:
|
||||
"""
|
||||
如果字符串为空则返回默认值
|
||||
|
||||
Args:
|
||||
text: 字符串
|
||||
default: 默认值
|
||||
|
||||
Returns:
|
||||
字符串或默认值
|
||||
"""
|
||||
return text if not StringHelper.is_empty(text) else default
|
||||
@@ -0,0 +1,204 @@
|
||||
"""
|
||||
测试数据管理工具(简化版)
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
from typing import List, Dict, Any, Callable
|
||||
from httpx import AsyncClient
|
||||
from loguru import logger
|
||||
|
||||
|
||||
class TestDataManager:
|
||||
"""测试数据管理器"""
|
||||
|
||||
def __init__(self, client: AsyncClient):
|
||||
self.client = client
|
||||
self._users: List[int] = []
|
||||
self._roles: List[int] = []
|
||||
self._menus: List[int] = []
|
||||
self._dictionaries: List[int] = []
|
||||
self._dict_types: List[int] = []
|
||||
self._configs: List[int] = []
|
||||
self._notices: List[int] = []
|
||||
self._files: List[int] = []
|
||||
self._messages: List[int] = []
|
||||
|
||||
def add_user(self, user_id: int):
|
||||
"""添加用户到清理列表"""
|
||||
self._users.append(user_id)
|
||||
|
||||
def add_role(self, role_id: int):
|
||||
"""添加角色到清理列表"""
|
||||
self._roles.append(role_id)
|
||||
|
||||
def add_menu(self, menu_id: int):
|
||||
"""添加菜单到清理列表"""
|
||||
self._menus.append(menu_id)
|
||||
|
||||
def add_dictionary(self, dict_id: int):
|
||||
"""添加字典到清理列表"""
|
||||
self._dictionaries.append(dict_id)
|
||||
|
||||
def add_dict_type(self, dict_type_id: int):
|
||||
"""添加字典类型到清理列表"""
|
||||
self._dict_types.append(dict_type_id)
|
||||
|
||||
def add_config(self, config_id: int):
|
||||
"""添加系统配置到清理列表"""
|
||||
self._configs.append(config_id)
|
||||
|
||||
def add_notice(self, notice_id: int):
|
||||
"""添加系统公告到清理列表"""
|
||||
self._notices.append(notice_id)
|
||||
|
||||
def add_file(self, file_id: int):
|
||||
"""添加文件到清理列表"""
|
||||
self._files.append(file_id)
|
||||
|
||||
def add_message(self, message_id: int):
|
||||
"""添加消息到清理列表"""
|
||||
self._messages.append(message_id)
|
||||
|
||||
async def cleanup_all(self):
|
||||
"""清理所有测试数据"""
|
||||
logger.info("Starting test data cleanup...")
|
||||
|
||||
cleanup_tasks = []
|
||||
|
||||
if self._messages:
|
||||
cleanup_tasks.extend([self._delete_message(msg_id) for msg_id in self._messages])
|
||||
self._messages.clear()
|
||||
|
||||
if self._files:
|
||||
cleanup_tasks.extend([self._delete_file(file_id) for file_id in self._files])
|
||||
self._files.clear()
|
||||
|
||||
if self._notices:
|
||||
cleanup_tasks.extend([self._delete_notice(notice_id) for notice_id in self._notices])
|
||||
self._notices.clear()
|
||||
|
||||
if self._configs:
|
||||
cleanup_tasks.extend([self._delete_config(config_id) for config_id in self._configs])
|
||||
self._configs.clear()
|
||||
|
||||
if self._dictionaries:
|
||||
cleanup_tasks.extend([self._delete_dictionary(dict_id) for dict_id in self._dictionaries])
|
||||
self._dictionaries.clear()
|
||||
|
||||
if self._dict_types:
|
||||
cleanup_tasks.extend([self._delete_dict_type(dict_type_id) for dict_type_id in self._dict_types])
|
||||
self._dict_types.clear()
|
||||
|
||||
if self._users:
|
||||
cleanup_tasks.extend([self._delete_user(user_id) for user_id in self._users])
|
||||
self._users.clear()
|
||||
|
||||
if self._roles:
|
||||
cleanup_tasks.extend([self._delete_role(role_id) for role_id in self._roles])
|
||||
self._roles.clear()
|
||||
|
||||
if self._menus:
|
||||
cleanup_tasks.extend([self._delete_menu(menu_id) for menu_id in self._menus])
|
||||
self._menus.clear()
|
||||
|
||||
if cleanup_tasks:
|
||||
results = await asyncio.gather(*cleanup_tasks, return_exceptions=True)
|
||||
failed_count = sum(1 for r in results if isinstance(r, Exception))
|
||||
if failed_count > 0:
|
||||
logger.warning(f"Failed to cleanup {failed_count} resources")
|
||||
|
||||
logger.info("Test data cleanup completed")
|
||||
|
||||
async def _delete_user(self, user_id: int):
|
||||
"""删除用户"""
|
||||
try:
|
||||
await self.client.delete(f"/api/users/{user_id}")
|
||||
logger.info(f"Cleaned up user {user_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup user {user_id}: {e}")
|
||||
|
||||
async def _delete_role(self, role_id: int):
|
||||
"""删除角色"""
|
||||
try:
|
||||
await self.client.delete(f"/api/roles/{role_id}")
|
||||
logger.info(f"Cleaned up role {role_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup role {role_id}: {e}")
|
||||
|
||||
async def _delete_menu(self, menu_id: int):
|
||||
"""删除菜单"""
|
||||
try:
|
||||
await self.client.delete(f"/api/menus/{menu_id}")
|
||||
logger.info(f"Cleaned up menu {menu_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup menu {menu_id}: {e}")
|
||||
|
||||
async def _delete_dictionary(self, dict_id: int):
|
||||
"""删除字典"""
|
||||
try:
|
||||
await self.client.delete(f"/api/dictionaries/{dict_id}")
|
||||
logger.info(f"Cleaned up dictionary {dict_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup dictionary {dict_id}: {e}")
|
||||
|
||||
async def _delete_dict_type(self, dict_type_id: int):
|
||||
"""删除字典类型"""
|
||||
try:
|
||||
await self.client.delete(f"/api/dict/types/{dict_type_id}")
|
||||
logger.info(f"Cleaned up dict type {dict_type_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup dict type {dict_type_id}: {e}")
|
||||
|
||||
async def _delete_config(self, config_id: int):
|
||||
"""删除系统配置"""
|
||||
try:
|
||||
await self.client.delete(f"/api/config/{config_id}")
|
||||
logger.info(f"Cleaned up config {config_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup config {config_id}: {e}")
|
||||
|
||||
async def _delete_notice(self, notice_id: int):
|
||||
"""删除系统公告"""
|
||||
try:
|
||||
await self.client.delete(f"/api/notices/{notice_id}")
|
||||
logger.info(f"Cleaned up notice {notice_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup notice {notice_id}: {e}")
|
||||
|
||||
async def _delete_file(self, file_id: int):
|
||||
"""删除文件"""
|
||||
try:
|
||||
await self.client.delete(f"/api/files/{file_id}")
|
||||
logger.info(f"Cleaned up file {file_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup file {file_id}: {e}")
|
||||
|
||||
async def _delete_message(self, message_id: int):
|
||||
"""删除消息"""
|
||||
try:
|
||||
await self.client.delete(f"/api/messages/{message_id}")
|
||||
logger.info(f"Cleaned up message {message_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to cleanup message {message_id}: {e}")
|
||||
|
||||
def get_stats(self) -> Dict[str, int]:
|
||||
"""获取统计信息"""
|
||||
return {
|
||||
"users": len(self._users),
|
||||
"roles": len(self._roles),
|
||||
"menus": len(self._menus),
|
||||
"dictionaries": len(self._dictionaries),
|
||||
"dict_types": len(self._dict_types),
|
||||
"configs": len(self._configs),
|
||||
"notices": len(self._notices),
|
||||
"files": len(self._files),
|
||||
"messages": len(self._messages)
|
||||
}
|
||||
|
||||
def has_data(self) -> bool:
|
||||
"""检查是否有待清理数据"""
|
||||
return any([
|
||||
self._users, self._roles, self._menus,
|
||||
self._dictionaries, self._dict_types, self._configs,
|
||||
self._notices, self._files, self._messages
|
||||
])
|
||||
@@ -0,0 +1,89 @@
|
||||
"""
|
||||
数据验证工具类
|
||||
|
||||
提供数据验证相关的工具方法
|
||||
|
||||
作者: 张翔
|
||||
日期: 2026-04-01
|
||||
"""
|
||||
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class Validator:
|
||||
"""数据验证工具类"""
|
||||
|
||||
EMAIL_PATTERN = r'^[a-zA-Z0-9_.+-]+@[a-zA-Z0-9-]+\.[a-zA-Z0-9-.]+$'
|
||||
PHONE_PATTERN = r'^1[3-9]\d{9}$'
|
||||
USERNAME_PATTERN = r'^[a-zA-Z0-9_-]{3,50}$'
|
||||
ID_CARD_PATTERN = r'^\d{17}[\dXx]$'
|
||||
|
||||
@staticmethod
|
||||
def is_valid_email(email: str) -> bool:
|
||||
"""验证邮箱格式"""
|
||||
if not email:
|
||||
return False
|
||||
return bool(re.match(Validator.EMAIL_PATTERN, email))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_phone(phone: str) -> bool:
|
||||
"""验证手机号格式"""
|
||||
if not phone:
|
||||
return False
|
||||
return bool(re.match(Validator.PHONE_PATTERN, phone))
|
||||
|
||||
@staticmethod
|
||||
def is_valid_username(username: str) -> bool:
|
||||
"""验证用户名格式"""
|
||||
if not username:
|
||||
return False
|
||||
return bool(re.match(Validator.USERNAME_PATTERN, username))
|
||||
|
||||
@staticmethod
|
||||
def is_strong_password(password: str) -> bool:
|
||||
"""验证密码强度"""
|
||||
if not password or len(password) < 8:
|
||||
return False
|
||||
|
||||
has_upper = bool(re.search(r'[A-Z]', password))
|
||||
has_lower = bool(re.search(r'[a-z]', password))
|
||||
has_digit = bool(re.search(r'\d', password))
|
||||
has_special = bool(re.search(r'[!@#$%^&*(),.?":{}|<>]', password))
|
||||
|
||||
return has_upper and has_lower and has_digit and has_special
|
||||
|
||||
@staticmethod
|
||||
def is_valid_id_card(id_card: str) -> bool:
|
||||
"""
|
||||
验证身份证号格式
|
||||
|
||||
Args:
|
||||
id_card: 身份证号
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
if not id_card:
|
||||
return False
|
||||
|
||||
if not re.match(Validator.ID_CARD_PATTERN, id_card):
|
||||
return False
|
||||
|
||||
if id_card[:6] == "123456":
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
def sanitize(text: str) -> str:
|
||||
"""清洗输入,移除危险字符"""
|
||||
if not text:
|
||||
return ""
|
||||
|
||||
sanitized = text
|
||||
sanitized = re.sub(r'<script[^>]*>.*?</script>', '', sanitized, flags=re.IGNORECASE | re.DOTALL)
|
||||
sanitized = re.sub(r'<[^>]+>', '', sanitized)
|
||||
sanitized = re.sub(r"[';\"\\]", '', sanitized)
|
||||
|
||||
return sanitized.strip()
|
||||
Reference in New Issue
Block a user