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:
yml2213
2026-10-01 12:17:00 +08:00
parent 610c65bb34
commit 40ee36d72a
11 changed files with 869 additions and 95 deletions
@@ -83,8 +83,12 @@ import {
import { notifyVoucherConsume } from '@/utils/voucher-consume'
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
import { isManuallyMatchableOrderStatus } from '@/utils/consignment-manual-match'
import { resolveConsignmentScreenshotUrls } from '@/utils/consignment-display'
import { canManuallyMatchConsignmentOrder } from '@/utils/consignment-manual-match'
import {
isConsignmentOrder,
isInternalConsignmentOrder,
resolveConsignmentScreenshotUrls,
} from '@/utils/consignment-display'
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
@@ -172,22 +176,6 @@ type CancelSharingFormValues = {
reason?: string
}
function isInternalConsignmentOrder(row: WorkOrder) {
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
if (consignment.fulfillmentType === 'worker_deposit' || consignment.workerId) return false
return Boolean(consignment.inventoryId) && !row.worker
}
function isConsignmentOrder(row: WorkOrder) {
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
return Boolean(
consignment.inventoryId ||
consignment.inventoryNo ||
consignment.fulfillmentType ||
row.gift?.boosterMaterial?.fields?.inventoryNo,
)
}
/** 工单 `material.consignment`(寄售履约信息);没有寄售履约信息时返回 null。 */
function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | null {
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
@@ -392,20 +380,25 @@ export default function WorkOrdersPanel() {
}, [searchParams, setSearchParams])
const ordersQuery = useQuery({
// ⚠️ 结构化 key:实时快照补丁(layouts/AdminLayout.tsx)要按名字读筛选条件。
// 以前这里是位置数组,AdminLayout 按下标去读,下标一改就全错(page 被读成 targetWorkOrderId,
// 导致任何页都被当成第 1 页,历史单被插到列表顶部"乱跳")。改字段时两边一起改。
queryKey: [
'admin-worker-platform-orders',
statuses.join(','),
keyword,
workerId,
categoryId,
workerLevelIds.join(','),
targetWorkOrderId,
acceptanceMode || '',
giftPhase || '',
page,
pageSize,
createdRange ? createdRange[0] : '',
createdRange ? createdRange[1] : '',
{
statuses: statuses.join(','),
keyword,
workerId,
categoryId,
workerLevelIds: workerLevelIds.join(','),
targetWorkOrderId,
acceptanceMode: acceptanceMode || '',
giftPhase: giftPhase || '',
page,
pageSize,
createdFrom: createdRange ? createdRange[0] : '',
createdTo: createdRange ? createdRange[1] : '',
},
],
queryFn: () =>
fetchAdminWorkOrders({
@@ -1664,7 +1657,7 @@ export default function WorkOrdersPanel() {
手动指定现货必须**常驻**:只在"检测到候选"的提醒弹窗里给入口,
订单没有任何系统候选时客服反而进不去——而那正是最需要人工指定的时候。
*/}
{canFulfillConsignment && isManuallyMatchableOrderStatus(row.status) ? (
{canFulfillConsignment && canManuallyMatchConsignmentOrder(row) ? (
<Button
icon={<ThunderboltOutlined />}
onClick={() => setManualMatchOrder(row)}