Commit Graph
599 Commits
Author SHA1 Message Date
yml2213 1dd5c4a279 feat(huya): 精英宝典工作台开通数量上限提升至 10000 2026-10-17 12:49:29 +08:00
yml2213 82c59bb4af feat(huya): 协作台账勾选导出 CK 到剪贴板(单点权限 + 逐行审计)+ 工具区固定两行 2026-10-16 22:24:08 +08:00
yml2213 6ef74c8e45 refactor(huya): 协作台账 failed 状态展示名「兑换失败」→「抢购失败」(枚举值不变) 2026-10-16 22:09:21 +08:00
yml2213 fdc57752dc refactor(huya): 协作台账 done 状态展示名「已完成」→「抢购成功」(枚举值不变) 2026-10-16 22:06:39 +08:00
yml2213 e8abd60734 feat(huya): 协作台账状态机新口径(跳步/已确认绑定/兑换失败)+ 账号全生命周期快照(迁移 0059) 2026-10-16 22:00:34 +08:00
yml2213 aac264b639 feat(huya): 协作台账店铺字典·新建强校验·同买家多单 + uid 放开(迁移 0057/0058)
- 店铺字典(迁移 0057):大按钮点选 + 「新店」自定义自动登记;重命名可级联、
  停用/备注;列表行内改、详情、筛选条共用同一个 picker
- 新建强校验:订单编号/店铺/CK/礼包名/主页截图必填;贴 CK 即探针预览,
  读不到绑定角色禁止提交;礼包用工作台配置的活动 SID 现拉(浏览器缓存 + 强制刷新)
- 图片:绑定图停用、「绑定斗鱼号」下线(绑定关系改由探针读 bind_game_*),
  主页截图改为新建必填(弹窗内先传 OSS 再随表单落库)
- 同买家多单软识别:按「绑定账号 + 绑定角色」整批一次查询,列表「同买家」列 +
  详情分组,点单号直接切单
- uid 放开唯一性(迁移 0058):同一份 CK 可服务多单,原 409 冲突改为提示 + 审计
- 列表体验:序号列、搜索扩到 11 个字段、CK 状态列截断、新建弹窗放大并去滚动条
- 验证:pytest 932 passed / 6 skipped;接口冒烟 56 项全绿;tsc+eslint+build 通过
2026-10-16 21:29:08 +08:00
yml2213 df8c63b829 feat(huya): 外部 CK 协作台账(S1–S6)+ 图片上传能力
协作台账:新表 huya_external_orders 替代 Excel 多人协同表(设计见
docs/设计-虎牙外部CK协作台账.md),一条 CK 一行,所有人共享、改动留痕。

- 前端 pages/HuyaCollabPage.tsx + 菜单「虎牙 → 外部单协作」/huya/collab:
  内联编辑(带乐观锁 version)、图片缩略图、详情抽屉(字段表单 + 两个图片槽位 +
  操作时间线)、粘贴导入弹窗(带 TSV 预览)、批量/单条验证 CK、页头统计条、筛选
- 两套状态机:业务 pending→bound→recharged→scripted→done(旁路 void)、
  凭据 unverified/valid/invalid/disabled;唯一写入口在 services/huya_collab_service.py,
  字段级审计 + 整数 version 乐观锁(不用 updated_at:DATETIME 只到秒)
- CK 只读探针 check_user_bind_game_account(真机判据:响应非 None 且 status == 200),
  成功即覆盖写绑定快照 bind_game_account/bind_game_role/bind_game_channel(迁移 0056),
  失败只改凭据状态、不动业务状态;人工只能停用/恢复(POST /{id}/ck/status)
- 贴/换/重建 CK、TSV 批量导入(SAVEPOINT 单行隔离,部分失败不整体失败)、
  权限 huya:collab:view/edit/manage(删除只给运营)
- 孤儿图清理脚本 scripts/cleanup_orphan_file_assets.py(默认 --dry-run)

图片上传(协作台账截图用):OSS/本地双后端 + 缩略图变体 + /api/files 代理回读,
权限 file:upload / file:view。

