关闭旧商品匹配默认回退

This commit is contained in:
yml2213
2026-08-18 13:50:56 +08:00
parent 79990066f0
commit c2305d419e
10 changed files with 186 additions and 25 deletions
@@ -4,6 +4,7 @@ export const APP_CONFIG_KEYS = {
fulfillmentRouting: 'fulfillment_routing',
workerFinance: 'worker_finance',
workerPlatformNotifications: 'worker_platform_notifications',
workerProductMatch: 'worker_product_match',
cloudtentaclesSources: 'cloudtentacles_sources',
cloudtentaclesSession: 'cloudtentacles_session',
cloudtentaclesOverrideRules: 'cloudtentacles_override_rules',
@@ -14,6 +14,7 @@ import {
deleteAdminWorkOrder,
deleteAdminWorkerLevel,
getAdminWorkerFinanceConfig,
getAdminWorkerProductMatchConfig,
getAdminWorkerPlatformNotificationConfig,
getAdminWorkerPlatformSummary,
getAdminWorkOrderEvents,
@@ -36,6 +37,7 @@ import {
resolveAdminProblemWorkOrder,
reviewAdminWorkerUser,
saveAdminWorkerFinanceConfig,
saveAdminWorkerProductMatchConfig,
saveAdminWorkerPlatformNotificationConfig,
saveAdminWorkerWithdrawalAccount,
saveAdminWorkCategory,
@@ -229,6 +231,32 @@ router.delete(
),
)
router.get(
'/worker-platform/product-match-config',
requireAdminRoles(['admin', 'operator', 'support']),
createJsonHandler(() => getAdminWorkerProductMatchConfig(), {
successMessage: 'ok',
errorMessage: '读取商品匹配配置失败',
scope: '[admin/worker-platform/product-match-config]',
}),
)
router.post(
'/worker-platform/product-match-config',
requireAdminRoles(['admin', 'operator']),
createJsonHandler((req) => saveAdminWorkerProductMatchConfig(req.body || {}), {
successMessage: '商品匹配配置已保存',
errorMessage: '保存商品匹配配置失败',
scope: '[admin/worker-platform/product-match-config]',
audit: (_req, data) => ({
action: 'worker_product_match_config_saved',
targetType: 'worker_product_match_config',
targetId: 'worker_product_match',
data: data && typeof data === 'object' ? (data as Record<string, unknown>) : {},
}),
}),
)
router.get(
'/worker-platform/product-match-sources',
requireAdminRoles(['admin', 'operator', 'support']),
@@ -72,6 +72,10 @@ import {
getWorkerPlatformNotificationConfig,
saveWorkerPlatformNotificationConfig,
} from './worker-platform-notification-config-service.js'
import {
getWorkerProductMatchConfig,
saveWorkerProductMatchConfig,
} from './worker-product-match-config-service.js'
import { refreshUploadedFileUrls } from '../file-storage/file-storage-service.js'
import {
createWorkOrderMaterialAdminNotification,
@@ -465,7 +469,9 @@ export async function testAdminKuaishouProductMatch(payload: JsonObject = {}) {
listWorkProductRules({ enabled: true }),
listWorkProductRuleMappings({ enabled: true }),
])
const decision = resolveMatchingProductRuleDecision(order, item, rules, mappings)
const decision = resolveMatchingProductRuleDecision(order, item, rules, mappings, {
legacyFallbackEnabled: getWorkerProductMatchConfig().legacyFallbackEnabled,
})
const context = resolveKuaishouWorkProductMatchContext(item)
return {
context,
@@ -972,6 +978,14 @@ export async function saveAdminWorkerPlatformNotificationConfig(payload: JsonObj
return saveWorkerPlatformNotificationConfig(payload)
}
export async function getAdminWorkerProductMatchConfig() {
return getWorkerProductMatchConfig()
}
export async function saveAdminWorkerProductMatchConfig(payload: JsonObject = {}) {
return saveWorkerProductMatchConfig(payload)
}
export async function listAdminWorkerFinanceRequests(query: JsonObject = {}) {
const page = normalizePage(query.page)
const pageSize = normalizePageSize(query.pageSize)
@@ -2002,11 +2016,14 @@ export async function syncWorkerOrdersForSourceOrder(
listWorkProductRules({ enabled: true }),
listWorkProductRuleMappings({ enabled: true }),
])
const productMatchConfig = getWorkerProductMatchConfig()
const created: WorkOrderRow[] = []
const skipped: Array<{ orderItemId: number; reason: string }> = []
for (const item of orderItems) {
const match = resolveMatchingProductRuleDecision(order, item, rules, mappings)
const match = resolveMatchingProductRuleDecision(order, item, rules, mappings, {
legacyFallbackEnabled: productMatchConfig.legacyFallbackEnabled,
})
const matchContext = resolveKuaishouWorkProductMatchContext(item)
if (matchContext.sellerId) {
const snapshot = safeParseJson(item.item_snapshot_json)
@@ -1,3 +1,4 @@
export * from './mappers.js'
export * from './worker-service.js'
export * from './admin-service.js'
export * from './worker-product-match-config-service.js'
@@ -611,8 +611,9 @@ export function resolveMatchingProductRule(
item: OrderItemRow,
rules: WorkProductRuleRow[],
mappings: WorkProductRuleMappingRow[] = [],
options: { legacyFallbackEnabled?: boolean } = {},
) {
return resolveMatchingProductRuleDecision(order, item, rules, mappings).rule
return resolveMatchingProductRuleDecision(order, item, rules, mappings, options).rule
}
export type WorkProductRuleMatchDecision = {
@@ -638,12 +639,15 @@ export function resolveMatchingProductRuleDecision(
item: OrderItemRow,
rules: WorkProductRuleRow[],
mappings: WorkProductRuleMappingRow[] = [],
options: { legacyFallbackEnabled?: boolean } = {},
): WorkProductRuleMatchDecision {
const context = resolveKuaishouWorkProductMatchContext(item)
const rulesById = new Map(rules.map((rule) => [Number(rule.id), rule]))
// 快手发码订单默认只允许确定性商品映射;非快手订单保留原有规则,避免影响其他来源。
const useLegacyFallback = !context.sellerId || options.legacyFallbackEnabled === true
const candidates = [
...mappings.map((mapping) => scoreWorkProductRuleMapping(context, mapping, rulesById)),
...rules.map((rule) => scoreWorkProductRule(order, item, rule)),
...(useLegacyFallback ? rules.map((rule) => scoreWorkProductRule(order, item, rule)) : []),
]
.filter(
(candidate): candidate is { rule: WorkProductRuleRow; score: number; mappingId?: number } =>
@@ -158,41 +158,63 @@ test('排行榜打手名称仅展示首字符', () => {
assert.equal(maskWorkerLeaderboardDisplayName(''), '****')
})
test('快手具体 SKU 精确规则优先于大标题包含规则', () => {
const decision = resolveMatchingProductRuleDecision(buildOrderRow(), buildOrderItemRow(), [
buildProductRule(),
buildProductRule({
id: 2,
rule_key: 'gift-box',
product_name: '精英尊尚专属礼盒',
match_json: {
sellerId: '3676797936',
itemTitle: '和平精英密钥指挥官特种兵侦察兵密钥',
skuNick: '1个精英尊尚专属礼盒',
itemTitleMatchType: 'exact',
skuNickMatchType: 'exact',
},
}),
])
test('开启旧规则兼容匹配后,快手具体 SKU 精确规则优先于大标题包含规则', () => {
const decision = resolveMatchingProductRuleDecision(
buildOrderRow(),
buildOrderItemRow(),
[
buildProductRule(),
buildProductRule({
id: 2,
rule_key: 'gift-box',
product_name: '精英尊尚专属礼盒',
match_json: {
sellerId: '3676797936',
itemTitle: '和平精英密钥指挥官特种兵侦察兵密钥',
skuNick: '1个精英尊尚专属礼盒',
itemTitleMatchType: 'exact',
skuNickMatchType: 'exact',
},
}),
],
[],
{ legacyFallbackEnabled: true },
)
assert.equal(decision.reason, 'matched')
assert.equal(decision.rule?.rule_key, 'gift-box')
})
test('快手同优先级精确规则进入冲突状态', () => {
test('开启旧规则兼容匹配后,快手同优先级精确规则进入冲突状态', () => {
const match = {
sellerId: '3676797936',
skuId: '189872452606936',
}
const decision = resolveMatchingProductRuleDecision(buildOrderRow(), buildOrderItemRow(), [
buildProductRule({ rule_key: 'gift-box-a', match_json: match }),
buildProductRule({ id: 2, rule_key: 'gift-box-b', match_json: match }),
])
const decision = resolveMatchingProductRuleDecision(
buildOrderRow(),
buildOrderItemRow(),
[
buildProductRule({ rule_key: 'gift-box-a', match_json: match }),
buildProductRule({ id: 2, rule_key: 'gift-box-b', match_json: match }),
],
[],
{ legacyFallbackEnabled: true },
)
assert.equal(decision.reason, 'ambiguous')
assert.equal(decision.rule, null)
})
test('快手订单默认不使用旧模板大标题匹配', () => {
const decision = resolveMatchingProductRuleDecision(buildOrderRow(), buildOrderItemRow(), [
buildProductRule({ rule_key: 'special-soldier', product_name: '特种兵' }),
])
assert.equal(decision.reason, 'unmatched')
assert.equal(decision.rule, null)
assert.deepEqual(decision.candidates, [])
})
test('快手 SKU 覆盖映射优先于商品默认映射', () => {
const defaultRule = buildProductRule({ rule_key: 'commander-default', product_name: '' })
const skuRule = buildProductRule({ id: 2, rule_key: 'gift-box-sku', product_name: '' })
@@ -0,0 +1,42 @@
import { APP_CONFIG_KEYS } from '../../config/app-config-keys.js'
import type { JsonObject } from '../../types/json.js'
import { readAppConfigEntry, saveAppConfigEntry } from '../config/app-config-store.js'
export type WorkerProductMatchConfig = {
legacyFallbackEnabled: boolean
}
export function getWorkerProductMatchConfig(): WorkerProductMatchConfig {
return readAppConfigEntry({
configKey: APP_CONFIG_KEYS.workerProductMatch,
fallback: createDefaultWorkerProductMatchConfig,
normalize: normalizeWorkerProductMatchConfig,
})
}
export async function saveWorkerProductMatchConfig(
rawValue: unknown,
): Promise<WorkerProductMatchConfig> {
return saveAppConfigEntry({
configKey: APP_CONFIG_KEYS.workerProductMatch,
value: rawValue,
normalize: normalizeWorkerProductMatchConfig,
})
}
export function createDefaultWorkerProductMatchConfig(): WorkerProductMatchConfig {
return { legacyFallbackEnabled: false }
}
export function normalizeWorkerProductMatchConfig(rawValue: unknown): WorkerProductMatchConfig {
const source = isPlainObject(rawValue) ? rawValue : {}
return {
// 默认关闭旧规则,避免大标题中的公共词触发错误接单。
legacyFallbackEnabled:
typeof source.legacyFallbackEnabled === 'boolean' ? source.legacyFallbackEnabled : false,
}
}
function isPlainObject(value: unknown): value is JsonObject {
return Object.prototype.toString.call(value) === '[object Object]'
}
@@ -26,16 +26,19 @@ import {
deleteAdminWorkProductRuleMapping,
fetchAdminKuaishouIndustryShops,
fetchAdminKuaishouMatchSources,
fetchAdminWorkerProductMatchConfig,
fetchAdminWorkProductMatchLogs,
fetchAdminWorkProductRuleMappings,
fetchAdminWorkProductRules,
saveAdminWorkProductRuleMapping,
saveAdminWorkerProductMatchConfig,
testAdminKuaishouProductMatch,
} from '@/services/admin'
import type {
KuaishouMatchSource,
WorkProductMatchLog,
WorkProductRuleMapping,
WorkerProductMatchConfig,
} from '@/types/worker-platform'
import type { AdminKuaishouIndustryShopOption } from '@/types/admin'
import { formatAdminDateTime } from '@/utils/admin-time'
@@ -89,12 +92,17 @@ export default function ProductMatchPanel() {
queryKey: ['admin-worker-platform-product-match-logs'],
queryFn: () => fetchAdminWorkProductMatchLogs(100),
})
const matchConfigQuery = useQuery({
queryKey: ['admin-worker-platform-product-match-config'],
queryFn: fetchAdminWorkerProductMatchConfig,
})
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 matchConfig = matchConfigQuery.data?.data
const shopNameBySellerId = createShopNameBySellerId(shops)
const sellerIds = new Set([
...shops.map((shop) => shop.sellerId),
@@ -209,6 +217,19 @@ export default function ProductMatchPanel() {
}
}
async function updateLegacyFallback(legacyFallbackEnabled: boolean) {
const nextConfig: WorkerProductMatchConfig = { legacyFallbackEnabled }
try {
await saveAdminWorkerProductMatchConfig(nextConfig)
message.success(legacyFallbackEnabled ? '旧规则兼容匹配已开启' : '旧规则兼容匹配已关闭')
await queryClient.invalidateQueries({
queryKey: ['admin-worker-platform-product-match-config'],
})
} catch (error) {
message.error(error instanceof Error ? error.message : '保存商品匹配配置失败')
}
}
const mappingColumns: TableColumnsType<WorkProductRuleMapping> = [
{
title: '店铺 / 商品',
@@ -391,6 +412,15 @@ export default function ProductMatchPanel() {
return (
<Card bordered={false} title="商品匹配">
<Space align="center" style={{ marginBottom: 16 }}>
<Switch
checked={matchConfig?.legacyFallbackEnabled === true}
loading={matchConfigQuery.isLoading || matchConfigQuery.isFetching}
onChange={updateLegacyFallback}
/>
<Typography.Text></Typography.Text>
<Typography.Text type="secondary"></Typography.Text>
</Space>
<Tabs
items={[
{
@@ -14,6 +14,7 @@ import type {
WorkerFinanceConfig,
WorkerFinanceRequest,
WorkerPlatformNotificationConfig,
WorkerProductMatchConfig,
WorkerLevel,
WorkerListResponse,
WorkerUser,
@@ -168,6 +169,17 @@ export function deleteAdminWorkProductRuleMapping(mappingId: number) {
)
}
export function fetchAdminWorkerProductMatchConfig() {
return apiGet<WorkerProductMatchConfig>('/api/v1/admin/worker-platform/product-match-config')
}
export function saveAdminWorkerProductMatchConfig(payload: WorkerProductMatchConfig) {
return apiPost<WorkerProductMatchConfig>(
'/api/v1/admin/worker-platform/product-match-config',
payload,
)
}
export function fetchAdminKuaishouMatchSources(limit = 100) {
return apiGet<{ items: KuaishouMatchSource[] }>(
'/api/v1/admin/worker-platform/product-match-sources',
@@ -137,6 +137,10 @@ export type WorkerPlatformNotificationConfig = {
>
}
export type WorkerProductMatchConfig = {
legacyFallbackEnabled: boolean
}
export type WorkerWithdrawalAccount = {
accountChannel: string
accountName: string