Commit Graph
110 Commits
Author SHA1 Message Date
yml 0e29717c97 feat(weibo): 增加协作 CK 批量复制与商品图片代理 2026-11-12 14:12:50 +08:00
yml 31045e0f2e feat(weibo): 支持管理员强制转入工作台且不影响原使用人 2026-11-12 12:05:26 +08:00
yml 11b5a4d938 feat(weibo): 展示 CK 导入重复账号明细 2026-11-11 21:10:23 +08:00
yml 3f860c3272 feat(weibo): 支持工作台账号转入外部单协作 2026-11-11 20:32:42 +08:00
yml 50bb829101 feat(weibo): 增加工作台批量充值与批量导入 2026-11-11 20:11:35 +08:00
yml c9814ab4d2 fix(huya): 修复外部单积分复核与抢购结果误判 2026-11-11 18:41:16 +08:00
yml2213 b18e46d72d 完善虎牙外部单展示、状态恢复与礼包配置 2026-11-11 12:40:06 +08:00
yml2213 5f1d78e1e4 fix(huya): 精英宝典绑定二维码对齐最新抓包并支持配置 bind_gid 2026-11-09 21:01:42 +08:00
yml2213 2292864754 feat(huya): 精英宝典账号支持转入外部单协作 2026-11-09 11:39:39 +08:00
yml2213 6b2285913f feat(weibo): 增加账号分配与客服数据隔离 2026-11-08 18:30:51 +08:00
yml2213 c8b62e5267 feat(weibo): 增加工作台独占与快捷操作 2026-11-08 16:39:32 +08:00
yml2213 d566518478 feat(weibo): 拆分账号管理与个人工作台 2026-11-08 16:26:53 +08:00
yml2213 66b4f72994 feat(weibo): 增加外部 CK 单行复制 2026-11-08 14:13:01 +08:00
yml2213 ed15701392 feat(weibo): 修正绑定小程序码并增加账号批量操作 2026-11-08 13:17:29 +08:00
yml2213 79cfd4a634 feat(weibo): 增加外部单协作台账 2026-11-08 11:41:54 +08:00
yml2213 a0afa430ec feat(weibo): 增加外部 CK 账号 CRUD 与生命周期 2026-11-08 11:07:23 +08:00
yml2213 b7f34df04d feat(weibo): 增加微博微豆基础自动化 2026-11-07 20:33:15 +08:00
yml2213 efa8bf2a46 feat(huya): 增加协作单礼包字典与名称编辑 2026-11-07 16:54:01 +08:00
yml2213 973042288f feat(douyu): 增加协作单礼包字典与权限控制 2026-11-06 15:09:50 +08:00
yml2213 4c1aff47da feat(douyu): 高价值套装改用官方 storehome 三态判定
高价值套装的准入判定改为只认官方精英宝典 storehome 里账号级的
status:0=可兑换、1=已达兑换上限,商品缺失/接口异常=unknown。
兑换记录和积分记录只作历史展示,不参与是否拥有的判定。

原来的 high_value_owned(JSON 名单)无法区分「确认可兑换」和
「官方接口没返回商品」,也无法表达部分失败。改为两套独立状态 +
检查时间(迁移 20261106_0065),旧快照保守回填:status 缺失的
一律 unknown,不猜。

CK 判定失效时在数据层作废结论(降为 unknown),不是展示层降级。
官方 status 是账号维度的,但 CK 失效那段时间账号可能被别人手工
登录并兑换(这些号共享给客服),缓存里的「可兑换」就不成立了;
只在展示层降级的话,CK 恢复后那条陈旧的「可换」会重新变绿。
error(查不到)不作废 —— 查不到不等于账号出问题了。

高价值结论有自己的时钟:资料刷新除了看 overview_queried_at,还看
high_value_checked_at。两者会脱钩(商品接口单独失败时资料时间被刷新、
高价值时间停在原地),少了这一条这类账号会永久停在旧结论。作废的
结论按 6 小时慢速重试,但「从来没查到过」不因此重选 —— 那是接口对
该账号稳定失败,每轮都重选只会反复撞墙。

商城商品列表改用精英宝典的 manualID 拉取,不再寄生在电竞手册分支里
(否则没配电竞手册的配置就永远拉不到商城,三态全部停在待确认)。