验证:pytest 880 passed / 6 skipped;ruff format 全绿、ruff check 仅剩既有告警;
前端 tsc -b / eslint / vite build 通过;接口端到端冒烟 29 项全绿(TestClient,探针用假客户端);
探针真机冒烟 3 例(有效 CK=True、别人 uid=False、裸 cookie=False)。
2026-10-16 19:38:59 +08:00
yml2213 99e8643d83 chore: 版本号 0.3.0 -> 0.4.0
本次包含:虎牙开通充值按份数下单与记住上次选择、精英宝典与其他开通商品分块、一键查询 CK 失效标红、支付码改 qr.alipay.com 扫码内容、开通商品只保留可购买 spu 并清理历史死商品行。
2026-10-15 21:01:47 +08:00
yml2213 9a0d59825c fix(huya): 开通商品只保留真正可购买的 spu,并清理历史死商品行
真机 2026-10-15(HAR 10.15-精英徽章两个-1 + 逐个 spu 探测):面板里的「精英徽章 hy-5872824」是积分中心「赠送指定礼物得积分」那排的礼物 spu —— getGoodsInfoV5 仍返回 200+价格,但 listPayChannelV5 返回 401「您的设备环境暂不支持购买该商品」,所以下单必报「当前支付渠道不可用」(与数量无关)。真能买的是 hy-8343549 精英徽章 ¥1 / hy-8343551 精英令 ¥10。

- 内置候选换成可购买的 4 个:hy-8328015 宝典 / hy-8327356 币 / hy-8343549 徽章 / hy-8343551 令
- 刷新商品增加「能买到」闸门:详情 200 后再校验 listPayChannelV5,非 200 或渠道为空即剔除并记录服务端原话
- 下单流程渠道失败时透出服务端消息,只有渠道真不在可用列表才说「当前支付渠道不可用(可用:…)」
- 商品列表只认「最近一次刷新」那一套:原先按账号并集返回,别的账号旧刷新留下的礼物 spu 会和最新一套叠成 7 个选项
- 刷新时清理历史死行:本账号旧行 + 全局默认行(account_id 为空)+ 其它账号里已不在候选中的 spu 行
- 测试:401 商品被剔除、渠道失败透传、列表只认最近一套、刷新清理别处死行
2026-10-15 21:01:23 +08:00
yml2213 86ca2ed021 feat(huya): 开通充值支持数量与记住上次选择,精英宝典单独一块
- 支持「其他开通商品」按份数下单(payload.count → createOrder 的 item_count),数量上限取商品 buy_limit
- 商品选择/数量写入 localStorage,下次进来还是上次那套
- 精英宝典(限购主商品)从其他开通商品里拆出来单独一块,避免混在一个下拉里选错
- 测试:payload.count=3 进入 run_open_flow 且结果/文案带 x3
2026-10-15 21:00:31 +08:00
yml2213 c89df9ae5b feat(huya): 开通支付码多选账号时每个账号各出一个码并排展示
多选账号点「生成开通支付码」只出一个码:create_planned_tasks 把 create_recharge_order 也当成单笔任务(accounts[:1]),前端本来就支持多码并排,是被后端截断的。

- 后端:create_planned_tasks 不再截断 create_recharge_order(refresh_* 仍只建一个,商品快照是全局的),N 个账号 = N 个支付码任务,各自独立下单/监听支付
- 前端:支付码任务的并发取 min(10, max(并发, 账号数)),否则超出并发的码要等前面的码付完/超时才生成
- 前端:支付弹窗改成网格并排展示多个 150px 码,标题显示待扫码数;新增「生成失败(没有二维码)」点名区和「还有 N 个账号在排队生成」提示
- 前端:去掉每个码回包就自动弹支付弹窗的行为(多选时会反复把弹窗顶到最前);不做付完自动跳转
- 测试:多账号生成支付码 → 每账号一个任务,refresh_recharge_goods 仍单任务
2026-10-15 20:15:25 +08:00
yml2213 82294aba80 feat(huya): 一键查询查不到角色标记 CK 失效并前端标红
外部导入的 CK 过期时,角色走的 webActUI.checkUserBindGameAccount 整包没有 tRsp 回包(真机日志 newdata keys: ['']),而积分中心通道仍能返回 200,于是页面显示绿色「成功」+「角色信息接口无响应」,看不出 CK 已经挂了。

