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