diff --git a/backend/.env.dev b/backend/.env.dev new file mode 100644 index 0000000..722a9a4 --- /dev/null +++ b/backend/.env.dev @@ -0,0 +1,6 @@ +# Local development +DB_HOST=localhost +DB_PORT=5432 +DB_USER=wecom +DB_PASSWORD=password +DB_NAME=wecom_ai diff --git a/backend/.env.example b/backend/.env.example new file mode 100644 index 0000000..a039a20 --- /dev/null +++ b/backend/.env.example @@ -0,0 +1,10 @@ +# Database (optional: set DATABASE_URL directly, or use DB_* below) +DB_HOST=localhost +DB_PORT=5432 +DB_USER=wecom +DB_PASSWORD=password +DB_NAME=wecom +DATABASE_URL= + +# Sync URL for Alembic (optional; default: built from DB_*) +# DATABASE_URL_SYNC= diff --git a/backend/alembic/env.py b/backend/alembic/env.py index 93bac2f..fabef4a 100644 --- a/backend/alembic/env.py +++ b/backend/alembic/env.py @@ -5,13 +5,13 @@ from sqlalchemy import engine_from_config from sqlalchemy.engine import Connection from sqlalchemy import pool -from app.config import settings from app.models import Base +from config.settings import DATABASE_URL_SYNC config = context.config if config.config_file_name is not None: fileConfig(config.config_file_name) -config.set_main_option("sqlalchemy.url", settings.database_url_sync) +config.set_main_option("sqlalchemy.url", DATABASE_URL_SYNC) target_metadata = Base.metadata diff --git a/backend/app/config.py b/backend/app/config.py index 5298c33..bb68a3b 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -1,13 +1,16 @@ from pydantic_settings import BaseSettings, SettingsConfigDict +# 数据库 URL 由 config.settings 提供(环境变量优先,支持 DB_* 拼接,容器兼容) +from config.settings import DATABASE_URL, DATABASE_URL_SYNC + class Settings(BaseSettings): model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") api_host: str = "0.0.0.0" api_port: int = 8000 - database_url: str = "postgresql+asyncpg://wecom:wecom_secret@localhost:5432/wecom_ai" - database_url_sync: str = "postgresql://wecom:wecom_secret@localhost:5432/wecom_ai" + database_url: str = DATABASE_URL + database_url_sync: str = DATABASE_URL_SYNC jwt_secret: str = "change-me" jwt_algorithm: str = "HS256" diff --git a/backend/config.py b/backend/config.py deleted file mode 100644 index 0dcd925..0000000 --- a/backend/config.py +++ /dev/null @@ -1,26 +0,0 @@ -from pydantic_settings import BaseSettings, SettingsConfigDict - - -class Settings(BaseSettings): - model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8", extra="ignore") - - api_host: str = "0.0.0.0" - api_port: int = 8000 - database_url: str = "postgresql+asyncpg://wecom:wecom_secret@localhost:5432/wecom_ai" - database_url_sync: str = "postgresql://wecom:wecom_secret@localhost:5432/wecom_ai" - jwt_secret: str = "change-me" - jwt_algorithm: str = "HS256" - jwt_expire_minutes: int = 60 - wecom_corp_id: str = "" - wecom_agent_id: str = "" - wecom_secret: str = "" - wecom_token: str = "" - wecom_encoding_aes_key: str = "" - wecom_api_base: str = "https://qyapi.weixin.qq.com" - wecom_api_timeout: int = 10 - wecom_api_retries: int = 2 - log_level: str = "INFO" - log_json: bool = True - - -settings = Settings() diff --git a/backend/config/__init__.py b/backend/config/__init__.py new file mode 100644 index 0000000..1afe696 --- /dev/null +++ b/backend/config/__init__.py @@ -0,0 +1 @@ +# Config package: env-based settings, DATABASE_URL from env or DB_*. diff --git a/backend/config/settings.py b/backend/config/settings.py new file mode 100644 index 0000000..cecba22 --- /dev/null +++ b/backend/config/settings.py @@ -0,0 +1,48 @@ +""" +统一配置加载:环境变量优先,fallback 到 .env,无 DATABASE_URL 时按 DB_* 拼接。 +支持多环境(dev/prod)、容器(DB_HOST=db)、向后兼容。 +""" +import os +from pathlib import Path + +# 环境变量优先;fallback 到 .env(不覆盖已存在的系统变量) +def _load_dotenv(): + try: + from dotenv import load_dotenv + except ImportError: + return + env = os.getenv("ENV", "").lower() + base = Path(__file__).resolve().parent.parent + if env == "prod": + load_dotenv(base / ".env.prod", override=False) + elif env == "dev": + load_dotenv(base / ".env.dev", override=False) + load_dotenv(base / ".env", override=False) + + +_load_dotenv() + + +def build_database_url_async() -> str: + """拼接异步数据库 URL(postgresql+asyncpg)。""" + user = os.getenv("DB_USER", "wecom") + password = os.getenv("DB_PASSWORD", "password") + host = os.getenv("DB_HOST", "localhost") + port = os.getenv("DB_PORT", "5432") + db = os.getenv("DB_NAME", "wecom_ai") + return f"postgresql+asyncpg://{user}:{password}@{host}:{port}/{db}" + + +def build_database_url_sync() -> str: + """拼接同步数据库 URL(postgresql,Alembic/psycopg2)。""" + user = os.getenv("DB_USER", "wecom") + password = os.getenv("DB_PASSWORD", "password") + host = os.getenv("DB_HOST", "localhost") + port = os.getenv("DB_PORT", "5432") + db = os.getenv("DB_NAME", "wecom_ai") + return f"postgresql://{user}:{password}@{host}:{port}/{db}" + + +# 优先使用环境变量,否则按 DB_* 拼接(向后兼容) +DATABASE_URL: str = os.getenv("DATABASE_URL") or build_database_url_async() +DATABASE_URL_SYNC: str = os.getenv("DATABASE_URL_SYNC") or build_database_url_sync() diff --git a/backend/docs/config.md b/backend/docs/config.md new file mode 100644 index 0000000..d8bf027 --- /dev/null +++ b/backend/docs/config.md @@ -0,0 +1,59 @@ +# 环境配置说明 + +## 多环境与加载顺序 + +- **系统环境变量** 优先于任何文件。 +- **config/settings.py** 会按 `ENV` 选择 dotenv 文件,再统一从环境变量或 `DB_*` 拼接数据库 URL。 +- 若已设置 `DATABASE_URL` / `DATABASE_URL_SYNC`,则直接使用,否则用 `DB_HOST`、`DB_PORT`、`DB_USER`、`DB_PASSWORD`、`DB_NAME` 自动拼接。 + +## 环境与文件 + +| 环境 | 建议使用的文件 | 说明 | +|------|----------------|------| +| **dev** | `.env.dev` | 本地开发,`DB_HOST=localhost` | +| **prod / Docker** | `.env.prod` 或 compose 传入的 env | 容器内 `DB_HOST=db`(compose 服务名) | +| 任意 | `.env` | 可选,可覆盖部分变量 | + +## 使用方式 + +### 本地开发(dev) + +```bash +cd backend +cp .env.example .env.dev # 首次可复制后按需修改 +# 可选:设置 ENV=dev 以显式加载 .env.dev +export ENV=dev +# 或直接依赖系统已设置的 DB_* / DATABASE_URL +alembic upgrade head +uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 +``` + +使用 `.env.dev` 时,默认 `DB_HOST=localhost`,数据库连接本机 PostgreSQL。 + +### Docker / 生产(prod) + +- 在 **docker-compose** 或 **k8s** 中通过环境变量传入配置,不要写死 localhost。 +- 示例:在 compose 中为 backend 服务设置: + + ```yaml + environment: + - DB_HOST=db + - DB_PORT=5432 + - DB_USER=wecom + - DB_PASSWORD=xxx + - DB_NAME=wecom_ai + ``` + + 或使用 `env_file: .env.prod`(`.env.prod` 中 `DB_HOST=db`)。 + +- 容器内应用和 Alembic 迁移都会使用上述变量;不再依赖 localhost。 + +### CI/CD + +- 在流水线中设置 `DB_HOST`、`DB_PORT`、`DB_USER`、`DB_PASSWORD`、`DB_NAME`,或直接设置 `DATABASE_URL` / `DATABASE_URL_SYNC`。 +- 无需在代码或镜像中写死数据库地址。 + +## 向后兼容 + +- 若仍在使用旧版只配置 `DATABASE_URL` 的 `.env`,无需修改,会继续生效。 +- 未设置 `DATABASE_URL` 时,将使用 `DB_*` 拼接;未设置 `DB_NAME` 时默认库名为 `wecom_ai`。 diff --git a/deploy/.env.prod.example b/deploy/.env.prod.example index eef16cb..11b2e85 100644 --- a/deploy/.env.prod.example +++ b/deploy/.env.prod.example @@ -4,8 +4,8 @@ # 镜像标签 TAG=latest -# 数据库配置 -DATABASE_URL=postgresql://postgres:CHANGE_ME@db:5432/wecom_ai +# 数据库配置(backend 也支持仅设置 DB_HOST/DB_PORT/DB_USER/DB_PASSWORD/DB_NAME,自动拼接) +DATABASE_URL=postgresql+asyncpg://postgres:CHANGE_ME@db:5432/wecom_ai DATABASE_URL_SYNC=postgresql://postgres:CHANGE_ME@db:5432/wecom_ai POSTGRES_USER=postgres POSTGRES_PASSWORD=CHANGE_ME diff --git a/deploy/docker-compose.prod.yml b/deploy/docker-compose.prod.yml index 6a0aa9f..c60db0b 100644 --- a/deploy/docker-compose.prod.yml +++ b/deploy/docker-compose.prod.yml @@ -61,8 +61,13 @@ services: ports: - "80:80" - "443:443" - volumes: - - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + # 注意:镜像构建时已经包含了配置,无需挂载 + # 如果需要覆盖配置,取消下面的注释并确保 nginx.conf 路径正确 + # volumes: + # - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + environment: + - NGINX_HOST=_ + - NGINX_PORT=80 depends_on: - backend - admin diff --git a/docker-compose.yml b/docker-compose.yml index fc365c6..46f1540 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -44,7 +44,7 @@ services: ports: - "80:80" volumes: - - ./deploy/nginx.conf:/etc/nginx/nginx.conf:ro + - ./deploy/nginx.conf:/etc/nginx/conf.d/default.conf:ro depends_on: - backend - admin