From 0c489bfdeeb5b68ae799d2f92bf28f844eacdb9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=96=87=E8=96=87=E5=AE=89?= Date: Fri, 23 Jan 2026 14:59:57 +0800 Subject: [PATCH] a --- .cursorrules | 14 + backend/api/routes/config.py | 203 +++++++++++++- backend/config_manager.py | 248 ++++++++++++++++-- .../database/add_global_strategy_config.sql | 45 ++++ backend/database/models.py | 68 +++++ frontend/src/components/GlobalConfig.jsx | 74 ++---- frontend/src/services/api.js | 17 +- 7 files changed, 575 insertions(+), 94 deletions(-) create mode 100644 .cursorrules create mode 100644 backend/database/add_global_strategy_config.sql diff --git a/.cursorrules b/.cursorrules new file mode 100644 index 0000000..9de17a7 --- /dev/null +++ b/.cursorrules @@ -0,0 +1,14 @@ +# 交易系统开发最高准则 + +## 1. 风险控制(核心) +- **止损高于一切**:严禁在任何平仓逻辑前添加时间限制。任何情况下,只要触发止损条件,必须立即执行平仓。 +- **严禁恢复时间锁**:绝对不允许重新启用 `MIN_HOLD_TIME_SEC` 来限制止损或止盈。 +- **异常处理**:所有涉及 `binance.create_order` 的操作必须包含 try-catch 逻辑,并有重试机制或错误预警。 + +## 2. 币安合约逻辑 +- **挂单确认**:在开仓订单成交后,必须立即调用 `_ensure_exchange_sltp_orders` 在交易所侧挂好止损单。 +- **价格类型**:区分 Mark Price(标记价格)和 Last Price(最新价格),止损逻辑应优先参考标记价格以防插针。 + +## 3. 代码风格 +- 使用 Python 异步编程 (asyncio)。 +- 所有的交易日志必须记录 Symbol、价格、原因和时间戳。 \ No newline at end of file diff --git a/backend/api/routes/config.py b/backend/api/routes/config.py index 82e8b33..e2112da 100644 --- a/backend/api/routes/config.py +++ b/backend/api/routes/config.py @@ -832,11 +832,208 @@ async def update_configs_batch( @router.get("/meta") async def get_config_meta(user: Dict[str, Any] = Depends(get_current_user)) -> Dict[str, Any]: - gid = _global_strategy_account_id() is_admin = (user.get("role") or "user") == "admin" return { - "global_strategy_account_id": int(gid), "is_admin": bool(is_admin), "user_risk_knobs": sorted(list(USER_RISK_KNOBS)), - "note": "平台兜底模式:策略核心由全局策略账号统一管理;普通用户仅可调整风险旋钮。", + "note": "平台兜底模式:策略核心由全局配置表统一管理(管理员专用);普通用户仅可调整风险旋钮。", } + + +@router.get("/global") +async def get_global_configs( + user: Dict[str, Any] = Depends(get_current_user), +): + """获取全局策略配置(仅管理员)""" + if (user.get("role") or "user") != "admin": + raise HTTPException(status_code=403, detail="仅管理员可访问全局策略配置") + + try: + from database.models import GlobalStrategyConfig + configs = GlobalStrategyConfig.get_all() + result = {} + for config in configs: + key = config['config_key'] + value = GlobalStrategyConfig._convert_value( + config['config_value'], + config['config_type'] + ) + result[key] = { + "value": value, + "type": config['config_type'], + "category": config['category'], + "description": config.get('description'), + } + + # 添加默认配置(如果数据库中没有) + for k, meta in CORE_STRATEGY_CONFIG_DEFAULTS.items(): + if k not in result: + result[k] = meta + + # 固定风险百分比配置 + FIXED_RISK_CONFIG_DEFAULTS = { + "USE_FIXED_RISK_SIZING": { + "value": True, + "type": "boolean", + "category": "risk", + "description": "使用固定风险百分比计算仓位(凯利公式)。启用后,每笔单子承受的风险固定为 FIXED_RISK_PERCENT,避免大额亏损。", + }, + "FIXED_RISK_PERCENT": { + "value": 0.02, + "type": "number", + "category": "risk", + "description": "每笔单子承受的风险百分比(相对于总资金)。例如 0.02 表示 2%。启用固定风险后,每笔亏损限制在该百分比内。", + }, + } + for k, meta in FIXED_RISK_CONFIG_DEFAULTS.items(): + if k not in result: + result[k] = meta + + return result + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.put("/global/{key}") +async def update_global_config( + key: str, + item: ConfigUpdate, + user: Dict[str, Any] = Depends(get_current_user), +): + """更新全局策略配置(仅管理员)""" + if (user.get("role") or "user") != "admin": + raise HTTPException(status_code=403, detail="仅管理员可修改全局策略配置") + + try: + from database.models import GlobalStrategyConfig + + # 获取现有配置以确定类型和分类 + existing = GlobalStrategyConfig.get(key) + if existing: + config_type = item.type or existing['config_type'] + category = item.category or existing['category'] + description = item.description or existing['description'] + else: + # 从默认配置获取 + meta = CORE_STRATEGY_CONFIG_DEFAULTS.get(key) or { + "USE_FIXED_RISK_SIZING": {"type": "boolean", "category": "risk", "description": "使用固定风险百分比计算仓位"}, + "FIXED_RISK_PERCENT": {"type": "number", "category": "risk", "description": "每笔单子承受的风险百分比"}, + }.get(key) + config_type = item.type or (meta.get("type") if meta else "string") + category = item.category or (meta.get("category") if meta else "strategy") + description = item.description or (meta.get("description") if meta else f"{key}配置") + + # 验证配置值 + if config_type == 'number': + try: + float(item.value) + except (ValueError, TypeError): + raise HTTPException(status_code=400, detail=f"Invalid number value for {key}") + elif config_type == 'boolean': + if not isinstance(item.value, bool): + item.value = str(item.value).lower() in ('true', '1', 'yes', 'on') + + # 更新全局配置 + GlobalStrategyConfig.set( + key, + item.value, + config_type, + category, + description, + updated_by=user.get("username") + ) + + # 更新Redis缓存 + try: + from config_manager import GlobalStrategyConfigManager + global_mgr = GlobalStrategyConfigManager() + if isinstance(item.value, (dict, list, bool, int, float)): + import json + value_str = json.dumps(item.value, ensure_ascii=False) + else: + value_str = str(item.value) + global_mgr._set_to_redis(key, item.value) + except Exception as e: + logger.warning(f"更新全局配置Redis缓存失败: {e}") + + return {"message": f"全局配置 {key} 已更新", "key": key, "value": item.value} + except HTTPException: + raise + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) + + +@router.post("/global/batch") +async def update_global_configs_batch( + configs: list[ConfigItem], + user: Dict[str, Any] = Depends(get_current_user), +): + """批量更新全局策略配置(仅管理员)""" + if (user.get("role") or "user") != "admin": + raise HTTPException(status_code=403, detail="仅管理员可修改全局策略配置") + + try: + from database.models import GlobalStrategyConfig + from config_manager import GlobalStrategyConfigManager + + updated_count = 0 + errors = [] + global_mgr = GlobalStrategyConfigManager() + + for item in configs: + try: + # 获取现有配置 + existing = GlobalStrategyConfig.get(item.key) + if existing: + config_type = item.type or existing['config_type'] + category = item.category or existing['category'] + description = item.description or existing['description'] + else: + # 从默认配置获取 + meta = CORE_STRATEGY_CONFIG_DEFAULTS.get(item.key) or { + "USE_FIXED_RISK_SIZING": {"type": "boolean", "category": "risk"}, + "FIXED_RISK_PERCENT": {"type": "number", "category": "risk"}, + }.get(item.key) + config_type = item.type or (meta.get("type") if meta else "string") + category = item.category or (meta.get("category") if meta else "strategy") + description = item.description or (meta.get("description") if meta else f"{item.key}配置") + + # 验证配置值 + if config_type == 'number': + try: + float(item.value) + except (ValueError, TypeError): + errors.append(f"{item.key}: Invalid number value") + continue + + # 更新全局配置 + GlobalStrategyConfig.set( + item.key, + item.value, + config_type, + category, + description, + updated_by=user.get("username") + ) + + # 更新Redis缓存 + global_mgr._set_to_redis(item.key, item.value) + updated_count += 1 + except Exception as e: + errors.append(f"{item.key}: {str(e)}") + + if errors: + return { + "message": f"成功更新 {updated_count} 个配置,{len(errors)} 个失败", + "updated": updated_count, + "errors": errors, + "note": "交易系统将在下次扫描时自动使用新配置" + } + + return { + "message": f"成功更新 {updated_count} 个全局配置", + "updated": updated_count, + "note": "交易系统将在下次扫描时自动使用新配置" + } + except Exception as e: + raise HTTPException(status_code=500, detail=str(e)) diff --git a/backend/config_manager.py b/backend/config_manager.py index 85bf326..a35054a 100644 --- a/backend/config_manager.py +++ b/backend/config_manager.py @@ -47,13 +47,10 @@ import logging logger = logging.getLogger(__name__) -# 平台兜底:策略核心使用全局账号配置(默认 account_id=1),普通用户账号只允许调整“风险旋钮” +# 平台兜底:策略核心使用全局配置表(global_strategy_config),普通用户账号只允许调整“风险旋钮” # - 风险旋钮:每个账号独立(仓位/频次等) -# - 其它策略参数:统一从全局账号读取,避免每个用户乱改导致策略不可控 -try: - GLOBAL_STRATEGY_ACCOUNT_ID = int(os.getenv("ATS_GLOBAL_STRATEGY_ACCOUNT_ID") or "1") -except Exception: - GLOBAL_STRATEGY_ACCOUNT_ID = 1 +# - 其它策略参数:统一从全局配置表读取,避免每个用户乱改导致策略不可控 +# 注意:不再依赖account_id=1,全局配置存储在独立的global_strategy_config表中 RISK_KNOBS_KEYS = { "MIN_MARGIN_USDT", @@ -74,6 +71,217 @@ except ImportError: redis = None +class GlobalStrategyConfigManager: + """全局策略配置管理器(独立于账户,管理员专用)""" + + _instance = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + def __init__(self): + if hasattr(self, '_initialized'): + return + self._initialized = True + self._cache = {} + self._redis_client: Optional[redis.Redis] = None + self._redis_connected = False + self._redis_hash_key = "global_strategy_config" # 独立的Redis键 + self._init_redis() + self._load_from_db() + + def _init_redis(self): + """初始化Redis客户端(同步)""" + if not REDIS_SYNC_AVAILABLE: + logger.debug("redis-py未安装,全局配置缓存将不使用Redis") + return + + try: + redis_url = os.getenv('REDIS_URL', 'redis://localhost:6379') + redis_use_tls = os.getenv('REDIS_USE_TLS', 'False').lower() == 'true' + redis_username = os.getenv('REDIS_USERNAME', None) + redis_password = os.getenv('REDIS_PASSWORD', None) + + if not redis_url or not isinstance(redis_url, str): + redis_url = 'redis://localhost:6379' + + if redis_use_tls and not redis_url.startswith('rediss://'): + if redis_url.startswith('redis://'): + redis_url = redis_url.replace('redis://', 'rediss://', 1) + + connection_kwargs = { + 'username': redis_username, + 'password': redis_password, + 'decode_responses': True + } + + if redis_url.startswith('rediss://') or redis_use_tls: + ssl_cert_reqs = os.getenv('REDIS_SSL_CERT_REQS', 'required') + ssl_ca_certs = os.getenv('REDIS_SSL_CA_CERTS', None) + connection_kwargs['select'] = int(os.getenv('REDIS_SELECT', 0)) + connection_kwargs['ssl_cert_reqs'] = ssl_cert_reqs + if ssl_ca_certs: + connection_kwargs['ssl_ca_certs'] = ssl_ca_certs + if ssl_cert_reqs == 'none': + connection_kwargs['ssl_check_hostname'] = False + elif ssl_cert_reqs == 'required': + connection_kwargs['ssl_check_hostname'] = True + else: + connection_kwargs['ssl_check_hostname'] = False + + self._redis_client = redis.from_url(redis_url, **connection_kwargs) + self._redis_client.ping() + self._redis_connected = True + logger.info("✓ 全局策略配置Redis缓存连接成功") + except Exception as e: + logger.debug(f"全局策略配置Redis缓存连接失败: {e},将使用数据库缓存") + self._redis_client = None + self._redis_connected = False + + def _get_from_redis(self, key: str) -> Optional[Any]: + """从Redis获取全局配置值""" + if not self._redis_connected or not self._redis_client: + return None + + try: + value = self._redis_client.hget(self._redis_hash_key, key) + if value is not None and value != '': + return ConfigManager._coerce_redis_value(value) + except Exception as e: + logger.debug(f"从Redis获取全局配置失败 {key}: {e}") + try: + self._redis_client.ping() + self._redis_connected = True + except: + self._redis_connected = False + + return None + + def _set_to_redis(self, key: str, value: Any): + """设置全局配置到Redis""" + if not self._redis_connected or not self._redis_client: + return False + + try: + if isinstance(value, (dict, list, bool, int, float)): + value_str = json.dumps(value, ensure_ascii=False) + else: + value_str = str(value) + + self._redis_client.hset(self._redis_hash_key, key, value_str) + self._redis_client.expire(self._redis_hash_key, 3600) + return True + except Exception as e: + logger.debug(f"设置全局配置到Redis失败 {key}: {e}") + try: + self._redis_client.ping() + self._redis_connected = True + except: + self._redis_connected = False + return False + + def _load_from_db(self): + """从数据库加载全局配置""" + try: + from database.models import GlobalStrategyConfig + except ImportError: + logger.warning("GlobalStrategyConfig未导入,无法从数据库加载全局配置") + self._cache = {} + return + + try: + # 先尝试从Redis加载 + if self._redis_connected and self._redis_client: + try: + self._redis_client.ping() + redis_configs = self._redis_client.hgetall(self._redis_hash_key) + if redis_configs and len(redis_configs) > 0: + for key, value_str in redis_configs.items(): + self._cache[key] = ConfigManager._coerce_redis_value(value_str) + logger.info(f"从Redis加载了 {len(self._cache)} 个全局配置项") + return + except Exception as e: + logger.debug(f"从Redis加载全局配置失败: {e},回退到数据库") + try: + self._redis_client.ping() + except: + self._redis_connected = False + + # 从数据库加载 + configs = GlobalStrategyConfig.get_all() + for config in configs: + key = config['config_key'] + # 使用TradingConfig的转换方法(GlobalStrategyConfig复用) + from database.models import TradingConfig + value = TradingConfig._convert_value( + config['config_value'], + config['config_type'] + ) + self._cache[key] = value + self._set_to_redis(key, value) + + logger.info(f"从数据库加载了 {len(self._cache)} 个全局配置项,已同步到Redis") + except Exception as e: + logger.warning(f"从数据库加载全局配置失败,使用默认配置: {e}") + self._cache = {} + + def get(self, key: str, default: Any = None) -> Any: + """获取全局配置值""" + # 1. 优先从Redis缓存读取 + if self._redis_connected and self._redis_client: + redis_value = self._get_from_redis(key) + if redis_value is not None: + self._cache[key] = redis_value + return redis_value + + # 2. 从本地缓存读取 + if key in self._cache: + return self._cache[key] + + # 3. 从数据库读取 + try: + from database.models import GlobalStrategyConfig + db_value = GlobalStrategyConfig.get_value(key) + if db_value is not None: + self._cache[key] = db_value + self._set_to_redis(key, db_value) + return db_value + except Exception: + pass + + # 4. 从环境变量读取 + env_value = os.getenv(key) + if env_value is not None: + return env_value + + # 5. 返回默认值 + return default + + def reload_from_redis(self): + """强制从Redis重新加载全局配置""" + if not self._redis_connected or not self._redis_client: + return + + try: + self._redis_client.ping() + except Exception as e: + logger.debug(f"Redis连接不可用: {e},跳过从Redis重新加载") + self._redis_connected = False + return + + try: + redis_configs = self._redis_client.hgetall(self._redis_hash_key) + if redis_configs and len(redis_configs) > 0: + self._cache = {} + for key, value_str in redis_configs.items(): + self._cache[key] = ConfigManager._coerce_redis_value(value_str) + logger.debug(f"从Redis重新加载了 {len(self._cache)} 个全局配置项") + except Exception as e: + logger.debug(f"从Redis重新加载全局配置失败: {e},保持现有缓存") + + class ConfigManager: """配置管理器 - 优先从Redis缓存读取,其次从数据库读取,回退到环境变量和默认值""" @@ -492,32 +700,24 @@ class ConfigManager: def get_trading_config(self): """获取交易配置字典(兼容原有config.py的TRADING_CONFIG)""" - # 全局策略配置管理器(避免递归:当 self 就是全局账号时,不做跨账号读取) - global_mgr = None - if self.account_id != int(GLOBAL_STRATEGY_ACCOUNT_ID or 1): - try: - global_mgr = ConfigManager.for_account(int(GLOBAL_STRATEGY_ACCOUNT_ID or 1)) - except Exception: - global_mgr = None - # 预热全局 cache:避免每个 key 都 HGET 一次 - if global_mgr is not None: - try: - global_mgr.reload_from_redis() - except Exception: - pass + # 全局策略配置管理器(从独立的global_strategy_config表读取) + global_config_mgr = GlobalStrategyConfigManager() + try: + global_config_mgr.reload_from_redis() + except Exception: + pass def eff_get(key: str, default: Any): """ - 策略核心:默认从全局账号读取(GLOBAL_STRATEGY_ACCOUNT_ID)。 + 策略核心:从全局配置表读取(global_strategy_config)。 风险旋钮:从当前账号读取。 """ # API key/secret/testnet 永远按账号读取(在 get() 内部已处理) - if key in RISK_KNOBS_KEYS or global_mgr is None: + if key in RISK_KNOBS_KEYS: return self.get(key, default) + # 从全局配置表读取 try: - if key in global_mgr._cache: # noqa: SLF001 - return global_mgr._cache.get(key, default) # noqa: SLF001 - return global_mgr.get(key, default) + return global_config_mgr.get(key, default) except Exception: return self.get(key, default) diff --git a/backend/database/add_global_strategy_config.sql b/backend/database/add_global_strategy_config.sql new file mode 100644 index 0000000..26ed0a7 --- /dev/null +++ b/backend/database/add_global_strategy_config.sql @@ -0,0 +1,45 @@ +-- 创建全局策略配置表(独立于账户) +-- 全局配置不依赖任何account_id,由管理员统一管理 + +CREATE TABLE IF NOT EXISTS `global_strategy_config` ( + `id` INT PRIMARY KEY AUTO_INCREMENT, + `config_key` VARCHAR(100) NOT NULL, + `config_value` TEXT NOT NULL, + `config_type` VARCHAR(50) NOT NULL COMMENT 'string, number, boolean, json', + `category` VARCHAR(50) NOT NULL COMMENT 'strategy, risk, scan', + `description` TEXT, + `updated_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + `updated_by` VARCHAR(50) COMMENT '更新人(用户名)', + INDEX `idx_category` (`category`), + UNIQUE KEY `uk_config_key` (`config_key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='全局策略配置表(管理员专用)'; + +-- 迁移现有account_id=1的核心策略配置到全局配置表 +-- 注意:只迁移非风险旋钮的配置 +INSERT INTO `global_strategy_config` (`config_key`, `config_value`, `config_type`, `category`, `description`) +SELECT + `config_key`, + `config_value`, + `config_type`, + `category`, + `description` +FROM `trading_config` +WHERE `account_id` = 1 + AND `config_key` NOT IN ( + 'MIN_MARGIN_USDT', + 'MIN_POSITION_PERCENT', + 'MAX_POSITION_PERCENT', + 'MAX_TOTAL_POSITION_PERCENT', + 'AUTO_TRADE_ENABLED', + 'MAX_OPEN_POSITIONS', + 'MAX_DAILY_ENTRIES', + 'BINANCE_API_KEY', + 'BINANCE_API_SECRET', + 'USE_TESTNET' + ) +ON DUPLICATE KEY UPDATE + `config_value` = VALUES(`config_value`), + `config_type` = VALUES(`config_type`), + `category` = VALUES(`category`), + `description` = VALUES(`description`), + `updated_at` = CURRENT_TIMESTAMP; diff --git a/backend/database/models.py b/backend/database/models.py index 7db8f20..9f5ac0d 100644 --- a/backend/database/models.py +++ b/backend/database/models.py @@ -292,6 +292,74 @@ class TradingConfig: return str(value) +class GlobalStrategyConfig: + """全局策略配置模型(独立于账户,管理员专用)""" + + @staticmethod + def get_all(): + """获取所有全局配置""" + if not _table_has_column("global_strategy_config", "config_key"): + return [] + return db.execute_query( + "SELECT * FROM global_strategy_config ORDER BY category, config_key" + ) + + @staticmethod + def get(key): + """获取单个全局配置""" + if not _table_has_column("global_strategy_config", "config_key"): + return None + return db.execute_one( + "SELECT * FROM global_strategy_config WHERE config_key = %s", + (key,) + ) + + @staticmethod + def set(key, value, config_type, category, description=None, updated_by=None): + """设置全局配置""" + if not _table_has_column("global_strategy_config", "config_key"): + # 表不存在时,回退到trading_config(兼容旧系统) + return TradingConfig.set(key, value, config_type, category, description, account_id=1) + + value_str = TradingConfig._convert_to_string(value, config_type) + db.execute_update( + """INSERT INTO global_strategy_config + (config_key, config_value, config_type, category, description, updated_by) + VALUES (%s, %s, %s, %s, %s, %s) + ON DUPLICATE KEY UPDATE + config_value = VALUES(config_value), + config_type = VALUES(config_type), + category = VALUES(category), + description = VALUES(description), + updated_by = VALUES(updated_by), + updated_at = CURRENT_TIMESTAMP""", + (key, value_str, config_type, category, description, updated_by), + ) + + @staticmethod + def get_value(key, default=None): + """获取全局配置值(自动转换类型)""" + result = GlobalStrategyConfig.get(key) + if result: + return GlobalStrategyConfig._convert_value(result['config_value'], result['config_type']) + return default + + @staticmethod + def _convert_value(value, config_type): + """转换配置值类型(复用TradingConfig的逻辑)""" + return TradingConfig._convert_value(value, config_type) + + @staticmethod + def delete(key): + """删除全局配置""" + if not _table_has_column("global_strategy_config", "config_key"): + return + db.execute_update( + "DELETE FROM global_strategy_config WHERE config_key = %s", + (key,) + ) + + class Trade: """交易记录模型""" diff --git a/frontend/src/components/GlobalConfig.jsx b/frontend/src/components/GlobalConfig.jsx index 250ba3a..02e5dfd 100644 --- a/frontend/src/components/GlobalConfig.jsx +++ b/frontend/src/components/GlobalConfig.jsx @@ -383,30 +383,17 @@ const GlobalConfig = () => { const loadConfigs = async () => { try { - // 管理员全局配置:始终使用全局策略账号的配置,不依赖当前 account - // 即使 configMeta 还没加载完成,也使用默认值 1 + // 管理员全局配置:从独立的全局配置表读取,不依赖任何 account if (isAdmin) { - const globalAccountId = configMeta?.global_strategy_account_id - ? parseInt(String(configMeta.global_strategy_account_id || '1'), 10) || 1 - : 1 // 如果 configMeta 还没加载,使用默认值 1 - const data = await api.getGlobalConfigs(globalAccountId) + const data = await api.getGlobalConfigs() setConfigs(data) } else { - // 非管理员:使用默认方式(会受当前 account 影响,但非管理员不应该访问这个页面) - const data = await api.getConfigs() - setConfigs(data) + // 非管理员不应该访问这个页面 + setConfigs({}) } } catch (error) { - console.error('Failed to load configs:', error) - // 如果加载失败,尝试使用默认值 1 重试一次 - if (isAdmin) { - try { - const data = await api.getGlobalConfigs(1) - setConfigs(data) - } catch (retryError) { - console.error('Retry load configs with accountId=1 failed:', retryError) - } - } + console.error('Failed to load global configs:', error) + setConfigs({}) } } @@ -474,22 +461,9 @@ const GlobalConfig = () => { loadAccounts() // 只有管理员才加载配置和系统状态 if (isAdmin) { - // 立即加载 configs(不等待 configMeta,使用默认值 1) - // 同时加载 configMeta,加载完成后会触发重新加载 configs(如果 global_strategy_account_id 不是 1) + // 加载全局配置(独立于账户) loadConfigs().catch(() => {}) - loadConfigMeta() - .then(() => { - // configMeta 加载完成后,如果 global_strategy_account_id 不是 1,重新加载 configs - // 这样可以确保使用正确的全局策略账号ID - const globalAccountId = configMeta?.global_strategy_account_id - ? parseInt(String(configMeta.global_strategy_account_id || '1'), 10) || 1 - : 1 - // 如果 globalAccountId 不是 1,说明之前加载的是默认值,需要重新加载 - if (globalAccountId !== 1) { - loadConfigs().catch(() => {}) - } - }) - .catch(() => {}) // 静默失败 + loadConfigMeta().catch(() => {}) // 静默失败 loadSystemStatus().catch(() => {}) // 静默失败 loadBackendStatus().catch(() => {}) // 静默失败 @@ -651,16 +625,13 @@ const GlobalConfig = () => { } }).filter(Boolean) - // 管理员全局配置:始终使用全局策略账号,即使 configMeta 还没加载也使用默认值 1 + // 管理员全局配置:使用独立的全局配置API let response if (isAdmin) { - const globalAccountId = configMeta?.global_strategy_account_id - ? parseInt(String(configMeta.global_strategy_account_id || '1'), 10) || 1 - : 1 - response = await api.updateGlobalConfigsBatch(configItems, globalAccountId) + response = await api.updateGlobalConfigsBatch(configItems) } else { // 非管理员不应该访问这个页面,但为了安全还是处理一下 - response = await api.updateConfigsBatch(configItems) + throw new Error('只有管理员可以修改全局配置') } setMessage(response.message || `已应用${preset.name}`) if (response.note) { @@ -700,13 +671,10 @@ const GlobalConfig = () => { } const buildConfigSnapshot = async (includeSecrets) => { - // 管理员全局配置:始终使用全局策略账号的配置,即使 configMeta 还没加载也使用默认值 1 + // 管理员全局配置:从独立的全局配置表读取 let data if (isAdmin) { - const globalAccountId = configMeta?.global_strategy_account_id - ? parseInt(String(configMeta.global_strategy_account_id || '1'), 10) || 1 - : 1 - data = await api.getGlobalConfigs(globalAccountId) + data = await api.getGlobalConfigs() } else { data = await api.getConfigs() } @@ -910,11 +878,7 @@ const GlobalConfig = () => { return
加载中...
} - // 简单计算:全局策略账号ID(在 render 时计算) - const globalStrategyAccountId = configMeta?.global_strategy_account_id - ? parseInt(String(configMeta?.global_strategy_account_id || '1'), 10) - : 1 - // 管理员全局配置页面:不依赖当前 account,直接管理全局策略账号 + // 管理员全局配置页面:不依赖任何 account,直接管理全局配置表 const isGlobalStrategyAccount = isAdmin // 简单计算:当前预设(直接在 render 时计算,不使用 useMemo) @@ -1217,13 +1181,7 @@ const GlobalConfig = () => { try { setSaving(true) setMessage('') - // 管理员始终使用全局策略账号,即使 configMeta 还没加载也使用默认值 1 - const globalAccountId = isAdmin - ? (configMeta?.global_strategy_account_id - ? parseInt(String(configMeta.global_strategy_account_id || '1'), 10) || 1 - : 1) - : null - if (!isAdmin || !globalAccountId) { + if (!isAdmin) { setMessage('只有管理员可以修改全局配置') return } @@ -1233,7 +1191,7 @@ const GlobalConfig = () => { type: config.type, category: config.category, description: config.description - }], globalAccountId) + }]) setMessage(`已更新 ${key}`) await loadConfigs() } catch (error) { diff --git a/frontend/src/services/api.js b/frontend/src/services/api.js index 382e9cd..bef05da 100644 --- a/frontend/src/services/api.js +++ b/frontend/src/services/api.js @@ -218,10 +218,10 @@ export const api = { return response.json() }, - // 全局配置:获取全局策略账号的配置(管理员专用,不依赖当前 account) - getGlobalConfigs: async (globalAccountId) => { - const response = await fetch(buildUrl('/api/config'), { - headers: withAuthHeaders({ 'X-Account-Id': String(globalAccountId || 1) }) + // 全局配置:获取全局策略配置(管理员专用,独立于账户) + getGlobalConfigs: async () => { + const response = await fetch(buildUrl('/api/config/global'), { + headers: withAuthHeaders() }) if (!response.ok) { const error = await response.json().catch(() => ({ detail: '获取全局配置失败' })) @@ -230,13 +230,12 @@ export const api = { return response.json() }, - // 全局配置:批量更新全局策略账号的配置(管理员专用) - updateGlobalConfigsBatch: async (configs, globalAccountId) => { - const response = await fetch(buildUrl('/api/config/batch'), { + // 全局配置:批量更新全局策略配置(管理员专用) + updateGlobalConfigsBatch: async (configs) => { + const response = await fetch(buildUrl('/api/config/global/batch'), { method: 'POST', headers: withAuthHeaders({ - 'Content-Type': 'application/json', - 'X-Account-Id': String(globalAccountId || 1) + 'Content-Type': 'application/json' }), body: JSON.stringify(configs) })