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

199 lines
9.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 设计:账号「一键查询」(斗鱼官方实时)
> 状态:**方案已定,待实现**。所有数据源已用**生产镜像库的真实数据**核实过。
> 决策:① 两个套装都属于**精英宝典**,`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` |
| 是否发事件 | 本方案**不发**(纯查询)。若希望"查过"留痕,可单独加一个轻量事件,但要克制 |
| 批量查询 | 用户尚未确认。若要做,形态应是「勾多个账号 → 批量建任务 → 汇总对比表」 |