Files
DadaoTask/backend/app/config.py
T
Steven e25a1a4e31
CI / backend (push) Canceled after 0s
feat(task): M1 任务域后端与 Web 列表(状态机/乐观锁/视图/搜索)
- item/tag/item_tag/comment/activity 五表迁移,RLS 全覆盖,pg_trgm 子串索引
- 状态机 domain/task.py:非法流转回 5002 并给 allowed,done 写 completed_at
- /workspaces/{ws}/items 列表/详情/新建/改/状态/软删/评论/标签
- 乐观锁 UPDATE...WHERE version,冲突 4009 + current_version
- Web 任务页:视图 tab、快速新建、勾选完成、开工/阻塞、详情事件流、游标分页
- 测试 43 全绿,ruff 干净,Web 构建通过
- 文档:2.Design/API.md 新增,ROADMAP 按已验证口径重写
2026-10-28 03:48:48 +08:00

115 lines
5.2 KiB
Python
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.
"""分层配置:dev / prod 全部走环境变量,启动即校验。"""
from __future__ import annotations
import os
from functools import lru_cache
from typing import Literal
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
DEV_JWT_SECRET = "dev-only-change-me" # noqa: S105 - 仅开发默认值,生产启动时会被校验拦下
class Settings(BaseSettings):
model_config = SettingsConfigDict(
env_file=".env", env_file_encoding="utf-8", extra="ignore", case_sensitive=False
)
# ── 基础 ──────────────────────────────────────────────
app_name: str = "达道Task"
env: Literal["dev", "test", "prod"] = "dev"
api_prefix: str = "/api/v1"
cors_origins: list[str] = Field(
default_factory=lambda: ["http://localhost:5180", "http://127.0.0.1:5180"]
)
# ── 数据库 ────────────────────────────────────────────
# 应用角色:受 RLS 约束,非 owner、非 superuser
database_url: str = "postgresql+asyncpg://dadao_app:dadao_dev_pw@127.0.0.1:5433/dadaotask"
# 迁移角色:owner,可绕过 RLS,仅 alembic / 运维脚本使用
migration_database_url: str = (
"postgresql+asyncpg://dadao_owner:dadao_dev_pw@127.0.0.1:5433/dadaotask"
)
# 测试专用库:测试会 TRUNCATE 所有表,绝不能落到开发库上。
# 留空则退回 database_url(CI 里直接给 DATABASE_URL 即可)。
test_database_url: str | None = None
test_migration_database_url: str | None = None
db_echo: bool = False
db_pool_size: int = 10
# ── Redis ─────────────────────────────────────────────
redis_url: str = "redis://127.0.0.1:6380/0"
# ── 业务 ──────────────────────────────────────────────
# "今天/本周"这类视图按用户时区切日界,不能按 UTC —— 中国用户 23:30 建的任务
# 在 UTC 下已经是"明天"了。M7 多地域时改成按工作区配置。
default_timezone: str = "Asia/Shanghai"
# ── 认证 ──────────────────────────────────────────────
jwt_secret: str = DEV_JWT_SECRET
jwt_algorithm: str = "HS256"
access_token_minutes: int = 30
refresh_token_days: int = 14
sms_code_ttl_seconds: int = 300
# 同一手机号两次发码的最小间隔;测试里设为 0 可关闭节流
sms_throttle_seconds: int = 60
sms_dev_code: str = "123456"
sms_provider: Literal["dev", "aliyun", "tencent"] = "dev"
# ── 幂等 ──────────────────────────────────────────────
idempotency_enabled: bool = True
idempotency_ttl_seconds: int = 86_400
# ── 构建标记(部署校验用)──────────────────────────────
build_commit: str = "unknown"
build_time: str = "unknown"
@field_validator("cors_origins", mode="before")
@classmethod
def _split_origins(cls, value: object) -> object:
"""支持 CORS_ORIGINS="a,b" 这种写法。"""
if isinstance(value, str) and not value.startswith("["):
return [item.strip() for item in value.split(",") if item.strip()]
return value
def validate_runtime(self) -> None:
"""生产环境的致命配置错误,必须在启动时炸掉,而不是等待请求。"""
if self.env != "prod":
return
problems: list[str] = []
if self.jwt_secret == DEV_JWT_SECRET or len(self.jwt_secret) < 32:
problems.append("JWT_SECRET 仍是开发默认值或长度不足 32")
if "dadao_dev_pw" in self.database_url:
problems.append("DATABASE_URL 仍在用开发默认口令")
if self.sms_provider == "dev":
problems.append("生产环境不允许 SMS_PROVIDER=dev(任意验证码可登录)")
if problems:
raise RuntimeError("配置校验失败:" + ";".join(problems))
def resolved_database_url(self) -> str:
"""应用实际连的库:测试环境优先用独立测试库(测试会 TRUNCATE)。"""
if self.env == "test" and self.test_database_url:
return self.test_database_url
return self.database_url
def resolved_migration_database_url(self) -> str:
"""迁移/清表实际连的库:必须和应用同一个库,否则清表清到别处去了。"""
if self.env == "test" and self.test_migration_database_url:
return self.test_migration_database_url
return self.migration_database_url
@lru_cache
def get_settings() -> Settings:
return Settings()
def build_commit() -> str:
"""优先读环境变量(CI/镜像注入),退回 git。"""
env_value = os.getenv("BUILD_COMMIT")
if env_value:
return env_value
return get_settings().build_commit