feat(consignment): 建议价精准匹配璀璨数 + 限制璀璨不超过钻石数量
- 修复价格匹配:parseSpecsFromText 支持「璀」字缩写(1璀/2璀/3璀)与「无璀/0璀」, 避免生产规则被误判为 0 璀;suggestConsignmentPriceFromRules 增加 brillianceCount 精准对齐匹配,未配对应模板则返回空,解决同钻级多条规则全中并取最高价的问题 - 透传参数:路由与打手端服务均增加 brillianceCount 传递 - 限制璀璨不超过钻级: - 前端选项动态禁用:大于当前钻级的璀璨芯片置灰 - 钻级调小自动收敛:若原选璀璨大于新钻级,自动 clamp 至新钻级 - 默认值与联动:璀璨数默认 initialValue=0,queryKey 纳入璀璨,切换即刻带出对应建议价 - 前后端双重校验:前端表单 Validator 校验 + 后端提报硬拦截(brilliance_exceeds_diamond) - 后端 487 / 前端 54 测试全绿,docs/寄卖系统优化方案.md 记录批次 Z
This commit is contained in:
@@ -36,6 +36,7 @@ router.get('/consignment/price-suggestion', requireActiveWorker, async (req, res
|
||||
system: req.query.system,
|
||||
series: req.query.series,
|
||||
diamondLevel: req.query.diamondLevel,
|
||||
brillianceCount: req.query.brillianceCount,
|
||||
})
|
||||
res.json({ ok: true, data: result })
|
||||
} catch (error) {
|
||||
|
||||
@@ -452,3 +452,91 @@ test('发布寄卖建议价:模板写「通用/双端」时两端都命中', (
|
||||
null,
|
||||
)
|
||||
})
|
||||
|
||||
test('extractPeacekeeperSpecs: 支持「璀」字缩写与「无璀/0璀」', () => {
|
||||
const s1 = extractPeacekeeperSpecs('苹果怒嚎5钻3璀')
|
||||
assert.equal(s1.diamondLevel, 5)
|
||||
assert.equal(s1.brillianceCount, 3)
|
||||
|
||||
const s2 = extractPeacekeeperSpecs('苹果怒嚎5钻无璀')
|
||||
assert.equal(s2.diamondLevel, 5)
|
||||
assert.equal(s2.brillianceCount, 0)
|
||||
|
||||
const s3 = extractPeacekeeperSpecs('苹果怒嚎5钻0璀')
|
||||
assert.equal(s3.diamondLevel, 5)
|
||||
assert.equal(s3.brillianceCount, 0)
|
||||
|
||||
const s4 = extractPeacekeeperSpecs('苹果怒嚎5钻1璀')
|
||||
assert.equal(s4.diamondLevel, 5)
|
||||
assert.equal(s4.brillianceCount, 1)
|
||||
|
||||
const s5 = extractPeacekeeperSpecs('苹果怒嚎5钻2璀璨')
|
||||
assert.equal(s5.diamondLevel, 5)
|
||||
assert.equal(s5.brillianceCount, 2)
|
||||
})
|
||||
|
||||
test('发布寄卖建议价:按璀璨数精准区分价格,不会同钻级多规则全命中取最高', () => {
|
||||
const rules = [
|
||||
{ ruleKey: 'rule-5z-0c', productName: '苹果怒嚎5钻', rewardAmountFen: 15000 },
|
||||
{ ruleKey: 'rule-5z-1c', productName: '苹果怒嚎5钻1璀', rewardAmountFen: 19000 },
|
||||
{ ruleKey: 'rule-5z-2c', productName: '苹果怒嚎5钻2璀璨', rewardAmountFen: 23000 },
|
||||
{ ruleKey: 'rule-5z-3c', productName: '苹果怒嚎5钻3璀', rewardAmountFen: 28000 },
|
||||
]
|
||||
|
||||
// 1. 选 0璀(无璀璨),命中 0璀(15000 分,1 条规则,不是 4 条)
|
||||
const hit0 = suggestConsignmentPriceFromRules(rules, {
|
||||
system: '苹果',
|
||||
series: '怒嚎',
|
||||
diamondLevel: 5,
|
||||
brillianceCount: 0,
|
||||
})
|
||||
assert.ok(hit0)
|
||||
assert.equal(hit0.suggestedPriceFen, 15000)
|
||||
assert.equal(hit0.matchedRules.length, 1)
|
||||
assert.equal(hit0.matchedRules[0].ruleKey, 'rule-5z-0c')
|
||||
|
||||
// 2. 选 1璀,命中 1璀(19000 分)
|
||||
const hit1 = suggestConsignmentPriceFromRules(rules, {
|
||||
system: '苹果',
|
||||
series: '怒嚎',
|
||||
diamondLevel: 5,
|
||||
brillianceCount: 1,
|
||||
})
|
||||
assert.ok(hit1)
|
||||
assert.equal(hit1.suggestedPriceFen, 19000)
|
||||
assert.equal(hit1.matchedRules.length, 1)
|
||||
assert.equal(hit1.matchedRules[0].ruleKey, 'rule-5z-1c')
|
||||
|
||||
// 3. 选 2璀,命中 2璀璨(23000 分)
|
||||
const hit2 = suggestConsignmentPriceFromRules(rules, {
|
||||
system: '苹果',
|
||||
series: '怒嚎',
|
||||
diamondLevel: 5,
|
||||
brillianceCount: 2,
|
||||
})
|
||||
assert.ok(hit2)
|
||||
assert.equal(hit2.suggestedPriceFen, 23000)
|
||||
assert.equal(hit2.matchedRules.length, 1)
|
||||
assert.equal(hit2.matchedRules[0].ruleKey, 'rule-5z-2c')
|
||||
|
||||
// 4. 选 3璀,命中 3璀(28000 分)
|
||||
const hit3 = suggestConsignmentPriceFromRules(rules, {
|
||||
system: '苹果',
|
||||
series: '怒嚎',
|
||||
diamondLevel: 5,
|
||||
brillianceCount: 3,
|
||||
})
|
||||
assert.ok(hit3)
|
||||
assert.equal(hit3.suggestedPriceFen, 28000)
|
||||
assert.equal(hit3.matchedRules.length, 1)
|
||||
assert.equal(hit3.matchedRules[0].ruleKey, 'rule-5z-3c')
|
||||
|
||||
// 5. 选 4璀(模板库未配 4璀 价格),不命中,返回 null
|
||||
const hit4 = suggestConsignmentPriceFromRules(rules, {
|
||||
system: '苹果',
|
||||
series: '怒嚎',
|
||||
diamondLevel: 5,
|
||||
brillianceCount: 4,
|
||||
})
|
||||
assert.equal(hit4, null)
|
||||
})
|
||||
|
||||
@@ -68,15 +68,15 @@ function parseSpecsFromText(rawText: string): Omit<ParsedOrderSpecs, 'rawText'>
|
||||
const text = String(rawText || '').trim()
|
||||
|
||||
const diamondMatch = text.toLowerCase().match(/([1-7])\s*钻/)
|
||||
const brillianceZero = /无璀璨|不带璀璨|0\s*璀璨/.test(text)
|
||||
const brillianceMatch = text.match(/([1-7])\s*璀璨/)
|
||||
const brillianceZero = /无\s*璀(?:璨)?|不带\s*璀(?:璨)?|0\s*璀(?:璨)?/.test(text)
|
||||
const brillianceMatch = text.match(/([1-7])\s*璀(?:璨)?/)
|
||||
|
||||
let brillianceCount = 0
|
||||
if (brillianceZero) {
|
||||
brillianceCount = 0
|
||||
} else if (brillianceMatch?.[1]) {
|
||||
brillianceCount = Number(brillianceMatch[1])
|
||||
} else if (text.includes('璀璨')) {
|
||||
} else if (text.includes('璀')) {
|
||||
brillianceCount = 1
|
||||
}
|
||||
|
||||
@@ -431,16 +431,28 @@ export type ConsignmentPriceSuggestionRule = {
|
||||
*
|
||||
* 模板的商品名(如「安卓暗潮1钻」)用与订单同一套规格解析器拆解:
|
||||
* 系列、钻级必须完全一致;系统按通配口径比对(规则或提报任一侧是「通用/双端」都算匹配)。
|
||||
* 区服不参与——模板价不分微信/QQ。璀璨也不参与——模板价没按璀璨细分,卖家可自行加价。
|
||||
* 区服不参与——模板价不分微信/QQ。
|
||||
* 若指定了璀璨数(0~7),模板商品解析出的璀璨数必须一致(0璀对应无璀璨/0璀,3璀对应3璀/3璀璨);未指定则忽略璀璨。
|
||||
* 多条命中取最高价(该规格能卖到的最好价钱),卖家仍可手动改。
|
||||
*/
|
||||
export function suggestConsignmentPriceFromRules(
|
||||
rules: ConsignmentPriceSuggestionRule[],
|
||||
spec: { system: string; series: string; diamondLevel: number },
|
||||
spec: {
|
||||
system: string
|
||||
series: string
|
||||
diamondLevel: number
|
||||
brillianceCount?: number | null | undefined
|
||||
},
|
||||
): { suggestedPriceFen: number; matchedRules: ConsignmentPriceSuggestionRule[] } | null {
|
||||
const wantedSystem = normalizePeacekeeperSystem(spec.system)
|
||||
const wantedSeries = normalizePeacekeeperSeries(spec.series)
|
||||
const wantedDiamond = Math.round(Number(spec.diamondLevel || 0))
|
||||
const wantedBrilliance =
|
||||
spec.brillianceCount !== undefined &&
|
||||
spec.brillianceCount !== null &&
|
||||
String(spec.brillianceCount).trim() !== ''
|
||||
? Math.max(0, Math.round(Number(spec.brillianceCount || 0)))
|
||||
: null
|
||||
if (!wantedSystem || !wantedSeries || wantedDiamond <= 0) return null
|
||||
|
||||
const matched = rules.filter((rule) => {
|
||||
@@ -450,6 +462,9 @@ export function suggestConsignmentPriceFromRules(
|
||||
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
|
||||
}
|
||||
return parsed.system === wantedSystem || parsed.system === '通用' || wantedSystem === '通用'
|
||||
})
|
||||
if (matched.length === 0) return null
|
||||
|
||||
@@ -71,3 +71,15 @@ test('苏尔南必须区分苹果/安卓(不收「通用」),区服固定
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
test('寄卖提报硬校验:璀璨数不能超过钻石等级(1钻最多1璀以此类推)', async () => {
|
||||
const serviceSource = await readServiceSource()
|
||||
assert.ok(
|
||||
serviceSource.includes('brillianceCount > diamondLevel'),
|
||||
'服务端必须校验璀璨数不能超过钻石等级',
|
||||
)
|
||||
assert.ok(
|
||||
serviceSource.includes("errorCode: 'brilliance_exceeds_diamond'"),
|
||||
'超限必须返回专门的错误码',
|
||||
)
|
||||
})
|
||||
|
||||
@@ -80,6 +80,12 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
||||
})
|
||||
}
|
||||
const brillianceCount = Math.max(0, Math.min(7, Number(payload.brillianceCount)))
|
||||
if (brillianceCount > diamondLevel) {
|
||||
throw createHttpError('璀璨数不能超过钻石等级(如1钻最多1璀、2钻最多2璀)', {
|
||||
statusCode: 400,
|
||||
errorCode: 'brilliance_exceeds_diamond',
|
||||
})
|
||||
}
|
||||
|
||||
const contactInfo = String(payload.sellerContact || payload.contactInfo || '').trim()
|
||||
if (!contactInfo) {
|
||||
@@ -432,6 +438,7 @@ export async function getWorkerConsignmentPriceSuggestion(payload: {
|
||||
system?: unknown
|
||||
series?: unknown
|
||||
diamondLevel?: unknown
|
||||
brillianceCount?: unknown
|
||||
}): Promise<{
|
||||
suggestedPriceFen: number | null
|
||||
matchedCount: number
|
||||
@@ -440,11 +447,22 @@ export async function getWorkerConsignmentPriceSuggestion(payload: {
|
||||
const system = String(payload.system || '').trim()
|
||||
const series = String(payload.series || '').trim()
|
||||
const diamondLevel = Math.round(Number(payload.diamondLevel || 0))
|
||||
const brillianceCount =
|
||||
payload.brillianceCount !== undefined &&
|
||||
payload.brillianceCount !== null &&
|
||||
String(payload.brillianceCount).trim() !== ''
|
||||
? Math.max(0, Math.round(Number(payload.brillianceCount || 0)))
|
||||
: undefined
|
||||
if (!system || !series || diamondLevel <= 0) {
|
||||
return { suggestedPriceFen: null, matchedCount: 0, matchedRules: [] }
|
||||
}
|
||||
const rules = await listEnabledWorkProductRulesForSuggestion()
|
||||
const suggestion = suggestConsignmentPriceFromRules(rules, { system, series, diamondLevel })
|
||||
const suggestion = suggestConsignmentPriceFromRules(rules, {
|
||||
system,
|
||||
series,
|
||||
diamondLevel,
|
||||
brillianceCount,
|
||||
})
|
||||
if (!suggestion) {
|
||||
return { suggestedPriceFen: null, matchedCount: 0, matchedRules: [] }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user