import os from functools import lru_cache from typing import Annotated, Any from pydantic import Field, field_validator from pydantic_settings import BaseSettings, NoDecode, SettingsConfigDict from surugaya_common import redis_keys # 根据 APP_ENV 确定环境配置文件(默认为 dev) APP_ENV = os.getenv("APP_ENV", "dev").lower() ENV_FILE = f".env.{APP_ENV}" class Settings(BaseSettings): """ 全局应用配置模型。 使用 pydantic-settings 校验类型并自动解析复杂数据类型。 """ # 运行环境 app_env: str = Field(default="dev", alias="APP_ENV") # 日志配置 log_level: str = Field(default="INFO", alias="LOG_LEVEL") log_dir: str = Field(default="./logs", alias="LOG_DIR") # 节点配置 node_id: str = Field(default="node_1", alias="NODE_ID") # Redis 配置 redis_host: str = Field(default="127.0.0.1", alias="REDIS_HOST") redis_port: int = Field(default=6379, alias="REDIS_PORT") redis_password: str | None = Field(default=None, alias="REDIS_PASSWORD") redis_db: int = Field(default=0, alias="REDIS_DB") redis_stream_key: str = Field(default=redis_keys.PURCHASE_TASK_STREAM, alias="REDIS_STREAM_KEY") redis_consumer_group: str = Field(default="surugaya_purchase_task_group", alias="REDIS_CONSUMER_GROUP") redis_consumer_name: str = Field(default="surugaya_purchase_task_worker", alias="REDIS_CONSUMER_NAME") redis_delay_zset_key: str = Field(default=redis_keys.TRADE_DELAY_ZSET, alias="REDIS_DELAY_ZSET_KEY") redis_delay_poll_interval: int = Field(default=3600, alias="REDIS_DELAY_POLL_INTERVAL") redis_delay_batch_size: int = Field(default=10, alias="REDIS_DELAY_BATCH_SIZE") # 浏览器控制 browser_headless: bool = Field(default=True, alias="BROWSER_HEADLESS") browser_executable_path: str | None = Field(default=None, alias="BROWSER_EXECUTABLE_PATH") browser_keep_open: bool = Field(default=False, alias="BROWSER_KEEP_OPEN") # 配置与资源路径 accounts_config_path: str = Field(default="./accounts.yaml", alias="ACCOUNTS_CONFIG_PATH") screenshot_dir: str = Field(default="./archives", alias="SCREENSHOT_DIR") # 任务上报配置 callback_url: str | None = Field(default=None, alias="CALLBACK_URL") appid: str | None = Field(default=None, alias="APPID") app_secret: str | None = Field(default=None, alias="APP_SECRET") # PayPay 配置 paypay_phone: str | None = Field(default=None, alias="PAYPAY_PHONE") paypay_password: str | None = Field(default=None, alias="PAYPAY_PASSWORD") # 通知配置 notify_enabled: bool = Field(default=False, alias="NOTIFY_ENABLED") notify_channels: Annotated[list[str], NoDecode] = Field( default_factory=lambda: ["email"], alias="NOTIFY_CHANNELS", ) notify_on_task_failed: bool = Field(default=True, alias="NOTIFY_ON_TASK_FAILED") # 邮件通知配置 mail_enabled: bool = Field(default=True, alias="MAIL_ENABLED") mail_smtp_host: str | None = Field(default=None, alias="MAIL_SMTP_HOST") mail_smtp_port: int = Field(default=465, alias="MAIL_SMTP_PORT") mail_smtp_use_ssl: bool = Field(default=True, alias="MAIL_SMTP_USE_SSL") mail_smtp_starttls: bool = Field(default=False, alias="MAIL_SMTP_STARTTLS") mail_username: str | None = Field(default=None, alias="MAIL_USERNAME") mail_password: str | None = Field(default=None, alias="MAIL_PASSWORD") mail_from_address: str | None = Field(default=None, alias="MAIL_FROM_ADDRESS") mail_from_name: str = Field(default="JP Purchase Bot", alias="MAIL_FROM_NAME") mail_to_addresses: Annotated[list[str], NoDecode] = Field( default_factory=list, alias="MAIL_TO_ADDRESSES", ) mail_subject_prefix: str = Field(default="[JP-Purchase]", alias="MAIL_SUBJECT_PREFIX") mail_timeout_seconds: int = Field(default=10, alias="MAIL_TIMEOUT_SECONDS") @field_validator("notify_channels", "mail_to_addresses", mode="before") @classmethod def _parse_csv_list(cls, value: Any) -> list[str]: if value is None: return [] if isinstance(value, str): return [item.strip() for item in value.split(",") if item.strip()] if isinstance(value, (list, tuple, set)): return [str(item).strip() for item in value if str(item).strip()] return [str(value).strip()] if str(value).strip() else [] # Pydantic Settings 配置项 model_config = SettingsConfigDict( env_file=ENV_FILE, env_file_encoding="utf-8", extra="ignore" # 忽略其他系统环境变量 ) @lru_cache def get_settings() -> Settings: """ 获取缓存的全局配置单例。 """ return Settings()