fix(consignment): 修掉发布时乱弹内部库存提醒,并把弹窗赏金换算成元

乱弹窗根因: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 例)全绿。
This commit is contained in:
yml2213
2026-09-17 10:30:17 +08:00
parent d158cc91bc
commit 99752dce9e
5 changed files with 116 additions and 19 deletions
@@ -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,
@@ -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('通用件与明确规格冲突时仍然排除', () => {
@@ -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)
}
@@ -2941,7 +2941,8 @@ export default function WorkOrdersPanel() {
<div>
当前工单原定接单赏金:
<strong>
¥{Number(consignmentInterceptModal.order?.rewardAmount || 0).toFixed(2)}
{/* rewardAmount 是分,必须除以 100:这里以前直接 toFixed,把 ¥40 显示成 ¥4000 */}
¥{(Number(consignmentInterceptModal.order?.rewardAmount || 0) / 100).toFixed(2)}
</strong>
</div>
<div style={{ color: '#389e0d', marginTop: 4 }}>
+17
View File
@@ -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. ✅ **确认加好友后现货一律作废下架,不再回池**(严口径):