From e731348bb0ef443632776c38baa2138589055ff2 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Thu, 17 Sep 2026 11:45:50 +0800 Subject: [PATCH] =?UTF-8?q?refactor(consignment):=20=E5=88=A0=E6=8E=89?= =?UTF-8?q?=E6=8C=89=E5=BA=93=E5=AD=98=E5=8F=B7=E6=93=8D=E4=BD=9C=E5=AF=84?= =?UTF-8?q?=E5=8D=96=E7=9A=84=E5=8D=96=E5=AE=B6=E5=85=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 手动入库停用后,"没有提报单的现货"已不可能再产生,批次 I 为兜这类货加的两个 按库存号入口(/worker/consignment/inventories/:inventoryId/...)成了纯负担。 - 删除两个按库存号的路由/服务函数、前端两个请求函数与页面里的锚点分派逻辑, 卖家动作只按提报单号(寄卖单号)定位 - 卖家列表里"没有提报单"的兜底行不再暴露内部仓库号(标题改「客服手工入库」), 且一律不给操作能力位,不给用户一个点下去必然失败的按钮 - 加静态守卫测试钉住以上两点,防止以后又长出按库存号的入口 真实库验证:卖家列表三件货都带寄卖单号与 submissionId,没有提报单的现货 0 件。 --- apps/backend/src/routes/worker/consignment.ts | 43 ------------- .../consignment-worker-service.test.ts | 18 +++++- .../consignment-worker-service.ts | 63 +++++-------------- .../pages/worker/WorkerConsignmentPage.tsx | 33 ++++------ apps/frontend/src/services/worker.ts | 21 ------- docs/寄卖系统优化方案.md | 15 +++++ 6 files changed, 61 insertions(+), 132 deletions(-) diff --git a/apps/backend/src/routes/worker/consignment.ts b/apps/backend/src/routes/worker/consignment.ts index be1c2b18..5e29170b 100644 --- a/apps/backend/src/routes/worker/consignment.ts +++ b/apps/backend/src/routes/worker/consignment.ts @@ -2,11 +2,9 @@ import { Router } from 'express' import { confirmWorkerConsignmentFriendAdded, - confirmWorkerConsignmentInventoryFriendAdded, listWorkerConsignments, publishWorkerConsignment, submitWorkerConsignmentGiftEvidence, - submitWorkerConsignmentInventoryGiftEvidence, } from '../../services/worker-platform/consignment-worker-service.js' import { sendRouteError } from '../../utils/http.js' import { getRequiredWorkerSession, requireActiveWorker } from './session.js' @@ -86,45 +84,4 @@ router.post( }, ) -/** - * 客服手工入库的现货没有提报单,按库存号走同一套卖家动作。 - * 单独开路由而不是"一个 id 两义",避免提报单 id 与库存 id 撞号时误操作别人的货。 - */ -router.post( - '/consignment/inventories/:inventoryId/confirm-friend', - requireActiveWorker, - async (req, res) => { - try { - const session = getRequiredWorkerSession(req) - const result = await confirmWorkerConsignmentInventoryFriendAdded( - session.workerId, - String(req.params.inventoryId || ''), - session.username || '', - ) - res.json({ ok: true, data: result }) - } catch (error) { - sendRouteError(res, error, '确认加好友失败', '[worker/consignment/inventory-confirm-friend]') - } - }, -) - -router.post( - '/consignment/inventories/:inventoryId/gift-evidence', - requireActiveWorker, - async (req, res) => { - try { - const session = getRequiredWorkerSession(req) - const result = await submitWorkerConsignmentInventoryGiftEvidence( - session.workerId, - String(req.params.inventoryId || ''), - req.body || {}, - session, - ) - res.json({ ok: true, data: result }) - } catch (error) { - sendRouteError(res, error, '提交赠送截图失败', '[worker/consignment/inventory-gift-evidence]') - } - }, -) - export default router 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 76dc1eb7..9e89cb63 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 @@ -10,7 +10,8 @@ test('我的寄卖以提报单号为唯一锚点聚合,不再返回 submission const serviceSource = await readServiceSource() // 以前接口返回两个数组、前端分两个列表渲染 → 同一件货两行两个号 assert.ok(serviceSource.includes('consignmentNo: sub.submission_no')) - assert.ok(serviceSource.includes('consignmentNo: inv.inventory_no')) + // 没有提报单的兜底行也不再暴露内部仓库号(手动入库已停用,理论上不该再有这种行) + assert.ok(!serviceSource.includes('consignmentNo: inv.inventory_no')) assert.ok(!serviceSource.includes('submissions: submissionsRes.items.map')) assert.ok(!serviceSource.includes('inventories: inventoriesRes.items.map')) }) @@ -35,3 +36,18 @@ test('赠送截图复用打手验收提交链路(冷却校验、必须带图 assert.ok(serviceSource.includes('submitWorkerOrderAcceptance(')) assert.ok(serviceSource.includes("errorCode: 'consignment_item_not_found'")) }) + +test('卖家动作只按提报单号定位:手动入库停用后,按库存号的入口必须不存在', async () => { + const serviceSource = await readServiceSource() + // 手动入库已停用 → 每件货必然由提报单产出 → 不该再保留 inventoryId 入口(含"一个 id 两义"的旧设计) + assert.ok(!serviceSource.includes('confirmWorkerConsignmentInventoryFriendAdded')) + assert.ok(!serviceSource.includes('submitWorkerConsignmentInventoryGiftEvidence')) + assert.ok(!serviceSource.includes('inventoryId: number | string')) + assert.ok(serviceSource.includes('resolveSellerConsignmentWorkOrder(workerId, submissionId)')) +}) + +test('没有提报单的兜底行不给卖家操作能力,也不暴露内部仓库号', async () => { + const serviceSource = await readServiceSource() + assert.ok(serviceSource.includes("consignmentNo: '客服手工入库'")) + assert.ok(serviceSource.includes('canConfirmFriend: false, canSubmitEvidence: false')) +}) 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 5a641480..e28fee50 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts @@ -2,13 +2,11 @@ import { withTransaction } from '../../db/client.js' import { freezeConsignmentPublishDepositWithClient } from '../../repositories/worker-platform/consignment-deposit-repo.js' import { createConsignmentSubmissionWithClient, - getConsignmentInventoryById, getConsignmentInventoryBySubmissionId, getConsignmentSubmissionById, listConsignmentInventories, listConsignmentSubmissions, } from '../../repositories/worker-platform/consignment-repo.js' -import type { ConsignmentInventoryRow } from '../../repositories/worker-platform/consignment-repo.js' import { getWorkerUserById } from '../../repositories/worker-platform/index.js' import { listWorkOrdersByIds } from '../../repositories/worker-platform/work-order-query-repo.js' import { deriveConsignmentItemStage } from '../../domain/consignment-item-stage.js' @@ -282,9 +280,13 @@ export async function listWorkerConsignments(workerId: number) { } }), ...orphanInventories.map((inv) => { + // 手动入库已停用:没有提报单的货不该再出现,这里只做兜底展示(不显示内部仓库号、不给操作能力) const stage = resolveStage('approved', inv) return { - consignmentNo: inv.inventory_no, + consignmentNo: '客服手工入库', + ...(stage.canConfirmFriend || stage.canSubmitEvidence + ? { canConfirmFriend: false, canSubmitEvidence: false } + : {}), submissionId: null, inventoryId: inv.id, workOrderId: inv.bound_work_order_id ?? null, @@ -328,30 +330,18 @@ export async function listWorkerConsignments(workerId: number) { */ async function resolveSellerConsignmentWorkOrder( workerId: number, - anchor: { submissionId: number | string } | { inventoryId: number | string }, + submissionId: number | string, ): Promise<{ workOrderId: number; inventoryId: number }> { - let inventory: ConsignmentInventoryRow | null = null - - if ('submissionId' in anchor) { - const submission = await getConsignmentSubmissionById(anchor.submissionId) - if (!submission || Number(submission.worker_id) !== Number(workerId)) { - throw createHttpError('寄卖单不存在或不属于当前账号', { - statusCode: 404, - errorCode: 'consignment_item_not_found', - }) - } - inventory = await getConsignmentInventoryBySubmissionId(submission.id) - } else { - // 客服手工入库的现货没有提报单,只能按库存号定位(提报单线不走这里,避免 id 撞号误操作) - inventory = await getConsignmentInventoryById(anchor.inventoryId) - if (!inventory || Number(inventory.worker_id || 0) !== Number(workerId)) { - throw createHttpError('寄卖单不存在或不属于当前账号', { - statusCode: 404, - errorCode: 'consignment_item_not_found', - }) - } + const submission = await getConsignmentSubmissionById(submissionId) + if (!submission || Number(submission.worker_id) !== Number(workerId)) { + throw createHttpError('寄卖单不存在或不属于当前账号', { + statusCode: 404, + errorCode: 'consignment_item_not_found', + }) } + // 手动入库已停用,每件货必然由提报单产出,所以只按提报单号定位(不再有 inventoryId 入口) + const inventory = await getConsignmentInventoryBySubmissionId(submission.id) if (!inventory) { throw createHttpError('该寄卖单还没有生成库存,暂无可执行的操作', { statusCode: 409, @@ -395,17 +385,7 @@ export async function confirmWorkerConsignmentFriendAdded( submissionId: number | string, workerName = '', ) { - const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { submissionId }) - return confirmAdminWorkOrderFriendAdded(workOrderId, workerName || `worker:${workerId}`) -} - -/** 客服手工入库现货(没有提报单)的同一动作:按库存号定位 */ -export async function confirmWorkerConsignmentInventoryFriendAdded( - workerId: number, - inventoryId: number | string, - workerName = '', -) { - const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { inventoryId }) + const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, submissionId) return confirmAdminWorkOrderFriendAdded(workOrderId, workerName || `worker:${workerId}`) } @@ -421,17 +401,6 @@ export async function submitWorkerConsignmentGiftEvidence( payload: JsonObject, session: WorkerSession, ) { - const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { submissionId }) - return submitWorkerOrderAcceptance(workOrderId, payload, session) -} - -/** 客服手工入库现货(没有提报单)的同一动作:按库存号定位 */ -export async function submitWorkerConsignmentInventoryGiftEvidence( - workerId: number, - inventoryId: number | string, - payload: JsonObject, - session: WorkerSession, -) { - const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, { inventoryId }) + const { workOrderId } = await resolveSellerConsignmentWorkOrder(workerId, submissionId) return submitWorkerOrderAcceptance(workOrderId, payload, session) } diff --git a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx index 7592ab96..215fc81e 100644 --- a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx @@ -35,12 +35,10 @@ import { useEffect, useMemo, useState } from 'react' import ImageUpload from '@/components/files/ImageUpload' import { confirmWorkerConsignmentFriendAdded, - confirmWorkerConsignmentInventoryFriendAdded, fetchWorkerConsignments, fetchWorkerProfile, publishWorkerConsignment, submitWorkerConsignmentGiftEvidence, - submitWorkerConsignmentInventoryGiftEvidence, } from '@/services/worker' import type { WorkerConsignmentRecord } from '@/services/worker' import type { UploadedFile } from '@/types/worker-platform' @@ -252,20 +250,19 @@ export default function WorkerConsignmentPage() { } /** - * 定位要操作的寄卖单:有提报单号(寄卖单号)就用它,客服手工入库的现货回退用库存号。 - * 两条路由在后端是分开的,这里用 submissionId/inventoryId 是否为 null 决定, - * 前端不会把两个号混用。 + * 定位要操作的寄卖单:只认提报单号(寄卖单号)。 + * 客服手动入库已停用,每件货都必然由提报单产出,所以不存在"只能用库存号操作"的货; + * 万一遇到没有提报单的历史行,直接拦下并提示联系客服,不做降级操作。 */ - function resolveActionAnchor(item: WorkerConsignmentRecord) { - if (item.submissionId != null) return { kind: 'submission' as const, id: item.submissionId } - if (item.inventoryId != null) return { kind: 'inventory' as const, id: item.inventoryId } - message.warning('该寄卖单缺少可操作的编号,请联系客服处理') + function resolveActionSubmissionId(item: WorkerConsignmentRecord) { + if (item.submissionId != null) return item.submissionId + message.warning('该寄卖单没有提报单号,无法在这里操作,请联系客服处理') return null } function handleConfirmFriend(item: WorkerConsignmentRecord) { - const anchor = resolveActionAnchor(item) - if (!anchor) return + const submissionId = resolveActionSubmissionId(item) + if (submissionId == null) return modal.confirm({ title: '确认已在游戏内加好买家好友?', @@ -287,9 +284,7 @@ export default function WorkerConsignmentPage() { centered: true, onOk: async () => { try { - await (anchor.kind === 'submission' - ? confirmWorkerConsignmentFriendAdded(anchor.id) - : confirmWorkerConsignmentInventoryFriendAdded(anchor.id)) + await confirmWorkerConsignmentFriendAdded(submissionId) message.success('已确认加好友,赠送冷却开始') await myConsignmentsQuery.refetch() } catch (err: any) { @@ -300,7 +295,7 @@ export default function WorkerConsignmentPage() { } function openEvidenceModal(item: WorkerConsignmentRecord) { - if (!resolveActionAnchor(item)) return + if (resolveActionSubmissionId(item) == null) return setEvidenceItem(item) setEvidenceFiles([]) setEvidenceNote('') @@ -313,8 +308,8 @@ export default function WorkerConsignmentPage() { } async function handleEvidenceSubmit() { - const anchor = evidenceItem ? resolveActionAnchor(evidenceItem) : null - if (!anchor) return + const submissionId = evidenceItem ? resolveActionSubmissionId(evidenceItem) : null + if (submissionId == null) return const urls = evidenceFiles.map((file) => file.url).filter(Boolean) if (urls.length === 0) { message.warning('请先上传赠送完成截图,客服需要凭截图验收') @@ -325,9 +320,7 @@ export default function WorkerConsignmentPage() { try { const note = evidenceNote.trim() const payload = note ? { imageUrls: urls, note } : { imageUrls: urls } - await (anchor.kind === 'submission' - ? submitWorkerConsignmentGiftEvidence(anchor.id, payload) - : submitWorkerConsignmentInventoryGiftEvidence(anchor.id, payload)) + await submitWorkerConsignmentGiftEvidence(submissionId, payload) message.success('赠送截图已提交,等待客服验收') closeEvidenceModal() await myConsignmentsQuery.refetch() diff --git a/apps/frontend/src/services/worker.ts b/apps/frontend/src/services/worker.ts index c67dca6c..2cac3185 100644 --- a/apps/frontend/src/services/worker.ts +++ b/apps/frontend/src/services/worker.ts @@ -483,24 +483,3 @@ export function submitWorkerConsignmentGiftEvidence( payload, ) } - -/** - * 客服手工入库的现货没有提报单号,按库存号走同一套动作 - * (后端单独开路由,不用"一个 id 两义",避免提报单号与库存号撞号时误操作)。 - */ -export function confirmWorkerConsignmentInventoryFriendAdded(inventoryId: number | string) { - return apiPost<{ order: WorkOrder }>( - `/api/v1/worker/consignment/inventories/${inventoryId}/confirm-friend`, - {}, - ) -} - -export function submitWorkerConsignmentInventoryGiftEvidence( - inventoryId: number | string, - payload: { imageUrls: string[]; note?: string }, -) { - return apiPost<{ order: WorkOrder }>( - `/api/v1/worker/consignment/inventories/${inventoryId}/gift-evidence`, - payload, - ) -} diff --git a/docs/寄卖系统优化方案.md b/docs/寄卖系统优化方案.md index 922ee543..65c0ca7d 100644 --- a/docs/寄卖系统优化方案.md +++ b/docs/寄卖系统优化方案.md @@ -21,6 +21,7 @@ | `632dddc4` | 上一轮:H5 渠道整体下线 + 凭据字段修复 + 状态机封闭(撤销守卫 / 库存绑定唯一性 / 生命周期解绑)+ 押金归属取台账 | 迁移 `079`、`consignment-*` 五个文件、`work-order-*` 三个仓库、两个前端面板 | | `5d11bf20` | 批次 C+D:指派部分成功补偿、凭证核销如实反馈、空规格不放行 + 履约/指派服务端规格复检 | `consignment-fulfillment-service.ts`、`consignment-matching-service.ts`、`consignment-repo.ts`、履约路由、`WorkOrdersPanel.tsx` | | `d6f6219f` | 批次 F1:内部现货履约停在「待加好友」,客服确认后才进赠送冷却 + 备注快捷项 | `work-order-gift-repo.ts`、履约服务、`WorkOrdersPanel.tsx`、`utils/gift-phase.ts` | +| 本批提交(批次 N,同一提交内含本文档,故不写自身 hash) | 批次 N:删掉按库存号的卖家入口,卖家动作只按寄卖单号定位 | `consignment-worker-service.ts`、`routes/worker/consignment.ts`、`WorkerConsignmentPage.tsx` | | 本批提交(批次 M,同一提交内含本文档,故不写自身 hash) | 批次 M:停用客服手动入库(前端入口删除 + 后端 409 守卫),寄卖货源只走平台提报 | `consignment-admin-service.ts`、`consignment-repo.ts`、`ConsignmentInventoryPanel.tsx` | | 本批提交(批次 L,同一提交内含本文档,故不写自身 hash) | 批次 L:匹配弹窗两个动作一律二次确认 + 按钮分区/图标/后果说明,防点错 | `WorkOrdersPanel.tsx` | | 本批提交(批次 K,同一提交内含本文档,故不写自身 hash) | 批次 K:匹配弹窗改用寄卖单号(候选查询补 join),客服界面不再展示内部仓库号 | `consignment-repo.ts`、`WorkOrdersPanel.tsx`、`ConsignmentInventoryPanel.tsx` | @@ -283,6 +284,20 @@ 前端在"部分匹配"候选上先 `modal.confirm` 危险确认,再带 `allowPartial: true` 重试,高配充低配的合法玩法保留但不可误触。 3. ⏳ 匹配下推 SQL + 批量匹配接口 + 工单列表角标(P2-8)未做:当前仍是"取全量在库 → 内存比对"。 +### 批次 N · 删掉"按库存号操作"的卖家入口(本轮) + +批次 I 为了兜住"客服手工入库的现货没有提报单"这种情况,给卖家动作额外开了两个按库存号的入口 +(`/worker/consignment/inventories/:inventoryId/...`)。手动入库停用后这类货已不可能再产生, +这两个入口就是纯负担(还要维护"一个 id 两义/分路由"的复杂度)。 + +**改法**(测试阶段,无历史数据包袱,直接删): + +1. ✅ 删掉两个按库存号的路由、两个服务函数、前端两个请求函数与页面里的锚点分派逻辑; + 卖家动作**只按提报单号(寄卖单号)定位**。 +2. ✅ 卖家列表里"没有提报单"的兜底行不再暴露内部仓库号(标题改「客服手工入库」), + 且一律不给操作能力位——不给用户一个点下去必然失败的按钮。 +3. ✅ 加静态守卫测试钉住这两点,防止以后又长出按库存号的入口。 + ### 批次 M · 停用客服手动入库(本轮) **背景**:手动入库是唯一会造出「没有提报单的现货」的入口,而这类货**没有寄卖单号**,