fix(consignment): 拼单份额验收收官补寄卖结算钩子 + 手动匹配弹窗三处复核遗留

- acceptWorkOrderShareAndSettle 最后一个份额把母单转 accepted 的分支,
  此前不释放发布押金也不结算供货货款(两钩子只在统一验收里):
  现在同口径补上;供货货款传 assignedWorkerId=0(拼单母单无单一接单人,
  份额打手已各自按 share_reward 结算,不会重复付款)
- 手动匹配弹窗:race_condition/unavailable/spec_mismatch/partial_requires_confirm
  四个错误码清空已选并自动重拉列表,数据过期不再死循环撞错
- 手动匹配弹窗库存侧:keyword+pageSize100 透传后端(原来 keyword 是死参数、
  只拉 50 条),300ms 防抖后端搜索;无供货打手时禁用指派选项并在提交前
  再拦一道(原来必 400 死路);选中订单回显 workerId/workerName
- 凭证核销反馈抽共享 utils/voucher-consume,WorkOrdersPanel 与弹窗共用,
  口径不再漂移(弹窗顺带获得"已提前核销 N 张"提示)
- docs/寄卖系统优化方案.md 记录批次 V

后端 477(+4)/ 前端 50(+1)测试全绿,typecheck / lint / format / check:sql 通过
This commit is contained in:
yml2213
2026-09-17 12:45:51 +08:00
parent 63837e473b
commit b666a9c951
8 changed files with 183 additions and 31 deletions
@@ -1,4 +1,4 @@
import { useMemo, useState } from 'react'
import { useEffect, useMemo, useState } from 'react'
import {
Alert,
Button,
@@ -30,6 +30,7 @@ import {
resolveManualMatchRequirement,
type ManualMatchType,
} from '@/utils/consignment-manual-match'
import { notifyVoucherConsume } from '@/utils/voucher-consume'
const { Text, Paragraph } = Typography
@@ -74,6 +75,10 @@ export default function ConsignmentManualMatchModal({
const [options, setOptions] = useState<ConsignmentMatchOptionView[]>([])
const [orders, setOrders] = useState<ConsignmentMatchableOrder[]>([])
const [inventoryLabel, setInventoryLabel] = useState('')
const [inventoryWorker, setInventoryWorker] = useState<{
workerId: number | null
workerName: string
} | null>(null)
const [selected, setSelected] = useState<Selected | null>(null)
const [deliveryKind, setDeliveryKind] = useState<'internal_stock' | 'assign_worker'>(
'internal_stock',
@@ -98,13 +103,22 @@ export default function ConsignmentManualMatchModal({
setLoading(true)
try {
if (inventory) {
const res = await fetchInventoryMatchableOrders(inventory.inventoryId)
// 关键字与分页透传后端:库存侧默认只拉 50 条时,第 51 单以后永远搜不到
const res = await fetchInventoryMatchableOrders(inventory.inventoryId, {
keyword: keyword.trim() || undefined,
pageSize: 100,
})
setOrders(res.data?.orders || [])
setInventoryLabel(
res.data?.inventory
? `${res.data.inventory.itemName}(${res.data.inventory.sourceSubmissionNo || res.data.inventory.inventoryNo})`
: inventory.label,
)
// 指派线需要现货的供货打手:库存侧入口从响应里记下来,选中订单时带上
setInventoryWorker({
workerId: res.data?.inventory?.workerId ?? null,
workerName: res.data?.inventory?.workerName || '',
})
} else if (workOrder) {
const res = await fetchWorkOrderConsignmentMatchOptions(workOrder.workOrderId)
setOptions(res.data?.options || [])
@@ -116,6 +130,16 @@ export default function ConsignmentManualMatchModal({
}
}
// 库存侧:关键字防抖透传后端搜索(本地过滤只作用在已拉取的 100 条内)
useEffect(() => {
if (!open || !inventory) return
const timer = setTimeout(() => {
void load()
}, 300)
return () => clearTimeout(timer)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [keyword])
const filteredOptions = useMemo(
() =>
filterManualMatchOptions(options, keyword, (item) => [
@@ -152,6 +176,10 @@ export default function ConsignmentManualMatchModal({
async function handleSubmit() {
if (!selected) return
if (deliveryKind === 'assign_worker' && !selected.workerId) {
message.error('该现货未关联供货打手,不能走指派履约;请改用内部现货履约')
return
}
setSubmitting(true)
try {
const payload = {
@@ -169,15 +197,10 @@ export default function ConsignmentManualMatchModal({
})
: await fulfillWorkOrderWithConsignment(payload)
// 核销失败时共享工具会弹警告(状态已变更但凭证没消耗),此时不能再弹"成功"掩盖
const voucher = (res.data as any)?.voucherConsume
if (voucher && voucher.ok === false) {
message.warning({
content: `状态已变更,但电子凭证核销失败:${
voucher.errorMessage || '未知错误'
}。请在快手后台人工核销`,
duration: 8,
})
} else {
notifyVoucherConsume(voucher)
if (!voucher || voucher.ok) {
message.success(
deliveryKind === 'assign_worker'
? '已手动匹配:指派供货打手履约,押金已冻结'
@@ -188,6 +211,27 @@ export default function ConsignmentManualMatchModal({
resetState()
onClose()
} catch (err: any) {
// 列表数据过期(并发占用 / 打开弹窗后订单规格被改):刷新列表按最新判定重来,
// 否则运营会对着已失效的候选反复重试、循环撞同一个错
const code = String(err?.errorCode || '')
if (
code === 'consignment_inventory_race_condition' ||
code === 'consignment_inventory_unavailable'
) {
message.error('该现货刚被其他操作占用或已下架,已刷新列表,请重新选择')
setSelected(null)
void load()
return
}
if (
code === 'consignment_spec_mismatch' ||
code === 'consignment_spec_partial_requires_confirm'
) {
message.error('订单或现货的规格已变化(列表数据过期),已刷新,请按最新判定重新确认')
setSelected(null)
void load()
return
}
message.error(err.message || '手动匹配失败')
} finally {
setSubmitting(false)
@@ -312,6 +356,10 @@ export default function ConsignmentManualMatchModal({
setConfirmedPartial(false)
setConfirmedMismatch(false)
setMatchNote('')
// 选了无打手的现货时指派不可用,兜底切回内部履约
if (!option.inventory.workerId && deliveryKind === 'assign_worker') {
setDeliveryKind('internal_stock')
}
}}
>
{isSelected ? '已选' : '选它'}
@@ -377,10 +425,17 @@ export default function ConsignmentManualMatchModal({
inventoryLabel,
matchType: order.matchType,
conflicts: order.conflicts,
// 指派线要用现货的供货打手:库存侧入口从接口响应的 inventory 里带过来
workerId: inventoryWorker?.workerId ?? null,
workerName: inventoryWorker?.workerName || '',
})
setConfirmedPartial(false)
setConfirmedMismatch(false)
setMatchNote('')
// 选了无打手的现货时指派不可用,兜底切回内部履约
if (!inventoryWorker?.workerId && deliveryKind === 'assign_worker') {
setDeliveryKind('internal_stock')
}
}}
>
{isSelected ? '已选' : '选它'}
@@ -421,8 +476,12 @@ export default function ConsignmentManualMatchModal({
},
{
value: 'assign_worker',
// 没关联打手的现货走指派必 400 consignment_worker_required,直接禁用并说明,别给死路
disabled: !selected.workerId,
label: `指派供货打手履约(冻结押金、工单进行中)${
selected.workerName ? ` · 打手:${selected.workerName}` : ''
selected.workerName
? ` · 打手:${selected.workerName}`
: ' · 该现货未关联打手,不可指派'
}`,
},
]}