筛选拆成三类「查不到」:unqueried(未查资料,旧口径不变)、
high_value_never(从未读到商城)、high_value_unknown(查过但结论
未知/过期)。之前第二类无处可寻 —— 它既不满足 unqueried 也不满足
high_value_unknown。
2026-11-06 12:25:08 +08:00
yml2213 2c87deed78 feat(douyu): 外部单协作 CK 复制统一账号密码格式 2026-10-23 10:32:29 +08:00
yml2213 84d7c8e0a4 feat(douyu): 精英宝典工作台支持 CK 检测与失效重登 2026-10-23 10:21:15 +08:00
yml2213 f074b75871 feat(douyu): 支持批量导出用户名 2026-10-23 09:52:47 +08:00
yml2213 bf2f120fdc feat(douyu): 外部单协作支持复制 CK 2026-10-23 09:29:53 +08:00
yml2213 96c4f41674 fix(assignments): 未分配列表不再混入 CK 失效账号
分配池统一为:持 CK、CK 未判失效、未被停用/归档。新增 assignable_only 参数贯穿账号列表、批量分配与分配概览;前端列表与一键全部分配使用同一口径。
2026-10-22 11:01:55 +08:00
yml2213 2f5a63ae27 feat(auth): 新增手机号与 DCAuth 设备认证登录
- 登录链路改为用户名 + 密码 + 短信验证码 + DCAuth v2 设备签名
- 新增 login_sms_codes / user_devices / device_challenges 三张表,迁移 0063/0064
- 设备绑定只比对 deviceId + publicKey,每账号一个 active 设备,管理员可重置
- 保留未绑定手机号账号的密码登录兜底,绑定后强制安全登录
- 登录页与用户管理页支持验证码、DCAuth 轮询、手机号绑定与设备重置
2026-10-21 22:00:03 +08:00
yml2213 71574908ff feat: 斗鱼外部单协作台账与店铺实时同步 2026-10-20 20:11:35 +08:00
yml2213 c778ae9a09 feat: 账号管理支持按账号名批量选号打标签,并支持复制账号名 2026-10-20 17:31:39 +08:00
yml2213 3a6649a054 feat: 重做协作台账状态机并支持充值核账 2026-10-19 16:06:02 +08:00
yml2213 781b27e63a feat: 协作台账支持归档并精简状态列 2026-10-19 13:04:27 +08:00
yml2213 21f7ee3f3f feat(huya): 礼包名默认空白,改用「常用礼包」备选按钮
新建协作单以前会把「上次选的礼包」自动填进礼包名(localStorage
huya.collab.lastGift)。连开几单同一个礼包是省事,但每开一单都顶着上一次的
选择,稍不留神就按错礼包建单;而且这个自动值会被草稿自动保存吃进去,
下次打开又「恢复」出来。

- 新建弹窗**不再预填礼包名**:删掉 lastGift 的读写,字段默认空白。
- 草稿恢复只带订单号 / 店铺 / CK / 截图 / 备注,礼包名留空;
  hasDraftContent() 也不再认「只有礼包名」的草稿(否则被旧逻辑自动填过的
  那份草稿会一直弹「已恢复上次未提交的草稿」)。
- 新增 GET /external-orders/gifts/frequent:按台账里的使用次数排序
  (次数相同看最近一次使用),新建与补资料弹窗在字段上方渲染最多 6 个按钮,
  只显示**当前礼包缓存里还有的**(活动换 SID 后老礼包会整批下架)。
- 新增 5 例测试,并做了 3 次反向验证(去掉次数排序 / 改成升序 / 去掉空名过滤)。

补资料弹窗的初值仍是「这一单自己的礼包」,详情抽屉编辑也不动 —— 那不是
「上次选的」。
2026-10-18 10:53:04 +08:00
yml2213 c441b6c663 feat(huya): 批量查询拆成「查积分」与「查兑换记录」两个按钮
前端工具栏原来一个按钮同时查两件事,拆开后每次只查一半,所以重点在
「没查的那一半不能被当成没查到」:

- 接口加 scope(both / score / records),默认 both 保持原行为;
  只查积分时不产生判定(空串「未判定」,与 unknown「无法判定」分开),
  也**不会**自动改业务状态(连判定都没有,凭什么改)。
