refactor(backend): 重命名后端项目为 gym-manage-api,修改包名为 cn.novalon.gym.manage

This commit is contained in:
张翔
2026-04-17 18:35:50 +08:00
parent 666189b676
commit deb961c427
916 changed files with 108360 additions and 38328 deletions
+9
View File
@@ -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']
+83
View File
@@ -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()
+72
View File
@@ -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()
+115
View File
@@ -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)
+33
View File
@@ -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()
+173
View File
@@ -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
+204
View File
@@ -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
])
+89
View File
@@ -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()