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。
This commit is contained in:
yml2213
2026-10-04 19:19:53 +08:00
parent 3866b49087
commit c2520613ca
4 changed files with 101 additions and 8 deletions
+16 -4
View File
@@ -139,8 +139,12 @@ export default function AccountDetailPage() {
// 一键查询:建一个只读聚合任务 → 轮询**账号自身**直到快照更新。
//
// 结果写在账号上(`overview_snapshot`),不是只留在任务里 ——
// 所以轮询账号就够了,也不依赖任务日志的寿命。
// 结果写在账号上(`overview_snapshot`),所以轮询账号就够了。
//
// ⚠️ 轮询必须**能自证在推进**。之前的 bug:详情接口忘了返回
// `overview_queried_at`(字段加到了列表接口上),于是比较永远相等、
// 轮询满 120 秒 —— 用户看到的就是"点一下就一直转圈刷请求"。
// 所以这里加了:拿不到标记字段就**立刻报错退出**,而不是傻等。
const runOverview = useCallback(async () => {
if (!id) return;
const accountId = Number(id);
@@ -153,11 +157,19 @@ export default function AccountDetailPage() {
task_type: 'query_account_overview',
handbook_scope: 'elite',
});
const deadline = Date.now() + 120_000; // 要串好几个斗鱼请求,给足时间
const deadline = Date.now() + 120_000;
let first = true;
while (Date.now() < deadline) {
await new Promise((resolve) => setTimeout(resolve, 2000));
const fresh = await accountApi.overview(accountId);
if ((fresh.overview_queried_at ?? '') !== before) {
const marker = fresh.overview_queried_at;
if (first && marker === undefined) {
// 接口没返回标记字段 —— 再怎么轮询也不可能变化,直接如实报错
setOvError('接口未返回查询时间标记,无法判断是否完成(请检查后端详情接口)');
return;
}
first = false;
if ((marker ?? '') !== before) {
setOverview(fresh);
message.success('查询完成');
return;