初始提交

This commit is contained in:
焦龙言
2026-07-24 09:43:10 +08:00
commit b5b4721456
793 changed files with 116305 additions and 0 deletions
@@ -0,0 +1,3 @@
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
__all__ = ['RUNTIME_ENVIRONMENT', 'RuntimeEnvironmentService']
@@ -0,0 +1,3 @@
from .service import APP_RUNTIME, AppRuntimeService
__all__ = ['APP_RUNTIME', 'AppRuntimeService']
@@ -0,0 +1,29 @@
from importlib import import_module
from types import ModuleType
class AppInfrastructureGateway:
"""
应用基础设施网关。
该对象负责延迟加载应用实例构建和环境配置模块,
供应用运行时 facade 与其协作对象统一复用。
"""
@staticmethod
def get_server_module() -> ModuleType:
"""
获取应用 server 模块。
:return: server 模块
"""
return import_module('server')
@staticmethod
def get_env_module() -> ModuleType:
"""
获取环境配置模块。
:return: 环境配置模块
"""
return import_module('config.env')
@@ -0,0 +1,69 @@
from typing import Any
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
from .gateway import AppInfrastructureGateway
from .support import AppSnapshotSupport
class AppRuntimeService:
"""
应用运行时服务。
该服务作为应用运行时 facade,对外统一暴露应用实例构建、
应用配置快照与环境信息快照入口。
:param runtime_environment: 运行时环境服务
:param infrastructure_gateway: 应用基础设施网关
:param snapshot_support: 应用快照支持对象
"""
def __init__(
self,
*,
runtime_environment: RuntimeEnvironmentService | None = None,
infrastructure_gateway: AppInfrastructureGateway | None = None,
snapshot_support: AppSnapshotSupport | None = None,
) -> None:
"""
初始化应用运行时服务。
:param runtime_environment: 运行时环境服务
:param infrastructure_gateway: 应用基础设施网关
:param snapshot_support: 应用快照支持对象
:return: None
"""
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
self.infrastructure_gateway = infrastructure_gateway or AppInfrastructureGateway()
self.snapshot_support = snapshot_support or AppSnapshotSupport(
self.infrastructure_gateway,
self.runtime_environment,
)
def build_app_instance(self) -> Any:
"""
构建当前环境下的 FastAPI 应用实例。
:return: FastAPI 应用实例
"""
server_module = self.infrastructure_gateway.get_server_module()
return server_module.create_app()
def get_app_config_snapshot(self) -> dict[str, Any]:
"""
读取当前运行环境的应用配置快照。
:return: 应用配置快照
"""
return self.snapshot_support.build_app_config_snapshot()
def get_app_env_snapshot(self) -> dict[str, Any]:
"""
读取当前 CLI 进程的环境解析结果快照。
:return: 环境解析结果快照
"""
return self.snapshot_support.build_app_env_snapshot()
APP_RUNTIME = AppRuntimeService()
@@ -0,0 +1,90 @@
import os
from pathlib import Path
from typing import Any
from cli.runtime.base import RuntimeEnvironmentService
from .gateway import AppInfrastructureGateway
class AppSnapshotSupport:
"""
应用快照支持对象。
该对象负责应用配置快照和环境快照的构建,
避免主运行时服务继续承载快照字段拼装细节。
:param infrastructure_gateway: 应用基础设施网关
:param runtime_environment: 运行时环境服务
"""
def __init__(
self,
infrastructure_gateway: AppInfrastructureGateway,
runtime_environment: RuntimeEnvironmentService,
) -> None:
"""
初始化应用快照支持对象。
:param infrastructure_gateway: 应用基础设施网关
:param runtime_environment: 运行时环境服务
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
self.runtime_environment = runtime_environment
def build_app_config_snapshot(self) -> dict[str, Any]:
"""
读取当前运行环境的应用配置快照。
:return: 应用配置快照
"""
env_module = self.infrastructure_gateway.get_env_module()
app_config = env_module.AppConfig
database_config = env_module.DataBaseConfig
log_config = env_module.LogConfig
redis_config = env_module.RedisConfig
transport_crypto_config = env_module.TransportCryptoConfig
return {
'env': app_config.app_env,
'name': app_config.app_name,
'host': app_config.app_host,
'port': app_config.app_port,
'rootPath': app_config.app_root_path,
'reload': app_config.app_reload,
'workers': app_config.app_workers,
'disableSwagger': app_config.app_disable_swagger,
'disableRedoc': app_config.app_disable_redoc,
'dbType': database_config.db_type,
'dbHost': database_config.db_host,
'dbPort': database_config.db_port,
'dbDatabase': database_config.db_database,
'redisHost': redis_config.redis_host,
'redisPort': redis_config.redis_port,
'logLevel': log_config.loguru_level,
'transportCryptoEnabled': transport_crypto_config.transport_crypto_enabled,
'transportCryptoMode': transport_crypto_config.transport_crypto_mode,
}
def build_app_env_snapshot(self) -> dict[str, Any]:
"""
读取当前 CLI 进程的环境解析结果快照。
:return: 环境解析结果快照
"""
env_module = self.infrastructure_gateway.get_env_module()
app_config = env_module.AppConfig
backend_dir = Path(self.runtime_environment.get_backend_dir())
resolved_env = os.environ.get('APP_ENV', '') or 'dev'
env_file_name = f'.env.{resolved_env}'
env_file_path = backend_dir / env_file_name
return {
'cliEnv': resolved_env,
'configEnv': app_config.app_env,
'appEnv': os.environ.get('APP_ENV', ''),
'envFile': env_file_name,
'envFilePath': str(env_file_path),
'envFileExists': env_file_path.exists(),
'backendDir': str(backend_dir),
'pythonExecutable': self.runtime_environment.get_python_executable(),
}
@@ -0,0 +1,72 @@
import getpass
import os
import sys
from pathlib import Path
class RuntimeEnvironmentService:
"""
CLI 运行时环境服务。
该服务集中管理后端根目录解析、Python 解释器定位等稳定运行时能力,
避免调用方继续依赖零散模块级函数。
"""
@staticmethod
def is_backend_project_dir(project_dir: Path) -> bool:
"""
判断目录是否为后端项目根目录。
:param project_dir: 待检查目录
:return: 是否为后端项目根目录
"""
return (
(project_dir / 'app.py').exists()
and (project_dir / 'config' / 'env.py').exists()
and (project_dir / 'cli').is_dir()
)
def get_backend_dir(self) -> str:
"""
获取后端根目录绝对路径。
:return: 后端根目录绝对路径
"""
current_dir = Path.cwd().resolve()
if self.is_backend_project_dir(current_dir):
return str(current_dir)
return os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
@staticmethod
def get_python_executable() -> str:
"""
获取当前 CLI 进程使用的 Python 解释器。
:return: Python 解释器路径或命令名
"""
return sys.executable or 'python'
class RuntimeOperatorService:
"""
CLI 运行时操作者解析服务。
该服务集中提供当前 CLI 进程的操作者名称解析能力,
避免各领域运行时对象继续复制相同的 `getpass.getuser()` 回退逻辑。
"""
@staticmethod
def resolve_operator() -> str:
"""
解析当前 CLI 操作者名称。
:return: 操作者名称
"""
try:
return getpass.getuser() or 'shierfeng-cli'
except Exception:
return 'shierfeng-cli'
RUNTIME_ENVIRONMENT = RuntimeEnvironmentService()
RUNTIME_OPERATOR = RuntimeOperatorService()
@@ -0,0 +1,3 @@
from .service import CACHE_RUNTIME, CacheRuntimeService
__all__ = ['CACHE_RUNTIME', 'CacheRuntimeService']
+41
View File
@@ -0,0 +1,41 @@
from importlib import import_module
from typing import Any
REDIS_TTL_KEY_NOT_FOUND = -2
REDIS_TTL_PERSISTENT = -1
class CacheInfrastructureGateway:
"""
缓存基础设施网关。
该对象负责延迟加载 Redis 相关基础设施依赖,
供缓存运行时 facade 和其协作对象统一复用。
"""
@staticmethod
def get_redis_error_class() -> type[Exception]:
"""
获取 Redis 异常类型。
:return: Redis 异常类型
"""
return import_module('redis.exceptions').RedisError
@staticmethod
def get_redis_init_key_config() -> Any:
"""
获取系统缓存初始化键枚举。
:return: 缓存初始化键枚举
"""
return import_module('common.enums').RedisInitKeyConfig
@staticmethod
def get_redis_util() -> Any:
"""
获取 Redis 工具类。
:return: Redis 工具类
"""
return import_module('config.get_redis').RedisUtil
+216
View File
@@ -0,0 +1,216 @@
from typing import Any
from cli.exit_codes import RUNTIME_ERROR
from .gateway import REDIS_TTL_KEY_NOT_FOUND, REDIS_TTL_PERSISTENT, CacheInfrastructureGateway
from .support import CacheDomainSupport, CacheRedisSupport
class CacheRuntimeService:
"""
缓存运行时服务。
该服务作为缓存运行时 facade,对外统一暴露缓存统计、键列表、键值、
TTL 读取,以及缓存清理和预热入口。
:param infrastructure_gateway: 缓存基础设施网关
:param domain_support: 缓存领域支持对象
:param redis_support: 缓存 Redis 访问支持对象
"""
def __init__(
self,
*,
infrastructure_gateway: CacheInfrastructureGateway | None = None,
domain_support: CacheDomainSupport | None = None,
redis_support: CacheRedisSupport | None = None,
) -> None:
"""
初始化缓存运行时服务。
:param infrastructure_gateway: 缓存基础设施网关
:param domain_support: 缓存领域支持对象
:param redis_support: 缓存 Redis 访问支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway or CacheInfrastructureGateway()
self.domain_support = domain_support or CacheDomainSupport(self.infrastructure_gateway)
self.redis_support = redis_support or CacheRedisSupport(self.infrastructure_gateway, self.domain_support)
async def get_cache_stats(self) -> dict[str, Any]:
"""
获取缓存统计信息。
:return: 缓存统计结果
"""
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
async with self.redis_support.redis_session() as (redis, _redis_util):
info = await redis.info()
db_size = await redis.dbsize()
command_stats_dict = await redis.info('commandstats')
command_stats = [
{'name': key.split('_', 1)[1], 'value': int(value.get('calls', 0))}
for key, value in sorted(command_stats_dict.items(), key=lambda item: item[0])
]
return {
'ok': True,
'dbSize': db_size,
'commandStats': command_stats,
'cacheNames': self.domain_support.build_cache_name_items(),
'info': info,
}
except redis_error as exc:
return self.redis_support.build_redis_error_result('读取缓存统计失败', exc)
async def list_cache_keys(self, cache_name: str) -> dict[str, Any]:
"""
列出指定缓存名称下的键名。
:param cache_name: 缓存名称
:return: 缓存键名列表结果
"""
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
async with self.redis_support.redis_session() as (redis, _redis_util):
cache_keys: list[str] = await redis.keys(self.redis_support.build_cache_name_keys_pattern(cache_name))
key_items = self.domain_support.extract_cache_key_items(cache_name, cache_keys)
return {'ok': True, 'cacheName': cache_name, 'count': len(key_items), 'keys': key_items}
except redis_error as exc:
return self.redis_support.build_redis_error_result('读取缓存键名失败', exc)
async def get_cache_value(self, cache_name: str, cache_key: str) -> dict[str, Any]:
"""
读取指定缓存键值。
:param cache_name: 缓存名称
:param cache_key: 缓存键名
:return: 缓存值结果
"""
redis_error = self.infrastructure_gateway.get_redis_error_class()
full_cache_key = self.domain_support.build_full_cache_key(cache_name, cache_key)
try:
async with self.redis_support.redis_session() as (redis, _redis_util):
cache_value = await redis.get(full_cache_key)
if cache_value is None:
return self.domain_support.build_missing_cache_result(cache_name, cache_key)
return {
'ok': True,
'cacheName': cache_name,
'cacheKey': cache_key,
'fullCacheKey': full_cache_key,
'cacheValue': cache_value,
}
except redis_error as exc:
return self.redis_support.build_redis_error_result('读取缓存内容失败', exc)
async def get_cache_ttl(self, cache_name: str, cache_key: str) -> dict[str, Any]:
"""
读取指定缓存键的剩余过期时间。
:param cache_name: 缓存名称
:param cache_key: 缓存键名
:return: 缓存 TTL 结果
"""
redis_error = self.infrastructure_gateway.get_redis_error_class()
full_cache_key = self.domain_support.build_full_cache_key(cache_name, cache_key)
try:
async with self.redis_support.redis_session() as (redis, _redis_util):
ttl_seconds = await redis.ttl(full_cache_key)
if ttl_seconds == REDIS_TTL_KEY_NOT_FOUND:
return self.domain_support.build_missing_cache_result(cache_name, cache_key)
return {
'ok': True,
'message': '缓存剩余过期时间读取成功' if ttl_seconds >= 0 else '缓存存在且未设置过期时间',
'cacheName': cache_name,
'cacheKey': cache_key,
'fullCacheKey': full_cache_key,
'ttlSeconds': ttl_seconds,
'persistent': ttl_seconds == REDIS_TTL_PERSISTENT,
'expires': ttl_seconds >= 0,
}
except redis_error as exc:
return self.redis_support.build_redis_error_result('读取缓存剩余过期时间失败', exc)
async def clear_cache(
self,
*,
cache_name: str = '',
cache_key: str = '',
clear_all: bool = False,
dry_run: bool = False,
) -> dict[str, Any]:
"""
清理缓存。
:param cache_name: 需要按缓存名称清理的前缀
:param cache_key: 需要按缓存键名模糊清理的键
:param clear_all: 是否清理全部缓存
:param dry_run: 是否仅演练执行
:return: 缓存清理结果
"""
selected_modes = [bool(cache_name), bool(cache_key), clear_all]
if sum(selected_modes) != 1:
return {
'ok': False,
'message': '必须且只能指定一种清理方式:--cache-name、--cache-key 或 --all',
'exit_code': RUNTIME_ERROR,
}
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
async with self.redis_support.redis_session() as (redis, redis_util):
target_pattern = self.redis_support.build_clear_target_pattern(
cache_name=cache_name,
cache_key=cache_key,
clear_all=clear_all,
)
if target_pattern is None:
target_keys = sorted(await redis.keys())
else:
target_keys = sorted(await redis.keys(target_pattern))
result = {
'ok': True,
'dryRun': dry_run,
'matchedCount': len(target_keys),
'matchedKeys': target_keys,
'scope': self.domain_support.build_clear_scope(
cache_name=cache_name,
cache_key=cache_key,
clear_all=clear_all,
),
}
if dry_run:
result['message'] = '缓存清理演练完成,未执行实际删除'
return result
if target_keys:
await redis.delete(*target_keys)
if clear_all:
await redis_util.init_sys_dict(redis)
await redis_util.init_sys_config(redis)
result['message'] = '缓存清理完成'
return result
except redis_error as exc:
return self.redis_support.build_redis_error_result('清理缓存失败', exc)
async def warmup_cache(self) -> dict[str, Any]:
"""
预热系统缓存。
:return: 缓存预热执行结果
"""
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
async with self.redis_support.redis_session() as (redis, redis_util):
await redis_util.init_sys_dict(redis)
await redis_util.init_sys_config(redis)
return {'ok': True, 'message': '缓存预热完成'}
except redis_error as exc:
return self.redis_support.build_redis_error_result('缓存预热失败', exc)
CACHE_RUNTIME = CacheRuntimeService()
+201
View File
@@ -0,0 +1,201 @@
from contextlib import asynccontextmanager
from typing import Any
from cli.exit_codes import REDIS_ERROR, RUNTIME_ERROR
from .gateway import CacheInfrastructureGateway
class CacheDomainSupport:
"""
缓存领域支持对象。
该对象负责缓存名称列表构建、键范围解析和 TTL 结果判定等
局部缓存规则,避免主运行时服务继续承载细碎领域判断。
:param infrastructure_gateway: 缓存基础设施网关
"""
def __init__(self, infrastructure_gateway: CacheInfrastructureGateway) -> None:
"""
初始化缓存领域支持对象。
:param infrastructure_gateway: 缓存基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
def build_cache_name_items(self) -> list[dict[str, Any]]:
"""
构建系统内置缓存名称列表。
:return: 缓存名称列表
"""
redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config()
return [
{
'cacheName': key_config.key,
'remark': key_config.remark,
}
for key_config in redis_init_key_config
]
@staticmethod
def build_full_cache_key(cache_name: str, cache_key: str) -> str:
"""
构建缓存完整键名。
:param cache_name: 缓存名称
:param cache_key: 缓存键名
:return: 完整缓存键名
"""
return f'{cache_name}:{cache_key}'
def build_missing_cache_result(self, cache_name: str, cache_key: str) -> dict[str, Any]:
"""
构建缓存不存在的统一结果。
:param cache_name: 缓存名称
:param cache_key: 缓存键名
:return: 缓存不存在结果
"""
full_cache_key = self.build_full_cache_key(cache_name, cache_key)
return {
'ok': False,
'message': f'缓存不存在:{full_cache_key}',
'cacheName': cache_name,
'cacheKey': cache_key,
'fullCacheKey': full_cache_key,
'exit_code': RUNTIME_ERROR,
}
@staticmethod
def build_cache_name_pattern(cache_name: str) -> str:
"""
构建按缓存名称扫描时使用的 Redis 键模式。
:param cache_name: 缓存名称
:return: Redis 键匹配模式
"""
return f'{cache_name}:*'
@staticmethod
def extract_cache_key_items(cache_name: str, cache_keys: list[str]) -> list[str]:
"""
从缓存完整键名列表中提取相对键名。
:param cache_name: 缓存名称
:param cache_keys: 完整缓存键名列表
:return: 相对键名列表
"""
return [key.split(':', 1)[1] for key in sorted(cache_keys) if key.startswith(f'{cache_name}:')]
@staticmethod
def build_clear_scope(
*,
cache_name: str,
cache_key: str,
clear_all: bool,
) -> dict[str, str]:
"""
构建缓存清理范围定义。
:param cache_name: 缓存名称
:param cache_key: 缓存键名片段
:param clear_all: 是否清理全部缓存
:return: 清理范围定义
"""
if clear_all:
return {'mode': 'all'}
if cache_name:
return {'mode': 'cacheName', 'cacheName': cache_name}
return {'mode': 'cacheKey', 'cacheKey': cache_key}
class CacheRedisSupport:
"""
缓存 Redis 访问支持对象。
该对象负责 Redis 连接生命周期、错误结果规整和缓存键模式拼装,
避免主运行时服务继续承载基础设施桥接细节。
:param infrastructure_gateway: 缓存基础设施网关
:param domain_support: 缓存领域支持对象
"""
def __init__(
self,
infrastructure_gateway: CacheInfrastructureGateway,
domain_support: CacheDomainSupport,
) -> None:
"""
初始化缓存 Redis 访问支持对象。
:param infrastructure_gateway: 缓存基础设施网关
:param domain_support: 缓存领域支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
self.domain_support = domain_support
@asynccontextmanager
async def redis_session(self) -> Any:
"""
创建并托管 Redis 会话。
:return: `(redis, redis_util)` 元组
"""
redis = None
redis_util = self.infrastructure_gateway.get_redis_util()
try:
redis = await redis_util.create_redis_pool(log_enabled=False)
yield redis, redis_util
finally:
if redis is not None:
await redis.close()
@staticmethod
def build_redis_error_result(message: str, exc: Exception) -> dict[str, Any]:
"""
构建统一 Redis 异常结果。
:param message: 失败消息
:param exc: 原始异常
:return: 标准失败结果
"""
return {
'ok': False,
'message': message,
'error': str(exc),
'exit_code': REDIS_ERROR,
}
def build_cache_name_keys_pattern(self, cache_name: str) -> str:
"""
构建按缓存名称读取键列表时使用的模式。
:param cache_name: 缓存名称
:return: Redis 键匹配模式
"""
return self.domain_support.build_cache_name_pattern(cache_name)
def build_clear_target_pattern(
self,
*,
cache_name: str,
cache_key: str,
clear_all: bool,
) -> str | None:
"""
构建缓存清理目标的 Redis 键匹配模式。
:param cache_name: 缓存名称
:param cache_key: 缓存键名片段
:param clear_all: 是否清理全部缓存
:return: Redis 键匹配模式;全部清理时返回 `None`
"""
if clear_all:
return None
if cache_name:
return self.domain_support.build_cache_name_pattern(cache_name)
return f'*{cache_key}'
@@ -0,0 +1,3 @@
from .service import CONFIG_RUNTIME, ConfigRuntimeService
__all__ = ['CONFIG_RUNTIME', 'ConfigRuntimeService']
@@ -0,0 +1,101 @@
from importlib import import_module
from typing import Any
class ConfigInfrastructureGateway:
"""
参数配置基础设施网关。
该对象负责延迟加载数据库会话、Redis、DAO、Service 与脱敏工具依赖,
供参数配置运行时 facade 和其协作对象统一复用。
"""
@staticmethod
def get_redis_error_class() -> type[Exception]:
"""
获取 Redis 异常类型。
:return: Redis 异常类型
"""
return import_module('redis.exceptions').RedisError
@staticmethod
def get_common_constant() -> Any:
"""
获取公共常量定义。
:return: 公共常量定义模块
"""
return import_module('common.constant').CommonConstant
@staticmethod
def get_redis_init_key_config() -> Any:
"""
获取 Redis 初始化键枚举。
:return: Redis 初始化键枚举
"""
return import_module('common.enums').RedisInitKeyConfig
@staticmethod
def get_page_model() -> Any:
"""
获取分页模型类型。
:return: 分页模型类型
"""
return import_module('common.vo').PageModel
@staticmethod
def get_async_session_local() -> Any:
"""
获取异步数据库会话工厂。
:return: 异步数据库会话工厂
"""
return import_module('config.database').AsyncSessionLocal
@staticmethod
def get_redis_util() -> Any:
"""
获取 Redis 工具类。
:return: Redis 工具类
"""
return import_module('config.get_redis').RedisUtil
@staticmethod
def get_config_dao() -> Any:
"""
获取参数配置 DAO。
:return: 参数配置 DAO
"""
return import_module('module_admin.dao.config_dao').ConfigDao
@staticmethod
def get_config_vo_module() -> Any:
"""
获取参数配置 VO 模块。
:return: 参数配置 VO 模块
"""
return import_module('module_admin.entity.vo.config_vo')
@staticmethod
def get_config_service() -> Any:
"""
获取参数配置服务类。
:return: 参数配置服务类
"""
return import_module('module_admin.service.config_service').ConfigService
@staticmethod
def get_log_sanitizer() -> Any:
"""
获取日志脱敏工具类。
:return: 日志脱敏工具类
"""
return import_module('utils.log_util').LogSanitizer
@@ -0,0 +1,406 @@
from typing import Any, Literal
from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, REDIS_ERROR
from .gateway import ConfigInfrastructureGateway
from .support import ConfigDomainSupport
class ConfigRuntimeService:
"""
参数配置运行时服务。
该服务作为参数配置运行时 facade,对外统一暴露参数配置列表、详情、
写入、缓存刷新与一致性诊断入口。
:param infrastructure_gateway: 参数配置基础设施网关
:param domain_support: 参数配置领域支持对象
"""
def __init__(
self,
*,
infrastructure_gateway: ConfigInfrastructureGateway | None = None,
domain_support: ConfigDomainSupport | None = None,
) -> None:
"""
初始化参数配置运行时服务。
:param infrastructure_gateway: 参数配置基础设施网关
:param domain_support: 参数配置领域支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway or ConfigInfrastructureGateway()
self.domain_support = domain_support or ConfigDomainSupport(self.infrastructure_gateway)
@staticmethod
def build_cli_config_model(config_vo_module: Any, config_record: Any) -> Any | None:
"""
将 ORM 配置记录显式转换为 CLI 可用的配置模型。
`ConfigModel` 当前未统一开启 `populate_by_name`,因此 CLI 侧在处理
ORM 蛇形字段对象时,需要主动映射为驼峰 alias 输入,避免详情查询结果
被静默序列化为空。
:param config_vo_module: 配置 VO 模块
:param config_record: ORM 配置记录
:return: CLI 可用配置模型
"""
if config_record is None:
return None
return config_vo_module.ConfigModel(
configId=getattr(config_record, 'config_id', None),
configName=getattr(config_record, 'config_name', None),
configKey=getattr(config_record, 'config_key', None),
configValue=getattr(config_record, 'config_value', None),
configType=getattr(config_record, 'config_type', None),
createBy=getattr(config_record, 'create_by', None),
createTime=getattr(config_record, 'create_time', None),
updateBy=getattr(config_record, 'update_by', None),
updateTime=getattr(config_record, 'update_time', None),
remark=getattr(config_record, 'remark', None),
)
async def load_config_from_database(
self,
config_key: str,
) -> tuple[Any | None, dict[str, Any] | None] | dict[str, Any]:
"""
从数据库加载参数配置。
:param config_key: 参数键名
:return: 成功时返回配置模型与序列化结果,失败时返回错误结果字典
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
config_dao = self.infrastructure_gateway.get_config_dao()
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
try:
async with async_session_local() as session:
config_record = await config_dao.get_config_detail_by_info(
session,
config_vo_module.ConfigModel(configKey=config_key),
)
except Exception as exc:
return {'ok': False, 'message': '读取参数配置失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
database_model = self.build_cli_config_model(config_vo_module, config_record)
return database_model, self.domain_support.serialize_config_model(database_model)
async def load_config_from_cache(
self,
config_key: str,
) -> tuple[str | None, dict[str, Any] | None] | dict[str, Any]:
"""
从缓存加载参数配置。
:param config_key: 参数键名
:return: 成功时返回原始缓存值与序列化结果,失败时返回错误结果字典
"""
redis = None
redis_util = self.infrastructure_gateway.get_redis_util()
config_service = self.infrastructure_gateway.get_config_service()
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
redis = await redis_util.create_redis_pool(log_enabled=False)
cache_value = await config_service.query_config_list_from_cache_services(redis, config_key)
return cache_value, self.domain_support.serialize_cache_payload(config_key, cache_value)
except redis_error as exc:
return {'ok': False, 'message': '读取参数缓存失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
finally:
if redis is not None:
await redis.close()
async def list_configs(
self,
config_name: str = '',
config_key: str = '',
config_type: Literal['Y', 'N'] | None = None,
begin_date: str = '',
end_date: str = '',
*,
paged: bool = False,
page_num: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""
查询参数配置列表。
:param config_name: 参数名称过滤条件
:param config_key: 参数键名过滤条件
:param config_type: 参数类型过滤条件
:param begin_date: 开始日期
:param end_date: 结束日期
:param paged: 是否开启分页
:param page_num: 页码
:param page_size: 每页数量
:return: CLI 标准结果字典
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
config_service = self.infrastructure_gateway.get_config_service()
query_model = self.domain_support.build_config_query(
config_name,
config_key,
config_type,
begin_date,
end_date,
page_num,
page_size,
)
try:
async with async_session_local() as session:
result = await config_service.get_config_list_services(session, query_model, is_page=paged)
except Exception as exc:
return {'ok': False, 'message': '读取参数配置列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
filters = self.domain_support.build_list_filters(
config_name=config_name,
config_key=config_key,
config_type=config_type,
begin_date=begin_date,
end_date=end_date,
paged=paged,
page_num=page_num,
page_size=page_size,
)
if paged and isinstance(result, self.infrastructure_gateway.get_page_model()):
page_payload = result.model_dump(by_alias=True)
page_payload['rows'] = [
self.domain_support.sanitize_config_mapping(item) for item in page_payload.get('rows', [])
]
return {'ok': True, 'filters': filters, 'page': page_payload}
items = [self.domain_support.sanitize_config_mapping(item) for item in result]
return {'ok': True, 'filters': filters, 'count': len(items), 'items': items}
async def get_config(
self,
config_key: str,
*,
source: Literal['db', 'cache', 'both'] = 'both',
) -> dict[str, Any]:
"""
按参数键读取配置详情。
:param config_key: 参数键名
:param source: 读取来源
:return: CLI 标准结果字典
"""
database_model: Any | None = None
database_payload: dict[str, Any] | None = None
cache_value: str | None = None
cache_payload: dict[str, Any] | None = None
if source in {'db', 'both'}:
database_result = await self.load_config_from_database(config_key)
if isinstance(database_result, dict):
return database_result
database_model, database_payload = database_result
if source in {'cache', 'both'}:
cache_result = await self.load_config_from_cache(config_key)
if isinstance(cache_result, dict):
error_payload = {**cache_result, 'source': source}
if database_payload is not None:
error_payload['database'] = database_payload
return error_payload
cache_value, cache_payload = cache_result
if source == 'db' and database_payload is None:
return self.domain_support.build_missing_config_result(config_key, source)
if source == 'cache' and cache_payload is None:
return self.domain_support.build_missing_config_result(config_key, source)
if source == 'both' and database_payload is None and cache_payload is None:
return self.domain_support.build_missing_config_result(config_key, source)
payload = {'ok': True, 'key': config_key, 'source': source}
if source in {'db', 'both'}:
payload['database'] = database_payload
if source in {'cache', 'both'}:
payload['cache'] = cache_payload
if source == 'both':
payload['inSync'] = (database_model.config_value if database_model else None) == cache_value
return payload
async def set_config(
self,
config_key: str,
config_value: str,
*,
config_name: str | None = None,
config_type: Literal['Y', 'N'] | None = None,
remark: str | None = None,
dry_run: bool = False,
) -> dict[str, Any]:
"""
新增或更新参数配置,并同步缓存。
:param config_key: 参数键名
:param config_value: 参数键值
:param config_name: 参数名称
:param config_type: 参数类型
:param remark: 备注
:param dry_run: 是否仅执行演练
:return: CLI 标准结果字典
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
config_dao = self.infrastructure_gateway.get_config_dao()
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
redis_util = self.infrastructure_gateway.get_redis_util()
redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config()
redis_error = self.infrastructure_gateway.get_redis_error_class()
existing_config = None
target_config = None
try:
async with async_session_local() as session:
config_record = await config_dao.get_config_detail_by_info(
session,
config_vo_module.ConfigModel(configKey=config_key),
)
existing_config = self.build_cli_config_model(config_vo_module, config_record)
if existing_config is None and not config_name:
return {
'ok': False,
'message': '新增参数配置时必须传入 --name',
'exit_code': ARGUMENT_ERROR,
}
target_config = self.domain_support.build_target_config_model(
config_key,
config_value,
config_name,
config_type,
remark,
existing_config,
)
if dry_run:
return {
'ok': True,
'message': '参数配置演练完成,未执行实际写入',
'dryRun': True,
'action': 'update' if existing_config else 'create',
'config': self.domain_support.serialize_config_model(target_config),
}
if existing_config:
await config_dao.edit_config_dao(session, target_config.model_dump(exclude_none=True))
else:
await config_dao.add_config_dao(session, target_config)
await session.commit()
except Exception as exc:
return {'ok': False, 'message': '写入参数配置失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
redis = None
try:
redis = await redis_util.create_redis_pool(log_enabled=False)
await redis.set(f'{redis_init_key_config.SYS_CONFIG.key}:{config_key}', config_value)
except redis_error as exc:
return {
'ok': False,
'message': '参数配置已写入数据库,但同步缓存失败',
'error': str(exc),
'config': self.domain_support.serialize_config_model(target_config),
'databaseCommitted': True,
'exit_code': REDIS_ERROR,
}
finally:
if redis is not None:
await redis.close()
return {
'ok': True,
'message': '参数配置已同步到数据库和缓存',
'action': 'update' if existing_config else 'create',
'config': self.domain_support.serialize_config_model(target_config),
}
async def sync_config_cache(self) -> dict[str, Any]:
"""
刷新参数配置缓存。
:return: CLI 标准结果字典
"""
redis = None
async_session_local = self.infrastructure_gateway.get_async_session_local()
config_service = self.infrastructure_gateway.get_config_service()
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
redis_util = self.infrastructure_gateway.get_redis_util()
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
async with async_session_local() as session:
config_list = await config_service.get_config_list_services(
session,
config_vo_module.ConfigPageQueryModel(),
is_page=False,
)
redis = await redis_util.create_redis_pool(log_enabled=False)
await config_service.init_cache_sys_config_services(session, redis)
except redis_error as exc:
return {'ok': False, 'message': '刷新参数缓存失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
except Exception as exc:
return {'ok': False, 'message': '刷新参数缓存失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
finally:
if redis is not None:
await redis.close()
return {'ok': True, 'message': '参数缓存刷新成功', 'count': len(config_list)}
async def diagnose_config(self, *, sample_limit: int = 10) -> dict[str, Any]:
"""
诊断数据库参数配置与 Redis 缓存的一致性状态。
:param sample_limit: 示例键名输出上限
:return: CLI 标准结果字典
"""
redis = None
async_session_local = self.infrastructure_gateway.get_async_session_local()
config_service = self.infrastructure_gateway.get_config_service()
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
redis_util = self.infrastructure_gateway.get_redis_util()
redis_init_key_config = self.infrastructure_gateway.get_redis_init_key_config()
redis_error = self.infrastructure_gateway.get_redis_error_class()
try:
async with async_session_local() as session:
config_list = await config_service.get_config_list_services(
session,
config_vo_module.ConfigPageQueryModel(),
is_page=False,
)
redis = await redis_util.create_redis_pool(log_enabled=False)
cache_prefix = f'{redis_init_key_config.SYS_CONFIG.key}:'
cache_keys = sorted(await redis.keys(f'{cache_prefix}*'))
cache_values = await redis.mget(*cache_keys) if cache_keys else []
except redis_error as exc:
return {'ok': False, 'message': '读取参数缓存诊断信息失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
except Exception as exc:
return {'ok': False, 'message': '读取参数配置诊断信息失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
finally:
if redis is not None:
await redis.close()
database_map = {str(item.get('configKey')): str(item.get('configValue', '')) for item in config_list}
cache_map = {
cache_key.replace(cache_prefix, '', 1): '' if cache_value is None else str(cache_value)
for cache_key, cache_value in zip(cache_keys, cache_values, strict=False)
}
missing_in_cache = sorted(key for key in database_map if key not in cache_map)
orphan_in_cache = sorted(key for key in cache_map if key not in database_map)
mismatch_keys = sorted(
key for key, database_value in database_map.items() if key in cache_map and database_value != cache_map[key]
)
in_sync = not missing_in_cache and not orphan_in_cache and not mismatch_keys
return {
'ok': in_sync,
'message': '参数配置数据库与缓存一致' if in_sync else '参数配置存在数据库与缓存不一致项',
'databaseCount': len(database_map),
'cacheCount': len(cache_map),
'missingInCacheCount': len(missing_in_cache),
'orphanInCacheCount': len(orphan_in_cache),
'mismatchCount': len(mismatch_keys),
'sampleLimit': sample_limit,
'missingInCache': missing_in_cache[:sample_limit],
'orphanInCache': orphan_in_cache[:sample_limit],
'mismatchKeys': mismatch_keys[:sample_limit],
}
CONFIG_RUNTIME = ConfigRuntimeService()
@@ -0,0 +1,191 @@
from datetime import datetime
from typing import Any, Literal
from cli.exit_codes import RUNTIME_ERROR
from cli.runtime.base import RUNTIME_OPERATOR, RuntimeOperatorService
from .gateway import ConfigInfrastructureGateway
class ConfigDomainSupport:
"""
参数配置领域支持对象。
该对象负责操作者解析、配置脱敏、序列化、查询模型构建与缺失结果构建,
避免主运行时服务继续承载大量领域规则。
:param infrastructure_gateway: 参数配置基础设施网关
"""
def __init__(
self,
infrastructure_gateway: ConfigInfrastructureGateway,
operator_service: RuntimeOperatorService = RUNTIME_OPERATOR,
) -> None:
"""
初始化参数配置领域支持对象。
:param infrastructure_gateway: 参数配置基础设施网关
:param operator_service: 运行时操作者解析服务
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
self.operator_service = operator_service
def sanitize_config_mapping(self, config_mapping: dict[str, Any]) -> dict[str, Any]:
"""
对配置字典进行脱敏处理。
:param config_mapping: 原始配置字典
:return: 脱敏后的配置字典
"""
return self.infrastructure_gateway.get_log_sanitizer().sanitize_data(config_mapping)
def serialize_config_model(self, config_model: Any | None) -> dict[str, Any] | None:
"""
将配置模型序列化为 CLI 输出字典。
:param config_model: 配置模型对象
:return: 序列化后的配置字典
"""
if config_model is None or config_model.config_id is None:
return None
return self.sanitize_config_mapping(config_model.model_dump(by_alias=True, exclude_none=True))
def serialize_cache_payload(self, config_key: str, config_value: str | None) -> dict[str, Any] | None:
"""
将缓存中的配置值序列化为 CLI 输出字典。
:param config_key: 参数键名
:param config_value: 参数键值
:return: 序列化后的缓存配置字典
"""
if config_value is None:
return None
return self.sanitize_config_mapping({'configKey': config_key, 'configValue': config_value})
@staticmethod
def build_missing_config_result(
config_key: str,
source: Literal['db', 'cache', 'both'],
) -> dict[str, Any]:
"""
构建配置不存在的统一结果。
:param config_key: 参数键名
:param source: 读取来源
:return: 配置不存在的结果字典
"""
message = f'参数缓存不存在:{config_key}' if source == 'cache' else f'参数配置不存在:{config_key}'
return {'ok': False, 'message': message, 'source': source, 'exit_code': RUNTIME_ERROR}
def build_config_query(
self,
config_name: str,
config_key: str,
config_type: Literal['Y', 'N'] | None,
begin_date: str,
end_date: str,
page_num: int,
page_size: int,
) -> Any:
"""
构建配置列表查询对象。
:param config_name: 参数名称过滤条件
:param config_key: 参数键名过滤条件
:param config_type: 参数类型过滤条件
:param begin_date: 开始日期
:param end_date: 结束日期
:param page_num: 页码
:param page_size: 每页数量
:return: 配置分页查询模型
"""
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
return config_vo_module.ConfigPageQueryModel(
configName=config_name or None,
configKey=config_key or None,
configType=config_type,
beginTime=begin_date or None,
endTime=end_date or None,
pageNum=page_num,
pageSize=page_size,
)
def build_target_config_model(
self,
config_key: str,
config_value: str,
config_name: str | None,
config_type: Literal['Y', 'N'] | None,
remark: str | None,
existing_config: Any | None,
) -> Any:
"""
基于当前状态构建目标配置模型。
:param config_key: 参数键名
:param config_value: 参数键值
:param config_name: 参数名称
:param config_type: 参数类型
:param remark: 参数备注
:param existing_config: 已存在的配置模型
:return: 目标配置模型
"""
operator = self.operator_service.resolve_operator()
current_time = datetime.now()
common_constant = self.infrastructure_gateway.get_common_constant()
config_vo_module = self.infrastructure_gateway.get_config_vo_module()
default_config_type = existing_config.config_type if existing_config else common_constant.NO
target_config = config_vo_module.ConfigModel(
configId=existing_config.config_id if existing_config else None,
configName=config_name
if config_name is not None
else (existing_config.config_name if existing_config else None),
configKey=config_key,
configValue=config_value,
configType=config_type if config_type is not None else default_config_type,
createBy=existing_config.create_by if existing_config else operator,
createTime=existing_config.create_time if existing_config else current_time,
updateBy=operator,
updateTime=current_time,
remark=remark if remark is not None else (existing_config.remark if existing_config else None),
)
target_config.validate_fields()
return target_config
def build_list_filters(
self,
*,
config_name: str,
config_key: str,
config_type: Literal['Y', 'N'] | None,
begin_date: str,
end_date: str,
paged: bool,
page_num: int,
page_size: int,
) -> dict[str, Any]:
"""
构建配置列表过滤条件。
:param config_name: 参数名称
:param config_key: 参数键名
:param config_type: 参数类型
:param begin_date: 开始日期
:param end_date: 结束日期
:param paged: 是否分页
:param page_num: 页码
:param page_size: 每页数量
:return: 过滤条件字典
"""
return {
'configName': config_name,
'configKey': config_key,
'configType': config_type,
'beginDate': begin_date,
'endDate': end_date,
'paged': paged,
'pageNum': page_num,
'pageSize': page_size,
}
@@ -0,0 +1,3 @@
from .service import CRYPTO_RUNTIME, CryptoRuntimeService
__all__ = ['CRYPTO_RUNTIME', 'CryptoRuntimeService']
@@ -0,0 +1,56 @@
from importlib import import_module
from typing import Any
class CryptoInfrastructureGateway:
"""
传输加密基础设施网关。
该对象负责延迟加载 cryptography、环境配置和传输加密工具依赖,
供传输加密运行时 facade 与其协作对象统一复用。
"""
@staticmethod
def get_serialization_module() -> Any:
"""
获取 cryptography 序列化模块。
:return: 序列化模块
"""
return import_module('cryptography.hazmat.primitives.serialization')
@staticmethod
def get_rsa_module() -> Any:
"""
获取 RSA 非对称算法模块。
:return: RSA 模块
"""
return import_module('cryptography.hazmat.primitives.asymmetric.rsa')
@staticmethod
def get_transport_crypto_config() -> Any:
"""
获取传输加密配置对象。
:return: 传输加密配置对象
"""
return import_module('config.env').TransportCryptoConfig
@staticmethod
def get_transport_crypto_util() -> Any:
"""
获取传输加密工具类。
:return: 传输加密工具类
"""
return import_module('utils.transport_crypto_util').TransportCryptoUtil
@staticmethod
def get_transport_key_provider() -> Any:
"""
获取传输加密密钥提供者。
:return: 传输加密密钥提供者
"""
return import_module('utils.transport_crypto_util').TransportKeyProvider
@@ -0,0 +1,141 @@
from datetime import datetime
from typing import Any
from .gateway import CryptoInfrastructureGateway
from .support import CryptoDomainSupport, CryptoResultSupport
class CryptoRuntimeService:
"""
传输加密运行时服务。
该服务作为传输加密运行时 facade,对外统一暴露配置校验、
公钥导出、密钥生成和轮换辅助方案构建入口。
:param infrastructure_gateway: 传输加密基础设施网关
:param domain_support: 传输加密领域支持对象
:param result_support: 传输加密结果支持对象
"""
def __init__(
self,
*,
infrastructure_gateway: CryptoInfrastructureGateway | None = None,
domain_support: CryptoDomainSupport | None = None,
result_support: CryptoResultSupport | None = None,
) -> None:
"""
初始化传输加密运行时服务。
:param infrastructure_gateway: 传输加密基础设施网关
:param domain_support: 传输加密领域支持对象
:param result_support: 传输加密结果支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway or CryptoInfrastructureGateway()
self.domain_support = domain_support or CryptoDomainSupport(self.infrastructure_gateway)
self.result_support = result_support or CryptoResultSupport()
def generate_crypto_key_pair(self, kid: str, key_size: int) -> dict[str, Any]:
"""
生成新的传输加密密钥对。
:param kid: 目标密钥版本标识
:param key_size: RSA 密钥长度
:return: 密钥生成结果
"""
def _operation() -> dict[str, Any]:
self.domain_support.validate_rsa_key_size(key_size)
private_key_pem, public_key_pem = self.domain_support.generate_rsa_key_pair(key_size)
return {
'ok': True,
'message': '传输加密密钥生成完成',
'kid': kid,
'keySize': key_size,
'privateKey': private_key_pem,
'publicKey': public_key_pem,
'envPatch': self.domain_support.build_key_pair_env_patch(kid, private_key_pem, public_key_pem),
}
return self.result_support.run_argument_guarded(_operation, failure_message='生成传输加密密钥失败')
def validate_crypto_config(self) -> dict[str, Any]:
"""
校验传输加密运行配置。
:return: 传输加密配置校验结果
"""
transport_key_provider = self.infrastructure_gateway.get_transport_key_provider()
def _operation() -> dict[str, Any]:
transport_key_provider.validate_runtime_configuration()
return {'ok': True, 'message': '传输加密配置校验通过'}
return self.result_support.run_runtime_guarded(_operation, failure_message='传输加密配置校验失败')
def export_public_key(self) -> dict[str, Any]:
"""
导出当前运行环境的传输加密公钥信息。
:return: 公钥导出结果
"""
transport_key_provider = self.infrastructure_gateway.get_transport_key_provider()
transport_crypto_util = self.infrastructure_gateway.get_transport_crypto_util()
def _operation() -> dict[str, Any]:
transport_key_provider.validate_runtime_configuration()
return {'ok': True, 'publicKey': transport_crypto_util.build_public_key_payload()}
return self.result_support.run_runtime_guarded(_operation, failure_message='导出传输加密公钥失败')
def build_rotation_payload(self, next_kid: str, key_size: int) -> dict[str, Any]:
"""
生成传输加密密钥轮换辅助结果。
:param next_kid: 新密钥版本标识
:param key_size: 新密钥的 RSA 长度
:return: 轮换辅助结果
"""
transport_key_provider = self.infrastructure_gateway.get_transport_key_provider()
def _operation() -> dict[str, Any]:
self.domain_support.validate_rsa_key_size(key_size)
transport_key_provider.validate_runtime_configuration()
current_key_pair = transport_key_provider.get_current_key_pair()
if next_kid == current_key_pair.kid:
raise ValueError('新密钥版本不能与当前版本相同')
private_key_pem, public_key_pem = self.domain_support.generate_rsa_key_pair(key_size)
legacy_key_pairs = self.domain_support.load_existing_legacy_key_pairs()
merged_legacy_key_pairs = self.domain_support.merge_rotation_legacy_key_pairs(
legacy_key_pairs,
current_key_pair,
next_kid,
)
return {
'ok': True,
'message': '传输加密密钥轮换方案已生成',
'currentKid': current_key_pair.kid,
'nextKid': next_kid,
'keySize': key_size,
'generatedAt': datetime.now().isoformat(),
'nextKeyPair': {
'kid': next_kid,
'privateKey': private_key_pem,
'publicKey': public_key_pem,
},
'legacyKeyPairs': merged_legacy_key_pairs,
'envPatch': self.domain_support.build_rotation_env_patch(
next_kid,
private_key_pem,
public_key_pem,
merged_legacy_key_pairs,
),
'applySteps': self.result_support.build_rotation_apply_steps(),
}
return self.result_support.run_argument_guarded(_operation, failure_message='生成密钥轮换方案失败')
CRYPTO_RUNTIME = CryptoRuntimeService()
@@ -0,0 +1,234 @@
import json
from collections.abc import Callable
from typing import Any
from cli.exit_codes import ARGUMENT_ERROR, RUNTIME_ERROR
from .gateway import CryptoInfrastructureGateway
MIN_RSA_KEY_SIZE = 2048
RSA_KEY_SIZE_STEP = 256
class CryptoDomainSupport:
"""
传输加密领域支持对象。
该对象负责 RSA 密钥规则校验、密钥对生成和历史密钥规整,
避免主运行时服务继续承载局部安全规则和数据拼装细节。
:param infrastructure_gateway: 传输加密基础设施网关
"""
def __init__(self, infrastructure_gateway: CryptoInfrastructureGateway) -> None:
"""
初始化传输加密领域支持对象。
:param infrastructure_gateway: 传输加密基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
@staticmethod
def validate_rsa_key_size(key_size: int) -> None:
"""
校验 RSA 密钥长度是否合法。
:param key_size: RSA 密钥长度
:return: None
:raises ValueError: 密钥长度不满足约束时抛出异常
"""
if key_size < MIN_RSA_KEY_SIZE or key_size % RSA_KEY_SIZE_STEP != 0:
raise ValueError('RSA 密钥长度必须大于等于 2048,且为 256 的整数倍')
def generate_rsa_key_pair(self, key_size: int) -> tuple[str, str]:
"""
生成 PEM 格式的 RSA 密钥对。
:param key_size: RSA 密钥长度
:return: 私钥与公钥 PEM 字符串
"""
rsa_module = self.infrastructure_gateway.get_rsa_module()
serialization_module = self.infrastructure_gateway.get_serialization_module()
private_key = rsa_module.generate_private_key(public_exponent=65537, key_size=key_size)
private_key_pem = private_key.private_bytes(
encoding=serialization_module.Encoding.PEM,
format=serialization_module.PrivateFormat.PKCS8,
encryption_algorithm=serialization_module.NoEncryption(),
).decode('utf-8')
public_key_pem = (
private_key.public_key()
.public_bytes(
encoding=serialization_module.Encoding.PEM,
format=serialization_module.PublicFormat.SubjectPublicKeyInfo,
)
.decode('utf-8')
)
return private_key_pem, public_key_pem
def load_existing_legacy_key_pairs(self) -> list[dict[str, str]]:
"""
解析当前配置中的历史密钥列表。
:return: 历史密钥列表
:raises ValueError: 历史密钥配置非法时抛出异常
"""
transport_crypto_config = self.infrastructure_gateway.get_transport_crypto_config()
raw_legacy_key_pairs = transport_crypto_config.transport_crypto_legacy_key_pairs or '[]'
parsed_legacy_key_pairs = json.loads(raw_legacy_key_pairs)
if not isinstance(parsed_legacy_key_pairs, list):
raise ValueError('TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS 必须为 JSON 数组')
return [item for item in parsed_legacy_key_pairs if isinstance(item, dict)]
@staticmethod
def merge_rotation_legacy_key_pairs(
legacy_key_pairs: list[dict[str, str]],
current_key_pair: Any,
next_kid: str,
) -> list[dict[str, str]]:
"""
合并轮换后的历史密钥列表。
:param legacy_key_pairs: 当前历史密钥列表
:param current_key_pair: 当前生效密钥对对象
:param next_kid: 即将切换的新密钥版本
:return: 合并后的历史密钥列表
"""
normalized_legacy_key_pairs: dict[str, dict[str, str]] = {
str(item.get('kid')): item for item in legacy_key_pairs if item.get('kid')
}
normalized_legacy_key_pairs[current_key_pair.kid] = {
'kid': current_key_pair.kid,
'privateKey': current_key_pair.private_key_pem,
'publicKey': current_key_pair.public_key_pem,
}
normalized_legacy_key_pairs.pop(next_kid, None)
return list(normalized_legacy_key_pairs.values())
@staticmethod
def build_key_pair_env_patch(kid: str, private_key_pem: str, public_key_pem: str) -> dict[str, str]:
"""
构建密钥对写回环境变量所需的补丁数据。
:param kid: 密钥版本标识
:param private_key_pem: 私钥 PEM 文本
:param public_key_pem: 公钥 PEM 文本
:return: 环境变量补丁数据
"""
return {
'TRANSPORT_CRYPTO_KID': kid,
'TRANSPORT_CRYPTO_PRIVATE_KEY': private_key_pem,
'TRANSPORT_CRYPTO_PUBLIC_KEY': public_key_pem,
}
def build_rotation_env_patch(
self,
next_kid: str,
private_key_pem: str,
public_key_pem: str,
merged_legacy_key_pairs: list[dict[str, str]],
) -> dict[str, str]:
"""
构建密钥轮换写回环境变量所需的补丁数据。
:param next_kid: 新密钥版本标识
:param private_key_pem: 新私钥 PEM 文本
:param public_key_pem: 新公钥 PEM 文本
:param merged_legacy_key_pairs: 合并后的历史密钥列表
:return: 环境变量补丁数据
"""
env_patch = self.build_key_pair_env_patch(next_kid, private_key_pem, public_key_pem)
env_patch['TRANSPORT_CRYPTO_LEGACY_KEY_PAIRS'] = json.dumps(merged_legacy_key_pairs, ensure_ascii=False)
return env_patch
class CryptoResultSupport:
"""
传输加密结果支持对象。
该对象负责参数错误/运行时错误结果翻译,以及固定轮换提示文案构建,
避免主运行时服务继续承载重复异常映射与静态 payload 片段。
"""
@staticmethod
def build_argument_error_result(message: str, exc: Exception) -> dict[str, Any]:
"""
构建参数错误结果。
:param message: 失败消息
:param exc: 原始异常
:return: 标准参数错误结果
"""
return {
'ok': False,
'message': message,
'error': str(exc),
'exit_code': ARGUMENT_ERROR,
}
@staticmethod
def build_runtime_error_result(message: str, exc: Exception) -> dict[str, Any]:
"""
构建运行时错误结果。
:param message: 失败消息
:param exc: 原始异常
:return: 标准运行时错误结果
"""
return {
'ok': False,
'message': message,
'error': str(exc),
'exit_code': RUNTIME_ERROR,
}
def run_argument_guarded(
self,
operation: Callable[[], dict[str, Any]],
*,
failure_message: str,
) -> dict[str, Any]:
"""
以统一参数/运行时错误翻译执行操作。
:param operation: 待执行操作
:param failure_message: 失败消息
:return: 操作结果
"""
try:
return operation()
except ValueError as exc:
return self.build_argument_error_result(failure_message, exc)
except Exception as exc:
return self.build_runtime_error_result(failure_message, exc)
def run_runtime_guarded(
self,
operation: Callable[[], dict[str, Any]],
*,
failure_message: str,
) -> dict[str, Any]:
"""
以统一运行时错误翻译执行操作。
:param operation: 待执行操作
:param failure_message: 失败消息
:return: 操作结果
"""
try:
return operation()
except Exception as exc:
return self.build_runtime_error_result(failure_message, exc)
@staticmethod
def build_rotation_apply_steps() -> list[str]:
"""
构建密钥轮换后的应用步骤说明。
:return: 轮换应用步骤列表
"""
return [
'将 envPatch 中的新密钥写入目标环境配置',
'发布后保留 legacyKeyPairs,确保旧请求在轮换窗口内可解密',
'确认前端已刷新到 nextKid 后,再移除不再需要的历史密钥',
]
@@ -0,0 +1,3 @@
from .service import DATABASE_RUNTIME, DatabaseRuntimeService
__all__ = ['DATABASE_RUNTIME', 'DatabaseRuntimeService']
@@ -0,0 +1,56 @@
from importlib import import_module
from typing import Any
class DatabaseInfrastructureGateway:
"""
数据库基础设施网关。
该对象负责延迟加载数据库引擎、SQLAlchemy 和 Alembic 相关依赖,
供数据库运行时 facade 与其协作对象统一复用。
"""
@staticmethod
def get_async_db_engine_factory() -> Any:
"""
获取异步数据库引擎工厂函数。
:return: 异步数据库引擎工厂函数
"""
return import_module('config.database').create_async_db_engine
@staticmethod
def get_sync_db_engine_factory() -> Any:
"""
获取同步数据库引擎工厂函数。
:return: 同步数据库引擎工厂函数
"""
return import_module('config.database').create_sync_db_engine
@staticmethod
def get_sqlalchemy_text() -> Any:
"""
获取 SQLAlchemy `text` 构造函数。
:return: SQLAlchemy `text` 函数
"""
return import_module('sqlalchemy').text
@staticmethod
def get_alembic_config_class() -> Any:
"""
获取 Alembic 配置类。
:return: Alembic 配置类
"""
return import_module('alembic.config').Config
@staticmethod
def get_alembic_script_directory_class() -> Any:
"""
获取 Alembic 脚本目录类。
:return: Alembic 脚本目录类
"""
return import_module('alembic.script').ScriptDirectory
@@ -0,0 +1,199 @@
from typing import Any
from cli.exit_codes import DATABASE_ERROR
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
from .gateway import DatabaseInfrastructureGateway
from .support import DatabaseAlembicCommandSupport, DatabaseRevisionSupport
class DatabaseRuntimeService:
"""
数据库运行时服务。
该服务作为数据库运行时 facade,对外统一暴露数据库连通性检查、
Alembic 版本读取与迁移命令执行入口。
:param runtime_environment: 运行时环境服务
:param infrastructure_gateway: 数据库基础设施网关
:param revision_support: 数据库迁移版本支持对象
:param alembic_command_support: 数据库 Alembic 命令支持对象
"""
def __init__(
self,
*,
runtime_environment: RuntimeEnvironmentService | None = None,
infrastructure_gateway: DatabaseInfrastructureGateway | None = None,
revision_support: DatabaseRevisionSupport | None = None,
alembic_command_support: DatabaseAlembicCommandSupport | None = None,
) -> None:
"""
初始化数据库运行时服务。
:param runtime_environment: 运行时环境服务
:param infrastructure_gateway: 数据库基础设施网关
:param revision_support: 数据库迁移版本支持对象
:param alembic_command_support: 数据库 Alembic 命令支持对象
:return: None
"""
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
self.infrastructure_gateway = infrastructure_gateway or DatabaseInfrastructureGateway()
self.revision_support = revision_support or DatabaseRevisionSupport(
self.infrastructure_gateway,
self.runtime_environment,
)
self.alembic_command_support = alembic_command_support or DatabaseAlembicCommandSupport(
self.runtime_environment
)
async def ping_database(self) -> dict[str, Any]:
"""
检查数据库连通性。
:return: 数据库检查结果
"""
create_async_db_engine = self.infrastructure_gateway.get_async_db_engine_factory()
text = self.infrastructure_gateway.get_sqlalchemy_text()
engine = create_async_db_engine(echo=False)
try:
async with engine.connect() as connection:
await connection.execute(text('SELECT 1'))
return {'ok': True, 'message': '数据库连接成功'}
except Exception as exc:
return {'ok': False, 'message': '数据库连接失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
finally:
await engine.dispose()
def get_current_revision(self) -> dict[str, Any]:
"""
获取数据库当前迁移版本。
:return: 当前迁移版本信息
"""
create_sync_db_engine = self.infrastructure_gateway.get_sync_db_engine_factory()
text = self.infrastructure_gateway.get_sqlalchemy_text()
engine = create_sync_db_engine(echo=False)
try:
with engine.connect() as connection:
revision = connection.execute(text('SELECT version_num FROM alembic_version')).scalar()
return {'ok': True, 'currentRevision': revision}
except Exception as exc:
return {'ok': False, 'message': '读取数据库迁移版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
finally:
engine.dispose()
def upgrade_database(self, revision: str = 'head', *, dry_run: bool = False) -> dict[str, Any]:
"""
执行数据库迁移升级。
:param revision: 目标迁移版本,默认为 `head`
:param dry_run: 是否仅演练执行
:return: 数据库迁移执行结果
"""
return self.alembic_command_support.run_alembic_command(
'upgrade',
revision,
success_message=f'数据库已升级到 {revision}',
failure_message='数据库升级失败',
dry_run=dry_run,
)
def init_database(self, *, dry_run: bool = False) -> dict[str, Any]:
"""
初始化数据库到最新迁移版本。
:param dry_run: 是否仅演练执行
:return: 数据库初始化结果
"""
return self.alembic_command_support.run_alembic_command(
'upgrade',
'head',
success_message='数据库初始化完成,当前版本已同步到 head',
failure_message='数据库初始化失败',
dry_run=dry_run,
)
def downgrade_database(self, revision: str = '-1', *, dry_run: bool = False) -> dict[str, Any]:
"""
执行数据库回退。
:param revision: 目标回退版本,默认为 `-1`
:param dry_run: 是否仅演练执行
:return: 数据库回退结果
"""
return self.alembic_command_support.run_alembic_command(
'downgrade',
revision,
success_message=f'数据库已回退到 {revision}',
failure_message='数据库回退失败',
dry_run=dry_run,
)
def create_revision(self, message: str, *, autogenerate: bool = False, dry_run: bool = False) -> dict[str, Any]:
"""
创建新的 Alembic 迁移版本文件。
:param message: 迁移说明
:param autogenerate: 是否自动生成变更
:param dry_run: 是否仅演练执行
:return: 迁移版本创建结果
"""
arguments: list[str] = ['-m', message]
if autogenerate:
arguments.append('--autogenerate')
return self.alembic_command_support.run_alembic_command(
'revision',
*arguments,
success_message='数据库迁移版本文件创建完成',
failure_message='数据库迁移版本文件创建失败',
dry_run=dry_run,
)
def get_alembic_heads(self) -> dict[str, Any]:
"""
读取当前代码仓库中的 Alembic heads 信息。
:return: Alembic heads 结果
"""
try:
script_directory = self.revision_support.build_alembic_script_directory()
items = [
self.revision_support.serialize_revision(revision)
for revision in script_directory.get_revisions('heads')
]
return {
'ok': True,
'message': '已读取 Alembic heads',
'count': len(items),
'items': items,
}
except Exception as exc:
return {'ok': False, 'message': '读取 Alembic heads 失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
def get_alembic_history(self, *, limit: int = 20) -> dict[str, Any]:
"""
读取当前代码仓库中的 Alembic 历史版本信息。
:param limit: 返回的最大历史记录数量
:return: Alembic 历史版本结果
"""
try:
script_directory = self.revision_support.build_alembic_script_directory()
history_items = [
self.revision_support.serialize_revision(revision) for revision in script_directory.walk_revisions()
]
limited_items = history_items[:limit]
return {
'ok': True,
'message': '已读取 Alembic 历史版本',
'count': len(limited_items),
'totalCount': len(history_items),
'limit': limit,
'items': limited_items,
}
except Exception as exc:
return {'ok': False, 'message': '读取 Alembic 历史版本失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
DATABASE_RUNTIME = DatabaseRuntimeService()
@@ -0,0 +1,160 @@
import os
import subprocess
from typing import Any
from cli.exit_codes import DATABASE_ERROR
from cli.runtime.base import RuntimeEnvironmentService
from .gateway import DatabaseInfrastructureGateway
class DatabaseRevisionSupport:
"""
数据库迁移版本支持对象。
该对象负责 Alembic script directory 构建、修订版本规整和
版本对象序列化,避免主运行时服务继续承载迁移元数据细节。
:param infrastructure_gateway: 数据库基础设施网关
:param runtime_environment: 运行时环境服务
"""
def __init__(
self,
infrastructure_gateway: DatabaseInfrastructureGateway,
runtime_environment: RuntimeEnvironmentService,
) -> None:
"""
初始化数据库迁移版本支持对象。
:param infrastructure_gateway: 数据库基础设施网关
:param runtime_environment: 运行时环境服务
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
self.runtime_environment = runtime_environment
def build_alembic_script_directory(self) -> Any:
"""
基于当前后端目录构建 Alembic 脚本目录对象。
:return: Alembic 脚本目录对象
"""
config_class = self.infrastructure_gateway.get_alembic_config_class()
script_directory_class = self.infrastructure_gateway.get_alembic_script_directory_class()
config = config_class(os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini'))
return script_directory_class.from_config(config)
@staticmethod
def normalize_revision_value(value: Any) -> list[str]:
"""
规范化 Alembic 修订版本引用值。
:param value: 原始修订版本引用
:return: 规范化后的修订版本列表
"""
if value is None:
return []
if isinstance(value, str):
return [value]
return [str(item) for item in value]
def serialize_revision(self, script_revision: Any) -> dict[str, Any]:
"""
序列化 Alembic 修订版本对象。
:param script_revision: Alembic 修订版本对象
:return: 序列化后的修订版本字典
"""
return {
'revision': str(script_revision.revision),
'downRevisions': self.normalize_revision_value(script_revision.down_revision),
'branchLabels': sorted(str(item) for item in script_revision.branch_labels or []),
'dependsOn': self.normalize_revision_value(script_revision.dependencies),
'doc': (script_revision.doc or '').strip(),
'path': str(script_revision.path),
}
class DatabaseAlembicCommandSupport:
"""
数据库 Alembic 命令支持对象。
该对象负责 Alembic 命令组装与子进程执行,
避免主运行时服务继续承载命令行桥接细节。
:param runtime_environment: 运行时环境服务
"""
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
"""
初始化数据库 Alembic 命令支持对象。
:param runtime_environment: 运行时环境服务
:return: None
"""
self.runtime_environment = runtime_environment
def build_alembic_command(self, command: str, *arguments: str) -> list[str]:
"""
构建 Alembic 命令。
:param command: Alembic 子命令名称
:param arguments: Alembic 子命令参数列表
:return: Alembic 命令参数列表
"""
alembic_ini_path = os.path.join(self.runtime_environment.get_backend_dir(), 'alembic.ini')
return ['alembic', '-c', alembic_ini_path, command, *arguments]
def run_alembic_command(
self,
command: str,
*arguments: str,
success_message: str,
failure_message: str,
dry_run: bool = False,
) -> dict[str, Any]:
"""
执行 Alembic 命令并返回统一结果。
:param command: Alembic 子命令名称
:param arguments: Alembic 子命令参数列表
:param success_message: 成功提示信息
:param failure_message: 失败提示信息
:param dry_run: 是否仅演练执行
:return: Alembic 执行结果
"""
command_arguments = self.build_alembic_command(command, *arguments)
if dry_run:
return {
'ok': True,
'message': f'{success_message}(dry-run)',
'dryRun': True,
'command': command_arguments,
'cwd': self.runtime_environment.get_backend_dir(),
}
try:
completed = subprocess.run(
command_arguments,
cwd=self.runtime_environment.get_backend_dir(),
env={**os.environ, 'APP_ENV': os.environ.get('APP_ENV', 'dev')},
capture_output=True,
text=True,
check=False,
)
if completed.returncode != 0:
return {
'ok': False,
'message': failure_message,
'error': completed.stderr.strip() or completed.stdout.strip(),
'exit_code': DATABASE_ERROR,
}
payload: dict[str, Any] = {'ok': True, 'message': success_message}
stdout = completed.stdout.strip()
if stdout:
payload['stdout'] = stdout
return payload
except Exception as exc:
return {'ok': False, 'message': failure_message, 'error': str(exc), 'exit_code': DATABASE_ERROR}
@@ -0,0 +1,3 @@
from .service import DEVELOPMENT_RUNTIME, DevelopmentRuntimeService
__all__ = ['DEVELOPMENT_RUNTIME', 'DevelopmentRuntimeService']
@@ -0,0 +1,60 @@
import subprocess
from typing import Any
from cli.exit_codes import RUNTIME_ERROR
from cli.runtime.base import RuntimeEnvironmentService
class DevelopmentProcessGateway:
"""
开发子进程执行网关。
该对象负责统一执行开发工具子进程命令,
避免支持对象直接依赖 `subprocess` 细节。
:param runtime_environment: 运行时环境服务
"""
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
"""
初始化开发子进程执行网关。
:param runtime_environment: 运行时环境服务
:return: None
"""
self.runtime_environment = runtime_environment
def run_command(self, command: list[str]) -> dict[str, Any]:
"""
执行子进程命令并返回统一结果。
:param command: 待执行命令
:return: 命令执行结果
"""
try:
completed = subprocess.run(
command,
cwd=self.runtime_environment.get_backend_dir(),
capture_output=True,
text=True,
check=False,
)
except Exception as exc:
return {
'ok': False,
'message': '开发命令执行失败',
'error': str(exc),
'command': command,
'exit_code': RUNTIME_ERROR,
}
payload: dict[str, Any] = {
'ok': completed.returncode == 0,
'command': command,
'returnCode': completed.returncode,
}
if completed.stdout.strip():
payload['stdout'] = completed.stdout.strip()
if completed.stderr.strip():
payload['stderr'] = completed.stderr.strip()
return payload
@@ -0,0 +1,158 @@
from typing import Any
from cli.exit_codes import DEPENDENCY_ERROR, RUNTIME_ERROR
from cli.runtime.base import RUNTIME_ENVIRONMENT, RuntimeEnvironmentService
from .gateway import DevelopmentProcessGateway
from .support import DevelopmentCommandSupport, DevelopmentToolingSupport
class DevelopmentRuntimeService:
"""
开发运行时服务。
该服务作为开发运行时 facade,对外统一暴露 lint/format 和 pytest
执行入口。
:param runtime_environment: 运行时环境服务
:param tooling_support: 开发工具支持对象
:param command_support: 开发命令构建支持对象
:param process_gateway: 开发子进程执行网关
"""
def __init__(
self,
*,
runtime_environment: RuntimeEnvironmentService | None = None,
tooling_support: DevelopmentToolingSupport | None = None,
command_support: DevelopmentCommandSupport | None = None,
process_gateway: DevelopmentProcessGateway | None = None,
) -> None:
"""
初始化开发运行时服务。
:param runtime_environment: 运行时环境服务
:param tooling_support: 开发工具支持对象
:param command_support: 开发命令构建支持对象
:param process_gateway: 开发子进程执行网关
:return: None
"""
self.runtime_environment = runtime_environment or RUNTIME_ENVIRONMENT
self.tooling_support = tooling_support or DevelopmentToolingSupport()
self.command_support = command_support or DevelopmentCommandSupport(self.runtime_environment)
self.process_gateway = process_gateway or DevelopmentProcessGateway(self.runtime_environment)
def run_lint(
self,
targets: list[str] | None,
*,
check_only: bool = False,
fix: bool = False,
unsafe_fixes: bool = False,
) -> dict[str, Any]:
"""
执行仓库 lint/format 检查。
:param targets: 目标路径列表
:param check_only: 是否仅检查不写回
:param fix: 是否允许 `ruff check --fix`
:param unsafe_fixes: 是否允许不安全修复
:return: lint 执行结果
"""
normalized_targets = self.tooling_support.resolve_targets(targets)
format_command = self.command_support.build_format_command(normalized_targets, check_only=check_only)
format_result = self.process_gateway.run_command(format_command)
if not format_result.get('ok', False):
return {
'ok': False,
'message': 'Ruff format 阶段失败',
'targets': normalized_targets,
'format': format_result,
'exit_code': RUNTIME_ERROR,
}
check_command = self.command_support.build_check_command(
normalized_targets,
check_only=check_only,
fix=fix,
unsafe_fixes=unsafe_fixes,
)
check_result = self.process_gateway.run_command(check_command)
if not check_result.get('ok', False):
return {
'ok': False,
'message': 'Ruff check 阶段失败',
'targets': normalized_targets,
'format': format_result,
'check': check_result,
'exit_code': RUNTIME_ERROR,
}
return {
'ok': True,
'message': '开发检查已完成',
'targets': normalized_targets,
'checkOnly': check_only,
'fix': fix and not check_only,
'unsafeFixes': unsafe_fixes and fix and not check_only,
'format': format_result,
'check': check_result,
}
def run_tests(
self,
targets: list[str] | None,
*,
keyword: str = '',
maxfail: int = 0,
quiet: bool = False,
) -> dict[str, Any]:
"""
执行项目测试。
:param targets: 目标测试路径列表
:param keyword: pytest `-k` 过滤表达式
:param maxfail: 最大失败数,0 表示不限制
:param quiet: 是否启用简洁输出
:return: 测试执行结果
"""
if not self.tooling_support.is_pytest_available():
return {
'ok': False,
'message': 'pytest 未安装,无法执行 dev test',
'hint': '请在当前环境安装 pytest 后重试',
'exit_code': DEPENDENCY_ERROR,
}
normalized_targets = self.tooling_support.resolve_targets(targets)
command = self.command_support.build_pytest_command(
normalized_targets,
keyword=keyword,
maxfail=maxfail,
quiet=quiet,
)
test_result = self.process_gateway.run_command(command)
if not test_result.get('ok', False):
return {
'ok': False,
'message': '测试执行失败',
'targets': normalized_targets,
'keyword': keyword,
'maxfail': maxfail,
'quiet': quiet,
'test': test_result,
'exit_code': RUNTIME_ERROR,
}
return {
'ok': True,
'message': '测试执行完成',
'targets': normalized_targets,
'keyword': keyword,
'maxfail': maxfail,
'quiet': quiet,
'test': test_result,
}
DEVELOPMENT_RUNTIME = DevelopmentRuntimeService()
@@ -0,0 +1,118 @@
import importlib.util
from cli.runtime.base import RuntimeEnvironmentService
class DevelopmentToolingSupport:
"""
开发工具支持对象。
该对象负责目标列表规整与开发依赖可用性检查,
避免主运行时服务继续承载轻量工具规则。
"""
@staticmethod
def resolve_targets(targets: list[str] | None) -> list[str]:
"""
规范化命令目标列表。
:param targets: 原始目标列表
:return: 规范化后的目标列表
"""
normalized_targets = [target.strip() for target in (targets or []) if target.strip()]
return normalized_targets or ['.']
@staticmethod
def is_pytest_available() -> bool:
"""
检查 pytest 是否已安装。
:return: pytest 是否可用
"""
return importlib.util.find_spec('pytest') is not None
class DevelopmentCommandSupport:
"""
开发命令构建支持对象。
该对象负责开发工具命令参数拼装,
避免主运行时服务继续承载命令行桥接细节。
:param runtime_environment: 运行时环境服务
"""
def __init__(self, runtime_environment: RuntimeEnvironmentService) -> None:
"""
初始化开发命令构建支持对象。
:param runtime_environment: 运行时环境服务
:return: None
"""
self.runtime_environment = runtime_environment
def build_format_command(self, normalized_targets: list[str], *, check_only: bool) -> list[str]:
"""
构建 Ruff format 命令。
:param normalized_targets: 已规整目标列表
:param check_only: 是否仅检查
:return: 格式化命令参数列表
"""
command = [self.runtime_environment.get_python_executable(), '-m', 'ruff', 'format']
if check_only:
command.append('--check')
command.extend(normalized_targets)
return command
def build_check_command(
self,
normalized_targets: list[str],
*,
check_only: bool,
fix: bool,
unsafe_fixes: bool,
) -> list[str]:
"""
构建 Ruff check 命令。
:param normalized_targets: 已规整目标列表
:param check_only: 是否仅检查
:param fix: 是否允许自动修复
:param unsafe_fixes: 是否允许不安全修复
:return: 检查命令参数列表
"""
command = [self.runtime_environment.get_python_executable(), '-m', 'ruff', 'check']
if fix and not check_only:
command.append('--fix')
if unsafe_fixes:
command.append('--unsafe-fixes')
command.extend(normalized_targets)
return command
def build_pytest_command(
self,
normalized_targets: list[str],
*,
keyword: str,
maxfail: int,
quiet: bool,
) -> list[str]:
"""
构建 pytest 命令。
:param normalized_targets: 已规整目标列表
:param keyword: pytest `-k` 表达式
:param maxfail: 最大失败数
:param quiet: 是否启用简洁输出
:return: pytest 命令参数列表
"""
command = [self.runtime_environment.get_python_executable(), '-m', 'pytest']
if quiet:
command.append('-q')
if keyword:
command.extend(['-k', keyword])
if maxfail > 0:
command.append(f'--maxfail={maxfail}')
command.extend(normalized_targets)
return command
@@ -0,0 +1,3 @@
from .service import GEN_RUNTIME, GenRuntimeService
__all__ = ['GEN_RUNTIME', 'GenRuntimeService']
@@ -0,0 +1,110 @@
from importlib import import_module
from typing import Any
class GenInfrastructureGateway:
"""
代码生成基础设施网关。
该对象负责延迟加载 sqlglot、数据库会话、分页模型、配置对象、
业务异常和代码生成服务依赖,供代码生成运行时 facade 与其协作对象统一复用。
"""
@staticmethod
def get_sqlglot_module() -> Any:
"""
获取 `sqlglot` 模块。
:return: `sqlglot` 模块
"""
return import_module('sqlglot')
@staticmethod
def get_sqlglot_expressions_module() -> Any:
"""
获取 `sqlglot.expressions` 模块。
:return: `sqlglot.expressions` 模块
"""
return import_module('sqlglot.expressions')
@staticmethod
def get_async_session_local() -> Any:
"""
获取异步数据库会话工厂。
:return: 异步数据库会话工厂
"""
return import_module('config.database').AsyncSessionLocal
@staticmethod
def get_page_model() -> Any:
"""
获取分页模型类型。
:return: 分页模型类型
"""
return import_module('common.vo').PageModel
@staticmethod
def get_database_config() -> Any:
"""
获取数据库配置对象。
:return: 数据库配置对象
"""
return import_module('config.env').DataBaseConfig
@staticmethod
def get_gen_config() -> Any:
"""
获取代码生成配置对象。
:return: 代码生成配置对象
"""
return import_module('config.env').GenConfig
@staticmethod
def get_service_exception_class() -> type[Exception]:
"""
获取业务异常类型。
:return: 业务异常类型
"""
return import_module('exceptions.exception').ServiceException
@staticmethod
def get_user_vo_module() -> Any:
"""
获取用户 VO 模块。
:return: 用户 VO 模块
"""
return import_module('module_admin.entity.vo.user_vo')
@staticmethod
def get_gen_vo_module() -> Any:
"""
获取代码生成 VO 模块。
:return: 代码生成 VO 模块
"""
return import_module('module_generator.entity.vo.gen_vo')
@staticmethod
def get_gen_table_service() -> Any:
"""
获取代码生成服务类。
:return: 代码生成服务类
"""
return import_module('module_generator.service.gen_service').GenTableService
@staticmethod
def get_gen_table_column_service() -> Any:
"""
获取代码生成字段服务类。
:return: 代码生成字段服务类
"""
return import_module('module_generator.service.gen_service').GenTableColumnService
@@ -0,0 +1,412 @@
from typing import Any
from cli.exit_codes import ARGUMENT_ERROR, DATABASE_ERROR, RUNTIME_ERROR
from .gateway import GenInfrastructureGateway
from .support import GenDomainSupport
class GenRuntimeService:
"""
代码生成运行时服务。
该服务作为代码生成运行时 facade,对外统一暴露业务表、数据库物理表、
建表 SQL、代码预览、导出与数据库同步等入口。
:param infrastructure_gateway: 代码生成基础设施网关
:param domain_support: 代码生成领域支持对象
"""
def __init__(
self,
*,
infrastructure_gateway: GenInfrastructureGateway | None = None,
domain_support: GenDomainSupport | None = None,
) -> None:
"""
初始化代码生成运行时服务。
:param infrastructure_gateway: 代码生成基础设施网关
:param domain_support: 代码生成领域支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway or GenInfrastructureGateway()
self.domain_support = domain_support or GenDomainSupport(self.infrastructure_gateway)
async def import_tables(self, table_names: list[str], *, dry_run: bool = False) -> dict[str, Any]:
"""
导入数据库表到代码生成业务表。
:param table_names: 待导入表名列表
:param dry_run: 是否仅演练执行
:return: 导入结果
"""
normalized_table_names = self.domain_support.normalize_table_names(table_names)
if not normalized_table_names:
return {'ok': False, 'message': '至少需要传入一个表名', 'exit_code': ARGUMENT_ERROR}
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
service_exception = self.infrastructure_gateway.get_service_exception_class()
try:
async with async_session_local() as session:
gen_table_list = await gen_table_service.get_gen_db_table_list_by_name_services(
session,
normalized_table_names,
)
matched_table_names = [gen_table.table_name for gen_table in gen_table_list if gen_table.table_name]
missing_table_names = [
table_name for table_name in normalized_table_names if table_name not in matched_table_names
]
if dry_run:
return {
'ok': True,
'message': '导入表结构演练完成,未执行实际写入',
'dryRun': True,
'requestedTables': normalized_table_names,
'matchedTables': matched_table_names,
'missingTables': missing_table_names,
}
result = await gen_table_service.import_gen_table_services(
session,
gen_table_list,
self.domain_support.build_cli_current_user(),
)
except service_exception as exc:
return {'ok': False, 'message': '导入表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
except Exception as exc:
return {'ok': False, 'message': '导入表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
return {
'ok': bool(result.is_success),
'message': result.message,
'requestedTables': normalized_table_names,
}
async def list_gen_tables(
self,
*,
table_name: str = '',
table_comment: str = '',
paged: bool = False,
page_num: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""
查询代码生成业务表列表。
:param table_name: 表名称过滤条件
:param table_comment: 表描述过滤条件
:param paged: 是否启用分页
:param page_num: 页码
:param page_size: 每页数量
:return: 查询结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
try:
gen_vo_module = self.infrastructure_gateway.get_gen_vo_module()
query_model = gen_vo_module.GenTablePageQueryModel(
tableName=table_name or None,
tableComment=table_comment or None,
pageNum=page_num,
pageSize=page_size,
)
async with async_session_local() as session:
result = await gen_table_service.get_gen_table_list_services(session, query_model, is_page=paged)
except Exception as exc:
return {
'ok': False,
'message': '读取代码生成业务表列表失败',
'error': str(exc),
'exit_code': DATABASE_ERROR,
}
filters = {
'tableName': table_name,
'tableComment': table_comment,
'paged': paged,
'pageNum': page_num,
'pageSize': page_size,
}
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
async def list_gen_db_tables(
self,
*,
table_name: str = '',
table_comment: str = '',
paged: bool = False,
page_num: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""
查询数据库中可导入的物理表列表。
:param table_name: 表名称过滤条件
:param table_comment: 表描述过滤条件
:param paged: 是否启用分页
:param page_num: 页码
:param page_size: 每页数量
:return: 查询结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
try:
gen_vo_module = self.infrastructure_gateway.get_gen_vo_module()
query_model = gen_vo_module.GenTablePageQueryModel(
tableName=table_name or None,
tableComment=table_comment or None,
pageNum=page_num,
pageSize=page_size,
)
async with async_session_local() as session:
result = await gen_table_service.get_gen_db_table_list_services(session, query_model, is_page=paged)
except Exception as exc:
return {'ok': False, 'message': '读取数据库表列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
filters = {
'tableName': table_name,
'tableComment': table_comment,
'paged': paged,
'pageNum': page_num,
'pageSize': page_size,
}
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
async def create_tables(self, sql: str, sql_file: str, *, dry_run: bool = False) -> dict[str, Any]:
"""
根据 SQL 创建表并导入代码生成业务表。
:param sql: 直接传入的 SQL 文本
:param sql_file: SQL 文件路径
:param dry_run: 是否仅演练执行
:return: 创建结果
"""
try:
sql_text = self.domain_support.resolve_sql_text(sql, sql_file)
sql_statements, table_names = self.domain_support.parse_create_table_sql(sql_text)
except ValueError as exc:
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': ARGUMENT_ERROR}
except Exception as exc:
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
if dry_run:
return {
'ok': True,
'message': '建表语句演练完成,未执行实际建表',
'dryRun': True,
'statementCount': len(sql_statements),
'tableNames': table_names,
'sql': sql_text,
}
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
service_exception = self.infrastructure_gateway.get_service_exception_class()
try:
async with async_session_local() as session:
result = await gen_table_service.create_table_services(
session,
sql_text,
self.domain_support.build_cli_current_user(),
)
except service_exception as exc:
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
except Exception as exc:
return {'ok': False, 'message': '创建表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
return {
'ok': bool(result.is_success),
'message': result.message,
'tableNames': table_names,
}
async def preview_code(self, table_id: int) -> dict[str, Any]:
"""
预览指定业务表的代码生成结果。
:param table_id: 业务表 ID
:return: 预览结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
service_exception = self.infrastructure_gateway.get_service_exception_class()
try:
async with async_session_local() as session:
preview_payload = await gen_table_service.preview_code_services(session, table_id)
except service_exception as exc:
return {'ok': False, 'message': '预览代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
except Exception as exc:
return {'ok': False, 'message': '预览代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
return {
'ok': True,
'tableId': table_id,
'templateCount': len(preview_payload),
'preview': preview_payload,
}
async def get_gen_table_detail(self, table_id: int) -> dict[str, Any]:
"""
读取单个代码生成业务表详情。
:param table_id: 业务表 ID
:return: 详情结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
gen_table_column_service = self.infrastructure_gateway.get_gen_table_column_service()
try:
async with async_session_local() as session:
info = await gen_table_service.get_gen_table_by_id_services(session, table_id)
if not getattr(info, 'table_id', None):
return {
'ok': False,
'message': f'代码生成业务表不存在:{table_id}',
'tableId': table_id,
'exit_code': RUNTIME_ERROR,
}
rows = await gen_table_column_service.get_gen_table_column_list_by_table_id_services(session, table_id)
tables = await gen_table_service.get_gen_table_all_services(session)
except Exception as exc:
return {
'ok': False,
'message': '读取代码生成业务表详情失败',
'error': str(exc),
'exit_code': DATABASE_ERROR,
}
detail_payload = {
'info': self.domain_support.serialize_gen_item(info),
'rows': self.domain_support.serialize_gen_items(rows),
'tables': self.domain_support.serialize_gen_items(tables),
}
return {
'ok': True,
'tableId': table_id,
'tableName': detail_payload['info'].get('tableName', ''),
'columnCount': len(detail_payload['rows']),
'tableCount': len(detail_payload['tables']),
'detail': detail_payload,
}
async def export_code(
self,
table_names: list[str],
*,
mode: str = 'zip',
output_file: str = '',
dry_run: bool = False,
) -> dict[str, Any]:
"""
导出代码生成结果。
:param table_names: 业务表名称列表
:param mode: 导出模式,支持 `zip` 与 `local`
:param output_file: zip 导出目标文件路径
:param dry_run: 是否仅演练执行
:return: 导出结果
"""
normalized_table_names = self.domain_support.normalize_table_names(table_names)
if not normalized_table_names:
return {'ok': False, 'message': '至少需要传入一个表名', 'exit_code': ARGUMENT_ERROR}
if mode not in {'zip', 'local'}:
return {'ok': False, 'message': '导出模式仅支持 zip 或 local', 'exit_code': ARGUMENT_ERROR}
gen_config = self.infrastructure_gateway.get_gen_config()
if mode == 'local' and not gen_config.allow_overwrite:
return {
'ok': False,
'message': '当前系统配置不允许生成文件覆盖到本地',
'hint': '请检查 GenConfig.allow_overwrite 配置',
'exit_code': RUNTIME_ERROR,
}
if dry_run:
dry_run_payload: dict[str, Any] = {
'ok': True,
'message': '代码导出演练完成,未执行实际导出',
'dryRun': True,
'mode': mode,
'tableNames': normalized_table_names,
}
if mode == 'zip':
target_file = output_file.strip() or f'gen_code_{"_".join(normalized_table_names)}.zip'
dry_run_payload['outputFile'] = self.domain_support.resolve_output_file_path(target_file)
else:
dry_run_payload['genPath'] = gen_config.GEN_PATH
return dry_run_payload
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
service_exception = self.infrastructure_gateway.get_service_exception_class()
try:
async with async_session_local() as session:
if mode == 'zip':
zip_bytes = await gen_table_service.batch_gen_code_services(session, normalized_table_names)
else:
messages = []
for table_name in normalized_table_names:
result = await gen_table_service.generate_code_services(session, table_name)
messages.append({'tableName': table_name, 'message': result.message, 'ok': result.is_success})
return {
'ok': True,
'message': '代码已生成到本地目录',
'mode': mode,
'tableNames': normalized_table_names,
'genPath': gen_config.GEN_PATH,
'results': messages,
}
except service_exception as exc:
return {'ok': False, 'message': '导出代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
except Exception as exc:
return {'ok': False, 'message': '导出代码失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
target_file = output_file.strip() or f'gen_code_{"_".join(normalized_table_names)}.zip'
try:
target_path = self.domain_support.write_export_zip(target_file, zip_bytes)
except Exception as exc:
return {'ok': False, 'message': '写出导出文件失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
return {
'ok': True,
'message': '代码压缩包导出完成',
'mode': mode,
'tableNames': normalized_table_names,
'outputFile': target_path,
'size': len(zip_bytes),
}
async def sync_gen_table_from_db(self, table_name: str) -> dict[str, Any]:
"""
将指定代码生成业务表与数据库表结构进行同步。
:param table_name: 业务表名称
:return: 同步结果
"""
normalized_table_name = table_name.strip()
if not normalized_table_name:
return {'ok': False, 'message': '表名不能为空', 'exit_code': ARGUMENT_ERROR}
async_session_local = self.infrastructure_gateway.get_async_session_local()
gen_table_service = self.infrastructure_gateway.get_gen_table_service()
service_exception = self.infrastructure_gateway.get_service_exception_class()
try:
async with async_session_local() as session:
result = await gen_table_service.sync_db_services(session, normalized_table_name)
except service_exception as exc:
return {'ok': False, 'message': '同步数据库表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
except Exception as exc:
return {'ok': False, 'message': '同步数据库表结构失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
return {
'ok': bool(result.is_success),
'message': result.message,
'tableName': normalized_table_name,
}
GEN_RUNTIME = GenRuntimeService()
@@ -0,0 +1,191 @@
from pathlib import Path
from typing import Any
from cli.runtime.base import RUNTIME_OPERATOR, RuntimeOperatorService
from .gateway import GenInfrastructureGateway
class GenDomainSupport:
"""
代码生成领域支持对象。
该对象负责 CLI 当前用户构建、表名规整、记录序列化、SQL 解析
以及导出路径处理,避免主运行时服务继续承载过多局部规则。
:param infrastructure_gateway: 代码生成基础设施网关
"""
def __init__(
self,
infrastructure_gateway: GenInfrastructureGateway,
operator_service: RuntimeOperatorService = RUNTIME_OPERATOR,
) -> None:
"""
初始化代码生成领域支持对象。
:param infrastructure_gateway: 代码生成基础设施网关
:param operator_service: 运行时操作者解析服务
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
self.operator_service = operator_service
def build_cli_current_user(self) -> Any:
"""
构建 CLI 场景使用的最小当前用户模型。
:return: CLI 当前用户模型
"""
operator = self.operator_service.resolve_operator()
user_vo_module = self.infrastructure_gateway.get_user_vo_module()
return user_vo_module.CurrentUserModel(
permissions=[],
roles=['admin'],
user=user_vo_module.UserInfoModel(
user_id=1,
user_name=operator,
nick_name=operator,
user_type='00',
status='0',
del_flag='0',
),
)
@staticmethod
def normalize_table_names(table_names: list[str]) -> list[str]:
"""
规范化表名列表。
:param table_names: 原始表名列表
:return: 规范化后的表名列表
"""
return [table_name.strip() for table_name in table_names if table_name.strip()]
@staticmethod
def serialize_gen_item(item: Any) -> dict[str, Any]:
"""
序列化单个代码生成表记录。
:param item: 原始记录对象
:return: 可输出的字典
"""
if hasattr(item, 'model_dump'):
return dict(item.model_dump(by_alias=True, exclude_none=True))
return dict(item)
def serialize_gen_items(self, items: list[Any]) -> list[dict[str, Any]]:
"""
序列化代码生成表记录列表。
:param items: 原始记录列表
:return: 序列化后的字典列表
"""
return [self.serialize_gen_item(item) for item in items]
def parse_create_table_sql(self, sql: str) -> tuple[list[Any], list[str]]:
"""
解析建表 SQL 并提取建表语句信息。
:param sql: 原始 SQL 文本
:return: SQL AST 列表与建表表名列表
:raises ValueError: SQL 非法时抛出异常
"""
sqlglot_module = self.infrastructure_gateway.get_sqlglot_module()
expressions_module = self.infrastructure_gateway.get_sqlglot_expressions_module()
database_config = self.infrastructure_gateway.get_database_config()
sql_statements = sqlglot_module.parse(sql, dialect=database_config.sqlglot_parse_dialect)
has_create = any(isinstance(sql_statement, expressions_module.Create) for sql_statement in sql_statements)
has_forbidden_keyword = any(
isinstance(
sql_statement,
(
expressions_module.Add,
expressions_module.Alter,
expressions_module.Delete,
expressions_module.Drop,
expressions_module.Insert,
expressions_module.TruncateTable,
expressions_module.Update,
),
)
for sql_statement in sql_statements
)
if not has_create or has_forbidden_keyword:
raise ValueError('建表语句不合法,仅允许 CREATE TABLE 语句')
table_names = [
sql_statement.find(expressions_module.Table).name
for sql_statement in sql_statements
if isinstance(sql_statement, expressions_module.Create)
]
if not table_names:
raise ValueError('未解析到建表表名')
return sql_statements, table_names
@staticmethod
def resolve_sql_text(sql: str, sql_file: str) -> str:
"""
解析命令输入中的 SQL 文本。
:param sql: 直接传入的 SQL 文本
:param sql_file: SQL 文件路径
:return: 最终 SQL 文本
:raises ValueError: 参数非法时抛出异常
"""
if bool(sql.strip()) == bool(sql_file.strip()):
raise ValueError('必须且只能传入 --sql 或 --sql-file 其中一种方式')
if sql.strip():
return sql.strip()
sql_path = Path(sql_file).expanduser().resolve()
if not sql_path.is_file():
raise ValueError(f'SQL 文件不存在:{sql_path}')
return sql_path.read_text(encoding='utf-8').strip()
@staticmethod
def resolve_output_file_path(target_file: str) -> str:
"""
解析导出文件绝对路径。
:param target_file: 原始导出文件路径
:return: 绝对路径字符串
"""
return str(Path(target_file).expanduser().resolve())
@staticmethod
def write_export_zip(target_file: str, zip_bytes: bytes) -> str:
"""
将导出的 zip 数据写入目标文件。
:param target_file: 目标文件路径
:param zip_bytes: zip 二进制内容
:return: 实际写入的绝对路径
"""
target_path = Path(target_file).expanduser().resolve()
target_path.parent.mkdir(parents=True, exist_ok=True)
target_path.write_bytes(zip_bytes)
return str(target_path)
def build_list_payload(
self,
result: Any,
*,
filters: dict[str, Any],
paged: bool,
) -> dict[str, Any]:
"""
统一构建代码生成列表返回结构。
:param result: 原始结果对象
:param filters: 查询过滤条件
:param paged: 是否分页
:return: 可输出结果
"""
if paged and isinstance(result, self.infrastructure_gateway.get_page_model()):
page_payload = result.model_dump(by_alias=True)
page_payload['rows'] = self.serialize_gen_items(page_payload.get('rows', []))
return {'ok': True, 'filters': filters, 'page': page_payload}
items = self.serialize_gen_items(result)
return {'ok': True, 'filters': filters, 'count': len(items), 'items': items}
@@ -0,0 +1,3 @@
from .service import JOB_RUNTIME, JobRuntimeService
__all__ = ['JOB_RUNTIME', 'JobRuntimeService']
@@ -0,0 +1,74 @@
from importlib import import_module
from typing import Any
class JobInfrastructureGateway:
"""
定时任务基础设施网关。
该对象负责延迟加载数据库会话、Redis、调度器和任务服务依赖,
供定时任务运行时 facade 与其协作对象统一复用。
"""
@staticmethod
def get_page_model() -> Any:
"""
获取分页模型类型。
:return: 分页模型类型
"""
return import_module('common.vo').PageModel
@staticmethod
def get_async_session_local() -> Any:
"""
获取异步数据库会话工厂。
:return: 异步数据库会话工厂
"""
return import_module('config.database').AsyncSessionLocal
@staticmethod
def get_redis_util() -> Any:
"""
获取 Redis 工具类。
:return: Redis 工具类
"""
return import_module('config.get_redis').RedisUtil
@staticmethod
def get_scheduler_util() -> Any:
"""
获取调度器工具类。
:return: 调度器工具类
"""
return import_module('config.get_scheduler').SchedulerUtil
@staticmethod
def get_job_vo_module() -> Any:
"""
获取定时任务 VO 模块。
:return: 定时任务 VO 模块
"""
return import_module('module_admin.entity.vo.job_vo')
@staticmethod
def get_job_service() -> Any:
"""
获取定时任务服务类。
:return: 定时任务服务类
"""
return import_module('module_admin.service.job_service').JobService
@staticmethod
def get_job_log_service() -> Any:
"""
获取定时任务日志服务类。
:return: 定时任务日志服务类
"""
return import_module('module_admin.service.job_log_service').JobLogService
@@ -0,0 +1,246 @@
from typing import Any
from cli.exit_codes import DATABASE_ERROR, RUNTIME_ERROR
from .gateway import JobInfrastructureGateway
from .support import JobDomainSupport, JobSchedulerSupport
class JobRuntimeService:
"""
定时任务运行时服务。
该服务作为定时任务运行时 facade,对外统一暴露任务列表、详情、日志、
执行一次、暂停与恢复入口。
:param infrastructure_gateway: 定时任务基础设施网关
:param domain_support: 定时任务领域支持对象
:param scheduler_support: 定时任务调度上下文支持对象
"""
def __init__(
self,
*,
infrastructure_gateway: JobInfrastructureGateway | None = None,
domain_support: JobDomainSupport | None = None,
scheduler_support: JobSchedulerSupport | None = None,
) -> None:
"""
初始化定时任务运行时服务。
:param infrastructure_gateway: 定时任务基础设施网关
:param domain_support: 定时任务领域支持对象
:param scheduler_support: 定时任务调度上下文支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway or JobInfrastructureGateway()
self.domain_support = domain_support or JobDomainSupport(self.infrastructure_gateway)
self.scheduler_support = scheduler_support or JobSchedulerSupport(self.infrastructure_gateway)
async def list_jobs(
self,
*,
job_name: str = '',
job_group: str = '',
status: str | None = None,
paged: bool = False,
page_num: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""
查询定时任务列表。
:param job_name: 任务名称过滤条件
:param job_group: 任务组过滤条件
:param status: 状态过滤条件
:param paged: 是否启用分页
:param page_num: 页码
:param page_size: 每页数量
:return: 定时任务列表结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
job_vo_module = self.infrastructure_gateway.get_job_vo_module()
job_service = self.infrastructure_gateway.get_job_service()
query_model = job_vo_module.JobPageQueryModel(
jobName=job_name or None,
jobGroup=job_group or None,
status=status,
pageNum=page_num,
pageSize=page_size,
)
try:
async with async_session_local() as session:
result = await job_service.get_job_list_services(session, query_model, is_page=paged)
except Exception as exc:
return {'ok': False, 'message': '读取定时任务列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
filters = self.domain_support.build_filters(
job_name=job_name,
job_group=job_group,
status=status,
paged=paged,
page_num=page_num,
page_size=page_size,
)
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
async def get_job_detail(self, job_id: int) -> dict[str, Any]:
"""
读取单个定时任务详情。
:param job_id: 任务 ID
:return: 定时任务详情结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
job_service = self.infrastructure_gateway.get_job_service()
try:
async with async_session_local() as session:
job_model = await job_service.job_detail_services(session, job_id)
except Exception as exc:
return {'ok': False, 'message': '读取定时任务详情失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
job_payload = self.domain_support.serialize_job_item(job_model)
if not job_payload.get('jobId'):
return {
'ok': False,
'message': f'定时任务不存在:{job_id}',
'jobId': job_id,
'exit_code': RUNTIME_ERROR,
}
return {'ok': True, 'job': job_payload}
async def list_job_logs(
self,
*,
job_name: str = '',
job_group: str = '',
status: str | None = None,
begin_date: str = '',
end_date: str = '',
paged: bool = False,
page_num: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""
查询定时任务执行日志列表。
:param job_name: 任务名称过滤条件
:param job_group: 任务组过滤条件
:param status: 执行状态过滤条件
:param begin_date: 开始日期,格式 `YYYY-MM-DD`
:param end_date: 结束日期,格式 `YYYY-MM-DD`
:param paged: 是否启用分页
:param page_num: 页码
:param page_size: 每页数量
:return: 定时任务日志列表结果
"""
async_session_local = self.infrastructure_gateway.get_async_session_local()
job_log_service = self.infrastructure_gateway.get_job_log_service()
job_vo_module = self.infrastructure_gateway.get_job_vo_module()
query_model = job_vo_module.JobLogPageQueryModel(
jobName=job_name or None,
jobGroup=job_group or None,
status=status,
beginTime=begin_date or None,
endTime=end_date or None,
pageNum=page_num,
pageSize=page_size,
)
try:
async with async_session_local() as session:
result = await job_log_service.get_job_log_list_services(session, query_model, is_page=paged)
except Exception as exc:
return {'ok': False, 'message': '读取定时任务日志列表失败', 'error': str(exc), 'exit_code': DATABASE_ERROR}
filters = self.domain_support.build_filters(
job_name=job_name,
job_group=job_group,
status=status,
begin_date=begin_date,
end_date=end_date,
paged=paged,
page_num=page_num,
page_size=page_size,
)
return self.domain_support.build_list_payload(result, filters=filters, paged=paged)
async def run_with_scheduler_context(
self,
operation: str,
job_id: int,
*,
status: str | None = None,
) -> dict[str, Any]:
"""
在调度器上下文中执行任务操作。
:param operation: 操作名称
:param job_id: 任务ID
:param status: 目标状态
:return: 任务操作结果
"""
operation_metadata = self.domain_support.build_job_operation_metadata(operation)
redis = None
async_session_local = self.infrastructure_gateway.get_async_session_local()
redis_util = self.infrastructure_gateway.get_redis_util()
scheduler_util = self.infrastructure_gateway.get_scheduler_util()
job_vo_module = self.infrastructure_gateway.get_job_vo_module()
job_service = self.infrastructure_gateway.get_job_service()
try:
async with async_session_local() as session:
redis = await redis_util.create_redis_pool(log_enabled=False)
await scheduler_util.init_system_scheduler(redis)
if operation == 'run-once':
result = await job_service.execute_job_once_services(session, job_vo_module.JobModel(jobId=job_id))
elif operation in {'pause', 'resume'}:
result = await job_service.edit_job_services(
session,
job_vo_module.EditJobModel(jobId=job_id, status=status, type='status'),
)
else:
raise ValueError(f'不支持的任务操作:{operation}')
except Exception as exc:
return {'ok': False, 'message': '执行定时任务操作失败', 'error': str(exc), 'exit_code': 22}
finally:
await self.scheduler_support.close_scheduler_context(redis)
return {
'ok': bool(result.is_success),
'jobId': job_id,
'operation': operation,
'operationLabel': operation_metadata['operationLabel'],
'targetStatus': status,
'message': operation_metadata['successMessage'] if result.is_success else '定时任务操作执行失败',
'serviceMessage': result.message,
}
async def run_job_once(self, job_id: int) -> dict[str, Any]:
"""
执行一次指定定时任务。
:param job_id: 任务ID
:return: 执行结果
"""
return await self.run_with_scheduler_context('run-once', job_id)
async def pause_job(self, job_id: int) -> dict[str, Any]:
"""
暂停指定定时任务。
:param job_id: 任务ID
:return: 暂停结果
"""
return await self.run_with_scheduler_context('pause', job_id, status='1')
async def resume_job(self, job_id: int) -> dict[str, Any]:
"""
恢复指定定时任务。
:param job_id: 任务ID
:return: 恢复结果
"""
return await self.run_with_scheduler_context('resume', job_id, status='0')
JOB_RUNTIME = JobRuntimeService()
@@ -0,0 +1,173 @@
from typing import Any
from .gateway import JobInfrastructureGateway
class JobDomainSupport:
"""
定时任务领域支持对象。
该对象负责任务序列化、分页结果规整和任务操作元数据定义,
避免主运行时服务继续承载细碎领域规则。
:param infrastructure_gateway: 定时任务基础设施网关
"""
def __init__(self, infrastructure_gateway: JobInfrastructureGateway) -> None:
"""
初始化定时任务领域支持对象。
:param infrastructure_gateway: 定时任务基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
@staticmethod
def serialize_job_item(job_item: Any) -> dict[str, Any]:
"""
序列化单个定时任务模型。
:param job_item: 原始定时任务模型或字典
:return: 可输出的定时任务字典
"""
if hasattr(job_item, 'model_dump'):
return dict(job_item.model_dump(by_alias=True, exclude_none=True))
return dict(job_item)
def serialize_job_items(self, job_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
"""
序列化任务列表结果。
:param job_items: 原始任务列表
:return: 可输出的任务列表
"""
return [self.serialize_job_item(item) for item in job_items]
@staticmethod
def build_job_operation_metadata(operation: str) -> dict[str, str]:
"""
构建任务操作的统一元数据。
:param operation: 操作名称
:return: 操作元数据字典
:raises ValueError: 操作名称不受支持时抛出异常
"""
metadata_mapping = {
'run-once': {
'operationLabel': '执行一次任务',
'successMessage': '定时任务已触发一次执行',
},
'pause': {
'operationLabel': '暂停任务',
'successMessage': '定时任务已暂停',
},
'resume': {
'operationLabel': '恢复任务',
'successMessage': '定时任务已恢复',
},
}
if operation not in metadata_mapping:
raise ValueError(f'不支持的任务操作:{operation}')
return metadata_mapping[operation]
def build_filters(
self,
*,
job_name: str = '',
job_group: str = '',
status: str | None = None,
begin_date: str = '',
end_date: str = '',
paged: bool = False,
page_num: int = 1,
page_size: int = 20,
) -> dict[str, Any]:
"""
构建任务列表或日志查询过滤条件。
:param job_name: 任务名称
:param job_group: 任务分组
:param status: 状态
:param begin_date: 开始日期
:param end_date: 结束日期
:param paged: 是否分页
:param page_num: 页码
:param page_size: 每页数量
:return: 过滤条件字典
"""
filters = {
'jobName': job_name,
'jobGroup': job_group,
'status': status,
'paged': paged,
'pageNum': page_num,
'pageSize': page_size,
}
if begin_date or end_date:
filters['beginDate'] = begin_date
filters['endDate'] = end_date
return filters
def build_list_payload(
self,
result: Any,
*,
filters: dict[str, Any],
paged: bool,
) -> dict[str, Any]:
"""
统一构建任务列表或日志列表返回结构。
:param result: 原始结果对象
:param filters: 查询过滤条件
:param paged: 是否分页
:return: 可输出结果
"""
if paged and isinstance(result, self.infrastructure_gateway.get_page_model()):
page_payload = result.model_dump(by_alias=True)
page_payload['rows'] = self.serialize_job_items(page_payload.get('rows', []))
return {'ok': True, 'filters': filters, 'page': page_payload}
items = self.serialize_job_items(result)
return {'ok': True, 'filters': filters, 'count': len(items), 'items': items}
class JobSchedulerSupport:
"""
定时任务调度上下文支持对象。
该对象负责统一关闭调度器上下文资源,避免调度型操作在 facade 中
持续堆叠资源清理细节。
:param infrastructure_gateway: 定时任务基础设施网关
"""
def __init__(self, infrastructure_gateway: JobInfrastructureGateway) -> None:
"""
初始化定时任务调度上下文支持对象。
:param infrastructure_gateway: 定时任务基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
async def close_scheduler_context(self, redis: Any) -> None:
"""
安全关闭任务调度相关资源。
:param redis: Redis 客户端实例
:return: None
"""
scheduler_util = self.infrastructure_gateway.get_scheduler_util()
try:
await scheduler_util.close_system_scheduler()
except Exception:
pass
if redis is None:
return
try:
await redis.close()
except Exception:
pass
@@ -0,0 +1,3 @@
from .service import OPERATIONS_RUNTIME, OperationsRuntimeService
__all__ = ['OPERATIONS_RUNTIME', 'OperationsRuntimeService']
@@ -0,0 +1,65 @@
from importlib import import_module
from typing import Any
class OperationsInfrastructureGateway:
"""
运维基础设施网关。
该对象负责延迟加载 Redis、调度器、服务器监控与系统工具依赖,
供运维运行时 facade 和其协作对象统一复用。
"""
@staticmethod
def get_redis_util() -> Any:
"""
获取 Redis 工具类。
:return: Redis 工具类
"""
return import_module('config.get_redis').RedisUtil
@staticmethod
def get_redis_error_class() -> type[Exception]:
"""
获取 Redis 异常类型。
:return: Redis 异常类型
"""
return import_module('redis.exceptions').RedisError
@staticmethod
def get_scheduler_util() -> Any:
"""
获取调度器工具类。
:return: 调度器工具类
"""
return import_module('config.get_scheduler').SchedulerUtil
@staticmethod
def get_server_service() -> Any:
"""
获取服务器监控服务类。
:return: 服务器监控服务类
"""
return import_module('module_admin.service.server_service').ServerService
@staticmethod
def get_psutil_module() -> Any:
"""
获取 `psutil` 模块。
:return: `psutil` 模块
"""
return import_module('psutil')
@staticmethod
def get_bytes2human() -> Any:
"""
获取字节数转可读文本函数。
:return: `bytes2human` 函数
"""
return import_module('utils.common_util').bytes2human
@@ -0,0 +1,132 @@
import socket
from typing import Any
from cli.exit_codes import REDIS_ERROR, RUNTIME_ERROR, SCHEDULER_ERROR
from .gateway import OperationsInfrastructureGateway
from .support import (
OperationsDependencyInspector,
OperationsServerInfoSupport,
)
class OperationsRuntimeService:
"""
运维运行时服务。
该服务作为运维运行时 facade,对外统一暴露依赖版本检查、Redis 探活、
调度同步以及服务器运行时信息采集入口。
:param infrastructure_gateway: 运维基础设施网关
:param dependency_inspector: 运维依赖检查器
:param server_info_support: 服务器信息支持对象
"""
def __init__(
self,
*,
infrastructure_gateway: OperationsInfrastructureGateway | None = None,
dependency_inspector: OperationsDependencyInspector | None = None,
server_info_support: OperationsServerInfoSupport | None = None,
) -> None:
"""
初始化运维运行时服务。
:param infrastructure_gateway: 运维基础设施网关
:param dependency_inspector: 运维依赖检查器
:param server_info_support: 服务器信息支持对象
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway or OperationsInfrastructureGateway()
self.dependency_inspector = dependency_inspector or OperationsDependencyInspector()
self.server_info_support = server_info_support or OperationsServerInfoSupport(self.infrastructure_gateway)
def get_dependency_versions(self, *, include_dev: bool = False) -> dict[str, Any]:
"""
读取 CLI 和后端运行所依赖的核心 Python 包版本。
:param include_dev: 是否附带开发阶段依赖
:return: 依赖检查结果
"""
return self.dependency_inspector.inspect(include_dev=include_dev)
async def ping_redis(self) -> dict[str, Any]:
"""
检查 Redis 连通性。
:return: Redis 检查结果
"""
redis_util = self.infrastructure_gateway.get_redis_util()
redis_error = self.infrastructure_gateway.get_redis_error_class()
redis = await redis_util.create_redis_pool(log_enabled=False)
try:
await redis.ping()
return {'ok': True, 'message': 'Redis连接成功'}
except redis_error as exc:
return {'ok': False, 'message': 'Redis连接失败', 'error': str(exc), 'exit_code': REDIS_ERROR}
finally:
await redis.close()
async def sync_jobs(self) -> dict[str, Any]:
"""
同步调度任务配置。
:return: 任务同步执行结果
"""
redis = None
scheduler_util = None
try:
redis_util = self.infrastructure_gateway.get_redis_util()
scheduler_util = self.infrastructure_gateway.get_scheduler_util()
redis = await redis_util.create_redis_pool(log_enabled=False)
await scheduler_util.init_system_scheduler(redis)
await scheduler_util.request_scheduler_sync()
return {
'ok': True,
'operation': 'sync',
'operationLabel': '同步调度配置',
'schedulerSyncRequested': True,
'message': '调度配置同步请求已发送',
'isLeader': scheduler_util._is_leader,
}
except Exception as exc:
return {
'ok': False,
'operation': 'sync',
'operationLabel': '同步调度配置',
'schedulerSyncRequested': False,
'message': '调度配置同步失败',
'error': str(exc),
'exit_code': SCHEDULER_ERROR,
}
finally:
if scheduler_util is not None:
try:
await scheduler_util.close_system_scheduler()
except Exception:
pass
if redis is not None:
try:
await redis.close()
except Exception:
pass
async def get_server_info(self) -> dict[str, Any]:
"""
获取服务器运行信息。
:return: 服务器运行信息字典
"""
try:
server_service = self.infrastructure_gateway.get_server_service()
return {'ok': True, 'server': (await server_service.get_server_monitor_info()).model_dump(by_alias=True)}
except socket.gaierror:
try:
return {'ok': True, 'server': self.server_info_support.build_server_info_fallback()}
except Exception as exc:
return {'ok': False, 'message': '读取服务器运行信息失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
except Exception as exc:
return {'ok': False, 'message': '读取服务器运行信息失败', 'error': str(exc), 'exit_code': RUNTIME_ERROR}
OPERATIONS_RUNTIME = OperationsRuntimeService()
@@ -0,0 +1,241 @@
import os
import platform
import socket
import time
from dataclasses import dataclass
from importlib import metadata
from typing import Any
from cli.exit_codes import RUNTIME_ERROR
from .gateway import OperationsInfrastructureGateway
@dataclass(frozen=True)
class OperationsDependencySpec:
"""
运维依赖检查项定义。
:param package_name: CLI 展示用包名
:param distribution: Python 分发名称
:param required: 是否为必需依赖
:param version: 显式版本,存在时跳过环境读取
"""
package_name: str
distribution: str
required: bool
version: str | None = None
class OperationsDependencyInspector:
"""
运维依赖检查器。
该对象负责核心依赖声明、版本探测与依赖检查结果构建,
使运行时 facade 不再直接维护细碎依赖定义。
"""
@staticmethod
def read_package_version(distribution_name: str) -> str | None:
"""
读取指定 Python 包的已安装版本号。
:param distribution_name: Python 包分发名称
:return: 已安装版本号,未安装时返回 `None`
"""
try:
return metadata.version(distribution_name)
except metadata.PackageNotFoundError:
return None
@staticmethod
def build_dependency_specs(*, include_dev: bool = False) -> list[OperationsDependencySpec]:
"""
构建当前依赖检查项列表。
:param include_dev: 是否附带开发阶段依赖
:return: 依赖检查项列表
"""
dependency_specs = [
OperationsDependencySpec('python', '', True, platform.python_version()),
OperationsDependencySpec('fastapi', 'fastapi', True),
OperationsDependencySpec('uvicorn', 'uvicorn', True),
OperationsDependencySpec('sqlalchemy', 'SQLAlchemy', True),
OperationsDependencySpec('alembic', 'alembic', True),
OperationsDependencySpec('redis', 'redis', True),
OperationsDependencySpec('pydantic', 'pydantic', True),
OperationsDependencySpec('pydantic-settings', 'pydantic-settings', True),
OperationsDependencySpec('typer', 'typer', True),
]
if include_dev:
dependency_specs.extend(
[
OperationsDependencySpec('pytest', 'pytest', False),
OperationsDependencySpec('ruff', 'ruff', False),
]
)
return dependency_specs
def inspect(self, *, include_dev: bool = False) -> dict[str, Any]:
"""
读取 CLI 和后端运行所依赖的核心 Python 包版本。
:param include_dev: 是否附带开发阶段依赖
:return: 依赖检查结果
"""
packages: dict[str, dict[str, Any]] = {}
missing_required: list[str] = []
for dependency_spec in self.build_dependency_specs(include_dev=include_dev):
version = dependency_spec.version or self.read_package_version(dependency_spec.distribution)
installed = bool(version)
package_payload = {
'installed': installed,
'version': version or '',
'required': dependency_spec.required,
}
if dependency_spec.distribution:
package_payload['distribution'] = dependency_spec.distribution
packages[dependency_spec.package_name] = package_payload
if dependency_spec.required and not installed:
missing_required.append(dependency_spec.package_name)
if missing_required:
return {
'ok': False,
'message': '存在缺失的核心运行依赖',
'missingRequired': missing_required,
'includeDev': include_dev,
'packages': packages,
'exit_code': RUNTIME_ERROR,
}
return {
'ok': True,
'message': '核心运行依赖已安装',
'missingRequired': [],
'includeDev': include_dev,
'packages': packages,
}
class OperationsServerInfoSupport:
"""
运维服务器信息支持对象。
该对象负责 CLI 兜底服务器信息构建与 IP 解析逻辑,
避免主运行时服务继续承载大量平台采集细节。
:param infrastructure_gateway: 运维基础设施网关
"""
def __init__(self, infrastructure_gateway: OperationsInfrastructureGateway) -> None:
"""
初始化服务器信息支持对象。
:param infrastructure_gateway: 运维基础设施网关
:return: None
"""
self.infrastructure_gateway = infrastructure_gateway
def resolve_server_ip(self, hostname: str) -> str:
"""
解析服务器 IP,优先使用主机名解析,失败时回退到网卡地址。
:param hostname: 当前主机名
:return: 可用的 IPv4 地址
"""
psutil = self.infrastructure_gateway.get_psutil_module()
try:
resolved_ip = socket.gethostbyname(hostname)
if resolved_ip:
return resolved_ip
except OSError:
pass
for interface_addresses in psutil.net_if_addrs().values():
for address_info in interface_addresses:
if (
address_info.family == socket.AF_INET
and address_info.address
and not address_info.address.startswith('127.')
):
return address_info.address
return '127.0.0.1'
def build_server_info_fallback(self) -> dict[str, Any]:
"""
构建 CLI 使用的服务器运行信息兜底数据。
:return: 与原服务输出结构兼容的服务器运行信息字典
"""
psutil = self.infrastructure_gateway.get_psutil_module()
bytes2human = self.infrastructure_gateway.get_bytes2human()
cpu_num = psutil.cpu_count(logical=True)
cpu_usage_percent = psutil.cpu_times_percent()
memory_info = psutil.virtual_memory()
hostname = socket.gethostname()
current_process = psutil.Process(os.getpid())
start_time_stamp = current_process.create_time()
current_time_stamp = time.time()
difference = current_time_stamp - start_time_stamp
days = int(difference // (24 * 60 * 60))
hours = int((difference % (24 * 60 * 60)) // (60 * 60))
minutes = int((difference % (60 * 60)) // 60)
process_memory_info = current_process.memory_info()
sys_files: list[dict[str, Any]] = []
for partition in psutil.disk_partitions():
try:
disk_usage = psutil.disk_usage(partition.mountpoint)
except Exception:
continue
mountpoint = partition.mountpoint.replace('\\', '')
sys_files.append(
{
'dirName': partition.device,
'sysTypeName': partition.fstype,
'typeName': f'本地固定磁盘({mountpoint})',
'total': bytes2human(disk_usage.total),
'used': bytes2human(disk_usage.used),
'free': bytes2human(disk_usage.free),
'usage': f'{disk_usage.percent}%',
}
)
return {
'cpu': {
'cpuNum': cpu_num,
'used': cpu_usage_percent.user,
'sys': cpu_usage_percent.system,
'free': cpu_usage_percent.idle,
},
'mem': {
'total': bytes2human(memory_info.total),
'used': bytes2human(memory_info.used),
'free': bytes2human(memory_info.free),
'usage': memory_info.percent,
},
'sys': {
'computerIp': self.resolve_server_ip(hostname),
'computerName': platform.node(),
'osArch': platform.machine(),
'osName': platform.platform(),
'userDir': os.getcwd(),
},
'py': {
'name': current_process.name(),
'version': platform.python_version(),
'startTime': time.strftime('%Y-%m-%d %H:%M:%S', time.localtime(start_time_stamp)),
'runTime': f'{days}天{hours}小时{minutes}分钟',
'home': current_process.exe(),
'total': bytes2human(memory_info.available),
'used': bytes2human(process_memory_info.rss),
'free': bytes2human(memory_info.available - process_memory_info.rss),
'usage': round((process_memory_info.rss / memory_info.available) * 100, 2),
},
'sysFiles': sys_files,
}