""" 统一配置存储模块 支持 MySQL 优先、JSON 文件 fallback 的读写策略。 """ import json import logging from pathlib import Path from typing import Optional from app.models import AppConfig from app.storage_manager import get_storage_manager from app.mysql_database import MySQLSessionLocal logger = logging.getLogger(__name__) CONFIG_DIR = Path(__file__).resolve().parent.parent / "config" CONFIG_FILES = { "symbols": CONFIG_DIR / "symbols_config.json", "ai": CONFIG_DIR / "ai_config.json", } DEFAULT_CONFIGS = { "symbols": {"futures": {}, "stock": {}}, "ai": { "models": [], "active_model": None, "analysis_settings": { "enable_technical_analysis": True, "enable_fundamental_analysis": False, "enable_sentiment_analysis": False, "risk_tolerance": "medium", "max_position_pct": 10, }, }, } class ConfigStore: """统一配置存储入口。""" def __init__( self, storage_manager=None, config_dir: Path = CONFIG_DIR, session_maker=None, ): self.storage_manager = storage_manager or get_storage_manager() self.config_dir = config_dir self.session_maker = session_maker or MySQLSessionLocal self.config_files = { "symbols": self.config_dir / "symbols_config.json", "ai": self.config_dir / "ai_config.json", } def get_config(self, key: str, fallback: Optional[dict] = None) -> dict: """读取配置,优先从 MySQL,其次 JSON 文件。""" if key not in self.config_files: raise ValueError(f"未知配置键: {key}") if self.storage_manager.check_mysql(): try: db_value = self._get_from_mysql(key) if db_value is not None: return db_value # 数据库未命中:从 JSON 读取并回填 json_value = self._load_json_from_file(key) if json_value is not None: self._save_to_mysql(key, json_value) return json_value # JSON 不存在或损坏,使用 fallback 但不回填 return fallback if fallback is not None else DEFAULT_CONFIGS.get(key, {}) except Exception as e: logger.warning(f"从 MySQL 读取配置 [{key}] 失败,降级到 JSON: {e}") json_value = self._load_json_from_file(key) if json_value is not None: return json_value return fallback if fallback is not None else DEFAULT_CONFIGS.get(key, {}) def set_config(self, key: str, value: dict) -> bool: """写入配置,MySQL 可用时先写数据库再写 JSON;MySQL 不可用时只写 JSON。""" if key not in self.config_files: raise ValueError(f"未知配置键: {key}") if self.storage_manager.check_mysql(): try: if not self._save_to_mysql(key, value): logger.warning(f"写入 MySQL 配置 [{key}] 失败,保留 JSON 旧值") return False except Exception as e: logger.warning(f"写入 MySQL 配置 [{key}] 失败,保留 JSON 旧值: {e}") return False # MySQL 写入成功或 MySQL 不可用,都更新 JSON fallback self._save_to_json(key, value) return True def _get_from_mysql(self, key: str) -> Optional[dict]: """从 MySQL 读取配置。""" if self.session_maker is None: return None with self.session_maker() as db: record = db.query(AppConfig).filter(AppConfig.config_key == key).first() if record: return record.config_value return None def _save_to_mysql(self, key: str, value: dict) -> bool: """保存配置到 MySQL。""" if self.session_maker is None: return False with self.session_maker() as db: record = db.query(AppConfig).filter(AppConfig.config_key == key).first() if record: record.config_value = value else: record = AppConfig(config_key=key, config_value=value) db.add(record) db.commit() return True def _load_json_from_file(self, key: str) -> Optional[dict]: """从 JSON 文件读取配置,文件不存在或损坏时返回 None。""" file_path = self.config_files[key] if file_path.exists(): try: with open(file_path, "r", encoding="utf-8") as f: return json.load(f) except Exception as e: logger.warning(f"读取 JSON 配置 [{key}] 失败: {e}") return None def _load_json(self, key: str, fallback: Optional[dict] = None) -> dict: """从 JSON 文件读取配置,文件不存在或损坏时返回 fallback。""" value = self._load_json_from_file(key) if value is not None: return value return fallback if fallback is not None else DEFAULT_CONFIGS.get(key, {}) def _save_to_json(self, key: str, value: dict) -> None: """保存配置到 JSON 文件(原子写入)。""" file_path = self.config_files[key] self.config_dir.mkdir(parents=True, exist_ok=True) tmp_path = file_path.with_suffix(".tmp") try: with open(tmp_path, "w", encoding="utf-8") as f: json.dump(value, f, ensure_ascii=False, indent=4) tmp_path.replace(file_path) except Exception: if tmp_path.exists(): tmp_path.unlink() raise def _config_exists_in_mysql(self, key: str) -> bool: """检查 MySQL 中是否已存在配置。""" return self._get_from_mysql(key) is not None _config_store = None def get_config_store() -> ConfigStore: """获取 ConfigStore 单例。""" global _config_store if _config_store is None: _config_store = ConfigStore() return _config_store