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 天档(待确认)。
This commit is contained in:
yml2213
2026-10-04 16:45:39 +08:00
parent a64380fbcc
commit 885a780bd3
10 changed files with 496 additions and 12 deletions
+1 -1
View File
@@ -508,7 +508,7 @@ WHERE a.cookie <> src.cookie;
| **S3** ✅ | 状态收口(**已完成 2026-10-04**):`account_lifecycle` 提供 `derive_status()` / `sync_status()` / `set_manual_state()` / `on_login_success()`;9 处判据写入点全部接线;6 状态状态机上线;自动化选行排除 `disabled`/`archived`;新增 `account:manage` 权限与停用/归档/恢复接口;前端列表加状态列与筛选、详情页加状态卡与操作入口 | `status` 可信 | S2 |
| **S4** ✅ | 事件发射点补齐(**已完成 2026-10-04**):`assigned` / `tag_changed` 接上(此前定义了却从没发过)、新增 `ck_cleared`、`ck_invalid` 改为只在转变时发;`audit_logs.account_id` 在分配/标签/检测/清 CK 处打通;新增 `emit_bulk`(批量操作逐账号写事件)与 `record_account_action`(审计+事件一次写完);时间线支持 `category` 筛选;前端补 `status_changed` / `ck_cleared` 样式与分类筛选;补上改备注接口 `PUT /api/accounts/{id}/remark`(`remark_changed` 从此有发射点)| 时间线完整 | S2 |
| **S5** ✅ | 业务事实直发事件(**已完成 2026-10-04**):新增 `services/business_events.py` 命名构造函数(`record_recharge` / `record_exchange` / `record_points_credit` / `record_bind`);9 处成功点直发(含**此前从未收录**的电竞兑换、和平小店兑换、锁单支付、电竞礼物积分);修掉「到账判据 `!=`」与「回调晚于超时」两个洞;幂等键 `douyu_task:{id}` 让直发与旧收集器/回调互相去重;**删除收集器**(含映射、水位线、测试,迁移 `0043` 清作业行);新增 `douyu_tasks` 保留策略作业(噪声 7 天 / 其余 90 天,默认关闭) | 台账可用,687MB 可控 | S4 |
| **S6** ◐ | `bind_status` 三列拆解(**已完成 2026-10-04**):新增 `services/account_binding.py`(四值枚举 + 唯一写入口 + 旧值映射)、迁移 `0044`(新增 `role_bind_status` 与 `esports_opened`、归一化三列、收窄列宽)、删除全部 29 处自由文本写入、前端改为读账号字段(不再从任务日志反推状态)、详情页显示三列绑定状态。**账号页重构(列表关键列 / 生命周期主页)尚未做**,`bind_status` 旧列待 S8 drop | 账号管理真正独立 | S3、S5 |
| **S6** ✅ | 绑定三列拆解 + 账号页重构(**已完成 2026-10-04**):①`services/account_binding.py` + 迁移 `0044`(新增 `role_bind_status` 与 `esports_opened`、归一化三列、收窄列宽、可回退),删除全部 29 处自由文本写入;②前端不再从任务日志反推账号状态,改为读账号字段;③**账号列表**加关键列(CK 健康 + 检测时间、精/电/店绑定、积分/鱼翅)与三个筛选(状态 / CK / 绑定);④**详情页做成生命周期主页**:顶部状态卡(生命周期状态 + 原因 + 变更时间 + CK + 三角色绑定 + 积分/鱼翅/兑换余额 + 归属与标注操作)、业务台账(补登录次数/最近登录)、分类时间线、生命周期操作入口。`bind_status` 旧列待 S8 drop | 账号管理真正独立 | S3、S5 |
| **S7** | 账号检测并入账号域(`account-check` 从「粘贴文本」改为「按账号选」,结果落事件) | 检测不再是孤岛 | S6 |
| **S8** | 清理:drop `login_tasks` 旧列与 `accounts` 三个 `*_bind_status` 列(**不可逆,前置:每日备份 + 反向回填脚本**) | 包袱丢干净 | S6 |