From 6ef74c8e452a8721d5968d7e287ce28c14cdd23c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 16 Oct 2026 22:09:21 +0800 Subject: [PATCH] =?UTF-8?q?refactor(huya):=20=E5=8D=8F=E4=BD=9C=E5=8F=B0?= =?UTF-8?q?=E8=B4=A6=20failed=20=E7=8A=B6=E6=80=81=E5=B1=95=E7=A4=BA?= =?UTF-8?q?=E5=90=8D=E3=80=8C=E5=85=91=E6=8D=A2=E5=A4=B1=E8=B4=A5=E3=80=8D?= =?UTF-8?q?=E2=86=92=E3=80=8C=E6=8A=A2=E8=B4=AD=E5=A4=B1=E8=B4=A5=E3=80=8D?= =?UTF-8?q?=EF=BC=88=E6=9E=9A=E4=B8=BE=E5=80=BC=E4=B8=8D=E5=8F=98=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/设计-虎牙外部CK协作台账.md | 29 +++++++++++---------- tests/test_huya_collab_orders.py | 2 +- web/backend/routers/huya/collab.py | 4 +-- web/backend/services/huya_collab_service.py | 6 ++--- web/frontend/src/pages/HuyaCollabPage.tsx | 16 ++++++------ 5 files changed, 29 insertions(+), 28 deletions(-) diff --git a/docs/设计-虎牙外部CK协作台账.md b/docs/设计-虎牙外部CK协作台账.md index b9482f4..da649c2 100644 --- a/docs/设计-虎牙外部CK协作台账.md +++ b/docs/设计-虎牙外部CK协作台账.md @@ -202,7 +202,7 @@ CK 自带完整生命周期(导入 → 验证 → 使用/失效 → 重贴/重 ``` pending ─人工确认─▶ bound ─充值─▶ recharged ─上脚本─▶ scripted ─抢购成功─▶ done │ │ │ │ │ - └─────────────────┴──────────────┴─────────────────┴──────────────────┴──▶ failed(兑换失败) + └─────────────────┴──────────────┴─────────────────┴──────────────────┴──▶ failed(抢购失败) │ │ │ │ │ └─────────────────┴──────────────┴─────────────────┴──────────────────┴──▶ void(作废) ``` @@ -214,7 +214,7 @@ pending ─人工确认─▶ bound ─充值─▶ recharged ─上脚本─▶ | `recharged` | 已充值 | 充值完成后手动切(可跳步);写 `recharged_at` | 人工 | | `scripted` | 已上脚本 | 脚本上完后手动切(可跳步);写 `scripted_at` | 人工 | | `done` | **抢购成功** | 人工切;**或**「已上脚本」的单查积分/兑换记录命中礼包后自动切(§5.7) | 人工 / 系统判定 | -| `failed` | **兑换失败** | 人工切;**或**「已上脚本」的单查兑换记录**没有**这个礼包后自动切(§5.7) | 人工 / 系统判定 | +| `failed` | **抢购失败** | 人工切;**或**「已上脚本」的单查兑换记录**没有**这个礼包后自动切(§5.7) | 人工 / 系统判定 | | `void` | 已作废 | 任意流程内状态可进;`void_reason` 必填 | 人工 | **规则**(2026-10-16 用户拍板的口径,旧版"只能一步一步往前"作废): @@ -391,7 +391,7 @@ CK 失效了业务上可能仍然有效。列表上只做**显式提示**(红 | 情况 | verdict | 业务状态 | | --- | --- | --- | | 兑换记录里有这一单的礼包(名字互相包含即算命中,`gift_matches_records()`) | `success` | 「已上脚本」→ 自动「抢购成功」`done` | -| 接口通了,但记录里没有这个礼包 | `failed` | 「已上脚本」→ 自动「兑换失败」`failed` | +| 接口通了,但记录里没有这个礼包 | `failed` | 「已上脚本」→ 自动「抢购失败」`failed` | | 接口没通 / 这单没填礼包名 | `unknown` | **不动**(数据不够,不许瞎判) | **四条边界**: @@ -543,7 +543,7 @@ CK 失效了业务上可能仍然有效。列表上只做**显式提示**(红 - **状态**:Tag 展示,行内"下一步"按钮推进(带二次确认); - **CK 状态**:Tag + 一键"验证"; - 筛选:店铺 / 负责人 / 业务状态 / CK 状态 / 关键词(订单号、绑定账号、角色、uid); -- 页头统计条:`待处理 N / 已确认绑定 N / 已充值·已上脚本 N / 抢购成功 N / 兑换失败 N / 已作废 N`(`done` 的展示名 2026-10-16 由「已完成」改为「抢购成功」,枚举值与库里数据不变)。 +- 页头统计条:`待处理 N / 已确认绑定 N / 已充值·已上脚本 N / 抢购成功 N / 抢购失败 N / 已作废 N`(`done` / `failed` 的展示名 2026-10-16 由「已完成」/「兑换失败」改成「抢购成功」/「抢购失败」,枚举值与库里数据不变)。 > 交互细节(2026-10-16 用户拍板):**店铺是点选按钮组**(列表里点单元格弹出同一组按钮, > 筛选条里也是),不是下拉;**新建时不用选负责人**——默认就是当前登录账号 @@ -802,12 +802,12 @@ CK 失效了业务上可能仍然有效。列表上只做**显式提示**(红 | 面 | 落地 | | --- | --- | -| 状态机(服务层) | 新增终态 `failed`(兑换失败);`bound` 标签改「已确认绑定」;`is_forward_status()` 允许往前跳步;`_assert_can_enter_bound()` 改成 CK + 绑定角色 + 主页截图;`void`/`failed` 不动时间戳 | +| 状态机(服务层) | 新增终态 `failed`(抢购失败);`bound` 标签改「已确认绑定」;`is_forward_status()` 允许往前跳步;`_assert_can_enter_bound()` 改成 CK + 绑定角色 + 主页截图;`void`/`failed` 不动时间戳 | | 批量改状态 | `set_status_batch()` + `POST /external-orders/status/batch`(逐行 SAVEPOINT 隔离,上限 50) | | 全生命周期 | 新表 `huya_external_order_metrics` + 迁移 `20261016_0059`;`query_order_activity()` / `query_activity_batch()`;`POST /{id}/activity/query`、`POST /activity/query`、`GET /{id}/metrics`;列表回填 `last_score/last_metric_at/last_verdict/last_metric_message` | -| 前端 | 状态筛选与汇总卡片加「兑换失败」;工具栏「批量改状态(下拉选目标)」+「批量查积分/兑换」;行操作加「查积分 / 兑换记录」「改状态…」;列表加「积分」列(积分 + 判定徽标 + 时间,全文进 Tooltip);详情抽屉加「账号全生命周期」时间线 + 逐条命中的兑换记录 | +| 前端 | 状态筛选与汇总卡片加「抢购失败」;工具栏「批量改状态(下拉选目标)」+「批量查积分/兑换」;行操作加「查积分 / 兑换记录」「改状态…」;列表加「积分」列(积分 + 判定徽标 + 时间,全文进 Tooltip);详情抽屉加「账号全生命周期」时间线 + 逐条命中的兑换记录 | | 测试 | 新增 `TestCollabStatusBatch`(4 例)、`TestCollabActivityLifecycle`(9 例);`tests/test_migrations.py` 的 `HEAD_REVISION` → `20261016_0059`;全量 `pytest` 944 passed / 6 skipped | -| 接口冒烟 | `/tmp/smoke_collab_api.py` 补到 **67 项全绿**:批量跳步 + 时间戳补齐、单行查询命中→自动「抢购成功」、非 `scripted` 只记快照、时间线倒序、列表回填、批量查询判定→自动「兑换失败」、`failed` 状态筛选、批量逐行隔离、回退仍要 `rollback`、删单级联删快照 | +| 接口冒烟 | `/tmp/smoke_collab_api.py` 补到 **67 项全绿**:批量跳步 + 时间戳补齐、单行查询命中→自动「抢购成功」、非 `scripted` 只记快照、时间线倒序、列表回填、批量查询判定→自动「抢购失败」、`failed` 状态筛选、批量逐行隔离、回退仍要 `rollback`、删单级联删快照 | **这一轮踩到/定下的点**: @@ -817,17 +817,18 @@ CK 失效了业务上可能仍然有效。列表上只做**显式提示**(红 反过来,回退必须清掉后面阶段的时间戳(`_sync_stage_timestamps()` 两个方向都管)。 3. **自动改状态只在 `scripted` 生效**,其它状态只写快照 + 在结论里说清原因 —— 自动改状态是最容易"越权"的地方,口径必须窄。 -4. **`unknown`(判不了)不写状态**:接口抖一下就把单子判成"兑换失败",比不判更糟。 +4. **`unknown`(判不了)不写状态**:接口抖一下就把单子判成"抢购失败",比不判更糟。 5. **`statusAction` 弹窗合一**:单行的「改状态 / 回退 / 作废」共用一个 Modal, 靠 `mode: 'set' | 'rollback' | 'void'` 分支(`set` 允许选全部状态含 `failed`/`void`, `rollback` 只在流程链里选),避免三个弹窗三套校验。 6. **尚未真机验证**:`getUserScore` / `getUserPrizeRecords` 两个接口只做了假客户端单测, 真机字段名(`available_score` / `records[].prize_name`)还没拿真 CK 跑过一次; - 真机跑通前不要把"兑换失败"当成定论(先用单行查询看一眼 `raw`)。 -7. **展示名调整(同日第二刀)**:`done` 的展示名由「已完成」改成**「抢购成功」** - —— 只改 `ORDER_STATUS_LABELS` / `STATUS_META` / 下拉与卡片文案, - **枚举值仍是 `done`**(库里数据、接口参数、审计 detail 全不变,无需迁移)。 - 「判定结论」里的 `success` 仍叫「兑换成功」(那是"查到兑换记录"的结论,与状态名分开)。 + 真机跑通前不要把"抢购失败"当成定论(先用单行查询看一眼 `raw`)。 +7. **展示名调整(同日第二刀)**:`done` 的展示名由「已完成」改成**「抢购成功」**、 + `failed` 由「兑换失败」改成**「抢购失败」** —— 只改 `ORDER_STATUS_LABELS` / `STATUS_META` / + 下拉与卡片文案,**枚举值仍是 `done` / `failed`**(库里数据、接口参数、审计 detail 全不变,无需迁移)。 + 注意「判定结论」(verdict)那三个标签仍是「兑换成功 / 兑换失败 / 无法判定」: + 它说的是"兑换记录里查到没有",与业务状态名是两个概念,徽标放在列表「积分」列与生命周期时间线上。 --- @@ -838,7 +839,7 @@ CK 失效了业务上可能仍然有效。列表上只做**显式提示**(红 | 与 `huya_accounts` 出现"两份 CK 谁是准的" | §2.3 已写死:本表 CK 只在协作语境有效,不自动同步;需要跑任务时人工导入 | | 探针接口被风控 | 只做人工触发、串行、带间隔;不做定时轮询 | | 积分 / 兑换记录查询被风控 | 同上:只手动触发(单行 + 勾选批量,串行带间隔),不做定时轮询(§5.7) | -| 自动改状态误判(接口抖动 → 判成"兑换失败") | 三态判定:取不到数据一律 `unknown` 且不动状态;只有 `scripted` 的单才自动改;每次查询留快照,人工可回退(`rollback`) | +| 自动改状态误判(接口抖动 → 判成"抢购失败") | 三态判定:取不到数据一律 `unknown` 且不动状态;只有 `scripted` 的单才自动改;每次查询留快照,人工可回退(`rollback`) | | 活动换 ID(SID 过期)→ 礼包列表空白 | 礼包 SID 一律读 `huya_config.sid`(工作台「虎牙配置」可改,见 §11.4);空列表时结论直接提示去改 SID | | 自由文本的"绑定斗鱼号"没法校验 | 先按文本存(Excel 里就是文本);能拿到结构化 uid 时补 `bind_douyu_uid` | | 图片越攒越多 | §6.4 孤儿清理脚本 + `file_assets` 台账已有 `size_bytes` 可统计 | diff --git a/tests/test_huya_collab_orders.py b/tests/test_huya_collab_orders.py index bc3be45..79d135c 100644 --- a/tests/test_huya_collab_orders.py +++ b/tests/test_huya_collab_orders.py @@ -1734,7 +1734,7 @@ class TestCollabActivityLifecycle(TestCollabBase): assert order.scripted_at is not None assert order.done_at is None - # 从「兑换失败」回流程内要显式回退 + # 从「抢购失败」回流程内要显式回退 with pytest.raises(collab.CollabError) as excinfo: collab.set_status(self.db, self.user, order, "scripted") assert excinfo.value.code == "status_not_forward" diff --git a/web/backend/routers/huya/collab.py b/web/backend/routers/huya/collab.py index e8a05da..ca9110c 100644 --- a/web/backend/routers/huya/collab.py +++ b/web/backend/routers/huya/collab.py @@ -341,7 +341,7 @@ def batch_query_external_order_activity( """勾选多行查询积分 + 兑换记录(**串行带间隔**,防风控)。 每行都会落一条生命周期快照;「已上脚本」的单按查询结果自动改状态 - (查到礼包 → 抢购成功;没查到 → 兑换失败),其它状态只记录不动状态。 + (查到礼包 → 抢购成功;没查到 → 抢购失败),其它状态只记录不动状态。 """ try: orders = collab.list_orders_by_ids(db, req.ids) @@ -479,7 +479,7 @@ def query_external_order_activity( """查这一单的积分 + 兑换记录(单行「查询」按钮):写一条生命周期快照。 「已上脚本」的单按查询结果自动改状态:查到这一单的礼包 → 抢购成功; - 接口通了但没查到 → 兑换失败;接口没通 → 不动(数据不够不许瞎判)。 + 接口通了但没查到 → 抢购失败;接口没通 → 不动(数据不够不许瞎判)。 """ try: order = collab.get_order(db, order_id) diff --git a/web/backend/services/huya_collab_service.py b/web/backend/services/huya_collab_service.py index daaefde..d2adc4b 100644 --- a/web/backend/services/huya_collab_service.py +++ b/web/backend/services/huya_collab_service.py @@ -78,7 +78,7 @@ ORDER_STATUS_LABELS = { # 抢购跑到结果:查兑换记录能确认(自动改)也可以人工切(2026-10-16 用户把「已完成」改叫这个) STATUS_DONE: "抢购成功", # 旁路终态:抢购后查兑换记录,没查到这一单的礼包 → 判失败(人工确认后可作废或回退重试) - STATUS_FAILED: "兑换失败", + STATUS_FAILED: "抢购失败", STATUS_VOID: "已作废", } @@ -124,7 +124,7 @@ def is_forward_status(from_status: str, target: str) -> bool: - 流程链内:目标阶段序号 **≥ 当前** 就是向前(含原地重复设置,幂等); - `void`(作废):任意流程内状态都能进; - - `failed`(兑换失败):流程内任意状态都能进(兑换记录查出来没有就是没有); + - `failed`(抢购失败):流程内任意状态都能进(兑换记录查出来没有就是没有); - 从 `void` / `failed` 回到流程内:**必须** rollback(这两步是"要人工再确认一次"的)。 """ if target == STATUS_VOID or target == STATUS_FAILED: @@ -1268,7 +1268,7 @@ def query_order_activity( | 情况 | verdict | 业务状态 | | --- | --- | --- | | 兑换记录里有这一单的礼包 | ``success`` | 「已上脚本」的单 → 自动「抢购成功」 | - | 接口通了,但记录里没有这个礼包 | ``failed`` | 「已上脚本」的单 → 自动「兑换失败」 | + | 接口通了,但记录里没有这个礼包 | ``failed`` | 「已上脚本」的单 → 自动「抢购失败」 | | 接口没通 / 这单没填礼包名 | ``unknown`` | **不动**(数据不够,不许瞎判) | 只有 ``已上脚本`` 的单会被自动改状态 —— 抢购是上脚本之后的事,其它阶段去改属于误判; diff --git a/web/frontend/src/pages/HuyaCollabPage.tsx b/web/frontend/src/pages/HuyaCollabPage.tsx index 83f1e4e..95fd0cf 100644 --- a/web/frontend/src/pages/HuyaCollabPage.tsx +++ b/web/frontend/src/pages/HuyaCollabPage.tsx @@ -51,12 +51,12 @@ const STATUS_META: Record = scripted: { label: '已上脚本', color: 'purple' }, // 抢购成功(枚举值仍是 done):从「已上脚本」查到兑换记录 → 自动进这一步 done: { label: '抢购成功', color: 'success' }, - // 旁路终态:抢购后查兑换记录没查到这一单的礼包 - failed: { label: '兑换失败', color: 'red' }, + // 旁路终态:抢购后查兑换记录没查到这一单的礼包(枚举值仍是 failed) + failed: { label: '抢购失败', color: 'red' }, void: { label: '已作废', color: 'error' }, }; -/** 判定结论的配色(生命周期时间线用) */ +/** 判定结论的配色(生命周期时间线用):与「业务状态名」分开,这里说的是"兑换记录里查到没有" */ const VERDICT_META: Record = { success: { label: '兑换成功', color: 'success' }, failed: { label: '兑换失败', color: 'error' }, @@ -70,11 +70,11 @@ const STATUS_ORDER_OPTIONS: { value: HuyaCollabStatus; label: string }[] = [ { value: 'recharged', label: '已充值' }, { value: 'scripted', label: '已上脚本' }, { value: 'done', label: '抢购成功' }, - { value: 'failed', label: '兑换失败' }, + { value: 'failed', label: '抢购失败' }, { value: 'void', label: '已作废' }, ]; -/** 「回退」能选的目标:只在流程链里选(要进「已作废 / 兑换失败」走「改状态」或「作废」入口) */ +/** 「回退」能选的目标:只在流程链里选(要进「已作废 / 抢购失败」走「改状态」或「作废」入口) */ const FLOW_STATUS_OPTIONS = STATUS_ORDER_OPTIONS.filter( (option) => option.value !== 'failed' && option.value !== 'void', ); @@ -1172,7 +1172,7 @@ export default function HuyaCollabPage() { return ( - {/* 卡片用 flex 平分:状态多了一个「兑换失败」,固定 span=4 会换行参差不齐 */} + {/* 卡片用 flex 平分:状态多了一个「抢购失败」,固定 span=4 会换行参差不齐 */} @@ -1505,7 +1505,7 @@ export default function HuyaCollabPage() { )} )} - 每次查询都会追加一条快照(只追加、不修改);「已上脚本」的单会按结论自动改成「抢购成功」或「兑换失败」。 + 每次查询都会追加一条快照(只追加、不修改);「已上脚本」的单会按结论自动改成「抢购成功」或「抢购失败」。 {!detail.has_cookie && (