Files
xiaoxia-saas/packages/config/api_settings.py
T
CI Bot 25820fc2dc
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 6s
CI/CD Pipeline / Validate - Code Quality (pull_request) Failing after 1m23s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m4s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 33s
CI/CD Pipeline / PR Build Web Image (pull_request) Successful in 18s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m21s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 51s
Preview Deploy / Deploy Preview Environment (pull_request) Failing after 22s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m10s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m57s
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 2m37s
AI Code Review / AI Code Review (pull_request) Successful in 7m16s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 9m23s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 5m21s
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 46s
refactor(#776): 统一配置入口,建立packages/config消除重复定义
- 新增 packages/config/ 作为配置统一入口(base/api_settings/worker_settings)
- SharedSettings 基类 + 统一单例管理(_settings_cache),消除3处重复单例实现
- API配置迁移到 packages/config/api_settings.py(APISettings)
- Worker配置迁移到 packages/config/worker_settings.py(WorkerSettings)
- 旧路径全部保留为 re-export 向后兼容层
- alembic/env.py 改用统一配置获取 database_url,不再直接读环境变量
- 测试更新:改用 reload_settings_cache() 替代手动 _settings=None

净减 397 行(4新文件 + 7修改)
2026-07-23 23:22:59 +08:00

289 lines
9.4 KiB
Python
Executable File
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.
"""API 服务配置 — 继承 SharedSettings,只追加 API 特有字段。
通用配置统一在 packages/config/base.py 的 SharedSettings 中定义,这里不重复。
历史上 API 端使用 UPPER_CASE 命名风格的字段,目前通过 property 别名向后兼容。
新代码统一使用 snake_case(继承自 SharedSettings)。
"""
import os
from typing import Optional
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import SettingsConfigDict
from packages.config.base import SharedSettings, get_cached_settings
class APISettings(SharedSettings):
"""API 服务专用配置。
通用配置继承自 SharedSettings,这里只定义 API 独有字段。
"""
# ── 应用基本信息 ────────────────────────────────────────────────────
app_name: str = "xiaoxia-saas"
app_version: str = "0.1.61"
# 应用基础 URL,用于生成认证邮件中的链接
app_base_url: str = "http://localhost:3000"
# Container bind address; external expose is controlled by Docker/Nginx.
api_host: str = "0.0.0.0" # nosec: B104
api_port: int = 8000
# ── 数据库特有 ──────────────────────────────────────────────────────
use_in_memory_db: bool = False
# ── Redis 特有 ──────────────────────────────────────────────────────
enable_redis_sessions: bool = False
# ── JWT ────────────────────────────────────────────────────────────
# JWT secret key - MUST be set via environment variable, no default allowed
jwt_secret_key: Optional[str] = None
# JWT 算法与过期时间
jwt_algorithm: str = "HS256"
jwt_access_token_expire_minutes: int = 30
jwt_refresh_token_expire_days: int = 30
@field_validator("jwt_secret_key", mode="before")
@classmethod
def validate_jwt_secret_key(cls, v):
if v is None or v == "":
raise ValueError(
"JWT_SECRET_KEY must be set via environment variable. "
"Do not use default value in production!"
)
# Block known insecure default values
insecure_defaults = [
"your-secret-key-change-in-production",
"your-secret-key",
"secret",
"changeme",
"password",
]
if v.lower() in [d.lower() for d in insecure_defaults]:
raise ValueError(
f"JWT_SECRET_KEY '{v}' is insecure. "
"Please set a strong random secret via environment variable."
)
return v
# ── 邮件 ────────────────────────────────────────────────────────────
enable_email_delivery: bool = False
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
smtp_from_email: str = ""
smtp_from_name: str = "小虾 SaaS"
smtp_use_tls: bool = True
# ── OSS 特有校验 ────────────────────────────────────────────────────
@field_validator("oss_access_key_id", mode="before")
@classmethod
def validate_oss_access_key_id(cls, v):
if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development":
raise ValueError(
"OSS_ACCESS_KEY_ID must be set via environment variable in non-development environments. "
"Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)."
)
return v or ""
@field_validator("oss_access_key_secret", mode="before")
@classmethod
def validate_oss_access_key_secret(cls, v):
if (v is None or v == "") and os.getenv("APP_ENV", "development") != "development":
raise ValueError(
"OSS_ACCESS_KEY_SECRET must be set via environment variable in non-development environments. "
"Check the server .env file (e.g. /var/lib/xiaoxia-saas-staging/.env)."
)
return v or ""
oss_direct_upload_max_mb: int = Field(
default=2000,
validation_alias=AliasChoices("oss_direct_upload_max_mb", "max_upload_size_mb"),
)
# ── CORS ────────────────────────────────────────────────────────────
cors_origins_raw: str = "http://localhost:3000,http://localhost:5173,http://localhost:8000"
# ── 渲染引擎 ────────────────────────────────────────────────────────
# 渲染引擎选择:legacy=旧VideoComposeServiceunified=新UnifiedRenderService
render_engine: str = "legacy"
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
@property
def cors_origins(self) -> list[str]:
return [origin.strip() for origin in self.cors_origins_raw.split(",") if origin.strip()]
# ── 向后兼容:UPPER_CASE property 别名 ──────────────────────────────
# 新代码请使用 snake_case(继承的字段名),以下别名仅用于兼容旧代码
@property
def APP_NAME(self) -> str:
return self.app_name
@property
def APP_VERSION(self) -> str:
return self.app_version
@property
def ENVIRONMENT(self) -> str:
return self.environment
@property
def DEBUG(self) -> bool:
return self.debug
@property
def APP_BASE_URL(self) -> str:
return self.app_base_url
@property
def API_HOST(self) -> str:
return self.api_host
@property
def API_PORT(self) -> int:
return self.api_port
@property
def DATABASE_URL(self) -> str:
return self.database_url
@property
def DATABASE_POOL_SIZE(self) -> int:
return self.database_pool_size
@property
def DATABASE_MAX_OVERFLOW(self) -> int:
return self.database_max_overflow
@property
def DATABASE_POOL_TIMEOUT(self) -> int:
return self.database_pool_timeout
@property
def DATABASE_POOL_RECYCLE(self) -> int:
return self.database_pool_recycle
@property
def USE_IN_MEMORY_DB(self) -> bool:
return self.use_in_memory_db
@property
def AUTO_CREATE_SCHEMA(self) -> bool:
return self.auto_create_schema
@property
def REDIS_URL(self) -> str:
return self.redis_url
@property
def ENABLE_REDIS_SESSIONS(self) -> bool:
return self.enable_redis_sessions
@property
def JWT_SECRET_KEY(self) -> Optional[str]:
return self.jwt_secret_key
@property
def JWT_ALGORITHM(self) -> str:
return self.jwt_algorithm
@property
def JWT_ACCESS_TOKEN_EXPIRE_MINUTES(self) -> int:
return self.jwt_access_token_expire_minutes
@property
def JWT_REFRESH_TOKEN_EXPIRE_DAYS(self) -> int:
return self.jwt_refresh_token_expire_days
@property
def ENABLE_EMAIL_DELIVERY(self) -> bool:
return self.enable_email_delivery
@property
def SMTP_HOST(self) -> str:
return self.smtp_host
@property
def SMTP_PORT(self) -> int:
return self.smtp_port
@property
def SMTP_USER(self) -> str:
return self.smtp_user
@property
def SMTP_PASSWORD(self) -> str:
return self.smtp_password
@property
def SMTP_FROM_EMAIL(self) -> str:
return self.smtp_from_email
@property
def SMTP_FROM_NAME(self) -> str:
return self.smtp_from_name
@property
def SMTP_USE_TLS(self) -> bool:
return self.smtp_use_tls
@property
def CELERY_BROKER_URL(self) -> str:
return self.celery_broker_url
@property
def CELERY_RESULT_BACKEND(self) -> str:
return self.celery_result_backend
@property
def OSS_ENDPOINT(self) -> str:
return self.oss_endpoint
@property
def OSS_ACCESS_KEY_ID(self) -> str:
return self.oss_access_key_id
@property
def OSS_ACCESS_KEY_SECRET(self) -> str:
return self.oss_access_key_secret
@property
def OSS_BUCKET_NAME(self) -> str:
return self.oss_bucket_name
@property
def OSS_DIRECT_UPLOAD_MAX_MB(self) -> int:
return self.oss_direct_upload_max_mb
@property
def OSS_DIRECT_UPLOAD_EXPIRE_SECONDS(self) -> int:
return self.oss_direct_upload_expire_seconds
@property
def CORS_ORIGINS_RAW(self) -> str:
return self.cors_origins_raw
@property
def CORS_ORIGINS(self) -> list[str]:
return self.cors_origins
@property
def RENDER_ENGINE(self) -> str:
return self.render_engine
def get_api_settings() -> APISettings:
"""获取 API 配置单例(统一入口)。"""
return get_cached_settings(APISettings)