feat(consignment): 规格口径定案——互通通配 + 后台全量/发布端按物品展示
业务定案: - 操作后台全量:系统 苹果/安卓/通用,区服 微信区/QQ区/互通;提报审核与 库存编辑的系统/区服从自由文本改为受控下拉(单一来源 utils/consignment-spec.ts) - 打手端发布按物品类型展示:苏尔南系统二选一(必须区分端),区服唯一 「互通」选项且默认选中;后续其他品类在共享常量按品类加条目 - 匹配引擎通配符对称化:区服通配=互通(通用/全区/不限归一化为别名), 系统通配=通用(双端/全端同理);订单侧写通配同样放行,只增不减匹配面; 差异描述不把通配对当冲突 - 提报白名单校验(API 层):系统只收 苹果/安卓,区服只收 微信/QQ/互通, 旧页面缓存或直调接口绕不过 - 物品名拼接:区服为互通时不拼入(对买家无信息量),前后端同口径 后端 481(+互通双向匹配/通配不算差异/真冲突仍排除/白名单校验)、 前端 54(+选项口径/表单引用/默认值)测试全绿;typecheck/lint/format/check:sql 通过 docs/寄卖系统优化方案.md 记录批次 W
This commit is contained in:
@@ -3,6 +3,7 @@ import test from 'node:test'
|
||||
|
||||
import type { ConsignmentInventoryRow } from '../../repositories/worker-platform/consignment-repo.js'
|
||||
import {
|
||||
describeConsignmentSpecDifferences,
|
||||
extractPeacekeeperSpecs,
|
||||
findMatchingPeacekeeperInventories,
|
||||
matchPeacekeeperOrderWithInventory,
|
||||
@@ -336,3 +337,62 @@ test('通用件与明确规格冲突时仍然排除', () => {
|
||||
const wrongZone = buildInventory({ server_zone: 'QQ区' })
|
||||
assert.equal(matchPeacekeeperOrderWithInventory(order, wrongZone), null)
|
||||
})
|
||||
|
||||
test('区服「互通」与微信/QQ 订单互相匹配,且不算差异', () => {
|
||||
const wxOrder = extractPeacekeeperSpecs('安卓微信区 暗潮 1钻')
|
||||
const qqOrder = extractPeacekeeperSpecs('安卓QQ区 暗潮 1钻')
|
||||
const interflowInventory = buildInventory({ server_zone: '互通' })
|
||||
|
||||
// 互通现货:微信 / QQ 订单都能精确匹配
|
||||
assert.equal(matchPeacekeeperOrderWithInventory(wxOrder, interflowInventory)?.matchType, 'exact')
|
||||
assert.equal(matchPeacekeeperOrderWithInventory(qqOrder, interflowInventory)?.matchType, 'exact')
|
||||
|
||||
// 订单写「互通」:微信 / QQ 现货也能匹配(卖家选哪个区服的货都能交付)
|
||||
const interflowOrder = extractPeacekeeperSpecs('安卓互通 暗潮 1钻')
|
||||
assert.equal(
|
||||
matchPeacekeeperOrderWithInventory(interflowOrder, buildInventory())?.matchType,
|
||||
'exact',
|
||||
)
|
||||
assert.equal(
|
||||
matchPeacekeeperOrderWithInventory(interflowOrder, buildInventory({ server_zone: 'QQ区' }))
|
||||
?.matchType,
|
||||
'exact',
|
||||
)
|
||||
|
||||
// 差异描述不把「互通」当冲突(手动指定时别吓唬客服)
|
||||
const diffs = describeConsignmentSpecDifferences(wxOrder, interflowInventory)
|
||||
assert.equal(
|
||||
diffs.some((item) => item.startsWith('区服')),
|
||||
false,
|
||||
)
|
||||
})
|
||||
|
||||
test('订单写「通用/双端」时买家不限系统,两端现货都匹配;旧数据里的「通用」库存不回归', () => {
|
||||
const dualOrder = extractPeacekeeperSpecs('通用微信区 暗潮 1钻')
|
||||
assert.ok(matchPeacekeeperOrderWithInventory(dualOrder, buildInventory()))
|
||||
assert.ok(matchPeacekeeperOrderWithInventory(dualOrder, buildInventory({ system: '苹果' })))
|
||||
|
||||
// 新提报已禁「通用」,但历史库存行仍是「通用」:与任一端订单照常匹配(既有行为)
|
||||
const legacyUniversal = buildInventory({ system: '通用' })
|
||||
assert.ok(
|
||||
matchPeacekeeperOrderWithInventory(
|
||||
extractPeacekeeperSpecs('苹果微信区 暗潮 1钻'),
|
||||
legacyUniversal,
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
test('真冲突仍然排除:互通救不了系统不符,也救不了系列不符', () => {
|
||||
const order = extractPeacekeeperSpecs('苹果微信区 暗潮 1钻')
|
||||
assert.equal(
|
||||
matchPeacekeeperOrderWithInventory(order, buildInventory({ server_zone: '互通' })),
|
||||
null,
|
||||
)
|
||||
assert.equal(
|
||||
matchPeacekeeperOrderWithInventory(
|
||||
order,
|
||||
buildInventory({ server_zone: '互通', series: '怒嚎' }),
|
||||
),
|
||||
null,
|
||||
)
|
||||
})
|
||||
|
||||
@@ -35,6 +35,8 @@ function extractPeacekeeperServerZone(value: string): string {
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!text) return ''
|
||||
// 互通 = QQ/微信都能收,是与 微信区/QQ区 平级的第三种区服取值
|
||||
if (text.includes('互通')) return '互通'
|
||||
if (text.includes('通用') || text.includes('全区') || text.includes('不限')) return '通用'
|
||||
if (text.includes('微信') || text === 'wx' || text.includes('wx区')) return '微信区'
|
||||
if (text.includes('qq') || text.includes('手q') || text.includes('企鹅')) return 'QQ区'
|
||||
@@ -126,18 +128,29 @@ export function extractPeacekeeperSpecs(
|
||||
|
||||
/**
|
||||
* 单项规格比对结论:
|
||||
* - `match` 双方都有值且一致(或库存侧是"通用"件)
|
||||
* - `match` 双方都有值且一致,或任一侧是通配值(区服的「互通」、系统的「通用」都表示不限,
|
||||
* 订单写"通用/双端"意味着买家两端都收,与具体库存同样算 match)
|
||||
* - `mismatch` 双方都有值且冲突 → 直接排除该库存
|
||||
* - `unknown` 任一侧没值,无法确认 → **不能算精确匹配**,只能作为"部分匹配"候选
|
||||
*/
|
||||
type SpecVerdict = 'match' | 'mismatch' | 'unknown'
|
||||
|
||||
function compareSpec(orderValue: string, inventoryValue: string): SpecVerdict {
|
||||
/** 系统侧通配值:新提报已不允许「通用」,但旧库存行与订单文案里仍可能出现 */
|
||||
const SYSTEM_WILDCARDS = ['通用']
|
||||
/** 区服侧通配值:「互通」= QQ/微信都能收;「通用/全区/不限」是订单文案里的旧写法 */
|
||||
const ZONE_WILDCARDS = ['通用', '互通']
|
||||
|
||||
function compareSpec(
|
||||
orderValue: string,
|
||||
inventoryValue: string,
|
||||
wildcards: string[] = SYSTEM_WILDCARDS,
|
||||
): SpecVerdict {
|
||||
const order = String(orderValue || '').trim()
|
||||
const inventory = String(inventoryValue || '').trim()
|
||||
if (!inventory) return 'unknown'
|
||||
if (inventory === '通用') return 'match'
|
||||
if (wildcards.includes(inventory)) return 'match'
|
||||
if (!order) return 'unknown'
|
||||
if (wildcards.includes(order)) return 'match'
|
||||
return order === inventory ? 'match' : 'mismatch'
|
||||
}
|
||||
|
||||
@@ -160,7 +173,7 @@ export function matchPeacekeeperOrderWithInventory(
|
||||
|
||||
const verdictSystem = compareSpec(order.system, invSystem)
|
||||
if (verdictSystem === 'mismatch') return null
|
||||
const verdictZone = compareSpec(order.serverZone, invZone)
|
||||
const verdictZone = compareSpec(order.serverZone, invZone, ZONE_WILDCARDS)
|
||||
if (verdictZone === 'mismatch') return null
|
||||
const verdictSeries = compareSpec(order.series, invSeries)
|
||||
if (verdictSeries === 'mismatch') return null
|
||||
@@ -268,15 +281,27 @@ export function describeConsignmentSpecDifferences(
|
||||
inventory: ConsignmentInventoryRow,
|
||||
): string[] {
|
||||
const diffs: string[] = []
|
||||
const compare = (label: string, orderValue: string, inventoryValue: string) => {
|
||||
const compare = (
|
||||
label: string,
|
||||
orderValue: string,
|
||||
inventoryValue: string,
|
||||
wildcards: string[] = SYSTEM_WILDCARDS,
|
||||
) => {
|
||||
const left = String(orderValue || '').trim() || '未指定'
|
||||
const right = String(inventoryValue || '').trim() || '未填写'
|
||||
// 通配值(互通/通用)表示"不限",不算差异,别在冲突清单里吓唬客服
|
||||
if (left === '未指定' || left === right) return
|
||||
if (wildcards.includes(left) || wildcards.includes(right)) return
|
||||
diffs.push(`${label}:订单「${left}」/ 现货「${right}」`)
|
||||
}
|
||||
|
||||
compare('系统', order.system, normalizePeacekeeperSystem(inventory.system))
|
||||
compare('区服', order.serverZone, normalizePeacekeeperServerZone(inventory.server_zone))
|
||||
compare(
|
||||
'区服',
|
||||
order.serverZone,
|
||||
normalizePeacekeeperServerZone(inventory.server_zone),
|
||||
ZONE_WILDCARDS,
|
||||
)
|
||||
compare('系列', order.series, normalizePeacekeeperSeries(inventory.series))
|
||||
const orderDiamond = Number(order.diamondLevel || 0)
|
||||
const invDiamond = Number(inventory.diamond_level || 0)
|
||||
|
||||
@@ -55,3 +55,19 @@ test('没有提报单的兜底行不给卖家操作能力,也不暴露内部
|
||||
assert.ok(serviceSource.includes("consignmentNo: '客服手工入库'"))
|
||||
assert.ok(serviceSource.includes('canConfirmFriend: false, canSubmitEvidence: false'))
|
||||
})
|
||||
|
||||
test('苏尔南必须区分苹果/安卓(不收「通用」),区服固定 微信区/QQ区/互通 三选', async () => {
|
||||
const serviceSource = await readServiceSource()
|
||||
assert.ok(
|
||||
serviceSource.includes("errorCode: 'consignment_system_invalid'"),
|
||||
'系统取值不合法要有显式拒绝,防止旧页面缓存绕过前端选项',
|
||||
)
|
||||
assert.ok(serviceSource.includes("errorCode: 'consignment_server_zone_invalid'"))
|
||||
// 校验必须是白名单式(只认 苹果/安卓 与 微信区/QQ区/互通),而不是只挡「通用」
|
||||
assert.ok(serviceSource.includes("system !== '苹果' && system !== '安卓'"))
|
||||
assert.ok(
|
||||
serviceSource.includes(
|
||||
"serverZone !== '微信区' && serverZone !== 'QQ区' && serverZone !== '互通'",
|
||||
),
|
||||
)
|
||||
})
|
||||
|
||||
@@ -41,6 +41,20 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
||||
errorCode: 'specs_required',
|
||||
})
|
||||
}
|
||||
// 苏尔南寄卖必须区分苹果/安卓(不收「通用」);区服支持 微信区/QQ区/互通 三选
|
||||
if (system !== '苹果' && system !== '安卓') {
|
||||
throw createHttpError('苏尔南寄卖的操作系统只能选 苹果 或 安卓', {
|
||||
statusCode: 400,
|
||||
errorCode: 'consignment_system_invalid',
|
||||
})
|
||||
}
|
||||
// 互通 = QQ/微信都能收(苏尔南发布端默认且唯一展示项)
|
||||
if (serverZone !== '微信区' && serverZone !== 'QQ区' && serverZone !== '互通') {
|
||||
throw createHttpError('游戏区服只能选 微信区 / QQ区 / 互通', {
|
||||
statusCode: 400,
|
||||
errorCode: 'consignment_server_zone_invalid',
|
||||
})
|
||||
}
|
||||
|
||||
if (
|
||||
payload.diamondLevel === undefined ||
|
||||
@@ -107,7 +121,9 @@ export async function publishWorkerConsignment(workerId: number, payload: JsonOb
|
||||
const depositAmountFen = getConsignmentPublishDepositFen()
|
||||
const sellerPhone = String(payload.sellerPhone || worker.phone || '').trim()
|
||||
const briStr = brillianceCount > 0 ? `${brillianceCount}璀璨` : ''
|
||||
const defaultItemName = `${system}${serverZone}${series}${diamondLevel}钻${briStr} 苏尔南`
|
||||
// 区服是「互通」(通配)时不拼进物品名:对买家无信息量,"安卓互通暗潮…"读着也别扭
|
||||
const zoneLabel = serverZone === '互通' ? '' : serverZone
|
||||
const defaultItemName = `${system}${zoneLabel}${series}${diamondLevel}钻${briStr} 苏尔南`
|
||||
const itemName = String(payload.itemName || defaultItemName).trim()
|
||||
const submissionNo = generateConsignmentSubmissionNo()
|
||||
const now = nowIso()
|
||||
|
||||
Reference in New Issue
Block a user