fix(consignment): 待匹配看板同分时璀璨不同优先排序

- 同分排序键改为 璀璨 → 系列 → 钻石;璀璨不同(冲突/高配)优先,璀璨相同靠后
- 新增「璀璨不同必须排在璀璨相同之前」单元测试,分数口径保持不变
This commit is contained in:
2026-11-15 14:01:46 +08:00
parent a3462605c7
commit 92564cd731
2 changed files with 72 additions and 11 deletions
@@ -262,7 +262,7 @@ const BUILT_IN_SPECS = [
sortOrder: Number(sortOrder),
}))
test('看板新口径:同分时按 系列 → 钻石 → 璀璨 优先,枪型相同的必须排前面', () => {
test('看板新口径:同分且璀璨相同时,按 系列 → 钻石 收敛', () => {
const profile = buildProfile({
config: { ...DEFAULT_CONSIGNMENT_PROFILE_CONFIG, specs: BUILT_IN_SPECS },
})
@@ -313,7 +313,7 @@ test('看板新口径:同分时按 系列 → 钻石 → 璀璨 优先,枪
)
})
test('看板新口径:系列都相同时,钻石相同优先于仅璀璨冲突', () => {
test('看板新口径:同分时璀璨不同优先于璀璨相同(即使钻石冲突)', () => {
const profile = buildProfile({
config: { ...DEFAULT_CONSIGNMENT_PROFILE_CONFIG, specs: BUILT_IN_SPECS },
})
@@ -361,6 +361,57 @@ test('看板新口径:系列都相同时,钻石相同优先于仅璀璨冲
)
})
test('看板新口径:同分时璀璨不同的组合必须排在璀璨相同的组合之前', () => {
const profile = buildProfile({
config: { ...DEFAULT_CONSIGNMENT_PROFILE_CONFIG, specs: BUILT_IN_SPECS },
})
const order = buildOrder({
id: 303,
product_name: '安卓血宴3钻带1璀璨',
material_json: JSON.stringify({ collect: { fields: { serverZone: '安卓' } } }),
})
// 璀璨不同(订单 1 / 库存 2),但枪型、钻级都相同。
const brillianceDiff = buildInventory({
id: 405,
category_id: profile.categoryId,
item_name: '安卓血宴3钻2璀璨 苏尔南',
system: '安卓',
server_zone: '互通',
series: '血宴',
diamond_level: 3,
brilliance_count: 2,
inbound_at: '2026-11-05T09:00:00.000Z',
})
// 璀璨相同,但枪型不同:与上一条同为 80 分。
const brillianceSame = buildInventory({
id: 406,
category_id: profile.categoryId,
item_name: '安卓怒嚎3钻1璀璨 苏尔南',
system: '安卓',
server_zone: '互通',
series: '怒嚎',
diamond_level: 3,
brilliance_count: 1,
inbound_at: '2026-11-01T09:00:00.000Z',
})
const result = buildPendingConsignmentMatchPairs({
orders: [order],
// 故意把「璀璨相同」的库存放前面,验证排序而不是输入顺序。
inventories: [brillianceSame, brillianceDiff],
profileByOrderId: new Map([[303, profile]]),
includeConflicts: true,
})
assert.deepEqual(
result.pairs.map((pair) => [pair.inventoryId, pair.score]),
[
[405, 80],
[406, 80],
],
)
})
test('匹配看板:关键字同时搜索工单号与寄售单号', () => {
const orders = [buildOrder({ id: 1, work_order_no: 'WK001' })]
const inventories = [
@@ -251,14 +251,14 @@ function scoreFromDetails(details: PendingConsignmentMatchDetail[]): number {
}
/**
* 同分排序的规格优先级:系列(枪型)> 钻石等级 > 璀璨数。
* 同分排序的规格优先级:璀璨数 > 系列(枪型)> 钻石等级。
*
* 等权评分下「系列冲突」和「璀璨冲突」都是 80 分,若不在这里分层,
* 同分就只能按工单 ID 排序,枪型不匹配的组合会压过枪型相同的高配组合。
* 硬性口径:同分时「璀璨不同」的组合必须排在「璀璨相同」之前,
* 让运营优先看到需要核对/补璀璨的配对;璀璨一致时再按枪型、钻级收敛。
*/
const SPEC_PRIORITY_KEYS = ['series', 'diamondLevel', 'brillianceCount'] as const
const SPEC_PRIORITY_KEYS = ['brillianceCount', 'series', 'diamondLevel'] as const
/** 字段状态的优先级:命中 > 高配(partial)> 订单未写 > 冲突。 */
/** 普通字段:命中优先(相同 > 高配 > 未写 > 冲突)。 */
const SPEC_STATUS_RANK: Record<string, number> = {
match: 4,
partial: 3,
@@ -267,11 +267,22 @@ const SPEC_STATUS_RANK: Record<string, number> = {
ignored: 0,
}
/** 璀璨数:不同优先(差异 > 高配 > 未写 > 相同)。 */
const BRILLIANCE_STATUS_RANK: Record<string, number> = {
conflict: 4,
partial: 3,
unknown: 2,
match: 1,
ignored: 0,
}
/** 生成可逐项比较的优先级键;缺少明细(旧评分兜底)时全为 0。 */
function buildSpecPriorityKey(details: PendingConsignmentMatchDetail[]): number[] {
return SPEC_PRIORITY_KEYS.map((key) => {
const detail = details.find((item) => item.key === key)
return detail ? (SPEC_STATUS_RANK[detail.status] ?? 0) : 0
if (!detail) return 0
const rank = key === 'brillianceCount' ? BRILLIANCE_STATUS_RANK : SPEC_STATUS_RANK
return rank[detail.status] ?? 0
})
}
@@ -565,9 +576,8 @@ export function buildPendingConsignmentMatchPairs(input: {
pairs.sort((left, right) => {
if (right.score !== left.score) return right.score - left.score
// 同分时先比关键规格:系列(枪型)> 钻石等级 > 璀璨数。
// 等权评分下「系列冲突」与「璀璨冲突」都是 80 分,不分层就会按工单 ID 排序,
// 导致枪型不匹配的组合压过「枪型相同、仅璀璨/钻级有差」的高配组合。
// 同分时先比璀璨数:璀璨不同(需人工核对)优先,再按 系列 → 钻石 收敛。
// 等权评分下「系列冲突」与「璀璨冲突」都是 80 分,不分层就会按工单 ID 排序。
const leftPriority = buildSpecPriorityKey(left.matchDetails)
const rightPriority = buildSpecPriorityKey(right.matchDetails)
for (let index = 0; index < leftPriority.length; index += 1) {