Files
012-kaopeilian/backend/app/core/config.py
111 998211c483 feat: 初始化考培练系统项目
- 从服务器拉取完整代码
- 按框架规范整理项目结构
- 配置 Drone CI 测试环境部署
- 包含后端(FastAPI)、前端(Vue3)、管理端

技术栈: Vue3 + TypeScript + FastAPI + MySQL
2026-01-24 19:33:28 +08:00

324 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
系统配置
支持两种配置来源:
1. 环境变量 / .env 文件(传统方式,向后兼容)
2. 数据库 tenant_configs 表(新方式,支持热更新)
配置优先级:数据库 > 环境变量 > 默认值
"""
import os
import json
from functools import lru_cache
from typing import Optional, Any
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
"""系统配置"""
# 应用基础配置
APP_NAME: str = "KaoPeiLian"
APP_VERSION: str = "1.0.0"
DEBUG: bool = Field(default=True)
# 租户配置(用于多租户部署)
TENANT_CODE: str = Field(default="demo", description="租户编码,如 hua, yy, hl")
# 服务器配置
HOST: str = Field(default="0.0.0.0")
PORT: int = Field(default=8000)
# 数据库配置
DATABASE_URL: Optional[str] = Field(default=None)
MYSQL_HOST: str = Field(default="localhost")
MYSQL_PORT: int = Field(default=3306)
MYSQL_USER: str = Field(default="root")
MYSQL_PASSWORD: str = Field(default="password")
MYSQL_DATABASE: str = Field(default="kaopeilian")
@property
def database_url(self) -> str:
"""构建数据库连接URL"""
if self.DATABASE_URL:
return self.DATABASE_URL
# 使用urllib.parse.quote_plus来正确编码特殊字符
import urllib.parse
password = urllib.parse.quote_plus(self.MYSQL_PASSWORD)
return f"mysql+aiomysql://{self.MYSQL_USER}:{password}@{self.MYSQL_HOST}:{self.MYSQL_PORT}/{self.MYSQL_DATABASE}?charset=utf8mb4"
# Redis配置
REDIS_URL: str = Field(default="redis://localhost:6379/0")
# JWT配置
SECRET_KEY: str = Field(default="your-secret-key-here")
ALGORITHM: str = Field(default="HS256")
ACCESS_TOKEN_EXPIRE_MINUTES: int = Field(default=30)
REFRESH_TOKEN_EXPIRE_DAYS: int = Field(default=7)
# 跨域配置
CORS_ORIGINS: list[str] = Field(
default=[
"http://localhost:3000",
"http://localhost:3001",
"http://localhost:5173",
"http://127.0.0.1:3000",
"http://127.0.0.1:3001",
"http://127.0.0.1:5173",
]
)
@field_validator('CORS_ORIGINS', mode='before')
@classmethod
def parse_cors_origins(cls, v):
"""解析 CORS_ORIGINS 环境变量(支持 JSON 格式字符串)"""
if isinstance(v, str):
try:
return json.loads(v)
except json.JSONDecodeError:
# 如果不是 JSON 格式,尝试按逗号分割
return [origin.strip() for origin in v.split(',')]
return v
# 日志配置
LOG_LEVEL: str = Field(default="INFO")
LOG_FORMAT: str = Field(default="text") # text 或 json
LOG_DIR: str = Field(default="logs")
# 上传配置
UPLOAD_DIR: str = Field(default="uploads")
MAX_UPLOAD_SIZE: int = Field(default=15 * 1024 * 1024) # 15MB
@property
def UPLOAD_PATH(self) -> str:
"""获取上传文件的完整路径"""
import os
return os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), self.UPLOAD_DIR)
# Coze 平台配置(陪练对话、播课等)
COZE_API_BASE: Optional[str] = Field(default="https://api.coze.cn")
COZE_WORKSPACE_ID: Optional[str] = Field(default=None)
COZE_API_TOKEN: Optional[str] = Field(default="pat_Sa5OiuUl0gDflnKstQTToIz0sSMshBV06diX0owOeuI1ZK1xDLH5YZH9fSeuKLIi")
COZE_TRAINING_BOT_ID: Optional[str] = Field(default=None)
COZE_CHAT_BOT_ID: Optional[str] = Field(default=None)
COZE_PRACTICE_BOT_ID: Optional[str] = Field(default="7560643598174683145") # 陪练专用Bot ID
# 播课工作流配置(多租户需在环境变量中覆盖,参见:应用配置清单.md
COZE_BROADCAST_WORKFLOW_ID: str = Field(default="7577983042284486666") # 默认:演示版播课工作流
COZE_BROADCAST_SPACE_ID: str = Field(default="7474971491470688296") # 播课工作流空间ID
COZE_BROADCAST_BOT_ID: Optional[str] = Field(default=None) # 播课工作流专用Bot ID
# OAuth配置可选
COZE_OAUTH_CLIENT_ID: Optional[str] = Field(default=None)
COZE_OAUTH_PUBLIC_KEY_ID: Optional[str] = Field(default=None)
COZE_OAUTH_PRIVATE_KEY_PATH: Optional[str] = Field(default=None)
# WebSocket语音配置
COZE_WS_BASE_URL: str = Field(default="wss://ws.coze.cn")
COZE_AUDIO_FORMAT: str = Field(default="pcm") # 音频格式
COZE_SAMPLE_RATE: int = Field(default=16000) # 采样率Hz
COZE_AUDIO_CHANNELS: int = Field(default=1) # 声道数(单声道)
COZE_AUDIO_BIT_DEPTH: int = Field(default=16) # 位深度
# 服务器公开访问域名
PUBLIC_DOMAIN: str = Field(default="http://aiedu.ireborn.com.cn")
# 言迹智能工牌API配置
YANJI_API_BASE: str = Field(default="https://open.yanjiai.com") # 正式环境
YANJI_CLIENT_ID: str = Field(default="1Fld4LCWt2vpJNG5")
YANJI_CLIENT_SECRET: str = Field(default="XE8w413qNtJBOdWc2aCezV0yMIHpUuTZ")
YANJI_TENANT_ID: str = Field(default="516799409476866048")
YANJI_ESTATE_ID: str = Field(default="516799468310364162")
# SCRM 系统对接 API Key用于内部服务间调用
SCRM_API_KEY: str = Field(default="scrm-kpl-api-key-2026-ruixiaomei")
# AI 服务配置(知识点分析 V2 使用)
# 首选服务商4sapi.com国内优化
AI_PRIMARY_API_KEY: str = Field(default="sk-9yMCXjRGANbacz20kJY8doSNy6Rf446aYwmgGIuIXQ7DAyBw") # 测试阶段 Key
AI_PRIMARY_BASE_URL: str = Field(default="https://4sapi.com/v1")
# 备选服务商OpenRouter模型全稳定性好
AI_FALLBACK_API_KEY: str = Field(default="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") # 测试阶段 Key
AI_FALLBACK_BASE_URL: str = Field(default="https://openrouter.ai/api/v1")
# 默认模型
AI_DEFAULT_MODEL: str = Field(default="gemini-3-flash-preview")
# 请求超时(秒)
AI_TIMEOUT: float = Field(default=120.0)
model_config = {
"env_file": ".env",
"env_file_encoding": "utf-8",
"case_sensitive": True,
"extra": "allow", # 允许额外的环境变量
}
@lru_cache()
def get_settings() -> Settings:
"""获取系统配置(缓存)"""
return Settings()
settings = get_settings()
# ============================================
# 动态配置获取(支持从数据库读取)
# ============================================
class DynamicConfig:
"""
动态配置管理器
用于在运行时从数据库获取配置,支持热更新。
向后兼容:如果数据库不可用,回退到环境变量配置。
"""
_tenant_loader = None
_initialized = False
@classmethod
async def init(cls, redis_url: Optional[str] = None):
"""
初始化动态配置管理器
Args:
redis_url: Redis URL可选用于缓存
"""
if cls._initialized:
return
try:
from app.core.tenant_config import TenantConfigManager
if redis_url:
await TenantConfigManager.init_redis(redis_url)
cls._initialized = True
except Exception as e:
import logging
logging.getLogger(__name__).warning(f"动态配置初始化失败: {e}")
@classmethod
async def get(cls, key: str, default: Any = None, tenant_code: Optional[str] = None) -> Any:
"""
获取配置值
Args:
key: 配置键(如 AI_PRIMARY_API_KEY
default: 默认值
tenant_code: 租户编码(可选,默认使用环境变量中的 TENANT_CODE
Returns:
配置值
"""
# 确定租户编码
if tenant_code is None:
tenant_code = settings.TENANT_CODE
# 配置键到分组的映射
config_mapping = {
# 数据库
"MYSQL_HOST": ("database", "MYSQL_HOST"),
"MYSQL_PORT": ("database", "MYSQL_PORT"),
"MYSQL_USER": ("database", "MYSQL_USER"),
"MYSQL_PASSWORD": ("database", "MYSQL_PASSWORD"),
"MYSQL_DATABASE": ("database", "MYSQL_DATABASE"),
# Redis
"REDIS_HOST": ("redis", "REDIS_HOST"),
"REDIS_PORT": ("redis", "REDIS_PORT"),
"REDIS_DB": ("redis", "REDIS_DB"),
# 安全
"SECRET_KEY": ("security", "SECRET_KEY"),
"CORS_ORIGINS": ("security", "CORS_ORIGINS"),
# Coze
"COZE_PRACTICE_BOT_ID": ("coze", "COZE_PRACTICE_BOT_ID"),
"COZE_BROADCAST_WORKFLOW_ID": ("coze", "COZE_BROADCAST_WORKFLOW_ID"),
"COZE_BROADCAST_SPACE_ID": ("coze", "COZE_BROADCAST_SPACE_ID"),
"COZE_OAUTH_CLIENT_ID": ("coze", "COZE_OAUTH_CLIENT_ID"),
"COZE_OAUTH_PUBLIC_KEY_ID": ("coze", "COZE_OAUTH_PUBLIC_KEY_ID"),
# AI
"AI_PRIMARY_API_KEY": ("ai", "AI_PRIMARY_API_KEY"),
"AI_PRIMARY_BASE_URL": ("ai", "AI_PRIMARY_BASE_URL"),
"AI_FALLBACK_API_KEY": ("ai", "AI_FALLBACK_API_KEY"),
"AI_FALLBACK_BASE_URL": ("ai", "AI_FALLBACK_BASE_URL"),
"AI_DEFAULT_MODEL": ("ai", "AI_DEFAULT_MODEL"),
"AI_TIMEOUT": ("ai", "AI_TIMEOUT"),
# 言迹
"YANJI_CLIENT_ID": ("yanji", "YANJI_CLIENT_ID"),
"YANJI_CLIENT_SECRET": ("yanji", "YANJI_CLIENT_SECRET"),
"YANJI_TENANT_ID": ("yanji", "YANJI_TENANT_ID"),
"YANJI_ESTATE_ID": ("yanji", "YANJI_ESTATE_ID"),
}
# 尝试从数据库获取
if cls._initialized and key in config_mapping:
try:
from app.core.tenant_config import TenantConfigManager
config_group, config_key = config_mapping[key]
loader = TenantConfigManager.get_loader(tenant_code)
value = await loader.get_config(config_group, config_key)
if value is not None:
return value
except Exception:
pass
# 回退到环境变量 / Settings
env_value = getattr(settings, key, None)
if env_value is not None:
return env_value
return default
@classmethod
async def is_feature_enabled(cls, feature_code: str, tenant_code: Optional[str] = None) -> bool:
"""
检查功能是否启用
Args:
feature_code: 功能编码
tenant_code: 租户编码
Returns:
是否启用
"""
if tenant_code is None:
tenant_code = settings.TENANT_CODE
if cls._initialized:
try:
from app.core.tenant_config import TenantConfigManager
loader = TenantConfigManager.get_loader(tenant_code)
return await loader.is_feature_enabled(feature_code)
except Exception:
pass
return True # 默认启用
@classmethod
async def refresh_cache(cls, tenant_code: Optional[str] = None):
"""
刷新配置缓存
Args:
tenant_code: 租户编码(为空则刷新所有)
"""
if not cls._initialized:
return
try:
from app.core.tenant_config import TenantConfigManager
if tenant_code:
await TenantConfigManager.refresh_tenant_cache(tenant_code)
else:
await TenantConfigManager.refresh_all_cache()
except Exception:
pass