From 99752dce9edb40a502b13e983ea1eeb1acf7980f Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 17 Sep 2026 10:30:17 +0800 Subject: [PATCH] =?UTF-8?q?fix(consignment):=20=E4=BF=AE=E6=8E=89=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E6=97=B6=E4=B9=B1=E5=BC=B9=E5=86=85=E9=83=A8=E5=BA=93?= =?UTF-8?q?=E5=AD=98=E6=8F=90=E9=86=92=EF=BC=8C=E5=B9=B6=E6=8A=8A=E5=BC=B9?= =?UTF-8?q?=E7=AA=97=E8=B5=8F=E9=87=91=E6=8D=A2=E7=AE=97=E6=88=90=E5=85=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 乱弹窗根因:matchPeacekeeperOrderWithInventory 精确匹配判定失败后无条件 fall through 到 部分匹配;而系统/区服/系列/钻级缺失时 compareSpec 返回 unknown(不是 mismatch)会被放行, 于是任何只写了游戏名的订单都命中仓库里全部在库货,表现为"随便发布点什么都会弹一下"。 严格模式(自动提醒)现在要求:① 系统/区服/系列都能确认一致;② 存在正信号(库存配置更高, 或订单压根没提钻级)。"订单没写规格"不再算命中。 同时给 matchPeacekeeperOrderWithInventory / findMatchingPeacekeeperInventories 增加 allowUnspecifiedSpecs 选项:人工履约的服务端复检(运营已人工选定现货)走宽松模式, 标题没写全规格不硬拦、真冲突仍拦,避免收紧自动提醒时把人工履约一起挡掉。 赏金显示:弹窗用 rewardAmount(单位分)直接 toFixed,把 ¥40 显示成 ¥4000; 同文件其他 30 余处均为 /100,仅此处漏了。弹窗内收购成本/履约押金本来就已 /100,核对无误。 验证:用截图里的现货(苹果QQ区怒嚎3钻2璀璨)对照 6 种订单—— "怒嚎"、"苏尔南" 从命中变为不命中;"苹果QQ区怒嚎1钻" 仍按高配充低配提醒; "5钻"(低配充高配)、"安卓微信区怒嚎1钻"(系统/区服不符)不命中。 后端 435 例(新增 3 例、改写 3 例)全绿。 --- .../consignment-fulfillment-service.ts | 6 +- .../consignment-matching-service.test.ts | 80 +++++++++++++++---- .../consignment-matching-service.ts | 29 ++++++- .../pages/admin/panels/WorkOrdersPanel.tsx | 3 +- docs/寄卖系统优化方案.md | 17 ++++ 5 files changed, 116 insertions(+), 19 deletions(-) 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() {
当前工单原定接单赏金: - ¥{Number(consignmentInterceptModal.order?.rewardAmount || 0).toFixed(2)} + {/* rewardAmount 是分,必须除以 100:这里以前直接 toFixed,把 ¥40 显示成 ¥4000 */} + ¥{(Number(consignmentInterceptModal.order?.rewardAmount || 0) / 100).toFixed(2)}
diff --git a/docs/寄卖系统优化方案.md b/docs/寄卖系统优化方案.md index 57d8ca65..d5d63e05 100644 --- a/docs/寄卖系统优化方案.md +++ b/docs/寄卖系统优化方案.md @@ -21,6 +21,7 @@ | `632dddc4` | 上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 | | `5d11bf20` | 批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | `consignment-fulfillment-service.ts`、`consignment-matching-service.ts`、`consignment-repo.ts`、履约路由、`WorkOrdersPanel.tsx` | | `d6f6219f` | 批次 F1:内部现货履约停在「待加好友」,客服确认后才进赠送冷却 + 备注快捷项 | `work-order-gift-repo.ts`、履约服务、`WorkOrdersPanel.tsx`、`utils/gift-phase.ts` | +| 本批提交(批次 H,同一提交内含本文档,故不写自身 hash) | 批次 H:修掉发布时乱弹内部库存提醒(部分匹配兜底过宽)+ 弹窗赏金分/元换算 | `consignment-matching-service.ts`、履约复检、`WorkOrdersPanel.tsx` | | 本批提交(批次 G,同一提交内含本文档,故不写自身 hash) | 批次 G:确认加好友后现货作废不回池 + 撤销寄售履约押金可选(含修打手押金永久冻结、真实库 42804 500) | `consignment-repo.ts`、`consignment-deposit-policy.ts`、`work-order-deposit-repo.ts`、撤销履约服务与路由、`WorkOrdersPanel.tsx` | | 本批提交(批次 F2,同一提交内含本文档,故不写自身 hash) | 批次 F2:确认加好友后卖家发布押金不退(`deposit_deduction` 落地)+ 二次冻结护栏 | `consignment-deposit-repo.ts`、取消/退单仓库、撤销履约服务 | | 本批提交(批次 E,同一提交内含本文档,故不写自身 hash) | 前端四权限门控收口、`allocation_status` 约束与权限文案修正、面板错误态/防双提交/真实待审计数 | `admin-auth.ts`、两个寄卖面板、`AdminWorkerPlatformPage.tsx`、迁移 `080` | @@ -277,6 +278,22 @@ 前端在"部分匹配"候选上先 `modal.confirm` 危险确认,再带 `allowPartial: true` 重试,高配充低配的合法玩法保留但不可误触。 3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。 +### 批次 H · 修掉"乱弹匹配提醒"与"赏金没换算成元"(本轮) + +1. ✅ **发布时不再乱弹「内部寄售库存匹配提醒」**: + 根因在 `matchPeacekeeperOrderWithInventory`——精确匹配判定失败后**无条件 fall through 到 + 部分匹配**。而对"订单没写系统/区服/系列/钻级"这类字段,`compareSpec` 返回 `unknown`(不是 + mismatch)会直接放行,于是**任何只写了游戏名的订单都会命中仓库里全部在库货**, + 表现为"随便发布点什么都会弹一下",提醒失去意义。 + 现在严格模式加两道门槛:① 系统/区服/系列必须都能确认一致;② 还要有正信号(库存配置更高, + 或订单压根没提钻级)。"订单没写规格"不再算命中。 +2. ✅ **人工履约复检不被连带收紧**:`matchPeacekeeperOrderWithInventory` 增加 + `allowUnspecifiedSpecs` 选项。运营已人工选定现货时走宽松模式(标题没写全规格不硬拦, + 但真冲突仍然拦),自动提醒永远走严格模式,两个语义互不污染。 +3. ✅ **接单赏金换算成元**:`WorkOrdersPanel.tsx` 里弹窗直接用 `rewardAmount`(单位是分) + 渲染,把 ¥40 显示成 ¥4000。同文件其他 30 余处都是 `/100`,只有这里漏了。已修正, + 并核对弹窗内 `内部收购成本` / `履约押金` 本来就是 `/100`(正确)。 + ### 批次 G · 现货作废口径 + 押金处置可选(本轮) 1. ✅ **确认加好友后现货一律作废下架,不再回池**(严口径):