|
|
|
@@ -1,4 +1,6 @@
|
|
|
|
|
import {
|
|
|
|
|
ArrowDownOutlined,
|
|
|
|
|
ArrowUpOutlined,
|
|
|
|
|
DeleteOutlined,
|
|
|
|
|
PlusOutlined,
|
|
|
|
|
ReloadOutlined,
|
|
|
|
@@ -30,9 +32,12 @@ import {
|
|
|
|
|
fetchAdminCloudtentaclesOverrideRules,
|
|
|
|
|
fetchAdminCloudtentaclesSkuList,
|
|
|
|
|
fetchAdminCloudtentaclesSourceConfig,
|
|
|
|
|
fetchAdminFulfillmentRoutingConfig,
|
|
|
|
|
fetchAdminKuaishouFeifeiConfig,
|
|
|
|
|
matchAdminKuaishouFeifeiProduct,
|
|
|
|
|
previewAdminFulfillmentRouting,
|
|
|
|
|
saveAdminCloudtentaclesOverrideRules,
|
|
|
|
|
saveAdminFulfillmentRoutingConfig,
|
|
|
|
|
syncAdminKuaishouFeifeiProducts,
|
|
|
|
|
} from '@/services/admin'
|
|
|
|
|
import type {
|
|
|
|
@@ -42,6 +47,9 @@ import type {
|
|
|
|
|
AdminCloudtentaclesSessionsMap,
|
|
|
|
|
AdminCloudtentaclesSkuItem,
|
|
|
|
|
AdminCloudtentaclesSourceItem,
|
|
|
|
|
AdminFulfillmentRoutingConfig,
|
|
|
|
|
AdminFulfillmentRoutingPreviewResult,
|
|
|
|
|
AdminFulfillmentRoutingRule,
|
|
|
|
|
AdminKuaishouFeifeiConfigResponse,
|
|
|
|
|
AdminKuaishouFeifeiMatchResult,
|
|
|
|
|
AdminKuaishouFeifeiProductRule,
|
|
|
|
@@ -51,18 +59,27 @@ import { hasAdminRole } from '@/utils/admin-auth'
|
|
|
|
|
type EditableOverrideRule = Omit<AdminCloudtentaclesOverrideRule, 'normalizedProductName'> & {
|
|
|
|
|
normalizedProductName?: string
|
|
|
|
|
}
|
|
|
|
|
type EditableRoutingRule = Omit<AdminFulfillmentRoutingRule, 'normalizedProductName'> & {
|
|
|
|
|
normalizedProductName?: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type SourceRow = AdminCloudtentaclesSourceItem & {
|
|
|
|
|
session: AdminCloudtentaclesSessionItem | null
|
|
|
|
|
ready: boolean
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ROUTING_EXECUTORS = [
|
|
|
|
|
{ key: 'kuaishou_ct_assisted', label: 'kuaishou-lewan', color: 'blue' },
|
|
|
|
|
{ key: 'kuaishou_feifei', label: 'kuaishou-feifei', color: 'purple' },
|
|
|
|
|
{ key: 'manual_dispatch', label: '人工履约', color: 'orange' },
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
export default function AdminPlatformFulfillmentPage() {
|
|
|
|
|
return (
|
|
|
|
|
<section className="page-stack">
|
|
|
|
|
<PageHeader
|
|
|
|
|
title="履约配置"
|
|
|
|
|
description="维护 91 卡券商品到 kuaishou-feifei 与 cloudtentacles 的履约映射规则。"
|
|
|
|
|
description="维护 91 卡券商品到 kuaishou-feifei 与 kuaishou-lewan 的履约映射规则。"
|
|
|
|
|
/>
|
|
|
|
|
|
|
|
|
|
{!hasAdminRole('admin') ? (
|
|
|
|
@@ -74,6 +91,11 @@ export default function AdminPlatformFulfillmentPage() {
|
|
|
|
|
className="platform-tabs"
|
|
|
|
|
destroyOnHidden={false}
|
|
|
|
|
items={[
|
|
|
|
|
{
|
|
|
|
|
key: 'fulfillment-routing',
|
|
|
|
|
label: '履约路由',
|
|
|
|
|
children: <FulfillmentRoutingPanel />,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'kuaishou-feifei',
|
|
|
|
|
label: 'kuaishou-feifei',
|
|
|
|
@@ -81,7 +103,7 @@ export default function AdminPlatformFulfillmentPage() {
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
key: 'kuaishou-cloud',
|
|
|
|
|
label: 'kuaishou-cloud',
|
|
|
|
|
label: 'kuaishou-lewan',
|
|
|
|
|
children: <KuaishouCloudFulfillmentPanel />,
|
|
|
|
|
},
|
|
|
|
|
]}
|
|
|
|
@@ -91,6 +113,384 @@ export default function AdminPlatformFulfillmentPage() {
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function FulfillmentRoutingPanel() {
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [saving, setSaving] = useState(false)
|
|
|
|
|
const [previewing, setPreviewing] = useState(false)
|
|
|
|
|
const [errorMessage, setErrorMessage] = useState('')
|
|
|
|
|
const [filePath, setFilePath] = useState('')
|
|
|
|
|
const [enabled, setEnabled] = useState(true)
|
|
|
|
|
const [priority, setPriority] = useState<string[]>(['kuaishou_ct_assisted', 'kuaishou_feifei'])
|
|
|
|
|
const [unmatchedExecutorKey, setUnmatchedExecutorKey] = useState('')
|
|
|
|
|
const [executors, setExecutors] = useState<AdminFulfillmentRoutingConfig['executors']>({})
|
|
|
|
|
const [rules, setRules] = useState<EditableRoutingRule[]>([])
|
|
|
|
|
const [previewInput, setPreviewInput] = useState('')
|
|
|
|
|
const [previewResult, setPreviewResult] = useState<AdminFulfillmentRoutingPreviewResult | null>(null)
|
|
|
|
|
|
|
|
|
|
const enabledRuleCount = rules.filter((rule) => rule.enabled !== false).length
|
|
|
|
|
const selectedExecutorKey = previewResult?.fulfillmentRoute?.selectedExecutorKey || ''
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
void loadConfig()
|
|
|
|
|
}, [])
|
|
|
|
|
|
|
|
|
|
async function loadConfig() {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
setErrorMessage('')
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetchAdminFulfillmentRoutingConfig()
|
|
|
|
|
hydrateConfig(response.data)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
setErrorMessage(error instanceof Error ? error.message : '读取履约路由配置失败')
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function saveConfig() {
|
|
|
|
|
setSaving(true)
|
|
|
|
|
setErrorMessage('')
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await saveAdminFulfillmentRoutingConfig({
|
|
|
|
|
enabled,
|
|
|
|
|
defaultExecutorPriority: priority,
|
|
|
|
|
unmatchedExecutorKey,
|
|
|
|
|
executors,
|
|
|
|
|
rules: rules.map((rule) => ({
|
|
|
|
|
id: rule.id,
|
|
|
|
|
enabled: rule.enabled !== false,
|
|
|
|
|
productName: rule.productName,
|
|
|
|
|
normalizedProductName: rule.normalizedProductName || '',
|
|
|
|
|
matchType: rule.matchType,
|
|
|
|
|
executorKey: rule.executorKey,
|
|
|
|
|
priority: rule.priority,
|
|
|
|
|
notes: rule.notes,
|
|
|
|
|
})),
|
|
|
|
|
})
|
|
|
|
|
hydrateConfig(response.data)
|
|
|
|
|
showSuccess('履约路由配置已保存')
|
|
|
|
|
} catch (error) {
|
|
|
|
|
const message = error instanceof Error ? error.message : '保存履约路由配置失败'
|
|
|
|
|
setErrorMessage(message)
|
|
|
|
|
showError(message)
|
|
|
|
|
} finally {
|
|
|
|
|
setSaving(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function previewRoute() {
|
|
|
|
|
const productName = previewInput.trim()
|
|
|
|
|
if (!productName) {
|
|
|
|
|
showError('请输入 91 商品名')
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setPreviewing(true)
|
|
|
|
|
setPreviewResult(null)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const response = await previewAdminFulfillmentRouting(productName)
|
|
|
|
|
setPreviewResult(response.data)
|
|
|
|
|
} catch (error) {
|
|
|
|
|
showError(error instanceof Error ? error.message : '预览履约路由失败')
|
|
|
|
|
} finally {
|
|
|
|
|
setPreviewing(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function hydrateConfig(data: AdminFulfillmentRoutingConfig) {
|
|
|
|
|
setFilePath(data.filePath || '')
|
|
|
|
|
setEnabled(data.enabled !== false)
|
|
|
|
|
setPriority(normalizeRoutingPriority(data.defaultExecutorPriority))
|
|
|
|
|
setUnmatchedExecutorKey(data.unmatchedExecutorKey || '')
|
|
|
|
|
setExecutors(normalizeRoutingExecutors(data.executors))
|
|
|
|
|
setRules(Array.isArray(data.rules) ? data.rules.map((rule) => normalizeRoutingRule(rule)) : [])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function movePriority(executorKey: string, offset: -1 | 1) {
|
|
|
|
|
const currentIndex = priority.indexOf(executorKey)
|
|
|
|
|
const nextIndex = currentIndex + offset
|
|
|
|
|
if (currentIndex < 0 || nextIndex < 0 || nextIndex >= priority.length) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const next = [...priority]
|
|
|
|
|
const [item] = next.splice(currentIndex, 1)
|
|
|
|
|
next.splice(nextIndex, 0, item)
|
|
|
|
|
setPriority(next)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateExecutorEnabled(executorKey: string, nextEnabled: boolean) {
|
|
|
|
|
setExecutors({
|
|
|
|
|
...executors,
|
|
|
|
|
[executorKey]: {
|
|
|
|
|
enabled: nextEnabled,
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function updateRule(index: number, patch: Partial<EditableRoutingRule>) {
|
|
|
|
|
setRules((current) =>
|
|
|
|
|
current.map((rule, ruleIndex) =>
|
|
|
|
|
ruleIndex === index
|
|
|
|
|
? {
|
|
|
|
|
...rule,
|
|
|
|
|
...patch,
|
|
|
|
|
}
|
|
|
|
|
: rule,
|
|
|
|
|
),
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function addRule() {
|
|
|
|
|
const productName = previewInput.trim()
|
|
|
|
|
const nextId = `${Date.now()}-${rules.length + 1}`
|
|
|
|
|
setRules([
|
|
|
|
|
{
|
|
|
|
|
id: nextId,
|
|
|
|
|
enabled: true,
|
|
|
|
|
productName,
|
|
|
|
|
normalizedProductName: '',
|
|
|
|
|
matchType: 'exact',
|
|
|
|
|
executorKey: 'kuaishou_feifei',
|
|
|
|
|
priority: 100,
|
|
|
|
|
notes: '',
|
|
|
|
|
},
|
|
|
|
|
...rules,
|
|
|
|
|
])
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function removeRule(index: number) {
|
|
|
|
|
setRules((current) => current.filter((_, ruleIndex) => ruleIndex !== index))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (loading) {
|
|
|
|
|
return <Spin />
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<section className="platform-panel-stack">
|
|
|
|
|
{errorMessage ? <Alert type="error" showIcon message={errorMessage} /> : null}
|
|
|
|
|
|
|
|
|
|
<div className="metric-grid four">
|
|
|
|
|
<MetricCard label="路由状态" value={enabled ? '启用' : '停用'} detail={filePath || '默认配置'} />
|
|
|
|
|
<MetricCard label="商品规则" value={String(rules.length)} detail={`启用 ${enabledRuleCount} 条`} />
|
|
|
|
|
<MetricCard
|
|
|
|
|
label="全局优先级"
|
|
|
|
|
value={`${priority.length} 个通道`}
|
|
|
|
|
detail={priority.map(formatRoutingExecutorLabel).join(' > ') || '未配置'}
|
|
|
|
|
/>
|
|
|
|
|
<Card>
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Button icon={<ReloadOutlined />} loading={loading} onClick={loadConfig}>
|
|
|
|
|
刷新
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
|
|
|
|
|
保存配置
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card title="通道开关">
|
|
|
|
|
<div className="platform-routing-grid">
|
|
|
|
|
<div className="field-switch">
|
|
|
|
|
<span>履约路由</span>
|
|
|
|
|
<Switch checked={enabled} checkedChildren="启用" unCheckedChildren="停用" onChange={setEnabled} />
|
|
|
|
|
</div>
|
|
|
|
|
{ROUTING_EXECUTORS.map((executor) => (
|
|
|
|
|
<div key={executor.key} className="field-switch">
|
|
|
|
|
<span>{executor.label}</span>
|
|
|
|
|
<Switch
|
|
|
|
|
checked={executors[executor.key]?.enabled !== false}
|
|
|
|
|
checkedChildren="启用"
|
|
|
|
|
unCheckedChildren="停用"
|
|
|
|
|
onChange={(checked) => updateExecutorEnabled(executor.key, checked)}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</div>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card title="全局优先级">
|
|
|
|
|
<Space direction="vertical" className="full-width" size={10}>
|
|
|
|
|
{priority.map((executorKey, index) => (
|
|
|
|
|
<div key={executorKey} className="platform-routing-priority-row">
|
|
|
|
|
<Space>
|
|
|
|
|
<Tag color={resolveRoutingExecutorColor(executorKey)}>
|
|
|
|
|
{index + 1}. {formatRoutingExecutorLabel(executorKey)}
|
|
|
|
|
</Tag>
|
|
|
|
|
</Space>
|
|
|
|
|
<Space>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<ArrowUpOutlined />}
|
|
|
|
|
disabled={index === 0}
|
|
|
|
|
onClick={() => movePriority(executorKey, -1)}
|
|
|
|
|
/>
|
|
|
|
|
<Button
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<ArrowDownOutlined />}
|
|
|
|
|
disabled={index === priority.length - 1}
|
|
|
|
|
onClick={() => movePriority(executorKey, 1)}
|
|
|
|
|
/>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
<Select
|
|
|
|
|
value={unmatchedExecutorKey}
|
|
|
|
|
options={[
|
|
|
|
|
{ label: '未命中时保持未配置', value: '' },
|
|
|
|
|
{ label: '未命中时转人工履约', value: 'manual_dispatch' },
|
|
|
|
|
]}
|
|
|
|
|
onChange={(value) => setUnmatchedExecutorKey(String(value || ''))}
|
|
|
|
|
/>
|
|
|
|
|
</Space>
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card
|
|
|
|
|
title="路由预览"
|
|
|
|
|
extra={
|
|
|
|
|
<Button icon={<PlusOutlined />} onClick={addRule}>
|
|
|
|
|
新增规则
|
|
|
|
|
</Button>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
<Space.Compact className="full-width">
|
|
|
|
|
<Input
|
|
|
|
|
allowClear
|
|
|
|
|
value={previewInput}
|
|
|
|
|
placeholder="91 商品名 / productNo"
|
|
|
|
|
onChange={(event) => setPreviewInput(event.target.value)}
|
|
|
|
|
onPressEnter={previewRoute}
|
|
|
|
|
/>
|
|
|
|
|
<Button icon={<SearchOutlined />} loading={previewing} onClick={previewRoute}>
|
|
|
|
|
预览路由
|
|
|
|
|
</Button>
|
|
|
|
|
</Space.Compact>
|
|
|
|
|
|
|
|
|
|
{previewResult ? (
|
|
|
|
|
<Alert
|
|
|
|
|
className="platform-section-gap"
|
|
|
|
|
type={previewResult.isConfigured ? 'success' : 'warning'}
|
|
|
|
|
showIcon
|
|
|
|
|
message={
|
|
|
|
|
previewResult.isConfigured
|
|
|
|
|
? `最终路由:${formatRoutingExecutorLabel(selectedExecutorKey)}`
|
|
|
|
|
: '未命中可用履约通道'
|
|
|
|
|
}
|
|
|
|
|
description={
|
|
|
|
|
<Space direction="vertical" size={4}>
|
|
|
|
|
<Typography.Text>
|
|
|
|
|
{previewResult.fulfillmentRoute?.reason || previewResult.normalizedProductName}
|
|
|
|
|
</Typography.Text>
|
|
|
|
|
<Space wrap>
|
|
|
|
|
{previewResult.fulfillmentRoute?.candidates.map((candidate) => (
|
|
|
|
|
<Tag
|
|
|
|
|
key={candidate.executorKey}
|
|
|
|
|
color={candidate.available ? 'green' : 'default'}
|
|
|
|
|
>
|
|
|
|
|
{formatRoutingExecutorLabel(candidate.executorKey)}
|
|
|
|
|
{candidate.available ? ' 可用' : ' 未命中'}
|
|
|
|
|
</Tag>
|
|
|
|
|
))}
|
|
|
|
|
</Space>
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
) : null}
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
<Card
|
|
|
|
|
title="商品路由规则"
|
|
|
|
|
extra={
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Button icon={<PlusOutlined />} onClick={addRule}>
|
|
|
|
|
新增规则
|
|
|
|
|
</Button>
|
|
|
|
|
<Button type="primary" icon={<SaveOutlined />} loading={saving} onClick={saveConfig}>
|
|
|
|
|
保存规则
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
}
|
|
|
|
|
>
|
|
|
|
|
{rules.length === 0 ? (
|
|
|
|
|
<Empty description="暂无商品路由规则" />
|
|
|
|
|
) : (
|
|
|
|
|
<Space direction="vertical" className="full-width" size={12}>
|
|
|
|
|
{rules.map((rule, index) => (
|
|
|
|
|
<div key={rule.id} className="platform-route-rule">
|
|
|
|
|
<div className="platform-route-rule-head">
|
|
|
|
|
<Space wrap>
|
|
|
|
|
<Switch
|
|
|
|
|
size="small"
|
|
|
|
|
checked={rule.enabled !== false}
|
|
|
|
|
onChange={(checked) => updateRule(index, { enabled: checked })}
|
|
|
|
|
/>
|
|
|
|
|
<Typography.Text strong>{rule.productName || '未命名规则'}</Typography.Text>
|
|
|
|
|
</Space>
|
|
|
|
|
<Button
|
|
|
|
|
danger
|
|
|
|
|
size="small"
|
|
|
|
|
icon={<DeleteOutlined />}
|
|
|
|
|
onClick={() => removeRule(index)}
|
|
|
|
|
>
|
|
|
|
|
删除
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
<div className="platform-route-rule-grid">
|
|
|
|
|
<Input
|
|
|
|
|
value={rule.productName}
|
|
|
|
|
placeholder="91 商品名 / productNo"
|
|
|
|
|
onChange={(event) => updateRule(index, { productName: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
<Select
|
|
|
|
|
value={rule.matchType}
|
|
|
|
|
options={[
|
|
|
|
|
{ label: '精确', value: 'exact' },
|
|
|
|
|
{ label: '包含', value: 'contains' },
|
|
|
|
|
]}
|
|
|
|
|
onChange={(matchType) =>
|
|
|
|
|
updateRule(index, {
|
|
|
|
|
matchType: matchType === 'contains' ? 'contains' : 'exact',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<Select
|
|
|
|
|
value={rule.executorKey}
|
|
|
|
|
options={ROUTING_EXECUTORS.map((executor) => ({
|
|
|
|
|
label: executor.label,
|
|
|
|
|
value: executor.key,
|
|
|
|
|
}))}
|
|
|
|
|
onChange={(executorKey) =>
|
|
|
|
|
updateRule(index, { executorKey: String(executorKey || '') })
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
<InputNumber
|
|
|
|
|
value={rule.priority}
|
|
|
|
|
min={0}
|
|
|
|
|
max={9999}
|
|
|
|
|
onChange={(priorityValue) =>
|
|
|
|
|
updateRule(index, { priority: Number(priorityValue || 0) || 0 })
|
|
|
|
|
}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
<Input
|
|
|
|
|
value={rule.notes}
|
|
|
|
|
placeholder="备注"
|
|
|
|
|
onChange={(event) => updateRule(index, { notes: event.target.value })}
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
))}
|
|
|
|
|
</Space>
|
|
|
|
|
)}
|
|
|
|
|
</Card>
|
|
|
|
|
</section>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function KuaishouFeifeiFulfillmentPanel() {
|
|
|
|
|
const [loading, setLoading] = useState(true)
|
|
|
|
|
const [syncing, setSyncing] = useState(false)
|
|
|
|
@@ -791,6 +1191,58 @@ function MetricCard({ label, value, detail }: { label: string; value: string; de
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoutingPriority(value: unknown) {
|
|
|
|
|
const rawItems = Array.isArray(value) ? value : []
|
|
|
|
|
const allowed = ROUTING_EXECUTORS
|
|
|
|
|
.filter((executor) => executor.key !== 'manual_dispatch')
|
|
|
|
|
.map((executor) => executor.key)
|
|
|
|
|
const seen = new Set<string>()
|
|
|
|
|
const items = rawItems
|
|
|
|
|
.map((item) => String(item || '').trim())
|
|
|
|
|
.filter((item) => allowed.includes(item) && !seen.has(item) && (seen.add(item), true))
|
|
|
|
|
|
|
|
|
|
return items.length > 0 ? items : ['kuaishou_ct_assisted', 'kuaishou_feifei']
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoutingExecutors(value: unknown): AdminFulfillmentRoutingConfig['executors'] {
|
|
|
|
|
const source = value && typeof value === 'object' && !Array.isArray(value)
|
|
|
|
|
? value as AdminFulfillmentRoutingConfig['executors']
|
|
|
|
|
: {}
|
|
|
|
|
|
|
|
|
|
return ROUTING_EXECUTORS.reduce<AdminFulfillmentRoutingConfig['executors']>((result, executor) => {
|
|
|
|
|
result[executor.key] = {
|
|
|
|
|
enabled: source[executor.key]?.enabled !== false,
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
}, {})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeRoutingRule(rule: AdminFulfillmentRoutingRule): EditableRoutingRule {
|
|
|
|
|
const productName = String(rule.productName || '').trim()
|
|
|
|
|
const executorKey = String(rule.executorKey || '').trim()
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id: String(rule.id || `${productName}-${executorKey}` || createRuleId()).trim(),
|
|
|
|
|
enabled: rule.enabled !== false,
|
|
|
|
|
productName,
|
|
|
|
|
normalizedProductName: String(rule.normalizedProductName || '').trim(),
|
|
|
|
|
matchType: rule.matchType === 'contains' ? 'contains' : 'exact',
|
|
|
|
|
executorKey: executorKey || 'kuaishou_feifei',
|
|
|
|
|
priority: Number(rule.priority || 0) || 0,
|
|
|
|
|
notes: String(rule.notes || '').trim(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function formatRoutingExecutorLabel(executorKey: unknown) {
|
|
|
|
|
const key = String(executorKey || '').trim()
|
|
|
|
|
return ROUTING_EXECUTORS.find((executor) => executor.key === key)?.label || key || '-'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function resolveRoutingExecutorColor(executorKey: unknown) {
|
|
|
|
|
const key = String(executorKey || '').trim()
|
|
|
|
|
return ROUTING_EXECUTORS.find((executor) => executor.key === key)?.color || 'default'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function normalizeFeifeiRule(
|
|
|
|
|
rule: Partial<AdminKuaishouFeifeiProductRule>,
|
|
|
|
|
): AdminKuaishouFeifeiProductRule {
|
|
|
|
|