From 56e718a65de9ceab221e0c7001ad74296c538387 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 2 Oct 2026 17:51:39 +0800 Subject: [PATCH] =?UTF-8?q?fix(admin):=20=E6=98=BE=E5=BC=8F=E5=8D=95?= =?UTF-8?q?=E5=8F=B7=E6=A3=80=E7=B4=A2=E4=B8=8B=EF=BC=8C=E5=85=B6=E5=AE=83?= =?UTF-8?q?=E7=AD=9B=E9=80=89=E4=B8=8D=E5=BE=97=E6=8A=8A=E6=AD=A3=E5=9C=A8?= =?UTF-8?q?=E7=9C=8B=E7=9A=84=E5=B7=A5=E5=8D=95=E4=BB=8E=E5=AE=9E=E6=97=B6?= =?UTF-8?q?=E8=A1=A5=E4=B8=81=E9=87=8C=E5=88=A0=E6=8E=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 修的是什么 实时补丁的判定链里,「状态 / 接单人员 / 日期范围」这些可证伪条件排在关键词护栏之前, 任一不成立就先返回 mismatch(允许删行),于是第 156 行那条「有关键词就不删行」的护栏 在带筛选时根本执行不到。 d6523635 只在 UI 侧补了一半:搜显式单号时清空 状态/赠送阶段/验收模式, 但**没清接单人员、分类、日期范围**,而 workerId 与 createdRange 同样会先返回 mismatch。 所以「单号 + 接单人员」或「单号 + 日期范围」仍会把行删掉。 实测(真实函数): 单号 + 状态筛选 -> mismatch(删行) 单号 + 接单人员筛选 -> mismatch(删行) 单号 + 日期范围 -> mismatch(删行) ## 修法 只对「显式单号检索」提前短路为 unknown(只原地更新,不插不移): if (isExplicitOrderNoSearch(filters.keyword)) return 'unknown' 刻意**不**推翻既有设计:模糊关键词(产品名/昵称等)仍保留「状态可证伪即移除」的语义, 因此原来锁定该语义的用例继续通过。 顺带把 isExplicitOrderNoSearch 合并到 utils:此前面板与 AdminLayout 各有一份规则, 重复两份正是上次 tab 名单漏改那类问题的温床。 ## 边界(重要) **本次修复不覆盖「无任何其它筛选、只按单号搜索」的场景。** 实测该场景下判定本就是 unknown、行不会被删(状态原地更新、分页与统计一致), 因此线上反馈的「状态变化后搜不到、必须整页刷新」若发生在无筛选条件下,另有他因,尚未定位。 --- .../pages/admin/panels/WorkOrdersPanel.tsx | 11 +---- .../src/utils/admin-realtime-work-orders.ts | 23 ++++++++++ .../tests/admin-realtime-work-orders.test.ts | 46 +++++++++++++++++++ 3 files changed, 70 insertions(+), 10 deletions(-) diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 5e2dccb9..979ea773 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -95,6 +95,7 @@ import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs' import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination' import { getAdminUsername, hasAdminPermission, hasAdminRole } from '@/utils/admin-auth' import { formatAdminDateTime } from '@/utils/admin-time' +import { isExplicitOrderNoSearch } from '@/utils/admin-realtime-work-orders' import { DeadlineCountdown, formatAcceptanceMode, @@ -1068,16 +1069,6 @@ export default function WorkOrdersPanel() { await doSubmit() } - function isExplicitOrderNoSearch(text: string) { - const trimmed = text.trim() - if (!trimmed) return false - return ( - /^WO[0-9a-zA-Z]{4,}$/i.test(trimmed) || - /^\d{6,}(-\d{6,})?$/.test(trimmed) || - /^OD[0-9a-zA-Z]{4,}$/i.test(trimmed) - ) - } - function searchOrders() { const trimmed = keywordInput.trim() setKeyword(trimmed) diff --git a/apps/frontend/src/utils/admin-realtime-work-orders.ts b/apps/frontend/src/utils/admin-realtime-work-orders.ts index 35381e51..1ee9d0ce 100644 --- a/apps/frontend/src/utils/admin-realtime-work-orders.ts +++ b/apps/frontend/src/utils/admin-realtime-work-orders.ts @@ -99,12 +99,34 @@ function matchesCreatedRange( return true } +/** + * 是否为「显式单号检索」——工单号 / 平台单号 / 纯数字单号。 + * + * 两处依赖它,必须保持一致,所以放在这里而不是面板内部: + * 1. 面板搜索时据此清空排他的状态/阶段筛选(避免流转后单子脱靶); + * 2. 实时补丁判定据此短路为「只原地更新」,绝不把正在看的单删掉。 + */ +export function isExplicitOrderNoSearch(text: unknown): boolean { + const trimmed = String(text || '').trim() + if (!trimmed) return false + return ( + /^WO[0-9a-zA-Z]{4,}$/i.test(trimmed) || + /^\d{6,}(-\d{6,})?$/.test(trimmed) || + /^OD[0-9a-zA-Z]{4,}$/i.test(trimmed) + ) +} + export function evaluateAdminWorkOrderRealtimeMatch( snapshot: unknown, filters: AdminWorkOrderListFilters, ): AdminWorkOrderRealtimeVerdict { if (!isRecord(snapshot)) return 'unknown' + // 显式单号检索要排在最前面:用户就是在找这一单,无论它流转到什么状态都必须留在视野里。 + // 只做原地更新,不插入也不移除 —— 否则状态流转(或前端对状态/接单人员/日期口径的复刻偏差) + // 会把正在看的单从结果里删掉,表现为「状态一变就搜不到,必须整页刷新」(生产已复现)。 + if (isExplicitOrderNoSearch(filters.keyword)) return 'unknown' + // 先跑所有「可以证伪」的条件:任何一条不满足就是明确的 mismatch(允许从列表移除) // // 状态必须用 rawStatus 比:快照的 `status` 是运营态(拼单母单 open 有份额时会被显示成 @@ -151,6 +173,7 @@ export function evaluateAdminWorkOrderRealtimeMatch( // - 赠送阶段:gift_ready 是"冷却到点"的实时阶段,含 NOW() 判定; // - 打手等级:只在服务端按接单人与拼单参与者计算; // - VIP 待补图:混在状态列表里的虚拟值,判定依赖 acceptance_json 多个字段与 NOW()。 + // 注:显式单号检索已在函数开头短路,走不到这里。 if (filters.acceptanceMode) return 'unknown' if (vipEvidencePendingActive) return 'unknown' if (filters.keyword) return 'unknown' diff --git a/apps/frontend/tests/admin-realtime-work-orders.test.ts b/apps/frontend/tests/admin-realtime-work-orders.test.ts index 7cdb645b..91918a8a 100644 --- a/apps/frontend/tests/admin-realtime-work-orders.test.ts +++ b/apps/frontend/tests/admin-realtime-work-orders.test.ts @@ -3,6 +3,7 @@ import test from 'node:test' import { evaluateAdminWorkOrderRealtimeMatch, + isExplicitOrderNoSearch, parseAdminWorkOrderListFilters, patchAdminWorkOrderListItems, shouldPrependAdminWorkOrder, @@ -372,3 +373,48 @@ test('关键词 / 赠送阶段 / 打手等级筛选:无法证真,只原地 'mismatch', ) }) + +/** + * 显式单号检索的护栏。 + * + * 生产现象:按工单号搜出来的那一行,在订单状态变化后会从结果里消失,必须整页刷新才回来。 + * 成因是实时补丁的判定链里,「状态/接单人员/日期范围」这些可证伪条件排在关键词护栏之前, + * 一旦它们判定 mismatch 就允许把行删掉。而面板搜索时只清空了状态,没清接单人员/日期范围, + * 所以护栏还会被这些条件绕过。 + */ +test('显式单号检索:状态流转后不得把正在看的单删掉', () => { + const cases: Array> = [ + { keyword: 'WO26100217194691', statuses: 'in_progress' }, + { keyword: 'WO26100217194691', workerId: 7 }, + { keyword: 'WO26100217194691', createdFrom: '2026-10-05' }, + { keyword: 'OD26100217', statuses: 'open' }, + { keyword: '2627502003065030', statuses: 'open' }, + ] + for (const override of cases) { + const filters = parseAdminWorkOrderListFilters(buildQueryKey(override))! + assert.equal( + evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'accepted' }), filters), + 'unknown', + `${JSON.stringify(override)} 应判为 unknown(只原地更新),否则状态一变就搜不到`, + ) + } +}) + +test('模糊关键词不享受单号护栏:状态可证伪时仍按原设计移除', () => { + const filters = parseAdminWorkOrderListFilters( + buildQueryKey({ keyword: '暗潮', statuses: 'open' }), + )! + assert.equal( + evaluateAdminWorkOrderRealtimeMatch(buildSnapshot({ status: 'accepted' }), filters), + 'mismatch', + ) +}) + +test('isExplicitOrderNoSearch 只认单号形态,普通关键词不误判', () => { + for (const orderNo of ['WO26100217194691', 'OD26100217', '2627502003065030', 'wo12345678']) { + assert.equal(isExplicitOrderNoSearch(orderNo), true, `${orderNo} 应识别为单号`) + } + for (const keyword of ['暗潮', '无限潮汐', 'WO1', '', 'abcd']) { + assert.equal(isExplicitOrderNoSearch(keyword), false, `${keyword} 不应识别为单号`) + } +})