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 141e2896..c5a9cb69 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 @@ -531,12 +531,72 @@ test('发布寄卖建议价:按璀璨数精准区分价格,不会同钻级 assert.equal(hit3.matchedRules.length, 1) assert.equal(hit3.matchedRules[0].ruleKey, 'rule-5z-3c') - // 5. 选 4璀(模板库未配 4璀 价格),不命中,返回 null + // 5. 选 4璀/5璀(号主出的璀璨比配置高):按当前钻级已配置的最高璀璨(3璀 28000 分)报价 const hit4 = suggestConsignmentPriceFromRules(rules, { system: '苹果', series: '怒嚎', diamondLevel: 5, brillianceCount: 4, }) - assert.equal(hit4, null) + assert.ok(hit4, '超出配置的璀璨应命中已配置的最高璀璨') + assert.equal(hit4.suggestedPriceFen, 28000) + assert.equal(hit4.matchedRules[0].ruleKey, 'rule-5z-3c') + + const hit5 = suggestConsignmentPriceFromRules(rules, { + system: '苹果', + series: '怒嚎', + diamondLevel: 5, + brillianceCount: 5, + }) + assert.equal(hit5?.suggestedPriceFen, 28000) +}) + +test('发布寄卖建议价:号主钻级比后台配置高时,按已配置的最高钻级报价(7钻报6钻价,低钻不报高钻)', () => { + const rules = [ + { ruleKey: 'rule-1z', productName: '安卓怒嚎1钻', rewardAmountFen: 10000 }, + { ruleKey: 'rule-6z-0c', productName: '安卓怒嚎6钻', rewardAmountFen: 35000 }, + { ruleKey: 'rule-6z-2c', productName: '安卓怒嚎6钻2璀', rewardAmountFen: 42000 }, + ] + + // 号主出 7钻(后台最高只有 6钻):钻级按 6钻 报价 + // 7钻 0璀 → 报 6钻 0璀(35000) + const hit7z0c = suggestConsignmentPriceFromRules(rules, { + system: '安卓', + series: '怒嚎', + diamondLevel: 7, + brillianceCount: 0, + }) + assert.equal(hit7z0c?.suggestedPriceFen, 35000) + assert.equal(hit7z0c?.matchedRules[0].ruleKey, 'rule-6z-0c') + + // 7钻 5璀 → 钻级按 6钻,璀璨按 6钻最大配置(2璀)报价(42000) + const hit7z5c = suggestConsignmentPriceFromRules(rules, { + system: '安卓', + series: '怒嚎', + diamondLevel: 7, + brillianceCount: 5, + }) + assert.equal(hit7z5c?.suggestedPriceFen, 42000) + assert.equal(hit7z5c?.matchedRules[0].ruleKey, 'rule-6z-2c') + + // 低配不能充高配:号主出 2钻(未超出 6钻 上限,但未单独配置 2钻):不命中返回 null,绝不误报 6钻 + const hit2z = suggestConsignmentPriceFromRules(rules, { + system: '安卓', + series: '怒嚎', + diamondLevel: 2, + brillianceCount: 0, + }) + assert.equal(hit2z, null) + + // 号主出 1钻,但如果后台只有 3钻 以上规则,不命中返回 null + const onlyHighRules = [{ ruleKey: 'rule-3z', productName: '安卓怒嚎3钻', rewardAmountFen: 20000 }] + assert.equal( + suggestConsignmentPriceFromRules(onlyHighRules, { + system: '安卓', + series: '怒嚎', + diamondLevel: 1, + brillianceCount: 0, + }), + 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 0fca5e05..3121985e 100644 --- a/apps/backend/src/services/worker-platform/consignment-matching-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-matching-service.ts @@ -430,10 +430,13 @@ export type ConsignmentPriceSuggestionRule = { * 从「接单模板」(work_product_rules)里给寄卖提报找建议价。 * * 模板的商品名(如「安卓暗潮1钻」)用与订单同一套规格解析器拆解: - * 系列、钻级必须完全一致;系统按通配口径比对(规则或提报任一侧是「通用/双端」都算匹配)。 - * 区服不参与——模板价不分微信/QQ。 - * 若指定了璀璨数(0~7),模板商品解析出的璀璨数必须一致(0璀对应无璀璨/0璀,3璀对应3璀/3璀璨);未指定则忽略璀璨。 - * 多条命中取最高价(该规格能卖到的最好价钱),卖家仍可手动改。 + * 系统(含通用通配)、系列必须一致;区服不参与比对。 + * 钻级与璀璨数支持高配向下兜底匹配(号主出得比配置高时,报价当前已配置的最高规格): + * 1. 钻级对齐:优先精确匹配号主钻级;若号主钻级高于后台配置上限(如号主出7钻但后台只配了1~6钻), + * 按后台已配置的最大钻级(6钻)报价。低钻不能配高钻(号主出1钻不报6钻价)。 + * 2. 璀璨对齐:在该钻级下,优先精确匹配号主璀璨;若号主璀璨高于后台配置上限(如号主出3~5璀但后台只配了1~2璀), + * 按后台该钻级已配置的最大璀璨档位报价。 + * 3. 命中多条(如通用与专属端同存)取最高价,卖家仍可手动修改。 */ export function suggestConsignmentPriceFromRules( rules: ConsignmentPriceSuggestionRule[], @@ -455,19 +458,61 @@ export function suggestConsignmentPriceFromRules( : null if (!wantedSystem || !wantedSeries || wantedDiamond <= 0) return null - const matched = rules.filter((rule) => { - const rewardFen = Math.round(Number(rule.rewardAmountFen || 0)) - if (rewardFen <= 0) return false - const parsed = extractPeacekeeperSpecs(rule.productName) - if (!parsed.system) return false - if (parsed.series !== wantedSeries) return false - if (Number(parsed.diamondLevel || 0) !== wantedDiamond) return false - if (wantedBrilliance !== null && Number(parsed.brillianceCount || 0) !== wantedBrilliance) { - return false + // 1. 筛选系统一致(含通用通配)且系列一致、金额大于 0 的有效候选规则 + const seriesCandidates = rules + .map((rule) => { + const rewardFen = Math.round(Number(rule.rewardAmountFen || 0)) + if (rewardFen <= 0) return null + const parsed = extractPeacekeeperSpecs(rule.productName) + if (!parsed.system) return null + if (parsed.series !== wantedSeries) return null + const isSystemMatch = + parsed.system === wantedSystem || parsed.system === '通用' || wantedSystem === '通用' + if (!isSystemMatch) return null + const diamondLevel = Number(parsed.diamondLevel || 0) + if (diamondLevel <= 0) return null + const brillianceCount = Number(parsed.brillianceCount || 0) + return { rule, parsed, rewardFen, diamondLevel, brillianceCount } + }) + .filter((item): item is NonNullable => item !== null) + + if (seriesCandidates.length === 0) return null + + // 2. 钻级对齐: + // 查找当前系统与系列已配置的最大钻级;若号主出的钻级比后台配置高(如后台只设置了6个价格,号主出7个), + // 按后台已配置的最大钻级报价;未超出配置上限时必须精确对齐钻级(低钻不报高钻,中间未配价的钻级不向下误报) + const maxConfiguredDiamond = Math.max(...seriesCandidates.map((item) => item.diamondLevel)) + const targetDiamond = wantedDiamond > maxConfiguredDiamond ? maxConfiguredDiamond : wantedDiamond + + const targetDiamondCandidates = seriesCandidates.filter( + (item) => item.diamondLevel === targetDiamond, + ) + if (targetDiamondCandidates.length === 0) return null + + // 3. 璀璨对齐: + // 若未指定璀璨(null),取 targetDiamond 下所有规则 + // 若指定了璀璨:比后台配置高(如后台该钻级只配置到 1~2 璀,号主出 3/4/5 等),按已配置的最大璀璨报价; + // 未超出配置上限时严格精确对齐璀璨档位(0璀对0璀、1璀对1璀,不拿高璀价格报给低璀) + let matchedItems: typeof targetDiamondCandidates + if (wantedBrilliance === null) { + matchedItems = targetDiamondCandidates + } else { + const maxConfiguredBrilliance = Math.max( + ...targetDiamondCandidates.map((item) => item.brillianceCount), + ) + if (wantedBrilliance > maxConfiguredBrilliance) { + matchedItems = targetDiamondCandidates.filter( + (item) => item.brillianceCount === maxConfiguredBrilliance, + ) + } else { + matchedItems = targetDiamondCandidates.filter( + (item) => item.brillianceCount === wantedBrilliance, + ) } - return parsed.system === wantedSystem || parsed.system === '通用' || wantedSystem === '通用' - }) - if (matched.length === 0) return null - const suggestedPriceFen = Math.max(...matched.map((rule) => Math.round(rule.rewardAmountFen))) - return { suggestedPriceFen, matchedRules: matched } + } + + if (matchedItems.length === 0) return null + const suggestedPriceFen = Math.max(...matchedItems.map((item) => item.rewardFen)) + const matchedRules = matchedItems.map((item) => item.rule) + return { suggestedPriceFen, matchedRules } } diff --git a/docs/寄卖系统优化方案.md b/docs/寄卖系统优化方案.md index 4d154ce6..2fb6316e 100644 --- a/docs/寄卖系统优化方案.md +++ b/docs/寄卖系统优化方案.md @@ -342,9 +342,12 @@ - 动态禁用:`BRILLIANCE_OPTIONS` 中 `value > diamondLevel` 自动置灰禁用(1钻最多1璀,2钻最多2璀…)。 - 自动收敛:调小钻级时(如 5钻改为 2钻),原选璀璨数超出时自动收敛至新钻级(2璀),并联动重算建议价与物品名。 - 默认值与联动:璀璨数默认 `initialValue={0}`(无),`queryKey` 纳入 `${bri}`,切换璀璨即刻重新带价。 - - 双重校验:前端 Validator 校验 + 后端 `publishWorkerConsignment` 拦截(`brilliance_exceeds_diamond`)。 +4. ✅ **高规格溢出自动按当前配置最高档位报价**: + - 璀璨溢出:当号主提报的璀璨数高于后台已配置上限(如后台该钻级只配了 0~2 璀,号主出 3/4/5 璀等),自动按当前钻级已配置的最高璀璨档位报价。 + - 钻级溢出:当号主提报的钻级高于后台配置上限(如后台只配了 1~6 钻,号主出 7 钻),自动按后台当前已配置的最大钻级报价(7钻报6钻价)。 + - 边界安全:仅在超出配置上限时触发兜底;未超出配置上限时严格精确对齐(低钻绝不报高钻价,中间未配价规格返回 null 由号主手动填写,防错价)。 -**验证**:后端 487 / 前端 54 测试全绿;两侧 `typecheck` / `lint:check` / `format:check` 通过。 +**验证**:后端 488 / 前端 54 测试全绿;两侧 `typecheck` / `lint:check` / `format:check` 通过。 ### 批次 W · 规格口径:互通通配 + 后台全量/发布端分物品展示(本轮) **业务定案**: