Commit Graph
34 Commits
Author SHA1 Message Date
yml2213 f101414f5e feat(accounts): 新增登录异常分型统计、多选/全量导出、筛选记忆及页面组件模块化拆分
- 新增登录失败异常分型统计:支持「必须绑定手机号」、「极验滑块」、「凭证错误」、「邮箱取码失败」、「邮箱配额已满」、「网络异常」分型识别、生命周期联动(待人工)与一键筛选;
- 新增账号名单与明细导出功能:支持勾选多选或按当前筛选条件全选,可导出纯用户名名单 (TXT) 或 UTF-8 BOM 结构化明细报表 (CSV),并支持一键复制到剪贴板;
- 优化筛选条件记忆机制:支持页面切换/返回后自动保留所有筛选与排序状态,新增顶部「重置全部筛选」快捷按钮;
- 模块化拆分 AccountsPage:将 2100+ 行的大文件拆解至 pages/accounts/ 子目录(包含状态卡片、筛选面板、工具栏、操作弹窗与表格构建器),提升可维护性;
- 验证方式:新增 5 个后端自动化单测 (test_account_failure_and_export.py),全量 694 个测试用例通过,前端 TypeScript 编译与 pre-commit 代码规范检查全部通过。
2026-10-07 17:23:39 +08:00
yml2213 44b762ea8c fix(overview): 锁定核心高价值套装并修复未查询空值误判
- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰
- 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换
- 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示
- 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
2026-10-07 15:27:30 +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
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 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 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 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 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 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 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 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 90a0065918 虎牙菜单按角色显示并修复确认框暗色模式
客服在虎牙导航只显示浏览器兑换,管理员/运营保留完整入口;退出登录
与显示凭据确认框改用 App.useApp() 的 modal 实例以适配暗色主题;
版本号升至 0.3.0。
2026-09-05 19:21:59 +08:00
yml2213 baed0e61ff 优化客服账号标签管理 2026-08-14 12:44:48 +08:00
yml2213 2e207f726c feat: 账号凭据默认脱敏并支持主动查看 2026-08-13 16:32:49 +08:00
yml2213 11b249f1fb 账号导入--可以直接打标签 2026-08-01 12:24:19 +08:00
yml2213 10f298b4e1 支持账号按筛选全量批量操作 2026-07-25 10:50:56 +08:00
yml2213 f91c05c108 优化列表分页和数据加载 2026-07-24 13:25:27 +08:00
yml2213 6840728da6 修复 antd 弃用警告 2026-07-24 12:48:02 +08:00
yml2213 a4b04e7733 修复 TypeScript 编译错误:删除未使用的 canViewAll 变量 2026-06-24 22:17:53 +08:00
yml2213 50f90eeeb8 实现账号信息隐藏权限系统
1. 拆分 account:view_all 和 account:view_full 权限
2. 运营只能看用户名、标签、分配,隐藏密码/邮箱等敏感信息
3. 管理员可看完整字段(需 account:view_full 权限)
4. 所有人都可分配账号给客服
5. 前端添加上传时间列
2026-06-24 22:15:46 +08:00
yml2213 4a38b06192 账号管理:分页按钮优化,支持切换每页条数并持久化 2026-06-24 20:21:30 +08:00
yml2213 98728bee87 fix: 修复前端7处eslint错误和6处警告
- AccountsPage/CookiePage/UsersPage/LoginTasksPage/ProxyPage:
  将数据加载函数改为useCallback包裹,补全useEffect依赖项
- LoginTasksPage: loadAccounts和loadTasks改为useCallback
- MainLayout: 补全navigate依赖
- eslint.config.js: 关闭react-hooks/set-state-in-effect(对初始
  数据获取场景过严),降级react-refresh/only-export-components为warn
  (Provider/Hook/Type共存文件是合理设计)
2026-06-23 08:42:00 +08:00
yml2213 dd56de9bd4 fix: 修复proxy_service和account_service中不当的顶层import
- proxy_service.py: 将requests和WhitelistManager改为函数内延迟import,
  避免启动时加载不需要的依赖;移除未使用的get_exit_ip_via_proxy导入
- account_service.py: 移除未使用的func、joinedload、AuditLog、
  user_has_permission顶层导入
2026-06-23 08:35:29 +08:00
yml2213 12c31c2e09 refactor: 消除前端52处any类型+删除冗余requirements.txt+调试print改日志
- 删除 requirements.txt,pyproject.toml 为唯一依赖源(含 requests[socks] extra)
- core/geetest 下 5 处 print() 替换为 loguru logger.debug()
- api/modules.ts 新增 13 个响应接口,消除所有 api.xxx<any, any>
- 新建 utils/error.ts 提供 getErrorMessage(e: unknown) 安全取消息
- 11 个页面组件 catch (e: any) 改为 catch (e: unknown)
- useState<any[]>、columns: any[]、render 参数全部类型化
- TypeScript 类型检查零错误通过
2026-06-23 07:14:08 +08:00
yml2213 5a4805814f 修复账号删除报错+添加批量删除功能
- 修复单个删除报错:删除账号前先删除关联的LoginTask(外键约束)
- 新增批量删除接口 DELETE /api/accounts/batch/delete
- 路由顺序:/batch/delete 在 /{account_id} 前注册避免路径冲突
- 前端添加批量删除按钮(带确认弹窗)
- 前端添加 batchDelete API
2026-06-23 01:29:03 +08:00
yml2213 9af92fc2a9 增加了账号管理的分组功能 2026-06-22 15:09:51 +08:00
yml2213 347edb8103 初步增加 web 界面 2026-06-22 13:11:15 +08:00