refactor(product-match): 抽离接单模板匹配引擎(来源适配器/引擎/价格收口 + 后台按订单项试匹配)
新增 services/product-match:来源适配器(kuaishou-91 合成 91+快手 / pdd / douyin-mashida)+ 平台无关匹配上下文 + 条件打分引擎(并列最高=ambiguous 不猜、逐维诊断)+ 价格数量拼单唯一实现。 - 商店键命名空间化(kuaishou:/pdd:/douyin:),店铺为可选作用域:留空即可按订单信息匹配 - 抖音匹配文本只取内部 SKU,引流标题只进 promoTitle,itemTitle 恒空(修复 0.85 元误命中碎梦浮光珍宝礼盒、拼单未开) - work-product-match-mappers 改为门面(签名不变),work-order-pricing 改为转发 - 新增后台「按订单项试匹配」接口与面板:走发单同一套链路,返回模板/数量/单价/总额/拼单 - 生产样本回放:91/PDD 6000 条决策 0 差异;抖音 5 条全为修正 设计见 docs/接单模板匹配引擎重构设计.md
This commit is contained in:
@@ -38,11 +38,14 @@ import {
|
||||
cleanupAdminWorkProductMatchLogs,
|
||||
saveAdminWorkerProductMatchConfig,
|
||||
testAdminKuaishouProductMatch,
|
||||
testAdminProductMatchByItem,
|
||||
type AdminProductMatchTestByItemResult,
|
||||
} from '@/services/admin'
|
||||
import type { KuaishouMatchSource, WorkProductMatchLog } from '@/types/worker-platform'
|
||||
import type { AdminKuaishouIndustryShopOption } from '@/types/admin'
|
||||
import { formatAdminDateTime } from '@/utils/admin-time'
|
||||
import { ADMIN_DEFAULT_PAGE_SIZE, buildAdminTablePagination } from '@/utils/admin-pagination'
|
||||
import { formatMoney } from './shared'
|
||||
|
||||
export default function ProductMatchPanel() {
|
||||
const { message } = App.useApp()
|
||||
@@ -52,6 +55,11 @@ export default function ProductMatchPanel() {
|
||||
const [testResult, setTestResult] = useState<
|
||||
Awaited<ReturnType<typeof testAdminKuaishouProductMatch>>['data'] | null
|
||||
>(null)
|
||||
const [testItemId, setTestItemId] = useState<number | null>(null)
|
||||
const [itemTestResult, setItemTestResult] = useState<AdminProductMatchTestByItemResult | null>(
|
||||
null,
|
||||
)
|
||||
const [testingByItem, setTestingByItem] = useState(false)
|
||||
const [selectedLog, setSelectedLog] = useState<WorkProductMatchLog | null>(null)
|
||||
const [activeTab, setActiveTab] = useState('sources')
|
||||
const [logStatus, setLogStatus] = useState('')
|
||||
@@ -180,6 +188,22 @@ export default function ProductMatchPanel() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runItemMatchTest() {
|
||||
if (!testItemId) {
|
||||
message.warning('请输入订单项 ID')
|
||||
return
|
||||
}
|
||||
setTestingByItem(true)
|
||||
try {
|
||||
const response = await testAdminProductMatchByItem(testItemId)
|
||||
setItemTestResult(response.data)
|
||||
} catch (error) {
|
||||
message.error(error instanceof Error ? error.message : '试匹配失败')
|
||||
} finally {
|
||||
setTestingByItem(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function updateLegacyFallback(legacyFallbackEnabled: boolean) {
|
||||
try {
|
||||
await saveAdminWorkerProductMatchConfig({ legacyFallbackEnabled })
|
||||
@@ -353,6 +377,94 @@ export default function ProductMatchPanel() {
|
||||
label: '匹配测试',
|
||||
children: (
|
||||
<div className="page-stack">
|
||||
<Card size="small" title="按订单项试匹配(与发单侧同一套链路)">
|
||||
<Space wrap>
|
||||
<InputNumber
|
||||
min={1}
|
||||
style={{ width: 200 }}
|
||||
placeholder="订单项 ID,如 334088"
|
||||
value={testItemId}
|
||||
onChange={(value) => setTestItemId(typeof value === 'number' ? value : null)}
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlayCircleOutlined />}
|
||||
loading={testingByItem}
|
||||
onClick={() => void runItemMatchTest()}
|
||||
>
|
||||
试匹配
|
||||
</Button>
|
||||
<Typography.Text type="secondary">
|
||||
读真实订单项 → 来源适配器 → 匹配引擎 → 数量/单价/拼单;抖音、拼多多、91
|
||||
都适用。
|
||||
</Typography.Text>
|
||||
</Space>
|
||||
{itemTestResult ? (
|
||||
<div className="page-stack" style={{ marginTop: 12 }}>
|
||||
<Descriptions bordered size="small" column={{ xs: 1, md: 3 }}>
|
||||
<Descriptions.Item label="来源">
|
||||
{itemTestResult.sourceKey}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="结果">
|
||||
<MatchStatusTag status={itemTestResult.status} />
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="命中模板">
|
||||
{itemTestResult.rule?.productName || itemTestResult.rule?.ruleKey || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="匹配文本">
|
||||
{itemTestResult.orderItem.matchSkuTexts.join(' / ') || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="店铺键">
|
||||
{itemTestResult.shopKeys.join(' / ') || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="文本兜底">
|
||||
{itemTestResult.allowTextFallback ? '允许' : '不允许'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="数量">
|
||||
{itemTestResult.plan ? `${itemTestResult.plan.skuQuantity} 份` : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item
|
||||
label={
|
||||
itemTestResult.plan?.pricingMode === 'unit' ? '按件单价' : '固定金额'
|
||||
}
|
||||
>
|
||||
{itemTestResult.plan
|
||||
? formatMoney(
|
||||
itemTestResult.plan.pricingMode === 'unit'
|
||||
? itemTestResult.plan.unitPriceFen
|
||||
: itemTestResult.plan.rewardAmount,
|
||||
)
|
||||
: '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="接单总额">
|
||||
<Typography.Text strong style={{ color: '#3f8600' }}>
|
||||
{itemTestResult.plan
|
||||
? formatMoney(itemTestResult.plan.rewardAmount)
|
||||
: '-'}
|
||||
</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="拼单">
|
||||
{itemTestResult.plan?.sharingEnabled
|
||||
? `开 · ${itemTestResult.plan.sharingTotalQuantity} 份 × ${formatMoney(itemTestResult.plan.sharingUnitReward)}`
|
||||
: '关'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="命中条目">
|
||||
{itemTestResult.entryKey || '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="店铺">
|
||||
{itemTestResult.order.shopName || itemTestResult.order.shopId}
|
||||
</Descriptions.Item>
|
||||
</Descriptions>
|
||||
<JsonPreview
|
||||
value={{
|
||||
candidates: itemTestResult.candidates,
|
||||
diagnosis: itemTestResult.diagnosis,
|
||||
counts: itemTestResult.counts,
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
) : null}
|
||||
</Card>
|
||||
<Space wrap>
|
||||
<Select
|
||||
showSearch
|
||||
|
||||
@@ -385,6 +385,55 @@ export function testAdminKuaishouProductMatch(rawPayload: string) {
|
||||
}>('/api/v1/admin/worker-platform/product-match-test', { rawPayload })
|
||||
}
|
||||
|
||||
export type AdminProductMatchTestByItemResult = {
|
||||
order: {
|
||||
id: number
|
||||
provider: string
|
||||
platform: string
|
||||
shopId: string
|
||||
shopName: string
|
||||
platformOrderId: string
|
||||
totalAmount: number
|
||||
}
|
||||
orderItem: {
|
||||
id: number
|
||||
skuCode: string
|
||||
skuName: string
|
||||
quantity: number
|
||||
matchSkuTexts: string[]
|
||||
}
|
||||
sourceKey: string
|
||||
hasSellerContext: boolean
|
||||
allowTextFallback: boolean
|
||||
shopKeys: string[]
|
||||
status: 'matched' | 'unmatched' | 'ambiguous'
|
||||
rule: WorkProductRule | null
|
||||
mappingId: number | null
|
||||
entryKey: string | null
|
||||
score: number
|
||||
candidates: Array<{ key: string; ruleKey: string; score: number; mappingId: number | null }>
|
||||
diagnosis: Array<{ key: string; ruleKey: string; blockedBy: string[] }>
|
||||
plan: {
|
||||
skuQuantity: number
|
||||
unitPriceFen: number
|
||||
pricingMode: 'unit' | 'fixed'
|
||||
rewardAmount: number
|
||||
sharingEnabled: boolean
|
||||
sharingTotalQuantity: number
|
||||
sharingUnitReward: number
|
||||
acceptanceMode: string
|
||||
} | null
|
||||
counts: { enabledRules: number; enabledMappings: number }
|
||||
}
|
||||
|
||||
/** 按订单项 ID 试匹配:与发单侧完全一致的来源适配器 + 引擎 + 价格(任何来源都适用)。 */
|
||||
export function testAdminProductMatchByItem(orderItemId: number) {
|
||||
return apiPost<AdminProductMatchTestByItemResult>(
|
||||
'/api/v1/admin/worker-platform/product-match-test-item',
|
||||
{ orderItemId },
|
||||
)
|
||||
}
|
||||
|
||||
export function fetchAdminWorkProductMatchLogs(params?: Record<string, unknown>) {
|
||||
return apiGet<WorkerListResponse<WorkProductMatchLog>>(
|
||||
'/api/v1/admin/worker-platform/product-match-logs',
|
||||
|
||||
Reference in New Issue
Block a user