# 设计:账号「一键查询」(斗鱼官方实时) > 状态:**方案已定,待实现**。所有数据源已用**生产镜像库的真实数据**核实过。 > 决策:① 两个套装都属于**精英宝典**,`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`,结构见下 ### 结果结构(建议) ```jsonc { "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. 实测样本可复现(镜像库) ```sql -- 已兑换的账号(用于验证判定规则) 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` | | 是否发事件 | 本方案**不发**(纯查询)。若希望"查过"留痕,可单独加一个轻量事件,但要克制 | | 批量查询 | 用户尚未确认。若要做,形态应是「勾多个账号 → 批量建任务 → 汇总对比表」 |