新增 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>
14 KiB
本地开发切 Docker PostgreSQL —— 设计文档
让本地开发与测试统一跑在 Docker 化的 PostgreSQL 上,彻底退掉 SQLite。
run.bat/run.sh启动时自动检测本机 PG,没起就拉起 Docker → 起 PG 容器(镜像缺失先拉), 目的是让开发/大模型能放心用 PG 专有的高效聚合函数,不再为兼容 SQLite 而退化成"取基础数据后内存聚合"。状态:已定稿(待用户复核)。作者对话日期:2026-07-08。 关联: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.pyDATABASE_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 根目录)
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
-- 仅在 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,主流程:
- sqlite 守卫:若
DATABASE_URL以sqlite开头 → 打印"dev 已切 PG,请把 .env 的 DATABASE_URL 改成postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia"→ 非 0 退出(D4)。 - TCP 探测
host:port(stdlibsocket,超时 1s)。通 → 打印"✅ PG 已就绪"直接返回(幂等:PG 已在跑时开销≈一次握手)。 - 不通 →
docker version探 CLI;缺失 → 中文报错"请先安装 Docker Desktop:https://www.docker.com/products/docker-desktop/" → 非 0 退出。 docker info探守护进程;不通 → 按平台启动:- Windows:
start "" "%ProgramFiles%\Docker\Docker\Docker Desktop.exe"(找不到则报错让用户手动开) - macOS:
open -a Docker - Linux:不自动 sudo,打印
sudo systemctl start docker让用户执行后重试 然后轮询docker info直到就绪或超时(默认 120s,每 3s 一次,打印进度)。
- Windows:
docker compose up -d(Compose 在镜像缺失时自动拉取,首用拉 alpine ~90MB;有进度输出)。- 轮询 healthcheck(
docker inspect的 health 状态)/ TCP 直到 PG 接受连接(默认 60s 超时)。 - 幂等确保测试库存在(兼容"老 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
把
DATABASE_URL=sqlite:///./data/app.db
改为
# 本地开发/测试统一用 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 设定):
- 设
os.environ["DATABASE_URL"] = "postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia_test"(测试库,永不碰 dev 业务库)。 - 调
scripts.ensure_pg.ensure()(保证容器在 + 测试库在;PG 已在时几乎零开销)。 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 vsTIMESTAMPTZ、事务边界(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. 验收标准
- 全新机器(装了 Docker Desktop、
.env从.env.example复制)执行run.bat(或run.sh):自动拉起 Docker→起 PG 容器→建库→alembic upgrade head→uvicorn 起在 8770,无手动装 PG 步骤。 docker ps见shaguabijia-pg健康;psql/客户端能连shaguabijia与shaguabijia_test两个库。- PG 已在跑时再次
run,ensure_pg秒过(不重复拉容器)。 pytest连shaguabijia_test跑;红用例全部修绿(PG 严格性暴露的问题)。.env的DATABASE_URL改回 sqlite 时,run/pytest硬失败并打印正确的 PG 串。- 能在
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切 PGtests/conftest.py切shaguabijia_test+ 调ensure_pg+ fixture 改 drop/create- 跑
pytest,按迁移指南 §2.2 修红用例 - 文档:
postgres-migration.md增「本地 Docker 一键起」节;CLAUDE.mdDB 段;init_postgres.py注释 .gitignore确认data/已忽略(compose 用命名卷,不落项目目录,无需额外忽略)