优化接单商品匹配

This commit is contained in:
yml2213
2026-08-18 12:05:12 +08:00
parent 41b39bc5a0
commit f2c95bc3c8
15 changed files with 1756 additions and 96 deletions
@@ -11,6 +11,7 @@ import FinancePanel from './panels/FinancePanel'
import LevelsPanel from './panels/LevelsPanel'
import NotificationsPanel from './panels/NotificationsPanel'
import ProductRulesPanel from './panels/ProductRulesPanel'
import ProductMatchPanel from './panels/ProductMatchPanel'
import WorkersPanel from './panels/WorkersPanel'
import WorkOrdersPanel from './panels/WorkOrdersPanel'
@@ -48,7 +49,8 @@ export default function AdminWorkerPlatformPage() {
? [{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> }]
: [
{ key: 'orders', label: '接单工单', children: <WorkOrdersPanel /> },
{ key: 'rules', label: '物品规则', children: <ProductRulesPanel /> },
{ key: 'rules', label: '接单模板', children: <ProductRulesPanel /> },
{ key: 'product-match', label: '商品匹配', children: <ProductMatchPanel /> },
{ key: 'categories', label: '分类', children: <CategoriesPanel /> },
{ key: 'workers', label: '打手', children: <WorkersPanel /> },
{ key: 'finance', label: '资金', children: <FinancePanel /> },
@@ -69,6 +71,7 @@ function loadActiveTabPreference(): string {
const saved = localStorage.getItem(ACTIVE_TAB_STORAGE_KEY)
return saved === 'orders' ||
saved === 'rules' ||
saved === 'product-match' ||
saved === 'categories' ||
saved === 'workers' ||
saved === 'finance' ||
@@ -86,6 +89,7 @@ function isWorkerPlatformTab(tab: string | null): tab is string {
return [
'orders',
'rules',
'product-match',
'categories',
'workers',
'finance',
@@ -0,0 +1,623 @@
import { DeleteOutlined, PlayCircleOutlined, ReloadOutlined } from '@ant-design/icons'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import {
App,
Button,
Card,
Descriptions,
Form,
Input,
Modal,
Popconfirm,
Select,
Space,
Switch,
Table,
Tabs,
Tag,
Typography,
} from 'antd'
import type { TableColumnsType } from 'antd'
import { useState } from 'react'
import JsonPreview from '@/components/admin/JsonPreview'
import {
deleteAdminWorkProductRuleMapping,
fetchAdminKuaishouIndustryShops,
fetchAdminKuaishouMatchSources,
fetchAdminWorkProductMatchLogs,
fetchAdminWorkProductRuleMappings,
fetchAdminWorkProductRules,
saveAdminWorkProductRuleMapping,
testAdminKuaishouProductMatch,
} from '@/services/admin'
import type {
KuaishouMatchSource,
WorkProductMatchLog,
WorkProductRuleMapping,
} from '@/types/worker-platform'
import type { AdminKuaishouIndustryShopOption } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
type MappingFormValues = {
mappingId?: number
ruleId: number
sellerIds: string[]
relItemId?: string
itemTitle?: string
relSkuId?: string
skuNick?: string
mappingType: 'product_default' | 'sku_exact' | 'sku_series'
enabled?: boolean
}
export default function ProductMatchPanel() {
const { message } = App.useApp()
const queryClient = useQueryClient()
const [mappingForm] = Form.useForm<MappingFormValues>()
const [testPayload, setTestPayload] = useState('')
const [testResult, setTestResult] = useState<
Awaited<ReturnType<typeof testAdminKuaishouProductMatch>>['data'] | null
>(null)
const [selectedLog, setSelectedLog] = useState<WorkProductMatchLog | null>(null)
const rulesQuery = useQuery({
queryKey: ['admin-worker-platform-product-rules', 'mapping-templates'],
queryFn: () => fetchAdminWorkProductRules({ page: 1, pageSize: 100 }),
})
const mappingsQuery = useQuery({
queryKey: ['admin-worker-platform-product-mappings'],
queryFn: () => fetchAdminWorkProductRuleMappings(),
})
const shopsQuery = useQuery({
queryKey: ['admin-kuaishou-industry-shops', 'product-match'],
queryFn: fetchAdminKuaishouIndustryShops,
})
const sourcesQuery = useQuery({
queryKey: ['admin-worker-platform-product-match-sources'],
queryFn: () => fetchAdminKuaishouMatchSources(200),
})
const logsQuery = useQuery({
queryKey: ['admin-worker-platform-product-match-logs'],
queryFn: () => fetchAdminWorkProductMatchLogs(100),
})
const rules = rulesQuery.data?.data.items || []
const mappings = mappingsQuery.data?.data.items || []
const sources = sourcesQuery.data?.data.items || []
const logs = logsQuery.data?.data.items || []
const shops = shopsQuery.data?.data.shops || []
const shopNameBySellerId = createShopNameBySellerId(shops)
const sellerIds = new Set([
...shops.map((shop) => shop.sellerId),
...sources.map((source) => source.sellerId),
])
const sellerOptions = Array.from(sellerIds)
.filter(Boolean)
.sort((left, right) => left.localeCompare(right))
.map((sellerId) => ({ value: sellerId, label: formatShopLabel(sellerId, shopNameBySellerId) }))
function resetMappingForm() {
mappingForm.resetFields()
mappingForm.setFieldsValue({ mappingType: 'product_default', enabled: true })
}
function fillMappingFromSource(
source: KuaishouMatchSource,
mappingType: 'product_default' | 'sku_exact' | 'sku_series',
) {
mappingForm.setFieldsValue({
mappingId: undefined,
sellerIds: [source.sellerId],
relItemId: mappingType === 'product_default' ? source.relItemId : '',
itemTitle: mappingType === 'product_default' ? source.itemTitle : '',
relSkuId: mappingType === 'sku_exact' ? source.relSkuId : '',
skuNick:
mappingType === 'product_default'
? ''
: mappingType === 'sku_series'
? normalizeSkuSeriesName(source.skuNick)
: source.skuNick,
mappingType,
enabled: true,
})
}
function editMapping(mapping: WorkProductRuleMapping) {
mappingForm.setFieldsValue({
mappingId: mapping.mappingId,
ruleId: mapping.ruleId,
sellerIds: mapping.sellerIds,
relItemId: mapping.relItemId,
itemTitle: mapping.itemTitle,
relSkuId: mapping.relSkuId,
skuNick: mapping.skuNick,
mappingType: mapping.mappingType,
enabled: mapping.enabled,
})
}
async function saveMapping(values: MappingFormValues) {
try {
await saveAdminWorkProductRuleMapping(values)
message.success(values.mappingId ? '商品映射已更新' : '商品映射已创建')
resetMappingForm()
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-mappings'] })
} catch (error) {
message.error(error instanceof Error ? error.message : '保存商品映射失败')
}
}
function handleMappingValuesChange(changedValues: Partial<MappingFormValues>) {
if (
changedValues.sellerIds &&
changedValues.sellerIds.length > 1 &&
mappingForm.getFieldValue('mappingType') === 'product_default'
) {
mappingForm.setFieldValue('relItemId', '')
}
}
async function deleteMapping(mappingId: number) {
try {
await deleteAdminWorkProductRuleMapping(mappingId)
message.success('商品映射已删除')
await queryClient.invalidateQueries({ queryKey: ['admin-worker-platform-product-mappings'] })
} catch (error) {
message.error(error instanceof Error ? error.message : '删除商品映射失败')
}
}
async function runMatchTest() {
if (!testPayload.trim()) {
message.warning('请选择或粘贴快手原始载荷')
return
}
try {
const response = await testAdminKuaishouProductMatch(testPayload)
setTestResult(response.data)
} catch (error) {
message.error(error instanceof Error ? error.message : '匹配测试失败')
}
}
const mappingColumns: TableColumnsType<WorkProductRuleMapping> = [
{
title: '店铺 / 商品',
key: 'product',
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong ellipsis={{ tooltip: row.itemTitle }}>
{row.itemTitle || row.relItemId}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{' '}
{row.sellerIds
.map((sellerId) => formatShopLabel(sellerId, shopNameBySellerId))
.join('、')}{' '}
{row.relItemId ? `· 商品 ${row.relItemId}` : ''}
</Typography.Text>
{row.mappingType !== 'product_default' ? (
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.mappingType === 'sku_series' ? 'SKU 系列' : 'SKU'} {row.skuNick || row.relSkuId}
</Typography.Text>
) : null}
</div>
),
},
{
title: '映射类型',
dataIndex: 'mappingType',
width: 110,
render: (value) => (
<Tag color={value === 'sku_exact' ? 'purple' : value === 'sku_series' ? 'cyan' : 'blue'}>
{value === 'sku_exact'
? 'SKU 精确'
: value === 'sku_series'
? 'SKU 数量系列'
: '商品默认'}
</Tag>
),
},
{
title: '接单模板',
key: 'template',
width: 170,
render: (_, row) => (
<div className="cell-stack">
<Typography.Text>{row.productName || row.ruleKey}</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{row.ruleKey}
</Typography.Text>
</div>
),
},
{
title: '状态',
dataIndex: 'enabled',
width: 80,
render: (enabled) => (
<Tag color={enabled ? 'green' : 'default'}>{enabled ? '启用' : '停用'}</Tag>
),
},
{
title: '操作',
key: 'actions',
width: 100,
render: (_, row) => (
<Space size={0}>
<Button type="link" onClick={() => editMapping(row)}>
</Button>
<Popconfirm title="删除此商品映射?" onConfirm={() => deleteMapping(row.mappingId)}>
<Button type="text" danger icon={<DeleteOutlined />} aria-label="删除商品映射" />
</Popconfirm>
</Space>
),
},
]
const sourceColumns: TableColumnsType<KuaishouMatchSource> = [
{
title: '快手商品 / SKU',
key: 'source',
render: (_, row) => (
<div className="cell-stack">
<Typography.Text strong ellipsis={{ tooltip: row.itemTitle }}>
{row.itemTitle || row.relItemId}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatShopLabel(row.sellerId, shopNameBySellerId)} · {row.skuNick || '商品默认'}
</Typography.Text>
</div>
),
},
{
title: '出现',
dataIndex: 'seenCount',
width: 70,
render: (value) => `${value}`,
},
{
title: '最近',
dataIndex: 'lastSeenAt',
width: 160,
render: (value) => formatAdminDateTime(value),
},
{
title: '操作',
key: 'actions',
width: 200,
render: (_, row) => (
<Space size={4} wrap>
<Button type="link" onClick={() => fillMappingFromSource(row, 'product_default')}>
</Button>
<Button type="link" onClick={() => fillMappingFromSource(row, 'sku_exact')}>
SKU
</Button>
<Button type="link" onClick={() => fillMappingFromSource(row, 'sku_series')}>
SKU
</Button>
<Button
type="text"
onClick={() => {
setTestPayload(JSON.stringify(row.rawPayload, null, 2))
}}
>
</Button>
</Space>
),
},
]
const logColumns: TableColumnsType<WorkProductMatchLog> = [
{
title: '时间',
dataIndex: 'createdAt',
width: 165,
render: (value) => formatAdminDateTime(value),
},
{
title: '店铺 / 商品 / SKU',
key: 'target',
render: (_, row) => (
<div className="cell-stack">
<Typography.Text ellipsis={{ tooltip: row.itemTitle }}>
{row.itemTitle || '-'}
</Typography.Text>
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
{formatShopLabel(row.sellerId, shopNameBySellerId)} ·{' '}
{row.skuNick || row.relSkuId || '商品默认'}
</Typography.Text>
</div>
),
},
{
title: '结果',
dataIndex: 'status',
width: 100,
render: (value) => <MatchStatusTag status={value} />,
},
{
title: '模板',
key: 'rule',
width: 170,
render: (_, row) => row.productName || row.ruleKey || '-',
},
{
title: '载荷',
key: 'payload',
width: 80,
render: (_, row) => (
<Button type="link" onClick={() => setSelectedLog(row)}>
</Button>
),
},
]
return (
<Card bordered={false} title="商品匹配">
<Tabs
items={[
{
key: 'mappings',
label: `商品映射 (${mappings.length})`,
children: (
<div className="page-stack">
<div
style={{
display: 'grid',
gridTemplateColumns: 'minmax(280px, 360px) minmax(0, 1fr)',
gap: 20,
alignItems: 'start',
}}
>
<Form
form={mappingForm}
layout="vertical"
initialValues={{ mappingType: 'product_default', enabled: true }}
onValuesChange={handleMappingValuesChange}
onFinish={saveMapping}
>
<Form.Item name="mappingId" hidden>
<Input />
</Form.Item>
<Form.Item
label="接单模板"
name="ruleId"
rules={[{ required: true, message: '请选择接单模板' }]}
>
<Select
showSearch
optionFilterProp="label"
placeholder="选择可复用模板"
options={rules.map((rule) => ({
value: rule.ruleId,
label: `${rule.productName || rule.ruleKey} (${rule.ruleKey})`,
}))}
/>
</Form.Item>
<Form.Item label="映射类型" name="mappingType">
<Select
options={[
{ value: 'product_default', label: '商品默认模板' },
{ value: 'sku_series', label: 'SKU 数量系列' },
{ value: 'sku_exact', label: 'SKU 精确覆盖' },
]}
/>
</Form.Item>
<Form.Item
label="覆盖店铺"
name="sellerIds"
rules={[{ required: true, message: '至少选择一个店铺' }]}
>
<Select
mode="tags"
tokenSeparators={[',', '']}
placeholder="选择快手店铺,可多选"
options={sellerOptions}
/>
</Form.Item>
<Form.Item label="关联商品 ID" name="relItemId">
<Input.TextArea
rows={2}
placeholder="可填写多个;用逗号、中文逗号或换行分隔"
/>
</Form.Item>
<Form.Item label="快手大标题" name="itemTitle">
<Input placeholder="可选;商品默认必填,同名 SKU 分流时填写" />
</Form.Item>
<Form.Item noStyle shouldUpdate>
{({ getFieldValue }) =>
getFieldValue('mappingType') === 'sku_exact' ? (
<>
<Form.Item label="关联 SKU ID" name="relSkuId">
<Input placeholder="ext.relSkuId0 自动忽略" />
</Form.Item>
<Form.Item label="具体 SKU 名称" name="skuNick">
<Input placeholder="ext.skuNick,完整精确匹配" />
</Form.Item>
</>
) : getFieldValue('mappingType') === 'sku_series' ? (
<Form.Item label="SKU 系列名称" name="skuNick">
<Input placeholder="例如:指挥官密钥;自动覆盖 1个、10个等数量规格" />
</Form.Item>
) : null
}
</Form.Item>
<Form.Item label="启用" name="enabled" valuePropName="checked">
<Switch />
</Form.Item>
<Space>
<Button type="primary" htmlType="submit">
</Button>
<Button onClick={resetMappingForm}></Button>
</Space>
</Form>
<Table<WorkProductRuleMapping>
rowKey="mappingId"
loading={mappingsQuery.isLoading}
columns={mappingColumns}
dataSource={mappings}
pagination={false}
size="small"
/>
</div>
<div>
<Space style={{ marginBottom: 10 }}>
<Typography.Text strong> SKU</Typography.Text>
<Button
icon={<ReloadOutlined />}
loading={sourcesQuery.isFetching}
onClick={() => sourcesQuery.refetch()}
>
</Button>
</Space>
<Table<KuaishouMatchSource>
rowKey={(row) =>
`${row.sellerId}-${row.relItemId}-${row.itemTitle}-${row.relSkuId}-${row.skuNick}`
}
loading={sourcesQuery.isLoading}
columns={sourceColumns}
dataSource={sources}
pagination={{ pageSize: 10, showSizeChanger: false }}
size="small"
/>
</div>
</div>
),
},
{
key: 'test',
label: '匹配测试',
children: (
<div className="page-stack">
<Space wrap>
<Select
showSearch
optionFilterProp="label"
style={{ minWidth: 360 }}
placeholder="选择已接收的快手原始载荷"
options={sources.map((source, index) => ({
value: index,
label: `${formatShopLabel(source.sellerId, shopNameBySellerId)} · ${source.itemTitle} · ${source.skuNick || '商品默认'}`,
}))}
onChange={(index) => {
const source = sources[Number(index)]
if (source) setTestPayload(JSON.stringify(source.rawPayload, null, 2))
}}
/>
<Button type="primary" icon={<PlayCircleOutlined />} onClick={runMatchTest}>
</Button>
</Space>
<Input.TextArea
value={testPayload}
onChange={(event) => setTestPayload(event.target.value)}
rows={16}
placeholder="粘贴快手 send-code 原始载荷"
/>
{testResult ? (
<div className="page-stack">
<Descriptions bordered size="small" column={{ xs: 1, md: 2 }}>
<Descriptions.Item label="结果">
<MatchStatusTag status={testResult.status} />
</Descriptions.Item>
<Descriptions.Item label="命中模板">
{testResult.rule?.productName || testResult.rule?.ruleKey || '-'}
</Descriptions.Item>
<Descriptions.Item label="店铺">
{testResult.context.sellerId
? formatShopLabel(testResult.context.sellerId, shopNameBySellerId)
: '-'}
</Descriptions.Item>
<Descriptions.Item label="具体 SKU">
{testResult.context.skuNick || '-'}
</Descriptions.Item>
</Descriptions>
<JsonPreview
value={{ mappingId: testResult.mappingId, candidates: testResult.candidates }}
/>
</div>
) : null}
</div>
),
},
{
key: 'logs',
label: `匹配日志 (${logs.length})`,
children: (
<div className="page-stack">
<Space>
<Button
icon={<ReloadOutlined />}
loading={logsQuery.isFetching}
onClick={() => logsQuery.refetch()}
>
</Button>
<Typography.Text type="secondary">
</Typography.Text>
</Space>
<Table<WorkProductMatchLog>
rowKey="logId"
loading={logsQuery.isLoading}
columns={logColumns}
dataSource={logs}
pagination={{ pageSize: 20, showSizeChanger: false }}
size="small"
/>
</div>
),
},
]}
/>
<Modal
title="匹配原始载荷"
open={Boolean(selectedLog)}
footer={null}
width={760}
onCancel={() => setSelectedLog(null)}
>
<JsonPreview value={selectedLog?.rawPayload} />
</Modal>
</Card>
)
}
function MatchStatusTag({ status }: { status: string }) {
const color = status === 'matched' ? 'green' : status === 'ambiguous' ? 'orange' : 'red'
const label = status === 'matched' ? '命中' : status === 'ambiguous' ? '冲突' : '未命中'
return <Tag color={color}>{label}</Tag>
}
function normalizeSkuSeriesName(value: string) {
return value
.normalize('NFKC')
.replace(/[\s\u3000]+/g, '')
.replace(/(?:\d+|[零一二三四五六七八九十百千两]+)(?:个|份|张|枚)/g, '')
}
function createShopNameBySellerId(shops: AdminKuaishouIndustryShopOption[]) {
const names = new Map<string, string>()
for (const shop of shops) {
const name = String(shop.shopName || '').trim()
if (!name) continue
const sellerId = String(shop.sellerId || '').trim()
const shopId = String(shop.shopId || '').trim()
if (sellerId) names.set(sellerId, name)
if (shopId) names.set(shopId, name)
}
return names
}
function formatShopLabel(sellerId: string, shopNameBySellerId: Map<string, string>) {
const name = shopNameBySellerId.get(sellerId)
return name ? `${name}${sellerId}` : sellerId
}
@@ -159,9 +159,10 @@ export default function ProductRulesPanel() {
await saveAdminWorkProductRule({
...payload,
ruleId: editingRule?.ruleId,
match: editingRule?.match,
unitPrice: unitMode ? Number(values.unitPrice || 0) : 0,
})
message.success(editingRule ? '物品规则已更新' : '物品规则已创建')
message.success(editingRule ? '接单模板已更新' : '接单模板已创建')
resetRuleForm()
await Promise.all([
queryClient.invalidateQueries({
@@ -218,7 +219,7 @@ export default function ProductRulesPanel() {
if (editingRule?.ruleId === rule.ruleId) {
resetRuleForm()
}
message.success('物品规则已删除')
message.success('接单模板已删除')
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-product-rules'],
})
@@ -268,20 +269,13 @@ export default function ProductRulesPanel() {
const columns: TableColumnsType<WorkProductRule> = [
{
title: '规则 / 商品',
title: '模板 / 商品',
key: 'rule',
width: '29%',
render: (_, row) => (
<div className="cell-stack">
<Typography.Text
strong
title={row.match.skuNick || row.match.itemTitle || row.productName || row.skuCode}
>
{row.match.skuNick ||
row.match.itemTitle ||
row.productName ||
row.skuCode ||
row.ruleKey}
<Typography.Text strong title={row.productName || row.ruleKey}>
{row.productName || row.ruleKey}
</Typography.Text>
<Space size={4} wrap style={{ marginTop: 2 }}>
<Tag color="blue" style={{ margin: 0, fontSize: 11, lineHeight: '18px' }}>
@@ -293,11 +287,6 @@ export default function ProductRulesPanel() {
</Typography.Text>
) : null}
</Space>
{row.match.itemTitle || row.match.skuNick ? (
<Typography.Text type="secondary" ellipsis={{ tooltip: true }} style={{ fontSize: 11 }}>
{[row.match.itemTitle, row.match.skuNick].filter(Boolean).join(' / ')}
</Typography.Text>
) : null}
</div>
),
},
@@ -442,7 +431,7 @@ export default function ProductRulesPanel() {
title={
<Space size={6}>
<FormOutlined style={{ color: '#1677ff' }} />
<span>{editingRule ? `编辑规则 · ${editingRule.ruleKey}` : '新建物品规则'}</span>
<span>{editingRule ? `编辑模板 · ${editingRule.ruleKey}` : '新建接单模板'}</span>
</Space>
}
extra={
@@ -499,71 +488,13 @@ export default function ProductRulesPanel() {
<Input placeholder="kuaishou" />
</Form.Item>
<Form.Item label="店铺" name="shopId">
<Input placeholder="留空通配" />
</Form.Item>
<Form.Item label="SKU" name="skuCode">
<Input placeholder="优先精确匹配" />
</Form.Item>
<Form.Item label="匹配方式" name="matchType">
<Select
options={[
{ value: 'contains', label: '包含' },
{ value: 'exact', label: '精确' },
]}
/>
</Form.Item>
<Form.Item label="商品名称" name="productName" className="span-2">
<Input placeholder="旧规则兼容字段;快手订单请配置下方大标题和 SKU" />
</Form.Item>
<Form.Item label="快手大标题" name="itemTitle" className="span-2">
<Input placeholder="itemTitle,例如:和平精英密钥……" />
</Form.Item>
<Form.Item label="大标题匹配" name="itemTitleMatchType">
<Select
options={[
{ value: 'exact', label: '精确' },
{ value: 'contains', label: '包含' },
]}
/>
</Form.Item>
<Form.Item label="具体 SKU 名称" name="skuNick">
<Input placeholder="ext.skuNick,例如:1个精英尊尚专属礼盒" />
</Form.Item>
<Form.Item label="SKU 名称匹配" name="skuNickMatchType">
<Select
options={[
{ value: 'exact', label: '精确' },
{ value: 'contains', label: '包含' },
]}
/>
</Form.Item>
<Form.Item label="快手卖家 ID" name="sellerId">
<Input placeholder="sellerId,留空通配" />
</Form.Item>
<Form.Item label="快手商品 ID" name="itemId">
<Input placeholder="itemId,精确匹配" />
</Form.Item>
<Form.Item label="快手 SKU ID" name="skuId">
<Input placeholder="skuId,精确匹配" />
</Form.Item>
<Form.Item label="关联商品 ID" name="relItemId">
<Input placeholder="ext.relItemId,精确匹配" />
</Form.Item>
<Form.Item label="关联 SKU ID" name="relSkuId">
<Input placeholder="ext.relSkuId,精确匹配" />
<Form.Item
label="模板名称"
name="productName"
className="span-2"
rules={[{ required: true, message: '请输入模板名称' }]}
>
<Input placeholder="例如:指挥官密钥标准模板" />
</Form.Item>
<Form.Item label="所属分类" name="categoryId">
@@ -714,7 +645,7 @@ export default function ProductRulesPanel() {
title={
<Space align="center" size={10}>
<Typography.Text strong style={{ fontSize: 15 }}>
</Typography.Text>
<Tag color="blue" style={{ margin: 0 }}>
{rulesPagination?.total || 0}
@@ -725,7 +656,7 @@ export default function ProductRulesPanel() {
<Space size={8} wrap>
<Input.Search
allowClear
placeholder="搜索规则/商品/SKU/店铺"
placeholder="搜索模板/商品/SKU/店铺"
style={{ width: 200 }}
value={keywordInput}
onChange={(event) => {
@@ -8,6 +8,9 @@ import type {
WorkOrderShare,
WorkOrderStatistics,
WorkProductRule,
WorkProductRuleMapping,
KuaishouMatchSource,
WorkProductMatchLog,
WorkerFinanceConfig,
WorkerFinanceRequest,
WorkerPlatformNotificationConfig,
@@ -135,6 +138,63 @@ export function deleteAdminWorkProductRule(ruleId: number) {
return apiDelete<{ deleted: boolean }>(`/api/v1/admin/worker-platform/product-rules/${ruleId}`)
}
export function fetchAdminWorkProductRuleMappings() {
return apiGet<{ items: WorkProductRuleMapping[] }>(
'/api/v1/admin/worker-platform/product-mappings',
)
}
export function saveAdminWorkProductRuleMapping(payload: {
mappingId?: number
ruleId: number
sellerIds: string[]
relItemId?: string
itemTitle?: string
relSkuId?: string
skuNick?: string
mappingType: 'product_default' | 'sku_exact' | 'sku_series'
enabled?: boolean
}) {
return apiPost<{ mapping: WorkProductRuleMapping }>(
'/api/v1/admin/worker-platform/product-mappings',
payload,
)
}
export function deleteAdminWorkProductRuleMapping(mappingId: number) {
return apiDelete<{ deleted: boolean }>(
`/api/v1/admin/worker-platform/product-mappings/${mappingId}`,
)
}
export function fetchAdminKuaishouMatchSources(limit = 100) {
return apiGet<{ items: KuaishouMatchSource[] }>(
'/api/v1/admin/worker-platform/product-match-sources',
{
limit,
},
)
}
export function testAdminKuaishouProductMatch(rawPayload: string) {
return apiPost<{
context: Record<string, string>
status: 'matched' | 'unmatched' | 'ambiguous'
mappingId: number | null
rule: WorkProductRule | null
candidates: Array<{ ruleKey: string; score: number; mappingId: number | null }>
}>('/api/v1/admin/worker-platform/product-match-test', { rawPayload })
}
export function fetchAdminWorkProductMatchLogs(limit = 100) {
return apiGet<{ items: WorkProductMatchLog[] }>(
'/api/v1/admin/worker-platform/product-match-logs',
{
limit,
},
)
}
export function reprocessAdminKuaishouWorkOrderMatches(limit = 100) {
return apiPost<{ scannedCount: number; createdCount: number; skippedCount: number }>(
'/api/v1/admin/worker-platform/orders/reprocess-matches',
@@ -226,6 +226,55 @@ export type WorkProductRule = {
updatedAt: string
}
export type WorkProductRuleMapping = {
mappingId: number
ruleId: number
ruleKey: string
productName: string
sellerIds: string[]
sellerId: string
relItemId: string
itemTitle: string
relSkuId: string
skuNick: string
mappingType: 'product_default' | 'sku_exact' | 'sku_series'
enabled: boolean
createdAt: string
updatedAt: string
}
export type KuaishouMatchSource = {
sellerId: string
relItemId: string
itemTitle: string
relSkuId: string
skuNick: string
sampleOid: string
lastSeenAt: string
seenCount: number
rawPayload: Record<string, unknown>
}
export type WorkProductMatchLog = {
logId: number
orderId: number | null
orderItemId: number | null
source: string
sellerId: string
relItemId: string
itemTitle: string
relSkuId: string
skuNick: string
status: 'matched' | 'unmatched' | 'ambiguous' | string
ruleId: number | null
mappingId: number | null
ruleKey: string
productName: string
candidates: Array<{ ruleKey?: string; score?: number; mappingId?: number | null }>
rawPayload: Record<string, unknown>
createdAt: string
}
export type UploadedFile = {
objectKey: string
url: string