From f8101c17304bd72d1b59979b2c1bc7bf850c1c8c Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 17 Sep 2026 13:01:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(consignment):=20=E8=A7=84=E6=A0=BC?= =?UTF-8?q?=E5=8F=A3=E5=BE=84=E5=AE=9A=E6=A1=88=E2=80=94=E2=80=94=E4=BA=92?= =?UTF-8?q?=E9=80=9A=E9=80=9A=E9=85=8D=20+=20=E5=90=8E=E5=8F=B0=E5=85=A8?= =?UTF-8?q?=E9=87=8F/=E5=8F=91=E5=B8=83=E7=AB=AF=E6=8C=89=E7=89=A9?= =?UTF-8?q?=E5=93=81=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 业务定案: - 操作后台全量:系统 苹果/安卓/通用,区服 微信区/QQ区/互通;提报审核与 库存编辑的系统/区服从自由文本改为受控下拉(单一来源 utils/consignment-spec.ts) - 打手端发布按物品类型展示:苏尔南系统二选一(必须区分端),区服唯一 「互通」选项且默认选中;后续其他品类在共享常量按品类加条目 - 匹配引擎通配符对称化:区服通配=互通(通用/全区/不限归一化为别名), 系统通配=通用(双端/全端同理);订单侧写通配同样放行,只增不减匹配面; 差异描述不把通配对当冲突 - 提报白名单校验(API 层):系统只收 苹果/安卓,区服只收 微信/QQ/互通, 旧页面缓存或直调接口绕不过 - 物品名拼接:区服为互通时不拼入(对买家无信息量),前后端同口径 后端 481(+互通双向匹配/通配不算差异/真冲突仍排除/白名单校验)、 前端 54(+选项口径/表单引用/默认值)测试全绿;typecheck/lint/format/check:sql 通过 docs/寄卖系统优化方案.md 记录批次 W --- .../consignment-matching-service.test.ts | 60 ++++++++++++++++++ .../consignment-matching-service.ts | 37 +++++++++-- .../consignment-worker-service.test.ts | 16 +++++ .../consignment-worker-service.ts | 18 +++++- .../panels/ConsignmentInventoryPanel.tsx | 7 ++- .../panels/ConsignmentSubmissionsPanel.tsx | 6 +- .../pages/worker/WorkerConsignmentPage.tsx | 22 ++++--- apps/frontend/src/utils/consignment-spec.ts | 36 +++++++++++ .../tests/consignment-spec-options.test.ts | 61 +++++++++++++++++++ docs/寄卖系统优化方案.md | 23 +++++++ 10 files changed, 265 insertions(+), 21 deletions(-) create mode 100644 apps/frontend/src/utils/consignment-spec.ts create mode 100644 apps/frontend/tests/consignment-spec-options.test.ts 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/互通) */} + +