fix(admin): 显式单号检索下,其它筛选不得把正在看的工单从实时补丁里删掉
## 修的是什么
实时补丁的判定链里,「状态 / 接单人员 / 日期范围」这些可证伪条件排在关键词护栏之前,
任一不成立就先返回 mismatch(允许删行),于是第 156 行那条「有关键词就不删行」的护栏
在带筛选时根本执行不到。
d6523635 只在 UI 侧补了一半:搜显式单号时清空 状态/赠送阶段/验收模式,
但**没清接单人员、分类、日期范围**,而 workerId 与 createdRange 同样会先返回 mismatch。
所以「单号 + 接单人员」或「单号 + 日期范围」仍会把行删掉。
实测(真实函数):
单号 + 状态筛选 -> mismatch(删行)
单号 + 接单人员筛选 -> mismatch(删行)
单号 + 日期范围 -> mismatch(删行)
## 修法
只对「显式单号检索」提前短路为 unknown(只原地更新,不插不移):
if (isExplicitOrderNoSearch(filters.keyword)) return 'unknown'
刻意**不**推翻既有设计:模糊关键词(产品名/昵称等)仍保留「状态可证伪即移除」的语义,
因此原来锁定该语义的用例继续通过。
顺带把 isExplicitOrderNoSearch 合并到 utils:此前面板与 AdminLayout 各有一份规则,
重复两份正是上次 tab 名单漏改那类问题的温床。
## 边界(重要)
**本次修复不覆盖「无任何其它筛选、只按单号搜索」的场景。**
实测该场景下判定本就是 unknown、行不会被删(状态原地更新、分页与统计一致),
因此线上反馈的「状态变化后搜不到、必须整页刷新」若发生在无筛选条件下,另有他因,尚未定位。
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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<Partial<AdminWorkOrderListFilters>> = [
|
||||
{ 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} 不应识别为单号`)
|
||||
}
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user