Files
live-hub-py/AGENTS.md
T
yml2213 5405d4b1d1 fix(overview): 纠正高价值套装以商城 status==1 为最高准入裁判
- 根因:斗鱼官方商城接口 status==1 代表「已达兑换上限」,历史代码误以为商品列表全为全局字段而单纯依赖常被官方清空的 exchange_records,导致已兑换账号被误判为可兑换
- 修复:extract_high_value_goods 结合商城完整列表 status==1 现场判定已兑换,exchange_records 作为历史展示与兜底
- 前端:优化工作台与一键查询展示,明确展示「已达上限 N 个: ...」与「未达上限」,消除反向歧义
- 文档:沉淀坑 #9 到 AGENTS.md,并更正设计-账号一键查询.md
2026-10-07 14:51:51 +08:00

118 lines
9.2 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 协作约定(给 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/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. 已踩过的坑(速查)
| # | 坑 | 应对 |
| --- | --- | --- |
| 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` 仅做历史展示兜底,绝不可当作否定已拥有的依据 |
## 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` = `20261004_0051`;`dev` = `20261007_0052`(**两侧不一致,切分支前先对齐库**) |
| 前端 | `tsc -b` 开了 `noUnusedLocals`,死代码/未用导入直接报 TS6133;镜像构建时会 `npm run build` |
| 测试 | `pytest tests/ -q`(用 `douyu_login_test`);`ruff format` + `ruff check` |