yml2213
|
6865b916ba
|
test(account): 一键查询 / 官方兑换记录 / 生命周期用例
围绕这几轮改动补齐用例(本文件共 53 条):
- **判定规则**:只看官方兑换记录;全局库存**永不**推出「该账号已拥有」
(回归用例 —— 正是第一版踩的坑);字段缺失是 None(未知)而不是 False(没有)
- **失败隔离**:单项外部调用失败不影响其余,且如实写进 errors
- **只读性**:一键查询不发事件、不改生命周期状态
- **官方记录补成事件**:带**真实兑换时间**(不是"发现它的时刻")、
幂等(重复查询不重复落)、无 commodityId 时跳过
- **排序回归**:「最近一次兑换」按事件时间,不是插入 id
- **时间归一化**:0 / "0" / 空 / 负数 → None(1970 那个 bug 的回归)
- **迁移安全**:不得在迁移里 commit(op.get_bind());加列前必须有存在性检查
(扫代码时**跳过注释** —— 注释里正解释着"不要这样做",第一版用例因此假红)
- **接口契约**:详情返回快照与查询时间、列表**不**带整份快照;
断言一律看 model_dump()(只看属性会假绿 —— AccountOut 有手写
@model_serializer,字段没列进去时属性有值、JSON 里没有)
另修两条**把 bug 当期望值**的既有用例:
1. 「最近一次兑换」原断言的是最后**插入**的那条(旧行为按 id 排),
改为真正最近的(按事件时间);
2. 按用户名查找夹具不可靠(make_account("key") 每次建新账号、库里一直累积),
改为按 id 精确查找。
|
2026-10-04 20:04:11 +08:00 |
|
yml2213
|
c2520613ca
|
fix(account): 修一键查询「一直循环」—— 快照字段加到了列表接口而不是详情接口
用户报:账号详情点「一键查询」会一直循环(Network 里刷一片 `overview` 请求)。
## 根因
上一轮我把 `overview_snapshot` / `overview_queried_at` 加到了**列表**接口
(锚点 `**bind_payload(acc),` 第一次出现的位置在列表构造器里),详情接口没有。
两层后果:
1. **详情不返回 `overview_queried_at`** → 前端 `marker !== before` 永远相等
→ 轮询到 120 秒超时。看起来就是"点一下一直转圈刷请求"。
2. **列表每个账号都带一整份 JSON 快照** → 15000 个账号的响应体积爆炸
(这个是我自己引入的回归,比第一个更隐蔽)。
## 修法
- 字段从**列表**移除、加到**详情**(列表刻意不返回,注释写明原因)
- 前端轮询**加自证**:第一次拿到响应时若没有标记字段,**立刻报错退出**,
而不是傻等 120 秒 —— 这类"字段没返回"的 bug 不该再以死循环的形式暴露
## ⚠️ 过程中我又踩了一个,被既有用例抓住
删那 4 行时**把 `**bind_payload(acc),` 一起删掉了**,导致列表不再返回绑定列
(`role_bind_status` 显示的是 Pydantic 默认值 `unknown`)。
`TestListViewKeyColumns::test_item_carries_key_columns` **立刻变红** ——
既有用例替我把这个真回归挡下来了。已恢复,并确认 diff 只剩预期的 3 行。
## 新增回归用例(+2,共 38)
`TestSnapshotExposedOnDetailOnly`:
- 详情**必须**返回快照与查询时间(不返回前端就会轮询到超时)
- 列表**不得**返回完整快照(每个账号一份 JSON,体积爆炸)
并做了**反向验证**:把"详情不返回字段"的 bug 注入回去 → 用例立刻红。
另外把 `TestListViewKeyColumns::_item` 按用户名查找改成按 id ——
`make_account("key")` 每次建新账号而库里一直累积,按用户名可能命中很早以前那个
(这次就让用例假红过一次)。
## 验证
`TZ=Asia/Shanghai` **590 passed / 6 skipped**;`TZ=UTC` 80 passed;
ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 19:19:53 +08:00 |
|
yml2213
|
5ae5d92978
|
feat(account): 兑换记录列 + 修掉 source_ref NOT NULL 导致的事件静默丢失
三件事合成一次提交(按反馈不再一改一提交)。
## ① 账号列表加「兑换记录」列
次数 + 最近一次(商品名 + 日期),填上列表右侧的空档。
数据源是**账号自己的事件流**(`account_events` 的 `exchange`),不是 `douyu_tasks`
的 JSON —— 后者是执行日志,S5 起业务事实已经在事件里了。
性能上刻意**每页只跑两条聚合查询**(不是每行一条 N+1):
一条分组算 `count` + `max(occurred_at)`(走 `(account_id, event_type, occurred_at)`
复合索引),一条用窗口函数 `ROW_NUMBER()` 取每个账号最新那条的商品名。
实测 6 个账号 3 条兑换事件,聚合与「最近」都正确。
「兑换 」前缀在前端剥掉(列头已经叫「兑换记录」,再带前缀是重复)。
## ② 修掉一个静默丢事件的 bug(排查中发现的)
**现象**:开发库上「状态变迁 / 分配 / 标签 / 备注 / CK 清除」这些事件**一条都没有**,
而带幂等键的事件(业务事实、`ck_invalid`、`relogin_*`)照常写入 ——
时间线看起来有数据,只是恰好缺了某一类,**极难发现**。
**根因**:`source_ref` 是**可选**幂等键(模型与迁移文件都是 `nullable=True`,
`emit()` 对没有天然幂等键的事件就传 `None`)。但早期那版迁移
(重编号成 `0041` 之前的 `0040`)建的是 **NOT NULL**,而我后来只改了
**迁移文件**、没管**已建好的库** —— Alembic 只记版本号,不会重放已应用的脚本。
`__pycache__` 里那个 `20261002_0040_*.pyc` 就是旧版留下的痕迹。
**为什么静默**:`emit()` 按设计吞掉任何异常(事件是旁路,不能影响主流程),
只记一条 warning,调用方也从不检查返回值。已实测复现:
`emit(...)` 返回 `False` + warning,实际落库 `0` 条。
**修法**:迁移 `20261004_0045` 改回可空(`MODIFY`,**唯一索引保留**,幂等,
生产若跑过旧版也一并修好)。修复后实测:`emit` 返回 `True`、落库 1 条。
## ③ 补一条能防「未来」同类问题的守卫
`tests/test_migrations.py` 原本只比对「表/列/索引在不在」,**不比对可空性**。
已加:迁移建出的列**不允许比模型更严**(模型说可空、库里 NOT NULL)。
只查这个**危险方向**:反方向(模型 NOT NULL、库里允许 NULL)是无害的宽松,
历史遗留里有 28 处,一并报出来只会制造噪声、逼人修没收益的东西。
> 这条守卫**有边界**,已在代码注释与设计文档 §3.5.1 写明:
> 它在**全新库**上跑整链,只能防「未来的迁移建错」,**防不了已有库的历史漂移**。
> 已验证:临时禁用 `0045`,该用例仍通过(全新链本来就是对的)。
## 验证
- `TZ=Asia/Shanghai`:**503 passed / 6 skipped**(+3 条兑换列用例)
- `TZ=UTC`(迁移 + 列表相关)45 passed;ruff 全绿;前端 build 通过、lint 0 errors
- 真实开发库:迁移已应用(head `20261004_0045`),
`source_ref` 现在 `IS_NULLABLE=YES` 且唯一索引仍在;
不带幂等键的事件确实能落库;兑换聚合返回值正确
注:验证时在开发库那个有 CK 的账号上留了 3 条 `exchange` 演示事件
(兔兔白日梦 / 冠军鸡腿 / 烟花),是为了让列有数据可看,随时可删。
|
2026-10-04 17:25:40 +08:00 |
|
yml2213
|
8a8c637db0
|
refactor(ui): 账号列表页重做 —— 状态分布当筛选入口,砍掉重复与噪声
这页还是 S6 之前的老壳子:4 张统计卡有 3 张是装饰、状态与 CK 两列在说同一件事、
ID 单独占一列、绑定列 12 行全是「精? 电? 店?」。
## 统计卡 → 生命周期分布(可点击筛选)
原来:账号总数 / **标签数** / **已分配** / **未分配**。
标签数在筛选里就能看;已分配与未分配是一对冗余;而且它们都回答不了
「我该干什么」。现在按**生命周期状态**分布:总数 / 正常 / CK失效 / 待人工 /
未登录 / 已停用,**点一下就是「只看这些」**(`statusFilter`),计数为 0 的自动变淡。
后端 `accounts_summary` 相应扩出 `by_status` / `visible_total` /
`ck_invalid_count` / `no_cookie_count`。口径与列表一致:**归档不计入**。
## 表格列收敛
| 改动 | 理由 |
| --- | --- |
| **ID 列 → 并入「账号」小字** | 客服不看 ID,但报障时还要念得出来;合并后省一列宽度 |
| **CK 列 → 并入「状态」副行** | 生命周期状态里的「CK 失效」就是 CK 的结论,两列在说同一件事。合并后主标签给结论、副行小字给细节(检测时间 / `error` 这种「状态正常但没查明白」) |
| **绑定列:三个都 unknown 时显示「—」** | 满屏问号只是噪声,不如一眼看出「还没查过绑定」 |
| **「上传时间」→「导入时间」,精确到日** | 名字一直叫错;列表里只关心「哪一批」,时分秒是噪声(精确值挪进 tooltip) |
| **去掉「按状态筛选」下拉** | 上面卡片就是筛选入口,两处并存是重复(`STATUS_OPTIONS` 随之删除) |
## 验证
- `TZ=Asia/Shanghai`:**500 passed / 6 skipped**(+3 条 summary 用例)
- `TZ=UTC`(相关文件)82 passed;ruff 全绿
- 前端 build 通过、`npm run lint` **0 errors**
- 真实开发库:`by_status = {active: 4, imported: 8}`、`ck_invalid_count=0`、
`no_cookie_count=8`、`visible_total=12`(与库内实际一致)
未动(待确认):「显示凭据」开关与密码/邮箱/邮箱密码三列 —— 这是老「卖号」视图的残留,
默认关闭所以不占地方,但不确定现在的重登/检测流程还需不需要人工读凭据。
|
2026-10-04 16:55:12 +08:00 |
|
yml2213
|
885a780bd3
|
feat(account): S6 账号页重构 —— 列表关键列 + 详情页生命周期状态卡
S6 的展示层部分(数据模型部分见上一个提交)。目标:客服打开账号页要能一眼回答
「这账号能不能用、要不要我处理、账上还有多少」。
## 账号列表:加关键列 + 三个筛选
新增四列,**全部直接来自 `accounts` 行**(不 JOIN、不聚合 —— 列表是全站最热接口):
| 列 | 内容 |
| --- | --- |
| **CK** | 有无 CK / 正常 / 已失效 / 检测异常 + 最近检测时间 |
| **绑定** | 精/电/店 三个作用域各一个小标签(✓ / … / ✗ / ?) |
| **积分 / 鱼翅** | 两行小字 |
新增筛选:**CK 健康**、**角色绑定**(三个作用域任一命中即算)。
`AccountOut` 相应补齐 `has_cookie` / `ck_check_status` / `ck_checked_at` /
`points` / `gold_balance` / 三组绑定状态与标签。
## 账号详情页:生命周期主页
顶部新增**生命周期状态卡**,把之前散落的信息聚成一眼可读的几格:
- **生命周期状态**:状态标签 + 不参与自动化标记 + 变更时间与原因
- **CK**:有无 / 健康 / 最近检测时间
- **角色绑定**:精/电/店三格
- **账上还有多少**:积分 / 鱼翅 / 兑换余额
- **归属与标注**:分配客服(下拉)+ 标签(就地编辑)—— 之前详情页只能看不能改
业务台账补上**登录成功次数**与**最近登录**(`stats` 里本来就有,只是没展示)。
时间线已有分类筛选,生命周期操作入口(停用/归档/恢复/自动化)S3 已加。
## ⚠️ 顺带修掉一个静默 bug
`bind_payload()` 的字段名按**作用域名**拼成了 `elite_bind_status` / `peace_bind_status`,
而 schema 声明的是 `role_bind_status` / `xpd_bind_status`。Pydantic 默认
`extra='ignore'`,于是多出来的键**被静默丢弃**、声明过的字段保持默认值 ——
**接口永远返回 unknown 而不报任何错**(这正是它躲过前面几轮测试的原因)。
修法:前缀改为由**列名**推导(`SCOPE_PAYLOAD_PREFIX`),并在模块导入时自检
「载荷前缀 == 列名前缀」。另加两条用例:`bind_payload` 的键必须都被
`DouyuTaskAccountOut` / `AccountOut` 声明(这是能抓到这类错的那种用例)。
顺带发现我自己之前那条 payload 用例**把错误的前缀当成了预期**,一并修正。
## 自己踩的坑
详情页加客服列表时又把 `useEffect` 写在了早期 `return` 之后 ——
和本次会话前面修 `LoginTasksPage` 的是同一个错。eslint(`react-hooks/rules-of-hooks`)
当场抓住,已上移。**`tsc` 与 `vite build` 都不会报这个**。
另外:列表关键列那处编辑我曾以为已生效(脚本打印了成功),实际没落到文件里 ——
后面是靠「读回文件确认」和用例失败才发现。已改为每次都读回核对。
## 验证
- `TZ=Asia/Shanghai`:**497 passed / 6 skipped**(S6 前半 490 → +7)
- `TZ=UTC`(相关文件)通过;ruff 全绿;前端 build 通过、lint **0 errors**
- **真实开发库端到端**:列表与工作台接口都返回了真实值
(`has_cookie=True` / `ck_check_status=valid` / `role_bind_label=未知`),
修 bug 前这两处返回的是被丢弃后的默认值
未做:`bind_status` 旧列待 S8 drop;保留策略里「状态派生类查询」已可下探到 7 天档(待确认)。
|
2026-10-04 16:45:39 +08:00 |
|
yml2213
|
0a922ce56d
|
feat(account): S4 事件发射点补齐 —— 审计与时间线打通、批量事件、ck_invalid 只认转变
S3 让 `status` 可信,S4 让**时间线完整**:账号上发生的事,时间线里都能查到。
## 补齐「定义了却从没发过」的事件
`assigned` / `tag_changed` 这两个类型早就定义了、前端 `EVENT_STYLE` 也渲染了,
但**全仓没有任何代码发过**(`grep` 只有定义处)。本次接上:
- 单个分配 / 批量分配 → `assigned`(detail 带 assigned_to / assigned_username)
- 单个改标签 / 批量改标签 / 按筛选批量改标签 → `tag_changed`(detail 带 from/to)
## 新增 `ck_cleared`,并修掉 `ck_invalid` 的刷屏
- **`ck_cleared`**:清 CK 是**不可逆**操作(重登当前成功率极低),时间线里却不留痕。
现在单个清除与批量清除共用 `account_lifecycle.on_ck_cleared()`。
- **`ck_invalid` 改为只在「由好变坏」时发**。旧实现每轮巡检只要判定 invalid 就发一条,
源码注释写着「避免每轮刷屏」,**但代码并没有做到** —— 6 小时一轮、失效一周就是
28 条一模一样的事件。判定失效是**一次状态转变**,不是每轮读数。`error`(查不到)
同样不改变结论、不发事件。
## 审计与时间线一次写完(防漏一边)
新增 `audit_service.record_account_action()`:**审计 + 账号事件一起写**,并把
`account_id` 落到审计上。这两件事必须成对出现 —— 只写审计,时间线就缺「谁改了什么」;
只发事件,合规审计就缺一条。分成两次调用写,迟早有人漏一边(`assigned` 就是这么漏的)。
顺带把批量删除的审计补上被删账号 id(`detail.account_ids`,截前 200 条):
删账号会**连带删掉它的事件时间线**(外键),审计是唯一痕迹 —— 这正是 10-03
那次事故要留的东西。
## 批量操作不再逐条开 SAVEPOINT
批量分配/改标签可能覆盖几千个账号,而每个账号的时间线都该各留一条。
逐条走 `emit()` 会给每条开一个 SAVEPOINT,几千条时请求明显变慢。
新增 `emit_bulk()`:一次 executemany,整批包在**一个** SAVEPOINT 里,
与 `emit()` 一样**绝不抛异常**(整批失败就整批丢弃 + warning,不影响业务写入)。
## 检测入口收口:4 处 → 1 个 `on_ck_verdict()`
`ck_health` / 人工检测 / 工作台执行前检测 / 重登后复检 原本各自写
`ck_check_status` + `ck_check_result` + `ck_checked_at` + 发事件 + 调 `sync_status`,
口径容易分叉(`ck_invalid` 的刷屏就是这么来的)。现在统一走
`account_lifecycle.on_ck_verdict()`,顺序固定:**写结论 → 由好变坏记事件 → 重算状态**。
至此 `account_lifecycle` 成为**唯一**写生命周期字段的模块,并加了一条**源码扫描用例**
把它钉死(任何地方直接 `acc.status = ...` 都会让它红,并指出 file:line)——
比写在文档里的约定可靠。虎牙的 `huya_accounts.status` 是同一个病,但属另一套模型,
已在用例里显式排除并注明。
## 接口与前端
- 时间线支持 `?category=` 筛选(lifecycle / auth / business / ops);
响应补 `category` / `subject_*` / `actor_*`
- 前端 `EVENT_STYLE` 补上 `status_changed`(**S3 我漏的**:加了事件类型却没加样式,
页面上显示成原始英文)与 `ck_cleared`;时间线加「分类」筛选
## 验证
- `TZ=Asia/Shanghai`:**420 passed / 6 skipped**(S3 后 404 → 新增 16 条)
- 迁移链 3 passed;ruff check/format 全绿;前端 `tsc -b && vite build` 与
`npm run lint`(0 errors)均通过
- **两条关键用例都做过「反向验证」**(先注入旧行为确认会红):
- 把 `ck_invalid` 改回「每轮都发」→ 用例报 `3 == 1`;
- 在路由里直接写 `acc.status = ...` → 不变量用例给出精确 file:line。
- 新用例还覆盖:批量分配/改标签逐账号发事件、清 CK 事件与审计关联、
`emit_bulk` 的跳过与容错、时间线分类筛选、批量删除的 id 留痕。
未做(后续):S5 业务事实直发事件 + `douyu_tasks` 保留策略、S6 账号页重构 +
`bind_status` 三列拆解。`remark_changed` 目前没有改备注的接口,故无发射点(已在文档标注)。
|
2026-10-04 15:09:46 +08:00 |
|
yml2213
|
1a8710834a
|
feat(account): S3 生命周期状态收口 —— 6 状态状态机、唯一推导入口、停用/归档闭环
`status` 列从「空壳」变成可信状态:S1 建了列、S2 让账号独立,本提交让状态真正
反映账号处境,并把「哪些账号要人处理」变成可筛选的事实。
## 状态机(7 → 6 个状态)
`imported` / `active` / `invalid` / `manual` / `disabled` / `archived`
**砍掉 `relogin`**:它完全等价于 `invalid + relogin_next_at is not None`,
独立成状态就多出一个必须与 `relogin_next_at` 同步的字段 —— 正是要消灭的「双份真相」。
## 唯一推导入口(防「双份真相」)
新增 `services/account_lifecycle.py`:
- `derive_status(account)` —— **纯函数**,按 cookie / ck_check_status / relogin_*
推导状态;人工态(disabled/archived)优先级最高,不被覆盖;
- `sync_status(db, account)` —— 推导 + 变了才写,**幂等**(没变则不写库、不发事件,
否则巡检每轮都往时间线刷一条);
- `set_manual_state(db, account, state)` —— 停用/归档/`auto`(恢复自动推导);
- `on_login_success(db, account)` —— **「登录成功对账号意味着什么」只定义一次**:
作废本轮重登退避痕迹 + 重算状态。于是**「待人工」的账号被人工登录救回来时状态
会自动恢复**,而不是卡在 manual。
- `MANUAL_KINDS` 从 `jobs/ck_relogin.py` 挪到这里(service 不该反向依赖 job)。
**9 处底层判据写入点全部接线**:登录成功、CK 清除(单个/批量)、ck_health 巡检、
人工 CK 检测、工作台执行前检测、重登复检、重登结算(成功/失败退避)。
## 消费端:补上一个真缺口 + 删掉两处临时派生
- ⚠️ **缺口**:`ck_health` / `ck_relogin` / `count_pending_relogin` 此前只按底层判据
选行,**没排除 disabled/archived** —— S2 提交里已标注,状态一上线就会变成
「停用的账号照样被自动重登」。现已全部排除。
- `automation.manual_queue` 原本用 4 个条件自己拼「需要人工」,现在一句
`status == 'manual'`。同一判据从「3 处各写一遍」收敛成「1 处推导」。
## 接口与前端
- `PUT /api/accounts/{id}/lifecycle`(disabled/archived/auto + 原因,写审计)
- `PUT /api/accounts/{id}/automation`(**接上死开关**:`auto_managed` 此前没有任何
接口/前端能改,永远是默认 True)
- 新增权限点 `account:manage`(运营默认拥有)
- 列表支持 `?status=` 筛选;**归档默认不出现**(要看需显式筛选)
- `AccountOut` / 概览接口补 status / status_label / status_reason / auto_managed
- 前端:账号列表加「状态」列与状态筛选;详情页状态卡 + 停用/归档/恢复/自动化开关
## 两个自查出来的问题(都已修)
1. **幂等键反而丢事件**:状态事件一开始用 `status:{id}:{target}:{秒级时间戳}` 作
`source_ref`,结果同一账号在同一秒内两次切到同一状态就撞唯一键,`emit` 静默吞掉
—— **状态改了、时间线却少一条**,正是本模块要消灭的「悄悄不一致」。
改为不传 `source_ref`:`sync_status` 本身已幂等,不存在重复写入路径。已加用例。
2. **迁移与运行时规则分叉**:迁移 0042 的状态回填 CASE 原本会写出已被砍掉的
`relogin`,且判据与 `derive_status` 不一致(次数用尽的会回填成 invalid 而非 manual)。
已改成逐字对应,并加一条用例**真跑那段 SQL 与纯函数逐条比对**(已验证:
临时改回旧 CASE 该用例立刻红)。
## 验证
- `TZ=Asia/Shanghai`:**404 passed / 6 skipped**(S2 后 373 → 新增 31 条)
- 迁移链 3 passed;ruff check/format 全绿;前端 `tsc -b && vite build` 通过
- 真实开发库核对:落库状态分布 active 4 / imported 8,**与 derive_status 逐账号比对无漂移**
- 新增用例覆盖:derive 真值表(每状态入口 + 人工态优先)、sync 幂等、
人工态切换、**全生命周期走查**(导入→登录→失效→重登→转人工→人工救回→清 CK→停用)、
自动化排除 disabled/archived、列表状态筛选、接口
未做(后续阶段):S4 事件发射点补齐、S5 业务事实直发事件、S6 账号页重构。
|
2026-10-04 14:48:22 +08:00 |
|