yml2213
|
88422f2f05
|
Merge branch 'fix/pyright-env-and-type-guards' into dev
|
2026-10-08 12:34:11 +08:00 |
|
yml2213
|
a3c3a46b9c
|
fix(tooling): 修复 pyright 解释器配置与类型守卫,补齐 ruff 门禁与两处历史 bug
- pyrightconfig.json 钉死 Python 3.12 虚拟环境,消除由于查找系统 Python 3.9 带来的假性报错
- 修复 huya/login.py 引用不存在函数的问题并补充单元测试
- 修复 browser_harvest.py 对 set 下标访问的错误并提升报错信息质量
- 虎牙 6 个子路由显式导入依赖,消除 import * 带来的未定义变量报警
- 规范化 Pydantic Field(default=...) 默认值声明
- 完善 solver.py / app_login.py 等核心路径类型守卫与 @overload 重载
- 在 pyproject.toml 中显式锁定 ruff lint 规则集 (E4, E7, E9, F, B, I, BLE)
- 门禁脚本同步增加 ruff check 检查并在 AGENTS.md 补齐实践经验总结
|
2026-10-08 12:33:39 +08:00 |
|
yml2213
|
224b147735
|
Merge branch 'main' into dev: 同步充值渠道切换修复
|
2026-10-08 11:53:26 +08:00 |
|
yml2213
|
ec07e4fc06
|
fix(config): 修复切换充值渠道报错及补齐 peace_enabled 字段声明
- 在 DouyuConfigOut 与 DouyuConfigUpdate 中补齐 peace_enabled 声明与序列化
- 在 update_config 中使用 getattr(req, field, None) 安全读取属性,避免 AttributeError 导致 500
- 在 _config_out 中加入 peace_enabled 输出映射
- 增加切换充值渠道与配置更新的单元测试
|
2026-10-08 11:49:41 +08:00 |
|
yml2213
|
578fb08dab
|
test(pool): 修复测试用例中硬编码历史日期导致租约过期判定的问题
|
2026-10-08 11:41:32 +08:00 |
|
yml2213
|
077f14ef23
|
Merge branch 'main' into dev: 同步分配管理优化、登录异常分型及历史数据刷正
|
2026-10-08 11:38:48 +08:00 |
|
yml2213
|
4eef45efe4
|
fix(accounts): 完善手机号必绑与账号注销等登录异常分型识别
- 补充必须绑定手机号与账号注销/封禁/冻结等错误文案的识别规则
- 增加数据修正迁移 20261008_0052,回写历史账号和登录任务的失败分型
- 增加列表查询与导出时根据错误文案自动兜底细化 failure_kind 的逻辑
- 增加并完善相关分型单元测试
|
2026-10-08 11:26:19 +08:00 |
|
yml2213
|
f8ad6e1777
|
perf(assignments): 优化分配管理页面加载性能与角标数量展示
- 前端改造为服务端分页查询 (accountApi.listPaged),避免一次性拉取 1.6 万+ 账号导致页面卡死
- 优化后端 assignments_summary 聚合查询,消除大子查询临时表
- 修复 Badge 默认 99+ 限制,设置 overflowCount 支持展示完整账号数量
- 修复测试用例 test_business_events.py 当前时间跨天断言问题
|
2026-10-08 11:08:41 +08:00 |
|
yml2213
|
f101414f5e
|
feat(accounts): 新增登录异常分型统计、多选/全量导出、筛选记忆及页面组件模块化拆分
- 新增登录失败异常分型统计:支持「必须绑定手机号」、「极验滑块」、「凭证错误」、「邮箱取码失败」、「邮箱配额已满」、「网络异常」分型识别、生命周期联动(待人工)与一键筛选;
- 新增账号名单与明细导出功能:支持勾选多选或按当前筛选条件全选,可导出纯用户名名单 (TXT) 或 UTF-8 BOM 结构化明细报表 (CSV),并支持一键复制到剪贴板;
- 优化筛选条件记忆机制:支持页面切换/返回后自动保留所有筛选与排序状态,新增顶部「重置全部筛选」快捷按钮;
- 模块化拆分 AccountsPage:将 2100+ 行的大文件拆解至 pages/accounts/ 子目录(包含状态卡片、筛选面板、工具栏、操作弹窗与表格构建器),提升可维护性;
- 验证方式:新增 5 个后端自动化单测 (test_account_failure_and_export.py),全量 694 个测试用例通过,前端 TypeScript 编译与 pre-commit 代码规范检查全部通过。
|
2026-10-07 17:23:39 +08:00 |
|
yml2213
|
603fc6eadc
|
feat(douyu-pool): 完善微信扫码支付与任务闭环及常用快捷选择
- 前端在扫码支付弹窗中增加显眼充值金额卡片与多账号 Tabs 支付标注
- 前端增加常用与历史快捷皮肤选择胶囊栏,支持本地缓存记忆与一键切换
- 后端补给链首步引入 query_elite_status 并支持 require_opened 前置熔断校验
- 优化供给与预估花费文案展示,增加兑换结果大图预览与凭证复制功能
- 验证方式:全量 784 个 pytest 用例全部通过,前端 tsc/build 校验通过
|
2026-10-07 16:59:43 +08:00 |
|
yml2213
|
7c1a6caf47
|
fix(config): 补充 peace_enabled 字段声明并增加安全属性读取
- 在 DouyuConfigOut 与 DouyuConfigUpdate 中加入 peace_enabled 声明与序列化
- 在 update_config 中使用 getattr(req, field, None) 做安全兜底,避免 500
|
2026-10-07 16:19:23 +08:00 |
|
yml2213
|
049174a795
|
Merge branch 'main' into dev
|
2026-10-07 15:29:43 +08:00 |
|
yml2213
|
44b762ea8c
|
fix(overview): 锁定核心高价值套装并修复未查询空值误判
- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰
- 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换
- 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示
- 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
|
2026-10-07 15:27:30 +08:00 |
|
yml2213
|
bb01ccf77c
|
Merge branch 'main' into dev
|
2026-10-07 14:52:22 +08:00 |
|
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 |
|
yml2213
|
82ba157154
|
Merge main into dev: 登录导出/失败入流水 + AGENTS.md 修正
把 main 上的两笔带过来,dev 继续在其上开发:
- d11fa10 登录任务按状态导出 CSV + 登录失败写进账号生命周期
- 05b2bf8 AGENTS.md 分支约定修正
两条线在此之前无文件重叠(dev 38 个 / main 9 个,交集为空),所以是干净合并。
注意:main 的登录功能不含迁移,dev 的 0052 仍是唯一新增迁移。
|
2026-10-07 13:27:56 +08:00 |
|
yml2213
|
6f8b0160d6
|
docs(pool): 更正实施记录的测试数字(基线 671 / 净新增 95)
- 「基线 727 / 新增 39」**两个数都错**:727 是 P1 完成时跑的全量,那时已经含了我
自己加的 56 条用例,被我误当成基线。真实基线是分叉点 `main@281769b` 的
`671 passed, 6 skipped`,净新增 **95**(6 个新测试文件 93 条 + overview 补 2 条),
`671 + 95 = 766` 可复算。更正过程也留在文档里,因为这个错法容易再犯。
- 顺带更正同节另一处:`**task_actor(task)` 调用点是 **14** 个(初稿写 13)。
- 补记唯一租约的反向验证**复验**结果:摘掉 `uq_lease_active_account` 后
恰好 2 个用例变红、重建后变绿;并记下结论 —— 该索引是**真承重**的,
仅靠「排除已租」的查询挡不住(第二个会话看不到第一个会话刚写入的租约)。
本次只改文档,无代码/迁移改动。
|
2026-10-07 13:25:33 +08:00 |
|
yml2213
|
05b2bf8579
|
docs(agents): 修正分支约定,补上这轮实践买的教训
- **删掉已作废的约定**「推完 main 再 `dev merge --ff-only main`」:
dev 现在与 main 已分叉(上面有未上线的账号自动分配功能),ff-only 必然失败。
改为:`main` = 已发布,新功能从 main 开 `feat/*`;要同步 dev 只能真合并且需显式确认。
- 新增一批实证教训:提交/推送的钩子要求(`pre-commit` 查的是 `ruff format`,
只跑 `ruff check` 会被拦)、测试库 `create_all` 不会 ALTER 已有表(症状
`Unknown column`)、`dev.sh` 会自动应用迁移导致切分支后应用起不来、
生产部署的完整流程与三项前置检查。
- 坑速查 4 → 8 条:新增 `pgrep -f` 自匹配白等、`set -e` + 管道过滤半途静默中止
(曾让镜像库变空)、`ruff check` ≠ 能提交;并补上 REPEATABLE READ 的具体应对。
- 据实补全本地库清单:先 grep 全仓库确认无引用,再查内容,确认
`douyu_login_prodclone` / `douyu_login_rehearsal` 是 S1–S7 演练留下的快照。
- 原有教训一条未删,整体按「干活顺序」重排(60 → 116 行)。
|
2026-10-07 13:20:33 +08:00 |
|
yml2213
|
d11fa1032d
|
feat(login-tasks): 按状态导出 CSV + 登录失败写进账号生命周期
一、失败入账号一生(此前完全缺失)
生产实测:`login_ok` 1,456 条、`relogin_failed` 73 条,但**没有 login_failed**,
而 login_tasks 里 attempt_status='failed' 有 247 条 —— 这 247 次失败在账号时间线上
一条痕迹都没有,「这账号为什么一直登录不上」在账号页上无从回答。
- 新增事件类型 login_failed,在 `_mark_attempt_failed` 里写入
- **被停止 / 被中断不写**:分型把「已停止/中断/服务重启」归到 interrupted,
那不是账号的问题 —— 否则运营按一次停止就能给几十个账号刷满假失败
- 事件带分型(captcha/credential/email_code/email_quota/network),
同时落进 detail.kind,以后可直接按「验证码失败」筛账号
- 幂等键 login_task:{id}:failed:{ts},与成功路径同构
二、失败分型下沉成共用 service
`classify_failure` 原在 automation/jobs/ck_relogin.py,手动登录也要用。
挪到 services/login_failure.py(job 改为 import,`ck_relogin.classify_failure`
这个名字保留,旧调用方与用例不受影响),并提供型别中文标签与
`is_account_fault` 判据 —— 与仓库既有的「service 不该依赖 job」分层一致。
三、按状态导出 CSV
- 新增 GET /api/login/tasks/export:不分页,导出当前筛选的**全部**匹配记录;
列:账号/状态/失败原因/失败分型/批次/最近尝试时间(UTC)/标签/归属客服
- 权限与筛选与列表**完全共用**,客服只导出自己名下账号
- 带 UTF-8 BOM(不加 Excel 打开中文表头是乱码),导出动作写审计
四、修一个口径不一致的真 bug
页面「失败 N」= failed + error 之和,但筛选只认 failed ——
所以截图里「失败 389」点进去只有 386 条(差的就是那几条 error);
「清空失败」同样漏删 error。现在把状态组收敛成一份 `_apply_status_filter`,
列表 / 导出 / 批量删除三处共用。
测试:684 passed, 6 skipped(本次新增 13 个);三条关键断言做了反向验证
(去掉 stopped 判断 / 失败组去掉 error / 去掉 BOM,对应用例均变红)。
|
2026-10-07 12:54:57 +08:00 |
|
yml2213
|
18bc7be4b1
|
feat(pool): 账号自动分配与回收(租约 + 补给链 + 独立工作台)
在既有「管理员分配 → 客服导入工作台 → 手动选号」之外**新增**一条
「客服选皮肤 + 数量 → 系统分配账号 + 按需补积分 → 用完自动收回」的路径。
旧路径只做并集与可选参数扩展,行为不变(有用例守着)。
- 归属与使用权分离:新增 account_leases(生成列 + 唯一索引,并发抢号由数据库裁决),
assigned_to 收窄为管理员/运营的维护责任;客服可见范围扩为「名下 ∪ 租用中」
- account_pool 服务:候选筛选、两梯队排序(先花快过期的 / 先选充值少的)、
供给与成本预估、租约领取/释放/回收
- 补给链:查余额 → 充鱼翅(单次 ≤800 元,超限拆单)→ 赠精英令 → 兑换;
运行期两道闸门:_skip_when(余额已够就别再花钱)、_depends_on(前置未成功就跳过)
- 转人工:鱼翅支付等 10 分钟未到账转 manual_required,租约先不还;
account_lease_reaper 兜 TTL 到期 / 漏释放 / 孤儿批次
- 运营流水:业务事件补 actor_id = task.created_by;账号详情页展示操作人、渠道
与租赁记录
- 新工作台:/douyu/pool + 菜单「账号分配台」(新建,不改造旧工作台)
- 修复 D1:官方补录的兑换事件漏写 subject_key(10,612 条主键为空),
会让 365 天同皮肤规则漏判 87% 的历史;补代码 + 回填脚本
- 迁移 20261007_0052;权限 account:pool_use / account:pool_admin
测试:766 passed, 6 skipped(改动前基线 727);ruff 与 tsc -b 干净。
设计稿与实施记录:docs/设计-账号自动分配与回收.md
|
2026-10-07 12:29:08 +08:00 |
|
yml2213
|
281769b440
|
feat(accounts): 账号列表按归属筛选,批量分配支持按筛选全选
原来只有 assigned_only(已分配),筛不出「未分配」,也看不到账号归谁。
新增 assign_status/assigned_user_id 可与标签、CK、搜索等叠加;
BatchAssign 支持 all_matching,「筛出未分配 → 一键分给某人」不必先勾选。
|
2026-10-07 09:13:54 +08:00 |
|
yml2213
|
5da12df16b
|
feat(account-check): 登录接口可选 wgapi / iframe
账号检测原来把 WgapiLoginAPI 写死在执行器里,收不到邮箱验证码时只能干等;
登录任务早就可选 iframe(邮箱验证走 iframe 接口)。默认仍 wgapi,旧行为不变。
|
2026-10-06 20:19:26 +08:00 |
|
 yml2213andCursor
