From 4a3b4ec14dd95b6210db4dd171ed1437d742df83 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 14 Sep 2026 20:33:42 +0800 Subject: [PATCH] =?UTF-8?q?refactor(fulfillment):=20=E5=9B=BA=E5=8C=96=209?= =?UTF-8?q?1=20=E4=B8=AD=E6=96=87=E5=90=8D=E2=86=92=E8=8B=B1=E6=96=87=20SK?= =?UTF-8?q?U=20=E6=9D=83=E5=A8=81=E6=98=A0=E5=B0=84=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 91 继续用中文编码;解析优先查内置常量表,配置仅作临时覆盖,Admin 改为展示权威表。 Co-authored-by: Cursor --- .../platform-config/affiliate-dash-service.ts | 16 ++++- .../product-resolution-service.test.ts | 25 ++++++++ .../fulfillment/product-resolution-service.ts | 6 +- .../platforms/affiliate-dash/sku-mapping.ts | 62 +++++++++++++++++++ .../admin/platform/AdminPlatformShopsPage.tsx | 37 ++++++++++- .../admin/platform-config/affiliate-dash.ts | 5 ++ .../affiliate-dash对接.md | 48 +++++--------- 7 files changed, 159 insertions(+), 40 deletions(-) create mode 100644 apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts diff --git a/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts b/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts index 9a18de83..2b558319 100644 --- a/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts +++ b/apps/backend/src/services/admin/platform-config/affiliate-dash-service.ts @@ -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 { + 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' } diff --git a/apps/backend/src/services/fulfillment/product-resolution-service.ts b/apps/backend/src/services/fulfillment/product-resolution-service.ts index 675b236d..de22f6af 100644 --- a/apps/backend/src/services/fulfillment/product-resolution-service.ts +++ b/apps/backend/src/services/fulfillment/product-resolution-service.ts @@ -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> { /** * 91 商品编码为中文名(如「幸运币90个」),映射键查询做归一化: * NFKC(全角→半角)→ 去所有空白 → 小写。先原样精确,再归一化遍历。 + * 权威表见 platforms/affiliate-dash/sku-mapping.ts。 */ export function matchAffiliateDashSku(mapping: AffiliateDashSkuMapping, productNo: string): string { const raw = String(productNo || '').trim() diff --git a/apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts b/apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts new file mode 100644 index 00000000..cb95cc84 --- /dev/null +++ b/apps/backend/src/services/platforms/affiliate-dash/sku-mapping.ts @@ -0,0 +1,62 @@ +/** + * 91 中文商品编码 → affiliate_dash 英文 SKU 权威映射表。 + * + * 91 店 productNo 使用中文名(方便运营);建单时在此转换为平台英文 sku。 + * 与 affiliate_dash/docs/商品目录.md 保持同步;新增商品改此常量即可。 + */ +export const AFFILIATE_DASH_SKU_MAPPING: Readonly> = 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 | null, +): Record { + const merged: Record = { ...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 +} diff --git a/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx b/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx index deed2316..39761b0d 100644 --- a/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx +++ b/apps/frontend/src/pages/admin/platform/AdminPlatformShopsPage.tsx @@ -1573,6 +1573,7 @@ function AffiliateDashPlatformPanel() { const [notifyUrl, setNotifyUrl] = useState('') const [timestampToleranceSeconds, setTimestampToleranceSeconds] = useState(300) const [skuRows, setSkuRows] = useState([]) + const [builtinSkuRows, setBuiltinSkuRows] = useState([]) const [matchInput, setMatchInput] = useState('') const [matchResult, setMatchResult] = useState(null) const [wallet, setWallet] = useState(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() { - + + + 91 店商品编码使用中文名;正式商品请改后端常量表 + + apps/backend/.../affiliate-dash/sku-mapping.ts + + ,与平台商品目录对齐。 + + + 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 }, + ]} + /> + + + + + 仅用于尚未发版的新商品临时加一行;会覆盖同名权威键。日常运营无需维护。 + {skuRows.map((row, index) => ( @@ -1858,7 +1891,7 @@ function AffiliateDashPlatformPanel() { icon={} onClick={() => setSkuRows((rows) => [...rows, { productNo: '', sku: '' }])} > - 新增映射 + 新增临时覆盖 diff --git a/apps/frontend/src/types/admin/platform-config/affiliate-dash.ts b/apps/frontend/src/types/admin/platform-config/affiliate-dash.ts index 143001c4..f01237ba 100644 --- a/apps/frontend/src/types/admin/platform-config/affiliate-dash.ts +++ b/apps/frontend/src/types/admin/platform-config/affiliate-dash.ts @@ -8,6 +8,7 @@ export interface AdminAffiliateDashConfig { notifyUrl: string timestampToleranceSeconds: number preferredMatchEnabled: boolean + /** 临时增量覆盖;正式商品请改后端权威常量表 */ skuMapping: Record } @@ -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 } export interface AdminAffiliateDashMatchResult { diff --git a/docs/ affiliate_dash_发货平台/affiliate-dash对接.md b/docs/ affiliate_dash_发货平台/affiliate-dash对接.md index 6383333c..756c297e 100644 --- a/docs/ affiliate_dash_发货平台/affiliate-dash对接.md +++ b/docs/ affiliate_dash_发货平台/affiliate-dash对接.md @@ -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 商品名解析,全部符合预期: