From 79990066f05935d74129a24cb239002729531979 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Tue, 18 Aug 2026 13:37:32 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E6=8E=A5=E5=8D=95=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E5=8C=B9=E9=85=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../035_worker_product_match_sku_mode.sql | 8 +++ .../worker-platform/product-match-repo.ts | 14 ++++-- .../src/repositories/worker-platform/types.ts | 1 + .../worker-platform/work-order-repo.ts | 10 ++++ .../services/worker-platform/admin-service.ts | 25 ++++++---- .../src/services/worker-platform/mappers.ts | 20 ++++++-- .../worker-platform-service.test.ts | 50 +++++++++++++++++++ .../pages/admin/panels/ProductMatchPanel.tsx | 24 +++++++-- .../pages/admin/panels/ProductRulesPanel.tsx | 12 +---- .../src/services/admin/worker-platform.ts | 1 + apps/frontend/src/types/worker-platform.ts | 1 + 11 files changed, 135 insertions(+), 31 deletions(-) create mode 100644 apps/backend/src/db/migrations/035_worker_product_match_sku_mode.sql diff --git a/apps/backend/src/db/migrations/035_worker_product_match_sku_mode.sql b/apps/backend/src/db/migrations/035_worker_product_match_sku_mode.sql new file mode 100644 index 00000000..d2da9b4c --- /dev/null +++ b/apps/backend/src/db/migrations/035_worker_product_match_sku_mode.sql @@ -0,0 +1,8 @@ +-- 035_worker_product_match_sku_mode.sql —— SKU 系列匹配方式。 + +ALTER TABLE work_product_rule_mappings + ADD COLUMN IF NOT EXISTS sku_match_mode TEXT NOT NULL DEFAULT 'fuzzy' + CHECK (sku_match_mode IN ('fuzzy', 'exact')); + +COMMENT ON COLUMN work_product_rule_mappings.sku_match_mode + IS 'SKU 数量系列匹配方式:fuzzy 兼容密钥/秘钥与附加文案,exact 仅完整系列名匹配'; diff --git a/apps/backend/src/repositories/worker-platform/product-match-repo.ts b/apps/backend/src/repositories/worker-platform/product-match-repo.ts index 0b74aa58..539f75aa 100644 --- a/apps/backend/src/repositories/worker-platform/product-match-repo.ts +++ b/apps/backend/src/repositories/worker-platform/product-match-repo.ts @@ -49,6 +49,7 @@ export async function upsertWorkProductRuleMapping(input: { itemTitle: string relSkuId: string skuNick: string + skuMatchMode: 'fuzzy' | 'exact' mappingType: 'product_default' | 'sku_exact' | 'sku_series' enabled: boolean now: string @@ -64,9 +65,10 @@ export async function upsertWorkProductRuleMapping(input: { item_title = $5, rel_sku_id = $6, sku_nick = $7, - mapping_type = $8, - enabled = $9, - updated_at = $10 + sku_match_mode = $8, + mapping_type = $9, + enabled = $10, + updated_at = $11 WHERE id = $1 RETURNING * `, @@ -78,6 +80,7 @@ export async function upsertWorkProductRuleMapping(input: { input.itemTitle, input.relSkuId, input.skuNick, + input.skuMatchMode, input.mappingType, input.enabled, input.now, @@ -90,8 +93,8 @@ export async function upsertWorkProductRuleMapping(input: { ` INSERT INTO work_product_rule_mappings ( rule_id, seller_ids_json, rel_item_id, item_title, rel_sku_id, sku_nick, - mapping_type, enabled, created_at, updated_at - ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $9) + sku_match_mode, mapping_type, enabled, created_at, updated_at + ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $10) RETURNING * `, [ @@ -101,6 +104,7 @@ export async function upsertWorkProductRuleMapping(input: { input.itemTitle, input.relSkuId, input.skuNick, + input.skuMatchMode, input.mappingType, input.enabled, input.now, diff --git a/apps/backend/src/repositories/worker-platform/types.ts b/apps/backend/src/repositories/worker-platform/types.ts index b0cfdae5..9bbc9c59 100644 --- a/apps/backend/src/repositories/worker-platform/types.ts +++ b/apps/backend/src/repositories/worker-platform/types.ts @@ -168,6 +168,7 @@ export type WorkProductRuleMappingRow = { item_title: string rel_sku_id: string sku_nick: string + sku_match_mode: 'fuzzy' | 'exact' | string mapping_type: 'product_default' | 'sku_exact' | 'sku_series' | string enabled: boolean created_at: string diff --git a/apps/backend/src/repositories/worker-platform/work-order-repo.ts b/apps/backend/src/repositories/worker-platform/work-order-repo.ts index 84d6b170..b01488a9 100644 --- a/apps/backend/src/repositories/worker-platform/work-order-repo.ts +++ b/apps/backend/src/repositories/worker-platform/work-order-repo.ts @@ -680,6 +680,16 @@ export async function getWorkProductRuleByKey(ruleKey: string): Promise { + const result = await query( + `${WORK_PRODUCT_RULE_SELECT} WHERE wpr.id = $1 LIMIT 1`, + [Number(ruleId)], + ) + return result.rows[0] || null +} + export async function deleteWorkProductRule( ruleId: number | string, ): Promise<{ deleted: boolean }> { diff --git a/apps/backend/src/services/worker-platform/admin-service.ts b/apps/backend/src/services/worker-platform/admin-service.ts index a3e30c96..40e2c20d 100644 --- a/apps/backend/src/services/worker-platform/admin-service.ts +++ b/apps/backend/src/services/worker-platform/admin-service.ts @@ -29,7 +29,7 @@ import { getWorkerUserById, getWorkOrderStatistics, getWorkOrderByOrderItemId, - getWorkProductRuleByKey, + getWorkProductRuleById, listAllWorkCategories, listWorkerFinanceRequests, listWorkOrders, @@ -306,6 +306,11 @@ export async function saveAdminWorkProductRuleMapping(payload: JsonObject = {}) mappingType === 'product_default' ? '' : String(payload.skuNick ?? payload.sku_nick ?? '').trim() + const skuMatchMode = + mappingType === 'sku_series' && + String(payload.skuMatchMode ?? payload.sku_match_mode ?? '').trim() === 'exact' + ? 'exact' + : 'fuzzy' const requiresProductScope = mappingType === 'product_default' if ( !ruleId || @@ -357,6 +362,7 @@ export async function saveAdminWorkProductRuleMapping(payload: JsonObject = {}) itemTitle, relSkuId, skuNick, + skuMatchMode, mappingType, enabled: normalizeBoolean(payload.enabled, true), now: nowIso(), @@ -488,6 +494,7 @@ function mapWorkProductRuleMapping(mapping: WorkProductRuleMappingRow) { itemTitle: String(mapping.item_title || ''), relSkuId: String(mapping.rel_sku_id || ''), skuNick: String(mapping.sku_nick || ''), + skuMatchMode: mapping.sku_match_mode === 'exact' ? 'exact' : 'fuzzy', mappingType: mapping.mapping_type === 'sku_series' ? 'sku_series' @@ -622,18 +629,16 @@ export async function saveAdminWorkProductRule(payload: JsonObject = {}) { } const finalRewardAmount = sharingEnabled ? resolvedSharingTotalAmount : rewardAmount - const fields = normalizeRequirementFieldsFromPayload(payload) - const ruleKey = normalizeSlugKey( - payload.ruleKey || payload.rule_key || skuCode || productName || matchedProductName, - `rule-${Date.now()}`, - ) const ruleId = normalizeOptionalId(payload.ruleId ?? payload.rule_id) - if (!ruleId && (await getWorkProductRuleByKey(ruleKey))) { - throw createHttpError('规则标识已存在,请更换后重试', { - statusCode: 409, - errorCode: 'work_product_rule_key_taken', + const existingRule = ruleId ? await getWorkProductRuleById(ruleId) : null + if (ruleId && !existingRule) { + throw createHttpError('接单模板不存在', { + statusCode: 404, + errorCode: 'work_product_rule_not_found', }) } + const fields = normalizeRequirementFieldsFromPayload(payload) + const ruleKey = existingRule?.rule_key || randomId('rule-') const rule = await upsertWorkProductRule({ ruleKey, provider: String(payload.provider || '').trim(), diff --git a/apps/backend/src/services/worker-platform/mappers.ts b/apps/backend/src/services/worker-platform/mappers.ts index 79c42534..f21434a3 100644 --- a/apps/backend/src/services/worker-platform/mappers.ts +++ b/apps/backend/src/services/worker-platform/mappers.ts @@ -760,9 +760,8 @@ function scoreWorkProductRuleMapping( if (skuNickMatched) score += 200 } if (mappingType === 'sku_series') { - const mappedSeries = normalizeSkuSeriesName(mapping.sku_nick) - const actualSeries = normalizeSkuSeriesName(context.skuNick) - if (!mappedSeries || !actualSeries || actualSeries !== mappedSeries) return null + const skuMatchMode = mapping.sku_match_mode === 'exact' ? 'exact' : 'fuzzy' + if (!matchSkuSeriesName(context.skuNick, mapping.sku_nick, skuMatchMode)) return null score += 200 } return { rule, score, mappingId: Number(mapping.id) } @@ -917,6 +916,21 @@ function normalizeSkuSeriesName(value: unknown) { ) } +function matchSkuSeriesName(actual: unknown, expected: unknown, matchMode: 'fuzzy' | 'exact') { + const normalizedActual = normalizeSkuSeriesName(actual) + const normalizedExpected = normalizeSkuSeriesName(expected) + if (!normalizedActual || !normalizedExpected) return false + if (matchMode === 'exact') return normalizedActual === normalizedExpected + + const fuzzyActual = normalizeFuzzySkuSeriesName(normalizedActual) + const fuzzyExpected = normalizeFuzzySkuSeriesName(normalizedExpected) + return fuzzyActual.includes(fuzzyExpected) || fuzzyExpected.includes(fuzzyActual) +} + +function normalizeFuzzySkuSeriesName(value: string) { + return value.replace(/秘钥|密匙|秘匙/g, '密钥') +} + export function normalizeWorkProductRuleMatch(payload: JsonObject): JsonObject { const match = payload.match && typeof payload.match === 'object' && !Array.isArray(payload.match) diff --git a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts index 7d22561d..b092e2fa 100644 --- a/apps/backend/src/services/worker-platform/worker-platform-service.test.ts +++ b/apps/backend/src/services/worker-platform/worker-platform-service.test.ts @@ -109,6 +109,7 @@ function buildProductMapping( item_title: '和平精英密钥指挥官特种兵侦察兵密钥', rel_sku_id: '', sku_nick: '', + sku_match_mode: 'fuzzy', mapping_type: 'product_default', enabled: true, created_at: '2026-08-18T00:00:00.000Z', @@ -253,6 +254,55 @@ test('SKU 数量系列可覆盖多店铺的前后数量规格', () => { assert.equal(decision.rule?.rule_key, 'commander-series') }) +test('SKU 数量系列默认模糊匹配密钥和秘钥', () => { + const item = buildOrderItemRow() + item.item_snapshot_json = { + kuaishouSendCode: { + ...item.item_snapshot_json.kuaishouSendCode, + skuNick: '指挥官秘钥8个', + }, + } + const rule = buildProductRule({ product_name: '' }) + const decision = resolveMatchingProductRuleDecision( + buildOrderRow(), + item, + [rule], + [ + buildProductMapping({ + mapping_type: 'sku_series', + sku_nick: '指挥官密钥', + }), + ], + ) + + assert.equal(decision.reason, 'matched') +}) + +test('SKU 数量系列精确匹配不兼容密钥和秘钥', () => { + const item = buildOrderItemRow() + item.item_snapshot_json = { + kuaishouSendCode: { + ...item.item_snapshot_json.kuaishouSendCode, + skuNick: '指挥官秘钥8个', + }, + } + const rule = buildProductRule({ product_name: '' }) + const decision = resolveMatchingProductRuleDecision( + buildOrderRow(), + item, + [rule], + [ + buildProductMapping({ + mapping_type: 'sku_series', + sku_nick: '指挥官密钥', + sku_match_mode: 'exact', + }), + ], + ) + + assert.equal(decision.reason, 'unmatched') +}) + test('商品默认映射支持用逗号填写多个关联商品 ID', () => { const item = buildOrderItemRow() item.item_snapshot_json = { diff --git a/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx index 9759ed0e..ea501dba 100644 --- a/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductMatchPanel.tsx @@ -49,6 +49,7 @@ type MappingFormValues = { itemTitle?: string relSkuId?: string skuNick?: string + skuMatchMode: 'fuzzy' | 'exact' mappingType: 'product_default' | 'sku_exact' | 'sku_series' enabled?: boolean } @@ -109,6 +110,7 @@ export default function ProductMatchPanel() { mappingForm.setFieldsValue({ mappingType: 'product_default', productScope: 'selected_products', + skuMatchMode: 'fuzzy', enabled: true, }) } @@ -130,6 +132,7 @@ export default function ProductMatchPanel() { : mappingType === 'sku_series' ? normalizeSkuSeriesName(source.skuNick) : source.skuNick, + skuMatchMode: 'fuzzy', mappingType, enabled: true, }) @@ -145,6 +148,7 @@ export default function ProductMatchPanel() { itemTitle: mapping.itemTitle, relSkuId: mapping.relSkuId, skuNick: mapping.skuNick, + skuMatchMode: mapping.skuMatchMode, mappingType: mapping.mappingType, enabled: mapping.enabled, }) @@ -224,6 +228,9 @@ export default function ProductMatchPanel() { {row.mappingType !== 'product_default' ? ( {row.mappingType === 'sku_series' ? 'SKU 系列' : 'SKU'} {row.skuNick || row.relSkuId} + {row.mappingType === 'sku_series' + ? ` · ${row.skuMatchMode === 'exact' ? '精确' : '模糊'}` + : ''} ) : null} @@ -405,6 +412,7 @@ export default function ProductMatchPanel() { initialValues={{ mappingType: 'product_default', productScope: 'selected_products', + skuMatchMode: 'fuzzy', enabled: true, }} onValuesChange={handleMappingValuesChange} @@ -492,9 +500,19 @@ export default function ProductMatchPanel() { ) : mappingType === 'sku_series' ? ( - - - + <> + + + + ) : null} ) diff --git a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx index 7cb55212..936a95b9 100644 --- a/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ProductRulesPanel.tsx @@ -86,7 +86,6 @@ export default function ProductRulesPanel() { function editRule(rule: WorkProductRule) { setEditingRule(rule) form.setFieldsValue({ - ruleKey: rule.ruleKey, provider: rule.provider, platform: rule.platform, shopId: rule.shopId, @@ -122,7 +121,7 @@ export default function ProductRulesPanel() { async function saveRule(values: { ruleId?: number - ruleKey: string + ruleKey?: string provider?: string platform?: string shopId?: string @@ -159,6 +158,7 @@ export default function ProductRulesPanel() { await saveAdminWorkProductRule({ ...payload, ruleId: editingRule?.ruleId, + ruleKey: editingRule?.ruleKey || '', match: editingRule?.match, unitPrice: unitMode ? Number(values.unitPrice || 0) : 0, }) @@ -472,14 +472,6 @@ export default function ProductRulesPanel() { onFinish={saveRule} >
- - - - diff --git a/apps/frontend/src/services/admin/worker-platform.ts b/apps/frontend/src/services/admin/worker-platform.ts index b508153c..eb1de656 100644 --- a/apps/frontend/src/services/admin/worker-platform.ts +++ b/apps/frontend/src/services/admin/worker-platform.ts @@ -152,6 +152,7 @@ export function saveAdminWorkProductRuleMapping(payload: { itemTitle?: string relSkuId?: string skuNick?: string + skuMatchMode?: 'fuzzy' | 'exact' mappingType: 'product_default' | 'sku_exact' | 'sku_series' enabled?: boolean }) { diff --git a/apps/frontend/src/types/worker-platform.ts b/apps/frontend/src/types/worker-platform.ts index 51179e1d..2eae9423 100644 --- a/apps/frontend/src/types/worker-platform.ts +++ b/apps/frontend/src/types/worker-platform.ts @@ -237,6 +237,7 @@ export type WorkProductRuleMapping = { itemTitle: string relSkuId: string skuNick: string + skuMatchMode: 'fuzzy' | 'exact' mappingType: 'product_default' | 'sku_exact' | 'sku_series' enabled: boolean createdAt: string