docs(agents): 修正分支约定,补上这轮实践买的教训

- **删掉已作废的约定**「推完 main 再 `dev merge --ff-only main`」:
  dev 现在与 main 已分叉(上面有未上线的账号自动分配功能),ff-only 必然失败。
  改为:`main` = 已发布,新功能从 main 开 `feat/*`;要同步 dev 只能真合并且需显式确认。
- 新增一批实证教训:提交/推送的钩子要求(`pre-commit` 查的是 `ruff format`,
  只跑 `ruff check` 会被拦)、测试库 `create_all` 不会 ALTER 已有表(症状
  `Unknown column`)、`dev.sh` 会自动应用迁移导致切分支后应用起不来、
  生产部署的完整流程与三项前置检查。
- 坑速查 4 → 8 条:新增 `pgrep -f` 自匹配白等、`set -e` + 管道过滤半途静默中止
  (曾让镜像库变空)、`ruff check` ≠ 能提交;并补上 REPEATABLE READ 的具体应对。
- 据实补全本地库清单:先 grep 全仓库确认无引用,再查内容,确认
  `douyu_login_prodclone` / `douyu_login_rehearsal` 是 S1–S7 演练留下的快照。
- 原有教训一条未删,整体按「干活顺序」重排(60 → 116 行)。
This commit is contained in:
yml2213
2026-10-07 13:20:33 +08:00
parent d11fa1032d
commit 05b2bf8579
+99 -43
View File
@@ -1,60 +1,116 @@
# 协作约定(给 AI 助手 / 协作者)
> 这些是用户明确表达过的偏好。**每次在这个仓库里干活前先读一遍。**
> 用户明确表达过的偏好,加上**已经用事故买来的教训**。每次在这个仓库里干活前先读一遍。
> 每条都对应一个真实踩过的坑 —— 删之前先确认那个坑不会再犯。
## 提交与推送
## 1. 分支与提交
- **不要改一点就提交、推送。**
小改动先攒着,**攒够一批**再提交;只有用户明确说「提交 / 推送」,
或者到了一个自然的里程碑(一个完整功能做完、一轮反馈全部改完)才 commit / push。
- 一次提交要**整合**相关改动,不要拆成一堆碎提交。
- 推送时 `main` 与 `dev` 一起同步(`dev` 是工作分支):
`git push gitea main` → `git checkout dev && git merge --ff-only main && git push gitea dev`。
- `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`
- **不要重复展示同一份数据。** 数据库里已有的字段(如 `accounts.points` / `gold_balance` /
`elite_opened`)就让已有界面显示,查询去**刷新**它们,不要另开一份副本 ——
同一屏两份会让人分不清哪份是准的。
- **单项外部调用失败不要让整个流程失败**,但要**如实暴露**(错误写进 `errors` / 返回值
并在 UI 展示),别让「没查到」被读成「没有」。
- 旁路动作(写事件、写审计、释放资源)**绝不能带崩主流程**:单独 try/except + 记 warning,
但要留兜底(如定时作业)把漏掉的补上。
## 报表 / 汇报
## 3. 验证要求(怎么证明你做对了)
- **如实报告,包括自己的错误、误判和局限。** 不确定就说不确定,别猜。
- **结论要有验证**:能实测就实测(本仓库有生产镜像库可用),
能反向验证(把 bug 注入回去看用例是否变红)就反向验证。
- **结论要有验证**:能实测就实测(有生产镜像库 `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/CREATE `douyu_login_test`)。
- **本地 `dev.sh` 在跑时,改迁移文件会被它自动应用到你正在用的库**。
所以切分支前先确认「库的迁移版本 == 目标分支的 head」,否则应用会因为
alembic 找不到 revision **直接起不来**(`run_migrations` 没有兜底,会带崩 lifespan)。
- 本地库:`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`**,核心步骤:
```bash
# ① 部署前(缺一不可):备份 + 采基线
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 账号数(部署后要一字不差地对比)
# 查在途任务:正在跑的批次会被重启打断,先看能不能等
# ② 执行:先停 → 再迁移 → 再起
docker compose stop douyu-login
docker compose run --rm -T douyu-login alembic upgrade head </dev/null
docker compose up -d douyu-login
# ③ 部署后:版本 / CK 基线 / 新接口 / 前端产物 / 日志无 ERROR,逐条核对
```
- 部署前**必须备份**(即使这次零迁移);**CK 检查是唯一不能省的功能性验证**
(CK 丢了补不回来)。
- `docker compose build douyu-login` 内部会 `npm run build` 前端 —— 改了前端不用手工构建,
但部署后要**去产物里确认新代码真在里面**(`grep` 一个新增的界面文案)。
- 只动 app 容器,**别碰 mysql**(历史上 `up -d --force-recreate` 把 mysql 一起重建,
每次部署 90 秒 502)。
## 6. 汇报
- **如实报告,包括自己的错误、误判和局限。** 不确定就说不确定,别猜;
报错了要主动更正(数字尤其要能复现:说「新增 N 个用例 / 基线 M」之前先跑一遍数出来)。
- 用中文回答。
## 一次踩过的坑(值得记住)
## 7. 已踩过的坑(速查)
- **别拿不同时刻的快照做对比。** 曾经把相隔两个月的两条记录对比,
把「时间差异」当成了「账号差异」,得出错误规则并写进设计文档。
对比必须在**同一时刻**。
- **Pydantic 会静默丢字段 —— 本仓库已经踩过三次**,每次表现都不同:
1. 构造函数传了、模型上没声明 → `extra='ignore'` 丢掉;
2. 模型上声明了、但**手写的 `@model_serializer` 里没列** → 属性有值、JSON 里没有;
3. 字段名与 schema 声明的不一致(如 `elite_bind_status` vs `role_bind_status`)。
**应对**:断言要看 **`model_dump()`(序列化结果)**,只看属性会假绿;
改完接口先在 Python 里打一遍 `model_dump()` 确认字段真在里面。
- **别信"测试通过" —— 用例可能是假绿的(缺 `session.commit()` 时,
MySQL REPEATABLE READ 会让它看不到别的会话刚提交的行)。
关键用例要做反向验证。
- **删代码块时用"从 A 到 B"的区间删除很危险**,容易连带删掉中间的行
(曾把 `**bind_payload(acc),` 一起删掉)。改完要**读回来确认**。
- **`docker compose run` 默认读 stdin**:脚本用 `ssh 'bash -s' < 脚本` 喂进去时,
它会把脚本剩下的行吃掉。要用 `docker compose run -T --rm ... </dev/null`。
| # | 坑 | 应对 |
| --- | --- | --- |
| 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` |
## 仓库事实
## 8. 仓库事实(速查)
| | |
| --- | --- |
| 工作分支 | `dev`(本地开发);`main` = 已发布;远端是 `gitea` |
| 生产 | SSH 别名 `order+douyu_huya`(47.114.114.225),部署目录 `/root/live-hub-py` |
| 本地生产镜像 | `douyu_login_prod`(`scripts/refresh_local_prod_mirror.sh` 同步) |
| 测试库 | `douyu_login_test`(独立库,**不要**拿镜像库跑测试套件) |
| 迁移 | 编号按落地顺序取,**不预留**;已应用的迁移不能改名 |
| 前端 | `tsc -b` 开了 `noUnusedLocals`,死代码/未用导入会直接报 TS6133 |
| 分支 | `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` = `20261004_0051`;`dev` = `20261007_0052`(**两侧不一致,切分支前先对齐库**) |
| 前端 | `tsc -b` 开了 `noUnusedLocals`,死代码/未用导入直接报 TS6133;镜像构建时会 `npm run build` |
| 测试 | `pytest tests/ -q`(用 `douyu_login_test`);`ruff format` + `ruff check` |