|
|
|
|
|
"""
|
|
|
|
|
|
统一配置存储模块
|
|
|
|
|
|
|
|
|
|
|
|
支持 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
|
|
|
|
|
|
|
|
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _get_mysql_session_local():
|
|
|
|
|
|
"""动态读取 app.mysql_database.MySQLSessionLocal,避免模块导入顺序问题。"""
|
|
|
|
|
|
import sys
|
|
|
|
|
|
module = sys.modules.get("app.mysql_database")
|
|
|
|
|
|
if module is None:
|
|
|
|
|
|
return None
|
|
|
|
|
|
return getattr(module, "MySQLSessionLocal", None)
|
|
|
|
|
|
|
|
|
|
|
|
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_override = session_maker
|
|
|
|
|
|
self.config_files = {
|
|
|
|
|
|
"symbols": self.config_dir / "symbols_config.json",
|
|
|
|
|
|
"ai": self.config_dir / "ai_config.json",
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@property
|
|
|
|
|
|
def session_maker(self):
|
|
|
|
|
|
"""动态读取 MySQLSessionLocal,避免初始化顺序问题。"""
|
|
|
|
|
|
if self._session_maker_override is not None:
|
|
|
|
|
|
return self._session_maker_override
|
|
|
|
|
|
return _get_mysql_session_local()
|
|
|
|
|
|
|
|
|
|
|
|
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
|