feat(account): 账号「一键查询」(异步 + 批量)—— 含一次重要的判定修正
按 `docs/设计-账号一键查询.md` 实现。异步任务、支持批量(后端每账号一条任务)。 ## 功能 - 新任务类型 `query_account_overview`:**只读**聚合查询,不改账号状态、不发事件 (纯查询不是「事实」,按既有设计例行查询不进时间线,否则会刷屏) - 一次拉全:鱼翅 / 钱包兑换余额 / 积分(含到期)/ 精英宝典 / 电竞手册 / **官方兑换记录** / 限兑商品,并**重点标出「兔兔白日梦」「狂战萌兔」是否已获得** - **单项失败不整体失败**:账号没绑电竞手册是常态,失败只写进 `errors` 并在 UI **如实展示** —— 避免「没查到」被读成「没有」 - 批量:`create_douyu_planned_tasks` 本来就是每账号一条任务,前端加「批量一键查询」 - 前端:详情页一键查询卡片 + 结果面板(两个套装单独高亮);任务页结果自动弹面板; 三个工作台的快捷操作/类型集合都加了 ## ⚠️ 修正:判定「是否已获得」用错了依据 设计文档第一版说「`storeStatus == 0` → 已拥有」,依据是 「账号 558(兑换过)count=0,账号 15407(没兑换)count=96982」。 **这是错的。** 那两条记录分别取自 **2026-08-01** 和 **2026-10-04**,相隔两个月 —— 我把**时间差异**当成了**账号差异**。 **同一时刻**同时查 558 / 607(都兑换过)和 15507(没有), 商品项的**所有字段完全一致**,`preExchange.isExists` 也都是 0; `count` 的取值分布是 0/98746/103350/109832…,**随时间变化 = 全局库存**。 用它判「这个账号有没有」必然给错结论 —— 而对高价值资产给错结论是本功能最不能接受的。 **正确依据:`exchange_records(manual_id)`**(斗鱼官方的**账号级**兑换记录)。 真实斗鱼实测: | 账号 | 官方兑换记录 | 判定 | | --- | --- | --- | | 558 | 破片手榴弹-兔兔白日梦 | **已获得 ✅** | | 607 | 破片手榴弹-兔兔白日梦 | **已获得 ✅** | | 15507 | (0 条) | 未获得 ✅ | 同时刻三者看到的兔兔白日梦库存**都是 96956**(全局),兑换记录却区分得干干净净。 顺带发现:同一件东西有多个商品名(`套装-兔兔白日梦`、`破片手榴弹-兔兔白日梦`), 所以按**核心词子串**匹配整个名字家族。 商品列表的 `score`/`count`/活动时间仍展示,但明确是**全局**信息 (「现在还能不能换、要多少分」),与「该账号是否已获得」分开。 ## 测试 `tests/test_account_overview.py` 22 条,含**回归用例**: `test_global_stock_never_implies_owned`(全局售罄不能推出账号拥有)—— 正是第一版踩的坑。另有失败隔离、只读性(不发事件/不改状态)、 批量每账号一条任务、`missing` 不被静默丢弃。 ## 验证 - `TZ=Asia/Shanghai` **574 passed / 6 skipped**;`TZ=UTC` 27 passed - ruff 全绿;前端 build 通过、lint 0 errors - **真实斗鱼端到端**:三个账号各跑一次真查询,8 项全部成功(`errors: []`), 已获得/未获得判定全部正确
This commit is contained in:
+53
-19
@@ -15,33 +15,66 @@
|
||||
|
||||
## 2. 关键实证发现(已核实,别再猜)
|
||||
|
||||
### 2.1 「是否已拥有某套装」能直接从官方查
|
||||
### 2.1 「是否已获得」的依据:**官方兑换记录**(⚠️ 修正过一次)
|
||||
|
||||
`query_limited_goods(manual_id, rid)` 返回的商品项里,**`count` / `foreverCount` /
|
||||
`storeStatus` 是账号维度的**。实测同一个商品「套装-兔兔白日梦」:
|
||||
**结论:用 `exchange_records(manual_id)` 判定,不要用 `query_limited_goods` 的库存字段。**
|
||||
|
||||
| 字段 | 账号 558(**已兑换**) | 账号 15407(未兑换) |
|
||||
#### ❌ 被推翻的第一版规则
|
||||
|
||||
第一版拿 `query_limited_goods` 商品项里的 `count` / `storeStatus` 判断,依据是:
|
||||
|
||||
| 字段 | 账号 558(兑换过) | 账号 15407(没兑换) |
|
||||
| --- | --- | --- |
|
||||
| `count` | **0** | 96982 |
|
||||
| `foreverCount` | **0** | 96982 |
|
||||
| `storeStatus` | **0** | 1 |
|
||||
| `count` | 0 | 96982 |
|
||||
| `storeStatus` | 0 | 1 |
|
||||
|
||||
**判定规则**:`storeStatus == 0`(或 `count == 0`)→ **已拥有**;否则可兑换。
|
||||
**这是错的。** 上面两条记录分别取自 **2026-08-01** 和 **2026-10-04**,
|
||||
相隔两个月 —— 我把**时间差异**当成了**账号差异**。
|
||||
|
||||
⚠️ 注意 `storeStatus` 也可能是「全局售罄/下架」导致 0,所以展示时**不要只写"已拥有"**,
|
||||
建议写「**不可再兑**(已拥有或已售罄)」,并把 `count` 一起显示,避免误判。
|
||||
在**同一时刻**同时查 558 / 607(都兑换过)和 15507(没有),
|
||||
商品项的**所有字段完全一致**,`preExchange.isExists` 也都是 0。
|
||||
`count` 的取值分布是 0 / 98746 / 103350 / 109832 … 随时间变化,
|
||||
**是全局库存/活动状态,不暴露账号维度信息**。
|
||||
|
||||
> 用全局字段判"这个账号有没有"必然给错结论 ——
|
||||
> 而对高价值资产给错结论是本功能最不能接受的失败方式。
|
||||
|
||||
#### ✅ 正确依据
|
||||
|
||||
`exchange_records(manual_id)` —— 斗鱼官方的**账号级**兑换记录。同一时刻实测:
|
||||
|
||||
| 账号 | 官方兑换记录 | 判定 |
|
||||
| --- | --- | --- |
|
||||
| 558 | `破片手榴弹-兔兔白日梦` | **已获得** |
|
||||
| 607 | `破片手榴弹-兔兔白日梦` | **已获得** |
|
||||
| 15507 | (0 条) | 未获得 |
|
||||
|
||||
同时刻三者看到的兔兔白日梦库存**都是 96956**(全局),但兑换记录把账号区分得干干净净。
|
||||
|
||||
#### 两处信息各司其职
|
||||
|
||||
| 来源 | 用途 |
|
||||
| --- | --- |
|
||||
| `exchange_records` | **是否已获得**(账号级) |
|
||||
| `query_limited_goods` | 当前积分价 / 库存 / 活动时间(**全局**,只用于"现在还能不能换、要多少分") |
|
||||
|
||||
#### 名字按子串匹配
|
||||
|
||||
同一件东西有多个商品名 —— 实测既有「**套装**-兔兔白日梦」也有
|
||||
「**破片手榴弹**-兔兔白日梦」。所以按核心词(`兔兔白日梦`)子串匹配,
|
||||
把整个名字家族都算上。
|
||||
|
||||
### 2.2 两个套装的确切标识
|
||||
|
||||
| 商品名 | `commodityId` | `score`(积分价) | `countLimited` |
|
||||
| --- | --- | --- | --- |
|
||||
| 套装-兔兔白日梦 | `20260120QYOOB_bag10` | 1280 | **1**(每账号限兑 1 次) |
|
||||
| 套装-狂战萌兔 | 待取(同法从官方拉) | 待取 | 待取 |
|
||||
| 核心词 | 出现过的完整商品名 |
|
||||
| --- | --- |
|
||||
| 兔兔白日梦 | `套装-兔兔白日梦`(`20260120QYOOB_bag10`,1280 分)、`破片手榴弹-兔兔白日梦` |
|
||||
| 狂战萌兔 | 实测 607 的列表里是 `套装-狂战萌兔`(库存 2551) |
|
||||
|
||||
两者均带 `eliteLimited = 1` → **属于精英宝典**(与决策① 一致)。
|
||||
|
||||
> 实现时**按 `commodityName` 匹配**(含「兔兔白日梦」「狂战萌兔」),
|
||||
> 并把 `commodityName` 做成配置项,不要硬编码到代码里(以后可能加更多高价值商品)。
|
||||
> 实现按**核心词**匹配即可,不必写死完整商品名或 `commodityId`;
|
||||
> 名单做成配置项,以后加更多高价值商品不用改代码。
|
||||
|
||||
### 2.3 一个易踩的坑
|
||||
|
||||
@@ -97,9 +130,10 @@
|
||||
"name": "套装-兔兔白日梦",
|
||||
"commodity_id": "20260120QYOOB_bag10",
|
||||
"score": 1280,
|
||||
"owned": true, // storeStatus == 0 || count == 0
|
||||
"store_status": 0,
|
||||
"count": 0,
|
||||
"owned": true, // 来自官方兑换记录,不是库存字段
|
||||
"obtained_names": ["破片手榴弹-兔兔白日梦"],
|
||||
"in_goods_list": true, // 当前限兑列表里有没有(全局信息)
|
||||
"count": 96956, // 全局库存,仅展示
|
||||
"start_time": "1769153181",
|
||||
"end_time": "1795967999"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user