- 后端 _execute_query_all:bind_status is None 时打 result.ck_expired / ck_expired_reason、account.status=ck_expired,message 追加「疑似 CK 已失效,请重新登录或重新导入 Cookie」;业务码非 200(活动已下线等)不算 CK 失效,避免整批误标红
- 前端 HuyaElitePage:状态列红色「CK 失效」(tooltip 带原因)、最近操作消息红色、游戏名空值红色「角色未查到」;HuyaTasksPage 账号状态映射补 ck_expired
- 测试:角色接口无回包 → ck_expired + account.status;业务码非 200 不误标
2026-10-15 20:02:28 +08:00
yml2213 bf1e2e9361 fix(huya): 支付二维码改用收银台页面里的扫码内容 qr.alipay.com
真机复现(2026-10-15):上一版把二维码内容放成了**收银台页面地址**
(excashier.alipay.com/standard/auth.htm?payOrderId=…),那是 PC 版页面
(页面里写死 qrcodeWidth=150)。有些安卓机是用系统相机/浏览器打开的,就停在
这张 PC 页上——只显示左上角一个很小的二维码,不触发付款。

收银台页面里真正的扫码内容是隐藏字段 `J_qrCode`:

    <input name="qrCode" value="https://qr.alipay.com/upx03110ghym9vm37sx7255c" id="J_qrCode"/>

- huya_pay_link 新增 PayTargets / resolve_pay_targets() / extract_pay_qr_code():
  跟随网关 302 取收银台页面并解析出 qr.alipay.com 扫码码(约 40 字符)。
- 出码时写入 pay_qr_code,保留 pay_cashier_url 兜底;拿不到扫码码会打 warning。
- 前端二维码优先级:pay_qr_code → pay_cashier_url → 本站短链 → 原始地址。

验证:全量 pytest 772 passed / 6 skipped;用真机那张收银台页面跑解析得到
https://qr.alipay.com/upx03110ghym9vm37sx7255c;typecheck / eslint / ruff 通过。
2026-10-15 19:51:51 +08:00
yml2213 0b9a01ab3f feat(huya): 开通支付码复用未支付旧单 + 二维码改用支付宝收银台直达地址
- 限购商品(精英宝典 buy_limit=1)挂着未支付旧单时,真正先失败的是购买权限
  校验 checkUserBuyAuth(真机 703「已超过该商品的购买上限,下单失败~」),
  createOrderV5 根本不会执行;现在 checkUserBuyAuth 与 createOrderV5 两处失败
  都会回退 find_reusable_order,拿同一商品的未支付旧单继续 payOrderSubmitV5。
- 新增支付宝收银台直达地址解析(resolve_pay_entry_url):跟随网关 302 后的
  excashier.alipay.com/standard/auth.htm?payOrderId=…(93 字符,原始网关地址
  1616 字符)写入结果 pay_cashier_url;解析失败回落本站短链
  /api/huya/pay/<task_id>?k=<hmac>,后端跑在 localhost 时客户也扫得开。
- 前端 QRCode 显式 color="#000" bgColor="#fff":antd v6 暗色主题默认
  color=token.colorText、bgColor=transparent,白码落在白底容器里根本看不见。
- 支付成功后列表「二维码」列不再展示二维码(qrDoneLabel → 已支付/已开通)。

测试:pytest 767 passed / 6 skipped;typecheck、eslint、ruff 通过。
2026-10-15 19:28:26 +08:00
yml2213 f61284e808 feat(huya): 开通支付码支持多选批量 + 支付后自动确认开通
多选账号点「生成开通支付码」以前只有弹窗里最后一个码能被看到(每个账号一个
任务,前端只留一个 qrTask),实际只能一个账号一个账号地开。

- 前端:支付码弹窗改成列表 —— 一次列出该批次所有账号的二维码(账号/UID/
  商品金额/状态),未支付的排前面,可逐个扫也可一起扫;行点击也会打开对应批次
- 支付成功后自动确认开通:新增 EliteMixin._refresh_elite_open_after_payment,
  交付有几秒延迟,tag5 还没翻成 1 时重试(默认 4 次 × 3 秒),顺带取最新积分,
  并把 elite_opened/elite_open_time/elite_expire_time 写回账号
- 已支付或已开通的账号二维码自动从弹窗收起,只留一行「已开通」标签
- 开通时间优先用支付回调里的支付时间(比再去翻订单更准,也少一次请求)
- 补 3 条测试:重试到已开通、重试到上限、支付成功后任务结果与账号字段齐全

