feat(consignment): 统一寄卖匹配看板口径并收口占用与重匹配

- 看板与引擎统一数量、模板规格、正向规格信号判定,纳入 internal_matching/unassigned

- 库存侧改走新引擎匹配,移除旧 /fulfill 手动入口与旧弹窗

- 验收时将 reserved allocation 收口为 consumed,补份额级收口

- 拼单重新匹配通过 material 排除全部原库存/原卖家

- 开发/测试环境放宽迁移顺序校验,生产保持严格;修正集成测试份额报酬口径
This commit is contained in:
2026-11-12 15:17:09 +08:00
parent 229eef31a8
commit e39f640dac
18 changed files with 962 additions and 759 deletions
@@ -52,7 +52,7 @@ import {
updateAdminConsignmentInventory,
} from '@/services/admin/consignment'
import { fetchAdminWorkerUsers } from '@/services/admin/worker-platform'
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
import ConsignmentInventoryMatchModal from '@/components/admin/ConsignmentInventoryMatchModal'
import type {
ConsignmentInventory,
ConsignmentInventoryFilterField,
@@ -1466,19 +1466,13 @@ export default function ConsignmentInventoryPanel() {
</Form>
</Modal>
{/* 手动匹配订单:从现货出发挑订单(与订单侧共用同一弹窗) */}
<ConsignmentManualMatchModal
{/* 匹配订单:从现货出发,走新引擎(数量/配置/重新匹配排除与看板同口径) */}
<ConsignmentInventoryMatchModal
key={manualMatchItem?.id ?? 'none'}
open={Boolean(manualMatchItem)}
inventoryId={manualMatchItem?.id ?? null}
inventoryLabel={manualMatchItem?.itemName}
onClose={() => setManualMatchItem(null)}
inventory={
manualMatchItem
? {
inventoryId: manualMatchItem.id,
label: manualMatchItem.itemName,
inboundAt: manualMatchItem.inboundAt || manualMatchItem.createdAt,
}
: null
}
onMatched={() => void inventoriesQuery.refetch()}
/>
@@ -92,20 +92,17 @@ import {
import type { CustomerRefundPreview } from '@/services/admin/worker-platform'
import {
checkWorkOrderConsignmentMatches,
fulfillWorkOrderWithConsignment,
matchAdminConsignmentWorkOrder,
revokeWorkOrderConsignmentFulfillment,
} from '@/services/admin/consignment'
import { notifyVoucherConsume } from '@/utils/voucher-consume'
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
import { canManuallyMatchConsignmentOrder } from '@/utils/consignment-manual-match'
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
import {
isConsignmentOrder,
isInternalConsignmentOrder,
resolveConsignmentScreenshotUrls,
} from '@/utils/consignment-display'
import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal'
import ConsignmentWorkOrderMatchModal from '@/components/admin/ConsignmentWorkOrderMatchModal'
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
@@ -433,7 +430,6 @@ export default function WorkOrdersPanel() {
}>()
// 寄售库存匹配拦截弹窗状态
const [manualMatchOrder, setManualMatchOrder] = useState<WorkOrder | null>(null)
const [consignmentMatchOrder, setConsignmentMatchOrder] = useState<WorkOrder | null>(null)
const [consignmentInterceptModal, setConsignmentInterceptModal] = useState<{
open: boolean
@@ -1207,17 +1203,23 @@ export default function WorkOrdersPanel() {
if (!consignmentInterceptModal.order) return
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: true }))
try {
const res = await fulfillWorkOrderWithConsignment({
const res = await matchAdminConsignmentWorkOrder({
workOrderId: consignmentInterceptModal.order.workOrderId,
inventoryId,
allowPartial,
inventoryIds: [inventoryId],
allowNonExact: allowPartial,
reason: allowPartial ? '内部寄售库存手动匹配(非精确)' : '',
})
message.success('已占用内部库存,跳过打手资料,已进入待加好友阶段')
notifyVoucherConsume(res?.data?.voucherConsume)
if (res.data?.matched === false) {
message.warning('工单状态或库存已变化,请刷新后重试')
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: false }))
await refreshAll()
return
}
message.success('已占用内部库存并匹配工单')
setConsignmentInterceptModal({ open: false, order: null, matchData: null, submitting: false })
await refreshAll()
} catch (err: any) {
message.error(err.message || '内部履约失败')
message.error(err.message || '内部库存匹配失败')
setConsignmentInterceptModal((prev) => ({ ...prev, submitting: false }))
}
}
@@ -2066,18 +2068,9 @@ export default function WorkOrdersPanel() {
</Button>
) : null}
{/*
手动指定现货必须**常驻**:只在"检测到候选"的提醒弹窗里给入口,
订单没有任何系统候选时客服反而进不去——而那正是最需要人工指定的时候。
寄卖匹配只保留新引擎入口:旧的 /fulfill 手动匹配不支持数量拆分,
还会绕过 profile/重新匹配排除,已从 UI 收敛掉。
*/}
{canFulfillConsignment && canManuallyMatchConsignmentOrder(row) ? (
<Button
icon={<ThunderboltOutlined />}
onClick={() => setManualMatchOrder(row)}
title="从全部在库现货里人工指定一件(支持规格冲突强制匹配,需填原因)"
>
手动匹配寄售
</Button>
) : null}
{canFulfillConsignment &&
['internal_matching', 'unassigned', 'open'].includes(String(row.status || '')) ? (
<Button
@@ -4411,10 +4404,10 @@ export default function WorkOrdersPanel() {
matchData: null,
submitting: false,
})
setManualMatchOrder(currentOrder)
setConsignmentMatchOrder(currentOrder)
}}
>
手动指定现货匹配
内部库存匹配
</Button>
) : null,
<Button
@@ -4627,23 +4620,6 @@ export default function WorkOrdersPanel() {
}}
/>
<ConsignmentManualMatchModal
open={Boolean(manualMatchOrder)}
onClose={() => setManualMatchOrder(null)}
workOrder={
manualMatchOrder
? {
workOrderId: manualMatchOrder.workOrderId,
label: `#${manualMatchOrder.workOrderId} ${manualMatchOrder.productName || ''}`,
createdAt: manualMatchOrder.createdAt,
}
: null
}
onMatched={() => {
void refreshAll()
}}
/>
{/* 撤单 / 撤销寄售履约弹窗 */}
<Modal
title={`撤单 / 撤销寄售履约 · ${getDisplayOrderNo(revokeConsignmentModal.order)}`}