- 列表的积分与判定各取「最近一条查到过它的」快照:点「查积分」不再把
  整页判定徽标冲成未判定,点「查兑换记录」也不再清空积分;但新的
  unknown 仍会盖掉旧的 success(抖动结果不许藏)。
- 结果弹窗 / 积分列 Tooltip / 生命周期时间线都按 scope 显示,
  不再把「本次没查」显示成「积分 -」「兑换记录 0 条」。
- 新增 12 例测试(TestCollabActivityScopeSplit),并做了 4 次反向验证
  (去掉判定分支 / 判定跟随最新 / 积分跟随最新 / 删掉响应字段),全部确认变红。

顺带修掉文档漂移:AGENTS.md 的迁移 head 早就过期(main 实际
20261016_0059、dev 20261008_0053),并补上「两侧已分叉成两个 head、
合并 dev 前必须先 alembic merge」这条;设计文档补 §11.9。
2026-10-17 23:32:51 +08:00
yml2213 fcc2fbdc09 feat(huya): 协作台账支持待补资料一键建单与表单暂存 2026-10-17 14:21:36 +08:00
yml2213 82c59bb4af feat(huya): 协作台账勾选导出 CK 到剪贴板(单点权限 + 逐行审计)+ 工具区固定两行 2026-10-16 22:24:08 +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 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 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 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 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 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
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 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 eec42aa8a7 feat(check): 账号检测新增「按账号选择」—— 旧的手动输入与逻辑完整保留
按反馈是**增加**而不是**替换**:旧路径一行未动,新路径另起一条。

## 两套来源并存

| | 手动输入(旧,默认) | 选择账号(新) |
| --- | --- | --- |
| 来源 | 粘贴文本 | 库里账号的凭据(不用人工粘贴) |
| 条目 `account_id` | **无** | 有 |
| 结果落账号时间线 | **否**(保持旧行为) | 是,新事件类型 `account_check` |
| 批次 / 并发 / 重试 / 代理 / 导出 zip | 完全不变 | 完全不变 |

**旧路径的「不发事件」是刻意的**:粘贴进来的账号没有归属,凭空造一条没有账号的
事件只是污染时间线。`parse_account_check_lines`、`export_line`、zip 分组全部未动,
有用例专门钉住这一点。

## 「选择账号」支持筛选(按反馈补充)

不是给一个扁平下拉,而是先按条件缩小范围再决定选法:

- 筛选:**标签 / 生命周期状态 / CK 健康**(复用账号列表同一套 `_filter_accounts_query`,
  两处口径不会不一致)
- 选法:手动多选(选项随筛选刷新)**或**「按筛选全选」(含列表里没显示出来的)

服务端支持 `select_all_matching + 各筛选条件`,与账号列表批量操作的字段形状一致。
可见性沿用账号域口径:客服只能检测自己名下的账号(越权 id 直接 403)。

## 结果表加账号回链

「选择账号」模式下条目带 `account_id`,账号列变成可点链接 → 直接跳到账号详情,
结果也在那里的时间线上。

## 排查中修掉两个 bug(都不是测试发现的)

**① `registry.create` 漏传 `account_id`**
它构造 `AccountCheckItemState` 时没把 `account_id` 从输入复制过去。
后果:批次照跑、结果照显示、zip 照导出,**事件永远不发且没有任何报错**。
是「跑一次真实检测再查事件表」才发现的(查出来 0 条)。已补,并加了用例。

**② 终态判断写成「键存在」而不是「非空」**
`_run_one` 置 `running` 时会显式传 `finished_at=None`,而判断写的是
`"finished_at" in updates` → **中间态也发事件**,于是时间线上记的是
`账号检查:running`;真正的终态再发时撞同一个幂等键被**静默丢弃** ——
结果是「只有一条,但记错了」,比没有更糟。

改成判非空。此处暴露了一个**假绿的用例**:我原来的「中间态不发事件」用例
没传 `finished_at=None`(与真实调用不一致),所以放过了这个 bug;
而且它还缺 `session.commit()`,在 MySQL REPEATABLE READ 下根本看不到
另一个会话刚提交的行 —— 双重假绿。两处都修了,并做了反向验证
(改回旧判断 → 用例立刻红)。

