From 577e0eec029310a671ee7ecf45922a7adcd957d7 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Mon, 21 Sep 2026 12:04:30 +0800 Subject: [PATCH] =?UTF-8?q?fix(consignment):=20=E5=86=85=E9=83=A8=E7=8E=B0?= =?UTF-8?q?=E8=B4=A7=E5=B1=A5=E7=BA=A6=E6=96=87=E6=A1=88=E5=AF=B9=E9=BD=90?= =?UTF-8?q?=E5=BE=85=E5=8A=A0=E5=A5=BD=E5=8F=8B=EF=BC=8C=E5=B9=B6=E7=A7=BB?= =?UTF-8?q?=E9=99=A4=E5=B7=B2=E5=BA=9F=E5=BC=83=E7=9A=84=E6=8C=87=E6=B4=BE?= =?UTF-8?q?=E6=89=93=E6=89=8B=E8=B7=AF=E5=BE=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- .../pages/admin/panels/WorkOrdersPanel.tsx | 73 +++++-------------- .../pages/worker/WorkerOrderDetailDrawer.tsx | 21 +++--- .../frontend/src/utils/consignment-display.ts | 21 ++++++ .../consignment-manual-match-mount.test.ts | 15 ++++ .../tests/consignment-worker-display.test.ts | 20 ++++- docs/寄卖库存优化现状与进度追踪.md | 2 +- 6 files changed, 83 insertions(+), 69 deletions(-) diff --git a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx index a6d56b6d..9a96f2e3 100644 --- a/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx +++ b/apps/frontend/src/pages/admin/panels/WorkOrdersPanel.tsx @@ -76,7 +76,6 @@ import { updateAdminWorkOrderSharing, } from '@/services/admin' import { - assignWorkOrderWithConsignmentWorker, checkWorkOrderConsignmentMatches, fulfillWorkOrderWithConsignment, revokeWorkOrderConsignmentFulfillment, @@ -85,6 +84,7 @@ import { notifyVoucherConsume } from '@/utils/voucher-consume' import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment' import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform' import { isManuallyMatchableOrderStatus } from '@/utils/consignment-manual-match' +import { resolveConsignmentScreenshotUrls } from '@/utils/consignment-display' import ConsignmentManualMatchModal from '@/components/admin/ConsignmentManualMatchModal' import { AdminRangePicker } from '@/components/admin/AdminDatePicker' import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs' @@ -798,26 +798,21 @@ export default function WorkOrdersPanel() { /** 非精确匹配(订单规格信息不全 / 高配充低配)必须人工确认,后端也会二次拦截 */ /** - * 匹配弹窗里的两个动作**都会立刻占用现货**,点错就得走「撤销寄售履约」才能退回来, - * 而且两个按钮挨在一起很容易点错,所以一律二次确认,并把后果写清楚: - * 用哪个号占用、给谁、要不要冻结押金、能不能回退。 + * 内部现货履约会立刻占用库存,点错就得走「撤销寄售履约」才能退回来, + * 所以二次确认写清楚:用哪个号占用、结算价、工单会停在待加好友。 */ function confirmConsignmentMatchAction(input: { - kind: 'assign_worker' | 'internal_stock' matchType?: string consignmentNo: string - workerName?: string - depositFen?: number | null costPriceFen?: number | null onOk: () => void }) { const isPartial = input.matchType !== 'exact' - const isAssignWorker = input.kind === 'assign_worker' - const depositText = - input.depositFen != null ? `履约押金 ¥${(input.depositFen / 100).toFixed(2)}` : '履约押金' + const costText = + input.costPriceFen != null ? `结算价 ¥${(input.costPriceFen / 100).toFixed(2)}` : '' modal.confirm({ - title: isAssignWorker ? '确认走接单平台指派打手?' : '确认用内部现货履约?', + title: '确认用内部现货履约?', width: 460, content: (
@@ -828,14 +823,11 @@ export default function WorkOrdersPanel() {
- {isAssignWorker - ? `动作:把订单指派给供货打手${input.workerName ? `「${input.workerName}」` : ''}接单,需冻结${depositText}` - : '动作:不向外部打手派单,直接用这件现货履约'} + 动作:不向外部打手派单,直接用这件现货履约 + {costText ? `(${costText})` : ''}
- {isAssignWorker - ? '结果:立刻占用该现货,订单转入打手履约,其它订单不能再匹配它。' - : '结果:立刻占用该现货,跳过「待打手提交资料」直接进入赠送冷却期,其它订单不能再匹配它。'} + 结果:立刻占用该现货,跳过「待打手提交资料」,工单进入「待加好友」;加好后再点确认加好友才会进入赠送冷却期。其它订单不能再匹配它。
退回需要走「撤销寄售履约」,请先核对商品与买家订单一致。 @@ -848,7 +840,7 @@ export default function WorkOrdersPanel() { ) : null}
), - okText: isAssignWorker ? '确认指派打手' : '确认占用并履约', + okText: '确认占用并履约', okButtonProps: isPartial ? { danger: true } : undefined, cancelText: '再想想', onOk: input.onOk, @@ -864,7 +856,7 @@ export default function WorkOrdersPanel() { inventoryId, allowPartial, }) - message.success('已占用内部库存,跳过打手资料,进入赠送冷静期') + message.success('已占用内部库存,跳过打手资料,已进入待加好友阶段') notifyVoucherConsume(res?.data?.voucherConsume) setConsignmentInterceptModal({ open: false, order: null, matchData: null, submitting: false }) await refreshAll() @@ -874,30 +866,6 @@ export default function WorkOrdersPanel() { } } - async function handleAssignWithConsignmentWorker( - inventoryId: number, - workerId?: number | null, - allowPartial = false, - ) { - if (!consignmentInterceptModal.order) return - setConsignmentInterceptModal((prev) => ({ ...prev, submitting: true })) - try { - const res = await assignWorkOrderWithConsignmentWorker({ - workOrderId: consignmentInterceptModal.order.workOrderId, - inventoryId, - workerId, - allowPartial, - }) - message.success('已通过接单平台指派打手!系统已自动冻结履约押金,等待打手交货核销') - notifyVoucherConsume(res?.data?.voucherConsume) - setConsignmentInterceptModal({ open: false, order: null, matchData: null, submitting: false }) - await refreshAll() - } catch (err: any) { - message.error(err.message || '走接单平台指派打手失败') - setConsignmentInterceptModal((prev) => ({ ...prev, submitting: false })) - } - } - function openRevokeConsignmentModal(row: WorkOrder) { setRevokeConsignmentModal({ open: true, @@ -2207,6 +2175,9 @@ export default function WorkOrdersPanel() { const effectiveNote = String( detailOrder.gift?.boosterMaterial?.note || consignmentInfo?.remark || '', ).trim() + const consignmentScreenshotUrls = resolveConsignmentScreenshotUrls( + consignmentInfo?.screenshots, + ) return ( - ) : Array.isArray(consignmentInfo?.screenshots) && - consignmentInfo.screenshots.length > 0 ? ( - typeof item === 'string' && item.trim(), - )} - size={72} - /> + ) : consignmentScreenshotUrls.length > 0 ? ( + ) : ( 未提交 )} @@ -3329,8 +3294,7 @@ export default function WorkOrdersPanel() {
若使用内部寄售库存履约,无需向外部打手支付佣金(打手支出 ¥0.00) - 。系统会立刻占用库存,并跳过「待打手提交资料」,直接进入该商品规则设定的赠送冷静期(未单独配置时为 - 72 小时);冷静期结束后再用库存账号赠送并验收。 + 。系统会立刻占用库存,并跳过「待打手提交资料」,工单进入「待加好友」。加好买家好友后再点确认加好友,才会进入赠送冷却期。
} @@ -3446,7 +3410,6 @@ export default function WorkOrdersPanel() { loading={consignmentInterceptModal.submitting} onClick={() => confirmConsignmentMatchAction({ - kind: 'internal_stock', matchType: m.matchType, consignmentNo: inv.sourceSubmissionNo || '客服手工入库', costPriceFen: inv.costPriceFen, @@ -3466,7 +3429,7 @@ export default function WorkOrdersPanel() { textAlign: 'center', }} > - 免外派佣金 · 不扣打手押金 · 自动进冷静期 + 免外派佣金 · 不扣打手押金 · 进入待加好友 diff --git a/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx b/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx index e60f796a..6cd53ade 100644 --- a/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrderDetailDrawer.tsx @@ -5,6 +5,7 @@ import WorkOrderEventTimeline from '@/components/WorkOrderEventTimeline' import ImagePreviewList from '@/components/files/ImagePreviewList' import type { WorkOrder } from '@/types/worker-platform' import { formatDateTime } from '@/utils/date-time' +import { resolveOrderConsignmentScreenshotUrls } from '@/utils/consignment-display' import { canEditAcceptanceImages, @@ -183,6 +184,9 @@ function GiftProgressCard({ order }: { order: WorkOrder }) { const boosterItems = Object.entries(boosterFields).filter(([, value]) => String(value || '').trim(), ) + const consignmentScreenshotUrls = resolveOrderConsignmentScreenshotUrls(order) + const hasBoosterScreenshots = (gift.boosterMaterial?.screenshots || []).length > 0 + const hasConsignmentScreenshots = consignmentScreenshotUrls.length > 0 return ( @@ -228,7 +232,7 @@ function GiftProgressCard({ order }: { order: WorkOrder }) { )} - {boosterItems.length > 0 || (gift.boosterMaterial?.screenshots || []).length > 0 ? ( + {boosterItems.length > 0 || hasBoosterScreenshots || hasConsignmentScreenshots ? (
已提交的本人资料
@@ -241,20 +245,13 @@ function GiftProgressCard({ order }: { order: WorkOrder }) { ))} ) : null} - {(gift.boosterMaterial?.screenshots || []).length > 0 ? ( + {hasBoosterScreenshots ? (
- +
- ) : Array.isArray( - (order.material as Record | undefined)?.consignment?.screenshots, - ) && - ((order.material as Record).consignment.screenshots as unknown[]) - .length > 0 ? ( + ) : hasConsignmentScreenshots ? (
- ).consignment.screenshots} - size={64} - /> +
) : null}
diff --git a/apps/frontend/src/utils/consignment-display.ts b/apps/frontend/src/utils/consignment-display.ts index 00b6de84..241de48f 100644 --- a/apps/frontend/src/utils/consignment-display.ts +++ b/apps/frontend/src/utils/consignment-display.ts @@ -6,6 +6,27 @@ function asRecord(value: unknown): Record { : {} } +/** 寄售截图可能是字符串 URL,也可能是 UploadedFile 对象。 */ +export function resolveConsignmentScreenshotUrls(screenshots: unknown): string[] { + if (!Array.isArray(screenshots)) return [] + return screenshots + .map((item) => { + if (typeof item === 'string') return item.trim() + if (item && typeof item === 'object' && !Array.isArray(item)) { + const rec = item as Record + return String(rec.url || rec.mediumUrl || rec.thumbnailUrl || '').trim() + } + return '' + }) + .filter(Boolean) +} + +export function resolveOrderConsignmentScreenshotUrls(order: { material?: unknown }): string[] { + return resolveConsignmentScreenshotUrls( + asRecord(asRecord(order.material).consignment).screenshots, + ) +} + export type ConsignmentWorkerDisplayInfo = { tagText: '供货打手' | '内部现货' primaryName: string diff --git a/apps/frontend/tests/consignment-manual-match-mount.test.ts b/apps/frontend/tests/consignment-manual-match-mount.test.ts index 4059b651..c6a63ca3 100644 --- a/apps/frontend/tests/consignment-manual-match-mount.test.ts +++ b/apps/frontend/tests/consignment-manual-match-mount.test.ts @@ -100,3 +100,18 @@ test('手动匹配弹窗:数据过期的 409 要清选择并刷新,走内部 assert.ok(source.includes("from '@/utils/voucher-consume'"), '核销反馈必须用共享出口') assert.ok(source.includes('keyword: keyword.trim() || undefined'), '库存侧关键字要透传后端') }) + +test('发布拦截弹窗:只走内部现货履约,文案停在待加好友而不是冷静期', () => { + const source = readFileSync( + new URL('../src/pages/admin/panels/WorkOrdersPanel.tsx', import.meta.url).pathname, + 'utf8', + ) + assert.ok(source.includes('fulfillWorkOrderWithConsignment'), '拦截弹窗必须调用内部现货履约') + assert.ok( + !source.includes('assignWorkOrderWithConsignmentWorker'), + '拦截弹窗不得再调用旧的指派打手接口', + ) + assert.ok(source.includes('进入待加好友'), '拦截弹窗必须写明工单进入待加好友') + assert.ok(!source.includes('自动进冷静期'), '拦截弹窗不得再写自动进冷静期') + assert.ok(!source.includes('进入赠送冷静期'), '成功提示不得再写成进入赠送冷静期') +}) diff --git a/apps/frontend/tests/consignment-worker-display.test.ts b/apps/frontend/tests/consignment-worker-display.test.ts index 5b9fb66e..8dbebc06 100644 --- a/apps/frontend/tests/consignment-worker-display.test.ts +++ b/apps/frontend/tests/consignment-worker-display.test.ts @@ -1,7 +1,10 @@ import assert from 'node:assert/strict' import test from 'node:test' -import { resolveConsignmentWorkerDisplay } from '../src/utils/consignment-display.ts' +import { + resolveConsignmentWorkerDisplay, + resolveConsignmentScreenshotUrls, +} from '../src/utils/consignment-display.ts' test('普通工单无寄售信息时 resolveConsignmentWorkerDisplay 返回 null', () => { const order = { @@ -53,3 +56,18 @@ test('寄售工单无供货打手名时回退展示角色昵称与现货标签', assert.equal(result.primaryName, '客服角色') assert.equal(result.secondaryInfo, 'ID:1234551') }) + +test('寄售截图兼容字符串 URL 与 UploadedFile 对象', () => { + assert.deepEqual( + resolveConsignmentScreenshotUrls([ + '/api/v1/files/object?key=consignment_screenshot%2Fa.webp', + { url: '/api/v1/files/object?key=consignment_screenshot%2Fb.webp' }, + { objectKey: 'x', url: '' }, + 12, + ]), + [ + '/api/v1/files/object?key=consignment_screenshot%2Fa.webp', + '/api/v1/files/object?key=consignment_screenshot%2Fb.webp', + ], + ) +}) diff --git a/docs/寄卖库存优化现状与进度追踪.md b/docs/寄卖库存优化现状与进度追踪.md index 74ec43f8..37cc444c 100644 --- a/docs/寄卖库存优化现状与进度追踪.md +++ b/docs/寄卖库存优化现状与进度追踪.md @@ -142,7 +142,7 @@ - 彻底解除 `selected.workerId` 强依赖与无法指派错误提示,详情卡片调整为展示供货来源(“供货打手”或“内部现货”); - 文案统一调整为“确认内部现货履约”,明确告知“免外派佣金、不冻结重复押金、自动跳过打手资料”。 2. **工单发布拦截弹窗候选操作收敛为【内部现货直接履约】**: - - 在 `WorkOrdersPanel.tsx` 中将卡片操作按钮替换为【内部现货直接履约】高亮主按钮(绿色),副标题标注“免外派佣金 · 不扣打手押金 · 自动进冷静期”; + - 在 `WorkOrdersPanel.tsx` 中将卡片操作按钮替换为【内部现货直接履约】高亮主按钮(绿色),副标题标注“免外派佣金 · 不扣打手押金 · 进入待加好友”; - 杜绝客服误点走旧指派流程。 3. **测试套件与类型风格全面达标**: - `consignment-manual-match-mount.test.ts` 更新为断言手动匹配走内部现货直接履约;