寄卖手动匹配改走内部现货并统一展示与超时口径

- 手动匹配不再指派供货打手:停在待加好友,清空截止时间与超时分钟
- 物品名模板去掉「0 璀璨」残留;寄卖单号无提报号时回退库存编号
- 状态列按寄卖库存显示内部匹配,打手列保留供货打手标签
- 超时配置增加启用开关,默认关闭(不限时)
- 补充前后端回归测试
This commit is contained in:
2026-11-14 13:15:41 +08:00
parent a0c7c90edf
commit 0dff00de97
20 changed files with 631 additions and 91 deletions
@@ -101,6 +101,7 @@ type ConfigFormValues = {
orderEffectiveFrom: Dayjs
}
timeout: {
enabled: boolean
hours: number
policy: 'reopen' | 'cancel_release' | 'cancel_deduct' | 'rematch_release' | 'rematch_deduct'
}
@@ -216,7 +217,8 @@ function toFormValues(profile: ConsignmentProfile): ConfigFormValues {
orderEffectiveFrom: dayjs(config.matching.orderEffectiveFrom || undefined),
},
timeout: {
hours: Math.max(0, Number(config.timeout.minutes ?? 30) / 60),
enabled: Number(config.timeout.minutes ?? 0) > 0,
hours: Number(config.timeout.minutes ?? 0) > 0 ? Number(config.timeout.minutes) / 60 : 0.5,
policy: config.timeout.policy,
},
priceDrop: { ...config.priceDrop },
@@ -255,7 +257,7 @@ function emptyFormValues(): ConfigFormValues {
preferLowReward: true,
orderEffectiveFrom: dayjs(),
},
timeout: { hours: 0.5, policy: 'reopen' },
timeout: { enabled: false, hours: 0.5, policy: 'reopen' },
priceDrop: { enabled: true, hours: 24, mode: 'percent', value: 10 },
}
}
@@ -338,7 +340,10 @@ function buildPayload(
values.matching.orderEffectiveFrom?.toISOString() || new Date().toISOString(),
},
timeout: {
minutes: Math.max(0, Math.round(Number(values.timeout.hours || 0) * 60)),
// 开关关闭 = 不限时(minutes=0),后端不会生成截止时间,也不会触发超时处置。
minutes: values.timeout.enabled
? Math.max(1, Math.round(Number(values.timeout.hours || 0) * 60))
: 0,
policy: values.timeout.policy,
},
priceDrop: {
@@ -1027,6 +1032,7 @@ export default function ConsignmentConfigPanel() {
const serviceFeeMode = Form.useWatch(['serviceFee', 'mode'], form)
const inventoryLimitEnabled = Form.useWatch(['inventoryLimit', 'enabled'], form)
const autoMatchEnabled = Form.useWatch(['matching', 'autoMatchEnabled'], form)
const timeoutEnabled = Form.useWatch(['timeout', 'enabled'], form)
const priceDropEnabled = Form.useWatch(['priceDrop', 'enabled'], form)
const priceDropMode = Form.useWatch(['priceDrop', 'mode'], form)
const headerStatus: ProfileStatusInfo = selectedProfile
@@ -1977,47 +1983,62 @@ export default function ConsignmentConfigPanel() {
<ConsignmentSectionTitle
index="09"
title="超时设置"
description="匹配成功进入代练中后的时限与超时处置动作"
description="默认关闭(不限时);开启后按小时计时并执行超时动作"
/>
</div>
<div style={gridStyle}>
<Form.Item
label="代练超时(小时)"
name={['timeout', 'hours']}
rules={[{ type: 'number', min: 0 }]}
extra="默认 0.5 小时;到点后按下方超时动作处置"
style={{ marginBottom: 0 }}
label="启用超时"
name={['timeout', 'enabled']}
valuePropName="checked"
extra="关闭后订单进入代练中不设截止时间(不限时),也不会自动超时处置"
style={{ marginBottom: 0, gridColumn: '1 / -1' }}
>
<InputNumber
min={0}
step={0.5}
precision={1}
style={{ width: '100%' }}
addonAfter="小时"
/>
</Form.Item>
<Form.Item
label="超时动作"
name={['timeout', 'policy']}
style={{ marginBottom: 0 }}
>
<Select
options={[
{ value: 'reopen', label: '退回大厅(退还押金)' },
{ value: 'cancel_release', label: '取消并释放押金' },
{ value: 'cancel_deduct', label: '取消并扣除押金' },
{ value: 'rematch_release', label: '重新匹配并退还押金' },
{ value: 'rematch_deduct', label: '重新匹配并扣除押金' },
]}
/>
<Switch checkedChildren="开启" unCheckedChildren="关闭" />
</Form.Item>
{timeoutEnabled ? (
<>
<Form.Item
label="代练超时(小时)"
name={['timeout', 'hours']}
rules={[{ type: 'number', min: 0.1, message: '请填写大于 0 的小时数' }]}
extra="开启后按此小时数生成截止时间"
style={{ marginBottom: 0 }}
>
<InputNumber
min={0.1}
step={0.5}
precision={1}
style={{ width: '100%' }}
addonAfter="小时"
/>
</Form.Item>
<Form.Item
label="超时动作"
name={['timeout', 'policy']}
style={{ marginBottom: 0 }}
>
<Select
options={[
{ value: 'reopen', label: '退回大厅(退还押金)' },
{ value: 'cancel_release', label: '取消并释放押金' },
{ value: 'cancel_deduct', label: '取消并扣除押金' },
{ value: 'rematch_release', label: '重新匹配并退还押金' },
{ value: 'rematch_deduct', label: '重新匹配并扣除押金' },
]}
/>
</Form.Item>
</>
) : null}
</div>
<Alert
type="info"
showIcon
style={{ marginTop: 12 }}
message="重新匹配:订单回到内部匹配池,并排除原卖家与原库存,避免同一卖家立刻接回同一单。"
/>
{timeoutEnabled ? (
<Alert
type="info"
showIcon
style={{ marginTop: 12 }}
message="重新匹配:订单回到内部匹配池,并排除原卖家与原库存,避免同一卖家立刻接回同一单。"
/>
) : null}
</section>
<section className="order-service-section">
@@ -1428,7 +1428,7 @@ export default function ConsignmentInventoryPanel() {
) : null}
{viewingItem.sourceType === 'worker_portal' && viewingItem.createdAt ? (
<Descriptions.Item label="打手提报时间">
{formatDateTime(viewingItem.createdAt)}
{formatDateTime(viewingItem.sourceSubmissionCreatedAt || viewingItem.createdAt)}
</Descriptions.Item>
) : null}
<Descriptions.Item label="卖家微信号">
@@ -98,6 +98,7 @@ import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-pla
import { formatConsignmentContactSupporter } from '@/utils/consignment-contact'
import {
isConsignmentOrder,
isConsignmentStockOrder,
isInternalConsignmentOrder,
resolveConsignmentScreenshotUrls,
} from '@/utils/consignment-display'
@@ -245,13 +246,11 @@ async function copyTextToClipboard(text: string): Promise<boolean> {
}
/**
* 寄卖单号 = 来源提报单号(提报单号即寄卖单号),客服界面只认这一个号。
*
* 内部仓库号 INV… 仍留在数据里(`info.inventoryNo`,撤销/解绑链路还在用),
* 只是不再展示;历史老单没有 sourceSubmissionNo 时明确写出来,不给内部编号。
* 寄卖单号:优先来源提报单号(提报即寄卖);历史老单没有提报号时回退库存编号 INV…,
* 保证客服手里始终是一个可用于定位的真实单号。
*/
function resolveConsignmentNo(info: ConsignmentFulfillmentInfo) {
return info.sourceSubmissionNo || (info.inventoryNo ? '老单无提报单号' : '')
return info.sourceSubmissionNo || info.inventoryNo || ''
}
/**
@@ -1885,7 +1884,7 @@ export default function WorkOrdersPanel() {
置顶
</Tag>
) : null}
{isInternalConsignmentOrder(row) ? (
{isConsignmentStockOrder(row) ? (
<Tag color="cyan">内部匹配</Tag>
) : row.acceptanceMode === 'friend_gift' ? (
<Tag color="purple">好友赠送</Tag>
@@ -29,7 +29,36 @@ export function setSkipPublishConfirm() {
/** 渲染工单关联打手:普通打手 / 拼单参与打手 / 寄卖供货打手与现货资料 */
export function renderWorkOrderWorkers(order: WorkOrder) {
if (order.worker) return order.worker.displayName || order.worker.username || '-'
const consignmentDisplay = resolveConsignmentWorkerDisplay(order)
const assignedName = order.worker
? order.worker.displayName || order.worker.username || `打手#${order.worker.workerId}`
: ''
if (order.worker) {
const supplierId = consignmentDisplay?.workerId || 0
const assignedId = Number(order.worker.workerId || 0)
// 押金履约:接单人就是库存供货打手,仍按「供货打手」口径展示。
if (consignmentDisplay?.tagText === '供货打手' && supplierId > 0 && supplierId === assignedId) {
return renderConsignmentWorkerBlock(consignmentDisplay)
}
// 指派了别的打手:显示实际接单人,并补一行供货打手,避免丢掉货源信息。
if (consignmentDisplay?.tagText === '供货打手') {
const supplierText = `供货打手:${consignmentDisplay.primaryName}`
return (
<div className="cell-stack">
<Typography.Text ellipsis={{ tooltip: assignedName }}>{assignedName}</Typography.Text>
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: supplierText }}
>
{supplierText}
</Typography.Text>
</div>
)
}
return assignedName
}
if (order.sharing?.enabled && order.sharingWorkers?.length) {
const names = order.sharingWorkers
@@ -49,38 +78,40 @@ export function renderWorkOrderWorkers(order: WorkOrder) {
)
}
// 寄卖库存 / 内部现货履约单:回显供货打手或卖家现货资料
const consignmentDisplay = resolveConsignmentWorkerDisplay(order)
if (consignmentDisplay) {
return (
<div className="cell-stack">
<Space size={4} align="center" wrap>
<Tag
color="cyan"
style={{ margin: 0, fontSize: 11, padding: '0 4px', lineHeight: '18px' }}
>
{consignmentDisplay.tagText}
</Tag>
<Typography.Text ellipsis={{ tooltip: consignmentDisplay.primaryName }}>
{consignmentDisplay.primaryName}
</Typography.Text>
</Space>
{consignmentDisplay.secondaryInfo ? (
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: consignmentDisplay.secondaryInfo }}
>
{consignmentDisplay.secondaryInfo}
</Typography.Text>
) : null}
</div>
)
return renderConsignmentWorkerBlock(consignmentDisplay)
}
return '-'
}
/** 寄卖库存履约:供货打手标签 + 姓名 + 卖家资料 */
function renderConsignmentWorkerBlock(
display: NonNullable<ReturnType<typeof resolveConsignmentWorkerDisplay>>,
) {
return (
<div className="cell-stack">
<Space size={4} align="center" wrap>
<Tag color="cyan" style={{ margin: 0, fontSize: 11, padding: '0 4px', lineHeight: '18px' }}>
{display.tagText}
</Tag>
<Typography.Text ellipsis={{ tooltip: display.primaryName }}>
{display.primaryName}
</Typography.Text>
</Space>
{display.secondaryInfo ? (
<Typography.Text
type="secondary"
style={{ fontSize: 12 }}
ellipsis={{ tooltip: display.secondaryInfo }}
>
{display.secondaryInfo}
</Typography.Text>
) : null}
</div>
)
}
export function getDisplayOrderNo(
order: Pick<WorkOrder, 'workOrderNo' | 'platformOrderId'> | null | undefined,
): string {