全量 746 passed / 6 skipped,前端 tsc 通过
2026-10-15 17:14:20 +08:00
yml2213 369361a62c style(huya): 精英宝典工作台表格表头与单元格统一居中
列宽按比例分配,所有列(含账号/游戏名/精英宝典/最近操作的双行内容)统一
align: center,积分不再单独右对齐 —— 之前左右跳着看很别扭。
2026-10-15 16:58:06 +08:00
yml2213 73536d62c8 feat(huya): 工作台补手机号/精英宝典列与一键查询
- 手机号列:外部 CK 的 `----手机号` 本来就落在 huya_accounts.game_phone,
  列表一直没展示;现在单独成列,并纳入搜索
- 精英宝典列:显示「已开通/未开通/未查询」+ 开通时间 + 有效期
  · 开通状态 = 积分中心 getRedeemNeedGoods 的 tag5(真机对照网页:1=已开通,
    0=未开通,此时 tag7 会列出开通套餐),拿不准时为 NULL(显示「未查询」)
  · 开通时间 = revenueWebUI.queryUserOrderList 里已支付的宝典订单 pay_time
    (按 SPU 白名单 + 订单名含「精英宝典」识别,未支付订单不计,取最近一次)
  · 有效期 = PlayMallUI.queryAllocation 页面配置的本赛季节点 tag10/tag11
    (2026-10-15 抓包:精英宝典福利季丨Q4,2026-10-09 ~ 2026-12-31 23:59:59),
    不写死索引,递归找带 tag10/tag11 的节点
- 新任务 query_all「一键查询」:勾选账号一次跑完角色绑定 + 宝典开通 + 积分,
  按部分失败如实汇总(某个接口挂了不影响其它结论),并把宝典状态写回账号
- 迁移 20261015_0053:huya_accounts 新增 elite_opened / elite_open_time /
  elite_expire_time(时间存 Unix 秒,与斗鱼 elite_expire_time 同约定)
- 开通商品按页面「赠送指定礼物得积分」那一排对齐成 6 款(去掉页面已不用的
  旧版精英宝典 hy-5879340),列表接口只返回账号级快照并按 spu 去重
- 建 WSS 会话失败(积分中心端点静默不回包)时回退 HTTP cdnws,
  不再让整批任务判失败
- 列表列宽改按比例分配,避免宽屏下被拉出奇怪的宽度;最近操作不再重复状态
- 新增 tests/test_huya_elite_query.py(10 条);同步迁移头版本断言

真机验证:#65 已开通(10-13 20:41:45,有效期 12-31)、#70 新开通
(10-15 16:37:37)判定与订单/网页一致
2026-10-15 16:53:30 +08:00
yml2213 7ca0f7ee28 fix(huya): 刷新开通商品不再依赖已下线的活动任务接口
真机(2026-10-15)实测 webActUI.getActTaskDetail(act 25135) 返回
601「活动已下线」,刷新开通商品第一步就失败,下拉框一直为空。

- 商品来源改为内置候选(积分中心页面「赠送指定礼物得积分」那一排,
  全部经 getGoodsInfoV5 真机验证 code=200):精英宝典 hy-8328015 ¥30、
  精英币 hy-8327356 ¥0.1、精英徽章 hy-5872824 ¥1、精英令 hy-5879346 ¥10、
  精英令*10 hy-5874269 ¥100、精英令*100 hy-5874270 ¥1000;
  旧版精英宝典 hy-5879340 页面已不再使用,从候选移除
- 活动任务详情降级为可选补充:返回非 200 只记 warning 并写入
  result.task_detail_error,不再让整次刷新失败;失败原因以商品详情为准
- 刷新时清掉该账号全部旧商品行(不限 sid),避免同名商品重复
- 列表接口只返回账号级快照并过滤 2026-07-04 的全局种子行,
  同一 spu_id 保留最新一行(原先 精英宝典 会出现两次)
- 前端:下拉标签带 spu 便于区分,按内置顺序排序,默认自动选中第一项
- 新增 tests/test_huya_recharge_goods_source.py(6 条):活动下线响应字节
  回归、下线不阻断刷新、活动商品仍会并入、失败原因归属、跨 sid 清理、
  列表去重与隐藏种子行(均已做反向验证)

