feat(consignment): 拼单一次拼满,内部匹配到期统一回大厅

- 拼单(自动/手动)先规划再一次落库,凑不齐整单失败并回滚,不再进入 internal_matching

- 删除「超时后回大厅」开关与 fallbackToHall 配置,到期统一回大厅(迁移 152 清理历史字段)
This commit is contained in:
2026-11-07 14:27:34 +08:00
parent 09f47a3be4
commit 8920afa0e5
11 changed files with 438 additions and 270 deletions
@@ -90,7 +90,7 @@ type ConfigFormValues = {
exactOnly: boolean
preferLowReward: boolean
}
internalMatch: { delayMinutes: number; fallbackToHall: boolean }
internalMatch: { delayMinutes: number }
timeout: {
hours: number
policy: 'reopen' | 'cancel_release' | 'cancel_deduct' | 'rematch_release' | 'rematch_deduct'
@@ -201,7 +201,15 @@ function toFormValues(profile: ConsignmentProfile): ConfigFormValues {
exactOnly: config.matching.exactOnly,
preferLowReward: config.matching.preferLowReward,
},
internalMatch: { ...config.internalMatch },
internalMatch: {
// 内部等待只属于手动匹配;非手动模式切回手动时给默认 30 分钟。
delayMinutes:
config.matching.mode === 'manual'
? Number(config.internalMatch.delayMinutes || 0)
: Number(config.internalMatch.delayMinutes) > 0
? Number(config.internalMatch.delayMinutes)
: 30,
},
timeout: {
hours: Math.max(0, Number(config.timeout.minutes ?? 30) / 60),
policy: config.timeout.policy,
@@ -234,7 +242,7 @@ function emptyFormValues(): ConfigFormValues {
exactOnly: true,
preferLowReward: true,
},
internalMatch: { delayMinutes: 30, fallbackToHall: true },
internalMatch: { delayMinutes: 30 },
timeout: { hours: 0.5, policy: 'reopen' },
priceDrop: { enabled: true, hours: 24, mode: 'percent', value: 10 },
}
@@ -303,11 +311,9 @@ function buildPayload(
preferLowReward: values.matching.preferLowReward,
},
internalMatch: {
// 关闭匹配时不再等待内部库存,直接进大厅。
// 内部等待只属于手动匹配:关闭匹配不再等待;自动匹配不使用等待窗口。
delayMinutes:
values.matching.mode === 'off' ? 0 : Number(values.internalMatch.delayMinutes || 0),
fallbackToHall:
values.matching.mode === 'off' ? false : values.internalMatch.fallbackToHall,
},
timeout: {
minutes: Math.max(0, Math.round(Number(values.timeout.hours || 0) * 60)),
@@ -1720,7 +1726,7 @@ export default function ConsignmentConfigPanel() {
<ConsignmentSectionTitle
index="08"
title="匹配设置"
description="匹配模式决定新单是否进入内部库存匹配队列"
description="匹配模式决定新单如何匹配库存;内部等待仅手动匹配使用"
/>
</div>
<div style={gridStyle}>
@@ -1755,41 +1761,21 @@ export default function ConsignmentConfigPanel() {
>
<Switch checkedChildren="优先" unCheckedChildren="不优先" />
</Form.Item>
<Form.Item
label="手动匹配等待(分钟)"
name={['internalMatch', 'delayMinutes']}
extra={
matchingMode === 'auto'
? '自动匹配在发布时立即匹配,未命中直接进大厅,不使用等待窗口'
: '仅手动匹配生效:工单在内部池等待运营挑库存'
}
style={{ marginBottom: 0 }}
>
<InputNumber
min={0}
precision={0}
style={{ width: '100%' }}
addonAfter="分钟"
disabled={matchingMode === 'auto'}
/>
</Form.Item>
<Form.Item
label="超时后回大厅"
name={['internalMatch', 'fallbackToHall']}
valuePropName="checked"
extra={
matchingMode === 'auto'
? '自动匹配未命中直接进大厅,不涉及此策略'
: '关闭后超时重新匹配(最多 3 次),仍未命中回大厅'
}
style={{ marginBottom: 0 }}
>
<Switch
checkedChildren="回大厅"
unCheckedChildren="重新匹配"
disabled={matchingMode === 'auto'}
/>
</Form.Item>
{matchingMode === 'manual' ? (
<Form.Item
label="手动匹配等待(分钟)"
name={['internalMatch', 'delayMinutes']}
extra="仅手动匹配生效:工单在内部池等待运营挑库存"
style={{ marginBottom: 0 }}
>
<InputNumber
min={0}
precision={0}
style={{ width: '100%' }}
addonAfter="分钟"
/>
</Form.Item>
) : null}
</>
) : null}
</div>
@@ -1801,6 +1787,14 @@ export default function ConsignmentConfigPanel() {
message="关闭匹配后:新单直接进入接单大厅,不等待内部库存,也不会自动匹配。"
/>
) : null}
{matchingMode === 'auto' ? (
<Alert
type="info"
showIcon
style={{ marginTop: 12 }}
message="自动匹配:发布/补资料时立即匹配,未命中直接进大厅,不使用内部等待窗口。"
/>
) : null}
</section>
<section className="order-service-section">
+1 -1
View File
@@ -418,7 +418,7 @@ export type ConsignmentProfileConfig = {
exactOnly: boolean
preferLowReward: boolean
}
internalMatch: { delayMinutes: number; fallbackToHall: boolean }
internalMatch: { delayMinutes: number }
timeout: {
minutes: number
policy: 'reopen' | 'cancel_release' | 'cancel_deduct' | 'rematch_release' | 'rematch_deduct'
@@ -10,21 +10,15 @@ function readPanelSource() {
).replace(/\r\n/g, '\n')
}
test('自动匹配:手动等待与超时策略在 auto 下禁用并给出说明', () => {
test('手动匹配:只保留等待时长,不再提供「超时后回大厅」开关', () => {
const source = readPanelSource()
assert.ok(source.includes('label="手动匹配等待(分钟)"'), '等待时长文案必须明确只作用于手动匹配')
assert.ok(source.includes('label="超时后回大厅"'), '必须提供超时回大厅开关')
assert.ok(source.includes("matchingMode === 'manual' ? ("), '等待时长必须只在手动匹配下展示')
assert.ok(!source.includes('超时后回大厅'), '回大厅是固定行为,不应再有开关')
assert.ok(!source.includes('fallbackToHall'), '配置与表单必须移除 fallbackToHall')
assert.ok(
source.includes('自动匹配在发布时立即匹配,未命中直接进大厅,不使用等待窗口'),
source.includes('自动匹配:发布/补资料时立即匹配,未命中直接进大厅,不使用内部等待窗口。'),
'auto 模式必须说明不使用等待窗口',
)
assert.ok(
source.includes('关闭后超时重新匹配(最多 3 次),仍未命中回大厅'),
'关闭回大厅时必须说明重试上限',
)
assert.ok(
(source.match(/disabled=\{matchingMode === 'auto'\}/g) || []).length >= 2,
'等待时长与超时策略在 auto 下都必须禁用',
)
assert.ok(!source.includes('label="内部匹配等待(分钟)"'), '旧文案必须移除')
})