Files
shaguabijia-app-server/docs/superpowers/specs/2026-07-08-local-dev-postgres-docker-design.md
T
guke 886e781a4f docs: 本地开发切 Docker PG 设计文档
新增 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>
2026-07-08 17:20:11 +08:00

14 KiB
Raw Blame History

本地开发切 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 专有能力(窗口函数、FILTERJSONB 操作符、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.batrun.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.pysettings.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 dataalembic 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)(供 runpython -m scripts.ensure_pg 跑)。ensure()app.core.config.settingsDATABASE_URL,解析 host/port,主流程:

  1. sqlite 守卫:若 DATABASE_URLsqlite 开头 → 打印"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

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 设定):

  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 冷启(~3060s)+ 拉镜像(数十秒数分钟,视网络)。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 psshaguabijia-pg 健康;psql/客户端能连 shaguabijiashaguabijia_test 两个库。
  3. PG 已在跑时再次 run,ensure_pg 秒过(不重复拉容器)。
  4. pytestshaguabijia_test 跑;红用例全部修绿(PG 严格性暴露的问题)。
  5. .envDATABASE_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.shrun.bat 接入 ensure_pg
  • .env.exampleDATABASE_URL 切 PG
  • tests/conftest.pyshaguabijia_test + 调 ensure_pg + fixture 改 drop/create
  • pytest,按迁移指南 §2.2 修红用例
  • 文档:postgres-migration.md 增「本地 Docker 一键起」节;CLAUDE.md DB 段;init_postgres.py 注释
  • .gitignore 确认 data/ 已忽略(compose 用命名卷,不落项目目录,无需额外忽略)