真机验证:刷新成功 6 个商品,价格/库存与网页一致
2026-10-15 15:57:25 +08:00
yml2213 905a7ef22c fix(huya): 查询积分改走积分中心新接口(含成长等级)
旧活动接口 webActUI.getUserScore 对任何 sid 都返回 603「活动已结束」
(接口已下线,与工作台 SID/活动配置无关),刷新积分只会报错。

- 协议层新增两个 RPC(WSS + HTTP 双侧同签名):
  shopMiddleUI.getRedeemNeedGoods(tag2=当前积分、tag3=兑换门槛,
  兑换商品号 POINTS_REDEEM_GOODS_NUM=8315982,传错直接 500)
  与 GameBizUI.getGrowLevelUserInfo(成长值/成长等级/昵称/头像)
- 新增结构体 GetRedeemNeedGoodsReq / GetGrowLevelUserInfoReq,
  与 HuyaEliteWssSession 透传
- 服务层 _execute_query_points 主源改为积分中心接口,结果带
  points/redeem_threshold/grow_level/grow_value,并同步 account.points;
  接口无响应时才回退旧活动接口并如实报告其 603 返回
- 开通支付成功后的积分刷新同样改走积分中心接口
- 前端任务页积分结果优先读 points(兼容旧记录的 available_score),
  并在有成长等级时一并展示
- 新增 tests/test_huya_points_query.py:真机响应字节回归 + 请求参数
  字面量断言 + 服务层积分来源/回退语义(已做反向验证)

真机验证:HTTP 通道取到积分 280 / 门槛 20 / 成长等级 3
2026-10-15 15:32:46 +08:00
yml2213 b5d8d5b5d8 feat(huya): 缺网页设备态的 CK 回退 HTTP cdnws 通道
问题:外部 CK(只带登录态)导入工作台后,任何任务都被「Cookie 缺少网页设备态
(sdid/udb_deviceid/udb_guiddata/udb_anobiztoken)」直接拦下,查询角色/开通都用不了。

实测结论(2026-10-15,同一张外部 CK):
- 这些字段是 WSS 通道的门禁,不是业务必需 —— 同一条 CK 走 HTTP cdnws 可以正常查询
  角色、商品,并走完整条开通链路(商品/小店/渠道/协议/返利/试算/同意/购买权限)
- A/B 对照(同一账号只抽掉 sdid):含 sdid 服务端回包,去掉即完全不回包,说明 sdid
  是 WSS 的硬门槛,而 HTTP 通道不需要
- 因此改为按能力选通道:设备态齐全走 WSS(行为不变),缺设备态回退 HTTP 并记日志

- http_client:补齐积分中心链路(get_supplier_info / accept_hy_protocol /
  calc_order_promotion / play_mall_get_my_promotion / play_mall_query_allocation /
  get_user_profile / get_follow_wechat_status);下单与支付参数对齐 2026-10-13 抓包
  (appInfo.scene=7、订单 src/orderScene=4、orderType=6、jifencenter 标记、空点击序列、
  m-shop.huya.com/points.html 回调),并去掉 orderType 1→10 的重试循环(有重复下单风险)
- huya_runner_core:_activity_client/_shop_client 不再抛 COOKIE_INCOMPLETE,改为
  返回 HTTP cdnws 客户端(方法签名与 WSS 会话保持一致,任务侧无感)

验证:tests/test_huya_http_shop_flow.py(下单/支付参数与回调域名)、
tests/test_huya_runner_transport.py(通道选择:缺设备态走 HTTP、齐全仍走 WSS);
真机 HTTP 开通链路 8 步全通(末步服务端返回业务提示「已超过该商品的购买上限」,
说明通道可用而非被丢弃)。反向验证:关掉回退即复现原报错。全量 pytest 717 passed。
2026-10-15 15:21:51 +08:00
yml2213 a9d711a7e9 fix(huya): 结构体尾部字段容错解析(服务端新增 ZERO 字段不再打挂整个 RPC)
真机现象(2026-10-15,外部 CK 导入后查询角色):checkUserBindGameAccount 响应结构
尾部多了 tag6(ZERO 类型 0x6c),共享的 read_struct 读到非 STRUCT_END 直接抛
「期望 STRUCT_END, 实际 0x0c」,整个 RPC 返回 None —— 工作台「查询角色」永远失败。

