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 e411ff45..59f90ff4 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 @@ -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, + ) +}) 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 545e3d50..f307e4e5 100644 --- a/apps/backend/src/services/worker-platform/consignment-matching-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-matching-service.ts @@ -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) diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts index e1bcc2d5..a188990a 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.test.ts @@ -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 !== '互通'", + ), + ) +}) diff --git a/apps/backend/src/services/worker-platform/consignment-worker-service.ts b/apps/backend/src/services/worker-platform/consignment-worker-service.ts index a325c04e..a5f37e05 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts @@ -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() diff --git a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx index 4e2f557c..b7e4bc22 100644 --- a/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/ConsignmentInventoryPanel.tsx @@ -41,6 +41,7 @@ import type { ConsignmentInventory, GiftEvidenceReviewStatus } from '@/types/con import { fetchAdminSession } from '@/services/admin/auth' import { resolveAdminPermission } from '@/utils/admin-auth' import { describeGiftEvidence, resolveGiftEvidenceBadge } from '@/utils/consignment-evidence' +import { CONSIGNMENT_SYSTEM_OPTIONS, CONSIGNMENT_ZONE_OPTIONS } from '@/utils/consignment-spec' import { formatDateTime } from '@/utils/date-time' const { Text } = Typography @@ -56,6 +57,7 @@ const ZONE_OPTIONS = [ { label: '全部区服', value: '' }, { label: '微信区', value: '微信区' }, { label: 'QQ区', value: 'QQ区' }, + { label: '互通', value: '互通' }, ] const SERIES_OPTIONS = [ @@ -842,12 +844,13 @@ export default function ConsignmentInventoryPanel() { - + {/* 后台全量选项:系统 苹果/安卓/通用,区服 微信/QQ/互通(互通=QQ/微信都能收) */} + + + {/* 审核可修正规格:与打手端发布同一套选项(苹果/安卓;微信/QQ/互通) */} + +