fix(consignment): 修复寄卖取消解绑、SUB 单号写空、拼单门禁与后台工单列表实时乱跳
1. 问题单处置「取消」后打手信息不消失:resolveProblemWorkOrder 的取消分支从未解绑寄卖现货,现货永久停在 consumed 占库不还,工单 material 上的寄售标记残留又让列表继续显示「供货打手」。取消时补上解绑;调用必须排在状态 UPDATE 之前——那条 UPDATE 会清空 friend_added_at,清完再解绑会把已确认加好友的货误判成可回池(二次出货); 2. 解绑函数在没有现货可解绑时直接 return,跳过了摘标记,于是「库存行早已不存在」的老帖永远摘不掉寄售标记(列表一直显示供货打手)。现在无论是否解绑到现货都摘标记,痕迹保留在 material.consignmentHistory 的 detachReason 里; 3. 内部现货履约写入的 sourceSubmissionNo 恒为空:取的是占库 `UPDATE ... RETURNING *` 的行,只带回本表列,没有 join 出来的 submission_no,导致工单列表把所有寄卖单都显示成「老单无提报单号」,SUB 单号这个唯一锚点丢失。改取带 join 的预读库存(内部现货资料、指派工单资料、指派事件留痕三处)。存量老单按业务口径不回填; 4. 后台接单工单列表实时乱跳:SSE 快照补丁按数组下标读 queryKey,面板 key 加了字段后下标全错(page 被读成 targetWorkOrderId → 任何页都被当成第 1 页),且完全没判「内部寄售匹配」等筛选,于是任意一条不在当前页的老单只要发生变更就被插到列表顶部。改为结构化 queryKey + 三态判定(match 才允许插顶、mismatch 才允许移除、其余只原地更新),插入还要求 page===1 且工单 id 大于当前首行;状态比对改用 rawStatus(快照的 status 是运营态,拼单母单会被显示成 in_progress),分类筛选因快照不带 parentCategoryId 归入不可证伪; 5. 新增「寄卖只做整单」硬门禁:已有人参与的拼单工单不参与现货匹配与库存提醒,履约入口一律返回 work_order_sharing_not_consignable,前端「手动匹配寄卖」入口按同一口径收敛;开了拼单但无人参与的整单不受影响,避免过度拦截。 验证:后端 520 项单测(517 pass / 3 skip)、前端 78 项全绿;并对真实开发库跑通问题单取消三类场景(老帖无库存行 / 回库 / 已加好友作废)、退回打手与退回大厅的反向用例(不得解绑)、履约写入真实 SUB 单号、拼单门禁四入口 + 未过度拦截反向用例。
This commit is contained in:
@@ -52,6 +52,12 @@ import {
|
||||
setAdminPermissions,
|
||||
} from '@/utils/admin-auth'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import {
|
||||
evaluateAdminWorkOrderRealtimeMatch,
|
||||
parseAdminWorkOrderListFilters,
|
||||
patchAdminWorkOrderListItems,
|
||||
shouldPrependAdminWorkOrder,
|
||||
} from '@/utils/admin-realtime-work-orders'
|
||||
import { useRealtimeEvents, type RealtimeEvent } from '@/hooks/useRealtimeEvents'
|
||||
import dayjs from '@/lib/dayjs'
|
||||
|
||||
@@ -436,40 +442,43 @@ function patchAdminWorkOrderCaches(
|
||||
queryClient: ReturnType<typeof useQueryClient>,
|
||||
event: RealtimeEvent,
|
||||
) {
|
||||
const item = event.data as {
|
||||
workOrderId?: unknown
|
||||
status?: unknown
|
||||
productName?: unknown
|
||||
platformOrderId?: unknown
|
||||
worker?: { workerId?: unknown } | null
|
||||
}
|
||||
const item = event.data
|
||||
for (const [queryKey, current] of queryClient.getQueriesData({
|
||||
queryKey: ['admin-worker-platform-orders'],
|
||||
})) {
|
||||
const statuses = String(queryKey[1] || '')
|
||||
.split(',')
|
||||
.filter(Boolean)
|
||||
const keyword = String(queryKey[2] || '')
|
||||
const workerId = Number(queryKey[3] || 0)
|
||||
const workerLevelIds = String(queryKey[4] || '')
|
||||
.split(',')
|
||||
.map((value) => Number(value))
|
||||
.filter(Boolean)
|
||||
const targetWorkOrderId = Number(queryKey[5] || 0)
|
||||
const page = Number(queryKey[6] || 1)
|
||||
// 等级筛选的匹配逻辑只在服务端(接单打手或拼单参与者等级),快照不含等级,
|
||||
// 无法在前端复现:已有行视为匹配(拉取时已通过服务端过滤),但不再插入新行。
|
||||
const levelFilterActive = workerLevelIds.length > 0
|
||||
const matches =
|
||||
(!statuses.length || statuses.includes(String(item.status || ''))) &&
|
||||
matchesWorkOrderKeyword(item, keyword) &&
|
||||
(!workerId || workerId === Number(item.worker?.workerId || 0)) &&
|
||||
(!targetWorkOrderId || targetWorkOrderId === Number(item.workOrderId || 0))
|
||||
const shouldInsert = event.operation === 'upsert' && page === 1 && matches && !levelFilterActive
|
||||
const operation = event.operation === 'remove' || !matches ? 'remove' : 'upsert'
|
||||
const filters = parseAdminWorkOrderListFilters(queryKey)
|
||||
if (!filters) {
|
||||
// 读不懂这个 key(旧结构 / 别的调用方):不猜、不插、不移除,交给服务端下一次拉取。
|
||||
continue
|
||||
}
|
||||
|
||||
// 三态判定:match 才允许插入顶部;mismatch 才允许移除;unknown 只原地更新已存在的行。
|
||||
const verdict =
|
||||
event.operation === 'remove' ? 'mismatch' : evaluateAdminWorkOrderRealtimeMatch(item, filters)
|
||||
if (verdict === 'unknown') {
|
||||
queryClient.setQueryData(
|
||||
queryKey,
|
||||
patchAdminWorkOrderListItems(current, item, { remove: false, insert: false }),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
const items =
|
||||
isRecord(current) && isRecord(current.data) && Array.isArray(current.data.items)
|
||||
? (current.data.items as unknown[])
|
||||
: []
|
||||
const insert =
|
||||
verdict === 'match' &&
|
||||
event.operation === 'upsert' &&
|
||||
filters.page === 1 &&
|
||||
shouldPrependAdminWorkOrder(items, item)
|
||||
|
||||
queryClient.setQueryData(
|
||||
queryKey,
|
||||
patchWorkOrderListEnvelope(current, { ...event, operation }, shouldInsert),
|
||||
patchAdminWorkOrderListItems(current, item, {
|
||||
remove: verdict === 'mismatch',
|
||||
insert,
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -597,20 +606,6 @@ function patchAdminOrderListEnvelope(
|
||||
}
|
||||
}
|
||||
|
||||
function patchWorkOrderListEnvelope(current: unknown, event: RealtimeEvent, insert = false) {
|
||||
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
|
||||
return current
|
||||
}
|
||||
const item = event.data
|
||||
const id = Number((item as { workOrderId?: unknown }).workOrderId || event.entityId)
|
||||
const items = current.data.items as unknown[]
|
||||
const nextItems =
|
||||
event.operation === 'remove'
|
||||
? items.filter((entry) => Number((entry as { workOrderId?: unknown })?.workOrderId) !== id)
|
||||
: upsertById(items, item, 'workOrderId', insert)
|
||||
return { ...current, data: { ...current.data, items: nextItems } }
|
||||
}
|
||||
|
||||
function patchFinanceRequestListEnvelope(current: unknown, event: RealtimeEvent) {
|
||||
if (!isRecord(current) || !isRecord(current.data) || !Array.isArray(current.data.items)) {
|
||||
return current
|
||||
@@ -682,17 +677,6 @@ function matchesAdminOrderDateRange(
|
||||
return true
|
||||
}
|
||||
|
||||
function matchesWorkOrderKeyword(
|
||||
item: { productName?: unknown; platformOrderId?: unknown },
|
||||
keyword: string,
|
||||
) {
|
||||
const normalizedKeyword = keyword.trim().toLowerCase()
|
||||
if (!normalizedKeyword) return true
|
||||
return [item.productName, item.platformOrderId]
|
||||
.map((value) => String(value || '').toLowerCase())
|
||||
.some((value) => value.includes(normalizedKeyword))
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return Boolean(value && typeof value === 'object' && !Array.isArray(value))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user