From 67d68d37f688b549b95b88f9bfe8420b703f67f8 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Sat, 19 Sep 2026 10:46:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(worker-platform):=20=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=8E=AF=E5=A2=83=E5=88=86=E7=B1=BB=E6=A0=91?= =?UTF-8?q?=E5=B9=B6=E5=A2=9E=E5=BC=BA=E5=AF=84=E5=8D=96=E5=8C=B9=E9=85=8D?= =?UTF-8?q?=E5=A4=9A=E5=93=81=E7=B1=BB=E7=A1=AC=E9=9A=94=E7=A6=BB=E9=97=A8?= =?UTF-8?q?=E7=A6=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 接入生产分类树体系:将 ItemCategoryType 对齐生产 work_categories 真实分类(苏尔南手枪、苏尔南钻石、特种兵密钥、地铁逃生、大转盘车皮、蛋仔派对、王者荣耀、第五人格、人气礼物等); 2. 强化跨游戏与跨子类隔离:精准拆分地铁逃生下的苏尔南钻石与苏尔南手枪,杜绝钻石充值单误匹配手枪现货; 3. 严格品类硬隔离门禁:对齐 matchPeacekeeperOrderWithInventory 与 describeConsignmentSpecDifferences,品类冲突时彻底拒绝匹配并在非精确列表中置为 conflict 沉底; 4. 补充全品类匹配与隔离单元测试,覆盖手枪、密钥、车皮、钻石、跨游戏等测试用例。 --- .../consignment-matching-service.test.ts | 20 +++ .../consignment-matching-service.ts | 160 +++++++++++++----- 2 files changed, 135 insertions(+), 45 deletions(-) diff --git a/apps/backend/src/services/worker-platform/consignment-matching-service.test.ts b/apps/backend/src/services/worker-platform/consignment-matching-service.test.ts index 179a5b79..6a9909b5 100644 --- a/apps/backend/src/services/worker-platform/consignment-matching-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-matching-service.test.ts @@ -641,4 +641,24 @@ test('品类隔离门禁:苏尔南手枪现货绝不匹配特种兵密钥、 const matched = matchPeacekeeperOrderWithInventory(pistolOrder, invPistol) assert.ok(matched) assert.equal(matched.matchType, 'partial') // 2璀璨充0璀璨 + + // 5. 跨游戏:蛋仔派对、王者荣耀、第五人格 + assert.equal( + matchPeacekeeperOrderWithInventory(extractPeacekeeperSpecs('蛋仔派对 6480许愿币'), invPistol), + null, + ) + assert.equal( + matchPeacekeeperOrderWithInventory(extractPeacekeeperSpecs('王者荣耀 荣耀水晶1颗'), invPistol), + null, + ) + assert.equal( + matchPeacekeeperOrderWithInventory(extractPeacekeeperSpecs('第五人格 6480回声'), invPistol), + null, + ) + + // 6. 苏尔南钻石(地铁逃生分类) vs 苏尔南手枪(手枪分类)隔离 + assert.equal( + matchPeacekeeperOrderWithInventory(extractPeacekeeperSpecs('苏尔南钻石100颗'), invPistol), + null, + ) }) diff --git a/apps/backend/src/services/worker-platform/consignment-matching-service.ts b/apps/backend/src/services/worker-platform/consignment-matching-service.ts index 45025dd4..57e1d59d 100644 --- a/apps/backend/src/services/worker-platform/consignment-matching-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-matching-service.ts @@ -64,61 +64,125 @@ export function normalizePeacekeeperSeries(value: string): string { return extractPeacekeeperSeries(value) || String(value || '').trim() } -/** 常见商品品类主体定义 */ +/** 生产环境商品分类与业务主体标准定义 */ export type ItemCategoryType = - | 'pistol_skin' // 手枪/苏尔南/枪皮 - | 'key_secret' // 特种兵密钥/钥匙 - | 'cannon' // 爆破型肩炮/肩甲 - | 'gift_box' // 专属礼盒/宝箱/福袋 - | 'rename_card' // 改名卡/更名卡 - | 'diamond_topup' // 钻石/点券/充值 + | 'pistol_suernan' // 苏尔南手枪 (暗潮 / 怒嚎 / 血宴) + | 'suernan_diamond' // 苏尔南钻石 + | 'peacekeeper_key' // 密钥 / 特种兵密钥 + | 'metro_escape' // 地铁逃生 (机甲/传世/护甲/背包/枪械类) + | 'spin_wheel_car' // 大转盘 / 车皮 / 960 / 1800 + | 'danzai_party' // 蛋仔派对 (商城/许愿币/至臻/机甲) + | 'honor_of_kings' // 王者荣耀 + | 'identity_v' // 第五人格 + | 'popularity_gift' // 人气礼物 / 专属礼盒 | 'other' export function detectItemCategory(text: string): ItemCategoryType { const normalized = String(text || '').toLowerCase() if (!normalized) return 'other' - // 1. 手枪/枪皮/苏尔南系列 + // 1. 跨游戏识别优先 + if ( + normalized.includes('蛋仔') || + normalized.includes('许愿币') || + normalized.includes('潮流纤维') + ) { + return 'danzai_party' + } + if ( + normalized.includes('王者') || + normalized.includes('王者荣耀') || + normalized.includes('荣耀水晶') || + normalized.includes('皮肤赠送') + ) { + return 'honor_of_kings' + } + if ( + normalized.includes('第五人格') || + normalized.includes('第五') || + normalized.includes('回声') + ) { + return 'identity_v' + } + + // 2. 苏尔南钻石(地铁逃生下的独立子类:如苏尔南钻石100颗等,与手枪区分) + if ( + (normalized.includes('苏尔南') || normalized.includes('地铁')) && + normalized.includes('钻石') && + (normalized.includes('颗') || + normalized.includes('万') || + (!normalized.includes('暗潮') && + !normalized.includes('怒嚎') && + !normalized.includes('血宴') && + !normalized.includes('手枪') && + !normalized.includes('枪皮'))) + ) { + return 'suernan_diamond' + } + + // 3. 苏尔南手枪 (暗潮 / 怒嚎 / 血宴 / 枪皮 / 璀璨钻级) if ( - normalized.includes('苏尔南') || - normalized.includes('手枪') || normalized.includes('暗潮') || normalized.includes('怒嚎') || normalized.includes('怒号') || normalized.includes('血宴') || - normalized.includes('枪皮') + normalized.includes('手枪') || + normalized.includes('枪皮') || + (normalized.includes('苏尔南') && !normalized.includes('钻石')) ) { - return 'pistol_skin' + return 'pistol_suernan' } - // 2. 密钥/特种兵密钥 + // 4. 密钥 (特种兵密钥 / 钥匙) if ( normalized.includes('密钥') || normalized.includes('密匙') || normalized.includes('特种兵密钥') || normalized.includes('钥匙') ) { - return 'key_secret' + return 'peacekeeper_key' } - // 3. 肩炮 - if (normalized.includes('肩炮') || normalized.includes('爆破型肩炮')) { - return 'cannon' + // 5. 大转盘 / 车皮 / 960 / 1800 + if ( + normalized.includes('大转盘') || + normalized.includes('车皮') || + normalized.includes('960') || + normalized.includes('1800') || + normalized.includes('载具') || + normalized.includes('玛莎') || + normalized.includes('特斯拉') || + normalized.includes('兰博基尼') + ) { + return 'spin_wheel_car' } - // 4. 专属礼盒/宝箱 - if (normalized.includes('礼盒') || normalized.includes('宝箱') || normalized.includes('福袋')) { - return 'gift_box' + // 6. 地铁逃生 (机甲 / 传世武器 / 护甲 / 背包 / 枪械类 / 肩炮) + if ( + normalized.includes('地铁') || + normalized.includes('逃生') || + normalized.includes('机甲') || + normalized.includes('传世') || + normalized.includes('护甲') || + normalized.includes('防弹衣') || + normalized.includes('头盔') || + normalized.includes('背包') || + normalized.includes('肩炮') || + normalized.includes('装甲武器') || + normalized.includes('辐射区') + ) { + return 'metro_escape' } - // 5. 改名卡 - if (normalized.includes('改名') || normalized.includes('更名')) { - return 'rename_card' - } - - // 6. 钻石/点券充值 - if (normalized.includes('点券') || normalized.includes('代充') || normalized.includes('充值')) { - return 'diamond_topup' + // 7. 人气礼物 / 专属礼盒 + if ( + normalized.includes('人气礼物') || + normalized.includes('专属礼盒') || + normalized.includes('礼盒') || + normalized.includes('宝箱') || + normalized.includes('福袋') + ) { + return 'popularity_gift' } return 'other' @@ -126,18 +190,24 @@ export function detectItemCategory(text: string): ItemCategoryType { export function formatItemCategoryLabel(type: ItemCategoryType): string { switch (type) { - case 'pistol_skin': - return '苏尔南手枪/枪皮' - case 'key_secret': + case 'pistol_suernan': + return '苏尔南手枪 (暗潮/怒嚎/血宴)' + case 'suernan_diamond': + return '苏尔南钻石' + case 'peacekeeper_key': return '特种兵密钥' - case 'cannon': - return '爆破型肩炮' - case 'gift_box': - return '专属礼盒' - case 'rename_card': - return '改名卡' - case 'diamond_topup': - return '点券/充值' + case 'metro_escape': + return '地铁逃生装备/机甲' + case 'spin_wheel_car': + return '大转盘/车皮' + case 'danzai_party': + return '蛋仔派对' + case 'honor_of_kings': + return '王者荣耀' + case 'identity_v': + return '第五人格' + case 'popularity_gift': + return '人气礼物/专属礼盒' default: return '其它类目' } @@ -246,19 +316,19 @@ export function matchPeacekeeperOrderWithInventory( return null } - // 1. 品类与物品主体强隔离门禁(防跨品类误判为非精确匹配) + // 1. 品类与物品主体强隔离门禁(基于生产环境分类体系) const orderCat = detectItemCategory(order.rawText) const invCat = detectItemCategory( `${inventory.item_name} ${inventory.series} ${inventory.category_name || ''}`, ) - // 若两端均能识别出品类且不一致(如订单是密钥/肩炮/礼盒,现货是手枪/枪皮),直接拒绝 + // 若两端均能识别出品类且不一致(如订单是密钥/机甲/车皮/其他游戏,现货是手枪),直接拒绝 if (orderCat !== 'other' && invCat !== 'other' && orderCat !== invCat) { return null } - // 若现货明确为手枪/枪皮类目,订单不能是无任何手枪特征的其他商品 - if (invCat === 'pistol_skin') { + // 若现货明确为苏尔南手枪类目,订单不能是无任何手枪特征的其他商品 + if (invCat === 'pistol_suernan') { const orderHasPistolSignal = Boolean(order.series) || order.rawText.includes('苏尔南') || @@ -392,14 +462,14 @@ export function describeConsignmentSpecDifferences( diffs.push( `品类:订单「${formatItemCategoryLabel(orderCat)}」/ 现货「${formatItemCategoryLabel(invCat)}」`, ) - } else if (invCat === 'pistol_skin') { + } else if (invCat === 'pistol_suernan') { const orderHasPistolSignal = Boolean(order.series) || order.rawText.includes('苏尔南') || order.rawText.includes('手枪') || order.rawText.includes('枪皮') if (!orderHasPistolSignal) { - diffs.push(`品类:订单商品非手枪/枪皮 / 现货「苏尔南手枪/枪皮」`) + diffs.push(`品类:订单商品非手枪/枪皮 / 现货「苏尔南手枪 (暗潮/怒嚎/血宴)」`) } }