yml2213
|
d11fa1032d
|
feat(login-tasks): 按状态导出 CSV + 登录失败写进账号生命周期
一、失败入账号一生(此前完全缺失)
生产实测:`login_ok` 1,456 条、`relogin_failed` 73 条,但**没有 login_failed**,
而 login_tasks 里 attempt_status='failed' 有 247 条 —— 这 247 次失败在账号时间线上
一条痕迹都没有,「这账号为什么一直登录不上」在账号页上无从回答。
- 新增事件类型 login_failed,在 `_mark_attempt_failed` 里写入
- **被停止 / 被中断不写**:分型把「已停止/中断/服务重启」归到 interrupted,
那不是账号的问题 —— 否则运营按一次停止就能给几十个账号刷满假失败
- 事件带分型(captcha/credential/email_code/email_quota/network),
同时落进 detail.kind,以后可直接按「验证码失败」筛账号
- 幂等键 login_task:{id}:failed:{ts},与成功路径同构
二、失败分型下沉成共用 service
`classify_failure` 原在 automation/jobs/ck_relogin.py,手动登录也要用。
挪到 services/login_failure.py(job 改为 import,`ck_relogin.classify_failure`
这个名字保留,旧调用方与用例不受影响),并提供型别中文标签与
`is_account_fault` 判据 —— 与仓库既有的「service 不该依赖 job」分层一致。
三、按状态导出 CSV
- 新增 GET /api/login/tasks/export:不分页,导出当前筛选的**全部**匹配记录;
列:账号/状态/失败原因/失败分型/批次/最近尝试时间(UTC)/标签/归属客服
- 权限与筛选与列表**完全共用**,客服只导出自己名下账号
- 带 UTF-8 BOM(不加 Excel 打开中文表头是乱码),导出动作写审计
四、修一个口径不一致的真 bug
页面「失败 N」= failed + error 之和,但筛选只认 failed ——
所以截图里「失败 389」点进去只有 386 条(差的就是那几条 error);
「清空失败」同样漏删 error。现在把状态组收敛成一份 `_apply_status_filter`,
列表 / 导出 / 批量删除三处共用。
测试:684 passed, 6 skipped(本次新增 13 个);三条关键断言做了反向验证
(去掉 stopped 判断 / 失败组去掉 error / 去掉 BOM,对应用例均变红)。
|
2026-10-07 12:54:57 +08:00 |
|
yml2213
|
281769b440
|
feat(accounts): 账号列表按归属筛选,批量分配支持按筛选全选
原来只有 assigned_only(已分配),筛不出「未分配」,也看不到账号归谁。
新增 assign_status/assigned_user_id 可与标签、CK、搜索等叠加;
BatchAssign 支持 all_matching,「筛出未分配 → 一键分给某人」不必先勾选。
|
2026-10-07 09:13:54 +08:00 |
|
yml2213
|
5da12df16b
|
feat(account-check): 登录接口可选 wgapi / iframe
账号检测原来把 WgapiLoginAPI 写死在执行器里,收不到邮箱验证码时只能干等;
登录任务早就可选 iframe(邮箱验证走 iframe 接口)。默认仍 wgapi,旧行为不变。
|
2026-10-06 20:19:26 +08:00 |
|
 yml2213andCursor
|
39388c56b5
|
fix(permissions): 收回客服的 CK 检测重登和自动巡检入口
客服默认不再有 cookie:operate;自动巡检菜单改按 automation:view 显示。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-06 12:20:30 +08:00 |
|
 yml2213andCursor
|
f2157f67f7
|
feat(accounts): 账号列表按列排序,默认把还能换套装的排到前面
分页在服务端,点表头按全库排,不能只排当前这一页。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-05 18:50:43 +08:00 |
|
 yml2213andCursor
