""" 配置管理 - Pydantic Settings ========================== 所有配置项通过环境变量或 .env 文件注入。 """ from functools import lru_cache from typing import Literal from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """应用配置""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", extra="ignore", arbitrary_types_allowed=True, ) # 应用基础配置 APP_NAME: str = Field(default="美家卡智影 API", description="应用名称") APP_VERSION: str = Field(default="1.7.1", description="应用版本") DEBUG: bool = Field(default=False, description="调试模式") ENV: Literal["development", "staging", "production"] = Field( default="development", description="运行环境" ) # 服务器配置 HOST: str = Field(default="0.0.0.0", description="监听地址") # nosec: B104 PORT: int = Field(default=8000, description="监听端口") APP_BASE_URL: str = Field( default="", description="应用公网地址(用于第三方回调,如 https://dev.tapi.meijiaka.cn)", ) WORKERS: int = Field(default=1, description="工作进程数(生产环境建议 > 1)") # 数据库配置(统一使用 PostgreSQL) DATABASE_URL: str = Field( default="postgresql+asyncpg://postgres:postgres@localhost:5432/meijiaka_zy", description="数据库连接字符串(PostgreSQL)", ) DATABASE_POOL_SIZE: int = Field(default=10, description="数据库连接池常驻连接数") DATABASE_MAX_OVERFLOW: int = Field(default=10, description="连接池临时溢出上限(建议 ≤ pool_size)") DATABASE_POOL_RECYCLE: int = Field( default=1800, description="连接回收时间(秒),防止长连接被数据库静默断开" ) DATABASE_POOL_TIMEOUT: int = Field( default=30, description="获取连接超时(秒),防止连接池耗尽时请求无限等待" ) # Redis 配置 REDIS_HOST: str = Field( default="localhost", description="Redis 主机地址", ) REDIS_PORT: int = Field( default=6379, description="Redis 端口", ) REDIS_DB: int = Field( default=0, description="Redis 数据库编号", ) REDIS_PASSWORD: str | None = Field( default=None, description="Redis 密码(无密码请留空)", ) # 安全配置 SECRET_KEY: str = Field( ..., description="JWT 签名密钥(生产环境必须修改)", ) ACCESS_TOKEN_EXPIRE_MINUTES: int = Field( default=30, description="Access Token 过期时间(分钟)", ) REFRESH_TOKEN_EXPIRE_DAYS: int = Field( default=30, description="Refresh Token 过期时间(天)", ) ALGORITHM: str = Field(default="HS256", description="JWT 算法") # CORS 配置 CORS_ORIGINS: str = Field( default="http://localhost:1420,http://127.0.0.1:1420,http://localhost:8080,http://127.0.0.1:8080,tauri://localhost,http://tauri.localhost,https://tauri.localhost", description="允许的跨域来源(逗号分隔)", ) # AI 模型配置 # 字节跳动 - 火山方舟 # 文档:https://www.volcengine.com/docs/82379/1399009 # base_url 已从 Settings 移除,改用 config/platform-config.yaml 配置 VOLCENGINE_API_KEY: str | None = Field(default=None, description="火山方舟 API Key") VOLCENGINE_MODEL: str = Field( default="doubao-seed-2-0-pro-260215", description="火山方舟默认模型(Model ID)", ) # 火山引擎音视频字幕服务 VOLCENGINE_CAPTION_APPID: str | None = Field(default=None, description="火山字幕 AppID") VOLCENGINE_CAPTION_TOKEN: str | None = Field(default=None, description="火山字幕 Token") # 火山引擎 MediaKit 服务(背景移除等多媒体处理) VOLCENGINE_MEDIAKIT_TOKEN: str | None = Field(default=None, description="火山引擎 MediaKit Token") # Vidu 密钥(base_url 已从 Settings 移除,改用 config/platform-config.yaml 配置) VIDU_API_KEY: str | None = Field(default=None, description="Vidu API Key") # 七牛云存储配置 QINIU_ACCESS_KEY: str | None = Field(default=None, description="七牛云 Access Key") QINIU_SECRET_KEY: str | None = Field(default=None, description="七牛云 Secret Key") QINIU_VIDEO_BUCKET: str = Field(default="media-liche", description="视频存储 Bucket") QINIU_VIDEO_DOMAIN: str = Field(default="media.liche.cn", description="视频存储域名") QINIU_IMAGE_BUCKET: str = Field(default="img-liche", description="图片存储 Bucket") QINIU_IMAGE_DOMAIN: str = Field(default="img.liche.cn", description="图片存储域名") # 微信支付配置(APIv2) WXPAY_MCHID: str | None = Field(default=None, description="微信支付商户号") WXPAY_APPID: str | None = Field(default=None, description="微信支付 AppID") WXPAY_API_KEY: str | None = Field(default=None, description="微信支付 APIv2 密钥") WXPAY_NOTIFY_URL: str | None = Field( default=None, description="微信支付回调地址(完整 URL)" ) # B2M 短信平台配置 SMS_APP_ID: str | None = Field(default=None, description="B2M 短信平台 AppID") SMS_SECRET_KEY: str | None = Field(default=None, description="B2M 短信平台 AES 密钥") SMS_BASE_URL: str | None = Field( default=None, description="B2M 短信平台接口地址(如 http://sms.b2m.cn:8080)" ) SMS_EXTENDED_CODE: str | None = Field( default=None, description="B2M 短信平台扩展码(选填)" ) SMS_CODE_WHITELIST: str = Field( default="", description="免验证码登录白名单(逗号分隔的手机号,如 13800138000,13900139000)", ) # 文件上传限制(字节) UPLOAD_MAX_VIDEO_SIZE: int = Field( default=500 * 1024 * 1024, description="视频最大上传大小(字节)" ) UPLOAD_MAX_IMAGE_SIZE: int = Field( default=20 * 1024 * 1024, description="图片最大上传大小(字节)" ) UPLOAD_MAX_AUDIO_SIZE: int = Field( default=100 * 1024 * 1024, description="音频最大上传大小(字节)" ) # 日志配置 LOG_LEVEL: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = Field( default="DEBUG", description="日志级别", ) @property def cors_origins_list(self) -> list[str]: """将 CORS_ORIGINS 字符串解析为列表""" return [origin.strip() for origin in self.CORS_ORIGINS.split(",")] @property def app_base_url(self) -> str: """应用公网地址(用于第三方回调)""" if self.APP_BASE_URL: return self.APP_BASE_URL.rstrip("/") if self.ENV == "production": return "https://tapi.meijiaka.cn" if self.ENV == "staging": return "https://dev.tapi.meijiaka.cn" return f"http://{self.HOST}:{self.PORT}" @property def use_redis(self) -> bool: """是否使用 Redis""" return bool(self.REDIS_HOST) @property def sms_code_whitelist_set(self) -> set[str]: """免验证码登录白名单(去重、去空格)""" if not self.SMS_CODE_WHITELIST: return set() return { mobile.strip() for mobile in self.SMS_CODE_WHITELIST.split(",") if mobile.strip() } @lru_cache def get_settings() -> Settings: """获取配置单例(带缓存)""" settings = Settings() # 生产环境安全检查 if settings.ENV == "production": if not settings.SECRET_KEY: raise ValueError( "生产环境必须设置强随机 SECRET_KEY!" "请在 .env 文件中设置一个随机字符串(至少 32 位)。" ) # 检查 CORS 配置 if settings.CORS_ORIGINS and "localhost" in settings.CORS_ORIGINS.lower(): import warnings warnings.warn( "生产环境 CORS 配置中包含 localhost,建议限制为实际域名", RuntimeWarning, stacklevel=2, ) return settings def reload_settings() -> Settings: """ 重新加载配置(仅用于开发调试)。 ⚠️ 生产环境推荐滚动重启,不要依赖此方法。 清除 lru_cache 后重新读取 .env,返回新的配置实例。 注意:已导入模块的模块级变量(如 `settings = get_settings()`)不会自动更新, 仅后续新的 `get_settings()` 调用会返回新实例。 """ get_settings.cache_clear() return get_settings()