2cece72abe
- Settings 新增 SMS_CODE_WHITELIST 配置(逗号分隔手机号) - login_with_sms 中白名单手机号跳过验证码校验 - 方便内部测试和演示账号使用
232 lines
8.5 KiB
Python
232 lines
8.5 KiB
Python
"""
|
||
配置管理 - 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.5.18", 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")
|
||
|
||
# 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()
|