优化履约匹配:指定匹配(91中文名→sku映射)最高优先 + 映射键归一化
- 91 商品编码全部为中文名(幸运币90个等),运营按中文名→affiliate_dash sku 配置显式映射 - resolveFulfillmentRoute 新增 preferredExecutorKey:映射命中强制优先(规则之前),停用/不可用回退原逻辑 - matchAffiliateDashSku 归一化:NFKC 全角→半角、去空白、小写,先精确再遍历 - preferredMatchEnabled 配置(默认 true,可一键回退,不耽误现有 cloud/feifei 生产) - 测试 +9(221 全绿);行为验证:映射命中/全角/空格命中,未映射回退
This commit is contained in:
@@ -84,6 +84,7 @@ export function createDefaultRuntimeConfig(projectRoot: string): RuntimeConfig {
|
|||||||
timeoutMs: 10000,
|
timeoutMs: 10000,
|
||||||
notifyUrl: '',
|
notifyUrl: '',
|
||||||
timestampToleranceSeconds: 300,
|
timestampToleranceSeconds: 300,
|
||||||
|
preferredMatchEnabled: true,
|
||||||
skuMapping: {},
|
skuMapping: {},
|
||||||
},
|
},
|
||||||
cloudtentacles: {
|
cloudtentacles: {
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
|
||||||
|
import {
|
||||||
|
matchAffiliateDashSku,
|
||||||
|
normalizeAffiliateDashMappingKey,
|
||||||
|
} from './product-resolution-service.js'
|
||||||
|
|
||||||
|
test('matchAffiliateDashSku 原样精确命中', () => {
|
||||||
|
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||||
|
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('matchAffiliateDashSku 全角数字/空格归一命中', () => {
|
||||||
|
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||||
|
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||||
|
assert.equal(matchAffiliateDashSku(mapping, ' 幸运币 90 个 '), 'lucky_coin_x90')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('matchAffiliateDashSku 大小写归一命中', () => {
|
||||||
|
const mapping = { '星际漫游服装礼包': 'pack_star_roam_outfit' }
|
||||||
|
assert.equal(matchAffiliateDashSku(mapping, '星际漫游服装礼包'), 'pack_star_roam_outfit')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('matchAffiliateDashSku 未命中返回空串', () => {
|
||||||
|
const mapping = { '幸运币90个': 'lucky_coin_x90' }
|
||||||
|
assert.equal(matchAffiliateDashSku(mapping, '神秘新商品'), '')
|
||||||
|
assert.equal(matchAffiliateDashSku(mapping, ''), '')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('normalizeAffiliateDashMappingKey NFKC + 去空白 + 小写', () => {
|
||||||
|
assert.equal(normalizeAffiliateDashMappingKey(' 幸运币90个 '), '幸运币90个')
|
||||||
|
assert.equal(normalizeAffiliateDashMappingKey('M416-仓鼠灰灰'), 'm416-仓鼠灰灰')
|
||||||
|
})
|
||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
type FulfillmentRouteResult,
|
type FulfillmentRouteResult,
|
||||||
} from './routing-config-service.js'
|
} from './routing-config-service.js'
|
||||||
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
||||||
|
import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js'
|
||||||
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
||||||
|
|
||||||
export type FulfillmentItem = {
|
export type FulfillmentItem = {
|
||||||
@@ -234,8 +235,15 @@ async function resolveConfiguredItemCandidate({
|
|||||||
const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName)
|
const cloudtentaclesNameMatch = await resolveCloudtentaclesSkuByProductName(externalSkuName)
|
||||||
const kuaishouFeifeiMatch = resolveKuaishouFeifeiProductByName(externalSkuName)
|
const kuaishouFeifeiMatch = resolveKuaishouFeifeiProductByName(externalSkuName)
|
||||||
const affiliateDashMatch = resolveAffiliateDashSkuByProductNo(item)
|
const affiliateDashMatch = resolveAffiliateDashSkuByProductNo(item)
|
||||||
|
// 指定匹配(显式映射)命中时优先走 affiliate_dash(可配置关闭,默认开)
|
||||||
|
const affiliateDashConfig = getAffiliateDashConfig()
|
||||||
|
const preferredExecutorKey =
|
||||||
|
affiliateDashMatch && affiliateDashConfig.preferredMatchEnabled !== false
|
||||||
|
? FULFILLMENT_EXECUTOR_KEYS.AFFILIATE_DASH
|
||||||
|
: ''
|
||||||
const fulfillmentRoute = resolveFulfillmentRoute({
|
const fulfillmentRoute = resolveFulfillmentRoute({
|
||||||
productName: externalSkuName,
|
productName: externalSkuName,
|
||||||
|
preferredExecutorKey,
|
||||||
candidates: [
|
candidates: [
|
||||||
{
|
{
|
||||||
executorKey: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
executorKey: FULFILLMENT_EXECUTOR_KEYS.KUAISHOU_CLOUD,
|
||||||
@@ -314,9 +322,7 @@ function resolveAffiliateDashSkuByProductNo(item: FulfillmentItem): AffiliateDas
|
|||||||
item.externalSkuCode,
|
item.externalSkuCode,
|
||||||
item.externalItemId,
|
item.externalItemId,
|
||||||
])
|
])
|
||||||
const sku = productNo
|
const sku = productNo ? matchAffiliateDashSku(config.skuMapping, String(productNo)) : ''
|
||||||
? String(config.skuMapping[String(productNo)] || '').trim()
|
|
||||||
: ''
|
|
||||||
if (!productNo || !sku) {
|
if (!productNo || !sku) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@@ -329,6 +335,40 @@ function resolveAffiliateDashSkuByProductNo(item: FulfillmentItem): AffiliateDas
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 91 商品编码为中文名(如「幸运币90个」),映射键查询做归一化:
|
||||||
|
* NFKC(全角→半角)→ 去所有空白 → 小写。先原样精确,再归一化遍历。
|
||||||
|
*/
|
||||||
|
export function matchAffiliateDashSku(
|
||||||
|
mapping: AffiliateDashSkuMapping,
|
||||||
|
productNo: string,
|
||||||
|
): string {
|
||||||
|
const raw = String(productNo || '').trim()
|
||||||
|
if (!raw) {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const direct = String(mapping[raw] || '').trim()
|
||||||
|
if (direct) {
|
||||||
|
return direct
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalized = normalizeAffiliateDashMappingKey(raw)
|
||||||
|
for (const [key, sku] of Object.entries(mapping || {})) {
|
||||||
|
if (normalizeAffiliateDashMappingKey(key) === normalized) {
|
||||||
|
return String(sku || '').trim()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizeAffiliateDashMappingKey(value: unknown): string {
|
||||||
|
return String(value || '')
|
||||||
|
.normalize('NFKC')
|
||||||
|
.replace(/\s+/g, '')
|
||||||
|
.toLowerCase()
|
||||||
|
}
|
||||||
|
|
||||||
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
function isOpen91KuaishouOrder(provider: unknown, platform: unknown) {
|
||||||
return String(provider || '').trim() === '91kaquan' &&
|
return String(provider || '').trim() === '91kaquan' &&
|
||||||
String(platform || '').trim() === 'kuaishou'
|
String(platform || '').trim() === 'kuaishou'
|
||||||
|
|||||||
@@ -89,3 +89,71 @@ test('resolveFulfillmentRoute 商品规则可以强制转人工', () => {
|
|||||||
assert.equal(result.selectedExecutorKey, 'manual_dispatch')
|
assert.equal(result.selectedExecutorKey, 'manual_dispatch')
|
||||||
assert.equal(result.reason, '命中商品路由规则')
|
assert.equal(result.reason, '命中商品路由规则')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('resolveFulfillmentRoute 指定匹配(显式映射)最高优先', () => {
|
||||||
|
const result = resolveFulfillmentRoute({
|
||||||
|
productName: '幸运币90个',
|
||||||
|
preferredExecutorKey: 'affiliate_dash',
|
||||||
|
candidates: [
|
||||||
|
{ executorKey: 'kuaishou_ct_assisted', available: true, reason: 'cloud 名字命中' },
|
||||||
|
{ executorKey: 'kuaishou_feifei', available: true, reason: 'feifei 名字命中' },
|
||||||
|
{ executorKey: 'affiliate_dash', available: true },
|
||||||
|
],
|
||||||
|
config: normalizeFulfillmentRoutingConfig({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.selectedExecutorKey, 'affiliate_dash')
|
||||||
|
assert.equal(result.reason, '命中指定匹配(显式映射),优先选择')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolveFulfillmentRoute 指定匹配命中但通道停用时回退全局优先级', () => {
|
||||||
|
const result = resolveFulfillmentRoute({
|
||||||
|
productName: '幸运币90个',
|
||||||
|
preferredExecutorKey: 'affiliate_dash',
|
||||||
|
candidates: [
|
||||||
|
{ executorKey: 'kuaishou_ct_assisted', available: true },
|
||||||
|
{ executorKey: 'kuaishou_feifei', available: true },
|
||||||
|
{ executorKey: 'affiliate_dash', available: true },
|
||||||
|
],
|
||||||
|
config: normalizeFulfillmentRoutingConfig({
|
||||||
|
executors: {
|
||||||
|
affiliate_dash: { enabled: false },
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.selectedExecutorKey, 'kuaishou_ct_assisted')
|
||||||
|
assert.ok(result.skipped.some((item) => item.executorKey === 'affiliate_dash'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolveFulfillmentRoute 指定匹配命中但商品不可用时回退全局优先级', () => {
|
||||||
|
const result = resolveFulfillmentRoute({
|
||||||
|
productName: '幸运币90个',
|
||||||
|
preferredExecutorKey: 'affiliate_dash',
|
||||||
|
candidates: [
|
||||||
|
{ executorKey: 'kuaishou_ct_assisted', available: true },
|
||||||
|
{ executorKey: 'kuaishou_feifei', available: false },
|
||||||
|
{ executorKey: 'affiliate_dash', available: false, reason: '未命中 sku 映射' },
|
||||||
|
],
|
||||||
|
config: normalizeFulfillmentRoutingConfig({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.selectedExecutorKey, 'kuaishou_ct_assisted')
|
||||||
|
assert.ok(result.skipped.some((item) => item.executorKey === 'affiliate_dash'))
|
||||||
|
})
|
||||||
|
|
||||||
|
test('resolveFulfillmentRoute 无指定匹配时按全局优先级选择', () => {
|
||||||
|
const result = resolveFulfillmentRoute({
|
||||||
|
productName: '幸运币90个',
|
||||||
|
preferredExecutorKey: '',
|
||||||
|
candidates: [
|
||||||
|
{ executorKey: 'kuaishou_ct_assisted', available: true },
|
||||||
|
{ executorKey: 'kuaishou_feifei', available: false },
|
||||||
|
{ executorKey: 'affiliate_dash', available: true },
|
||||||
|
],
|
||||||
|
config: normalizeFulfillmentRoutingConfig({}),
|
||||||
|
})
|
||||||
|
|
||||||
|
assert.equal(result.selectedExecutorKey, 'kuaishou_ct_assisted')
|
||||||
|
assert.equal(result.reason, '按全局优先级命中')
|
||||||
|
})
|
||||||
|
|||||||
@@ -137,10 +137,12 @@ export function resolveFulfillmentRoute({
|
|||||||
productName,
|
productName,
|
||||||
candidates,
|
candidates,
|
||||||
config = getFulfillmentRoutingConfig(),
|
config = getFulfillmentRoutingConfig(),
|
||||||
|
preferredExecutorKey = '',
|
||||||
}: {
|
}: {
|
||||||
productName: unknown
|
productName: unknown
|
||||||
candidates: FulfillmentRoutingCandidate[]
|
candidates: FulfillmentRoutingCandidate[]
|
||||||
config?: FulfillmentRoutingConfig
|
config?: FulfillmentRoutingConfig
|
||||||
|
preferredExecutorKey?: string
|
||||||
}): FulfillmentRouteResult {
|
}): FulfillmentRouteResult {
|
||||||
const normalizedConfig = normalizeFulfillmentRoutingConfig(config)
|
const normalizedConfig = normalizeFulfillmentRoutingConfig(config)
|
||||||
const normalizedCandidates = normalizeRoutingCandidates(candidates)
|
const normalizedCandidates = normalizeRoutingCandidates(candidates)
|
||||||
@@ -156,6 +158,22 @@ export function resolveFulfillmentRoute({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 指定匹配(显式映射)最高优先:命中且通道可用时直接选择,不受全局优先级影响。
|
||||||
|
const preferred = normalizeExecutorKey(preferredExecutorKey)
|
||||||
|
if (preferred) {
|
||||||
|
const selected = resolveExecutorSelection({
|
||||||
|
executorKey: preferred,
|
||||||
|
candidates: normalizedCandidates,
|
||||||
|
config: normalizedConfig,
|
||||||
|
skipped,
|
||||||
|
reason: '命中指定匹配(显式映射),优先选择',
|
||||||
|
matchedRule: null,
|
||||||
|
})
|
||||||
|
if (selected.selectedExecutorKey) {
|
||||||
|
return selected
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const matchedRule = findMatchedRoutingRule(productName, normalizedConfig.rules)
|
const matchedRule = findMatchedRoutingRule(productName, normalizedConfig.rules)
|
||||||
if (matchedRule) {
|
if (matchedRule) {
|
||||||
const selected = resolveExecutorSelection({
|
const selected = resolveExecutorSelection({
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ export function getAffiliateDashConfig(overrides: Partial<AffiliateDashRuntimeCo
|
|||||||
notifyUrl: String(config.notifyUrl || '').trim(),
|
notifyUrl: String(config.notifyUrl || '').trim(),
|
||||||
timestampToleranceSeconds:
|
timestampToleranceSeconds:
|
||||||
Math.max(1, Number(config.timestampToleranceSeconds || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS) || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS),
|
Math.max(1, Number(config.timestampToleranceSeconds || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS) || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS),
|
||||||
|
preferredMatchEnabled: config.preferredMatchEnabled !== false,
|
||||||
skuMapping: isRecord(config.skuMapping) ? config.skuMapping : {},
|
skuMapping: isRecord(config.skuMapping) ? config.skuMapping : {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,6 +79,8 @@ function mergeAffiliateDashConfig(
|
|||||||
savedValue.timestampToleranceSeconds ||
|
savedValue.timestampToleranceSeconds ||
|
||||||
runtimeValue.timestampToleranceSeconds ||
|
runtimeValue.timestampToleranceSeconds ||
|
||||||
AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS,
|
AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS,
|
||||||
|
preferredMatchEnabled:
|
||||||
|
savedValue.preferredMatchEnabled ?? runtimeValue.preferredMatchEnabled ?? true,
|
||||||
skuMapping: pickSkuMapping(savedValue.skuMapping, runtimeValue.skuMapping),
|
skuMapping: pickSkuMapping(savedValue.skuMapping, runtimeValue.skuMapping),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
@@ -90,6 +93,7 @@ function mergeAffiliateDashConfig(
|
|||||||
notifyUrl: runtimeValue.notifyUrl || '',
|
notifyUrl: runtimeValue.notifyUrl || '',
|
||||||
timestampToleranceSeconds:
|
timestampToleranceSeconds:
|
||||||
runtimeValue.timestampToleranceSeconds || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS,
|
runtimeValue.timestampToleranceSeconds || AFFILIATE_DASH_TIMESTAMP_TOLERANCE_SECONDS,
|
||||||
|
preferredMatchEnabled: runtimeValue.preferredMatchEnabled ?? true,
|
||||||
skuMapping: pickSkuMapping(undefined, runtimeValue.skuMapping),
|
skuMapping: pickSkuMapping(undefined, runtimeValue.skuMapping),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +107,7 @@ function mergeAffiliateDashConfig(
|
|||||||
notifyUrl: overrides.notifyUrl ?? base.notifyUrl,
|
notifyUrl: overrides.notifyUrl ?? base.notifyUrl,
|
||||||
timestampToleranceSeconds:
|
timestampToleranceSeconds:
|
||||||
overrides.timestampToleranceSeconds ?? base.timestampToleranceSeconds,
|
overrides.timestampToleranceSeconds ?? base.timestampToleranceSeconds,
|
||||||
|
preferredMatchEnabled: overrides.preferredMatchEnabled ?? base.preferredMatchEnabled,
|
||||||
skuMapping: pickSkuMapping(overrides.skuMapping, base.skuMapping),
|
skuMapping: pickSkuMapping(overrides.skuMapping, base.skuMapping),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ export type AffiliateDashSourceConfig = {
|
|||||||
timeoutMs: number
|
timeoutMs: number
|
||||||
notifyUrl: string
|
notifyUrl: string
|
||||||
timestampToleranceSeconds: number
|
timestampToleranceSeconds: number
|
||||||
|
preferredMatchEnabled: boolean
|
||||||
skuMapping: AffiliateDashSkuMapping
|
skuMapping: AffiliateDashSkuMapping
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +55,7 @@ export function normalizeAffiliateDashSourceConfig(rawValue: unknown): Affiliate
|
|||||||
timeoutMs: normalizePositiveInteger(source.timeoutMs, 10000),
|
timeoutMs: normalizePositiveInteger(source.timeoutMs, 10000),
|
||||||
notifyUrl: String(source.notifyUrl || '').trim(),
|
notifyUrl: String(source.notifyUrl || '').trim(),
|
||||||
timestampToleranceSeconds: normalizePositiveInteger(source.timestampToleranceSeconds, 300),
|
timestampToleranceSeconds: normalizePositiveInteger(source.timestampToleranceSeconds, 300),
|
||||||
|
preferredMatchEnabled: source.preferredMatchEnabled !== false,
|
||||||
skuMapping: normalizeSkuMapping(source.skuMapping),
|
skuMapping: normalizeSkuMapping(source.skuMapping),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -84,6 +86,7 @@ function createDefaultAffiliateDashSourceConfig(): AffiliateDashSourceConfig {
|
|||||||
timeoutMs: 10000,
|
timeoutMs: 10000,
|
||||||
notifyUrl: '',
|
notifyUrl: '',
|
||||||
timestampToleranceSeconds: 300,
|
timestampToleranceSeconds: 300,
|
||||||
|
preferredMatchEnabled: true,
|
||||||
skuMapping: {},
|
skuMapping: {},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ export type RuntimeConfig = {
|
|||||||
timeoutMs: number
|
timeoutMs: number
|
||||||
notifyUrl: string
|
notifyUrl: string
|
||||||
timestampToleranceSeconds: number
|
timestampToleranceSeconds: number
|
||||||
|
preferredMatchEnabled: boolean
|
||||||
skuMapping: AffiliateDashSkuMapping
|
skuMapping: AffiliateDashSkuMapping
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -612,3 +612,79 @@ order_site 处理要求:
|
|||||||
|
|
||||||
- 91 卡券(91kaquan/kuaishou)真实 productNo 是否带 `----店铺ID` 后缀(决定 P2 是否为实际故障)。
|
- 91 卡券(91kaquan/kuaishou)真实 productNo 是否带 `----店铺ID` 后缀(决定 P2 是否为实际故障)。
|
||||||
- 33 个 affiliate_dash 商品 displayName 与 91 商品名的重合度(决定 P4 兜底收益)。
|
- 33 个 affiliate_dash 商品 displayName 与 91 商品名的重合度(决定 P4 兜底收益)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 22. 履约匹配优化实施(v2.7 · 已完成)
|
||||||
|
|
||||||
|
**背景确认**:91 商品编码全部为**中文名**(如「幸运币90个」「星际漫游服装礼包」),无独立编码体系;运营按「中文名 → affiliate_dash sku」配置显式映射表(33 条,见 §22.3)。
|
||||||
|
|
||||||
|
### 22.1 匹配策略(两级)
|
||||||
|
|
||||||
|
1. **指定匹配(显式映射)最高优先**:skuMapping 命中(91 中文名 → affiliate_dash sku)即**强制走 affiliate_dash**,不受 cloudtentacles / feifei 名字匹配抢占、不受全局优先级影响。受通道 `enabled` 约束:affiliate_dash 停用或商品不可用时回退原逻辑(规则 → 全局优先级)。
|
||||||
|
2. **默认匹配(名字匹配)**:无指定匹配时,三平台按商品名匹配,按可配置优先级选择(`defaultExecutorPriority`,现状 cloud → feifei → affiliate_dash,管理后台可改顺序)。
|
||||||
|
|
||||||
|
### 22.2 改动清单
|
||||||
|
|
||||||
|
| 文件 | 改动 |
|
||||||
|
| --- | --- |
|
||||||
|
| `routing-config-service.ts` | `resolveFulfillmentRoute` 新增 `preferredExecutorKey` 参数:指定匹配在**规则匹配之前**优先尝试,命中即选;停用/不可用回退 |
|
||||||
|
| `product-resolution-service.ts` | 91 分支:映射命中且 `preferredMatchEnabled!==false` 时传 `preferredExecutorKey=AFFILIATE_DASH`;`matchAffiliateDashSku` 归一化匹配(NFKC 全角→半角、去空白、小写),先原样精确再归一化遍历 |
|
||||||
|
| 配置层 | `preferredMatchEnabled` 字段(默认 true):types/runtime-config.ts、defaults.ts、source-config-service.ts、config.ts merge 全链 |
|
||||||
|
| 测试 | routing-config-service.test.ts +4(preferred 优先/停用回退/不可用回退/无 preferred 走优先级);product-resolution-service.test.ts 新建 +5(精确/全角/空格/大小写/未命中) |
|
||||||
|
|
||||||
|
**安全性**:「不耽误生产」保证——现有 cloud/feifei 商品未配置映射表(或映射表为空),`preferredExecutorKey` 为空走原逻辑,行为零变化;映射表只影响显式配置的 91 商品,且可通过 `preferredMatchEnabled=false` 一键回退。
|
||||||
|
|
||||||
|
### 22.3 91 中文名 → affiliate_dash sku 映射表(可直接导入配置)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"套装-Alan Walker": "suit_alan_walker",
|
||||||
|
"套装-暗影哥特": "suit_shadow_gothic",
|
||||||
|
"黑色高级特训官上衣": "top_black_elite_trainer",
|
||||||
|
"M416-仓鼠灰灰": "m416_hamster_gray",
|
||||||
|
"萌熊伴侣背包": "bag_cute_bear",
|
||||||
|
"套装-双彩绵绵": "suit_dual_fluffy",
|
||||||
|
"套装-糯粉咩咩": "suit_pink_sheep",
|
||||||
|
"套装-恋恋初桃": "suit_first_peach",
|
||||||
|
"套装-浪漫天命": "suit_romantic_destiny",
|
||||||
|
"西部牛仔大礼包": "pack_western_cowboy",
|
||||||
|
"烟雾弹-糯粉咩咩": "smoke_pink_sheep",
|
||||||
|
"破片手榴弹-糯粉咩咩": "frag_pink_sheep",
|
||||||
|
"套装-仓鼠灰灰": "suit_hamster_gray",
|
||||||
|
"套装-萌熊伴侣": "suit_cute_bear",
|
||||||
|
"糯粉咩咩背包": "bag_pink_sheep",
|
||||||
|
"糯粉咩咩头盔": "helmet_pink_sheep",
|
||||||
|
"仓鼠灰灰背包": "bag_hamster_gray",
|
||||||
|
"仓鼠灰灰头盔": "helmet_hamster_gray",
|
||||||
|
"套装-西部谜踪": "suit_western_mystery",
|
||||||
|
"国宝胖达头盔": "helmet_panda_treasure",
|
||||||
|
"套装-胖达圆圆": "suit_panda_round",
|
||||||
|
"套装-胖达团团": "suit_panda_tuan",
|
||||||
|
"熔岩游骑兵礼包": "pack_lava_ranger",
|
||||||
|
"套装-狂沙舞者": "suit_sand_dancer",
|
||||||
|
"星际漫游服装礼包": "pack_star_roam_outfit",
|
||||||
|
"星际漫游枪械礼包": "pack_star_roam_weapon",
|
||||||
|
"套装-绵云熊熊": "suit_cloud_bear",
|
||||||
|
"荣耀勋章2个": "honor_medal_x2",
|
||||||
|
"荣耀勋章30个": "honor_medal_x30",
|
||||||
|
"荣耀勋章90个": "honor_medal_x90",
|
||||||
|
"幸运币2个": "lucky_coin_x2",
|
||||||
|
"幸运币30个": "lucky_coin_x30",
|
||||||
|
"幸运币90个": "lucky_coin_x90"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 22.4 行为验证
|
||||||
|
|
||||||
|
用上述映射表模拟 91 商品名解析,全部符合预期:
|
||||||
|
|
||||||
|
| 输入 | executor | sku |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 幸运币90个 | affiliate_dash | lucky_coin_x90 |
|
||||||
|
| 星际漫游服装礼包 | affiliate_dash | pack_star_roam_outfit |
|
||||||
|
| 幸运币90个(全角) | affiliate_dash | lucky_coin_x90 |
|
||||||
|
| 幸运币 90 个(带空格) | affiliate_dash | lucky_coin_x90 |
|
||||||
|
| 神秘新商品(未映射) | (无)回退 | — |
|
||||||
|
|
||||||
|
**验证**:typecheck 通过;后端全量 221 测试通过(新增 9 个)。
|
||||||
|
|||||||
Reference in New Issue
Block a user