Files
shaguabijia-app-server/docs/postgres-migration.md
T
marco 6c7eaa9734 docs: 跟齐 5 月 PR (#4 user/feedback / #5 ad-cooldown / #6 PG / abd8eab meituan 降级)
整轮 doc 跟齐 2026-05 落地但 doc 没补全/没更新的内容. 按 P0~P3 优先级处理.

P0 - 新 API doc + index 更新:
* docs/api/user-profile.md  (新) PATCH /api/v1/user/profile 改昵称
* docs/api/user-avatar.md   (新) POST /api/v1/user/avatar 上传头像(魔数嗅探/旧文件清理)
* docs/api/user-delete.md   (新) DELETE /api/v1/user 注销账号(软删+匿名化)
* docs/api/feedback.md      (新) POST /api/v1/feedback 提交反馈(含截图)
* docs/api/README.md           加 User/Feedback 组 + /media 静态服务段 +
                                修 UserOut "无接口可改恒为 null" 过时描述

P0 - 美团降级 / 现有 doc 校准:
* docs/api/meituan-coupons.md      加 "未配 MT_CPS 凭证降级返空" 说明
* docs/api/meituan-referral-link.md 同上 + 客户端兜底建议
* docs/api/meituan-feed.md         区分两种空结果路径(未配 vs 调用失败) +
                                    /health.mt_cps_configured 排障入口
* docs/integrations/meituan.md     "未配凭证降级" + "feed 静默吞错" 写进集成层
* ad-reward-status.md              PR #5 commit 已自带 doc 更新, 复核无需再改

P1 - 主文档大改:
* docs/后端技术实现.md
  - §1 能力表加 用户资料/外卖比价/钱包/福利/广告/反馈 (从 3 条扩到 9 条)
  - §1 数据模型纠正 "仅 1 张 user 表" -> 现有 10 张 + alembic_version
  - §2 DB 行 PG 主 / SQLite 兜底 + 加 psycopg3 行
  - §3 目录树补 user.py / feedback.py / media.py / repositories/feedback.py /
        scripts 子项 / docs/postgres-migration.md
  - §7 数据模型重写: 10 张业务表速查表 + alembic 合并迁移 f01db5d77dac 始末
  - §8 部署: 加 init_postgres.py 一句话流程 + checklist 加 "DATABASE_URL 切 PG"
        + "psycopg3 已装" + "MEDIA_ROOT 可写"
  - §10 已知问题: 加 美团降级 / agent 不鉴权 / MEDIA_ROOT serve / init_postgres bug /
        alembic 多 head 风险; 删过时 SQLite 行
  - 接口计数 11 -> 38

* docs/待办与技术债.md
  - 已解决追加: PG 切换 / 外卖比价透传 / 用户资料反馈接口 / 美团降级 /
                看广告 4 态冷却
  - 新增 "P2 DB 与 Alembic 治理" 段: alembic 多 head 团队规约 (含本次撞坑实录) +
    init_postgres.py 已知 5 处小 bug + MEDIA_ROOT serve

P2 - PG 迁移文档互指 + 迁移链补完:
* docs/数据库迁移.md
  - 顶部加 postgres-migration.md / 后端技术实现.md 互指
  - §6 迁移链 9 条 -> 12 条 (含 feedback_table / convert_dishes_jsonb /
        merge_pg_jsonb_and_feedback_heads)
  - §7 踩坑里 "多 head" 一行扩成完整故事 + 修复命令 + 团队预防规约
* docs/postgres-migration.md
  - 顶部加 数据库迁移.md / 后端技术实现.md §10 互指

P3 - 上线清单:
* docs/看广告赚金币上线清单.md
  - 设计要点加 PR #5 4 态 CTA + 后端权威派生说明 +
    VIDEO_ROUND_REQUIRED_COUNT/VIDEO_ROUND_COOLDOWN_SECONDS 常量
  - §E 端到端验收加 本轮冷却 / 弹窗 limit note / 跨设备一致 三项

所有最后更新日期同步到 2026-05-31.
2026-05-31 11:05:10 +08:00

11 KiB

SQLite → PostgreSQL 迁移指南

上线前把数据库从 SQLite 切到 PostgreSQL。本服务自始就为这一步预留了路径(db/session.py 的 dialect 特判 / alembic/env.py 的 batch mode 开关 / SQLAlchemy 2.0 + Alembic),代码改动很小。

相关文档:

  • 这份文档讲 切引擎的完整步骤(本机装 PG / 建库 / scripts/init_postgres.py 用法 / 生产切换)
  • 数据库迁移.mdalembic 操作(如何建表/升级/新增迁移/多 head 排查/迁移链 12 条)
  • 后端技术实现.md §10 列待办与已知问题(含 init_postgres.py 已知小 bug)

背景假设:当前生产无真实用户数据(MVP 阶段),所以整个迁移本质是"切引擎",不涉及数据搬迁。如果将来已有真实数据,本文档不适用,需补充 pgloader 演练 + 钱表金额逐行核对 + 停服窗口 + 回滚预案。

最后更新:2026-05-31


0. 为什么切

SQLite 三个硬伤决定它不能上线:

  • 并发写锁库:整个文件一把锁,多请求并发写会排队,极易超时。当前 uvicorn --workers 1 勉强能跑,扩 worker 立刻撞墙。
  • 类型宽松:整数列能塞字符串、CHECK 约束当装饰品。钱表(wallet / withdraw_order / cash_transaction)用 SQLite 等于裸奔。
  • 无 JSONB / 无并发索引 / 无分区:PG 的核心能力一个都享受不到。savings.dishes 现在用 SQLite 的 JSON(实际存 TEXT),无索引、无操作符,纯展示用。

PG 默认上 16 版(工具链最齐),驱动用 psycopg3(SQLAlchemy 2.0 时代默认,不要再装 psycopg2)。


1. 本地起 PG + 跑通空库(半天)

1.1 装 PG

macOS:

brew install postgresql@16
brew services start postgresql@16

Linux(Ubuntu/Debian):

sudo apt install postgresql-16
sudo systemctl enable --now postgresql

1.2 建库 + 用户 + 授权

psql postgres <<'EOF'
CREATE USER shaguabijia_app WITH PASSWORD 'change-me-strong-random';
CREATE DATABASE shaguabijia OWNER shaguabijia_app ENCODING 'UTF8';
GRANT ALL PRIVILEGES ON DATABASE shaguabijia TO shaguabijia_app;
EOF

密码用 python -c "import secrets; print(secrets.token_urlsafe(32))" 生成,不要复用 JWT_SECRET_KEY

1.3 装驱动

pip install "psycopg[binary]>=3.1"

psycopg[binary] 是预编译版,免装 libpq 头文件。生产同款,无需源码编译版。

1.4 改 .env

DATABASE_URL=postgresql+psycopg://shaguabijia_app:<password>@localhost:5432/shaguabijia

⚠️ URL scheme 必须是 postgresql+psycopg://(显式声明 psycopg3),不能写成 postgresql://——SQLAlchemy 默认会去找 psycopg2,装的不一致就报 ModuleNotFoundError。

1.5 建表

alembic upgrade head

1.6 验证

psql -U shaguabijia_app -d shaguabijia -c "\dt"

应该列出全部 9 张表 + alembic_version:

user, coin_account, coin_transaction, cash_transaction,
withdraw_order, signin_record, task_claim_record,
savings_record, ad_reward_record, alembic_version

再手动 INSERT 一行 user,确认 created_at 默认值正常落:

INSERT INTO "user" (phone, register_channel, status) VALUES ('13800138000', 'sms', 'active');
SELECT id, phone, created_at, last_login_at FROM "user";

created_at 应该是当前时间戳带时区,不是 NULL。user 表名是 PG 关键字,必须用双引号包裹。


2. 测试套件全绿(1 小时)

2.1 测试连 PG

测试默认连 SQLite。临时让 tests/conftest.pyDATABASE_URL 也指到本地 PG(可以建个 shaguabijia_test 库专门跑测试,免得污染 dev 库)。

createdb -O shaguabijia_app shaguabijia_test
DATABASE_URL=postgresql+psycopg://shaguabijia_app:xxx@localhost:5432/shaguabijia_test pytest

2.2 处理失败用例

所有红色测试都不能放过——SQLite 类型/约束都很宽松,PG 严格起来暴露的就是真 bug。常见问题:

  • 字符串和整数比较:SQLite 允许 WHERE phone = 13800138000(自动转字符串),PG 直接报错。改成 :phone 参数 + 类型严格
  • timezone 处理:SQLite 存 naive datetime,PG 的 TIMESTAMPTZ 必须带时区。检查所有 datetime.utcnow(),改成 datetime.now(timezone.utc)
  • 隐式 commit:SQLite 某些情况下隐式 commit,PG 严格事务边界。如果某用例报 current transaction is aborted,说明业务代码缺 commit/rollback

2.3 验证

pytest 全绿,且本地起 ./run.sh 后用 Apifox 调通登录 / 美团 feed / 领券透传三个真接口


3. 代码扫雷(1 天)

3.1 必改清单

文件 / 位置 改动 原因
app/models/savings.py from sqlalchemy import JSONfrom sqlalchemy.dialects.postgresql import JSONB;mapped_column(JSON, ...)mapped_column(JSONB, ...) JSONB 在 PG 上有 GIN 索引和操作符支持,是 PG 核心优势之一。SQLite 上 JSON 实际是 TEXT,切到 PG 后用 JSON 类型存的是 json 不是 jsonb,查询/索引能力差一大截
app/db/session.pycreate_engine 调用 增加连接池参数:pool_size=10, max_overflow=20, pool_recycle=3600 SQLite 单文件不需要池,PG 必须显式池化。pool_recycle=3600 防 PG 主动断开的 idle 连接
alembic/versions/ 下旧迁移里的 sa.text('(CURRENT_TIMESTAMP)') 不要改 PG 能接受这个语法。改了反而搞乱迁移历史 + 让 alembic 觉得 schema drift。新写的迁移统一用 sa.func.now()

3.2 保留的 dialect 特判

下面这些保留原样,它们的 SQLite 分支在 PG 下自动绕过、不影响新部署,但本地开发想用 SQLite 临时跑还能用:

  • app/db/session.py 里的 _ensure_sqlite_dircheck_same_thread 特判
  • alembic/env.py 里的 render_as_batch 特判

3.3 顺便清掉

app/main.py 启动日志里如果打 DATABASE_URL,确认密码不会进日志(只打 scheme + host,不打完整 URL)。

3.4 验证

本地 PG 起 ./run.sh,跑一遍下面的烟测脚本:

# 登录
curl -X POST http://localhost:8770/api/v1/auth/sms/send  -d '{"phone":"13800138000"}'
curl -X POST http://localhost:8770/api/v1/auth/sms/login -d '{"phone":"13800138000","code":"123456"}'
# 拿到 access_token 后调 me
curl -H "Authorization: Bearer <token>" http://localhost:8770/api/v1/auth/me
# 美团 feed
curl -X POST http://localhost:8770/api/v1/meituan/feed -d '{"longitude":116.4,"latitude":39.9,"page":0}'

三条都 200 即通过。psqluser 表多了一条 13800138000


4. 生产切换(半小时,无需停服窗口)

前提:生产无真实数据。如已有数据,本节不适用。

4.1 装 PG

ssh server
sudo apt install postgresql-16
sudo systemctl enable --now postgresql

4.2 建库

sudo -u postgres psql <<'EOF'
CREATE USER shaguabijia_app WITH PASSWORD '<strong-random-password>';
CREATE DATABASE shaguabijia OWNER shaguabijia_app ENCODING 'UTF8';
GRANT ALL PRIVILEGES ON DATABASE shaguabijia TO shaguabijia_app;
EOF

生产密码单独生成,不要复用本地的。

4.3 装驱动到生产 venv

cd /opt/shaguabijia-app-server
.venv/bin/pip install "psycopg[binary]>=3.1"

4.4 改生产 .env

DATABASE_URL=postgresql+psycopg://shaguabijia_app:<password>@localhost:5432/shaguabijia

4.5 建表 + 重启

.venv/bin/alembic upgrade head
sudo systemctl restart shaguabijia-app-server
sudo journalctl -u shaguabijia-app-server -f

启动日志里看到 Application startup complete + 没有 5xx 即通过。

4.6 验证

  • 用真账号(手机号)走一遍极光登录或短信登录
  • GET /api/v1/auth/me,确认返回当前用户
  • POST /api/v1/meituan/feed,确认券列表正常
  • psql -U shaguabijia_app -d shaguabijia -c 'SELECT id, phone, created_at FROM "user"' 看到刚登录的用户

4.7 老 SQLite 文件归档

cd /opt/shaguabijia-app-server
mv data/app.db data/app.db.legacy-$(date +%Y%m%d)

不要删——万一要回查测试期残留数据(开发期可能塞了一些手工记录),保留几个月。


5. 兜底:切换失败如何回滚

万一改完起不来:

# 1. .env 改回 SQLite
sed -i 's|^DATABASE_URL=postgresql.*|DATABASE_URL=sqlite:///./data/app.db|' /opt/shaguabijia-app-server/.env

# 2. 把归档的 sqlite 文件还原(如果已经改名)
mv data/app.db.legacy-* data/app.db

# 3. 重启
sudo systemctl restart shaguabijia-app-server

由于 SQLite 文件没动过,无数据丢失,损失只是切换过程中的几分钟 5xx。


6. 顺便建议:同时装上 Redis

PG 上线后,Redis 是下一个明确要补的基础设施(已在 待办与技术债.md 里列了用途)。建议本次维护窗口顺手把 Redis 也装上,不开始用、只装服务,免得下次还要单独申请部署:

sudo apt install redis-server
sudo systemctl enable --now redis-server
# 改 /etc/redis/redis.conf:bind 127.0.0.1(只本机访问)+ requirepass <strong-password>
sudo systemctl restart redis-server

下面三个用途等接的时候再写代码,本次只装服务、不接代码:

用途 当前临时方案 何时必须接 Redis
短信验证码冷却 进程内存 dict(integrations/sms.py) SMS_MOCK=false 上线时 / 扩 worker 时
pricebot trace_id session 进程内存 dict(在 pricebot-backend) pricebot 扩 worker 时
JWT 黑名单(logout 真失效) 当前 logout 占位无失效 P1 鉴权改造时

7. 后续优化(不阻塞上线,一周内补)

  • 每天凌晨 pg_dump 备份:用 systemd timer 或 cron,保留 7 天。备份文件 pg_dump -Fc shaguabijia > /backup/shaguabijia-$(date +%Y%m%d).dump
  • 金钱表加 CHECK 约束:新迁移给 coin_account.balancewithdraw_order.amount_yuan_x100CHECK (... >= 0)。PG 严格执行,SQLite 形同虚设
  • timezone 统一:生产 PG 配 timezone='Asia/Shanghai' 或维持 UTC,团队定一个写进本文档,业务代码统一处理。当前模型混用 func.now()(server-side)和 _utcnow()(models/user.pylast_login_at,Python-side),后者依赖应用层时区,迁完 PG 后建议统一改 func.now()
  • 慢查询观察:shared_preload_libraries = 'pg_stat_statements' 打开扩展,每周看一次 Top 10 慢查询,提前优化

8. 不要做的事

  • 不要搞主从复制 / 读写分离——量级远没到,先用单实例
  • 不要装 PgBouncer / 连接池中间件——SQLAlchemy 自带池够用
  • 不要 squash 旧迁移文件——9 个增量保持原样,让 alembic 一次跑过空库
  • 不要在生产 PG 上裸跑测试套件——用单独的 shaguabijia_test