|
998998c390
|
feat(automation): 巡检和重登优先保住高价值账号的 CK
价值按套装和积分现算,不看标签。第 1、2 档 6 小时巡检一次,并先占重登名额。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-05 17:51:04 +08:00 |
|
yml2213
|
27e9107e38
|
fix(accounts): 修复鱼翅筛选参数类型为浮点数并优化错误提示
1. 后端将 min_gold 与 max_gold 参数及 Schema 类型从 int 改为 float | None,支持小数鱼翅(如 >0 快捷筛选的 0.01);
2. 修复前端 axios 拦截器在遇到 FastAPI 422 验证错误时未展开 detail 导致弹出 [object Object] 的问题;
3. 单元测试补充验证浮点数鱼翅过滤场景。
|
2026-10-05 11:36:46 +08:00 |
|
yml2213
|
512ea3db06
|
feat(accounts): 增加高价值资产一键筛选与高级自定义筛选功能
1. 后端新增高价值套装状态筛选(双套均可换/可换白日梦/可换萌兔/已拥有/高价值待救/未查资料),兼容 MySQL 原生 JSON 与 SQLite unicode 转义存储;
2. 后端新增多维资产与状态区间筛选(积分范围、鱼翅范围、宝典开通、电竞开通、角色绑定状态),并支持与批量打标签/删除联动;
3. 统计接口 accounts_summary 新增 high_value 聚合数据,毫秒级返回各类高价值资产数量;
4. 前端账号管理页面新增「高价值一键筛选」胶囊区(带数量徽标与激活状态,即点即滤);
5. 前端新增可折叠「高级自定义筛选」面板,支持套装状态下拉、积分快捷区间(≥300、≥600)、鱼翅快捷区间(>0、≥6)、宝典与角色绑定状态多条件组合;
6. 补充 6 个高价值筛选与资产区间单元测试,全量用例通过。
|
2026-10-05 11:23:48 +08:00 |
|
yml2213
|
19f1cbceba
|
fix(automation): 修复作业完成状态回写崩溃、统计字段对齐及表单布局挤压
1. 修复 DouyuBatchRunner 在 run 结束时私自关闭外部 Session 导致作业状态无法回写并死锁在 running 的问题(增加 close_db 参数);
2. 修复资料补全作业返回值映射,将 succeeded/failed 对齐到 valid/invalid,并在全库暂无待查账号时给出清晰说明;
3. 增强调度器 _run_one 边界防御:支持字段名兜底与 Session 崩溃时独立连接兜底回写,防止作业无限重复发起;
4. 修复巡检作业在全库无到期账号时的说明信息,如实展示最久检测时间与重检间隔;
5. 修复 AutomationPage 布局:移除外层 Form 的 layout=inline,Card 恢复 100% 满宽纵向排列,内部字段横向弹性自适应;
6. 修复未启用作业因残留历史 next_run_at 而错误显示「排队中」的问题。
|
2026-10-04 22:11:44 +08:00 |
|
yml2213
|
e5f2d2d5d2
|
fix(automation): 修回被我上一处改动挤坏的布局(5 张卡横排、无间距)
## 是我引入的回归
上一轮为了修「保存时存错作业」,我把各卡自己的 `<Form layout="inline">`
提到了外层统一包一层。但 `layout="inline"` **会把直接子元素排成一行** ——
于是 5 张 Card 横着挤在一起,还把原有的间距吃掉了(用户截图:一点间隔都没有)。
**同一个改动同时修了一个 bug、引入了一个 UI 回归** —— 说明我当时只验证了
"能存对作业",没看渲染结果。
## 修法
- 外层 Form 里加 **`<Space direction="vertical" size={16}>`** 纵向容器,
Card 恢复竖排、16px 间距
- 每张 Card `width: 100%`(不再由内容决定宽度)
- 字段行用 `<Space wrap size={16}>` 显式撑开,按钮组单独一段 `marginLeft: 16`
保留上一轮的关键修复:**仍然只有一层 Form**(`Form.Item` 靠 `[job.key, 字段]`
区分),保存按钮仍是显式 `onClick={() => saveJob(job)}` —— 不再依赖
"哪个 Form 的 onFinish 生效"。
并用注释把两个坑都写清了(各自包 Form 会存错作业;直接放 inline 子元素会挤成一行),
避免下次再踩。
## 验证
前端 build 通过。
|
2026-10-04 21:43:38 +08:00 |
|
yml2213
|
bb767bb84a
|
fix(automation): 保存配置时存错作业 —— 5 张卡共用一个 form 实例
## 现象(用户报的)
点「账号资料定时补全」的保存,提示却是**「积分到期补记 配置已保存」**。
## 根因:不是提示文案错,是**真的存错了作业**
`jobs.map()` 里**每张卡各包一个 `<Form form={forms}>`,但它们共用同一个
`Form.useForm()` 实例**。React 里多个 `<Form>` 绑同一实例时,
**`onFinish` 会被最后挂载的那个覆盖** —— 于是:
点第 1 张卡的「保存」→ 触发的是**最后一张卡**(积分到期补记)的 onFinish
→ saveJob(积分到期补记)
## 实际影响(不涉及数据损坏)
- `saveJob(job)` 读的是**那个 job 自己的**字段值 → 所以「积分到期补记」
只是被用它自己的值**重存了一遍**(无副作用)
- 而用户真正想保存的「账号资料定时补全」**根本没被保存** —— 静默失败
## 修法
把 Form **提到外面只包一层**(`Form.Item` 靠 `[job.key, 字段]` 天然区分各自的值),
保存按钮从 `htmlType="submit"` 改成显式的 `onClick={() => saveJob(job)}` ——
不再依赖"哪个 Form 的 onFinish 生效"这种隐含约定。
并在原处留了注释说明为什么**不能**各自包 Form(否则下一个人很可能又这么写)。
## 说明
前端这类"共用实例"的错误没有自动化测试兜住(仓库没有组件测试),
所以这次是靠用户点出来的。已在代码注释里写清原因,降低复发概率。
## 验证
前端 build 通过、lint 0 errors。
|
2026-10-04 21:40:46 +08:00 |
|
yml2213
|
55f899c615
|
feat(automation): 拆开「多久跑一次」与「同一账号多久重检」—— 调度频率终于可配
用户反馈:「单账号重检间隔(小时) 很别扭,直接设置多久跑一次、一次跑多少个更符合直觉」。
**对,而且比"别扭"更严重:调度频率以前根本改不了。**
## 问题:两个概念挤在一个字段里
`_run_one` 里原本写着:
job.next_run_at = finished + timedelta(seconds=JOB_INTERVAL_SECONDS) # 硬编码 300 秒
也就是不管界面上填什么,下一轮永远是「跑完 + 5 分钟」。
而界面那个「单账号重检间隔(小时)」对应的是 `interval_seconds` ——
它只被作业**自己的选行逻辑**使用("这行多久该重检"),**完全不参与调度**。
于是出现最坏的情况:**看起来能配调度,其实配的是一个不相干的参数**。
上一轮我还拿它算过吞吐("10 分钟一轮"),算错了 —— 已在调度器注释里纠正。
## 拆法
| 概念 | 字段 | 界面 |
| --- | --- | --- |
| **多久跑一次** | `run_interval_seconds`(迁移 0051,默认 300)| 「多久跑一次(分钟)」|
| 同一账号多久重检 | `interval_seconds`(不变)| 「同一账号多久重检(小时)」|
- `next_run_at = finished + run_interval_seconds`(有下限 30 秒,避免忙轮询)
- 接口:`GET` 返回新字段、`PATCH` 可改
- 前端:两个输入框分开,并把原来那段绕口的 tooltip
("它不决定调度频率 —— 调度周期见页面顶部说明")改掉 ——
那句话本身就是这个设计缺陷的自白
## 默认 300 秒
与改动前**实际行为一致** —— 不会因为这次迁移让已有作业突然变快或变慢。
想调快就自己填(比如「账号资料定时补全」填 5 分钟 + 每轮 400 条 ≈ 3 小时补完)。
## ⚠️ 顺带说明一个你截图里可能看到的现象
截图里「账号资料定时补全」显示的是 **0.5 小时 / 30 条** —— 那是我**最初的默认值**。
因为 `ensure_jobs` **只创建不更新**,我后面改成 10 分钟/400 **不会作用到已存在的行**。
这是刻意的(否则会覆盖运维的手工调整),但意味着**默认值改了要手动在界面上改**,
或者删掉那行让 `ensure_jobs` 重建。现在有了「多久跑一次」这个框,直接填就行。
## 测试(+3)
- 下一轮按**运行间隔**排,不是按「同一行重检间隔」(两个值刻意设成不同来钉住)
- 不填时默认 300 秒(保持旧行为)
- 过小的间隔被夹到下限
## 验证
TZ=Asia/Shanghai **639 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 21:30:13 +08:00 |
|
yml2213
|
b199428619
|
feat(account): 一键查询展示「角色绑定明细」—— 不只一个"已绑定"
用户看到绑定状态后问:「是不是在详情中增加详细的绑定信息啥的」。
`bind_info` 的返回里本来就有明细(`_parse_bind_info` 会解析出来),
之前只取了 `role_name` 一项。现在把运营真正要看的都带上:
- **角色名**(`role_name`)
- **区服 / 平台**(`area_name` / `plat_name`)
- 换绑冷却(`change_role_wait_time`)与可换绑时间(`can_change_time`)
实测(账号 61):
精英: 柿子洋音 / 微信 / 安卓(android)
电竞: 资跳 / 微信 / 苹果(iOS),可换绑 1788624000
面板新增「🎮 角色绑定明细」卡片,三个作用域并排;
冷却时间格式化成"无 / x 小时 / x 天 y 小时",
时间戳过 `formatUnixSeconds` 那套(`0`/空显示 `-`,不显示 1970)。
## 验证(本地)
TZ=Asia/Shanghai **615 passed / 6 skipped**;ruff 全绿;前端 build 通过。
|
2026-10-04 20:26:31 +08:00 |
|
yml2213
|
6b5271c983
|
feat(account): 一键查询补上「角色绑定状态」—— 以前从来没人查过,97% 显示"未知"
## 问题(用户:"这个做了吗?我看好像都是未知")
**只做了一半**:
- 列的拆分做了(S6):三列存在、写入收敛到 set_bind_status、迁移映射与运行时对齐
- 但**没有任何一处真的去查绑定状态**(grep -c bind_info douyu_runner_overview.py = 0)
那三列的值从来不是查来的,而是 S6 迁移时从旧的垃圾场列 bind_status
**能推的推、推不出的留 unknown**。只把明确陈述了绑定事实的旧值映射过来
(bind_confirmed → bound、bind_qr_generated → pending、game_not_bound → unbound),
其余一律 unknown —— 因为旧列被"查限兑商品""送礼物"这类**无关动作**覆盖过,
从无关动作推绑定状态就是造假。当时留 unknown 是对的,但结果是:
真实生产分布(15182 个账号)
精英 unknown 14683 (96.7%) 电竞 14730 (97.0%) 小店 15045 (99.1%)
## 修法:真的去查,用唯一写入者写回
一键查询新增三个绑定查询(接口都已存在,活动别名走后台配置):
bind_info / esports_bind_info / xpd_bind_info。
新增 derive_from_bind_info() 把返回推导成 4 态,**并做了实测校准**:
账号 列值(旧) is_bound_role 角色名 推导
51 unknown False (空) unbound
60 unknown True 唉清静的一生 bound
61 unknown True 柿子洋音 bound
62 unknown True FatalistIc bound
**4 个里 3 个其实已绑定,全被 unknown 盖住了。**
两个刻意的设计:
1. **推导不出来时返回 None,且不动原值** —— "没查到"和"没绑定"必须分开,
写 unbound 就是编造结论(小店未开通、返回里没有标志时实测就是这种情况)
2. **不产出 pending** —— 它描述"正在绑定/待确认"这种**进行中的动作**
(如刚生成过二维码),那是一次查询的副作用,不是查询能观测到的状态
3. need_bind_role **不是**"未绑定"的信号:实测**已绑定**的账号上它也是 1,
拿它当判据会把这 3 个已绑定的判成未绑
端到端实测(账号 51):
查询前: 精英=unknown 电竞=unknown 小店=unknown
查询后: 精英=unbound 电竞=unbound 小店=unknown ← 推不出来,正确保持原值
## 显示:把"没查过"和"查了推不出"分开
三处都改了(用户要求):全 unknown 时不再显示「精? 电? 店?」,
而是 **未查询**(点一键查询就有了)/ **未能判定**(斗鱼这次没给结论)。
## 验证(本地)
TZ=Asia/Shanghai **615 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
新增 9 条用例(含"need_bind_role 不是信号"的反向回归)。
|
2026-10-04 20:22:54 +08:00 |
|
yml2213
|
85a6186993
|
feat(account): 电竞手册到期时间落库与展示;迁移 0048 改名
## 背景(用户反馈)
「精英宝典和电竞手册已开通的话,应该增加时间展示,可以看到开通时间和过期时间」
- 精英宝典:开通时间来自事件;过期时间 `elite_expire_time` 本来就有
- 电竞手册:开通时间来自事件;**过期时间原来没有落库** ——
只在 `_refresh_esports_handbook` 的返回值里带着,界面上只能看到「已开通」
## 改动
- 新增 `accounts.esports_expire_time`(迁移 0049,与 `elite_expire_time` 对称),
刷新电竞手册时写回
- 详情页:**已开通才显示**「开通 x / 到期 x」两行小字;顺手删掉底部那段
重复的「开通时间」说明(已挪到每项旁边)
- 新增 `formatUnixSeconds()`:斗鱼的到期时间是**秒级时间戳**,
原 `formatTime()` 只收字符串;新函数对 `0`/空/负数一律显示 `-`
## 明确回答:历史开通记录**查不到**
实测官方兑换记录里**只有商品兑换、没有开通记录**(账号 61:精英 2 条都是套装)。
我们自己的 `elite_opened` / `esports_opened` 事件也只有上线后的。
所以历史上的开通时间拿不回来 —— 按用户说的「没有就算了」,
并把这点写进界面说明,别再让人去找。
## 测的时候抓出一个真 bug(又是 1970)
电竞到期时间的 `"0"`(斗鱼用它表示「没有到期时间」)被原样存成 `0`
→ 界面会显示 **1970/1/1**。已修(`0`/空 → `None`)并加回归用例 ——
这个坑在积分到期上踩过一次,这次差点重演。
## 迁移 0048 改名
`20261004_0048_overview_summary.py` → `20261004_0048_high_value_owned.py`:
上一轮把内容从「汇总」收敛成单一用途的 `high_value_owned`,文件名没跟着改。
revision id 不变、迁移状态不受影响;同时清了 `__pycache__`
(本仓库被过期 `.pyc` 咬过,症状是「改了没生效」)。
## 验证(本地)
`TZ=Asia/Shanghai` **608 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 20:17:05 +08:00 |
|
yml2213
|
f416397cc8
|
feat(account): 详情页重排 + 列表高价值套装列 + 兑换记录点击预览
## 详情页重排(4 张卡,消除重复)
用户反馈「设计太乱、数据不好看、布局也乱」——根因是**同一份数据在一屏出现两次**:
积分/鱼翅/兑换余额在状态条和资产区各一次,角色绑定在状态条和信息表各一次。
新布局:**账号信息**(身份+状态+归属,一张 Descriptions)→ **资产与统计**
(数字只出现一次,上面"现在有多少"、下面"历史累计")→ **高价值套装与兑换记录**
(从最顶上移到这里,它是补充信息,不该抢主视觉)→ **账号时间线**。
零散的 `flex + gap` 统计行改成 `Row/Col` 栅格,列宽对齐。
## 高价值套装:颜色按**价值**口径,别标反
用户澄清的业务语义与直觉相反:
- **没兑换过** = 还能用来绑定兑换 = **高价值**(绿色「可兑换」)
- 兑换过 1 个 → 价值降低;两个都兑换过 → 价值最低(橙色「已兑换」)
判定依据是**斗鱼官方兑换记录**(账号级)。⚠️ 不能用 `query_limited_goods` 的
`count`/`storeStatus` —— 那是**全局库存**(同一时刻不同账号看到的都一样),
用它判"这个账号有没有"必然给错结论。这个结论是推翻过一版才得到的:
第一版拿相隔两个月的两条快照对比,把**时间差异**当成了**账号差异**。
## 列表
- 新增「高价值套装」列:🟢两个都可兑换 / 🟠某套装已兑换 / ⚪未查询
(「未查询」必须与「没兑换」分开,否则会把未知当成高价值)
- 「兑换记录」列只显示官方口径,**点击弹窗**看全部(按作用域分组,
含商品/积分/真实兑换时间/当时用的昵称与平台),不用进详情页
## 三个作用域的记录展示
面板与预览都按 精英宝典 / 电竞手册 / 和平小店 分三块展示,
每条带**真实兑换时间**与"使用昵称"(排查时有用)。
## 顺带修
`exchange_records` 从数组改成按作用域分组的对象后,面板同步适配;
`formatTime` 对 `0` 显示 `-`(斗鱼用 0 表示"没有这个时间",
直接格式化会显示成 1970/1/1)。
|
2026-10-04 20:03:52 +08:00 |
|
yml2213
|
aea05952af
|
refactor(account): 一键查询不该重复展示账号字段 —— 改成刷新它们,只留账号表里没有的
用户反馈:「一键查询也太重复了,直接更新账号信息不行吗?为啥很多都要再展示一次?
那建立账号信息表有啥用」——**说得对,是我设计错了**。
## 问题
面板把鱼翅 / 积分 / 钱包余额 / 精英宝典 / 电竞手册又原样列了一遍,
而这些账号表(`accounts`)本来就有对应列,账号详情也在显示同一批值 ——
同一屏出现两份,既冗余又让人分不清哪份是准的。
## 改法:查询 = 刷新,不是另开副本
| 内容 | 处理 |
| --- | --- |
| 鱼翅 / 积分 / 交易余额 / 开通状态 | 写回**账号表**(`_refresh_*` 本来就在写),由账号详情已有界面展示 —— **面板不再重复** |
| **积分到期时间** | 有 `accounts.points_expire_at` 列,但**查询漏了没写、界面也没显示** → 现在写回,并加到账号信息区展示 |
| **高价值套装是否已获得** | 账号表没有 → **只有这个该单独展示** |
| **官方兑换记录** | 账号表没有 → 单独展示 |
面板因此瘦身成只含「🎯 高价值套装」+「兑换记录」(+ 错误提示 + 查询时间),
标题也改成「高价值套装与兑换记录(一键查询)」,不再叫"账号信息"。
## 修了一个连带 bug
积分到期时间一直没被查询写回账号(只在快照里躺着),界面也没展示 ——
现在 `_parse_expire_at` 后写入 `accounts.points_expire_at`,
且斗鱼返回 `0` 时写 `None` 而不是 `0`(否则又显示 1970/1/1)。
## 测试(+3,共 41)
`TestOverviewRefreshesAccountFields`:资产/开通状态确实写回账号列、
**积分到期时间写回**、`willExpireTime="0"` 时清成 None 而非 0。
## 验证
`TZ=Asia/Shanghai` **593 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 19:22:34 +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
|
3866b49087
|
fix(account): 一键查询按反馈收敛 —— 只在账号管理展示、结果落账号、修 1970 显示
按用户反馈改了四处。
## ① 结果只在账号管理展示(不再出现在精英宝典/电竞手册页)
移除了工作台页的结果弹窗与导入。**类型集合保留**(三个工作台都有)——
因为账号页批量建任务要用 `handbook_scope` 过 `DOUYU_HANDBOOK_TASK_TYPES` 校验,
去掉会被「该任务不属于当前工作台」拦掉;去掉的只是**快捷操作按钮**。
## ② 不再每次弹出
原来任务跑完就自动弹面板,而且任务会被反复轮询到。现在整个弹窗机制去掉了:
结果落在账号上,打开账号就能看。
## ③ 结果写进账号(新增迁移 0047)
新增 `accounts.overview_snapshot`(JSON)+ `overview_queried_at`,覆盖式只留最新一份。
详情页直接读账号上的快照,不再轮询任务。
**为什么放账号而不是只留任务里**:任务会被保留策略清掉(长保留档 90 天),
而「这个账号现在什么情况」是**账号的信息**,不该依赖一条日志的寿命。
## ④ 修 1970/1/1 08:00:00(真 bug)
斗鱼用 `0` / `"0"` / 空串表示「没有这个时间」。原实现直接透传,
前端 `new Date(0)` → **1970/1/1 08:00:00** —— 运营会以为"到期时间是 1970 年"。
两端都修了:
- 后端新增 `_ts()`,把 `0`/`"0"`/空/负数 归成 `None`(积分到期、宝典到期、
电竞到期、商品活动时间四处都过一遍)
- 前端 `formatTime` 也把 `0`/负数显示成 `-`(上游再出错也不至于显示 1970)
## 测试(+14 条,共 36)
- `TestTimestampNormalisation`:`0`/`"0"`/空/负数 → `None`;真实时间戳透传
- `TestSnapshotIsWrittenToAccount`:快照与时间落到账号上、
**`willExpireTime="0"` 不能落库成 0**(1970 的回归用例)、覆盖式不追加
## 验证
`TZ=Asia/Shanghai` **588 passed / 6 skipped**;`TZ=UTC` 41 passed;
ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 19:12:51 +08:00 |
|
yml2213
|
d285196287
|
feat(account): 账号「一键查询」(异步 + 批量)—— 含一次重要的判定修正
按 `docs/设计-账号一键查询.md` 实现。异步任务、支持批量(后端每账号一条任务)。
## 功能
- 新任务类型 `query_account_overview`:**只读**聚合查询,不改账号状态、不发事件
(纯查询不是「事实」,按既有设计例行查询不进时间线,否则会刷屏)
- 一次拉全:鱼翅 / 钱包兑换余额 / 积分(含到期)/ 精英宝典 / 电竞手册 /
**官方兑换记录** / 限兑商品,并**重点标出「兔兔白日梦」「狂战萌兔」是否已获得**
- **单项失败不整体失败**:账号没绑电竞手册是常态,失败只写进 `errors` 并在 UI
**如实展示** —— 避免「没查到」被读成「没有」
- 批量:`create_douyu_planned_tasks` 本来就是每账号一条任务,前端加「批量一键查询」
- 前端:详情页一键查询卡片 + 结果面板(两个套装单独高亮);任务页结果自动弹面板;
三个工作台的快捷操作/类型集合都加了
## ⚠️ 修正:判定「是否已获得」用错了依据
设计文档第一版说「`storeStatus == 0` → 已拥有」,依据是
「账号 558(兑换过)count=0,账号 15407(没兑换)count=96982」。
**这是错的。** 那两条记录分别取自 **2026-08-01** 和 **2026-10-04**,相隔两个月 ——
我把**时间差异**当成了**账号差异**。
**同一时刻**同时查 558 / 607(都兑换过)和 15507(没有),
商品项的**所有字段完全一致**,`preExchange.isExists` 也都是 0;
`count` 的取值分布是 0/98746/103350/109832…,**随时间变化 = 全局库存**。
用它判「这个账号有没有」必然给错结论 —— 而对高价值资产给错结论是本功能最不能接受的。
**正确依据:`exchange_records(manual_id)`**(斗鱼官方的**账号级**兑换记录)。
真实斗鱼实测:
| 账号 | 官方兑换记录 | 判定 |
| --- | --- | --- |
| 558 | 破片手榴弹-兔兔白日梦 | **已获得 ✅** |
| 607 | 破片手榴弹-兔兔白日梦 | **已获得 ✅** |
| 15507 | (0 条) | 未获得 ✅ |
同时刻三者看到的兔兔白日梦库存**都是 96956**(全局),兑换记录却区分得干干净净。
顺带发现:同一件东西有多个商品名(`套装-兔兔白日梦`、`破片手榴弹-兔兔白日梦`),
所以按**核心词子串**匹配整个名字家族。
商品列表的 `score`/`count`/活动时间仍展示,但明确是**全局**信息
(「现在还能不能换、要多少分」),与「该账号是否已获得」分开。
## 测试
`tests/test_account_overview.py` 22 条,含**回归用例**:
`test_global_stock_never_implies_owned`(全局售罄不能推出账号拥有)——
正是第一版踩的坑。另有失败隔离、只读性(不发事件/不改状态)、
批量每账号一条任务、`missing` 不被静默丢弃。
## 验证
- `TZ=Asia/Shanghai` **574 passed / 6 skipped**;`TZ=UTC` 27 passed
- ruff 全绿;前端 build 通过、lint 0 errors
- **真实斗鱼端到端**:三个账号各跑一次真查询,8 项全部成功(`errors: []`),
已获得/未获得判定全部正确
|
2026-10-04 19:07:35 +08:00 |
|
yml2213
|
eec42aa8a7
|
feat(check): 账号检测新增「按账号选择」—— 旧的手动输入与逻辑完整保留
按反馈是**增加**而不是**替换**:旧路径一行未动,新路径另起一条。
## 两套来源并存
| | 手动输入(旧,默认) | 选择账号(新) |
| --- | --- | --- |
| 来源 | 粘贴文本 | 库里账号的凭据(不用人工粘贴) |
| 条目 `account_id` | **无** | 有 |
| 结果落账号时间线 | **否**(保持旧行为) | 是,新事件类型 `account_check` |
| 批次 / 并发 / 重试 / 代理 / 导出 zip | 完全不变 | 完全不变 |
**旧路径的「不发事件」是刻意的**:粘贴进来的账号没有归属,凭空造一条没有账号的
事件只是污染时间线。`parse_account_check_lines`、`export_line`、zip 分组全部未动,
有用例专门钉住这一点。
## 「选择账号」支持筛选(按反馈补充)
不是给一个扁平下拉,而是先按条件缩小范围再决定选法:
- 筛选:**标签 / 生命周期状态 / CK 健康**(复用账号列表同一套 `_filter_accounts_query`,
两处口径不会不一致)
- 选法:手动多选(选项随筛选刷新)**或**「按筛选全选」(含列表里没显示出来的)
服务端支持 `select_all_matching + 各筛选条件`,与账号列表批量操作的字段形状一致。
可见性沿用账号域口径:客服只能检测自己名下的账号(越权 id 直接 403)。
## 结果表加账号回链
「选择账号」模式下条目带 `account_id`,账号列变成可点链接 → 直接跳到账号详情,
结果也在那里的时间线上。
## 排查中修掉两个 bug(都不是测试发现的)
**① `registry.create` 漏传 `account_id`**
它构造 `AccountCheckItemState` 时没把 `account_id` 从输入复制过去。
后果:批次照跑、结果照显示、zip 照导出,**事件永远不发且没有任何报错**。
是「跑一次真实检测再查事件表」才发现的(查出来 0 条)。已补,并加了用例。
**② 终态判断写成「键存在」而不是「非空」**
`_run_one` 置 `running` 时会显式传 `finished_at=None`,而判断写的是
`"finished_at" in updates` → **中间态也发事件**,于是时间线上记的是
`账号检查:running`;真正的终态再发时撞同一个幂等键被**静默丢弃** ——
结果是「只有一条,但记错了」,比没有更糟。
改成判非空。此处暴露了一个**假绿的用例**:我原来的「中间态不发事件」用例
没传 `finished_at=None`(与真实调用不一致),所以放过了这个 bug;
而且它还缺 `session.commit()`,在 MySQL REPEATABLE READ 下根本看不到
另一个会话刚提交的行 —— 双重假绿。两处都修了,并做了反向验证
(改回旧判断 → 用例立刻红)。
## 验证
- `TZ=Asia/Shanghai`:**520 passed / 6 skipped**(新增 17 条检测模式用例)
- `TZ=UTC`(检测 + 迁移)20 passed;ruff 全绿;前端 build 通过、lint 0 errors
- **真实端到端**:用真实库账号起批次 → 检测跑完(斗鱼返回「账号已认证」)→
事件确实落库;清掉脏数据后用**与 `_run_one` 完全相同的调用序列**复验:
两次 `_set_item` 只产生一条事件,且 title 是
`账号检测:账号已认证`、`status=account_verified`(不再记成 running)
注:验证时真实触发了几次斗鱼登录检测(开发库那两个账号),结果都是「已认证」。
|
2026-10-04 17:34:58 +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
|
d3f5198227
|
style(ui): 账号列表列宽优化 —— 账号列不再吃掉全部剩余空间
截图里的毛病很具体:**「账号」列被撑到 1000+px**,中间空出一大块,
而「状态」列只有 150px,把「检测 2026-10-01 19:07:25」挤成了两行。
根因是我前面把 ID 合并进「账号」列时,顺手把它的 `width` 去掉了 ——
于是它成了唯一的弹性列,把剩余空间全吃了。
修法:
- **账号列定宽 200 + `ellipsis`**,用户名过长就省略(`title` 保留全文)
- **CK 时间改用 `formatTimeShort`**(`10-01 19:07`),状态列加宽到 180,
一句话能放下,不再折行
- 其余列宽按内容重新配比:绑定 190→150、积分 110→120、标签 170→190、操作 80→90(居中)
- **`tableLayout="fixed"`**:列宽按比例分配、剩余空间**均匀摊开**,
而不是像 auto 布局那样全塞给内容最宽的那一列 —— 这才不会再出现「一列独大」
顺带清掉因此失效的 `formatTime` 导入(导入时间列上轮已删,它没有其它调用点)。
验证:pytest 500 passed / 6 skipped;ruff 全绿;build 通过、lint 0 errors。
|
2026-10-04 17:19:09 +08:00 |
|
yml2213
|
a21e4a7e2a
|
feat(ui): 账号列表恢复「标签」列(含就地编辑)
按反馈把标签列加回来。这列不只是展示 —— **有 `account:tag` 权限时就是一个
内联编辑框**:选已有标签或直接输入新标签即保存,无权限时退化为只读 Tag。
因此一并恢复它所依赖的东西:
- `handleSetTag`(单条改标签)
- `tagColorMap` + `TAG_COLORS`(只读态的标签配色)
- `useMemo` 导入
所以现在改标签有三个入口,各管一段场景:
| 入口 | 场景 |
| --- | --- |
| 列表「标签」列 | 单条快速改 |
| 列表「批量打标签」 | 一批一起改 |
| 详情页状态卡 | 在账号全貌下改(S6) |
列表列最终为:**账号 / 状态 / 绑定 / 积分·鱼翅 / 标签 /(凭据三列,开关控制)/ 操作**
验证:pytest 500 passed / 6 skipped;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 17:12:13 +08:00 |
|
yml2213
|
e24af9ad75
|
fix(ui): 账号列表按反馈调整 —— 保留批量操作与凭据开关,去掉「分配给」列
上一个提交把四项一起砍了,其中两项理解反了,按反馈改回来:
## 恢复(撤销上一提交的 ① ④)
- **「显示凭据」开关 + 密码/邮箱/邮箱密码三列**
- **「批量打标签」「批量删除」按钮**,以及随之必需的**整套勾选机制**:
`rowSelection`、`selectedRowKeys`、`selectedAllMatching`、
「已选择 N 个 / 选择当前筛选下全部」提示条、批量设置标签弹窗、
`handleBatchTag` / `handleBatchDelete`
## 新增去掉:**「分配给」列**
分配统一走「分配管理」页(`/accounts/assignments`,页面一直在),
列表里的内联分配下拉是重复入口。随之清掉只服务于它的
`handleAssign` / `loadUsers` / `users` 状态 / `userApi` 与 `UserInfo` 导入。
## 保持删除(反馈里未提)
**「标签」列**与**「导入时间」列**仍不恢复。注意因此产生的一处连带:
标签列里的**内联编辑框**也没了 —— 现在改标签的入口是
**批量打标签**(列表)+ **详情页状态卡**(S6 已做),够用。
## 踩到并修掉
用「区间删除」砍 `handleAssign`→`handleDelete` 时,把夹在中间的
**`handleBatchTag` 一起删了**(后来 `handleBatchTag` 引用未定义才暴露)。
已补回。教训:按区间删代码前要先确认区间里没有别的东西 ——
这类错误 `tsc` 只在**引用处**报,很容易看漏。
## 最终形态
- 列:账号 / 状态 / 绑定 / 积分·鱼翅 /(凭据三列,开关控制)/ 操作
- 工具栏:筛选(标签 · CK · 绑定 · 搜索)+ 显示凭据 + 批量打标签 + 批量删除 + 批量导入
- 618 行;`TAG_COLORS` / `tagColorMap` / `handleSetTag`(单条改标签)
等确认无用的死代码已清
## 验证
- `TZ=Asia/Shanghai`:500 passed / 6 skipped;ruff 全绿
- 前端 build 通过、`npm run lint` **0 errors**
|
2026-10-04 17:10:17 +08:00 |
|
yml2213
|
cb87a844b3
|
refactor(ui): 账号列表按「只做管理」重做 —— 砍掉凭据/标签/时间列与批量操作
承接上一个提交。按确认的结果,四类老壳子残留一并移除:
## ① 「显示凭据」开关 + 密码 / 邮箱 / 邮箱密码 三列
这是老「卖号」视图的残留。移掉后列表不再有任何凭据字段,
`include_sensitive` 参数不再从列表请求里传(后端能力保留,需要时仍可显式请求)。
⚠️ 副作用:**界面上暂时没有地方能看到账号密码了**。当前重登全自动,
不需要人工读凭据;但如果你偶尔要抄出来核对,告诉我,我在**详情页**加一个
带权限校验 + 写审计的「查看凭据」,而不是把三列塞回列表。
## ② 「标签」列
标签仍可**筛选**(顶部下拉),但不再占一列。设置标签的入口统一到**详情页**
(S6 已在状态卡里做了就地编辑),列表里那个内联编辑框随之删除 ——
所以 `handleSetTag` / `tagColorMap` / `TAG_COLORS` 一并清掉。
## ③ 「导入时间」列
去掉(详情页仍有完整的导入时间与状态变更时间)。
## ④ 「批量打标签」「批量删除」按钮
随之移除**整套勾选机制**:`rowSelection`、`selectedRowKeys`、
`selectedAllMatching`、「已选择 N 个 / 选择当前筛选下全部」提示条、
批量设置标签弹窗、以及两个 handler。**单条删除保留**(操作列,受 `account:delete` 管)。
批量接口在后端保留不动(S4 已给它们接了逐账号事件,别浪费)。
## 结果
- 列表列:**账号 / 状态 / 绑定 / 积分·鱼翅 / 分配给 / 操作**
- 工具栏:筛选(标签 / CK / 绑定 / 搜索)+ 批量导入
- 文件 601 → 478 行;`Alert` / `TagOutlined` / `Eye*` / `App` / `modal` 等
随之失效的导入与变量全部清掉(靠 `tsc --noUnusedLocals` 逐个抓出来)
## 验证
- `TZ=Asia/Shanghai`:500 passed / 6 skipped;ruff 全绿
- 前端 build 通过、`npm run lint` **0 errors**
- 逐个删除后都用 `tsc` 反查死代码,没有留下未使用变量
|
2026-10-04 17:01:34 +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
|
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
|
253571730a
|
feat(account): 补上改备注接口,remark_changed 事件从此有发射点
`remark_changed` 此前是「定义了事件类型、但压根没有改备注的接口」,于是永远不发。
按 S4 的原则(留着一个永远不发的类型比删掉更糟 —— 看代码的人会以为它有数据),
补接口而不是删类型。
## 接口
`PUT /api/accounts/{id}/remark`,权限点 `account:remark`(授予 super_admin /
operation / support,与 `account:tag` 一致)。
- 与改标签一样走 `record_account_action()`:**审计 + 账号时间线一次写完**,
审计带 `account_id`,事件 detail 带 `{from, to}`;
- **只做单个、不做批量**:备注是自由文本,给一堆账号写同一句备注没有实际意义
(标签是短枚举才有批量);
- 内容 trim 后截断到 256(与 `accounts.remark` 列宽一致),清空也记一条事件;
- 备注没变化时直接返回、**不写事件**:否则时间线会被「点开保存了一下但没改」刷满。
## 权限点新增前的安全检查
新权限点会被 `custom_permissions` 覆盖,所以先查了库:
`admin` / `kf1_test` 的 `custom_permissions` 是 **JSON null**(不是 SQL NULL),
Python 侧取出来是 `None` → `get_user_permissions` 走角色默认。
也就是说**没有任何用户在用自定义权限列表**,新增权限点不会把谁的能力锁掉。
(这一点很容易看错:`IS NULL` 判出来是「有自定义权限」,`JSON_LENGTH` 还是 1。)
## 前端
- 详情页「备注」一行改为可编辑(铅笔 → 输入框 → 保存/取消,回车即存);
- `EVENT_STYLE` 补 `remark_changed`,时间线里能看到备注的每次变更。
## 一个自己踩到又修掉的坑
第一版把两个 `useState` 写在了 `if (!accountId) return <Empty/>` **之后** ——
正是本次会话刚在 LoginTasksPage 修掉的那类 hooks 顺序违规(`react-hooks/rules-of-hooks`
直接报错)。已上移到早期 return 之前的 state 区块。lint 抓到,构建不报,属于
「能跑但迟早炸」的那类问题。
## 验证
- `TZ=Asia/Shanghai`:**426 passed / 6 skipped**(S4 后 420 → 新增 6 条)
- `TZ=UTC`(相关文件)77 passed;ruff check/format 全绿
- 前端 `tsc -b && vite build` 通过、`npm run lint` 0 errors
- 新增用例:写入并带 from/to 事件与审计关联、未变化不写事件、清空也记一条、
trim + 截断到 256、账号不存在 404、**新权限点确实在三个角色的默认权限里**
(防止「加了接口但没人能用」)
|
2026-10-04 15:21:48 +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
|
fbab805610
|
fix(ui): 修掉 LoginTasksPage 的 hooks 顺序告警(eslint 由红转绿)
`setTaskSummary` 在 useEffect 里被使用(约 259 行),但它的 useState 声明在
约 420 行 —— eslint `react-hooks/immutability` 判为「访问了尚未声明的变量」。
这是 main 上就存在的历史问题,会让 `npm run lint` 失败。
**运行时其实不会踩 TDZ**(useEffect 在渲染后执行、且内部是 async,等到真正调用
setter 时声明早已执行完),但**声明与使用相隔 160 行**本身就是隐患:任何一次
把调用改成同步、或把 effect 往上移的重构都会真的炸。
处理:把 useState 声明移到任务相关 state 区块(taskPage / taskStatus / taskTotal
旁边)—— 既满足规则,也让这组状态聚在一起。先确认过**组件在声明之前没有任何
早期 return**,所以上移不会改变 hooks 的调用顺序。
验证:
- `npx eslint src/pages/LoginTasksPage.tsx` → 0 errors(原本 1 error)
- `npm run lint` → **exit 0**(29 个 warning 是既有的 react-refresh 提示,未动)
- `npm run build`(tsc -b && vite build)通过
|
2026-10-04 14:52:05 +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 |
|
yml2213
|
00125fbfc4
|
refactor(account): S2 读写一次切换 —— CK/健康/重登彻底搬到账号,登录记录退化为历史
S1 加好了列,本提交把**读与写一起切**过去。这是设计里「同一份数据的读与写必须
在同一个提交里切」的要求:先切读或先切写都会出现「写账号、读记录」的中间态,
跑起来不报错,只是默默用错数据。
## 核心改动
**判定「谁有 CK」不再借道登录记录**
- `cookie_holding_filter()` / `cookie_operable_filter()` / `cookie_holding_account_ids_query()`
/ `visible_douyu_task_accounts()` 全部改为作用于 `Account`;
- `cookie_operable_filter()` 的「在途排除」仍查 `login_tasks`(那是**某次尝试**的属性),
用 `NOT EXISTS` 子查询 + 迁移 0042 新增的 `(account_id, attempt_status)` 索引;
- 删除死代码 `latest_cookie_login_task()`;`latest_cookie()` 改为直接读账号。
**CK 写入**
- `_write_login_success()` 写 `Account.cookie / cookie_updated_at / cookie_source`;
- `_mark_attempt_failed()` 改看**账号**有没有 CK(决定是否沿用旧的失败语义);
- 检测模式的 `task.cookie = ""` **不再清空账号的 CK**(见下「行为变更」)。
**自动化作业按账号调度**
- `ck_health`:`_select_tasks` → `_select_accounts`,`db.query(Account)`;新增
`count_remaining()`(刻意不带 LIMIT,否则积压数恒等于批次上限);
- `ck_relogin`:同样按账号选行;结算改为「账号 + 本轮尝试记录」,
退避进度写账号;`settle_fresh_results(batch_id)` 按 `attempt_batch_id` 配对,
`run_ck_relogin` 把 **account_ids** 交给 runner;
- 顺带让**没有任何登录历史的账号**也能被巡检/重登(以前它们永远进不了队列)。
**登录批次**
- `LoginBatchRunner` 去掉 `relogin_task_ids`,重登与登录统一走 `account_ids`;
- 批次行复用逻辑重写:只保证「同一账号不并发两行」,不再关心「哪一行持有 CK」。
失败行复用同时看 `attempt_status`(账号有 CK 时 `_mark_attempt_failed` 刻意不动
`status`,只看 status 会永远命不中 → 每次失败刷一行)。
**删除护栏语义变更(用户可感知)**
- 去掉「持有 CK 的记录不可删」——CK 在账号上,删历史不再影响登录态,历史终于可清理;
- **保留**「在途尝试不可删」(批次线程正拿着那条记录)。
## 行为变更(需评审)
1. **账号检测不再清空 CK**。旧代码在 check 模式写 `task.cookie = ""`,而那时记录的
cookie 就是账号唯一凭证 —— 一次「批量检测」会把账号登录态删掉。解耦后
「检测账号凭据」与「CK 是否有效」本就是两件事(后者归 `ck_health` 判),
继续清空等于凭空制造不可逆的数据丢失(重登当前成功率极低)。
2. **Cookie 页的记录单位从「登录记录」变成「账号」**,所有接口的 id 入参/出参
变成 `account_id`;`DELETE /cookies/{id}` 清的是该账号的 CK,**必写审计**。
3. 重复检测从「同一账号多行持 CK」改为「**同一个 CK 出现在多个账号上**」——
后者才是真风险(两个账号共用一个登录态)。用 sha256 指纹分组:
CK 是 AES-GCM + 随机 nonce 加密,**同一明文每次密文都不同**,比密文永远比不出重复。
## 修掉一个真实并发缺陷
`_write_login_success` 原先被传入 `self.db`(runner 主会话),而它是在**工作线程**里
用 `worker_db` 执行的。`emit()` 会往 `account_events` 插一行,其 `account_id` 外键对
`accounts` 那一行加**共享锁**;这把锁加在另一个会话上,紧接着本会话的
`UPDATE accounts SET cookie=...` 就被自己人挡住 —— 实测 **50 秒后
`Lock wait timeout exceeded`,登录明明成功却写不回 CK**(改造前 UPDATE 打在
`login_tasks` 上所以没撞上)。改为传 `worker_db`:事件本就该和状态变更同事务提交,
顺带修掉跨线程共用一个 Session 的隐患。
## 验证
- 全量 pytest **368 passed / 6 skipped**;迁移链 3 passed;
- 前端 `tsc -b && vite build` 通过;
- `ruff check` / `ruff format --check` 全绿(顺带修掉 main 上遗留的 F821);
- 新增/改写的关键回归:**删掉全部登录历史后账号的 CK 与健康状态仍在**、
**无登录历史的账号也能进巡检队列**、结算不碰 CK、删除护栏改为在途语义。
仍未做(设计里排期的后续阶段):`account_lifecycle` 状态收口(S3)、事件发射点补齐(S4)、
业务事实直发事件(S5)、账号页重构(S6)。注意 `status` 目前只有模型默认值与迁移回填,
所以 `disabled/archived` 还没有任何写入方——S3 落地时要把自动化选行的状态过滤一起补上。
|
2026-10-04 14:10:13 +08:00 |
|
yml2213
|
834bfdd1d0
|
feat(account-events): 账号详情页与账号事件历史全链路支持
1. 新增账号事件数据表 AccountEvent 与迁移版本 20261002_0040_account_events,支持审计 Cookie 变更、自动重登、健康检查及任务执行事件;
2. 后端新增 account_events_service 并扩展 /api/accounts 路由,支持事件查询、过滤与上报;
3. 前端新增 AccountDetailPage 账号详情页组件,支持账号元信息展示、事件历史瀑布流与快捷重登操作;
4. 修复 CK 重登与自动化定时任务中事件关联逻辑,补充 test_account_events 单元测试;
5. 新增生产服务器高负载排查分析报告及相关设计文档。
|
2026-10-03 12:58:58 +08:00 |
|
yml2213
|
8310a9115e
|
fix(login): 删除任务不再丢 CK —— 只删不持有 CK 的记录【数据安全】
事故:斗鱼账号的 CK **只存在 `login_tasks.cookie`**(`accounts` 表没有
cookie 列),删掉记录即永久丢失登录态。而列表显示的「状态」是最近一次尝试
(`attempt_status`),于是一条**显示「失败」但实际仍持有有效 CK** 的记录
(status=success + cookie + 上次尝试失败)会被「清空失败」当成垃圾删掉。
修复:
- 批量删除只删空 cookie 的记录,持有 CK 的一律跳过,并在响应里报告跳过数;
- 单条删除遇到持有 CK 的记录直接 409,提示先处理该账号的 CK;
- 前端确认弹窗写明「持有 CK 的记录会跳过」。
顺带修统计口径:分页后 success/failed 只统计当前页,却和「共 N 个任务」
并排显示(曾出现「共 8508,成功 48」)。改走 /tasks/summary 的全局统计。
回归测试补 5 条,专门盯「显示失败但持有 CK」这个事故场景。
|
2026-10-03 10:50:23 +08:00 |
|
yml2213
|
1722b0a81b
|
fix(login): 翻页请求不再被轮询吞掉
loadTasks 原来用「有请求在跑就直接返回」防重入,加了分页后这会把用户点
翻页/改筛选时发出的请求一并丢掉:页面停在上一页,要等下一次轮询(3~15s)
才会更新。改为请求序号「最新请求获胜」:过期响应直接丢弃,不再丢请求。
|
2026-10-03 10:44:52 +08:00 |
|
yml2213
|
7293d758f1
|
fix(login): 勾选跨页保留,并去掉 loadTasks 的多余依赖
两个自己引入的问题:
- loadTasks 依赖 selectedAllMatching 会让每次翻页重建回调,进而触发
上面的 effect 重新加载标签(多余请求);
- 翻页静默清空勾选会让人困惑(「选了 10 个翻页就没了」),改为
preserveSelectedRowKeys 跨页保留,删除数量在按钮上明确显示。
|
2026-10-03 10:39:53 +08:00 |
|
yml2213
|
a0db26def3
|
feat(login): 任务列表支持分页、跨页全选与一键清空
任务量早就过了单页能装下的规模(库里 1.4 万条,单批就 177 条),但接口
硬编码 limit(200)、前端 pagination={false},于是「全选」其实只能选到前
200 条,失败任务清不干净。
- 接口加 page/page_size/status:不传 page 时仍返回裸数组(Cookie 页等
老调用方依赖这个契约,不能改);传了才返回 {items,total},total 供跨页全选。
- 删除支持 all_matching:按当前筛选条件删全部,不必先翻完所有页去勾。
- 前端:分页控件(20/50/100/200/500,记住选择)、状态筛选、表头
「选择全部 N 条(含其他页)」、「清空失败」一键操作。
两条护栏:
- **pending/running 一律不删** —— 删掉会让批次线程找不到记录;要停批次
走 /stop/{batch_id}。
- 不给任何条件就拒绝删除,绝不默认全删。
顺带:带 join 的 query 不能 bulk delete(SQLAlchemy 会拒绝),改成先查
id 再按主键删。
|
2026-10-03 10:37:52 +08:00 |
|
yml2213
|
d6c6113112
|
fix(automation): 被重启打断的轮次记为 interrupted,不再混进 failed
部署重建容器会打断正在跑的作业,那轮永远写不回结果,由启动时的
cleanup_orphan_runs 收尾。之前收尾成 failed,于是页面上一片红 —— 但这并不是
作业本身的失败:账号侧的 CK 一个没丢(旧 Cookie 已保留),被中断的账号也会按
退避自动重试。混在 failed 里会掩盖真正的失败(如 SQL 语法错误)。
改为独立状态 interrupted,前端用中性灰显示「被中断」,与红色的「失败」区分开。
|
2026-10-02 22:08:18 +08:00 |
|
yml2213
|
df62898d22
|
fix(automation): 调度心跳与作业周期解耦,修掉系统性跳轮
线上现象:同一页面上「CK 自动巡检」的最近运行是 21:45:34,而「下次运行」显示
21:50:34 且已经过去了 —— 巡检明明只要 19 秒,却总是等在那里。
根因是把心跳周期直接当成了作业周期:TICK_SECONDS=300 既是「循环多久醒一次」,
又被写进 next_run_at = 作业结束 + 300s。两者同频会系统性跳轮:
心跳 t=0 ck_health 19 秒跑完 → next = 5:19
心跳 t=5:00 差 19 秒没到点 → 整轮空转,什么都不做
心跳 t=10:00 才真正跑到
于是作业实际是每 10 分钟一轮,而不是设计的 5 分钟;只要作业耗时不是刚好 0,
就会稳定地"差一点点"被跳过。两个作业共享同一个心跳时,还会互相把对方的到期
时刻推后。
改动:
- 拆成两个常量:TICK_SECONDS=30(心跳,只做「有没有到点的作业」检查,几乎不
耗资源)、JOB_INTERVAL_SECONDS=300(作业周期,跑完一轮后隔多久再跑)。
- next_run_at 改用 JOB_INTERVAL_SECONDS。
- /api/automation/jobs 的 scheduler 同时暴露两个值;启动日志一并打印。
- 页面:「单账号重检间隔」加 tooltip 说明它不决定调度频率(它只控制同一行多久
重检/重试),避免和调度周期混淆。
回归测试(3 条):心跳必须比作业周期密至少 4 倍;周期未到时连跳多次心跳都不跑;
把 next_run_at 拨到过去后下一次心跳必须跑到。
全量 304 通过。
|
2026-10-02 21:57:41 +08:00 |
|
yml2213
|
847ed4539a
|
fix(ui): 运行历史的假耗时、以及「下次运行」显示成过去时间
两处显示问题(来自线上页面核对):
1. 耗时列:被重启打断的轮次,finished_at 是 cleanup_orphan_runs 在重启后补上的,
于是「耗时」把停机时间也算进去了(线上出现过 235s / 687s 这种假数字)。
改为只有真正扫描过数据的轮次才计算耗时,其余显示 '-' —— 一轮就报错的
(比如历史上的 SQL 语法错误)同理。
2. 下次运行:两个作业串行跑在同一个调度线程里,前一个耗时长时后一个会排队,
next_run_at 因此会停在「已经过了」的时刻;直接显示一个过去时间看起来像故障。
改为:未启用 → 「未启用」;已过期 → 「排队中(<时间> 到期,等待前序作业让出
线程)」,未到期照常显示。
|
2026-10-02 21:51:37 +08:00 |
|
yml2213
|
ed6e186655
|
fix(ui,test): 说明巡检与重登的联动关系;修掉一条按分钟偶发失败的测试
看图时发现两个问题:
一、页面上那段说明是 P1a 时代的文案,已经过时且误导:「重登仍由人工在
「CK 检测与重登」页面操作」——P1b 上线后自动重登已存在。两个作业其实是联动的
上下游:巡检把账号标成 invalid,「CK 自动重登」的候选条件正是这个字段,所以
巡检一发现失效、重登作业下一周期就会接手。刻意分成两个作业是为了止损粒度
(巡检只读零风险;重登会真实登录,出问题可单独关掉)。据此改写说明文案,并在
两张卡片上标出「下游:CK 自动重登」/「上游:巡检标记为失效且有 CK 的账号」。
二、jobs 接口新增 pending_relogin:当前有多少条正等着自动重登。页面靠它把
上下游关系显性化 —— 没有这个数字,两个开关看起来像互不相干。计数走重登作业
自己的候选查询,保证「页面显示的待办」和「作业实际会捞的」是同一口径。
顺带修掉 tests/test_douyu_email_code_pick.py 里一条既有缺陷:它用
max(0, now.minute - 30) 构造「30 分钟前的邮件」,分钟数小于 30 时会退化成当前
整点(21:00:12 → "今天 21:00"),只比 now 早十几秒,于是断言随「当前几点」偶发
失败 —— 每小时前 30 分钟必红(已确认在我本次所有改动之前的提交上同样失败,
不是本次引入)。改成真实的时间减法,跨天时用「昨天」。
全量 298 通过。提交时另一路 SQLite→MySQL 的测试改造仍在同一工作区进行,
本次只暂存本功能相关文件。
|
2026-10-02 21:01:17 +08:00 |
|
yml2213
|
8fc8e623b9
|
feat(automation): CK 自动重登(P1b)+ 失败分型与待人工队列
P1a 的巡检只产出「谁失效了」的清单,不兑现修复:1,356 个失效账号仍要人工逐个
选集→点重登→过滑块。线上实测 20 个失效账号重登 20/20 成功(2 分 13 秒、零极验),
证实失效是会话级过期而非凭证问题,可救量约 1,350。
后端:
- 迁移 20261002_0038:login_tasks 加 relogin_failure_kind(枚举),供作业决定
「继续退避重试」还是「转人工」;不用 attempt_message 做字符串匹配(文案一改
就失效,页面也要按类型筛选);
- jobs/ck_relogin.py:
- 候选 = 巡检判定 invalid + 允许自动管理 + 无在途尝试 + 退避到期 + 未超每日上限
+ **未转人工**(极验/凭证问题不再自动重登);
- 复用 LoginBatchRunner(mode="relogin"),成功才替换 CK(沿用既有安全语义)。
自动化身份取最早的超管做 created_by,权限给 login:view_all 以免受归属限制;
总时长上限压到 240 秒,避免长时间占住调度线程;
- settle_results() 独立成纯函数便于单测:成功清空退避状态;失败按分型分流 ——
captcha/credential 转人工(不排期)、email_quota 退避到次日零点+随机抖动、
email_code/interrupted/network 走 1h→3h→12h→24h 阶梯;次数用尽也停下等人工;
- 未知文案默认按可重试处理(有每日上限兜底),别把可能只是抖动的账号推给人工;
- 注册第二个作业「CK 自动重登」(batch 10 / 并发 3,**默认关闭**);
- 新增 GET /api/automation/manual-queue:按原因分组列出待人工处理的账号。可重试
类型但已无下次排期的(次数用尽)单独成组 —— 否则会混进「验证码失败」里,客服
看不出这个已经不会再自动试了。
前端:
- /automation 页新增「待人工处理」卡片,按分型分组展示(极验要过滑块、凭证要核对、
次数用尽要人工确认),各带账号与最近失败原因。
测试:新增 26 条(分型顺序陷阱「上限」优先于「验证码」、退避阶梯、配额退避次日、
候选筛选、MySQL 方言编译、结算、待人工队列分组)。
提交前全量 297 通过、ruff 全绿、前端 tsc 通过。
注意:提交时另一路「测试改用 MySQL」的改造正在同一工作区进行,本次刻意只暂存
本功能相关文件,未触碰 tests/conftest.py、tests/support/ 及对方已转换的测试文件。
|
2026-10-02 20:51:19 +08:00 |
|
yml2213
|
7aaaeebe2b
|
fix(automation): 修正调度口径——作业每 5 分钟跑一轮,interval_seconds 只是单行重检间隔
P1a 把两个「间隔」混成了一个:next_run_at 用的是 job.interval_seconds,于是
作业 6 小时才跑一轮、每轮 200 条 = 800 条/天,全库 12,881 条要 16 天才能过
一遍 —— 等于没巡检,与规划里「账号 6 小时内被查一次」的目标差了两个数量级。
按规划原意修正:
- TICK_SECONDS 60 → 300:作业每个 tick(5 分钟)跑一轮;
- next_run_at 改为 finished + TICK_SECONDS,不再用 interval_seconds;
- interval_seconds 只作为「同一行多久重新检测」的选行 cutoff(选行逻辑本来就
是这么用的,无需改);
- 容量口径随之更新:5 分钟一轮 × 200 条 = 40 条/分钟 ≥ 需要的 36 条/分钟,
冷启动约 5.4 小时覆盖全库,稳态每轮一百多条;
- 前端标签由「巡检周期(小时)」改为「单账号重检间隔(小时)」,说明文案写清
「每 5 分钟跑一轮」,避免运维误以为是作业周期。
同步更新规划文档 §7.2/§7.3(含两个间隔不能混的说明)与附录 C 的三段上线
步骤(立即跑一次 → 每轮 50 观察 → 每轮 200 启用)。
|
2026-10-02 19:54:31 +08:00 |
|
yml2213
|
f745a61df1
|
feat(automation): CK 自动巡检(P1a),只检测不改 CK
后台原本靠人工逐批点「CK 检测」发现失效账号,1.2 万个账号根本盯不过来。
P1a 把「哪些账号失效了」自动化:自动挑久未检测的账号、调现成检测接口、
把结果写回 ck_check_*。**本作业不替换任何 CK**,重登仍由人工点,符合规划里
「先做只读/只提示,确认无误再做自动动作」的渐进上线原则。
实现:
- 迁移 20261002_0037:automation_jobs / automation_runs 两张表,
login_tasks 加 relogin_attempts / relogin_next_at / relogin_last_error
(为 P1b 备好),accounts 加 auto_managed(特殊账号不参与自动化);
- services/automation/scheduler.py:进程内调度线程,60s tick,状态全部落库
(重启不丢调度进度、不重复跑轮);AUTOMATION_ENABLED 默认关闭,作业开关
也默认关闭;ensure_jobs 只补缺失行、不覆盖运维改过的配置;
作业异常只记 failed,不拖垮调度线程;单容器 --workers 1,无需分布式锁;
- services/automation/jobs/ck_health.py:选行口径与人工检测一致
(cookie_holding_filter:持有 CK 且无在途尝试),叠加久未检测、
auto_managed、batch_size,按 ck_checked_at 升序(最久没查的优先);
只写 ck_check_status / ck_check_result / ck_checked_at;
- routers/automation.py:GET /jobs、PATCH /jobs/{key}、POST /jobs/{key}/run、
GET /runs;新增权限点 automation:view(运营可看)与 automation:manage
(仅超管),改配置与手动触发都写审计;
- 顺带补 POST /api/login/batch 的审计(此前批次是谁发起、多少账号无据可查);
- 前端 /automation 页:作业卡片(开关/周期/单轮条数/并发)、运行历史、
「立即跑一次」;总开关未打开、调度线程未运行时给明确提示。
验证:
- 22 条新测试覆盖调度器行为(只跑到点的、异常不抛出、下轮时间、配置不被重置)、
选行条件(在途尝试/auto_managed/间隔/batch_size)、接口与权限,其中一条
专门断言巡检后 cookie/status/message/attempt_status 一字未动;
- 全量 257 条通过;前端 tsc 与构建通过;
- 端到端用 3 个真实账号跑真实斗鱼接口:巡检结果与直接调 check_douyu_cookie
逐条一致(3/3 valid),CK 与尝试状态全程未变。
顺带修掉一处会炸生产的坑:DB 时间列读出来是 naive UTC,直接与
datetime.now(UTC) 比较会 TypeError,现统一走 _as_utc() 补时区。
|
2026-10-02 19:47:41 +08:00 |
|
yml2213
|
168421b40c
|
fix(douyu): 恢复二维码常驻显示,绑定类任务终态也要能看到码
上一版加了「任务进终态后隐藏账号列表里的小二维码」,但绑定类任务等不到扫码
时会以 success 收尾(douyu_runner_bind: "已生成绑定二维码,未检测到新扫码
角色"),url 仍在结果里,码却被藏掉,客服再也点不开 —— 绑定/电竞绑定/小店
绑定都受影响。
二维码的意义与任务终态无关:只要 result 里还有地址就一直显示。这里恢复
taskQrUrl(),删除 activeTaskQrUrl 与 QR_TERMINAL_STATUSES。
「开通时二维码闪一下就变小」不会复发:根因是支付弹窗被自动关闭 + 已开通账号
被误判到账,现在弹窗不再自动关,且已开通账号在开通前就被拦下、根本不生成
支付码。
|
2026-10-02 09:14:40 +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 |
|
 yml2213andCursor
|
4806d4bbf1
|
feat(douyu): 精英宝典导入支持按多行账户名筛选
对齐 Cookie 自定义 CK 的粘贴导入方式,按账户名精确匹配并保持粘贴顺序。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-09-29 15:02:53 +08:00 |
|
yml2213
|
1ac6a13a2c
|
fix(audit): 修复审计日志时区差并支持供应商单号搜索与展示
- 解决审计日志 8 小时时区差:后端输出带时区 UTC ISO 字符串,前端 formatTime 增加 UTC 自动解析换算
- 审计日志支持单号检索:list_audit_logs 关联 DouyuTask 匹配供应商单号、外部单号、UID、昵称等
- 列表摘要与详情抽屉优化:摘要直观展示供应商单号与外部单号,抽屉增加专用供应商订单列表并支持一键复制
- 增加测试用例覆盖时间格式与单号反向检索
|
2026-09-12 14:52:26 +08:00 |
|