2026-10-18 部署礼包修复时实际踩到:`docker compose build` 出了新镜像 (ID 变了),`docker compose up -d douyu-login` 只打了句 Started, 容器仍是上一版(ps 显示 Up 11 hours,容器里搜不到新函数),差点把 「没生效」报成「部署成功」。 - AGENTS 坑 #10:重建过镜像就 `up -d --force-recreate <服务名>` (必须带服务名,不带会把 mysql 一起重建),部署后核对镜像 ID 或 grep 容器内新代码。 - 部署清单同步改命令(顺带给 `docker compose run` 补上 -T 与 </dev/null, 同 AGENTS 坑 #5)。
11 KiB
11 KiB
协作约定(给 AI 助手 / 协作者)
用户明确表达过的偏好,加上已经用事故买来的教训。每次在这个仓库里干活前先读一遍。 每条都对应一个真实踩过的坑 —— 删之前先确认那个坑不会再犯。
1. 分支与提交
main= 已发布(生产跑的就是它)。功能开发从 main 开分支:git checkout -b feat/<功能名> main,做完合回main。dev是历史工作分支,已与 main 分叉(dev 上有未上线的账号自动分配功能)。 ⚠️ 旧的「推完 main 再dev merge --ff-only main」约定已作废 —— 分叉后这么做会失败;以后要同步 dev 只能真合并,并且必须显式确认。- 不要改一点就提交、推送。 小改动先攒着、攒够一批再提交;只有用户明确说 「提交 / 推送」,或到了一个自然的里程碑(一个完整功能做完、一轮反馈全部改完) 才 commit / push。一次提交要整合相关改动,别拆成一堆碎提交。
- 远端名不统一:本地是
gitea,生产服务器上的同一个仓库叫origin(URL 相同)。
2. 功能改动
- 新功能是「增加」,不是「替换」。 旧路径要完整保留(例:账号检测保留了手动粘贴, 新增「按账号选择」;旧的手动输入逻辑一行未动)。做新功能时先想清楚旧路径会不会被影响, 需要扩展的地方用可选参数 / 并集,不要改默认行为。
- 改动要务实、克制:不做没人要的重构,不加用不上的抽象。
- 不要重复展示同一份数据。 数据库里已有的字段(如
accounts.points/gold_balance/elite_opened)就让已有界面显示,查询去刷新它们,不要另开一份副本 —— 同一屏两份会让人分不清哪份是准的。 - 单项外部调用失败不要让整个流程失败,但要如实暴露(错误写进
errors/ 返回值 并在 UI 展示),别让「没查到」被读成「没有」。 - 旁路动作(写事件、写审计、释放资源)绝不能带崩主流程:单独 try/except + 记 warning, 但要留兜底(如定时作业)把漏掉的补上。
3. 验证要求(怎么证明你做对了)
- 结论要有验证:能实测就实测(有生产镜像库
douyu_login_prod可只读查), 关键是反向验证 —— 把 bug 注入回去,确认用例真的会变红,再还原。 「跑了一下没报错」不算验证。 - 提交/推送会跑钩子,所以本地必须按同样标准自查:
pre-commit:ruff format --check(只跑ruff check不够,格式不对会直接拦下提交)- 前端
tsc -b;
- 前端
pre-push:上面这些 + 全量 pytest。
- 断言要看
model_dump()(序列化结果),只看属性会假绿(原因见坑 #2)。 - 别信「测试通过」 —— 用例可能是假绿的(原因见坑 #3)。
4. 数据库与迁移
- 迁移编号按落地顺序取,不预留;已应用的迁移不能改名。
- 加列/加表后,测试库不会自己长出新结构 ——
tests/support/db.py用的是Base.metadata.create_all,它只建缺失的表,不会 ALTER 已有表。 症状是Unknown column,处理办法:重建测试库(DROP/CREATEdouyu_login_test)。 - 本地
dev.sh在跑时,改迁移文件会被它自动应用到你正在用的库。 所以切分支前先确认「库的迁移版本 == 目标分支的 head」,否则应用会因为 alembic 找不到 revision 直接起不来(run_migrations没有兜底,会带崩 lifespan)。 main与dev的迁移链已经分叉成两个 head(共同祖先是20261008_0052):main走20261015_0053→ … →20261016_0059,dev走20261008_0053(账号租约)。 两个 0053 的 revision id 不同,所以 alembic 不会报「重复 revision」, 而是报 Multiple head revisions are present —— 合并dev时必须先补一个alembic merge节点,否则alembic upgrade head会直接失败(部署会卡在迁移那一步)。 顺带:本地douyu_login现在停在main的 head,切到 dev 分支前必须先回退/重建库。- 本地库:
douyu_login(dev)、douyu_login_test(测试,不要拿别的库跑测试套件)、douyu_login_prod(生产镜像)。 另有douyu_login_prodclone(停在20261003_0040)与douyu_login_rehearsal(停在20261004_0049):是 S1–S7 上线演练留下的生产克隆快照(各 15,182 账号), 代码里没有任何引用 —— 只读参考,别当测试库用。
5. 部署与生产
生产:SSH 别名 order+douyu_huya,目录 /root/live-hub-py,跑的就是 main。
完整清单见 docs/上线清单-S1-S7部署.md,核心步骤:
# ① 部署前(缺一不可):备份 + 采基线
docker exec live-hub-py-mysql-1 sh -c 'mysqldump ... douyu_login' | gzip > /root/backup_$(date +%m%d_%H%M).sql.gz
gzip -t /root/backup_*.sql.gz
# 采 CK 基线:样本解密长度 + 持有 CK 账号数(部署后要一字不差地对比)
# 查在途任务:正在跑的批次会被重启打断,先看能不能等
# ② 执行:先停 → 再迁移 → 再起(镜像重建过就必须 --force-recreate + 服务名)
docker compose stop douyu-login
docker compose run --rm -T douyu-login alembic upgrade head </dev/null
docker compose up -d --force-recreate douyu-login
# ③ 部署后:版本 / CK 基线 / 新接口 / 前端产物 / 日志无 ERROR,逐条核对
# 另外核对「运行中的镜像 ID == 刚构建的镜像 ID」(见坑 #10)
- 部署前必须备份(即使这次零迁移);CK 检查是唯一不能省的功能性验证 (CK 丢了补不回来)。
docker compose build douyu-login内部会npm run build前端 —— 改了前端不用手工构建, 但部署后要去产物里确认新代码真在里面(grep一个新增的界面文案)。- 只动 app 容器,别碰 mysql(历史上
up -d --force-recreate把 mysql 一起重建, 每次部署 90 秒 502)。
6. 汇报
- 如实报告,包括自己的错误、误判和局限。 不确定就说不确定,别猜; 报错了要主动更正(数字尤其要能复现:说「新增 N 个用例 / 基线 M」之前先跑一遍数出来)。
- 用中文回答。
7. 已踩过的坑(速查)
| # | 坑 | 应对 |
|---|---|---|
| 1 | 别拿不同时刻的快照做对比 —— 曾把相隔两个月的两条记录对比,把「时间差异」当成「账号差异」,得出错误规则写进设计文档 | 对比必须在同一时刻;时间跨度大就重新采数 |
| 2 | Pydantic 静默丢字段(本仓库踩过三次,表现各不相同):① 构造函数传了、模型没声明 → extra='ignore' 丢掉;② 模型声明了、手写 @model_serializer 里没列 → 属性有值、JSON 没有;③ 字段名与 schema 不一致(elite_bind_status vs role_bind_status) |
断言看 model_dump();改完接口先在 Python 里打一遍确认字段真在里面 |
| 3 | 用例假绿:缺 session.commit() 时 MySQL REPEATABLE READ 让本会话看不到别的会话刚提交的行(脚本改没改都显示没改) |
跨会话断言前先 rollback()/commit() 结束当前事务再读;关键用例做反向验证 |
| 4 | 用「从 A 到 B」的区间删除删代码块很危险,会连带删掉中间的行(曾把 **bind_payload(acc), 一起删掉) |
改完读回来确认;能用精确替换就别用区间 |
| 5 | docker compose run 默认读 stdin:用 ssh 'bash -s' < 脚本 喂进去时,它会把脚本剩下的行吃掉 |
用 docker compose run -T --rm ... </dev/null |
| 6 | 镜像同步脚本会白等 15 分钟:pgrep -f 'mysqldump.*douyu_login' 匹配到了自己所在的 shell,等待循环永不提前退出 |
判定远端进程存活别用会自匹配的 pgrep -f 模式 |
| 7 | set -e + 输出过滤(管道接 grep -v)会半途静默中止:过滤把输出全吃掉时 grep 返回 1,被 set -e 当成失败(曾在「已删库、还没导入」处中止,镜像库一度是空的) |
过滤输出后别让 set -e 接管退出码;关键步骤显式检查成功 |
| 8 | ruff check 通过 ≠ 能提交:钩子查的是 ruff format |
提交前本地跑一遍 .githooks/pre-commit |
| 9 | 偷换概念把「账号级状态」当成「全局字段」:斗鱼商城 storehome 的 count(库存)确实是全局的,但 status 字段是强账号维度的!status=1 就是「已达兑换上限」(限量 1 件已满/已兑换)。曾把两者混为一谈,误以为商品列表全不可信,跑去依赖早已被官方清空的 exchangeList,导致已兑换的账号被错误打上「两个都可兑换」绿标 |
判定高价值资产准入:以 storehome.status==1 为最高裁判;exchangeList 仅做历史展示兜底,绝不可当作否定已拥有的依据 |
| 10 | 镜像重建了,docker compose up -d 不一定换容器:2026-10-18 实际踩到 —— build 出了新镜像(ID 变了),up -d douyu-login 只打了句 Started,容器还是旧镜像(docker compose ps 显示 Up 11 hours,容器里根本找不到新函数)。差点把「没生效」报成「部署成功」 |
重建过镜像就 docker compose up -d --force-recreate <服务名>(必须带服务名,不带会把 mysql 一起重建);部署后核对镜像 ID:docker inspect -f '{{.Image}}' <容器> 要等于 docker images --no-trunc 里 latest 的 ID,或直接 grep 容器内新增的函数/文案 |
8. 仓库事实(速查)
| 分支 | main = 已发布(生产跑它);dev = 已分叉的历史工作分支;新功能从 main 开 feat/* |
| 生产 | SSH order+douyu_huya(47.114.114.225),目录 /root/live-hub-py,容器 live-hub-py-douyu-login-1 |
| 本地服务 | ./dev.sh(8800 端口,--reload);启动时会自动跑迁移 |
| 本地 MySQL | 容器 live-hub-py-mysql-1,端口 3307;库见 §4 |
| 生产镜像库 | douyu_login_prod(scripts/refresh_local_prod_mirror.sh 同步,只读用) |
| 迁移 head | main = 20261016_0059;dev = 20261008_0053(已经分叉成两个 head,见 §4) |
| 前端 | tsc -b 开了 noUnusedLocals,死代码/未用导入直接报 TS6133;镜像构建时会 npm run build |
| 测试 | pytest tests/ -q(用 douyu_login_test);ruff format + ruff check |