refactor(fulfillment): 固化 91 中文名→英文 SKU 权威映射表
91 继续用中文编码;解析优先查内置常量表,配置仅作临时覆盖,Admin 改为展示权威表。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,6 +2,11 @@ import { getAffiliateDashConfig } from '../../platforms/affiliate-dash/config.js
|
||||
import type { JsonObject } from '../../../types/json.js'
|
||||
import { getAffiliateDashWallet } from '../../platforms/affiliate-dash/order-service.js'
|
||||
import { listAffiliateDashProducts } from '../../platforms/affiliate-dash/product-service.js'
|
||||
import {
|
||||
AFFILIATE_DASH_SKU_MAPPING,
|
||||
mergeAffiliateDashSkuMapping,
|
||||
} from '../../platforms/affiliate-dash/sku-mapping.js'
|
||||
import { matchAffiliateDashSku } from '../../fulfillment/product-resolution-service.js'
|
||||
import {
|
||||
getAffiliateDashSourceConfig,
|
||||
hasAffiliateDashConfig,
|
||||
@@ -15,6 +20,7 @@ export function getAdminAffiliateDashConfig() {
|
||||
|
||||
return {
|
||||
source,
|
||||
builtinSkuMapping: { ...AFFILIATE_DASH_SKU_MAPPING },
|
||||
effective: mapEffectiveAffiliateDashConfig(effective),
|
||||
}
|
||||
}
|
||||
@@ -25,15 +31,17 @@ export async function updateAdminAffiliateDashConfig(payload: JsonObject = {}) {
|
||||
|
||||
return {
|
||||
source: saved,
|
||||
builtinSkuMapping: { ...AFFILIATE_DASH_SKU_MAPPING },
|
||||
effective: mapEffectiveAffiliateDashConfig(effective),
|
||||
}
|
||||
}
|
||||
|
||||
/** 按 91 商品编码(productNo)测试 sku 映射命中。 */
|
||||
/** 按 91 商品编码(productNo)测试 sku 映射命中(权威表 + 增量覆盖,含归一化)。 */
|
||||
export function matchAdminAffiliateDashSku(payload: JsonObject = {}) {
|
||||
const productNo = String(payload.productNo || payload.product_no || '').trim()
|
||||
const source = getAdminEditableAffiliateDashConfig()
|
||||
const sku = productNo ? String(source.skuMapping[productNo] || '').trim() : ''
|
||||
const mapping = mergeAffiliateDashSkuMapping(source.skuMapping)
|
||||
const sku = productNo ? matchAffiliateDashSku(mapping, productNo) : ''
|
||||
|
||||
return {
|
||||
productNo,
|
||||
@@ -72,6 +80,8 @@ function mapEffectiveAffiliateDashConfig(config: ReturnType<typeof getAffiliateD
|
||||
hasAppKey: Boolean(config.appKey),
|
||||
hasAppSecret: Boolean(config.appSecret),
|
||||
hasCallbackSecret: Boolean(config.callbackSecret),
|
||||
skuMappingCount: Object.keys(config.skuMapping).length,
|
||||
skuMappingCount: Object.keys(mergeAffiliateDashSkuMapping(config.skuMapping)).length,
|
||||
builtinSkuMappingCount: Object.keys(AFFILIATE_DASH_SKU_MAPPING).length,
|
||||
overlaySkuMappingCount: Object.keys(config.skuMapping || {}).length,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,31 @@ import {
|
||||
matchAffiliateDashSkuPassthrough,
|
||||
normalizeAffiliateDashMappingKey,
|
||||
} from './product-resolution-service.js'
|
||||
import {
|
||||
AFFILIATE_DASH_SKU_MAPPING,
|
||||
mergeAffiliateDashSkuMapping,
|
||||
} from '../platforms/affiliate-dash/sku-mapping.js'
|
||||
|
||||
test('权威表覆盖核心中文商品', () => {
|
||||
assert.equal(AFFILIATE_DASH_SKU_MAPPING['幸运币90个'], 'lucky_coin_x90')
|
||||
assert.equal(AFFILIATE_DASH_SKU_MAPPING['套装-花面小丑'], 'suit_jester_face')
|
||||
assert.equal(AFFILIATE_DASH_SKU_MAPPING['龙宫之主大礼包'], 'pack_dragon_palace')
|
||||
assert.ok(Object.keys(AFFILIATE_DASH_SKU_MAPPING).length >= 36)
|
||||
})
|
||||
|
||||
test('mergeAffiliateDashSkuMapping 增量覆盖同名键', () => {
|
||||
const merged = mergeAffiliateDashSkuMapping({ 幸运币90个: 'override_sku' })
|
||||
assert.equal(merged['幸运币90个'], 'override_sku')
|
||||
assert.equal(merged['幸运币2个'], 'lucky_coin_x2')
|
||||
})
|
||||
|
||||
test('权威表经 matchAffiliateDashSku 归一命中', () => {
|
||||
const mapping = mergeAffiliateDashSkuMapping()
|
||||
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||
assert.equal(matchAffiliateDashSku(mapping, '幸运币90个'), 'lucky_coin_x90')
|
||||
assert.equal(matchAffiliateDashSku(mapping, ' 幸运币 90 个 '), 'lucky_coin_x90')
|
||||
assert.equal(matchAffiliateDashSku(mapping, '套装-花面小丑'), 'suit_jester_face')
|
||||
})
|
||||
|
||||
test('matchAffiliateDashSku 原样精确命中', () => {
|
||||
const mapping = { 幸运币90个: 'lucky_coin_x90' }
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { resolveFulfillmentRoute, type FulfillmentRouteResult } from './routing-config-service.js'
|
||||
import { getAffiliateDashConfig } from '../platforms/affiliate-dash/config.js'
|
||||
import { listAllAffiliateDashProducts } from '../platforms/affiliate-dash/product-service.js'
|
||||
import { mergeAffiliateDashSkuMapping } from '../platforms/affiliate-dash/sku-mapping.js'
|
||||
import type { AffiliateDashSkuMapping } from '../../types/runtime-config.js'
|
||||
import { FULFILLMENT_EXECUTOR_KEYS } from './executors/types.js'
|
||||
import { normalizeProductMatchName } from '../order/product-match-utils.js'
|
||||
@@ -239,7 +240,9 @@ async function resolveAffiliateDashSkuByProductNo(
|
||||
return null
|
||||
}
|
||||
|
||||
const mappedSku = matchAffiliateDashSku(config.skuMapping, String(productNo))
|
||||
// ① 权威中英表(+ 配置增量覆盖)② 英文 sku 透传兜底
|
||||
const mapping = mergeAffiliateDashSkuMapping(config.skuMapping)
|
||||
const mappedSku = matchAffiliateDashSku(mapping, String(productNo))
|
||||
if (mappedSku) {
|
||||
return {
|
||||
sku: mappedSku,
|
||||
@@ -308,6 +311,7 @@ async function getAffiliateDashSkuSet(): Promise<Set<string>> {
|
||||
/**
|
||||
* 91 商品编码为中文名(如「幸运币90个」),映射键查询做归一化:
|
||||
* NFKC(全角→半角)→ 去所有空白 → 小写。先原样精确,再归一化遍历。
|
||||
* 权威表见 platforms/affiliate-dash/sku-mapping.ts。
|
||||
*/
|
||||
export function matchAffiliateDashSku(mapping: AffiliateDashSkuMapping, productNo: string): string {
|
||||
const raw = String(productNo || '').trim()
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/**
|
||||
* 91 中文商品编码 → affiliate_dash 英文 SKU 权威映射表。
|
||||
*
|
||||
* 91 店 productNo 使用中文名(方便运营);建单时在此转换为平台英文 sku。
|
||||
* 与 affiliate_dash/docs/商品目录.md 保持同步;新增商品改此常量即可。
|
||||
*/
|
||||
export const AFFILIATE_DASH_SKU_MAPPING: Readonly<Record<string, string>> = Object.freeze({
|
||||
'套装-花面小丑': 'suit_jester_face',
|
||||
'套装-沙丘之歌': 'suit_dune_song',
|
||||
'龙宫之主大礼包': 'pack_dragon_palace',
|
||||
'幸运币90个': 'lucky_coin_x90',
|
||||
'幸运币30个': 'lucky_coin_x30',
|
||||
'幸运币2个': 'lucky_coin_x2',
|
||||
'荣耀勋章90个': 'honor_medal_x90',
|
||||
'荣耀勋章30个': 'honor_medal_x30',
|
||||
'荣耀勋章2个': 'honor_medal_x2',
|
||||
'套装-绵云熊熊': 'suit_cloud_bear',
|
||||
'星际漫游枪械礼包': 'pack_star_roam_weapon',
|
||||
'星际漫游服装礼包': 'pack_star_roam_outfit',
|
||||
'套装-狂沙舞者': 'suit_sand_dancer',
|
||||
'熔岩游骑兵礼包': 'pack_lava_ranger',
|
||||
'套装-胖达团团': 'suit_panda_tuan',
|
||||
'套装-胖达圆圆': 'suit_panda_round',
|
||||
'国宝胖达头盔': 'helmet_panda_treasure',
|
||||
'套装-西部谜踪': 'suit_western_mystery',
|
||||
'仓鼠灰灰头盔': 'helmet_hamster_gray',
|
||||
'仓鼠灰灰背包': 'bag_hamster_gray',
|
||||
'糯粉咩咩头盔': 'helmet_pink_sheep',
|
||||
'糯粉咩咩背包': 'bag_pink_sheep',
|
||||
'套装-萌熊伴侣': 'suit_cute_bear',
|
||||
'套装-仓鼠灰灰': 'suit_hamster_gray',
|
||||
'破片手榴弹-糯粉咩咩': 'frag_pink_sheep',
|
||||
'烟雾弹-糯粉咩咩': 'smoke_pink_sheep',
|
||||
'西部牛仔大礼包': 'pack_western_cowboy',
|
||||
'套装-浪漫天命': 'suit_romantic_destiny',
|
||||
'套装-恋恋初桃': 'suit_first_peach',
|
||||
'套装-糯粉咩咩': 'suit_pink_sheep',
|
||||
'套装-双彩绵绵': 'suit_dual_fluffy',
|
||||
'萌熊伴侣背包': 'bag_cute_bear',
|
||||
'M416-仓鼠灰灰': 'm416_hamster_gray',
|
||||
'黑色高级特训官上衣': 'top_black_elite_trainer',
|
||||
'套装-暗影哥特': 'suit_shadow_gothic',
|
||||
'套装-Alan Walker': 'suit_alan_walker',
|
||||
})
|
||||
|
||||
/** 合并权威表与运行时增量覆盖(覆盖同名键)。 */
|
||||
export function mergeAffiliateDashSkuMapping(
|
||||
overlay?: Record<string, string> | null,
|
||||
): Record<string, string> {
|
||||
const merged: Record<string, string> = { ...AFFILIATE_DASH_SKU_MAPPING }
|
||||
if (!overlay) {
|
||||
return merged
|
||||
}
|
||||
for (const [key, value] of Object.entries(overlay)) {
|
||||
const productNo = String(key || '').trim()
|
||||
const sku = String(value || '').trim()
|
||||
if (productNo && sku) {
|
||||
merged[productNo] = sku
|
||||
}
|
||||
}
|
||||
return merged
|
||||
}
|
||||
@@ -1573,6 +1573,7 @@ function AffiliateDashPlatformPanel() {
|
||||
const [notifyUrl, setNotifyUrl] = useState('')
|
||||
const [timestampToleranceSeconds, setTimestampToleranceSeconds] = useState(300)
|
||||
const [skuRows, setSkuRows] = useState<SkuMappingRow[]>([])
|
||||
const [builtinSkuRows, setBuiltinSkuRows] = useState<SkuMappingRow[]>([])
|
||||
const [matchInput, setMatchInput] = useState('')
|
||||
const [matchResult, setMatchResult] = useState<AdminAffiliateDashMatchResult | null>(null)
|
||||
const [wallet, setWallet] = useState<AdminAffiliateDashWallet | null>(null)
|
||||
@@ -1625,6 +1626,12 @@ function AffiliateDashPlatformPanel() {
|
||||
sku: String(sku || ''),
|
||||
})),
|
||||
)
|
||||
setBuiltinSkuRows(
|
||||
Object.entries(response.builtinSkuMapping || {}).map(([productNo, sku]) => ({
|
||||
productNo,
|
||||
sku: String(sku || ''),
|
||||
})),
|
||||
)
|
||||
}
|
||||
|
||||
async function saveConfig() {
|
||||
@@ -1832,7 +1839,33 @@ function AffiliateDashPlatformPanel() {
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="91 商品编码 → affiliate_dash sku 映射">
|
||||
<Card
|
||||
size="small"
|
||||
title={`权威中英映射表(内置 ${builtinSkuRows.length} 条)`}
|
||||
>
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
91 店商品编码使用中文名;正式商品请改后端常量表
|
||||
<Typography.Text code>
|
||||
apps/backend/.../affiliate-dash/sku-mapping.ts
|
||||
</Typography.Text>
|
||||
,与平台商品目录对齐。
|
||||
</Typography.Paragraph>
|
||||
<Table<SkuMappingRow>
|
||||
rowKey={(row) => row.productNo}
|
||||
size="small"
|
||||
pagination={{ pageSize: 10, showSizeChanger: false }}
|
||||
dataSource={builtinSkuRows}
|
||||
columns={[
|
||||
{ title: '91 中文编码', dataIndex: 'productNo', key: 'productNo' },
|
||||
{ title: 'affiliate_dash sku', dataIndex: 'sku', key: 'sku', width: 220 },
|
||||
]}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Card size="small" title="临时增量覆盖(可选)">
|
||||
<Typography.Paragraph type="secondary" style={{ marginBottom: 12 }}>
|
||||
仅用于尚未发版的新商品临时加一行;会覆盖同名权威键。日常运营无需维护。
|
||||
</Typography.Paragraph>
|
||||
<Space direction="vertical" style={{ width: '100%' }} size="middle">
|
||||
{skuRows.map((row, index) => (
|
||||
<Space key={`${index}_${row.productNo}`} wrap>
|
||||
@@ -1858,7 +1891,7 @@ function AffiliateDashPlatformPanel() {
|
||||
icon={<PlusOutlined />}
|
||||
onClick={() => setSkuRows((rows) => [...rows, { productNo: '', sku: '' }])}
|
||||
>
|
||||
新增映射
|
||||
新增临时覆盖
|
||||
</Button>
|
||||
</Space>
|
||||
</Card>
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface AdminAffiliateDashConfig {
|
||||
notifyUrl: string
|
||||
timestampToleranceSeconds: number
|
||||
preferredMatchEnabled: boolean
|
||||
/** 临时增量覆盖;正式商品请改后端权威常量表 */
|
||||
skuMapping: Record<string, string>
|
||||
}
|
||||
|
||||
@@ -22,11 +23,15 @@ export interface AdminAffiliateDashEffectiveConfig {
|
||||
hasAppSecret: boolean
|
||||
hasCallbackSecret: boolean
|
||||
skuMappingCount: number
|
||||
builtinSkuMappingCount?: number
|
||||
overlaySkuMappingCount?: number
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashConfigResponse {
|
||||
source: AdminAffiliateDashConfig
|
||||
effective: AdminAffiliateDashEffectiveConfig
|
||||
/** 代码内置权威表(91 中文名 → 英文 sku) */
|
||||
builtinSkuMapping?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface AdminAffiliateDashMatchResult {
|
||||
|
||||
@@ -635,46 +635,26 @@ order_site 处理要求:
|
||||
|
||||
**安全性**:「不耽误生产」保证——现有 cloud/feifei 商品未配置映射表(或映射表为空),`preferredExecutorKey` 为空走原逻辑,行为零变化;映射表只影响显式配置的 91 商品,且可通过 `preferredMatchEnabled=false` 一键回退。
|
||||
|
||||
### 22.3 91 中文名 → affiliate_dash sku 映射表(可直接导入配置)
|
||||
### 22.3 91 中文名 → affiliate_dash sku(权威常量表)
|
||||
|
||||
**维护位置**:`apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts`(`AFFILIATE_DASH_SKU_MAPPING`)
|
||||
|
||||
- 91 店 `productNo` 继续用**中文名**(方便运营)
|
||||
- 解析时查权威表(含 NFKC/去空白归一),再可选叠加配置里的临时覆盖
|
||||
- 与平台 `affiliate_dash/docs/商品目录.md` 同步;新增商品改常量表,不必在后台维护整张表
|
||||
|
||||
当前表含目录全量(含花面小丑 / 沙丘之歌 / 龙宫之主等),示例:
|
||||
|
||||
```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"
|
||||
"幸运币90个": "lucky_coin_x90",
|
||||
"套装-花面小丑": "suit_jester_face",
|
||||
"龙宫之主大礼包": "pack_dragon_palace"
|
||||
}
|
||||
```
|
||||
|
||||
完整条目以代码常量为准。
|
||||
|
||||
### 22.4 行为验证
|
||||
|
||||
用上述映射表模拟 91 商品名解析,全部符合预期:
|
||||
|
||||
Reference in New Issue
Block a user