yml2213
|
a64380fbcc
|
feat(account): S6 bind_status 三列拆解 —— 四值枚举、唯一写入口、前端改读账号
`accounts.bind_status` 是个**语义垃圾场**:一个自由文本列,6 个文件、**29 处写入**、
20 多种取值,混了三种东西 —— 真状态(`bind_confirmed`)、动作痕迹(`goods_locked`)、
纯查询痕迹(`points_queried`)。它是**覆盖式**的,所以 `bind_confirmed` 会被之后
任意一次查询盖掉 —— 「这个账号到底绑没绑」根本查不出来。
`esports_bind_status` / `xpd_bind_status` 是同一个病。
## 收敛成四值 + 三列一义
`unknown` / `unbound` / `pending` / `bound`,每个作用域一列:
| 列 | 作用域 |
| --- | --- |
| `accounts.role_bind_status`(**新增**) | 精英宝典角色 |
| `accounts.esports_bind_status`(收敛) | 电竞手册角色 |
| `accounts.xpd_bind_status`(收敛) | 和平小店角色 |
- 新增 `services/account_binding.py`:唯一写入口 `set_bind_status()`,
传旧自由文本直接 `ValueError`(否则垃圾会被写回去);`map_legacy_bind_status()` 负责旧值映射。
- **29 处自由文本写入全部删除或改走枚举**:真状态走枚举,动作/查询痕迹**不再写状态列**
(动作已由 S5 的直发事件记录,查询痕迹本来就不该是状态)。
- `accounts.bind_status` 保留原值便于比对,**已无任何写入方**,待 S8 drop。
- 一条**源码扫描用例**禁止任何地方再出现 `account.bind_status = "..."` 这类写入。
## 回填不猜(设计文档 §5.5)
只映射**明确陈述了绑定关系**的取值(`bind_confirmed`/`esports_bound`/`xpd_bound` → bound,
`*_qr_*`/`*_ready` → pending,`game_not_bound`/`xpd_not_bound` → unbound),
**其余一律 unknown**。因为列是覆盖式的,「最后一件事是查询」并不代表没绑定。
迁移 SQL 与 Python 映射有一致性用例:**解析迁移源码里的 CASE 逐条比对**
(已反向验证:故意改错一条映射,用例立刻红)。
## ⚠️ 顺带救回一个差点被我自己丢掉的状态
「**电竞手册是否已开通**」以前**只寄生在 `esports_bind_status` 的 `esports_opened`
取值里**(`elite_opened` 有独立布尔列,电竞没有)。归一化会把那个取值抹成 `unknown`
→ **静默丢状态**。所以:
- 新增 `accounts.esports_opened` 布尔列(与 `elite_opened` 对称),开通任务成功时置位;
- 迁移里**必须先 `save_esports_opened()` 再归一化**,顺序用例钉住;
- 详情页「电竞手册」开通状态改读 `esports_opened`。
## 前端不再从任务日志反推账号状态
工作台原来从**当前加载的那一页任务**里挑每个账号最新的一条来推绑定状态
(`latestConfirmBindTaskByAccount` 等)。两个问题:① 支撑任务一旦翻出窗口,
**显示的状态就是错的**;② 这让任务日志变成「账号状态库」,于是它永远不敢清理。
现在状态读账号字段,任务日志只补「扫码流程中的瞬时信息」(如确认前的
`pending_role_name`)。删掉了两个不再需要的派生与对应的 `clearPending` 绕行逻辑。
## 踩到并修掉的东西
- **模型与迁移不一致**:我把模型收窄成 `String(16)`,迁移却留着 `String(32)` ——
测试库按模型建、生产按迁移建,两边列宽不同。已让迁移也收窄,且**收窄必须在
归一化之后**(旧取值最长 19 字符,先收窄会截断成废值)。downgrade 也补齐了反向操作,
已实测 `downgrade -1 && upgrade head` 干净。
- **parity 用例第一版是抄了一份 SQL**(迁移改了用例也不会红,等于没测)——
改成解析迁移源码;后来又因为列已收窄、19 字符的旧值塞不进测试库,
干脆改成源码级解析比对(不造数据)。
## 验证
- `TZ=Asia/Shanghai`:**490 passed / 6 skipped**(S5 后 450 → 净增 40)
- `TZ=UTC`(相关文件)106 passed;迁移链 3 passed;ruff 全绿
- 前端 `tsc -b && vite build` 通过、`npm run lint` **0 errors**
- 真实开发库:迁移重放后三列均为 `varchar(16) NOT NULL DEFAULT 'unknown'`;
12 个账号的旧值 `elite_status_queried`(纯查询痕迹)→ **unknown**(没有猜成 bound)
未做(后续):S6 的**账号页重构**(列表关键列 / 生命周期主页)保留;
`bind_status` 旧列待 S8 drop;保留策略里「状态派生类查询」现在技术上可下探到 7 天档,
保守起见留待确认(影响的是在用页面显示,不是历史)。
|
2026-10-04 16:28:06 +08:00 |
|
yml2213
|
c3fd299f3a
|
test(db): 测试改用真实 MySQL,方言差异不再漏到生产
生产是 MySQL 8、测试是 SQLite,两者的方言差异在本地完全测不出来。今天为此
付出了两次代价:
1. `ORDER BY ck_checked_at ASC NULLS FIRST` 在 SQLite 上完全正常,本地 258 条
全绿,一部署到生产第一次巡检就 1064 语法错误(MySQL 不支持 NULLS FIRST);
2. P1b 自动重登的结算复用调度器事务读取,在 SQLite 下看不出问题,但在 MySQL 的
REPEATABLE READ 下读到的是批次开始前的快照 —— 10 条实际成功的重登全部被记成
失败。SQLite 的读一致性与 MySQL 不同,这类问题跑一万遍也复现不出来。
改动:
- 新增 tests/support/db.py:测试专用引擎/会话工具,连接串从 .env 读取,
默认指向独立的 douyu_login_test 库;
- 新增 tests/conftest.py:会话级建好表结构,每个用例前后清空所有表做隔离;
- 根 conftest.py 接管连接串设置:它在 pytest 收集阶段最先执行,在这里把
DATABASE_URL 指向测试库,应用模块首次 import 拿到就是 MySQL,不依赖任何
import 顺序(原先这里是 sqlite:// 兜底,改造后会变成「先建 SQLite engine
再打补丁换成 MySQL」,顺序一变就静默跑错库);
- 21 个测试文件的 setup 样板统一改用共享测试引擎;
- 两个迁移测试改为在临时库 douyu_login_mig_<随机> 上跑完整 alembic upgrade head,
跑完 drop —— 迁移脚本本身也终于在 MySQL 上被验证;
- dev.sh 与 README 的过时说明一并更新(原先写着「不需要 MySQL / .env 配置」;
「测试各自建 SQLite 内存库」)。
安全与易用:
- 测试库名与开发库同名时**直接拒绝运行**(clear_tables 会 DELETE 所有表,
一旦 TEST_MYSQL_DATABASE 被指到 douyu_login 就是数据灾难);
- 连不上 MySQL 时给出可操作提示(先 ./dev.sh 起库 / 用 TEST_MYSQL_DATABASE 覆盖),
而不是抛一段 SQLAlchemy 原始堆栈。
验收:
- 全量 301 条通过,连跑两次结果一致(隔离有效),耗时约 12.5 秒(原 SQLite 约 3.6 秒);
- 单文件独立运行通过(不依赖全量执行顺序);
- `grep -rn sqlite tests/ conftest.py` 已无实际用法;
- **方言确实生效**:临时把一条查询改回 `NULLS FIRST`,13 条测试立刻变红(含运行期
失败),还原即恢复;这条由 test_generated_sql_is_mysql_compatible 长期守着。
代价(已在 README 与规划附录 C.2 记明):跑测试前必须先有 MySQL,pre-push 钩子
同理;CI 需要 service container。
|
2026-10-02 21:22:08 +08:00 |
|
yml2213
|
99db772ddd
|
feat(douyu): 精英宝典支持查询开通状态,开通前先校验且不再误判到账
线上两个问题:工作台看不到账号有没有开通精英宝典;点开通时二维码会「闪一下
就变小」——大图弹窗 2~4 秒后自动关闭,只剩列表里 56px 的小二维码。
根因:
- getUserInfo 的 manualType/expireTime 才是开通状态的权威口径,但只有兑换
流程用过一次,没有任务类型对外暴露;前端只看积分列,而「开通后积分花完」
和「从未开通」都是 0 分,区分不了(实测 13456 manualType=1/score=0);
- 开通前不查状态,且到账判定写成「积分 >= 300」:账上本来就有 680 分的账号
在出码后第一轮轮询就被判成功,前端按 paymentArrived 自动关掉了弹窗。
改动:
- 新增任务类型 query_elite_status:查 manualType/manualScore/expireTime 并
落库到 accounts.elite_opened / elite_expire_time(迁移 20261001_0036),
精英工作台加查询按钮与「宝典状态」列(已开通+到期 / 未开通);
- create_elite_qr 开通前先查状态:已开通直接判成功、不再生成支付码(方案 A),
既消除二维码闪现,也避免给已开通账号开出真实支付单;
- 到账判定改为「积分比开通前增长」,基线用与轮询同源的 credit/balance 取,
取不到基线才退回旧口径;
- 支付弹窗不再自动关闭:到账/失败/停止都在弹窗里给结论 + 手动关闭;列表小
二维码在任务进终态后隐藏。
新增 9 条测试覆盖上述行为,回退 runner 时其中 8 条失败。
|
2026-10-01 19:08:25 +08:00 |
|