diff --git a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts index aa10567b..68dde903 100644 --- a/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-fulfillment-service.ts @@ -77,7 +77,11 @@ function assertConsignmentSpecMatchedOrder( series: fields.series, }) - const match = matchPeacekeeperOrderWithInventory(orderSpecs, inventory) + // 宽松模式:运营已经人工选定这件现货,订单标题没写全规格不该硬拦; + // 真正冲突(系统/区服/系列不符)仍然会被下面的 null 拦下。 + const match = matchPeacekeeperOrderWithInventory(orderSpecs, inventory, { + allowUnspecifiedSpecs: true, + }) if (!match) { throw createHttpError('该寄卖库存与订单规格不符(系统/区服/系列/钻级不匹配),已拒绝本次履约', { statusCode: 409, 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 fcb01c8d..e411ff45 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 @@ -241,20 +241,17 @@ test('findMatchingPeacekeeperInventories matches 苹果微信暗潮1钻 to 苹 assert.equal(matches[1]!.matchType, 'exact') }) -test('订单规格信息不全时绝不判为"精确匹配",钻级不再兜底为 1', () => { +test('订单没写系统/区服/系列时不再命中任何库存(否则每次发布都弹匹配提醒)', () => { // 商品名只写"苏尔南":系统/区服/系列/钻级全部缺失 const vague = extractPeacekeeperSpecs('苏尔南') assert.equal(vague.diamondLevel, 0, '解析不到钻级必须记为 0,不能兜底成 1') const inv = buildInventory({ diamond_level: 1, brilliance_count: 0 }) - const matched = matchPeacekeeperOrderWithInventory(vague, inv) - assert.ok(matched, '信息不全的订单仍应给出候选,但只能是部分匹配') - assert.equal(matched.matchType, 'partial') - assert.ok(matched.reason.includes('订单规格信息不全')) - assert.ok(matched.reason.includes('系统')) - assert.ok(matched.reason.includes('区服')) - assert.ok(matched.reason.includes('系列')) - assert.ok(matched.reason.includes('钻级')) + assert.equal( + matchPeacekeeperOrderWithInventory(vague, inv), + null, + '规格确认不了的订单不能给出候选:仓库里任何在库货都会命中,提醒就没意义了', + ) }) test('订单缺钻级但其余规格完整时,也不得判为精确匹配', () => { @@ -267,15 +264,68 @@ test('订单缺钻级但其余规格完整时,也不得判为精确匹配', () assert.ok(matched.reason.includes('钻级')) }) -test('库存侧缺系统/区服时不能算精确匹配(无法确认一致)', () => { +test('系统/区服/系列任一项对不上时直接不命中,不给"部分匹配"兜底', () => { const order = extractPeacekeeperSpecs('安卓微信区 暗潮 1钻') - const inv = buildInventory({ system: '', server_zone: '', series: '暗潮', diamond_level: 1 }) - const matched = matchPeacekeeperOrderWithInventory(order, inv) + // 库存侧缺系统/区服:无法确认是同一件货,不能提醒 + assert.equal( + matchPeacekeeperOrderWithInventory( + order, + buildInventory({ system: '', server_zone: '', series: '暗潮', diamond_level: 1 }), + ), + null, + ) + // 订单侧缺系列:同样确认不了 + assert.equal( + matchPeacekeeperOrderWithInventory( + extractPeacekeeperSpecs('安卓微信区 1钻'), + buildInventory({ diamond_level: 1 }), + ), + null, + ) +}) + +test('宽松模式:运营已人工选定现货时,标题没写全规格仍可复检放行(但仍拦真冲突)', () => { + const vague = extractPeacekeeperSpecs('苏尔南') + const inv = buildInventory({ diamond_level: 1, brilliance_count: 0 }) + + // 严格模式(提醒用)不命中 + assert.equal(matchPeacekeeperOrderWithInventory(vague, inv), null) + // 宽松模式(人工履约复检用)保留旧的候选行为 + const lenient = matchPeacekeeperOrderWithInventory(vague, inv, { allowUnspecifiedSpecs: true }) + assert.ok(lenient) + assert.equal(lenient.matchType, 'partial') + assert.ok(lenient.reason.includes('订单规格信息不全')) + + // 宽松也不能放过真冲突 + const conflicting = buildInventory({ series: '怒嚎', diamond_level: 1 }) + assert.equal( + matchPeacekeeperOrderWithInventory( + extractPeacekeeperSpecs('安卓微信区 暗潮 1钻'), + conflicting, + { + allowUnspecifiedSpecs: true, + }, + ), + null, + ) +}) + +test('findMatchingPeacekeeperInventories 默认严格,只有显式传选项才宽松', () => { + const inventory = [buildInventory({ diamond_level: 3, brilliance_count: 2 })] + // 订单只写了系列、没写系统/区服:默认严格 → 一条都不给 + assert.equal(findMatchingPeacekeeperInventories('暗潮', inventory).length, 0) + assert.equal( + findMatchingPeacekeeperInventories('暗潮', inventory, { allowUnspecifiedSpecs: true }).length, + 1, + ) +}) + +test('订单写清了规格与钻级、库存完全相同时走精确匹配而非部分匹配', () => { + const order = extractPeacekeeperSpecs('安卓微信区 暗潮 1钻') + const matched = matchPeacekeeperOrderWithInventory(order, buildInventory({ diamond_level: 1 })) assert.ok(matched) - assert.equal(matched.matchType, 'partial') - assert.ok(matched.reason.includes('系统')) - assert.ok(matched.reason.includes('区服')) + assert.equal(matched.matchType, 'exact') }) test('通用件与明确规格冲突时仍然排除', () => { 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 6c0a0db3..eabd4a4b 100644 --- a/apps/backend/src/services/worker-platform/consignment-matching-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-matching-service.ts @@ -147,6 +147,7 @@ function compareSpec(orderValue: string, inventoryValue: string): SpecVerdict { export function matchPeacekeeperOrderWithInventory( order: ParsedOrderSpecs, inventory: ConsignmentInventoryRow, + options?: { allowUnspecifiedSpecs?: boolean | undefined }, ): ConsignmentMatchResult | null { // 必须开启匹配,且处于在库状态 if (!inventory.allow_matching || inventory.status !== 'in_stock') { @@ -197,7 +198,20 @@ export function matchPeacekeeperOrderWithInventory( } } - // 2. 部分匹配(Partial / Superset Match):高配充低配,或订单规格信息不全 + // 2. 部分匹配(Partial / Superset Match):高配充低配。 + // + // 严格模式(默认,供"发布时内部库存提醒"这类自动判断使用)加一道硬门槛: + // 系统/区服/系列必须**都能确认一致**。以前对"订单没写规格"是放行的,于是任何只写了 + // 游戏名的订单都会命中仓库里所有在库货,表现为"随便发布点什么都会弹内部库存匹配提醒"。 + // + // 宽松模式(allowUnspecifiedSpecs)保留旧行为,只给"运营已人工选定某件现货、服务端复检" + // 的场景用:标题没写全规格不该硬拦,但仍会拦住真正冲突(系统/区服/系列不符)。 + if (!options?.allowUnspecifiedSpecs) { + if (verdictSystem !== 'match' || verdictZone !== 'match' || verdictSeries !== 'match') { + return null + } + } + const reasons: string[] = [] if (unknownFields.length > 0) { reasons.push(`订单规格信息不全(未写: ${unknownFields.join('、')}),需人工确认`) @@ -209,7 +223,14 @@ export function matchPeacekeeperOrderWithInventory( reasons.push(`多出【${invBrilliance - orderBrilliance}璀璨】`) } + // 严格模式下还得有正信号:要么库存配置更高,要么订单压根没提钻级 + // (写清了规格但没写配置时仍需人工判断)。都不满足说明这件货对这张单没有任何 + // 超出精确匹配的价值,不该提醒运营。 const strictlyHigher = orderDiamond > 0 && invDiamond > orderDiamond + const brillianceHigher = invBrilliance > orderBrilliance + if (!options?.allowUnspecifiedSpecs && !strictlyHigher && !brillianceHigher && orderDiamond > 0) { + return null + } return { inventory, @@ -233,13 +254,17 @@ export function findMatchingPeacekeeperInventories( serverZone?: string | undefined series?: string | undefined diamondLevel?: number | undefined + /** 宽松模式:仅给"运营已人工选定现货"的复检用,自动提醒必须保持严格 */ + allowUnspecifiedSpecs?: boolean | undefined }, ): ConsignmentMatchResult[] { const orderSpecs = extractPeacekeeperSpecs(rawRequirementText, extraMeta) const results: ConsignmentMatchResult[] = [] for (const inv of inventories) { - const matched = matchPeacekeeperOrderWithInventory(orderSpecs, inv) + const matched = matchPeacekeeperOrderWithInventory(orderSpecs, inv, { + allowUnspecifiedSpecs: extraMeta?.allowUnspecifiedSpecs, + }) if (matched) { results.push(matched) } diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index 17334a4d..a79a7ca8 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -2941,7 +2941,8 @@ export default function WorkOrdersPanel() {