## 验证

- `TZ=Asia/Shanghai`:**520 passed / 6 skipped**(新增 17 条检测模式用例)
- `TZ=UTC`(检测 + 迁移)20 passed;ruff 全绿;前端 build 通过、lint 0 errors
- **真实端到端**:用真实库账号起批次 → 检测跑完(斗鱼返回「账号已认证」)→
  事件确实落库;清掉脏数据后用**与 `_run_one` 完全相同的调用序列**复验:
  两次 `_set_item` 只产生一条事件,且 title 是
  `账号检测:账号已认证`、`status=account_verified`(不再记成 running)

注:验证时真实触发了几次斗鱼登录检测(开发库那两个账号),结果都是「已认证」。
2026-10-04 17:34:58 +08:00
yml2213 5ae5d92978 feat(account): 兑换记录列 + 修掉 source_ref NOT NULL 导致的事件静默丢失
三件事合成一次提交(按反馈不再一改一提交)。

## ① 账号列表加「兑换记录」列

次数 + 最近一次(商品名 + 日期),填上列表右侧的空档。

数据源是**账号自己的事件流**(`account_events` 的 `exchange`),不是 `douyu_tasks`
的 JSON —— 后者是执行日志,S5 起业务事实已经在事件里了。

性能上刻意**每页只跑两条聚合查询**(不是每行一条 N+1):
一条分组算 `count` + `max(occurred_at)`(走 `(account_id, event_type, occurred_at)`
复合索引),一条用窗口函数 `ROW_NUMBER()` 取每个账号最新那条的商品名。
实测 6 个账号 3 条兑换事件,聚合与「最近」都正确。

「兑换 」前缀在前端剥掉(列头已经叫「兑换记录」,再带前缀是重复)。

## ② 修掉一个静默丢事件的 bug(排查中发现的)

**现象**:开发库上「状态变迁 / 分配 / 标签 / 备注 / CK 清除」这些事件**一条都没有**,
而带幂等键的事件(业务事实、`ck_invalid`、`relogin_*`)照常写入 ——
时间线看起来有数据,只是恰好缺了某一类,**极难发现**。

**根因**:`source_ref` 是**可选**幂等键(模型与迁移文件都是 `nullable=True`,
`emit()` 对没有天然幂等键的事件就传 `None`)。但早期那版迁移
(重编号成 `0041` 之前的 `0040`)建的是 **NOT NULL**,而我后来只改了
**迁移文件**、没管**已建好的库** —— Alembic 只记版本号,不会重放已应用的脚本。
`__pycache__` 里那个 `20261002_0040_*.pyc` 就是旧版留下的痕迹。

**为什么静默**:`emit()` 按设计吞掉任何异常(事件是旁路,不能影响主流程),
只记一条 warning,调用方也从不检查返回值。已实测复现:
`emit(...)` 返回 `False` + warning,实际落库 `0` 条。

**修法**:迁移 `20261004_0045` 改回可空(`MODIFY`,**唯一索引保留**,幂等,
生产若跑过旧版也一并修好)。修复后实测:`emit` 返回 `True`、落库 1 条。

## ③ 补一条能防「未来」同类问题的守卫

`tests/test_migrations.py` 原本只比对「表/列/索引在不在」,**不比对可空性**。
已加:迁移建出的列**不允许比模型更严**(模型说可空、库里 NOT NULL)。

只查这个**危险方向**:反方向(模型 NOT NULL、库里允许 NULL)是无害的宽松,
历史遗留里有 28 处,一并报出来只会制造噪声、逼人修没收益的东西。

> 这条守卫**有边界**,已在代码注释与设计文档 §3.5.1 写明:
> 它在**全新库**上跑整链,只能防「未来的迁移建错」,**防不了已有库的历史漂移**。
> 已验证:临时禁用 `0045`,该用例仍通过(全新链本来就是对的)。

## 验证

- `TZ=Asia/Shanghai`:**503 passed / 6 skipped**(+3 条兑换列用例)
- `TZ=UTC`(迁移 + 列表相关)45 passed;ruff 全绿;前端 build 通过、lint 0 errors
- 真实开发库:迁移已应用(head `20261004_0045`),
  `source_ref` 现在 `IS_NULLABLE=YES` 且唯一索引仍在;
  不带幂等键的事件确实能落库;兑换聚合返回值正确

