Merge remote-tracking branch 'origin/develop' into fix/develop-ruff-f401

This commit is contained in:
2026-07-24 00:16:27 +08:00
23 changed files with 966 additions and 569 deletions
+180 -42
View File
@@ -1,60 +1,198 @@
# 小虾 SaaS 环境变量配置
# ============================================================
# 小虾 SaaS 环境变量完整配置
# ============================================================
# 本文件列出所有可配置的环境变量及默认值。
# 复制为 .env 后按需修改;生产环境务必覆盖所有密钥类配置。
#
# 配置读取规则(pydantic-settings,大小写不敏感):
# 1. 系统环境变量(最高优先级)
# 2. .env.{APP_ENV} 文件(如 .env.staging
# 3. .env 文件
# 4. 代码中的默认值(最低优先级)
# ============================================================
# ==================== 应用配置 ====================
APP_NAME=小虾 SaaS
APP_BASE_URL=http://localhost:3000
# ==================== 应用基本配置 ====================
# 应用名称
APP_NAME=xiaoxia-saas
# 应用版本号(展示用,代码中已内置默认)
APP_VERSION=0.1.61
# 环境标识:development / staging / production
# 决定读取 .env.{APP_ENV} 还是 .env,也影响部分配置的严格校验
APP_ENV=development
# ==================== 数据库配置 ====================
DATABASE_URL=postgresql://xiaoxia_user:your_password@localhost:5432/xiaoxia_saas
# 开发环境:使用内存数据库(不需要 PostgreSQL)
USE_IN_MEMORY_DB=true
# 生产环境:使用 PostgreSQL
# USE_IN_MEMORY_DB=false
# ==================== Redis 配置 ====================
REDIS_URL=redis://localhost:6379/0
# ==================== JWT 配置 ====================
JWT_SECRET_KEY=your-super-secret-key-change-this-in-production-min-32-chars
JWT_ALGORITHM=HS256
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
# ==================== 邮件配置 ====================
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASSWORD=your-app-specific-password
SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com
SMTP_FROM_NAME=小虾 SaaS
# ==================== 环境配置 ====================
ENVIRONMENT=development
# 是否开启 Debug 模式(开发环境 true,生产环境 false)
DEBUG=true
# ==================== CORS 配置 ====================
# 逗号分隔的域名列表(Settings 读取 CORS_ORIGINS_RAW
CORS_ORIGINS_RAW=http://localhost:3000,http://localhost:5173
# 应用基础 URL,用于生成认证邮件、回调链接等
APP_BASE_URL=http://localhost:3000
# API 服务监听地址(容器内绑定,外部暴露由 Docker/Nginx 控制)
API_HOST=0.0.0.0
# API 服务监听端口
API_PORT=8000
# 是否自动创建数据库表结构(开发环境可开启,生产环境用 alembic migration
AUTO_CREATE_SCHEMA=false
# ==================== 数据库配置 ====================
# 数据库连接串(格式:postgresql+psycopg://user:password@host:port/dbname
DATABASE_URL=postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas
# 连接池大小(常驻连接数)
DATABASE_POOL_SIZE=20
# 连接池最大溢出连接数(pool_size + max_overflow = 最大并发连接数)
DATABASE_MAX_OVERFLOW=10
# 获取连接超时时间(秒)
DATABASE_POOL_TIMEOUT=30
# 连接回收时间(秒),防止数据库端主动断开导致的死连接
DATABASE_POOL_RECYCLE=3600
# 是否使用内存数据库(SQLite,仅开发/测试可用;生产务必 false)
USE_IN_MEMORY_DB=false
# ==================== Redis 配置 ====================
# Redis 连接 URL(格式:redis://[:password@]host:port/db
REDIS_URL=redis://localhost:6379/0
# 是否使用 Redis 存储 Session(多实例部署时必须开启;开发可用内存存储)
ENABLE_REDIS_SESSIONS=false
# ==================== Celery 任务队列 ====================
# Celery Broker(任务分发),默认用 Redis db0
CELERY_BROKER_URL=redis://localhost:6379/0
# Celery Result Backend(任务结果存储),默认用 Redis db1
CELERY_RESULT_BACKEND=redis://localhost:6379/1
# ==================== Worker 配置 ====================
# Worker 进程名称
WORKER_NAME=xiaoxia-saas-worker
# Worker 并发数(同时执行的任务数)
WORKER_CONCURRENCY=4
# 每个子进程最多处理多少任务后重启(防止内存泄漏)
WORKER_MAX_TASKS_PER_CHILD=1000
# ==================== JWT 认证配置 ====================
# JWT 签名密钥 — 生产环境必须设置为强随机字符串(至少32字符)
# 内置不安全值会被拒绝:secret / changeme / password / your-secret-key 等
JWT_SECRET_KEY=your-super-secret-key-change-this-in-production-min-32-chars
# JWT 签名算法
JWT_ALGORITHM=HS256
# Access Token 过期时间(分钟)
JWT_ACCESS_TOKEN_EXPIRE_MINUTES=30
# Refresh Token 过期时间(天)
JWT_REFRESH_TOKEN_EXPIRE_DAYS=30
# ==================== 邮件配置 ====================
# 是否启用邮件投递(关闭时邮件内容打印到日志,开发调试用)
ENABLE_EMAIL_DELIVERY=false
# SMTP 服务器地址
SMTP_HOST=smtp.gmail.com
# SMTP 端口
SMTP_PORT=587
# SMTP 用户名
SMTP_USER=your-email@gmail.com
# SMTP 密码 / 应用专用密码
SMTP_PASSWORD=your-app-specific-password
# 发件人邮箱
SMTP_FROM_EMAIL=noreply@xiaoxia-saas.com
# 发件人显示名称
SMTP_FROM_NAME=小虾 SaaS
# 是否启用 TLS
SMTP_USE_TLS=true
# ==================== 阿里云 OSS 配置 ====================
# OSS 区域 endpoint
OSS_ENDPOINT=oss-cn-hangzhou.aliyuncs.com
# OSS Access Key ID — 非开发环境必须设置
OSS_ACCESS_KEY_ID=your-access-key-id
# OSS Access Key Secret — 非开发环境必须设置
OSS_ACCESS_KEY_SECRET=your-access-key-secret
# OSS Bucket 名称
OSS_BUCKET_NAME=xiaoxia-autocut
# ==================== CosyVoice 语音合成配置 ====================
# 注意:base_url 只需写到 /api/v1,具体路径由代码拼接
# 模型: cosyvoice-v3-flash (推荐,支持系统音色,性价比高)
# cosyvoice-v3-plus (高质量,系统音色少)
# cosyvoice-v3.5-flash / cosyvoice-v3.5-plus (仅支持克隆/设计音色,无系统音色)
# 音色: v3系列系统音色带 _v3 后缀,如 longxiaochun_v3, longxiaoxia_v3, longanyang (无后缀)
# 注意:COSYVOICE_* 变量由 packages/shared/config.py 的 SharedSettings 读取
# 直传最大文件大小(MB
OSS_DIRECT_UPLOAD_MAX_MB=2000
# 直传签名有效期(秒)
OSS_DIRECT_UPLOAD_EXPIRE_SECONDS=900
# ==================== CORS 配置 ====================
# 允许跨域的前端域名列表,逗号分隔
CORS_ORIGINS_RAW=http://localhost:3000,http://localhost:5173,http://localhost:8000
# ==================== 渲染引擎配置 ====================
# 渲染引擎选择:
# legacy — 旧 VideoComposeService(稳定,功能完整)
# unified — 新 UnifiedRenderService(新架构,部分场景仍在验证)
RENDER_ENGINE=legacy
# ==================== CosyVoice 语音合成 ====================
# 阿里云百灵语音合成服务
# 模型选择:
# cosyvoice-v3-flash — 推荐,系统音色多,性价比高
# cosyvoice-v3-plus — 高质量,系统音色少
# cosyvoice-v3.5-flash / cosyvoice-v3.5-plus — 仅支持克隆/设计音色,无系统音色
# 音色:v3 系列系统音色带 _v3 后缀,如 longxiaochun_v3 / longxiaoxia_v3 / longanyang
COSYVOICE_API_KEY=your-cosyvoice-api-key
COSYVOICE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
COSYVOICE_MODEL=cosyvoice-v3-flash
COSYVOICE_VOICE=longxiaochun_v3
COSYVOICE_SAMPLE_RATE=22050
COSYVOICE_FORMAT=mp3
# 音色克隆模型名(固定为 voice-enrollment,通常不需修改)
COSYVOICE_CLONE_MODEL=voice-enrollment
# ==================== 豆包大模型(火山引擎方舟) ====================
# 用于 AI 文案生成、智能剪辑等需要大模型能力的场景
DOUBAO_API_KEY=your-doubao-api-key
DOUBAO_MODEL=doubao-seed-1-6-250615
DOUBAO_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
DOUBAO_TIMEOUT=30
DOUBAO_MAX_RETRIES=2
Regular → Executable
+7 -4
View File
@@ -1,4 +1,3 @@
import os
from logging.config import fileConfig
from sqlalchemy import engine_from_config, pool
@@ -8,13 +7,17 @@ from alembic import context
# Import your models' Base here
from packages.adapters.sqlalchemy_impl.models import Base
# 使用统一配置入口获取 database_url,而非直接读环境变量
from packages.config import get_shared_settings
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
database_url = os.getenv("DATABASE_URL")
if database_url:
config.set_main_option("sqlalchemy.url", database_url)
# 从统一配置系统获取 database_url,确保与应用使用同一配置源
settings = get_shared_settings()
if settings.database_url:
config.set_main_option("sqlalchemy.url", settings.database_url)
# Interpret the config file for Python logging.
# This line sets up loggers basically.
+22 -301
View File
@@ -1,307 +1,28 @@
"""API 服务配置 — 继承 SharedSettings,只追加 API 特有字段
"""API 服务配置(向后兼容层)
通用配置(DB/Redis/OSS/Celery/CosyVoice/Doubao 等)统一在
packages/shared/config.py 的 SharedSettings 中定义,这里不重复。
统一配置已迁移到 packages.config.api_settings。
新代码请使用:
from packages.config import APISettings, get_api_settings
历史上 API 端使用 UPPER_CASE 命名风格的字段,目前通过 property
别名向后兼容。新代码统一使用 snake_case(继承自 SharedSettings
本文件保留 Settings 类名、get_settings() 函数、settings 模块级单例,
确保所有旧的 import 路径仍然有效
"""
import os
from typing import Optional
from pydantic import AliasChoices, Field, field_validator
from pydantic_settings import SettingsConfigDict
from packages.shared.config import SharedSettings
class Settings(SharedSettings):
"""API 服务专用配置。
通用配置继承自 SharedSettings,这里只定义 API 独有字段。
"""
# ── 应用基本信息 ────────────────────────────────────────────────────
app_name: str = "xiaoxia-saas"
app_version: str = "0.1.61"
# 应用基础 URL,用于生成认证邮件中的链接
# 开发环境默认 http://localhost:3000
# 生产环境应通过环境变量 APP_BASE_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
_settings: Optional["Settings"] = None
def get_settings() -> "Settings":
"""获取 API 配置单例。
优先读取 APP_ENV 指定的环境文件(.env.{env}),不存在则读 .env。
"""
global _settings
if _settings is None:
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
if os.path.exists(env_file):
_settings = Settings(_env_file=env_file)
else:
_settings = Settings()
return _settings
from packages.config import APISettings as Settings
from packages.config import get_api_settings as get_settings
from packages.config import reload_settings_cache
# 模块级单例(向后兼容)
settings = get_settings()
# 暴露旧的 reload_settings 函数名
def reload_settings():
"""重新加载配置(测试用)。"""
reload_settings_cache()
global settings
settings = get_settings()
return settings
__all__ = ["Settings", "get_settings", "settings", "reload_settings"]
Regular → Executable
+10 -9
View File
@@ -1,19 +1,20 @@
"""Compatibility layer for the canonical API settings module.
"""向后兼容层 — 配置已统一到 packages.config。
Use `app.config` as the single source of truth for API configuration.
This module remains only for older imports during migration.
新代码请使用:
from packages.config import get_api_settings, APISettings
"""
from app.config import Settings as AppSettings
from app.config import get_settings, settings
from packages.config import APISettings as AppSettings
from packages.config import get_api_settings as get_settings
from packages.config import reload_settings_cache
def reload_settings() -> AppSettings:
"""Reload settings for tests and legacy callers."""
import app.config as canonical_config
"""重新加载配置(测试用)。"""
reload_settings_cache()
return get_settings()
canonical_config.settings = canonical_config.get_settings()
return canonical_config.settings
settings = get_settings()
__all__ = ["AppSettings", "get_settings", "reload_settings", "settings"]
+13 -52
View File
@@ -1,61 +1,22 @@
"""Worker 服务配置 — 继承 SharedSettings,只追加 Worker 特有字段
"""Worker 服务配置(向后兼容层)
通用配置(DB/Redis/Celery/OSS/CosyVoice/Doubao 等)统一在
packages/shared/config.py 的 SharedSettings 中定义,这里不重复。
统一配置已迁移到 packages.config.worker_settings。
新代码请使用:
from packages.config import WorkerSettings, get_worker_settings
"""
import os
from typing import Optional
from pydantic_settings import SettingsConfigDict
from packages.shared.config import SharedSettings
from packages.config import WorkerSettings, get_worker_settings, reload_settings_cache
class WorkerSettings(SharedSettings):
"""Worker 服务专用配置。
通用配置继承自 SharedSettings,这里只定义 Worker 独有字段。
Celery broker/backend 使用继承的 celery_broker_url / celery_result_backend
历史上 Worker 使用 broker_url / result_backend 字段名,通过 property 别名兼容。
"""
# ── Worker 特有 ────────────────────────────────────────────────────
worker_name: str = "xiaoxia-saas-worker"
worker_concurrency: int = 4
worker_max_tasks_per_child: int = 1000
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── 向后兼容:Celery 字段名别名 ──────────────────────────────────
@property
def broker_url(self) -> str:
return self.celery_broker_url
@property
def result_backend(self) -> str:
return self.celery_result_backend
def get_settings() -> WorkerSettings:
"""获取 Worker 配置单例(向后兼容)。"""
return get_worker_settings()
_settings: Optional["WorkerSettings"] = None
def reload_settings() -> WorkerSettings:
"""重新加载配置(测试用)。"""
reload_settings_cache()
return get_worker_settings()
def get_settings() -> "WorkerSettings":
"""获取 Worker 配置单例。
优先读取 APP_ENV 指定的环境文件(.env.{env}),不存在则读 .env。
"""
global _settings
if _settings is None:
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
if os.path.exists(env_file):
_settings = WorkerSettings(_env_file=env_file)
else:
_settings = WorkerSettings()
return _settings
__all__ = ["WorkerSettings", "get_settings", "reload_settings"]
+3 -1
View File
@@ -10,6 +10,8 @@ from typing import Optional
import redis
from redis import Redis
from packages.domain.auth.session_store import SessionStorePort
class RedisConfig:
"""Redis 配置"""
@@ -50,7 +52,7 @@ class NoopSessionStore:
return False
class SessionStore:
class SessionStore(SessionStorePort):
"""Session 存储服务"""
def __init__(self, redis_client: Optional[Redis] = None, config: Optional[RedisConfig] = None):
+3 -15
View File
@@ -4,11 +4,12 @@
"""
import smtplib
from dataclasses import dataclass
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from typing import List, Optional
from packages.domain.auth.email_service import EmailConfig, EmailServicePort
class NoopEmailService:
def send_verification_email(self, **kwargs):
@@ -18,20 +19,7 @@ class NoopEmailService:
return False, "Email delivery is disabled"
@dataclass
class EmailConfig:
"""邮件配置"""
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
from_email: str = ""
from_name: str = "小虾 SaaS"
use_tls: bool = True
class EmailService:
class EmailService(EmailServicePort):
"""邮件服务类"""
def __init__(self, config: Optional[EmailConfig] = None):
+3 -1
View File
@@ -6,6 +6,8 @@ from typing import Any, Dict, Optional
import jwt
from jwt.exceptions import ExpiredSignatureError, InvalidTokenError
from packages.domain.auth.jwt_service import JWTServicePort
class JWTConfig:
"""JWT 配置"""
@@ -57,7 +59,7 @@ class TokenType:
REFRESH = "refresh"
class JWTService:
class JWTService(JWTServicePort):
"""JWT 服务类"""
def __init__(self, config: JWTConfig = None):
+5 -3
View File
@@ -3,12 +3,14 @@
使用 bcrypt 安全存储密码
"""
from typing import Optional
from typing import Optional, Tuple
import bcrypt
from packages.domain.auth.password_hasher import PasswordHasherPort, PasswordValidatorPort
class PasswordHasher:
class PasswordHasher(PasswordHasherPort):
"""密码哈希服务"""
def __init__(self, rounds: int = 12):
@@ -98,7 +100,7 @@ class PasswordHasher:
return False
class PasswordValidator:
class PasswordValidator(PasswordValidatorPort):
"""密码强度验证器"""
def __init__(
+34
View File
@@ -0,0 +1,34 @@
"""统一配置入口 — 整个项目的配置都从这里获取。
所有服务共享同一个配置包,消除重复定义和不一致。
用法:
from packages.config import get_api_settings, get_worker_settings, get_shared_settings
from packages.config import APISettings, WorkerSettings, SharedSettings
架构:
packages/config/
├── base.py # SharedSettings 基类 + 统一单例管理
├── api_settings.py # APISettingsAPI 特有配置)
└── worker_settings.py # WorkerSettingsWorker 特有配置)
"""
from packages.config.api_settings import APISettings, get_api_settings
from packages.config.base import (
SharedSettings,
get_cached_settings,
get_shared_settings,
reload_settings_cache,
)
from packages.config.worker_settings import WorkerSettings, get_worker_settings
__all__ = [
"SharedSettings",
"APISettings",
"WorkerSettings",
"get_shared_settings",
"get_api_settings",
"get_worker_settings",
"get_cached_settings",
"reload_settings_cache",
]
+286
View File
@@ -0,0 +1,286 @@
"""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)
+110
View File
@@ -0,0 +1,110 @@
"""统一配置基类 — 所有服务共享的基础配置。
数据库/Redis/OSS/Celery/AI服务等通用配置统一定义在此。
API 和 Worker 各自的 Settings 类继承本类,只追加服务特有字段。
单例模式和 env 文件加载逻辑也统一在这里实现。
"""
import os
from typing import Optional, Type, TypeVar
from pydantic_settings import BaseSettings, SettingsConfigDict
T = TypeVar("T", bound=BaseSettings)
class SharedSettings(BaseSettings):
"""所有服务共享的基础配置。
API 和 Worker 都继承本类,确保:
1. 数据库/Redis/OSS/Celery 等核心配置默认值一致
2. 环境变量命名统一(snake_casepydantic-settings 自动兼容大写)
3. env 文件加载逻辑只实现一次
"""
# ── 环境 ──────────────────────────────────────────────────────────────
environment: str = "development"
debug: bool = True
auto_create_schema: bool = False
# ── 数据库 ────────────────────────────────────────────────────────────
database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas"
database_pool_size: int = 20
database_max_overflow: int = 10 # pool_size(20) + max_overflow(10) = 最大30连接
database_pool_timeout: int = 30
database_pool_recycle: int = 3600
# ── Redis ────────────────────────────────────────────────────────────
redis_url: str = "redis://localhost:6379/0"
# ── Celery ───────────────────────────────────────────────────────────
celery_broker_url: str = "redis://localhost:6379/0"
celery_result_backend: str = "redis://localhost:6379/1"
# ── OSS 阿里云 ──────────────────────────────────────────────────────
oss_endpoint: str = "oss-cn-hangzhou.aliyuncs.com"
oss_access_key_id: str = ""
oss_access_key_secret: str = ""
oss_bucket_name: str = "xiaoxia-autocut"
oss_direct_upload_max_mb: int = 2000
oss_direct_upload_expire_seconds: int = 900
# ── CosyVoice (阿里云百炼语音合成) ───────────────────────────────────
cosyvoice_api_key: str = ""
cosyvoice_base_url: str = "https://dashscope.aliyuncs.com/api/v1"
cosyvoice_model: str = "cosyvoice-v3-flash"
cosyvoice_voice: str = "longxiaochun_v3" # 默认音色(v3 系列系统音色带 _v3 后缀)
cosyvoice_sample_rate: int = 22050
cosyvoice_format: str = "mp3" # 输出格式:mp3/wav/pcm
# 音色克隆模型名(固定为 voice-enrollment
cosyvoice_clone_model: str = "voice-enrollment"
# ── 豆包大模型(火山引擎方舟) ────────────────────────────────────────
doubao_api_key: str = ""
doubao_model: str = "doubao-seed-1-6-250615"
doubao_base_url: str = "https://ark.cn-beijing.volces.com/api/v3"
doubao_timeout: int = 30
doubao_max_retries: int = 2
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── 统一单例管理 ────────────────────────────────────────────────────────
# 所有 Settings 类的单例缓存都在这里,消除每处各自实现的重复代码
_settings_cache: dict[str, BaseSettings] = {}
def _get_env_file() -> str:
"""根据 APP_ENV 决定读取哪个 env 文件。"""
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
return env_file if os.path.exists(env_file) else ".env"
def get_cached_settings(settings_class: Type[T], cache_key: Optional[str] = None) -> T:
"""统一的 Settings 单例获取函数。
所有服务都通过这个函数获取配置,消除重复的单例实现。
按类名缓存,同一类只初始化一次。
"""
key = cache_key or settings_class.__name__
if key not in _settings_cache:
env_file = _get_env_file()
_settings_cache[key] = settings_class(_env_file=env_file)
return _settings_cache[key] # type: ignore[return-value]
def reload_settings_cache() -> None:
"""清空配置缓存,下次获取时重新加载。测试用。"""
_settings_cache.clear()
def get_shared_settings() -> SharedSettings:
"""获取共享配置单例(统一入口)。"""
return get_cached_settings(SharedSettings)
+44
View File
@@ -0,0 +1,44 @@
"""Worker 服务配置 — 继承 SharedSettings,只追加 Worker 特有字段。
通用配置(DB/Redis/Celery/OSS/CosyVoice/Doubao 等)统一在
packages/config/base.py 的 SharedSettings 中定义,这里不重复。
"""
from pydantic_settings import SettingsConfigDict
from packages.config.base import SharedSettings, get_cached_settings
class WorkerSettings(SharedSettings):
"""Worker 服务专用配置。
通用配置继承自 SharedSettings,这里只定义 Worker 独有字段。
Celery broker/backend 使用继承的 celery_broker_url / celery_result_backend
历史上 Worker 使用 broker_url / result_backend 字段名,通过 property 别名兼容。
"""
# ── Worker 特有 ────────────────────────────────────────────────────
worker_name: str = "xiaoxia-saas-worker"
worker_concurrency: int = 4
worker_max_tasks_per_child: int = 1000
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ── 向后兼容:Celery 字段名别名 ──────────────────────────────────
@property
def broker_url(self) -> str:
return self.celery_broker_url
@property
def result_backend(self) -> str:
return self.celery_result_backend
def get_worker_settings() -> WorkerSettings:
"""获取 Worker 配置单例(统一入口)。"""
return get_cached_settings(WorkerSettings)
Regular → Executable
+23 -29
View File
@@ -1,36 +1,30 @@
"""Authentication domain services.
"""领域层认证相关端口接口.
Only pure domain authentication helpers are exported here. Infrastructure-backed
services such as Redis session storage and SMTP email delivery live under
`packages.adapters` and should be injected into use cases.
遵循 DDD 依赖倒置原则:领域层定义端口(抽象接口),
外层(应用层、基础设施层)实现端口。
使用方式:
from packages.domain.auth import EmailServicePort, SessionStorePort
from packages.domain.auth import JWTServicePort, PasswordHasherPort
from packages.domain.auth.sms_service import SmsService
注意:具体实现请从对应的适配器层导入:
- 邮件服务: from packages.adapters.smtp import EmailService
- Session存储: from packages.adapters.redis import SessionStore
- JWT服务: from packages.application.auth.jwt_service import JWTService
- 密码哈希: from packages.application.auth.password_hasher import PasswordHasher
"""
from packages.application.auth.jwt_service import (
JWTConfig,
JWTService,
TokenType,
jwt_service,
)
from packages.application.auth.password_hasher import (
PasswordHasher,
PasswordValidator,
password_hasher,
password_validator,
)
from packages.domain.auth.email_service import EmailConfig, EmailService
from packages.domain.auth.session_store import RedisConfig, SessionStore
from packages.domain.auth.email_service import EmailConfig, EmailServicePort
from packages.domain.auth.jwt_service import JWTServicePort
from packages.domain.auth.password_hasher import PasswordHasherPort, PasswordValidatorPort
from packages.domain.auth.session_store import SessionStorePort
__all__ = [
"JWTService",
"JWTConfig",
"TokenType",
"jwt_service",
"PasswordHasher",
"PasswordValidator",
"password_hasher",
"password_validator",
"SessionStore",
"RedisConfig",
"EmailService",
"EmailConfig",
"EmailServicePort",
"JWTServicePort",
"PasswordHasherPort",
"PasswordValidatorPort",
"SessionStorePort",
]
+58 -6
View File
@@ -1,10 +1,62 @@
"""Compatibility import for SMTP email delivery.
"""邮件服务端口(领域层接口).
Infrastructure implementations live under `packages.adapters`. New code should
import `packages.adapters.smtp.email_service` directly or inject an email-sender
port into the use case.
定义邮件发送服务的抽象接口,具体实现由基础设施层(adapters)提供。
遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。
"""
from packages.adapters.smtp.email_service import EmailConfig, EmailService
from __future__ import annotations
__all__ = ["EmailConfig", "EmailService"]
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import List, Optional
@dataclass
class EmailConfig:
"""邮件配置(领域层值对象)"""
smtp_host: str = "smtp.gmail.com"
smtp_port: int = 587
smtp_user: str = ""
smtp_password: str = ""
from_email: str = ""
from_name: str = "小虾 SaaS"
use_tls: bool = True
class EmailServicePort(ABC):
"""邮件服务端口(抽象接口)"""
@abstractmethod
def send_email(
self,
to_email: str,
subject: str,
html_body: str,
text_body: Optional[str] = None,
cc: Optional[List[str]] = None,
bcc: Optional[List[str]] = None,
) -> tuple[bool, Optional[str]]:
"""发送邮件"""
...
@abstractmethod
def send_verification_email(
self,
to_email: str,
verification_code: str,
username: str = "",
) -> tuple[bool, Optional[str]]:
"""发送验证邮件"""
...
@abstractmethod
def send_password_reset_email(
self,
to_email: str,
reset_token: str,
reset_url: str = "",
username: str = "",
) -> tuple[bool, Optional[str]]:
"""发送密码重置邮件"""
...
+44
View File
@@ -0,0 +1,44 @@
"""JWT 服务端口(领域层接口).
定义 JWT Token 生成、验证、解析的抽象接口,具体实现由应用层或基础设施层提供。
遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Any, Dict
class JWTServicePort(ABC):
"""JWT 服务端口(抽象接口)"""
@abstractmethod
def create_access_token(
self,
user_id: str,
role: str = "",
additional_claims: Dict[str, Any] | None = None,
) -> str:
"""创建 access_token"""
...
@abstractmethod
def create_refresh_token(self, user_id: str, session_id: str) -> str:
"""创建 refresh_token"""
...
@abstractmethod
def verify_token(self, token: str) -> Dict[str, Any]:
"""验证任意 Token"""
...
@abstractmethod
def verify_access_token(self, token: str) -> Dict[str, Any]:
"""验证 access_token"""
...
@abstractmethod
def verify_refresh_token(self, token: str) -> Dict[str, Any]:
"""验证 refresh_token"""
...
+38
View File
@@ -0,0 +1,38 @@
"""密码哈希服务端口(领域层接口).
定义密码哈希与验证的抽象接口,具体实现由应用层或基础设施层提供。
遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from typing import Optional, Tuple
class PasswordHasherPort(ABC):
"""密码哈希服务端口(抽象接口)"""
@abstractmethod
def hash_password(self, password: str) -> str:
"""哈希密码"""
...
@abstractmethod
def verify_password(self, password: str, hashed_password: str) -> bool:
"""验证密码"""
...
@abstractmethod
def needs_rehash(self, hashed_password: str) -> bool:
"""检查哈希是否需要重新计算"""
...
class PasswordValidatorPort(ABC):
"""密码强度验证端口(抽象接口)"""
@abstractmethod
def validate(self, password: str) -> Tuple[bool, Optional[str]]:
"""验证密码强度"""
...
+63 -6
View File
@@ -1,10 +1,67 @@
"""Compatibility import for Redis-backed session storage.
"""Session 存储端口(领域层接口).
Infrastructure implementations live under `packages.adapters`. New code should
import `packages.adapters.redis.session_store` directly or inject a session-store
port into the use case.
定义 Session 存储服务的抽象接口,具体实现由基础设施层(adapters)提供。
遵循 DDD 依赖倒置原则:领域层定义端口,外层实现端口。
"""
from packages.adapters.redis.session_store import RedisConfig, SessionStore
from __future__ import annotations
__all__ = ["RedisConfig", "SessionStore"]
from abc import ABC, abstractmethod
from typing import Optional
class SessionStorePort(ABC):
"""Session 存储端口(抽象接口)"""
@abstractmethod
def save_session(
self,
session_id: str,
user_id: str,
refresh_token: str,
user_agent: str = "",
ip_address: str = "",
expires_in_days: int = 7,
) -> bool:
"""保存 Session"""
...
@abstractmethod
def get_session(self, session_id: str) -> Optional[dict]:
"""获取 Session"""
...
@abstractmethod
def get_session_by_refresh_token(self, refresh_token: str) -> Optional[dict]:
"""通过 refresh_token 查找 Session"""
...
@abstractmethod
def get_refresh_token(self, session_id: str) -> Optional[str]:
"""获取 refresh_token"""
...
@abstractmethod
def update_last_active(self, session_id: str) -> bool:
"""更新最后活跃时间"""
...
@abstractmethod
def delete_session(self, session_id: str) -> bool:
"""删除 Session"""
...
@abstractmethod
def get_user_sessions(self, user_id: str) -> list[dict]:
"""获取用户所有 Session"""
...
@abstractmethod
def delete_all_user_sessions(self, user_id: str) -> int:
"""删除用户所有 Session"""
...
@abstractmethod
def session_exists(self, session_id: str) -> bool:
"""检查 Session 是否存在"""
...
+7 -86
View File
@@ -1,90 +1,11 @@
"""统一配置入口 — API 和 Worker 共享的基础配置
"""向后兼容层 — 统一配置已迁移到 packages.config
所有服务通用配置定义在这里,两端各自的 Settings 类继承本类,
只追加服务特有字段。彻底消除重复定义和默认值不一致问题。
新代码请直接使用:
from packages.config import SharedSettings, get_shared_settings
本文件保留仅为兼容旧的 import 路径。
"""
import os
from typing import Optional
from packages.config import SharedSettings, get_cached_settings, get_shared_settings
from pydantic_settings import BaseSettings, SettingsConfigDict
class SharedSettings(BaseSettings):
"""所有服务共享的基础配置。
API 和 Worker 都继承本类,确保:
1. 数据库/Redis/OSS/Celery 等核心配置默认值一致
2. 环境变量命名统一(小写风格,pydantic-settings 自动兼容大写)
3. 单例模式和 env 文件加载逻辑只实现一次
"""
# ── 环境 ──────────────────────────────────────────────────────────────
environment: str = "development"
debug: bool = True
auto_create_schema: bool = False
# ── 数据库 ────────────────────────────────────────────────────────────
database_url: str = "postgresql+psycopg://postgres:postgres@localhost:5432/xiaoxia_saas"
database_pool_size: int = 20
database_max_overflow: int = 10 # pool_size(20) + max_overflow(10) = 最大30连接
database_pool_timeout: int = 30
database_pool_recycle: int = 3600
# ── Redis ────────────────────────────────────────────────────────────
redis_url: str = "redis://localhost:6379/0"
# ── Celery ───────────────────────────────────────────────────────────
celery_broker_url: str = "redis://localhost:6379/0"
celery_result_backend: str = "redis://localhost:6379/1"
# ── OSS 阿里云 ──────────────────────────────────────────────────────
oss_endpoint: str = "oss-cn-hangzhou.aliyuncs.com"
oss_access_key_id: str = ""
oss_access_key_secret: str = ""
oss_bucket_name: str = "xiaoxia-autocut"
oss_direct_upload_max_mb: int = 2000
oss_direct_upload_expire_seconds: int = 900
# ── CosyVoice (阿里云百炼语音合成) ───────────────────────────────────
cosyvoice_api_key: str = ""
cosyvoice_base_url: str = "https://dashscope.aliyuncs.com/api/v1"
cosyvoice_model: str = "cosyvoice-v3-flash"
cosyvoice_voice: str = "longxiaochun_v3" # 默认音色(v3 系列系统音色带 _v3 后缀)
cosyvoice_sample_rate: int = 22050
cosyvoice_format: str = "mp3" # 输出格式:mp3/wav/pcm
# 音色克隆模型名(固定为 voice-enrollment
cosyvoice_clone_model: str = "voice-enrollment"
# ── 豆包大模型(火山引擎方舟) ────────────────────────────────────────
doubao_api_key: str = ""
doubao_model: str = "doubao-seed-1-6-250615"
doubao_base_url: str = "https://ark.cn-beijing.volces.com/api/v3"
doubao_timeout: int = 30
doubao_max_retries: int = 2
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
_settings: Optional[SharedSettings] = None
def get_shared_settings() -> SharedSettings:
"""获取共享配置单例。
优先读取 APP_ENV 指定的环境文件(.env.{env}),不存在则读 .env。
"""
global _settings
if _settings is None:
env = os.getenv("APP_ENV", "development")
env_file = f".env.{env}" if env != "development" else ".env"
if os.path.exists(env_file):
_settings = SharedSettings(_env_file=env_file)
else:
_settings = SharedSettings()
return _settings
__all__ = ["SharedSettings", "get_shared_settings", "get_cached_settings"]
Regular → Executable
+1 -1
View File
@@ -6,7 +6,7 @@ from unittest.mock import MagicMock, patch
import pytest
from packages.domain.auth.email_service import EmailConfig, EmailService
from packages.adapters.smtp.email_service import EmailConfig, EmailService
class TestEmailService:
+3 -4
View File
@@ -5,15 +5,14 @@ from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[2] / "apps" / "api"))
import app.config as app_config
from app.core.storage import OSSStorageService
import packages.shared.config as shared_config
from packages.config import reload_settings_cache
def _reset_settings() -> None:
app_config._settings = None
shared_config._settings = None
"""清空所有配置缓存,让测试设置的环境变量生效。"""
reload_settings_cache()
def test_create_direct_upload_post_limits_key_and_size(monkeypatch):
+8 -8
View File
@@ -15,23 +15,23 @@ import pytest
def _fresh_settings(env: str):
"""清除 config 模块缓存,以指定 APP_ENV 重新导入 Settings
"""清除配置缓存,以指定 APP_ENV 重新创建设置实例
为非开发环境预设 OSS 环境变量,确保模块级 get_settings() 能成功完成导入
为非开发环境预设 OSS 环境变量,确保实例化能成功完成。
测试方法内可根据需要清除这些变量来测试验证器。
"""
for mod_name in [m for m in list(sys.modules) if "app.config" in m]:
del sys.modules[mod_name]
import os
os.environ["APP_ENV"] = env
# 非开发环境下,为模块级导入提供有效凭证(避免导入时验证失败)
if env != "development":
os.environ.setdefault("OSS_ACCESS_KEY_ID", "test-key-for-import")
os.environ.setdefault("OSS_ACCESS_KEY_SECRET", "test-secret-for-import")
# 重置单例,让测试方法自行控制实例化
from apps.api.app import config as _cfg
from apps.api.app.config import Settings
# 清空统一配置缓存,让测试方法自行控制实例化
from packages.config import APISettings as Settings
from packages.config import reload_settings_cache
_cfg._settings = None
reload_settings_cache()
return Settings
Regular → Executable
+1 -1
View File
@@ -7,7 +7,7 @@ from unittest.mock import Mock
import pytest
from packages.domain.auth.session_store import SessionStore
from packages.adapters.redis.session_store import SessionStore
class TestSessionStore: