Files
live-hub-py/docs/设计-账号一键查询.md
T
yml2213 5405d4b1d1 fix(overview): 纠正高价值套装以商城 status==1 为最高准入裁判
- 根因:斗鱼官方商城接口 status==1 代表「已达兑换上限」,历史代码误以为商品列表全为全局字段而单纯依赖常被官方清空的 exchange_records,导致已兑换账号被误判为可兑换
- 修复:extract_high_value_goods 结合商城完整列表 status==1 现场判定已兑换,exchange_records 作为历史展示与兜底
- 前端:优化工作台与一键查询展示,明确展示「已达上限 N 个: ...」与「未达上限」,消除反向歧义
- 文档:沉淀坑 #9 到 AGENTS.md,并更正设计-账号一键查询.md
2026-10-07 14:51:51 +08:00

9.5 KiB
Raw Blame History

设计:账号「一键查询」(斗鱼官方实时)

状态:方案已定,待实现。所有数据源已用生产镜像库的真实数据核实过。 决策:① 两个套装都属于精英宝典,manual_id/rid 用后台配置; ② 交互方式 = 异步任务(复用现有 runner + WebSocket 推送)。


1. 目标

账号管理/详情页一个「一键查询」按钮 → 实时向斗鱼拉取该账号的全部关键信息 → 汇总展示,并高亮两个高价值套装是否已拥有。


2. 关键实证发现(已核实,别再猜)

2.1 「是否已获得」的依据:官方兑换记录(⚠️ 修正过一次)

结论:以商城 storehome 商品项的 status == 1(已达兑换上限)为最高裁判;exchange_records 仅作历史展示与下架商品兜底。

❌ 历史踩坑与认知误区复盘(2026-10-07 事故修正)

第一版曾误以为商品列表里的 count(库存数)和 storeStatus(店铺上架状态)在不同账号间一致, 就武断地认为“商品列表整个对象都是全局的,不暴露账号信息”,因而全盘依赖 exchange_records(manual_id)。

这是严重的偷换概念与认知漏洞! 实测(HAR 抓包与生产 50 账号对比采样)表明:

  1. 斗鱼官方的 exchange_records(exchangeList)接口对早期兑换记录早已返回空列表(0 条),根本不可信;
  2. 斗鱼官方真正的账号准入状态就在商城接口 storehome(list_goods)中:
    • status == 1:官方前端渲染为【已达兑换上限】(该账号限量 1 件已满,已兑换,不可再兑);
    • status == 0:官方前端渲染为【立即兑换】(该账号未兑换,可兑换)。 生产 50 账号实测中,兔兔白日梦有 23 个账号 status=0(可兑换),27 个账号 status=1(已达上限)。

✅ 正确判定法则(双重承重)

来源 条件 判定结果
商城商品列表(storehome) status == 1 已获得 / 已达兑换上限(不可再兑)(最高现场裁判)
商城商品列表(storehome) status == 0 且 records 无记录 未获得(可兑换)
官方兑换记录(exchange_records) 存在匹配记录 已获得(用于已下架商品的历史兜底)
两处均查无此商品 - missing = True(未上架/未查到)

名字按子串匹配

同一件东西有多个商品名 —— 实测既有「套装-兔兔白日梦」也有 「破片手榴弹-兔兔白日梦」。所以按核心词(兔兔白日梦)子串匹配, 把整个名字家族都算上。

2.2 两个套装的确切标识

核心词 出现过的完整商品名
兔兔白日梦 套装-兔兔白日梦(20260120QYOOB_bag10,1280 分)、破片手榴弹-兔兔白日梦
狂战萌兔 实测 607 的列表里是 套装-狂战萌兔(库存 2551)

两者均带 eliteLimited = 1 → 属于精英宝典(与决策① 一致)。

实现按核心词匹配即可,不必写死完整商品名或 commodityId; 名单做成配置项,以后加更多高价值商品不用改代码。

2.3 一个易踩的坑

query_exchange_records 这个任务类型在生产从未运行过(0 行)。真正的官方兑换记录 接口是客户端的 exchange_records()(activity_client.py:950),它没有被任何任务接线。 一键查询要用它。


3. 数据源矩阵(客户端方法都已存在)

展示项 客户端方法 参数来源
鱼翅余额 gold_account() 无
钱包兑换余额 exchange_balance() 无
积分 + 到期时间 query_points(uid=, ctn=) account.uid;ctn 来自 acf_ccn()
精英宝典开通状态 现有 query_elite_status 任务的实现(见 runner) 后台配置
电竞手册状态 + 积分 esports_user_info(manual_id=) 后台配置
兑换记录(官方) exchange_records(manual_id=, limit=, offset=) 后台配置
限兑商品(含两套装是否已拥有) query_limited_goods(manual_id=, rid=) 后台配置
小店余额 / 碎片 xpd_embed_query(act_alias=, rid=) 后台配置

「现有 query_elite_status 的实现」需要实现时去 runner 里确认它调的是哪个方法 (我的检索没直接命中该方法名,可能在 runner 内联了请求)。


