You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
buffer_platform/app/config_store.py

170 lines
5.9 KiB

"""
统一配置存储模块
支持 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 可用时先写数据库再写 JSONMySQL 不可用时只写 JSON。"""
if key not in self.config_files:
raise ValueError(f"未知配置键: {key}")
if self.storage_manager.check_mysql():
try:
self._save_to_mysql(key, value)
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