Windows cmd.exe 默认 GBK,print ✅(U+2705)抛 UnicodeEncodeError → 脚本退非0、 run.bat 误判 ensure_pg 失败。改为启动时对 stdout/stderr 设 errors=backslashreplace: 中文照常,仅不可编码字符转义。顺带订正计划里的测试计数(11)。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
27 KiB
本地开发切 Docker PostgreSQL 实现计划
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: 让 app-server 本地开发运行与 pytest 都跑在 Docker 化的 PostgreSQL 16 上,退掉 SQLite,run.bat/run.sh 启动时自动检测并拉起 PG(必要时先启 Docker Desktop、缺镜像先拉)。
Architecture: 新增 docker-compose.yml(声明 PG 服务/卷/健康检查)+ scripts/ensure_pg.py(跨平台引导:探测→启 Docker→compose up→等就绪→幂等建测试库),由 run.sh/run.bat/tests/conftest.py 三处共用。.env.example 默认切 PG。app/db/session.py 与 alembic/env.py 已天然支持 PG,无需改。
Tech Stack: Docker Compose、postgres:16-alpine、Python 3.10+ 标准库(socket/subprocess/urllib.parse)、psycopg3(已装)、SQLAlchemy 2.0 + Alembic、pytest。
工作目录: 本计划在 worktree .worktrees/local-dev-postgres-docker(分支 chore/local-dev-postgres-docker,基于 main)内执行。下面所有路径相对该 worktree 根(= 仓库根)。
设计依据: docs/superpowers/specs/2026-07-08-local-dev-postgres-docker-design.md
连接参数(全程固定值):
- 镜像
postgres:16-alpine,容器名shaguabijia-pg,宿主端口5432 - 用户
shaguabijia_app,dev 密码shaguabijia_dev_pw(本地非机密) - 业务库
shaguabijia,测试库shaguabijia_test,命名卷pgdata - dev URL:
postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia - test URL:
...@localhost:5432/shaguabijia_test
前置: 执行机已安装 Docker Desktop。
Task 1: Docker Compose + 测试库 initdb 脚本
Files:
-
Create:
docker-compose.yml -
Create:
docker/initdb/01-create-test-db.sql -
Step 1: 写
docker-compose.yml
# 本地开发/测试用 PostgreSQL。生产用原生 PG(scripts/init_postgres.py),不使用本文件。
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:
- Step 2: 写
docker/initdb/01-create-test-db.sql
-- 仅在 pgdata 卷首次初始化时执行一次(以 shaguabijia_app 连 shaguabijia 库运行)。
-- 幂等兜底见 scripts/ensure_pg.py 的 _ensure_test_db()。
CREATE DATABASE shaguabijia_test OWNER shaguabijia_app;
- Step 3: 起容器验证
Run: docker compose up -d
Expected: 拉取 postgres:16-alpine(首次)后 Container shaguabijia-pg Started。
- Step 4: 验证两个库都在 + 健康
Run: docker compose exec -T postgres psql -U shaguabijia_app -d shaguabijia -tAc "SELECT datname FROM pg_database WHERE datname IN ('shaguabijia','shaguabijia_test') ORDER BY 1"
Expected 输出:
shaguabijia
shaguabijia_test
- Step 5: 验证
.worktrees/与data/忽略不受影响、compose 无落盘到项目目录
Run: git status --short
Expected: 只列出本任务新增的 docker-compose.yml、docker/initdb/01-create-test-db.sql(数据在命名卷 pgdata,不在项目目录;.worktrees/ 已忽略)。
- Step 6: Commit
git add docker-compose.yml docker/initdb/01-create-test-db.sql
git commit -m "feat(dev): docker-compose 起本地 PostgreSQL(含测试库 initdb)"
Task 2: scripts/ensure_pg.py 引导脚本(TDD)
Files:
- Create:
scripts/ensure_pg.py - Test:
tests/test_ensure_pg.py
说明:纯函数(URL 解析 / sqlite 判定 / 端口探测 / 平台命令映射 / sqlite 守卫 / 端口通时短路)走 TDD 单测;真正拉 Docker 的编排
ensure()全链路靠 Task 3/5 的运行来验证(需真 Docker,不做单测)。此时conftest.py仍是 SQLite,不依赖 PG,单测可独立跑。
- Step 1: 写失败测试
tests/test_ensure_pg.py
"""scripts/ensure_pg.py 纯函数单测(不需要 Docker/PG)。"""
from __future__ import annotations
import socket
from scripts.ensure_pg import (
_docker_desktop_cmd,
_is_sqlite,
_parse_host_port,
_port_open,
ensure,
)
def test_is_sqlite():
assert _is_sqlite("sqlite:///./data/app.db")
assert _is_sqlite(" SQLite:///x ")
assert not _is_sqlite("postgresql+psycopg://u:p@localhost:5432/db")
def test_parse_host_port_full():
assert _parse_host_port(
"postgresql+psycopg://u:p@localhost:5432/shaguabijia"
) == ("localhost", 5432)
def test_parse_host_port_defaults():
# 缺端口 → 5432
assert _parse_host_port("postgresql+psycopg://u:p@db.example/x")[1] == 5432
# 缺 host → localhost
assert _parse_host_port("postgresql+psycopg:///x") == ("localhost", 5432)
def test_parse_host_port_testdb():
assert _parse_host_port(
"postgresql+psycopg://u:p@localhost:5432/shaguabijia_test"
) == ("localhost", 5432)
def test_port_open_true():
srv = socket.socket()
srv.bind(("127.0.0.1", 0))
srv.listen(1)
port = srv.getsockname()[1]
try:
assert _port_open("127.0.0.1", port, timeout=1.0)
finally:
srv.close()
def test_port_open_false():
s = socket.socket()
s.bind(("127.0.0.1", 0))
port = s.getsockname()[1]
s.close() # 释放端口,无人监听 → 连接应失败
assert not _port_open("127.0.0.1", port, timeout=0.3)
def test_docker_desktop_cmd_windows():
cmd = _docker_desktop_cmd("win32", r"C:\Program Files")
assert cmd is not None
assert cmd[0].endswith("Docker Desktop.exe")
assert "Docker" in cmd[0]
def test_docker_desktop_cmd_darwin():
assert _docker_desktop_cmd("darwin", "") == ["open", "-a", "Docker"]
def test_docker_desktop_cmd_linux():
assert _docker_desktop_cmd("linux", "") is None
def test_ensure_rejects_sqlite():
# dev 守卫:sqlite 直接 False(不碰 Docker)
assert ensure("sqlite:///./data/app.db") is False
def test_ensure_shortcircuits_when_pg_up(monkeypatch):
# 端口通 → 直接 True,绝不触碰 docker
monkeypatch.setattr("scripts.ensure_pg._port_open", lambda *a, **k: True)
def _boom():
raise AssertionError("端口通时不应调用 docker")
monkeypatch.setattr("scripts.ensure_pg._docker_cli_ok", _boom)
assert ensure("postgresql+psycopg://u:p@localhost:5432/shaguabijia") is True
- Step 2: 跑测试确认失败
Run: pytest tests/test_ensure_pg.py -v
Expected: FAIL —— ModuleNotFoundError: No module named 'scripts.ensure_pg'(还没建)。
- Step 3: 写实现
scripts/ensure_pg.py
"""确保本地 PostgreSQL 就绪(开发/测试统一用 Docker PG)。
被三处复用:
- run.sh / run.bat:`python -m scripts.ensure_pg`(CLI,失败退非 0)
- tests/conftest.py:`from scripts.ensure_pg import ensure; ensure(test_url)`
流程:读 DATABASE_URL → TCP 探测 → 没起就(必要时启 Docker Desktop)→
`docker compose up -d` → 等 PG ready → 幂等确保测试库存在。全程无 SQLite 兜底。
生产用原生 PG(scripts/init_postgres.py),不走本模块。
"""
from __future__ import annotations
import os
import socket
import subprocess
import sys
import time
from pathlib import Path
from urllib.parse import urlsplit
ROOT = Path(__file__).resolve().parent.parent
# 日志里可能含 emoji(如 ✅);Windows GBK 控制台(cmd.exe)无法编码会抛 UnicodeEncodeError → 脚本崩、
# run.bat 误判 ensure_pg 失败。用 backslashreplace 保底:中文仍正常,仅不可编码字符被转义,不崩。
for _stream in (sys.stdout, sys.stderr):
try:
_stream.reconfigure(errors="backslashreplace")
except (AttributeError, ValueError):
pass
APP_DB = "shaguabijia"
TEST_DB = "shaguabijia_test"
DB_USER = "shaguabijia_app"
COMPOSE_SERVICE = "postgres"
DOCKER_START_TIMEOUT = int(os.environ.get("ENSURE_PG_DOCKER_TIMEOUT", "120"))
PG_READY_TIMEOUT = int(os.environ.get("ENSURE_PG_READY_TIMEOUT", "60"))
POLL_INTERVAL = 3.0
SQLITE_FIX_HINT = (
"postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia"
)
def _log(msg: str) -> None:
print(f"[ensure_pg] {msg}", flush=True)
def _is_sqlite(url: str) -> bool:
return url.strip().lower().startswith("sqlite")
def _parse_host_port(url: str) -> tuple[str, int]:
"""从 SQLAlchemy URL 取 host/port,缺省 localhost:5432。"""
parts = urlsplit(url)
return (parts.hostname or "localhost"), (parts.port or 5432)
def _port_open(host: str, port: int, timeout: float = 1.0) -> bool:
try:
with socket.create_connection((host, port), timeout=timeout):
return True
except OSError:
return False
def _docker_desktop_cmd(platform: str, program_files: str) -> list[str] | None:
"""按平台给出启动 Docker Desktop 的命令;Linux 返回 None(daemon 需 sudo,让用户手动)。"""
if platform.startswith("win"):
return [str(Path(program_files) / "Docker" / "Docker" / "Docker Desktop.exe")]
if platform == "darwin":
return ["open", "-a", "Docker"]
return None
def _docker_ok(subcmd: str) -> bool:
"""`docker version`(CLI 在不在)/`docker info`(daemon 起没起)成功与否。"""
try:
subprocess.run(
["docker", subcmd],
cwd=ROOT,
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
check=True,
)
return True
except (OSError, subprocess.CalledProcessError):
return False
def _docker_cli_ok() -> bool:
return _docker_ok("version")
def _docker_daemon_ok() -> bool:
return _docker_ok("info")
def _start_docker_daemon() -> bool:
"""守护进程没起时按平台拉起,轮询到就绪。返回是否成功。"""
if _docker_daemon_ok():
return True
cmd = _docker_desktop_cmd(
sys.platform, os.environ.get("ProgramFiles", r"C:\Program Files")
)
if cmd is None:
_log("Docker 守护进程未运行。Linux 请手动:sudo systemctl start docker,然后重试。")
return False
if sys.platform.startswith("win") and not Path(cmd[0]).exists():
_log(f"找不到 Docker Desktop:{cmd[0]}。请手动启动 Docker Desktop 后重试。")
return False
_log(f"启动 Docker Desktop(首次冷启可能 30-60s)…")
try:
subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
except OSError as e:
_log(f"启动 Docker Desktop 失败:{e}")
return False
deadline = time.monotonic() + DOCKER_START_TIMEOUT
while time.monotonic() < deadline:
if _docker_daemon_ok():
_log("Docker 守护进程已就绪。")
return True
_log("等待 Docker 守护进程…")
time.sleep(POLL_INTERVAL)
_log(f"等待 Docker 守护进程超时({DOCKER_START_TIMEOUT}s)。")
return False
def _compose_up() -> bool:
_log("docker compose up -d(镜像缺失会自动拉取,首用约几十秒)…")
try:
subprocess.run(["docker", "compose", "up", "-d"], cwd=ROOT, check=True)
return True
except (OSError, subprocess.CalledProcessError) as e:
_log(f"docker compose up 失败:{e}")
return False
def _pg_isready() -> bool:
r = subprocess.run(
["docker", "compose", "exec", "-T", COMPOSE_SERVICE,
"pg_isready", "-U", DB_USER, "-d", APP_DB],
cwd=ROOT, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL,
)
return r.returncode == 0
def _wait_pg_ready(host: str, port: int) -> bool:
deadline = time.monotonic() + PG_READY_TIMEOUT
while time.monotonic() < deadline:
if _port_open(host, port) and _pg_isready():
_log("PostgreSQL 已就绪。")
return True
_log("等待 PostgreSQL 就绪…")
time.sleep(POLL_INTERVAL)
_log(f"等待 PostgreSQL 就绪超时({PG_READY_TIMEOUT}s)。")
return False
def _ensure_test_db() -> None:
"""幂等建测试库(兼容老 pgdata 卷首启没跑 initdb 的情况)。"""
check = subprocess.run(
["docker", "compose", "exec", "-T", COMPOSE_SERVICE,
"psql", "-U", DB_USER, "-d", APP_DB, "-tAc",
f"SELECT 1 FROM pg_database WHERE datname='{TEST_DB}'"],
cwd=ROOT, capture_output=True, text=True,
)
if check.returncode == 0 and check.stdout.strip() == "1":
return
_log(f"建测试库 {TEST_DB}…")
subprocess.run(
["docker", "compose", "exec", "-T", COMPOSE_SERVICE,
"psql", "-U", DB_USER, "-d", APP_DB, "-c",
f"CREATE DATABASE {TEST_DB} OWNER {DB_USER}"],
cwd=ROOT, check=False,
)
def ensure(database_url: str | None = None) -> bool:
"""确保 PG 就绪,返回 True/False。database_url 缺省从 settings 读(尊重 .env)。"""
if database_url is None:
from app.core.config import settings # 延迟导入,避免过早固化 settings
database_url = settings.DATABASE_URL
if _is_sqlite(database_url):
_log("检测到 DATABASE_URL 仍是 SQLite。本地开发/测试已切 PostgreSQL,请改成:")
_log(f" DATABASE_URL={SQLITE_FIX_HINT}")
return False
host, port = _parse_host_port(database_url)
if _port_open(host, port):
_log(f"✅ PostgreSQL 已在 {host}:{port} 运行,跳过 Docker。")
return True
_log(f"{host}:{port} 无 PostgreSQL,准备用 Docker 拉起…")
if not _docker_cli_ok():
_log("未检测到 docker 命令。请先安装 Docker Desktop:")
_log(" https://www.docker.com/products/docker-desktop/")
return False
if not _start_docker_daemon():
return False
if not _compose_up():
return False
if not _wait_pg_ready(host, port):
return False
_ensure_test_db()
return True
if __name__ == "__main__":
sys.exit(0 if ensure() else 1)
- Step 4: 跑测试确认通过
Run: pytest tests/test_ensure_pg.py -v
Expected: 11 passed。
- Step 5: 手动冒烟(PG 已在跑时应秒过短路)
Run: python -m scripts.ensure_pg
Expected: 打印 [ensure_pg] ✅ PostgreSQL 已在 localhost:5432 运行,跳过 Docker。,退出码 0。
- Step 6: Commit
git add scripts/ensure_pg.py tests/test_ensure_pg.py
git commit -m "feat(dev): scripts/ensure_pg.py 探测/拉起本地 Docker PostgreSQL"
Task 3: run.sh / run.bat 接入 ensure_pg
Files:
-
Modify:
run.sh(在alembic upgrade head前插一步) -
Modify:
run.bat(同上) -
Step 1: 改
run.sh
在 mkdir -p data 之后、"$PY" -m alembic upgrade head 之前插入:
"$PY" -m scripts.ensure_pg # 确保本地 Docker PostgreSQL 就绪(没起会自动拉起;失败即退出)
(set -e 已在文件顶部,ensure_pg 失败会自动终止脚本。)
- Step 2: 改
run.bat
在 if not exist data mkdir data 之后、call "%PY%" -m alembic upgrade head 之前插入:
REM 确保本地 Docker PostgreSQL 就绪(没起会自动拉起 Docker + PG 容器)
call "%PY%" -m scripts.ensure_pg
if errorlevel 1 (
echo [X] ensure_pg failed ^(PostgreSQL 未就绪^)
exit /b %errorlevel%
)
- Step 3: 验证
run.sh(PG 已在跑,应短路后继续 alembic + uvicorn)
Run(Git Bash):bash run.sh 8770
Expected: 依次出现 [ensure_pg] ✅ PostgreSQL 已在 localhost:5432 运行 → alembic 无报错 → uvicorn Application startup complete。Ctrl-C 停。
- Step 4: 验证
run.bat(同上,Windows 原生)
Run(cmd/PowerShell):.\run.bat 8770
Expected: 同 Step 3。Ctrl-C 停。
- Step 5: Commit
git add run.sh run.bat
git commit -m "feat(dev): run.sh/run.bat 启动前确保 Docker PostgreSQL 就绪"
Task 4: .env.example 默认切 PostgreSQL
Files:
-
Modify:
.env.example(第 8-10 行「数据库」段) -
Step 1: 改
.env.example的 DATABASE_URL
把:
# ===== 数据库 =====
# SQLite 本地文件路径。生产环境用 /opt/shaguabijia-app-server/data.db
DATABASE_URL=sqlite:///./data/app.db
改成:
# ===== 数据库 =====
# 本地开发/测试统一用 Docker PostgreSQL:run.bat/run.sh 会自动拉起容器
# (docker-compose.yml + scripts/ensure_pg.py)。详见 docs/database/postgres-migration.md。
# 生产用原生 PG,由 scripts/init_postgres.py 写入强随机密码的连接串。
# ⚠️ scheme 必须是 postgresql+psycopg://(psycopg3);不要写成 postgresql://(会去找未装的 psycopg2)。
DATABASE_URL=postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia
- Step 2: 验证(新 .env 从模板复制后能起服务)
Run: cp .env.example /tmp/env.check && grep '^DATABASE_URL=' /tmp/env.check
Expected: DATABASE_URL=postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia
- Step 3: Commit
git add .env.example
git commit -m "feat(dev): .env.example 默认 DATABASE_URL 切 Docker PostgreSQL"
Task 5: tests/conftest.py 切 PostgreSQL 测试库
Files:
-
Modify:
tests/conftest.py(整体替换:去掉临时 SQLite,改指shaguabijia_test+ 调 ensure_pg + fixture 改 drop/create) -
Step 1: 整体替换
tests/conftest.py
"""测试用 fixtures。
测试库用 Docker PG 的 shaguabijia_test(与 dev 业务库 shaguabijia 隔离)。
顺序(必须):设 test DATABASE_URL(在 import app.* 之前)→ ensure PG 就绪 →
import app → 建表。持久卷可能残留上次的表 → session 开头先 drop 再 create。
"""
from __future__ import annotations
import os
from collections.abc import Iterator
# 1) 测试库连接串——必须在 import app.* 之前设好(app.db.session 在 import 期建 engine)
_TEST_DB_URL = (
"postgresql+psycopg://shaguabijia_app:shaguabijia_dev_pw@localhost:5432/shaguabijia_test"
)
os.environ["DATABASE_URL"] = _TEST_DB_URL
os.environ.setdefault("JWT_SECRET_KEY", "test-secret-please-ignore-this-is-only-for-pytest-not-real")
os.environ.setdefault("ADMIN_JWT_SECRET", "test-admin-secret-please-ignore-only-for-pytest-not-real")
os.environ.setdefault("JG_APP_KEY", "test-key")
os.environ.setdefault("JG_MASTER_SECRET", "test-secret")
os.environ.setdefault("SMS_MOCK", "true")
os.environ.setdefault("APP_ENV", "dev")
os.environ.setdefault("APP_DEBUG", "false") # 测试不打 SQL 日志
os.environ.setdefault("WECHAT_APP_ID", "wxtest0000000000")
os.environ.setdefault("WECHAT_APP_SECRET", "test-secret")
os.environ.setdefault("WXPAY_MCH_ID", "test-mch")
os.environ.setdefault("WXPAY_MCH_SERIAL_NO", "test-serial")
os.environ.setdefault("WXPAY_PUBLIC_KEY_ID", "test-pubkey-id")
os.environ.setdefault("RATE_LIMIT_ENABLED", "false")
os.environ.setdefault("PANGLE_CALLBACK_ENABLED", "true")
os.environ.setdefault("PANGLE_REWARD_SECRET", "test-pangle-secret-only-for-pytest")
# 2) 保证 Docker PG 就绪 + 测试库存在(必须在 import app.db.session 建 engine 之前)
from scripts.ensure_pg import ensure
if not ensure(_TEST_DB_URL):
raise RuntimeError(
"测试需要 Docker PostgreSQL 就绪。请确认已装 Docker Desktop;"
"或先跑一次 run.bat/run.sh 把 PG 拉起,再重试 pytest。"
)
import pytest
from fastapi.testclient import TestClient
from app.db.base import Base
from app.db.session import engine
from app.main import app
@pytest.fixture(scope="session", autouse=True)
def _setup_db() -> Iterator[None]:
# 持久卷可能残留上次跑崩后的表/数据 → 先 drop 再 create,保证干净起点
Base.metadata.drop_all(engine)
Base.metadata.create_all(engine)
yield
Base.metadata.drop_all(engine)
@pytest.fixture()
def client() -> TestClient:
return TestClient(app)
- Step 2: 验证 conftest 能引导 PG 并收集用例(选一个不涉 DB 的测试文件)
Run: pytest tests/test_ensure_pg.py -v
Expected: conftest 先打印 [ensure_pg] ✅ PostgreSQL 已在 localhost:5432 运行(或拉起过程),随后 12 passed。说明「测试走 PG 引导」链路通、且纯函数测试不受影响。
- Step 3: 验证建表落到 PG 测试库(跑一个 DB 相关用例)
Run: pytest tests/test_invite.py -v
Expected: 用例在 shaguabijia_test 上建表并执行(可能有个别红,留待 Task 6);关键是不再出现 SQLite 临时文件、engine 连的是 PG。
- Step 4: Commit
git add tests/conftest.py
git commit -m "test(dev): conftest 切 shaguabijia_test(Docker PG),引导+drop/create"
Task 6: 全量跑 pytest on PG,逐个修红用例
SQLite 宽松、PG 严格,切库会暴露一批真 bug(迁移指南 §2.2 已列)。本任务是发现驱动:先跑全量、按类别归因、按下述配方修,直到全绿。修改范围限被测业务/模型代码,不改测试来掩盖真 bug(除非测试本身依赖 SQLite 特性,如秒级时间精度)。
Files:
-
Modify: 视失败而定(常见:
app/models/*.py、app/**/repositories/*.py、少量tests/*.py) -
Step 1: 全量跑,拿到失败清单
Run: pytest -q
Expected: 大部分通过;记录所有 FAIL 的用例名与报错文本,按下面类别归因。
- Step 2: 修「naive datetime / 时区」类
定位:git grep -n "utcnow()" app/。把 datetime.utcnow() 改成 datetime.now(timezone.utc)(并 from datetime import timezone)。
症状:PG TIMESTAMPTZ 与 naive datetime 比较/写入报错或结果错位;tests/test_cps_admin.py 已注释过 SQLite 忽略 tzinfo 的行为。
例:
# 改前
from datetime import datetime
ts = datetime.utcnow()
# 改后
from datetime import datetime, timezone
ts = datetime.now(timezone.utc)
- Step 3: 修「字符串/整数隐式比较」类
症状:SQLite 允许 WHERE phone = 13800138000(自动转型),PG 直接报类型错。定位报错用例引用的查询,确保比较两侧类型一致(手机号等一律按字符串传参 :phone,不要传裸 int)。
- Step 3.5: 修「事务已中止」类
症状:某用例后续报 current transaction is aborted, commands ignored until end of transaction block,根因是前一句 SQL 出错后业务代码缺 db.rollback()/db.commit() 边界。补上正确的 commit/rollback。
- Step 4: 修「测试依赖 SQLite 特性」类(仅此类可改测试)
症状:测试断言依赖 SQLite 秒级时间精度或 FK 不强制(见 test_invite.py:328、test_compare_harvest.py:153 的注释)。PG 下时间精度更高/FK 更严——调整测试数据(如手动拉开时间间隔、用合法 FK)使断言在 PG 下成立,不改业务逻辑。
- Step 5: 反复跑到全绿
Run: pytest -q
Expected: N passed(0 failed)。若仍有红,回到 Step 2-4 继续归因。
- Step 6: Commit
git add -A
git commit -m "fix(db): 测试套件切 PostgreSQL 后修复严格性暴露的用例"
Task 7: 文档更新
Files:
-
Modify:
docs/database/postgres-migration.md(§1 增「本地 Docker 一键起」小节) -
Modify:
CLAUDE.md(DB 段注明 dev/test = Docker PG) -
Modify:
scripts/init_postgres.py(顶部注释区分生产/本地) -
Step 1:
postgres-migration.md在「## 1. 本地起 PG」开头插入推荐做法
在 ## 1. 本地起 PG + 跑通空库(半天) 标题下、### 1.1 装 PG 之前插入:
### 1.0 推荐:Docker 一键起(本地开发/测试)
本地开发不必手动装 PG。已提供 `docker-compose.yml` + `scripts/ensure_pg.py`:
```bash
cp .env.example .env # DATABASE_URL 默认已是 Docker PG 连接串
./run.sh # 或 run.bat;会自动:探测 PG → 没起则启 Docker → 起 PG 容器 → 建库 → alembic → uvicorn
pytest # conftest 自动引导同一容器的 shaguabijia_test 库
容器:postgres:16-alpine(名 shaguabijia-pg,端口 5432,命名卷 pgdata 持久化),
首启即建业务库 shaguabijia 与测试库 shaguabijia_test。下面 1.1-1.5 的手动装 PG 步骤仅在
不用 Docker 时才需要;生产仍走 §4 的原生 PG。
- [ ] **Step 2: `CLAUDE.md` DB 段补充**
找到 DB 相关行(`**Prod**: PostgreSQL — just change DATABASE_URL...`)所在段,在其上方加一行:
```markdown
- **Dev/Test**: Docker PostgreSQL 16 — `run.sh`/`run.bat` 经 `scripts/ensure_pg.py` + `docker-compose.yml` 自动拉起;`.env.example` 默认即 PG 连接串;pytest 用同容器的 `shaguabijia_test` 库。**本地不再用 SQLite**。
- Step 3:
scripts/init_postgres.py顶部注释区分场景
把模块 docstring 第一行下方(新机器初始化用。前置:... 那行)改为:
新机器初始化用(面向【生产原生 PG】:apt/systemd 装好的 PostgreSQL)。
本地开发/测试请改用 docker-compose.yml + scripts/ensure_pg.py(run.sh/run.bat 自动拉起),不必跑本脚本。
前置:已装 PostgreSQL 16 + 知道 postgres 超级用户密码。
- Step 4: 验证无坏链接/格式
Run: git diff --stat
Expected: 三个文档文件有改动,无其他文件被误改。
- Step 5: Commit
git add docs/database/postgres-migration.md CLAUDE.md scripts/init_postgres.py
git commit -m "docs(dev): 记录本地 Docker PostgreSQL 用法,区分生产原生 PG 路径"
完成标准(对齐 spec §8 验收)
- 全新机器(装了 Docker Desktop、
.env从.env.example复制)跑run.bat/run.sh全自动拉起 PG 并起服务,无手动装 PG。 docker ps见shaguabijia-pghealthy;shaguabijia与shaguabijia_test两库都在。- PG 已在跑时再跑
run,ensure_pg 秒过短路。 pytest -q全绿(连shaguabijia_test)。.env改回 sqlite 时,python -m scripts.ensure_pg硬失败并打印正确 PG 串。- 能在
admin/repositories写一段 PG 专有聚合(如count(*) FILTER (WHERE ...)),run手动跑通且相应 pytest 通过。
Self-Review 记录(计划作者已核)
- Spec 覆盖: spec §9 待实现清单 8 项 → Task 1(compose+initdb)、Task 2(ensure_pg)、Task 3(run 接线)、Task 4(.env.example)、Task 5(conftest)、Task 6(修红用例)、Task 7(文档);「session.py 无需改」在 Header 与 spec §4.7 说明;「data/ 忽略」在 Task 1 Step 5 验证。无遗漏。
- 占位符: 全部步骤含真实代码/命令/期望输出。Task 6 是发现驱动,已用「类别+具体转换配方+定位命令」代替不可预知的逐条 diff——非占位。
- 类型/命名一致:
ensure(database_url=None)签名在 Task 2 定义,Task 5 以ensure(_TEST_DB_URL)调用一致;库名/用户/密码/端口全程为 Header 固定值;compose 服务名postgres与 ensure_pgCOMPOSE_SERVICE一致;shaguabijia_test在 initdb SQL、_ensure_test_db()、conftest 三处一致。