- taf_protocol:read_struct 收尾改为按 TAF 语义跳过未建模的尾部字段(上限 32 个,
  防止解析错位时无限吞字节);新增 skip_to_struct_end()
- frame_decoder / shop_structs:抽出 decode_struct_bytes 与 ShopRawRsp.from_payload,
  WSS 与 HTTP 两条通道共用一份通用解码逻辑(wss_client 同步改用)

验证:tests/test_huya_struct_parse_resilience.py 用真机响应字节回归(含 tag6=ZERO 的
响应能解析出角色「泪雨全是谎言」);按 AGENTS.md 反向验证 —— 注掉
skip_to_struct_end() 即复现线上报错「期望 STRUCT_END, 实际 0x0c」,还原后恢复。
全量 pytest 通过。
2026-10-15 15:21:34 +08:00
yml2213 4ce30e5c26 feat(huya): 精英宝典工作台支持导入外部 CK
- 新增 POST /api/huya/workbench-accounts/import-cookies(huya:task + huya:import):
  外部 CK 复用现有解析入库后直接加入当前用户的工作台集合,导入完即可在工作台执行
  「生成开通支付码/绑定二维码/查询角色/刷新积分」等操作
- 新账号自动 assigned_to=导入者:客服的账号列表按归属过滤,不设归属会「导入成功但看不到」
- import_huya_cookies_detailed 返回账号对象供工作台复用,原 import_huya_cookies 行为不变
- 工作台账号卡片新增「导入外部 CK」入口(huya:import 权限控制)与导入弹窗

验证:新增 tests/test_huya_workbench_import_ck.py(入库、归属、工作台集合、重复导入幂等、
全脏数据不建记录);按 AGENTS.md 做了反向验证(关掉归属赋值后该用例变红,还原后恢复)。
前端 tsc -b 与全量 pytest 通过。
2026-10-15 14:56:58 +08:00
yml2213 f540dcfc29 feat(huya): 对齐积分中心开通链路(2026-10-13 抓包)
按 10.13「开通精英宝典/购买幸运币」抓包(m-shop.huya.com/points.html)对齐商城链路:

- WSS host 改为 launch.queryHttpDns 动态解析(失败回退抓包实测 host);握手 baseinfo
  只带 uid/guid/页面 UA/exp,Origin 与页面同源
- wsLaunch 改为带真实 lUid/sGuid;初始化补上 AUTH(0x0A) → getConfig →
  UpdateUserExps(0x17),并把 getConfig 下发的版本 map 原样回传(不回传则业务 RPC 无响应)
- 补齐新 RPC:getSupplierInfoV5 / acceptHyProtocolV5 / calcOrderPromotion /
  PlayMallUI.getMyPromotion|queryAllocation / wupui.* / huyauserui.getUserProfile
- 下单参数对齐:shopAppInfo.scene=7 与订单 src/orderScene=4 分离、orderType=6、
  bizType=5、gameCategoryId=507、extraParam.bizParams 标记 jifencenter、env 仅空点击
  序列、支付回调指向 m-shop.huya.com/points.html
- 开通链路抽到 services/huya_shop_flow.py,充值任务改为调用它

验证:新增 tests/test_huya_points_shop_protocol.py 与抓包逐字节比对(心跳、版本回传、
wsLaunch/getConfig/下单/支付帧与新 RPC 参数,共 10 条);按 AGENTS.md 做了反向验证
(把 jifencenter 改成 0 后该用例变红,还原后恢复)。ruff format --check 与全量 pytest 通过。
2026-10-15 14:56:30 +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 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 44b762ea8c fix(overview): 锁定核心高价值套装并修复未查询空值误判
- 高价值套装精确匹配「套装-兔兔白日梦」与「套装-狂战萌兔」,排除头盔/背包/手榴弹散件干扰
- 增加 is_suit_queried_expr,未查询或 NULL 账号严禁误判为双套均可兑换
- 修复前后端序列化空值保持,完善列表与详情页准入状态和判定依据展示
- 新增头盔排除与 NULL 统计防错回归单测,全量通过 689 个用例
2026-10-07 15:27:30 +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 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 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