Files
diary-news/backend/app/config.py
Mavis 60b062daf2 feat: initial MVP - FastAPI backend + Vue3 frontend + docker-compose
- backend: FastAPI + SQLAlchemy 2.0(async) + asyncpg + Alembic
- 7 API routes: auth/me/articles/sources/bookmarks/subscriptions/admin
- models: User/Source/Article/Bookmark/Subscription/ApiToken
- services: RSS fetcher (feedparser) + Tencent TMT translator with quota + cache + local NLLB fallback
- workers: APScheduler + asyncio pipeline (fetch -> dedupe -> insert -> translate)
- seed scripts: create_user, seed_sources (5 RSS: Reuters/BBC/Al Jazeera/NHK/DW)
- frontend: Vue 3 + Vite + Naive UI + Pinia + vue-router
- pages: Login, Feed (24h), ArticleDetail, Sources, Bookmarks, AdminSources
- deploy: docker-compose (postgres/redis/api/worker/frontend/caddy)
- docs: README, DEPLOY, architecture, acceptance
2026-06-07 21:51:01 +08:00

105 lines
2.8 KiB
Python

"""应用配置:从 .env / 环境变量读取,集中管理所有开关。"""
from __future__ import annotations
from functools import lru_cache
from pathlib import Path
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env",
env_file_encoding="utf-8",
case_sensitive=False,
extra="ignore",
)
# ===== 通用 =====
tz: str = "Asia/Hong_Kong"
log_level: str = "INFO"
# ===== 数据库 =====
postgres_user: str
postgres_password: str
postgres_db: str
postgres_host: str = "postgres"
postgres_port: int = 5432
@property
def database_url(self) -> str:
# asyncpg
return (
f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}"
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
@property
def sync_database_url(self) -> str:
# alembic 用的同步 URL
return (
f"postgresql+psycopg2://{self.postgres_user}:{self.postgres_password}"
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}"
)
# ===== Redis =====
redis_host: str = "redis"
redis_port: int = 6379
redis_password: str
redis_db: int = 0
@property
def redis_url(self) -> str:
return (
f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}"
)
# ===== JWT =====
jwt_secret: str
jwt_algorithm: str = "HS256"
access_token_ttl_min: int = 60
refresh_token_ttl_day: int = 14
# ===== 腾讯云 TMT =====
tencentcloud_secret_id: str = ""
tencentcloud_secret_key: str = ""
tencentcloud_region: str = "ap-hongkong"
tencent_tmt_endpoint: str = "tmt.tencentcloudapi.com"
tencent_tmt_quota_month: int = 5_000_000
tencent_tmt_quota_buffer: float = 0.05
tencent_tmt_max_chars_per_req: int = 4500
@field_validator("tencent_tmt_quota_buffer")
@classmethod
def _check_buffer(cls, v: float) -> float:
if not 0.0 <= v <= 0.5:
raise ValueError("buffer 必须在 0~0.5")
return v
# ===== 本地翻译 =====
local_translate_enabled: bool = False
local_translate_model: str = "nllb-200-distilled-600M"
local_translate_device: str = "cpu"
# ===== 抓取 =====
fetch_global_qps: int = 4
fetch_timeout: int = 20
fetch_fail_pause_threshold: int = 3
fetch_max_retries: int = 2
# ===== Caddy / 域名 =====
domain: str = ""
acme_email: str = ""
# ===== 内部路径(部署后可调) =====
project_root: Path = Path(__file__).resolve().parents[2]
@lru_cache
def get_settings() -> Settings:
return Settings() # type: ignore[call-arg]
settings = get_settings()