yml2213
|
62d9053e8a
|
feat(account): 官方兑换记录补成账号事件(三作用域)+ 一键查询结果落到账号
## 官方兑换记录 → 真正的账号事件
关键发现:斗鱼官方兑换记录**带真实兑换时间**(`time` 字段),所以可以直接
补成正常的历史事件,时间线因此变完整(这就是「账号全生命周期」该有的样子),
列表的兑换聚合也随之准确 —— 上线前的兑换不再是一片空白。
- **三个作用域都拉**:精英宝典 / 电竞手册(共用 `exchange_records`,只有 manual_id
不同)/ 和平小店(`xpd_embed_query` → `xpd_purchase_records`)
- 幂等键含作用域:`official_exchange:{scope}:{account}:{commodity}:{time}`
- `detail.scope` 标作用域;`source=official_sync` 与"当场记的"分得开
- 任一作用域失败只记进 `errors`,不影响其余(小店未开通会报
`Livelink登录态校验失败`,属正常)
## 一键查询结果落到账号(不再只留在任务里)
- 新增 `accounts.high_value_owned`(迁移 0048):**已兑换过的高价值套装名**,
供列表展示。单开一列而不是让列表读 `overview_snapshot` —— 那份 JSON 有 1~2KB,
15000 个账号各带一份会把响应放大到几十 MB。
- 迁移**可安全重跑**(启动时会自动跑):加列前查 `information_schema`。
⚠️ 回填里**不要** `op.get_bind().commit()` —— MySQL 的 DDL 自动提交而 commit
会打乱版本记账,表现为"列加上了、版本号没更新",下次启动重复 ADD COLUMN
直接把服务起不来(生产上就是启动即挂)。
- 迁移里从旧快照**回填** `high_value_owned`,已查过的账号不必重查。
## 两个 bug
1. **「最近一次兑换」按插入 id 排**(应为事件时间):补录的历史事件 id 最大、
时间却最老,会把几个月前的老兑换显示成"最近一次"。改为 `occurred_at DESC, id DESC`。
2. **手写的 `@model_serializer` 没列新字段** → 属性有值、JSON 里没有。
这是本项目第三次踩 Pydantic 静默丢字段;已补,并把断言改为看 `model_dump()`。
## 列表「兑换记录」口径
只聚合**官方**记录(`source=official_sync`):用户要求列表看官方的权威口径,
我们当场记的那些留给详情页时间线。另加 `GET /accounts/{id}/exchange-records`
供列表点击预览(按需拉取,不进列表响应)。
|
2026-10-04 20:03:42 +08:00 |
|
yml2213
|
aea05952af
|
refactor(account): 一键查询不该重复展示账号字段 —— 改成刷新它们,只留账号表里没有的
用户反馈:「一键查询也太重复了,直接更新账号信息不行吗?为啥很多都要再展示一次?
那建立账号信息表有啥用」——**说得对,是我设计错了**。
## 问题
面板把鱼翅 / 积分 / 钱包余额 / 精英宝典 / 电竞手册又原样列了一遍,
而这些账号表(`accounts`)本来就有对应列,账号详情也在显示同一批值 ——
同一屏出现两份,既冗余又让人分不清哪份是准的。
## 改法:查询 = 刷新,不是另开副本
| 内容 | 处理 |
| --- | --- |
| 鱼翅 / 积分 / 交易余额 / 开通状态 | 写回**账号表**(`_refresh_*` 本来就在写),由账号详情已有界面展示 —— **面板不再重复** |
| **积分到期时间** | 有 `accounts.points_expire_at` 列,但**查询漏了没写、界面也没显示** → 现在写回,并加到账号信息区展示 |
| **高价值套装是否已获得** | 账号表没有 → **只有这个该单独展示** |
| **官方兑换记录** | 账号表没有 → 单独展示 |
面板因此瘦身成只含「🎯 高价值套装」+「兑换记录」(+ 错误提示 + 查询时间),
标题也改成「高价值套装与兑换记录(一键查询)」,不再叫"账号信息"。
## 修了一个连带 bug
积分到期时间一直没被查询写回账号(只在快照里躺着),界面也没展示 ——
现在 `_parse_expire_at` 后写入 `accounts.points_expire_at`,
且斗鱼返回 `0` 时写 `None` 而不是 `0`(否则又显示 1970/1/1)。
## 测试(+3,共 41)
`TestOverviewRefreshesAccountFields`:资产/开通状态确实写回账号列、
**积分到期时间写回**、`willExpireTime="0"` 时清成 None 而非 0。
## 验证
`TZ=Asia/Shanghai` **593 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 19:22:34 +08:00 |
|
yml2213
|
3866b49087
|
fix(account): 一键查询按反馈收敛 —— 只在账号管理展示、结果落账号、修 1970 显示
按用户反馈改了四处。
## ① 结果只在账号管理展示(不再出现在精英宝典/电竞手册页)
移除了工作台页的结果弹窗与导入。**类型集合保留**(三个工作台都有)——
因为账号页批量建任务要用 `handbook_scope` 过 `DOUYU_HANDBOOK_TASK_TYPES` 校验,
去掉会被「该任务不属于当前工作台」拦掉;去掉的只是**快捷操作按钮**。
## ② 不再每次弹出
原来任务跑完就自动弹面板,而且任务会被反复轮询到。现在整个弹窗机制去掉了:
结果落在账号上,打开账号就能看。
## ③ 结果写进账号(新增迁移 0047)
新增 `accounts.overview_snapshot`(JSON)+ `overview_queried_at`,覆盖式只留最新一份。
详情页直接读账号上的快照,不再轮询任务。
**为什么放账号而不是只留任务里**:任务会被保留策略清掉(长保留档 90 天),
而「这个账号现在什么情况」是**账号的信息**,不该依赖一条日志的寿命。
## ④ 修 1970/1/1 08:00:00(真 bug)
斗鱼用 `0` / `"0"` / 空串表示「没有这个时间」。原实现直接透传,
前端 `new Date(0)` → **1970/1/1 08:00:00** —— 运营会以为"到期时间是 1970 年"。
两端都修了:
- 后端新增 `_ts()`,把 `0`/`"0"`/空/负数 归成 `None`(积分到期、宝典到期、
电竞到期、商品活动时间四处都过一遍)
- 前端 `formatTime` 也把 `0`/负数显示成 `-`(上游再出错也不至于显示 1970)
## 测试(+14 条,共 36)
- `TestTimestampNormalisation`:`0`/`"0"`/空/负数 → `None`;真实时间戳透传
- `TestSnapshotIsWrittenToAccount`:快照与时间落到账号上、
**`willExpireTime="0"` 不能落库成 0**(1970 的回归用例)、覆盖式不追加
## 验证
`TZ=Asia/Shanghai` **588 passed / 6 skipped**;`TZ=UTC` 41 passed;
ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 19:12:51 +08:00 |
|
yml2213
|
d285196287
|
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: []`),
已获得/未获得判定全部正确
|
2026-10-04 19:07:35 +08:00 |
|