注:验证时在开发库那个有 CK 的账号上留了 3 条 `exchange` 演示事件
(兔兔白日梦 / 冠军鸡腿 / 烟花),是为了让列有数据可看,随时可删。
2026-10-04 17:25:40 +08:00
yml2213 885a780bd3 feat(account): S6 账号页重构 —— 列表关键列 + 详情页生命周期状态卡
S6 的展示层部分(数据模型部分见上一个提交)。目标:客服打开账号页要能一眼回答
「这账号能不能用、要不要我处理、账上还有多少」。

## 账号列表:加关键列 + 三个筛选

新增四列,**全部直接来自 `accounts` 行**(不 JOIN、不聚合 —— 列表是全站最热接口):

| 列 | 内容 |
| --- | --- |
| **CK** | 有无 CK / 正常 / 已失效 / 检测异常 + 最近检测时间 |
| **绑定** | 精/电/店 三个作用域各一个小标签(✓ / … / ✗ / ?) |
| **积分 / 鱼翅** | 两行小字 |

新增筛选:**CK 健康**、**角色绑定**(三个作用域任一命中即算)。
`AccountOut` 相应补齐 `has_cookie` / `ck_check_status` / `ck_checked_at` /
`points` / `gold_balance` / 三组绑定状态与标签。

## 账号详情页:生命周期主页

顶部新增**生命周期状态卡**,把之前散落的信息聚成一眼可读的几格:

- **生命周期状态**:状态标签 + 不参与自动化标记 + 变更时间与原因
- **CK**:有无 / 健康 / 最近检测时间
- **角色绑定**:精/电/店三格
- **账上还有多少**:积分 / 鱼翅 / 兑换余额
- **归属与标注**:分配客服(下拉)+ 标签(就地编辑)—— 之前详情页只能看不能改

业务台账补上**登录成功次数**与**最近登录**(`stats` 里本来就有,只是没展示)。
时间线已有分类筛选,生命周期操作入口(停用/归档/恢复/自动化)S3 已加。

## ⚠️ 顺带修掉一个静默 bug

`bind_payload()` 的字段名按**作用域名**拼成了 `elite_bind_status` / `peace_bind_status`,
而 schema 声明的是 `role_bind_status` / `xpd_bind_status`。Pydantic 默认
`extra='ignore'`,于是多出来的键**被静默丢弃**、声明过的字段保持默认值 ——
**接口永远返回 unknown 而不报任何错**(这正是它躲过前面几轮测试的原因)。

修法:前缀改为由**列名**推导(`SCOPE_PAYLOAD_PREFIX`),并在模块导入时自检
「载荷前缀 == 列名前缀」。另加两条用例:`bind_payload` 的键必须都被
`DouyuTaskAccountOut` / `AccountOut` 声明(这是能抓到这类错的那种用例)。

顺带发现我自己之前那条 payload 用例**把错误的前缀当成了预期**,一并修正。

## 自己踩的坑

详情页加客服列表时又把 `useEffect` 写在了早期 `return` 之后 ——
和本次会话前面修 `LoginTasksPage` 的是同一个错。eslint(`react-hooks/rules-of-hooks`)
当场抓住,已上移。**`tsc` 与 `vite build` 都不会报这个**。

另外:列表关键列那处编辑我曾以为已生效(脚本打印了成功),实际没落到文件里 ——
后面是靠「读回文件确认」和用例失败才发现。已改为每次都读回核对。

## 验证

- `TZ=Asia/Shanghai`:**497 passed / 6 skipped**(S6 前半 490 → +7)
- `TZ=UTC`(相关文件)通过;ruff 全绿;前端 build 通过、lint **0 errors**
- **真实开发库端到端**:列表与工作台接口都返回了真实值
  (`has_cookie=True` / `ck_check_status=valid` / `role_bind_label=未知`),
  修 bug 前这两处返回的是被丢弃后的默认值

未做:`bind_status` 旧列待 S8 drop;保留策略里「状态派生类查询」已可下探到 7 天档(待确认)。
2026-10-04 16:45:39 +08:00