初始提交
This commit is contained in:
@@ -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']
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
Reference in New Issue
Block a user