This commit is contained in:
薇薇安 2026-01-23 14:59:57 +08:00
parent f798782a6d
commit 0c489bfdee
7 changed files with 575 additions and 94 deletions

14
.cursorrules Normal file
View File

@ -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、价格、原因和时间戳。

View File

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

View File

@ -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):
# 全局策略配置管理器从独立的global_strategy_config表读取
global_config_mgr = GlobalStrategyConfigManager()
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()
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)

View File

@ -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;

View File

@ -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:
"""交易记录模型"""

View File

@ -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 <div className="global-config">加载中...</div>
}
// 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) {

View File

@ -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)
})