config_service.py 9.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. from sqlalchemy.orm import Session
  2. from typing import Dict, List, Optional, Any
  3. import json
  4. from app.models.config import SystemConfig, ConfigHistory
  5. from app.schemas.config_schema import ConfigItem, ConfigUpdate
  6. from app.services.system_config_manager import clear_config_cache
  7. class ConfigService:
  8. def __init__(self, db: Session, tenant_id: Optional[int] = None):
  9. self.db = db
  10. self.tenant_id = tenant_id
  11. def _base_query(self):
  12. return self.db.query(SystemConfig).filter(
  13. SystemConfig.tenant_id == self.tenant_id
  14. )
  15. _KEY_CATEGORY = {
  16. "system_name": "basic", "system_logo": "basic",
  17. "contact_email": "basic", "icp_number": "basic",
  18. "enable_registration": "feature", "enable_recharge": "feature",
  19. "enable_search": "feature", "enable_local_models": "feature",
  20. "new_user_bonus": "feature",
  21. "max_tokens_per_request": "limit", "max_images_per_request": "limit",
  22. "max_audio_chars": "limit", "max_video_duration": "limit",
  23. "min_balance_required": "limit",
  24. }
  25. # 配置项中文描述
  26. _KEY_DESCRIPTION = {
  27. "system_name": "平台名称,显示在登录页和导航栏",
  28. "system_logo": "平台 Logo 图片 URL",
  29. "contact_email": "联系邮箱",
  30. "icp_number": "ICP 备案号",
  31. "enable_registration": "是否开放用户注册",
  32. "enable_recharge": "是否开放充值功能",
  33. "enable_search": "是否开放联网搜索功能",
  34. "enable_local_models": "是否允许用户使用本地模型",
  35. "new_user_bonus": "新用户注册后自动赠送的余额(元)",
  36. "max_tokens_per_request": "单次请求最大 Token 数",
  37. "max_images_per_request": "单次请求最多生成图片数",
  38. "max_audio_chars": "单次音频请求最大字符数",
  39. "max_video_duration": "单次视频请求最大时长(秒)",
  40. "min_balance_required": "发起请求所需最低余额(元)",
  41. }
  42. # 需要在前端展示的默认配置项(即使数据库中没有记录也要显示)
  43. _DEFAULT_DISPLAY_CONFIGS = [
  44. {"key": "enable_registration", "type": "boolean", "category": "feature", "default": True},
  45. {"key": "enable_recharge", "type": "boolean", "category": "feature", "default": True},
  46. {"key": "enable_search", "type": "boolean", "category": "feature", "default": True},
  47. {"key": "enable_local_models", "type": "boolean", "category": "feature", "default": True},
  48. {"key": "new_user_bonus", "type": "number", "category": "feature", "default": 10.0},
  49. {"key": "max_tokens_per_request", "type": "number", "category": "limit", "default": 4000},
  50. {"key": "max_images_per_request", "type": "number", "category": "limit", "default": 4},
  51. {"key": "max_audio_chars", "type": "number", "category": "limit", "default": 5000},
  52. {"key": "max_video_duration", "type": "number", "category": "limit", "default": 10},
  53. {"key": "min_balance_required", "type": "number", "category": "limit", "default": 0.01},
  54. ]
  55. def _get_category(self, key: str) -> str:
  56. return self._KEY_CATEGORY.get(key, "basic")
  57. def get_all_configs(self) -> Dict[str, List[ConfigItem]]:
  58. configs = self._base_query().all()
  59. # 先把数据库中已有的配置按 key 索引
  60. db_map = {c.config_key: c for c in configs}
  61. grouped: Dict[str, List[ConfigItem]] = {}
  62. # 1. 先填入数据库中已有的记录
  63. for config in configs:
  64. category = config.category
  65. if category not in grouped:
  66. grouped[category] = []
  67. grouped[category].append(ConfigItem(
  68. key=config.config_key,
  69. value=self._parse_value(config.config_value, config.config_type),
  70. type=config.config_type,
  71. category=config.category,
  72. description=config.description or self._KEY_DESCRIPTION.get(config.config_key, ""),
  73. updated_at=config.updated_at,
  74. ))
  75. # 2. 补充数据库中没有记录的默认配置项,确保前端始终能看到这些配置
  76. for meta in self._DEFAULT_DISPLAY_CONFIGS:
  77. key = meta["key"]
  78. if key in db_map:
  79. continue # 已有记录,跳过
  80. category = meta["category"]
  81. if category not in grouped:
  82. grouped[category] = []
  83. grouped[category].append(ConfigItem(
  84. key=key,
  85. value=meta["default"],
  86. type=meta["type"],
  87. category=category,
  88. description=self._KEY_DESCRIPTION.get(key, ""),
  89. updated_at=None,
  90. ))
  91. return grouped
  92. def get_config(self, key: str) -> Optional[ConfigItem]:
  93. config = self._base_query().filter(SystemConfig.config_key == key).first()
  94. if not config:
  95. return None
  96. return ConfigItem(
  97. key=config.config_key,
  98. value=self._parse_value(config.config_value, config.config_type),
  99. type=config.config_type,
  100. category=config.category,
  101. description=config.description,
  102. updated_at=config.updated_at,
  103. )
  104. def update_configs(self, configs: List[ConfigUpdate], admin_id: Optional[int]):
  105. for config_update in configs:
  106. self._update_single_config(config_update.key, config_update.value, admin_id)
  107. self.db.commit()
  108. clear_config_cache()
  109. def _update_single_config(self, key: str, value: Any, admin_id: Optional[int]):
  110. config = self._base_query().filter(SystemConfig.config_key == key).first()
  111. if not config:
  112. from app.services.system_config_manager import SystemConfigManager
  113. default_value = SystemConfigManager().DEFAULT_VALUES.get(key)
  114. if default_value is None:
  115. raise ValueError(f"配置项 {key} 不存在且没有默认值")
  116. if isinstance(default_value, bool):
  117. config_type = "boolean"
  118. elif isinstance(default_value, (int, float)):
  119. config_type = "number"
  120. elif isinstance(default_value, str):
  121. config_type = "string"
  122. else:
  123. config_type = "json"
  124. config = SystemConfig(
  125. tenant_id=self.tenant_id,
  126. config_key=key,
  127. config_value=self._serialize_value(default_value, config_type),
  128. config_type=config_type,
  129. category=self._get_category(key),
  130. description="",
  131. updated_by=admin_id,
  132. )
  133. self.db.add(config)
  134. old_value = ""
  135. else:
  136. self._validate_value(value, config.config_type)
  137. old_value = config.config_value
  138. new_value = self._serialize_value(value, config.config_type)
  139. history = ConfigHistory(
  140. tenant_id=self.tenant_id,
  141. config_key=key,
  142. old_value=old_value,
  143. new_value=new_value,
  144. updated_by=admin_id,
  145. )
  146. self.db.add(history)
  147. config.config_value = new_value
  148. config.updated_by = admin_id
  149. def get_config_history(self, key: str, page: int = 1, size: int = 20):
  150. offset = (page - 1) * size
  151. q = self.db.query(ConfigHistory).filter(
  152. ConfigHistory.tenant_id == self.tenant_id,
  153. ConfigHistory.config_key == key,
  154. )
  155. return {
  156. "items": q.order_by(ConfigHistory.updated_at.desc()).offset(offset).limit(size).all(),
  157. "total": q.count(),
  158. "page": page,
  159. "size": size,
  160. }
  161. def reset_config(self, key: str, admin_id: Optional[int]):
  162. default_values = {
  163. "system_name": "智创空间",
  164. "enable_registration": True,
  165. "enable_recharge": True,
  166. "enable_search": True,
  167. "enable_local_models": True,
  168. "new_user_bonus": 10,
  169. "max_tokens_per_request": 4000,
  170. "max_images_per_request": 4,
  171. "max_audio_chars": 5000,
  172. "max_video_duration": 10,
  173. "min_balance_required": 0.01,
  174. }
  175. if key not in default_values:
  176. raise ValueError(f"配置项 {key} 没有默认值")
  177. self._update_single_config(key, default_values[key], admin_id)
  178. self.db.commit()
  179. # ── 序列化 / 反序列化 ──────────────────────────────────────────────────────
  180. def _parse_value(self, value_str: str, value_type: str) -> Any:
  181. return json.loads(value_str)
  182. def _serialize_value(self, value: Any, value_type: str) -> str:
  183. if value_type == "boolean" and isinstance(value, str):
  184. value = value.lower() in ("true", "1")
  185. elif value_type == "number" and isinstance(value, str):
  186. value = float(value) if "." in value else int(value)
  187. return json.dumps(value, ensure_ascii=False)
  188. def _validate_value(self, value: Any, value_type: str):
  189. if value_type == "string" and not isinstance(value, str):
  190. raise ValueError("配置值必须是字符串类型")
  191. elif value_type == "number":
  192. if isinstance(value, str):
  193. try:
  194. float(value)
  195. except ValueError:
  196. raise ValueError("配置值必须是数字类型")
  197. elif not isinstance(value, (int, float)):
  198. raise ValueError("配置值必须是数字类型")
  199. elif value_type == "boolean":
  200. if isinstance(value, str):
  201. if value.lower() not in ("true", "false", "1", "0"):
  202. raise ValueError("配置值必须是布尔类型")
  203. elif not isinstance(value, bool):
  204. raise ValueError("配置值必须是布尔类型")