|
39388c56b5
|
fix(permissions): 收回客服的 CK 检测重登和自动巡检入口
客服默认不再有 cookie:operate;自动巡检菜单改按 automation:view 显示。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-06 12:20:30 +08:00 |
|
 yml2213andCursor
|
f2157f67f7
|
feat(accounts): 账号列表按列排序,默认把还能换套装的排到前面
分页在服务端,点表头按全库排,不能只排当前这一页。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-05 18:50:43 +08:00 |
|
 yml2213andCursor
|
ab4c0a5422
|
fix(login): 重登成功后把 CK 检测结论写回账号
登录凭据快照盖掉了 ORM 账号,on_ck_verdict 读不到 ck_check_status,新 Cookie 被丢掉。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-05 18:20:10 +08:00 |
|
 yml2213andCursor
|
998998c390
|
feat(automation): 巡检和重登优先保住高价值账号的 CK
价值按套装和积分现算,不看标签。第 1、2 档 6 小时巡检一次,并先占重登名额。
Co-authored-by: Cursor <cursoragent@cursor.com>
|
2026-10-05 17:51:04 +08:00 |
|
yml2213
|
27e9107e38
|
fix(accounts): 修复鱼翅筛选参数类型为浮点数并优化错误提示
1. 后端将 min_gold 与 max_gold 参数及 Schema 类型从 int 改为 float | None,支持小数鱼翅(如 >0 快捷筛选的 0.01);
2. 修复前端 axios 拦截器在遇到 FastAPI 422 验证错误时未展开 detail 导致弹出 [object Object] 的问题;
3. 单元测试补充验证浮点数鱼翅过滤场景。
|
2026-10-05 11:36:46 +08:00 |
|
yml2213
|
512ea3db06
|
feat(accounts): 增加高价值资产一键筛选与高级自定义筛选功能
1. 后端新增高价值套装状态筛选(双套均可换/可换白日梦/可换萌兔/已拥有/高价值待救/未查资料),兼容 MySQL 原生 JSON 与 SQLite unicode 转义存储;
2. 后端新增多维资产与状态区间筛选(积分范围、鱼翅范围、宝典开通、电竞开通、角色绑定状态),并支持与批量打标签/删除联动;
3. 统计接口 accounts_summary 新增 high_value 聚合数据,毫秒级返回各类高价值资产数量;
4. 前端账号管理页面新增「高价值一键筛选」胶囊区(带数量徽标与激活状态,即点即滤);
5. 前端新增可折叠「高级自定义筛选」面板,支持套装状态下拉、积分快捷区间(≥300、≥600)、鱼翅快捷区间(>0、≥6)、宝典与角色绑定状态多条件组合;
6. 补充 6 个高价值筛选与资产区间单元测试,全量用例通过。
|
2026-10-05 11:23:48 +08:00 |
|
yml2213
|
19f1cbceba
|
fix(automation): 修复作业完成状态回写崩溃、统计字段对齐及表单布局挤压
1. 修复 DouyuBatchRunner 在 run 结束时私自关闭外部 Session 导致作业状态无法回写并死锁在 running 的问题(增加 close_db 参数);
2. 修复资料补全作业返回值映射,将 succeeded/failed 对齐到 valid/invalid,并在全库暂无待查账号时给出清晰说明;
3. 增强调度器 _run_one 边界防御:支持字段名兜底与 Session 崩溃时独立连接兜底回写,防止作业无限重复发起;
4. 修复巡检作业在全库无到期账号时的说明信息,如实展示最久检测时间与重检间隔;
5. 修复 AutomationPage 布局:移除外层 Form 的 layout=inline,Card 恢复 100% 满宽纵向排列,内部字段横向弹性自适应;
6. 修复未启用作业因残留历史 next_run_at 而错误显示「排队中」的问题。
|
2026-10-04 22:11:44 +08:00 |
|
yml2213
|
e5f2d2d5d2
|
fix(automation): 修回被我上一处改动挤坏的布局(5 张卡横排、无间距)
## 是我引入的回归
上一轮为了修「保存时存错作业」,我把各卡自己的 `<Form layout="inline">`
提到了外层统一包一层。但 `layout="inline"` **会把直接子元素排成一行** ——
于是 5 张 Card 横着挤在一起,还把原有的间距吃掉了(用户截图:一点间隔都没有)。
**同一个改动同时修了一个 bug、引入了一个 UI 回归** —— 说明我当时只验证了
"能存对作业",没看渲染结果。
## 修法
- 外层 Form 里加 **`<Space direction="vertical" size={16}>`** 纵向容器,
Card 恢复竖排、16px 间距
- 每张 Card `width: 100%`(不再由内容决定宽度)
- 字段行用 `<Space wrap size={16}>` 显式撑开,按钮组单独一段 `marginLeft: 16`
保留上一轮的关键修复:**仍然只有一层 Form**(`Form.Item` 靠 `[job.key, 字段]`
区分),保存按钮仍是显式 `onClick={() => saveJob(job)}` —— 不再依赖
"哪个 Form 的 onFinish 生效"。
并用注释把两个坑都写清了(各自包 Form 会存错作业;直接放 inline 子元素会挤成一行),
避免下次再踩。
## 验证
前端 build 通过。
|
2026-10-04 21:43:38 +08:00 |
|
yml2213
|
bb767bb84a
|
fix(automation): 保存配置时存错作业 —— 5 张卡共用一个 form 实例
## 现象(用户报的)
点「账号资料定时补全」的保存,提示却是**「积分到期补记 配置已保存」**。
## 根因:不是提示文案错,是**真的存错了作业**
`jobs.map()` 里**每张卡各包一个 `<Form form={forms}>`,但它们共用同一个
`Form.useForm()` 实例**。React 里多个 `<Form>` 绑同一实例时,
**`onFinish` 会被最后挂载的那个覆盖** —— 于是:
点第 1 张卡的「保存」→ 触发的是**最后一张卡**(积分到期补记)的 onFinish
→ saveJob(积分到期补记)
## 实际影响(不涉及数据损坏)
- `saveJob(job)` 读的是**那个 job 自己的**字段值 → 所以「积分到期补记」
只是被用它自己的值**重存了一遍**(无副作用)
- 而用户真正想保存的「账号资料定时补全」**根本没被保存** —— 静默失败
## 修法
把 Form **提到外面只包一层**(`Form.Item` 靠 `[job.key, 字段]` 天然区分各自的值),
保存按钮从 `htmlType="submit"` 改成显式的 `onClick={() => saveJob(job)}` ——
不再依赖"哪个 Form 的 onFinish 生效"这种隐含约定。
并在原处留了注释说明为什么**不能**各自包 Form(否则下一个人很可能又这么写)。
## 说明
前端这类"共用实例"的错误没有自动化测试兜住(仓库没有组件测试),
所以这次是靠用户点出来的。已在代码注释里写清原因,降低复发概率。
## 验证
前端 build 通过、lint 0 errors。
|
2026-10-04 21:40:46 +08:00 |
|
yml2213
|
9ac98dad89
|
fix(automation): 补全作业改用界面上的「同一账号多久重检」—— 之前那个字段对它无效
用户问「同一账号多久重检是啥意思」,顺着查发现**我的作业没在用它**。
## 「同一账号多久重检」是什么
**每个账号的体检周期**:一个账号这轮被查了,多久之后才重新进入候选。
批量扫描类作业的通用参数,配合另外两个决定"扫不扫得过来":
需要的能力 = 总账号数 ÷ 重检间隔
实际的能力 = 每轮条数 ÷ 运行间隔
**实际 < 需要 → 永远扫不完**(队列越来越长)。
按这个口径核对「CK 自动巡检」的默认值(生产 14,574 个持有 CK 的账号):
需要 14,574 ÷ 6 小时 = 2,429 个/小时
实际 200 条 ÷ 5 分钟 = 2,400 个/小时
**几乎精确匹配** —— 那两个默认值是校准过的,不用动。
## 但我的作业读的是 config 里的 refresh_days
于是界面上那个「同一账号多久重检」**对本作业是死的**(改了没反应)——
正是用户上一条批评过的同一类缺陷:「看起来能配、其实配的是不相干的东西」。
我在同一个问题上连着栽了两次(上一次是拿 `interval_seconds` 当调度频率算吞吐)。
改成 `job.interval_seconds`,与其它批量作业**同一套口径**;
默认 7 天(查过的账号 7 天后再查)。config 里的 `refresh_days` 整个去掉。
## 顺带
- 注册处显式写明两个参数:`interval_seconds=7 天`(重检)+ `run_interval_seconds=5 分钟`(调度)
- 用例改名并加注释,钉住"重检间隔必须来自界面那个字段"
## 验证
TZ=Asia/Shanghai **639 passed / 6 skipped**;ruff 全绿;前端 build 通过。
|
2026-10-04 21:33:50 +08:00 |
|
yml2213
|
55f899c615
|
feat(automation): 拆开「多久跑一次」与「同一账号多久重检」—— 调度频率终于可配
用户反馈:「单账号重检间隔(小时) 很别扭,直接设置多久跑一次、一次跑多少个更符合直觉」。
**对,而且比"别扭"更严重:调度频率以前根本改不了。**
## 问题:两个概念挤在一个字段里
`_run_one` 里原本写着:
job.next_run_at = finished + timedelta(seconds=JOB_INTERVAL_SECONDS) # 硬编码 300 秒
也就是不管界面上填什么,下一轮永远是「跑完 + 5 分钟」。
而界面那个「单账号重检间隔(小时)」对应的是 `interval_seconds` ——
它只被作业**自己的选行逻辑**使用("这行多久该重检"),**完全不参与调度**。
于是出现最坏的情况:**看起来能配调度,其实配的是一个不相干的参数**。
上一轮我还拿它算过吞吐("10 分钟一轮"),算错了 —— 已在调度器注释里纠正。
## 拆法
| 概念 | 字段 | 界面 |
| --- | --- | --- |
| **多久跑一次** | `run_interval_seconds`(迁移 0051,默认 300)| 「多久跑一次(分钟)」|
| 同一账号多久重检 | `interval_seconds`(不变)| 「同一账号多久重检(小时)」|
- `next_run_at = finished + run_interval_seconds`(有下限 30 秒,避免忙轮询)
- 接口:`GET` 返回新字段、`PATCH` 可改
- 前端:两个输入框分开,并把原来那段绕口的 tooltip
("它不决定调度频率 —— 调度周期见页面顶部说明")改掉 ——
那句话本身就是这个设计缺陷的自白
## 默认 300 秒
与改动前**实际行为一致** —— 不会因为这次迁移让已有作业突然变快或变慢。
想调快就自己填(比如「账号资料定时补全」填 5 分钟 + 每轮 400 条 ≈ 3 小时补完)。
## ⚠️ 顺带说明一个你截图里可能看到的现象
截图里「账号资料定时补全」显示的是 **0.5 小时 / 30 条** —— 那是我**最初的默认值**。
因为 `ensure_jobs` **只创建不更新**,我后面改成 10 分钟/400 **不会作用到已存在的行**。
这是刻意的(否则会覆盖运维的手工调整),但意味着**默认值改了要手动在界面上改**,
或者删掉那行让 `ensure_jobs` 重建。现在有了「多久跑一次」这个框,直接填就行。
## 测试(+3)
- 下一轮按**运行间隔**排,不是按「同一行重检间隔」(两个值刻意设成不同来钉住)
- 不填时默认 300 秒(保持旧行为)
- 过小的间隔被夹到下限
## 验证
TZ=Asia/Shanghai **639 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 21:30:13 +08:00 |
|
yml2213
|
aea8e4f73d
|
perf(automation): 调度器改为**作业之间并发**,慢作业不再挡住快作业
用户要求做这条(原本串行)。串行的代价现在很明显:新加的「账号资料定时补全」
一轮要 4 分钟以上,而源码注释里写的是「目前只有 CK 巡检,单轮约 20~40 秒」——
那 4 分钟里 CK 巡检/重登只能干等。
## 两处保护(缺一不可)
**① 在途守卫**(并发化之后最容易出的事故)
`next_run_at` 是作业**跑完之后**才更新的,所以一个跑得比 tick 还久的作业,
下一个 tick 看它仍然是「到期」状态 —— 没有守卫就会被**再派发一次**,
等于把速率翻倍(对斗鱼接口就是"并发放开",正是风控最敏感的地方)。
所以用 `_IN_FLIGHT` 记录在跑的作业 key,且**成功/失败都必须清标记**
(否则该作业以后再也不跑了)。两条都有用例钉住。
**② 独立 session**
SQLAlchemy 的 Session **不是线程安全的**。作业现在真的并行了,
所以每个作业在自己的 `SessionLocal()` 里跑,按 key 重新取行
(调度线程里那行可能已经过期)。共用 session 会互相踩事务。
## tick 不再被拖住
调度循环改用 `run_due_jobs(db, wait=False)`:**派发完就返回**,
tick 保持 5 分钟一轮,不再等作业跑完。
`wait=True`(默认)保留原语义:顺序执行、用调用方的 session 并返回 outcomes ——
测试与手动触发走这条,所以既有的 33 条调度器用例**一条没改**。
## ⚠️ 顺带纠正我自己上一轮的一个错误说法
我上一条说「interval_seconds=10 分钟」会生效 —— **不对**。`_run_one` 里写着:
下一轮固定在一个 tick 之后;interval_seconds 是「同一行多久重检」,
由作业自己在选行时用,**不参与调度**。
也就是下一轮永远是「跑完 + 5 分钟」。所以「账号资料定时补全」的真实节奏是
`4.4 分钟跑 + 5 分钟等 ≈ 9.4 分钟一轮` ≈ 2,553 个/小时 ≈ **3.05 小时**,
和之前估的 3.2 小时吻合。
## 测试(+4)
- `wait=False` 立刻返回、作业在后台真的开始了
- **在途作业不会被重复派发**(速率翻倍那个事故的守卫)
- 失败后仍会清在途标记、作业能再跑(别被永久卡死)
- `_release` 即使对不存在的 key 也不炸
## 验证
TZ=Asia/Shanghai **636 passed / 6 skipped**;ruff 全绿。
|
2026-10-04 21:24:35 +08:00 |
|
yml2213
|
0d60cfbec9
|
perf(automation): 账号资料补全提速到 ~3.2 小时(原默认值要 5 天多)
用户反馈「几个小时补充完成就行吧,几天也太慢了」——**对,是我把调度设得太保守**。
## 算一下原默认值有多离谱
30 个 / 30 分钟 = 60 个/小时 → 7,784 个要 **130 小时(5 天多)**
(我上一条说"3 天"还算乐观了。)
而**试水实测的吞吐**是 200 个 / 131 秒(并发 3)= **1.53 个/秒 ≈ 5,500 个/小时**。
**瓶颈根本不在查询速度,在我把调度频率设低了。**
## 新默认值:10 分钟一轮 × 400 个
400 个 × 0.65 秒 ≈ 4.4 分钟/轮
10 分钟一轮 → 2,400 个/小时 → 7,784 个约 **3.2 小时**
## ⚠️ 为什么不是更激进
查调度器源码发现一条**架构约束**(`run_due_jobs` 的注释写明):
作业串行执行 —— 目前只有 CK 巡检,单轮约 20~40 秒,远小于 tick
也就是**我的作业跑着的时候,CK 巡检/重登都得等着**(tick 5 分钟)。
所以一轮时长必须**明显小于间隔** —— 4.4 分钟 / 10 分钟是刻意留的余量。
一轮跑 6.5 分钟以上会把其他自动化作业饿住。
想更快有两条路(都需要你定):
1. 调大 `batch_size` —— 但**跑得越快越容易被风控**,而风控是全部账号一起受影响
2. 让调度器**并发跑作业**(目前是串行)—— 属另一处改动
## 验证
TZ=Asia/Shanghai **632 passed / 6 skipped**;ruff 全绿。
|
2026-10-04 21:17:02 +08:00 |
|
yml2213
|
fd578683b6
|
feat(automation): 新增「账号资料定时补全」作业 —— 分批慢跑,不撞风控
## 为什么不能一次性跑全量
生产约 **8,000 个**「CK 有效但没查过」的账号。一次性发起 ≈ **8 万次斗鱼请求**,
很容易触发风控 —— 而风控一旦触发是**所有账号一起受影响**。试水 200 个时
0.65 秒/账号看着很香(全量 86 分钟),但那正是危险的地方:**跑得越快越容易被封**。
## 新作业 `account_overview_refresh`
照「自动巡检」(`ck_health`)那套做,并复用它的两道保护。
| 项 | 默认 | 说明 |
| --- | --- | --- |
| `interval_seconds` | 30 分钟 | 多久一轮 |
| `batch_size` | 30 | 每轮查多少(约 20 秒跑完)|
| `concurrency` | 3 | 并发 |
| `refresh_days` | 7(config 可调)| 查过的多久后再查 |
| `enabled` | **False** | 默认关,由运维手动开 |
按默认值:8,000 个约 **3 天**补完,全程不打满斗鱼接口。
## 两类账号**分开**(用户明确要求)
- **没查过的**(`overview_queried_at IS NULL`)**优先**
- 查过的只在超过 `refresh_days` 后重新入选
否则"周期性刷新老账号"会永远挤掉"从没查过的新账号"。
MySQL 没有 `NULLS FIRST`,用 `ORDER BY overview_queried_at IS NULL DESC` 等价实现。
## 只查有效 CK
`ck_check_status == 'valid'`。失效的查了也是白查(只会多一条失败记录),
交给「CK 检测与重登」处理。另外沿用 `cookie_operable_filter()`(有 CK 且无在途尝试)、
排除人工状态(停用/归档)与「不参与自动化」的账号。
## 风控熔断(沿用巡检口径)
一轮里失败率 > 50%(且样本 ≥ 10)就**自己把自己关掉**,交给人确认。
消息里写明可能原因,否则运维只看到"失败率高"会一头雾水:
失败率过高,已自动关闭作业(大面积失败通常是**被风控**或接口抖动,请先确认再重开)
样本 < 10 不熔断 —— 1 个失败就是 100%,比例没意义。
## 查询记录
每轮都建真实的 `douyu_tasks` 行(payload 标 `source=scheduled_refresh`),
「哪个账号、什么时候、查出来什么」都有据可查,也能在任务列表里
区分「定时巡检」与「人工点查询」。账号上的 `overview_queried_at`
则是"下次该不该再查"的依据。
## 验证
- **镜像库实测**:`本轮查 3 个:成功 3 / 失败 0(剩 7784 个待查)`
- 本地 `TZ=Asia/Shanghai` **632 passed / 6 skipped**;ruff 全绿
- 新增 11 条用例:选择口径(只查有效 CK、没查过优先、refresh_days 可调、
人工状态排除、batch_size 限流)+ 熔断(高失败率关闭、正常不关、小样本不熔断)
+ 一条回归:计数不能走带 LIMIT 的查询(作业第一版就是这么挂的)
|
2026-10-04 21:14:16 +08:00 |
|
yml2213
|
8f231e6edf
|
fix(binding): 角色绑定只看 is_bound_role —— 「是否开通」与「是否绑定角色」是两件事
用户明确指出:**精英/电竞是否开通,和是否绑定角色没关系**。
## 我确实混了(虽然这次没触发)
`derive_from_bind_info` 里有个兜底:
if bound_role is True or bound_act is True: return BOUND
`role_bind_status` 这一列问的是「**角色**绑没绑」,而 `is_bound_act` 是
「**活动**维度」的标志 —— 拿它兜底就是把两件事混起来。
一旦有账号 `is_bound_act=True` 而角色未绑,就会报出"角色已绑定"的**错结论**。
## 生产数据证实了用户的说法
查已查询的 196 个账号:
电竞 opened=False 且 bind=bound → **187 个**
也就是**187 个账号绑了角色、但电竞手册压根没开通** —— 两者完全独立。
同时数据也证明 `is_bound_role` 本身就够用(不需要那个兜底):
判为 bound 的里面:精英 183 个全都有角色名(0 个没有)
电竞 187 个全都有角色名(0 个没有)
## 改法
去掉 `is_bound_act` 的参与,**只看 `is_bound_role`**:
is_bound_role 真 → bound
is_bound_role 假 → unbound
缺失 → None(保持 unknown,别把"没查到"写成"没绑定")
语义上不再有"把活动当角色"的路径。
## 测试
把两条**编码了旧行为**的用例改掉,并补一条钉住领域事实:
- 只有 `is_bound_act` 没有 `is_bound_role` → **None**(推不出角色结论)
- `{"is_bound_role": True, "manual_type": 0, "expire_time": "0"}` → **bound**
(绑了角色+没开通,两者独立,不该因为"没开通"而变成别的)
## 验证(本地)
TZ=Asia/Shanghai **620 passed / 6 skipped**;ruff 全绿。
|
2026-10-04 21:01:42 +08:00 |
|
yml2213
|
ccd05cba9c
|
feat(overview): 关掉和平小店查询 + 修电竞「已开通」不落库
## ① 和平小店总开关(默认关)
小店目前是**关闭状态**,但不跳过的话一键查询仍会对每个账号发小店请求,
结果 100% 失败 —— 实测 200 个账号里 `exchange_records_peace` 失败 **195 次(100%)**:
Livelink登录态校验失败
100% 意味着不是账号问题。这既淹掉真错误、又让人以为出故障。
新增 `douyu_config.peace_enabled`(迁移 `0050`,**默认 False**)。
关着时**完全跳过**小店的所有请求(绑定 + 兑换记录),并且**不记 errors** ——
「配置成关的」和「查失败」是两回事,混在一起会让 errors 失去意义。
将来小店恢复,在后台打开这个开关即可;打开后小店失败仍会**如实暴露**
(有用例专门钉住这一点,别把开关当成遮羞布)。
## ② 修:电竞手册「已开通」不会被一键查询更新
用户问"电竞到期时间为什么拿不到",查下去发现**两层**:
**第一层不是 bug**:那 200 个账号 `esports_opened` 全是 0(没开通电竞手册),
所以 `expireTime="0"`、没有到期时间可存。注意
**"电竞已绑定角色"≠"电竞手册已开通"**。实测 `opened_but_no_expire = 0`,没有异常。
**第二层是真 bug(不对称)**:
_refresh_elite_handbook → 写 account.elite_opened ✅
_refresh_esports_handbook → 不写 account.esports_opened ❌
`esports_opened` 只在"开通"任务里被设置,所以**详情页电竞的「已开通/未开通」
根本不会被一键查询更新**。已改成与精英对称(口径同 create_esports_qr:
`manual_type >= 1` 即已开通)。
## 测试(+2)
- 小店关着时:**不发**小店请求、**不查**小店绑定、**不记** peace 相关的 errors,
且结果里标明 `peace_skipped`
- 小店打开时:失败**仍然如实暴露**(反向用例,防止开关变成遮羞布)
## 验证(本地)
TZ=Asia/Shanghai **620 passed / 6 skipped**;ruff 全绿。
|
2026-10-04 20:59:17 +08:00 |
|
yml2213
|
631e33a4f6
|
fix(events): 撞唯一键是幂等命中,不该刷 WARNING + dump 整条 SQL
## 现象(用户从日志里发现的)
WARNING | 写入账号事件失败(account=60 exchange):
(1062, "Duplicate entry 'official_exchange:elite:60:20260120QYOOB_bag360:...'")
[SQL: INSERT INTO account_events (...) VALUES (...)]
[parameters: {...}]
## 分析:**不是 bug,是幂等机制在工作,但日志级别错了**
`emit()` 刻意**不做预检查**,直接 INSERT,靠 `source_ref` 的唯一索引去重;
撞了就 catch(SAVEPOINT 只回滚这一条)。所以:
- **数据是对的** —— 重复的被挡住,`exchange_events_added` 只计新写入的
- **日志是错的** —— 撞唯一键被当成异常打 WARNING,还把整条 SQL + 参数 dump 出来
而"撞唯一键"在这里是**预期内的正常情况**:一键查询会把斗鱼官方兑换记录补成事件,
同一个账号会被反复查询 —— 第二次开始每条记录都会撞。一次查询能刷几十屏,
既淹掉真日志、又让人以为出事了(用户就是因此来问的)。
## 修法
区分「唯一键冲突」与「真错误」:
- 确认是 **1062**(并带 `Duplicate entry` 字样)→ 降为 **DEBUG**,一句话说明"已存在,跳过"
- 其它完整性错误(如非空约束 1048)→ **仍然是 WARNING**
刻意**不把 IntegrityError 整个静音** —— 那会把真问题一起藏起来。
新增 `_is_duplicate_entry()` 只在两种信号都对上时才降级。
## 测试(+3)
- 重复写入返回 False,且**不产生**任何 "写入账号事件失败" 的 WARNING
(用 loguru 的临时 sink 真的抓日志来断言,不是看代码)
- 重复写入不会写进第二条
- `_is_duplicate_entry` 只认 1062,1048(非空约束)仍然返回 False
|
2026-10-04 20:35:24 +08:00 |
|
yml2213
|
b199428619
|
feat(account): 一键查询展示「角色绑定明细」—— 不只一个"已绑定"
用户看到绑定状态后问:「是不是在详情中增加详细的绑定信息啥的」。
`bind_info` 的返回里本来就有明细(`_parse_bind_info` 会解析出来),
之前只取了 `role_name` 一项。现在把运营真正要看的都带上:
- **角色名**(`role_name`)
- **区服 / 平台**(`area_name` / `plat_name`)
- 换绑冷却(`change_role_wait_time`)与可换绑时间(`can_change_time`)
实测(账号 61):
精英: 柿子洋音 / 微信 / 安卓(android)
电竞: 资跳 / 微信 / 苹果(iOS),可换绑 1788624000
面板新增「🎮 角色绑定明细」卡片,三个作用域并排;
冷却时间格式化成"无 / x 小时 / x 天 y 小时",
时间戳过 `formatUnixSeconds` 那套(`0`/空显示 `-`,不显示 1970)。
## 验证(本地)
TZ=Asia/Shanghai **615 passed / 6 skipped**;ruff 全绿;前端 build 通过。
|
2026-10-04 20:26:31 +08:00 |
|
yml2213
|
6b5271c983
|
feat(account): 一键查询补上「角色绑定状态」—— 以前从来没人查过,97% 显示"未知"
## 问题(用户:"这个做了吗?我看好像都是未知")
**只做了一半**:
- 列的拆分做了(S6):三列存在、写入收敛到 set_bind_status、迁移映射与运行时对齐
- 但**没有任何一处真的去查绑定状态**(grep -c bind_info douyu_runner_overview.py = 0)
那三列的值从来不是查来的,而是 S6 迁移时从旧的垃圾场列 bind_status
**能推的推、推不出的留 unknown**。只把明确陈述了绑定事实的旧值映射过来
(bind_confirmed → bound、bind_qr_generated → pending、game_not_bound → unbound),
其余一律 unknown —— 因为旧列被"查限兑商品""送礼物"这类**无关动作**覆盖过,
从无关动作推绑定状态就是造假。当时留 unknown 是对的,但结果是:
真实生产分布(15182 个账号)
精英 unknown 14683 (96.7%) 电竞 14730 (97.0%) 小店 15045 (99.1%)
## 修法:真的去查,用唯一写入者写回
一键查询新增三个绑定查询(接口都已存在,活动别名走后台配置):
bind_info / esports_bind_info / xpd_bind_info。
新增 derive_from_bind_info() 把返回推导成 4 态,**并做了实测校准**:
账号 列值(旧) is_bound_role 角色名 推导
51 unknown False (空) unbound
60 unknown True 唉清静的一生 bound
61 unknown True 柿子洋音 bound
62 unknown True FatalistIc bound
**4 个里 3 个其实已绑定,全被 unknown 盖住了。**
两个刻意的设计:
1. **推导不出来时返回 None,且不动原值** —— "没查到"和"没绑定"必须分开,
写 unbound 就是编造结论(小店未开通、返回里没有标志时实测就是这种情况)
2. **不产出 pending** —— 它描述"正在绑定/待确认"这种**进行中的动作**
(如刚生成过二维码),那是一次查询的副作用,不是查询能观测到的状态
3. need_bind_role **不是**"未绑定"的信号:实测**已绑定**的账号上它也是 1,
拿它当判据会把这 3 个已绑定的判成未绑
端到端实测(账号 51):
查询前: 精英=unknown 电竞=unknown 小店=unknown
查询后: 精英=unbound 电竞=unbound 小店=unknown ← 推不出来,正确保持原值
## 显示:把"没查过"和"查了推不出"分开
三处都改了(用户要求):全 unknown 时不再显示「精? 电? 店?」,
而是 **未查询**(点一键查询就有了)/ **未能判定**(斗鱼这次没给结论)。
## 验证(本地)
TZ=Asia/Shanghai **615 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
新增 9 条用例(含"need_bind_role 不是信号"的反向回归)。
|
2026-10-04 20:22:54 +08:00 |
|
yml2213
|
85a6186993
|
feat(account): 电竞手册到期时间落库与展示;迁移 0048 改名
## 背景(用户反馈)
「精英宝典和电竞手册已开通的话,应该增加时间展示,可以看到开通时间和过期时间」
- 精英宝典:开通时间来自事件;过期时间 `elite_expire_time` 本来就有
- 电竞手册:开通时间来自事件;**过期时间原来没有落库** ——
只在 `_refresh_esports_handbook` 的返回值里带着,界面上只能看到「已开通」
## 改动
- 新增 `accounts.esports_expire_time`(迁移 0049,与 `elite_expire_time` 对称),
刷新电竞手册时写回
- 详情页:**已开通才显示**「开通 x / 到期 x」两行小字;顺手删掉底部那段
重复的「开通时间」说明(已挪到每项旁边)
- 新增 `formatUnixSeconds()`:斗鱼的到期时间是**秒级时间戳**,
原 `formatTime()` 只收字符串;新函数对 `0`/空/负数一律显示 `-`
## 明确回答:历史开通记录**查不到**
实测官方兑换记录里**只有商品兑换、没有开通记录**(账号 61:精英 2 条都是套装)。
我们自己的 `elite_opened` / `esports_opened` 事件也只有上线后的。
所以历史上的开通时间拿不回来 —— 按用户说的「没有就算了」,
并把这点写进界面说明,别再让人去找。
## 测的时候抓出一个真 bug(又是 1970)
电竞到期时间的 `"0"`(斗鱼用它表示「没有到期时间」)被原样存成 `0`
→ 界面会显示 **1970/1/1**。已修(`0`/空 → `None`)并加回归用例 ——
这个坑在积分到期上踩过一次,这次差点重演。
## 迁移 0048 改名
`20261004_0048_overview_summary.py` → `20261004_0048_high_value_owned.py`:
上一轮把内容从「汇总」收敛成单一用途的 `high_value_owned`,文件名没跟着改。
revision id 不变、迁移状态不受影响;同时清了 `__pycache__`
(本仓库被过期 `.pyc` 咬过,症状是「改了没生效」)。
## 验证(本地)
`TZ=Asia/Shanghai` **608 passed / 6 skipped**;ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 20:17:05 +08:00 |
|
yml2213
|
6865b916ba
|
test(account): 一键查询 / 官方兑换记录 / 生命周期用例
围绕这几轮改动补齐用例(本文件共 53 条):
- **判定规则**:只看官方兑换记录;全局库存**永不**推出「该账号已拥有」
(回归用例 —— 正是第一版踩的坑);字段缺失是 None(未知)而不是 False(没有)
- **失败隔离**:单项外部调用失败不影响其余,且如实写进 errors
- **只读性**:一键查询不发事件、不改生命周期状态
- **官方记录补成事件**:带**真实兑换时间**(不是"发现它的时刻")、
幂等(重复查询不重复落)、无 commodityId 时跳过
- **排序回归**:「最近一次兑换」按事件时间,不是插入 id
- **时间归一化**:0 / "0" / 空 / 负数 → None(1970 那个 bug 的回归)
- **迁移安全**:不得在迁移里 commit(op.get_bind());加列前必须有存在性检查
(扫代码时**跳过注释** —— 注释里正解释着"不要这样做",第一版用例因此假红)
- **接口契约**:详情返回快照与查询时间、列表**不**带整份快照;
断言一律看 model_dump()(只看属性会假绿 —— AccountOut 有手写
@model_serializer,字段没列进去时属性有值、JSON 里没有)
另修两条**把 bug 当期望值**的既有用例:
1. 「最近一次兑换」原断言的是最后**插入**的那条(旧行为按 id 排),
改为真正最近的(按事件时间);
2. 按用户名查找夹具不可靠(make_account("key") 每次建新账号、库里一直累积),
改为按 id 精确查找。
|
2026-10-04 20:04:11 +08:00 |
|
yml2213
|
f416397cc8
|
feat(account): 详情页重排 + 列表高价值套装列 + 兑换记录点击预览
## 详情页重排(4 张卡,消除重复)
用户反馈「设计太乱、数据不好看、布局也乱」——根因是**同一份数据在一屏出现两次**:
积分/鱼翅/兑换余额在状态条和资产区各一次,角色绑定在状态条和信息表各一次。
新布局:**账号信息**(身份+状态+归属,一张 Descriptions)→ **资产与统计**
(数字只出现一次,上面"现在有多少"、下面"历史累计")→ **高价值套装与兑换记录**
(从最顶上移到这里,它是补充信息,不该抢主视觉)→ **账号时间线**。
零散的 `flex + gap` 统计行改成 `Row/Col` 栅格,列宽对齐。
## 高价值套装:颜色按**价值**口径,别标反
用户澄清的业务语义与直觉相反:
- **没兑换过** = 还能用来绑定兑换 = **高价值**(绿色「可兑换」)
- 兑换过 1 个 → 价值降低;两个都兑换过 → 价值最低(橙色「已兑换」)
判定依据是**斗鱼官方兑换记录**(账号级)。⚠️ 不能用 `query_limited_goods` 的
`count`/`storeStatus` —— 那是**全局库存**(同一时刻不同账号看到的都一样),
用它判"这个账号有没有"必然给错结论。这个结论是推翻过一版才得到的:
第一版拿相隔两个月的两条快照对比,把**时间差异**当成了**账号差异**。
## 列表
- 新增「高价值套装」列:🟢两个都可兑换 / 🟠某套装已兑换 / ⚪未查询
(「未查询」必须与「没兑换」分开,否则会把未知当成高价值)
- 「兑换记录」列只显示官方口径,**点击弹窗**看全部(按作用域分组,
含商品/积分/真实兑换时间/当时用的昵称与平台),不用进详情页
## 三个作用域的记录展示
面板与预览都按 精英宝典 / 电竞手册 / 和平小店 分三块展示,
每条带**真实兑换时间**与"使用昵称"(排查时有用)。
## 顺带修
`exchange_records` 从数组改成按作用域分组的对象后,面板同步适配;
`formatTime` 对 `0` 显示 `-`(斗鱼用 0 表示"没有这个时间",
直接格式化会显示成 1970/1/1)。
|
2026-10-04 20:03:52 +08:00 |
|
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
|
93d972d3fb
|
chore(memory): 协作约定 AGENTS.md
把用户明确表达过的偏好固化下来,避免每次重新摸索:
- **不要改一点就提交推送**,攒够一批或到里程碑再提交
- 新功能是「增加」不是「替换」,旧路径完整保留
- 不要重复展示同一份数据(库里有的字段让已有界面显示,查询去刷新它)
- 单项外部调用失败不整体失败,但要如实暴露
- 如实报告(含自己的错误与局限);结论要有验证,关键用例做反向验证
以及这个仓库踩过的坑:
- 别拿不同时刻的快照对比(曾把时间差异当成账号差异,得出错误规则)
- Pydantic 静默丢字段的三种变体(含手写 @model_serializer)→ 断言要看 model_dump()
- 用例可能假绿(缺 session.commit,MySQL REPEATABLE READ)
- 区间删除容易连带删行;docker compose run 会吞 stdin
另附仓库事实:分支/生产/镜像库/测试库/迁移编号/前端 tsc 严格项。
|
2026-10-04 20:03:25 +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
|
c2520613ca
|
fix(account): 修一键查询「一直循环」—— 快照字段加到了列表接口而不是详情接口
用户报:账号详情点「一键查询」会一直循环(Network 里刷一片 `overview` 请求)。
## 根因
上一轮我把 `overview_snapshot` / `overview_queried_at` 加到了**列表**接口
(锚点 `**bind_payload(acc),` 第一次出现的位置在列表构造器里),详情接口没有。
两层后果:
1. **详情不返回 `overview_queried_at`** → 前端 `marker !== before` 永远相等
→ 轮询到 120 秒超时。看起来就是"点一下一直转圈刷请求"。
2. **列表每个账号都带一整份 JSON 快照** → 15000 个账号的响应体积爆炸
(这个是我自己引入的回归,比第一个更隐蔽)。
## 修法
- 字段从**列表**移除、加到**详情**(列表刻意不返回,注释写明原因)
- 前端轮询**加自证**:第一次拿到响应时若没有标记字段,**立刻报错退出**,
而不是傻等 120 秒 —— 这类"字段没返回"的 bug 不该再以死循环的形式暴露
## ⚠️ 过程中我又踩了一个,被既有用例抓住
删那 4 行时**把 `**bind_payload(acc),` 一起删掉了**,导致列表不再返回绑定列
(`role_bind_status` 显示的是 Pydantic 默认值 `unknown`)。
`TestListViewKeyColumns::test_item_carries_key_columns` **立刻变红** ——
既有用例替我把这个真回归挡下来了。已恢复,并确认 diff 只剩预期的 3 行。
## 新增回归用例(+2,共 38)
`TestSnapshotExposedOnDetailOnly`:
- 详情**必须**返回快照与查询时间(不返回前端就会轮询到超时)
- 列表**不得**返回完整快照(每个账号一份 JSON,体积爆炸)
并做了**反向验证**:把"详情不返回字段"的 bug 注入回去 → 用例立刻红。
另外把 `TestListViewKeyColumns::_item` 按用户名查找改成按 id ——
`make_account("key")` 每次建新账号而库里一直累积,按用户名可能命中很早以前那个
(这次就让用例假红过一次)。
## 验证
`TZ=Asia/Shanghai` **590 passed / 6 skipped**;`TZ=UTC` 80 passed;
ruff 全绿;前端 build 通过、lint 0 errors。
|
2026-10-04 19:19:53 +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 |
|