886e781a4f
新增 spec:让 dev 运行与 pytest 都跑在 Docker 化的 PostgreSQL 上,退掉 SQLite,使 admin 报表聚合可用 PG 专有函数(不再内存聚合)。方案 A: docker-compose + scripts/ensure_pg.py,run.bat/run.sh/conftest 共用。 顺带把 .worktrees/ 加入 .gitignore(superpowers 隔离工作区)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
209 lines
14 KiB
Markdown
209 lines
14 KiB
Markdown
# 本地开发切 Docker PostgreSQL —— 设计文档
|
||
|
||
> 让本地开发与测试统一跑在 Docker 化的 PostgreSQL 上,彻底退掉 SQLite。
|
||
> `run.bat` / `run.sh` 启动时自动检测本机 PG,没起就拉起 Docker → 起 PG 容器(镜像缺失先拉),
|
||
> 目的是让开发/大模型能放心用 PG 专有的高效聚合函数,不再为兼容 SQLite 而退化成"取基础数据后内存聚合"。
|
||
>
|
||
> 状态:已定稿(待用户复核)。作者对话日期:2026-07-08。
|
||
> 关联:[postgres-migration.md](../../database/postgres-migration.md)(切引擎完整步骤)、`scripts/init_postgres.py`(生产原生 PG 初始化)。
|
||
|
||
---
|
||
|
||
## 1. 背景与目标
|
||
|
||
### 问题
|
||
当前开发环境默认用 SQLite(`DATABASE_URL=sqlite:///./data/app.db`),生产用 PostgreSQL 16。两套引擎并存,导致写数据访问代码时(尤其 `app/admin/repositories/` 的报表聚合)为了"两边都能跑",放弃 PG 专有能力(窗口函数、`FILTER`、`JSONB` 操作符、`GROUPING SETS` 等),改成"先查基础数据、再在 Python 内存里聚合"——既慢又啰嗦。
|
||
|
||
### 目标
|
||
本地开发与测试都跑在 PG 上,SQLite 退出本地开发闭环。之后写 PG 专有 SQL 时:
|
||
- 开发运行时(`run.bat`/`run.sh`)直接连 PG,手动验证可行;
|
||
- `pytest` 也连 PG,PG 专有 SQL 在被测代码路径里也安全,不会因 SQLite 而挂——**这是"双库兼容代码彻底消失"的必要条件**。
|
||
|
||
### 非目标(本期不做)
|
||
- 不动**生产**部署(生产仍是原生 PG16 + systemd,无 Docker;`init_postgres.py` 保持不变)。
|
||
- 不做数据搬迁(MVP 阶段无真实用户数据,详见迁移指南背景假设)。
|
||
- 不接 CI(仓库当前无 `.github/workflows`;若将来加 CI,再单独让 CI 起 PG service)。
|
||
- 不引入 Redis / testcontainers / 连接池中间件。
|
||
|
||
---
|
||
|
||
## 2. 决策记录(本次对话已拍板)
|
||
|
||
| # | 决策点 | 结论 | 理由 |
|
||
|---|---|---|---|
|
||
| D1 | PG 覆盖范围 | **dev 运行 + 测试都切 PG** | 只切运行时的话,被 SQLite 测试覆盖的代码路径(如 `test_cps_admin.py` 覆盖的 `admin/repositories/cps.py`)仍不能用 PG 专有 SQL,双库代码不会真正消失 |
|
||
| D2 | 打包方式 | **方案 A:Compose + `scripts/ensure_pg.py`** | 唯一真正需要定制的部分(启动 Docker 守护进程、等 PG 就绪)集中到一个跨平台模块,`run.bat`/`run.sh`/`conftest.py` 共用;声明式的容器/卷/健康检查交给 Compose |
|
||
| D3 | 宿主端口 | **5432**(与生产/文档一致) | 边界:若本机已有原生 PG 占 5432,`ensure_pg` 会探测到"PG 已在"直接复用它(可能连到不带业务库的实例)——见 §7 风险,文档提示 |
|
||
| D4 | dev 下 `DATABASE_URL` 仍是 sqlite | **硬失败**(打印一行 fix 后非 0 退出) | 彻底断掉 SQLite 退路,符合"让大家都用 PG"的目标 |
|
||
| D5 | dev 数据库密码 | 固定 `shaguabijia_dev_pw`,写进 compose + `.env.example` | 本地容器仅绑 `localhost`,非机密;保证 `.env.example` 复制即可用。生产密码另由 `init_postgres.py` 强随机生成,不复用 |
|
||
| D6 | 改哪些启动脚本 | `run.bat` 与 `run.sh` **都改** | 仓库一贯保持两者同步 |
|
||
| D7 | 镜像 | `postgres:16-alpine` | 对齐生产 PG16;alpine 体积小 |
|
||
|
||
---
|
||
|
||
## 3. 现状(改动前)
|
||
|
||
- **配置**:`app/core/config.py` `DATABASE_URL` 默认 `sqlite:///./data/app.db`,pydantic-settings 从 `.env` 读(环境变量优先级高于 `.env` 文件)。
|
||
- **引擎**:`app/db/session.py` 用 `_is_sqlite = DATABASE_URL.startswith("sqlite")` 分流——SQLite 加 `check_same_thread=False`、不建池;非 SQLite 加 `pool_size=10/max_overflow=20/pool_recycle=3600`。**已天然支持 PG,无需改。**
|
||
- **迁移**:`alembic/env.py` 从 `settings.DATABASE_URL` 读连接串,`render_as_batch` 仅对 sqlite 开;PG 下自动关。**无 psycopg2 硬编码,切 PG 无需改。**
|
||
- **驱动**:`pyproject.toml` 已装 `psycopg[binary]>=3.1`(psycopg3)。URL scheme 必须 `postgresql+psycopg://`(裸 `postgresql://` 会被 SQLAlchemy 路由到未安装的 psycopg2 → ModuleNotFoundError)。
|
||
- **测试**:`tests/conftest.py` 在 import app 前把 `DATABASE_URL` 设成临时文件 SQLite;session 级 autouse fixture 做 `Base.metadata.create_all(engine)` / 结束 `drop_all`(schema 来自 model 而非 alembic,无逐用例 rollback,全会话共享一个库)。
|
||
- **启动脚本**:`run.bat` / `run.sh` 均为:校验 `.env` 存在 → `mkdir data` → `alembic upgrade head` → uvicorn 监听 `0.0.0.0:8770`(`.sh` 带 `--reload --reload-dir app`)。
|
||
- **现有 PG 资产**:`scripts/init_postgres.py`(交互式:建用户/建库/授权/写 .env/跑迁移,面向**已装好的原生 PG**)、`docs/database/postgres-migration.md`(切引擎完整步骤,含 §2 测试切 PG、§2.2 会暴露的真 bug 清单)。
|
||
- **CI**:无(`.github/workflows` 不存在),故测试切 PG 无 CI 联动负担。
|
||
|
||
---
|
||
|
||
## 4. 方案详解
|
||
|
||
### 4.1 新增 `docker-compose.yml`(app-server 根目录)
|
||
```yaml
|
||
services:
|
||
postgres:
|
||
image: postgres:16-alpine
|
||
container_name: shaguabijia-pg
|
||
environment:
|
||
POSTGRES_USER: shaguabijia_app
|
||
POSTGRES_PASSWORD: shaguabijia_dev_pw
|
||
POSTGRES_DB: shaguabijia
|
||
ports:
|
||
- "5432:5432"
|
||
volumes:
|
||
- pgdata:/var/lib/postgresql/data
|
||
- ./docker/initdb:/docker-entrypoint-initdb.d:ro
|
||
healthcheck:
|
||
test: ["CMD-SHELL", "pg_isready -U shaguabijia_app -d shaguabijia"]
|
||
interval: 3s
|
||
timeout: 3s
|
||
retries: 20
|
||
volumes:
|
||
pgdata:
|
||
```
|
||
- `POSTGRES_USER` 设定后,该用户以超级用户身份创建并拥有 `POSTGRES_DB`,故能再建测试库。
|
||
- 首启 initdb 脚本建测试库(见 4.2)。命名卷 `pgdata` 让数据跨重启留存。
|
||
|
||
### 4.2 新增 `docker/initdb/01-create-test-db.sql`
|
||
```sql
|
||
-- 仅在 pgdata 卷首次初始化时执行一次。以 shaguabijia_app(超级用户)连 shaguabijia 库运行。
|
||
CREATE DATABASE shaguabijia_test OWNER shaguabijia_app;
|
||
```
|
||
|
||
### 4.3 新增 `scripts/ensure_pg.py`(纯标准库 + docker CLI,跨平台)
|
||
对外同时暴露**可导入函数** `ensure()`(供 `conftest.py` 直接调)和 **CLI 入口** `if __name__ == "__main__": sys.exit(0 if ensure() else 1)`(供 `run` 以 `python -m scripts.ensure_pg` 跑)。`ensure()` 从 `app.core.config.settings` 读 `DATABASE_URL`,解析 host/port,主流程:
|
||
1. **sqlite 守卫**:若 `DATABASE_URL` 以 `sqlite` 开头 → 打印"dev 已切 PG,请把 .env 的 DATABASE_URL 改成 `postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia`"→ 非 0 退出(D4)。
|
||
2. **TCP 探测** `host:port`(stdlib `socket`,超时 1s)。通 → 打印"✅ PG 已就绪"直接返回(幂等:PG 已在跑时开销≈一次握手)。
|
||
3. 不通 → `docker version` 探 CLI;缺失 → 中文报错"请先安装 Docker Desktop:https://www.docker.com/products/docker-desktop/" → 非 0 退出。
|
||
4. `docker info` 探守护进程;不通 → 按平台启动:
|
||
- Windows:`start "" "%ProgramFiles%\Docker\Docker\Docker Desktop.exe"`(找不到则报错让用户手动开)
|
||
- macOS:`open -a Docker`
|
||
- Linux:不自动 sudo,打印 `sudo systemctl start docker` 让用户执行后重试
|
||
然后轮询 `docker info` 直到就绪或超时(默认 120s,每 3s 一次,打印进度)。
|
||
5. `docker compose up -d`(Compose 在镜像缺失时**自动拉取**,首用拉 alpine ~90MB;有进度输出)。
|
||
6. 轮询 healthcheck(`docker inspect` 的 health 状态)/ TCP 直到 PG 接受连接(默认 60s 超时)。
|
||
7. **幂等确保测试库存在**(兼容"老 pgdata 卷没跑过 initdb"的情况):
|
||
`docker compose exec -T postgres psql -U shaguabijia_app -tc "SELECT 1 FROM pg_database WHERE datname='shaguabijia_test'"`,不存在则 `CREATE DATABASE shaguabijia_test OWNER shaguabijia_app`。
|
||
|
||
失败即清晰中文报错 + 非 0 退出,**全程不回退 SQLite**。所有超时可用环境变量覆盖(如 `ENSURE_PG_DOCKER_TIMEOUT`)。
|
||
|
||
### 4.4 `run.bat` / `run.sh` 接线
|
||
在 `alembic upgrade head` **之前**插一行调用,失败即退出:
|
||
- `run.sh`:`"$PY" -m scripts.ensure_pg`(`set -e` 已在,失败自动退出)
|
||
- `run.bat`:`call "%PY%" -m scripts.ensure_pg` + `if errorlevel 1 exit /b 1`
|
||
|
||
其余逻辑不动(`mkdir data` 保留给 media 等落盘目录)。
|
||
|
||
### 4.5 `.env.example` 默认切 PG
|
||
把
|
||
```ini
|
||
DATABASE_URL=sqlite:///./data/app.db
|
||
```
|
||
改为
|
||
```ini
|
||
# 本地开发/测试统一用 Docker PG(run.bat/run.sh 会自动拉起容器;详见 docs/database/postgres-migration.md §本地 Docker 一键起)。
|
||
# 生产用原生 PG,由 scripts/init_postgres.py 写入强随机密码的连接串。
|
||
DATABASE_URL=postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia
|
||
```
|
||
|
||
### 4.6 `tests/conftest.py` 切 PG
|
||
调整顶部顺序(仍必须在 `import app.*` 之前完成 env 设定):
|
||
1. 设 `os.environ["DATABASE_URL"] = "postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia_test"`(测试库,永不碰 dev 业务库)。
|
||
2. 调 `scripts.ensure_pg.ensure()`(保证容器在 + 测试库在;PG 已在时几乎零开销)。
|
||
3. `import app...`。
|
||
session 级 autouse fixture:改为 **`Base.metadata.drop_all(engine)` → `create_all(engine)`(开头先清干净,防持久卷里上一次跑残留的表/数据)→ yield → 结束 `drop_all`**;删掉临时 SQLite 文件相关代码。
|
||
|
||
> 预期:部分用例会因 PG 的严格性变红(SQLite 宽松、PG 严格),按迁移指南 §2.2 逐个修——常见为:字符串/整数隐式比较、`datetime.utcnow()` naive vs `TIMESTAMPTZ`、事务边界(`current transaction is aborted`)。这既是工作量也是本次改造的**直接收益**(暴露真 bug)。实现阶段需为"跑 pytest 并修红用例"单列步骤。
|
||
|
||
### 4.7 `app/db/session.py`
|
||
**无需改动**——`_is_sqlite` 为假时自动走 PG 池化分支。
|
||
|
||
### 4.8 文档
|
||
- `docs/database/postgres-migration.md` 增一节「本地 Docker 一键起 PG(推荐)」,指向 compose + `ensure_pg`,并说明它替代了 §1.1 的手动 brew/apt 装 PG。
|
||
- `CLAUDE.md` 的 DB 段注明:dev/test = Docker PG(`run` 自动拉起);prod = 原生 PG(`init_postgres.py`)。
|
||
- `scripts/init_postgres.py` 顶部注释补一句"本脚本面向生产原生 PG;本地开发用 docker-compose + scripts/ensure_pg"。
|
||
|
||
---
|
||
|
||
## 5. 连接参数汇总
|
||
|
||
| 项 | 值 |
|
||
|---|---|
|
||
| 镜像 | `postgres:16-alpine` |
|
||
| 容器名 | `shaguabijia-pg` |
|
||
| 宿主端口 | `5432` |
|
||
| 超级/业务用户 | `shaguabijia_app` |
|
||
| dev 密码 | `shaguabijia_dev_pw`(本地非机密) |
|
||
| 业务库(dev 运行) | `shaguabijia` |
|
||
| 测试库(pytest) | `shaguabijia_test` |
|
||
| dev `DATABASE_URL` | `postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia` |
|
||
| test `DATABASE_URL` | `postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia_test` |
|
||
| 数据持久化 | 命名卷 `pgdata` |
|
||
|
||
---
|
||
|
||
## 6. 失败处理矩阵(无 SQLite 兜底)
|
||
|
||
| 情形 | ensure_pg 行为 |
|
||
|---|---|
|
||
| `DATABASE_URL` 是 sqlite | 打印应改成的 PG 串 → 非 0 退出 |
|
||
| PG 已在跑(TCP 通) | 打印"已就绪" → 返回 0(跳过 docker) |
|
||
| 无 docker CLI | 提示装 Docker Desktop + 官网链接 → 非 0 退出 |
|
||
| docker 守护进程未起 | 尝试按平台启动 Docker Desktop,轮询到就绪;超时则报错 → 非 0 退出 |
|
||
| 镜像缺失 | `docker compose up -d` 自动拉取(不额外处理) |
|
||
| 容器起了但 PG 未 ready | 轮询 healthcheck 到超时;超时报错 → 非 0 退出 |
|
||
| 老 pgdata 卷缺测试库 | 幂等 `CREATE DATABASE shaguabijia_test` |
|
||
|
||
---
|
||
|
||
## 7. 风险与边界
|
||
|
||
- **端口占用(原生 PG 撞 5432)**:D3 选了 5432。若开发机已有原生 PG 监听 5432,step 2 的 TCP 探测会判"PG 已在"并复用它——但那个实例可能没有 `shaguabijia`/`shaguabijia_test` 库或用户,后续 `alembic upgrade head` / 测试会报连不上库或认证失败。**缓解**:文档提示"本机别再单独跑原生 PG";报错信息里提示检查是不是撞了原生 PG。
|
||
- **首次启动慢**:首用需 Docker Desktop 冷启(~30–60s)+ 拉镜像(~数十秒~数分钟,视网络)。`ensure_pg` 全程打印进度,超时可配。
|
||
- **`DATABASE_URL` 环境变量优先级**:pydantic-settings 里 shell 环境变量优先于 `.env`。若开发者 shell 残留旧的 `DATABASE_URL`(如指向 sqlite),会盖过 `.env`。sqlite 守卫(D4)能挡住 sqlite 残留;但若残留的是另一个 PG 串,则以它为准——文档提示。
|
||
- **持久卷脏状态**:测试用 drop_all→create_all 开头清库,避免上次崩溃残留污染;dev 业务库随卷留存(符合预期)。
|
||
- **Docker 未安装/公司网络拉镜像受限**:硬失败并给出明确指引;不提供 SQLite 退路是刻意选择(D4/目标)。
|
||
|
||
---
|
||
|
||
## 8. 验收标准
|
||
|
||
1. 全新机器(装了 Docker Desktop、`.env` 从 `.env.example` 复制)执行 `run.bat`(或 `run.sh`):自动拉起 Docker→起 PG 容器→建库→`alembic upgrade head`→uvicorn 起在 8770,无手动装 PG 步骤。
|
||
2. `docker ps` 见 `shaguabijia-pg` 健康;`psql`/客户端能连 `shaguabijia` 与 `shaguabijia_test` 两个库。
|
||
3. PG 已在跑时再次 `run`,`ensure_pg` 秒过(不重复拉容器)。
|
||
4. `pytest` 连 `shaguabijia_test` 跑;红用例全部修绿(PG 严格性暴露的问题)。
|
||
5. `.env` 的 `DATABASE_URL` 改回 sqlite 时,`run`/`pytest` 硬失败并打印正确的 PG 串。
|
||
6. 能在 `admin/repositories/` 里写一段 PG 专有聚合 SQL(如带 `FILTER (WHERE ...)` 的聚合),`run` 下手动跑通、相应 pytest 也通过——即"双库兼容负担消失"的实证。
|
||
|
||
---
|
||
|
||
## 9. 待实现清单(供 writing-plans 拆解)
|
||
|
||
- [ ] 新增 `docker-compose.yml`
|
||
- [ ] 新增 `docker/initdb/01-create-test-db.sql`
|
||
- [ ] 新增 `scripts/ensure_pg.py`(TCP 探测 / 启 Docker Desktop 轮询 / compose up / 等 healthy / 幂等建测试库 / sqlite 守卫)
|
||
- [ ] `run.sh`、`run.bat` 接入 `ensure_pg`
|
||
- [ ] `.env.example` 的 `DATABASE_URL` 切 PG
|
||
- [ ] `tests/conftest.py` 切 `shaguabijia_test` + 调 `ensure_pg` + fixture 改 drop/create
|
||
- [ ] 跑 `pytest`,按迁移指南 §2.2 修红用例
|
||
- [ ] 文档:`postgres-migration.md` 增「本地 Docker 一键起」节;`CLAUDE.md` DB 段;`init_postgres.py` 注释
|
||
- [ ] `.gitignore` 确认 `data/` 已忽略(compose 用命名卷,不落项目目录,无需额外忽略)
|