4. 任务设计

新增任务类型:query_account_overview

  • 走现有 douyu_tasks 体系(异步 + WebSocket 进度推送 + 离线可查)
  • 只读:不改任何账号状态、不发业务事件 (纯查询不是"事实",按既有设计例行查询不进时间线,避免刷屏)
  • 结果写入 task.result,结构见下

结果结构(建议)

{
  "queried_at": "2026-10-04T18:00:00Z",
  "assets": {
    "gold_balance": 38,              // 鱼翅
    "exchange_balance": 120,         // 钱包兑换余额
    "points": 990,
    "points_expire_at": "2026-12-01 00:00:00"   // 来自 willExpireTime
  },
  "elite":   { "opened": true,  "expire_time": "..." },
  "esports": { "opened": false, "points": null },
  "xpd":     { "balance": null, "fragments": null },
  "high_value_goods": [              // 🎯 重点
    {
      "name": "套装-兔兔白日梦",
      "commodity_id": "20260120QYOOB_bag10",
      "score": 1280,
      "owned": true,                 // 来自官方兑换记录,不是库存字段
      "obtained_names": ["破片手榴弹-兔兔白日梦"],
      "in_goods_list": true,         // 当前限兑列表里有没有(全局信息)
      "count": 96956,                // 全局库存,仅展示
      "start_time": "1769153181",
      "end_time": "1795967999"
    },
    { "name": "套装-狂战萌兔", "...": "同上" }
  ],
  "limited_goods_total": 87,         // 商品总数(便于判断是否拿到了完整列表)
  "exchange_records": [ /* 官方兑换记录前 N 条 */ ],
  "errors": { "esports": "该账号未绑定电竞手册" }   // 单项失败不整体失败
}

关键设计点:

  1. 单项失败不整体失败 —— 每个查询独立 try/except,失败写进 errors[项], 其余照常返回。账号没绑定电竞手册是常态,不该让整次查询失败。
  2. errors 必须如实展示 —— 避免"没查到"被误读成"没有"。
  3. exchange_records 默认取前 20 条,完整记录量大。

5. 实现清单

后端

文件 改动
web/backend/services/douyu_runner_*.py 新增 query_account_overview 的 handler(聚合各查询)
任务类型注册处 加入 query_account_overview(放在只读/查询类,不要放进写操作类)
web/backend/routers/douyu.py 建任务时允许该类型;payload 只需 account_id
后台配置 取精英宝典的 manual_id / rid(复用现有配置项,不新增硬编码)
高价值商品名单 加配置项,默认 ["兔兔白日梦", "狂战萌兔"]

前端

文件 改动
web/frontend/src/pages/AccountDetailPage.tsx 顶部加「一键查询」按钮;结果面板展示资产/开通/绑定/兑换记录;两个套装单独高亮卡片(已拥有 / 可兑换 + 积分价 + 活动时间)
web/frontend/src/pages/douyu-tasks/DouyuTasksPage.tsx 新任务类型的展示(结果渲染成上面的面板,而不是原始 JSON)
(可选,强烈建议)AccountsPage.tsx 列表加「高价值套装」列/标记,支持筛选"拥有兔兔白日梦的账号" —— 这才是"找高价值账号"的真正用法

测试

用例 要点
高价值商品判定 storeStatus=0 → owned=true;=1 → false;缺该项时不能报 owned
单项失败隔离 电竞查询抛异常 → 其余字段仍返回,且 errors.esports 有值
只读性 该任务不改任何 accounts.* 字段、不发任何 account_events(用既有的事件计数断言)
结果结构 high_value_goods 一定包含配置里的商品名(即使官方列表里没有 → 标记 "missing": true,不能默默消失)
契约 用真实返回样本(已存于镜像库)做解析回归,避免字段改名静默失效

6. 实测样本可复现(镜像库)

-- 已兑换的账号(用于验证判定规则)
SELECT DISTINCT account_id FROM douyu_tasks
WHERE task_type='exchange_goods' AND status='success'
  AND result LIKE CONCAT('%', 0xE58594E58594E799BDE697A5E6A2A6, '%') LIMIT 5;
-- → 558, 607, 2682, 3094, 3090

-- 未兑换但跑过商品查询的账号
-- → 15407, 15406, 15401

注意:SQL 里中文用 LIKE '%兔兔白日梦%' 在多层引号里会被 shell 搞坏, 要写成 0x... 十六进制(上面的写法)。


7. 待确认 / 风险

项 说明
rid(房间号) 决策是用后台配置。要确认同一套配置对所有账号都适用(房间号通常绑定活动,不随账号变)
狂战萌兔的 commodityId 实现时从官方列表取,不要写死
「已拥有」的准确措辞 storeStatus=0 也可能是售罄/下架,UI 上应写「不可再兑」并显示 count
是否发事件 本方案不发(纯查询)。若希望"查过"留痕,可单独加一个轻量事件,但要克制
批量查询 用户尚未确认。若要做,形态应是「勾多个账号 → 批量建任务 → 汇总对比表」