Files
insistence 9f7a0daa3b feat: 新增cli系统 (#103)
* feat: 新增cli系统

* feat: ruoyi completion支持powershell

* perf: 优化tui显示

* fix: 修复前端构建异常
2026-05-13 11:19:31 +08:00

407 lines
18 KiB
Python

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()