| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231 |
- from sqlalchemy.orm import Session
- from typing import Dict, List, Optional, Any
- import json
- from app.models.config import SystemConfig, ConfigHistory
- from app.schemas.config_schema import ConfigItem, ConfigUpdate
- from app.services.system_config_manager import clear_config_cache
- class ConfigService:
- def __init__(self, db: Session, tenant_id: Optional[int] = None):
- self.db = db
- self.tenant_id = tenant_id
- def _base_query(self):
- return self.db.query(SystemConfig).filter(
- SystemConfig.tenant_id == self.tenant_id
- )
- _KEY_CATEGORY = {
- "system_name": "basic", "system_logo": "basic",
- "contact_email": "basic", "icp_number": "basic",
- "enable_registration": "feature", "enable_recharge": "feature",
- "enable_search": "feature", "enable_local_models": "feature",
- "new_user_bonus": "feature",
- "max_tokens_per_request": "limit", "max_images_per_request": "limit",
- "max_audio_chars": "limit", "max_video_duration": "limit",
- "min_balance_required": "limit",
- }
- # 配置项中文描述
- _KEY_DESCRIPTION = {
- "system_name": "平台名称,显示在登录页和导航栏",
- "system_logo": "平台 Logo 图片 URL",
- "contact_email": "联系邮箱",
- "icp_number": "ICP 备案号",
- "enable_registration": "是否开放用户注册",
- "enable_recharge": "是否开放充值功能",
- "enable_search": "是否开放联网搜索功能",
- "enable_local_models": "是否允许用户使用本地模型",
- "new_user_bonus": "新用户注册后自动赠送的余额(元)",
- "max_tokens_per_request": "单次请求最大 Token 数",
- "max_images_per_request": "单次请求最多生成图片数",
- "max_audio_chars": "单次音频请求最大字符数",
- "max_video_duration": "单次视频请求最大时长(秒)",
- "min_balance_required": "发起请求所需最低余额(元)",
- }
- # 需要在前端展示的默认配置项(即使数据库中没有记录也要显示)
- _DEFAULT_DISPLAY_CONFIGS = [
- {"key": "enable_registration", "type": "boolean", "category": "feature", "default": True},
- {"key": "enable_recharge", "type": "boolean", "category": "feature", "default": True},
- {"key": "enable_search", "type": "boolean", "category": "feature", "default": True},
- {"key": "enable_local_models", "type": "boolean", "category": "feature", "default": True},
- {"key": "new_user_bonus", "type": "number", "category": "feature", "default": 10.0},
- {"key": "max_tokens_per_request", "type": "number", "category": "limit", "default": 4000},
- {"key": "max_images_per_request", "type": "number", "category": "limit", "default": 4},
- {"key": "max_audio_chars", "type": "number", "category": "limit", "default": 5000},
- {"key": "max_video_duration", "type": "number", "category": "limit", "default": 10},
- {"key": "min_balance_required", "type": "number", "category": "limit", "default": 0.01},
- ]
- def _get_category(self, key: str) -> str:
- return self._KEY_CATEGORY.get(key, "basic")
- def get_all_configs(self) -> Dict[str, List[ConfigItem]]:
- configs = self._base_query().all()
- # 先把数据库中已有的配置按 key 索引
- db_map = {c.config_key: c for c in configs}
- grouped: Dict[str, List[ConfigItem]] = {}
- # 1. 先填入数据库中已有的记录
- for config in configs:
- category = config.category
- if category not in grouped:
- grouped[category] = []
- grouped[category].append(ConfigItem(
- key=config.config_key,
- value=self._parse_value(config.config_value, config.config_type),
- type=config.config_type,
- category=config.category,
- description=config.description or self._KEY_DESCRIPTION.get(config.config_key, ""),
- updated_at=config.updated_at,
- ))
- # 2. 补充数据库中没有记录的默认配置项,确保前端始终能看到这些配置
- for meta in self._DEFAULT_DISPLAY_CONFIGS:
- key = meta["key"]
- if key in db_map:
- continue # 已有记录,跳过
- category = meta["category"]
- if category not in grouped:
- grouped[category] = []
- grouped[category].append(ConfigItem(
- key=key,
- value=meta["default"],
- type=meta["type"],
- category=category,
- description=self._KEY_DESCRIPTION.get(key, ""),
- updated_at=None,
- ))
- return grouped
- def get_config(self, key: str) -> Optional[ConfigItem]:
- config = self._base_query().filter(SystemConfig.config_key == key).first()
- if not config:
- return None
- return ConfigItem(
- key=config.config_key,
- value=self._parse_value(config.config_value, config.config_type),
- type=config.config_type,
- category=config.category,
- description=config.description,
- updated_at=config.updated_at,
- )
- def update_configs(self, configs: List[ConfigUpdate], admin_id: Optional[int]):
- for config_update in configs:
- self._update_single_config(config_update.key, config_update.value, admin_id)
- self.db.commit()
- clear_config_cache()
- def _update_single_config(self, key: str, value: Any, admin_id: Optional[int]):
- config = self._base_query().filter(SystemConfig.config_key == key).first()
- if not config:
- from app.services.system_config_manager import SystemConfigManager
- default_value = SystemConfigManager().DEFAULT_VALUES.get(key)
- if default_value is None:
- raise ValueError(f"配置项 {key} 不存在且没有默认值")
- if isinstance(default_value, bool):
- config_type = "boolean"
- elif isinstance(default_value, (int, float)):
- config_type = "number"
- elif isinstance(default_value, str):
- config_type = "string"
- else:
- config_type = "json"
- config = SystemConfig(
- tenant_id=self.tenant_id,
- config_key=key,
- config_value=self._serialize_value(default_value, config_type),
- config_type=config_type,
- category=self._get_category(key),
- description="",
- updated_by=admin_id,
- )
- self.db.add(config)
- old_value = ""
- else:
- self._validate_value(value, config.config_type)
- old_value = config.config_value
- new_value = self._serialize_value(value, config.config_type)
- history = ConfigHistory(
- tenant_id=self.tenant_id,
- config_key=key,
- old_value=old_value,
- new_value=new_value,
- updated_by=admin_id,
- )
- self.db.add(history)
- config.config_value = new_value
- config.updated_by = admin_id
- def get_config_history(self, key: str, page: int = 1, size: int = 20):
- offset = (page - 1) * size
- q = self.db.query(ConfigHistory).filter(
- ConfigHistory.tenant_id == self.tenant_id,
- ConfigHistory.config_key == key,
- )
- return {
- "items": q.order_by(ConfigHistory.updated_at.desc()).offset(offset).limit(size).all(),
- "total": q.count(),
- "page": page,
- "size": size,
- }
- def reset_config(self, key: str, admin_id: Optional[int]):
- default_values = {
- "system_name": "智创空间",
- "enable_registration": True,
- "enable_recharge": True,
- "enable_search": True,
- "enable_local_models": True,
- "new_user_bonus": 10,
- "max_tokens_per_request": 4000,
- "max_images_per_request": 4,
- "max_audio_chars": 5000,
- "max_video_duration": 10,
- "min_balance_required": 0.01,
- }
- if key not in default_values:
- raise ValueError(f"配置项 {key} 没有默认值")
- self._update_single_config(key, default_values[key], admin_id)
- self.db.commit()
- # ── 序列化 / 反序列化 ──────────────────────────────────────────────────────
- def _parse_value(self, value_str: str, value_type: str) -> Any:
- return json.loads(value_str)
- def _serialize_value(self, value: Any, value_type: str) -> str:
- if value_type == "boolean" and isinstance(value, str):
- value = value.lower() in ("true", "1")
- elif value_type == "number" and isinstance(value, str):
- value = float(value) if "." in value else int(value)
- return json.dumps(value, ensure_ascii=False)
- def _validate_value(self, value: Any, value_type: str):
- if value_type == "string" and not isinstance(value, str):
- raise ValueError("配置值必须是字符串类型")
- elif value_type == "number":
- if isinstance(value, str):
- try:
- float(value)
- except ValueError:
- raise ValueError("配置值必须是数字类型")
- elif not isinstance(value, (int, float)):
- raise ValueError("配置值必须是数字类型")
- elif value_type == "boolean":
- if isinstance(value, str):
- if value.lower() not in ("true", "false", "1", "0"):
- raise ValueError("配置值必须是布尔类型")
- elif not isinstance(value, bool):
- raise ValueError("配置值必须是布尔类型")
|