feat(consignment): 优化寄售仓库与接单工单双向手动匹配,支持工单号与提报单号检索与强制匹配

1. 寄售仓库手动匹配订单扩充支持 open 状态工单,增强 WO 工单号与纯数字的模糊/前缀搜索,修复工单搜不到的问题并在卡片高亮展示内部工单号与状态;
2. 接单工单操作列全面支持【手动匹配寄售】,允许从接单工单主动调起寄售匹配弹窗;
3. 寄售现货筛选支持通过 SUB 提报单号、INV 仓库号、供货打手等快速搜索;
4. 完善规格冲突下的强制匹配确认与必填原因提交机制,确保履约全流程留痕合规。
This commit is contained in:
yml2213
2026-09-19 17:59:05 +08:00
parent e5cc7cadef
commit f07e4087ad
6 changed files with 42 additions and 19 deletions
@@ -846,13 +846,13 @@ export function buildWorkOrderWhere({
params.push(normalizedKeyword)
const paramIndex = `$${params.length}`
filters.push(
`(wo.platform_order_id = ${paramIndex} OR wo.platform_order_id LIKE ${paramIndex} || '%')`,
`(wo.platform_order_id = ${paramIndex} OR wo.platform_order_id LIKE ${paramIndex} || '%' OR wo.work_order_no ILIKE '%' || ${paramIndex} || '%')`,
)
} else if (isWorkOrderNoLookupKeyword(normalizedKeyword)) {
params.push(normalizedKeyword)
const paramIndex = `$${params.length}`
filters.push(
`(wo.work_order_no = ${paramIndex} OR wo.work_order_no ILIKE ${paramIndex} || '%' OR LOWER(wo.work_order_no) = LOWER(${paramIndex}))`,
`(wo.work_order_no = ${paramIndex} OR wo.work_order_no ILIKE ${paramIndex} || '%' OR wo.work_order_no ILIKE '%' || ${paramIndex} || '%' OR LOWER(wo.work_order_no) = LOWER(${paramIndex}))`,
)
} else {
params.push(`%${normalizedKeyword}%`)
@@ -250,7 +250,11 @@ export async function listInventoryMatchableOrders(
}
const { items } = await listWorkOrders({
statuses: [WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.PENDING_MATERIAL],
statuses: [
WORK_ORDER_STATUS.UNASSIGNED,
WORK_ORDER_STATUS.PENDING_MATERIAL,
WORK_ORDER_STATUS.OPEN,
],
keyword: String(input.keyword || '').trim(),
pageSize: Math.min(Math.max(Number(input.pageSize || 50), 1), 100),
sort: 'id_desc',
@@ -358,11 +362,13 @@ export async function fulfillWorkOrderWithConsignment(input: {
throw createHttpError('工单不存在', { statusCode: 404, errorCode: 'work_order_not_found' })
}
// 仅未分配或待补资料工单支持选用库存履约
// 未分配、待补资料或大厅待接单工单支持选用库存履约
if (
![WORK_ORDER_STATUS.UNASSIGNED, WORK_ORDER_STATUS.PENDING_MATERIAL].includes(
workOrder.status as any,
)
![
WORK_ORDER_STATUS.UNASSIGNED,
WORK_ORDER_STATUS.PENDING_MATERIAL,
WORK_ORDER_STATUS.OPEN,
].includes(workOrder.status as any)
) {
throw createHttpError('当前工单状态不允许直接使用内部库存履约', {
statusCode: 409,
@@ -141,6 +141,10 @@ export default function ConsignmentManualMatchModal({
item.inventory.system,
item.inventory.serverZone,
item.inventory.series,
item.inventory.workerName,
item.inventory.buyerName,
item.inventory.sellerGameId,
item.inventory.id,
]),
[options, keyword],
)
@@ -148,10 +152,12 @@ export default function ConsignmentManualMatchModal({
() =>
filterManualMatchOptions(orders, keyword, (item) => [
item.workOrderNo,
item.workOrderId,
item.productName,
item.requiredSpec.system,
item.requiredSpec.serverZone,
item.requiredSpec.series,
item.status,
]),
[orders, keyword],
)
@@ -179,8 +185,8 @@ export default function ConsignmentManualMatchModal({
workOrderId: selected.workOrderId,
inventoryId: selected.inventoryId,
workerId: selected.workerId,
allowPartial: selected.matchType !== 'exact',
allowSpecMismatch: selected.matchType === 'conflict',
allowPartial: selected.matchType !== 'exact' || confirmedPartial,
allowSpecMismatch: selected.matchType === 'conflict' || confirmedMismatch,
matchNote: matchNote.trim(),
})
@@ -259,8 +265,8 @@ export default function ConsignmentManualMatchModal({
<Input
placeholder={
mode === 'order'
? '搜索物品名 / 寄卖单号 / 仓库号 / 规格'
: '搜索工单号 / 平台单号 / 标题 / 规格'
? '搜索物品名 / 寄卖单号 (SUB...) / 仓库号 (INV...) / 规格 / 供货打手'
: '搜索工单号 (WO...) / 平台单号 / 标题 / 规格'
}
prefix={<SearchOutlined />}
value={keyword}
@@ -296,6 +302,9 @@ export default function ConsignmentManualMatchModal({
>
<Space direction="vertical" size={2}>
<Space size={6} wrap>
<Tag color="cyan">
{option.inventory.sourceSubmissionNo || option.inventory.inventoryNo}
</Tag>
<Text strong>{option.inventory.itemName}</Text>
<Tag color={meta.color}>{meta.label}</Tag>
</Space>
@@ -369,9 +378,16 @@ export default function ConsignmentManualMatchModal({
<Space style={{ width: '100%', justifyContent: 'space-between' }} align="start">
<Space direction="vertical" size={2}>
<Space size={6} wrap>
<Text strong>#{order.workOrderId}</Text>
<Text>{order.productName}</Text>
<Tag color="purple">{order.workOrderNo || `#${order.workOrderId}`}</Tag>
<Text strong>{order.productName}</Text>
<Tag color={meta.color}>{meta.label}</Tag>
{order.status === 'open' ? (
<Tag color="blue">大厅待抢</Tag>
) : order.status === 'unassigned' ? (
<Tag color="orange">待指派</Tag>
) : order.status === 'pending_material' ? (
<Tag color="gold">待补资料</Tag>
) : null}
</Space>
<Text type="secondary" style={{ fontSize: 12 }}>
要求:{order.requiredSpec.system || '未指定'}/
@@ -398,7 +414,7 @@ export default function ConsignmentManualMatchModal({
onClick={() => {
setSelected({
workOrderId: order.workOrderId,
workOrderLabel: `#${order.workOrderId} ${order.productName}`,
workOrderLabel: `${order.workOrderNo || `#${order.workOrderId}`} · ${order.productName}`,
inventoryId: inventory?.inventoryId || 0,
inventoryLabel,
matchType: order.matchType,
@@ -1644,7 +1644,7 @@ export default function WorkOrdersPanel() {
onClick={() => setManualMatchOrder(row)}
title="从全部在库现货里人工指定一件(支持规格冲突强制匹配,需填原因)"
>
手动匹配现货
手动匹配寄售
</Button>
) : null}
{row.capabilities?.canCancel && (canReturnToHall || canCancelOrder) ? (
@@ -74,9 +74,9 @@ export function filterManualMatchOptions<T>(
/**
* 哪些订单状态可以手动指定现货。
*
* 与后端 `fulfillWorkOrderWithConsignment` 的前置校验保持一致(只允许 unassigned /
* pending_material):入口先按同一口径收敛,避免"按钮点了才报错"。
* 与后端 `fulfillWorkOrderWithConsignment` 的前置校验保持一致(允许 unassigned /
* pending_material / open):入口先按同一口径收敛,避免"按钮点了才报错"。
*/
export function isManuallyMatchableOrderStatus(status: string): boolean {
return status === 'unassigned' || status === 'pending_material'
return status === 'unassigned' || status === 'pending_material' || status === 'open'
}
@@ -60,11 +60,12 @@ test('关键词过滤:现货能按寄卖单号/物品名搜到,大小写与
assert.equal(filterManualMatchOptions(list, '不存在', pick).length, 0)
})
test('只有待指派/待补资料的订单能手动指定现货(与后端前置校验同口径)', async () => {
test('待指派/待补资料/大厅待抢的订单均能手动指定现货(与后端前置校验同口径)', async () => {
const { isManuallyMatchableOrderStatus } =
await import('../src/utils/consignment-manual-match.ts')
assert.equal(isManuallyMatchableOrderStatus('unassigned'), true)
assert.equal(isManuallyMatchableOrderStatus('pending_material'), true)
assert.equal(isManuallyMatchableOrderStatus('open'), true)
for (const status of ['in_progress', 'pending_acceptance', 'accepted', 'cancelled', 'disputed']) {
assert.equal(isManuallyMatchableOrderStatus(status), false, `${status} 不该给手动匹配入口`)
}