fix(worker-platform): 修复寄售匹配工单详情中卖家上架截图丢失问题,并收敛手动匹配为内部现货直接履约

1. 修复文件归一化器 normalizeUploadedFiles 遇到纯字符串 URL 数组被误过滤为空的问题,自动提取 objectKey 并构造规范 UploadedFile 结构;
2. 优化工单展示映射器 resolveWorkOrderEffectiveBoosterMaterial:当工单资料中截图为空时,自动回退并加载寄售卖家上架/主页截图;
3. 前端管理后台详情抽屉与打手端详情抽屉支持读取 detailOrder.gift.boosterMaterial.screenshots 与 consignmentInfo.screenshots 降级兜底渲染;
4. 手动匹配弹窗与发布拦截弹窗全面收敛为单一标准的【内部现货直接履约】,废除旧版指派打手带来的重复扣押金与二次填资料问题;
5. 新增 consignment-booster-material 单元测试,更新寄卖库存优化现状与进度追踪文档,前后端全套测试通过。
This commit is contained in:
yml2213
2026-09-21 11:35:38 +08:00
parent 040b3db84c
commit c80bd0e337
9 changed files with 372 additions and 102 deletions
@@ -16,9 +16,9 @@ import {
import { SearchOutlined, TeamOutlined } from '@ant-design/icons'
import {
assignWorkOrderWithConsignmentWorker,
fetchInventoryMatchableOrders,
fetchWorkOrderConsignmentMatchOptions,
fulfillWorkOrderWithConsignment,
} from '@/services/admin/consignment'
import type { ConsignmentMatchOptionView, ConsignmentMatchableOrder } from '@/types/consignment'
import {
@@ -39,7 +39,7 @@ type Selected = {
inventoryLabel: string
matchType: ManualMatchType
conflicts: string[]
/** 供货打手(接单平台指派履约对象) */
/** 供货打手(用于回显供货来源) */
workerId?: number | null
workerName?: string
}
@@ -57,7 +57,7 @@ export type ConsignmentManualMatchModalProps = {
/**
* 「手动指定现货」弹窗:订单侧与库存侧共用一套交互与落地逻辑。
*
* 统一走接单平台指派供货打手履约(自动冻结押金、开启交付倒计时与履约监控)。
* 统一走内部现货直接履约(原子占库、免外派佣金、不冻结重复押金、跳过打手资料直接进入待加好友)。
*/
export default function ConsignmentManualMatchModal({
open,
@@ -106,7 +106,6 @@ export default function ConsignmentManualMatchModal({
? `${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 || '',
@@ -165,7 +164,6 @@ export default function ConsignmentManualMatchModal({
const requirement = resolveManualMatchRequirement(selected?.matchType || 'exact')
const canSubmit =
Boolean(selected) &&
Boolean(selected?.workerId) &&
canSubmitManualMatch({
matchType: selected?.matchType || 'exact',
confirmedPartial,
@@ -175,16 +173,11 @@ export default function ConsignmentManualMatchModal({
async function handleSubmit() {
if (!selected) return
if (!selected.workerId) {
message.error('该现货未关联供货打手,无法走平台指派履约;请先关联打手')
return
}
setSubmitting(true)
try {
const res = await assignWorkOrderWithConsignmentWorker({
const res = await fulfillWorkOrderWithConsignment({
workOrderId: selected.workOrderId,
inventoryId: selected.inventoryId,
workerId: selected.workerId,
allowPartial: selected.matchType !== 'exact' || confirmedPartial,
allowSpecMismatch: selected.matchType === 'conflict' || confirmedMismatch,
matchNote: matchNote.trim(),
@@ -194,7 +187,7 @@ export default function ConsignmentManualMatchModal({
const voucher = (res.data as any)?.voucherConsume
notifyVoucherConsume(voucher)
if (!voucher || voucher.ok) {
message.success('已手动匹配并指派供货打手履约,履约押金已冻结')
message.success('已使用内部寄售现货直接履约,已进入待加好友阶段')
}
onMatched?.()
resetState()
@@ -221,7 +214,7 @@ export default function ConsignmentManualMatchModal({
void load()
return
}
message.error(err.message || '手动匹配指派失败')
message.error(err.message || '内部现货履约失败')
} finally {
setSubmitting(false)
}
@@ -232,7 +225,7 @@ export default function ConsignmentManualMatchModal({
title={mode === 'order' ? `手动指定现货 · ${workOrder?.label || ''}` : '手动匹配订单'}
open={open}
width={860}
okText="确认指派履约"
okText="确认内部现货履约"
okButtonProps={{
disabled: !canSubmit,
loading: submitting,
@@ -252,13 +245,13 @@ export default function ConsignmentManualMatchModal({
>
<Space direction="vertical" size={12} style={{ width: '100%' }}>
<Alert
type="warning"
type="info"
showIcon
message="手动匹配将直接占用现货并指派供货打手履约(冻结履约押金并纳入交付监控)。"
message="手动匹配将直接选用内部寄售现货履约(原子占库、免外派佣金、不冻结重复押金、自动跳过打手资料)。"
description={
mode === 'order'
? '下面是该订单的全部在库现货(含规格不一致的),请核对差异后再决定。'
: `当前现货:${inventoryLabel || inventory?.label || ''}。下面是可匹配的订单列表,请核对规格后指派。`
? '下面是该订单的全部在库现货(含规格不一致的),核对差异后直接内部履约。'
: `当前现货:${inventoryLabel || inventory?.label || ''}。下面是可匹配的订单列表,核对后直接内部履约。`
}
/>
@@ -442,18 +435,18 @@ export default function ConsignmentManualMatchModal({
<Descriptions size="small" column={1} style={{ marginBottom: 8 }}>
<Descriptions.Item label="订单">{selected.workOrderLabel || '—'}</Descriptions.Item>
<Descriptions.Item label="现货">{selected.inventoryLabel || '—'}</Descriptions.Item>
<Descriptions.Item label="指派打手">
<Descriptions.Item label="供货来源">
{selected.workerId ? (
<Space size={6}>
<Tag icon={<TeamOutlined />} color="blue">
{selected.workerName || `打手 #${selected.workerId}`}
{selected.workerName || `供货打手 #${selected.workerId}`}
</Tag>
<Text type="secondary" style={{ fontSize: 12 }}>
(工单将进入该打手“我的接单”并冻结履约押金)
(验收后将自动为该打手发放货款)
</Text>
</Space>
) : (
<Text type="danger">该现货未关联供货打手,无法指派</Text>
<Tag color="default">内部库存现货(无打手关联)</Tag>
)}
</Descriptions.Item>
<Descriptions.Item label="匹配判定">
@@ -469,15 +462,6 @@ export default function ConsignmentManualMatchModal({
</Descriptions>
<Space direction="vertical" size={8} style={{ width: '100%' }}>
{!selected.workerId ? (
<Alert
type="error"
showIcon
message="无法指派打手"
description="该现货未绑定任何供货打手,无法在接单平台中执行指派与押金冻结流程。请先在寄卖管理中关联打手。"
/>
) : null}
{requirement.needsPartialConfirm ? (
<Checkbox
checked={confirmedPartial}
@@ -516,7 +500,7 @@ export default function ConsignmentManualMatchModal({
) : null}
<Paragraph type="secondary" style={{ fontSize: 12, marginBottom: 0 }}>
确认后现货立即被占用:内部现货履约进入「待加好友」,指派打手会冻结履约押金。
确认后现货立即被占用并进入「待加好友」,双方加好买家好友后点确认加好友进入赠送冷却期。
如需退回请使用「撤销寄售履约」。
</Paragraph>
</Space>
@@ -808,6 +808,7 @@ export default function WorkOrdersPanel() {
consignmentNo: string
workerName?: string
depositFen?: number | null
costPriceFen?: number | null
onOk: () => void
}) {
const isPartial = input.matchType !== 'exact'
@@ -2413,6 +2414,14 @@ export default function WorkOrdersPanel() {
files={detailOrder.gift.boosterMaterial.screenshots}
size={72}
/>
) : Array.isArray(consignmentInfo?.screenshots) &&
consignmentInfo.screenshots.length > 0 ? (
<ImagePreviewList
imageUrls={(consignmentInfo.screenshots as string[]).filter(
(item) => typeof item === 'string' && item.trim(),
)}
size={72}
/>
) : (
<Typography.Text type="secondary">未提交</Typography.Text>
)}
@@ -3429,49 +3438,36 @@ export default function WorkOrdersPanel() {
backgroundColor: '#fff',
}}
>
{inv.workerId ? (
<>
<Button
block
type="primary"
icon={<TeamOutlined />}
style={{ backgroundColor: '#722ed1', borderColor: '#722ed1' }}
loading={consignmentInterceptModal.submitting}
onClick={() =>
confirmConsignmentMatchAction({
kind: 'assign_worker',
matchType: m.matchType,
consignmentNo: inv.sourceSubmissionNo || '客服手工入库',
workerName: inv.workerName,
depositFen: inv.depositAmountFen,
onOk: () => {
void handleAssignWithConsignmentWorker(
inv.id,
inv.workerId!,
!isExact,
)
},
})
}
>
走接单平台指派打手
</Button>
<div
style={{
fontSize: 11,
color: '#8c8c8c',
marginTop: 6,
textAlign: 'center',
}}
>
指派供货打手履约 · 自动冻结履约押金
</div>
</>
) : (
<div style={{ fontSize: 12, color: '#cf1322', textAlign: 'center' }}>
该现货未关联供货打手,无法走平台指派;请先关联打手
</div>
)}
<Button
block
type="primary"
icon={<PushpinOutlined />}
style={{ backgroundColor: '#52c41a', borderColor: '#52c41a' }}
loading={consignmentInterceptModal.submitting}
onClick={() =>
confirmConsignmentMatchAction({
kind: 'internal_stock',
matchType: m.matchType,
consignmentNo: inv.sourceSubmissionNo || '客服手工入库',
costPriceFen: inv.costPriceFen,
onOk: () => {
void handleFulfillWithConsignment(inv.id, !isExact)
},
})
}
>
内部现货直接履约
</Button>
<div
style={{
fontSize: 11,
color: '#8c8c8c',
marginTop: 6,
textAlign: 'center',
}}
>
免外派佣金 · 不扣打手押金 · 自动进冷静期
</div>
</div>
</div>
</div>
@@ -245,6 +245,17 @@ function GiftProgressCard({ order }: { order: WorkOrder }) {
<div style={{ marginTop: 8 }}>
<ImagePreviewList files={gift.boosterMaterial.screenshots} size={64} />
</div>
) : Array.isArray(
(order.material as Record<string, any> | undefined)?.consignment?.screenshots,
) &&
((order.material as Record<string, any>).consignment.screenshots as unknown[])
.length > 0 ? (
<div style={{ marginTop: 8 }}>
<ImagePreviewList
imageUrls={(order.material as Record<string, any>).consignment.screenshots}
size={64}
/>
</div>
) : null}
</div>
</div>