fix(consignment): 内部现货履约文案对齐待加好友,并移除已废弃的指派打手路径

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
yml2213
2026-09-21 12:04:30 +08:00
co-authored by Cursor
parent 29589bb9af
commit 577e0eec02
6 changed files with 83 additions and 69 deletions
@@ -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: (
<div style={{ fontSize: 13, lineHeight: 1.8 }}>
@@ -828,14 +823,11 @@ export default function WorkOrdersPanel() {
</Typography.Text>
</div>
<div>
{isAssignWorker
? `动作:把订单指派给供货打手${input.workerName ? `「${input.workerName}」` : ''}接单,需冻结${depositText}`
: '动作:不向外部打手派单,直接用这件现货履约'}
动作:不向外部打手派单,直接用这件现货履约
{costText ? `(${costText})` : ''}
</div>
<div>
{isAssignWorker
? '结果:立刻占用该现货,订单转入打手履约,其它订单不能再匹配它。'
: '结果:立刻占用该现货,跳过「待打手提交资料」直接进入赠送冷却期,其它订单不能再匹配它。'}
结果:立刻占用该现货,跳过「待打手提交资料」,工单进入「待加好友」;加好后再点确认加好友才会进入赠送冷却期。其它订单不能再匹配它。
</div>
<div style={{ color: '#8c8c8c' }}>
退回需要走「撤销寄售履约」,请先核对商品与买家订单一致。
@@ -848,7 +840,7 @@ export default function WorkOrdersPanel() {
) : null}
</div>
),
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 (
<Card
@@ -2414,14 +2385,8 @@ export default function WorkOrdersPanel() {
files={detailOrder.gift.boosterMaterial.screenshots}
size={72}
/>
) : Array.isArray(consignmentInfo?.screenshots) &&
consignmentInfo.screenshots.length > 0 ? (
<ImagePreviewList
imageUrls={(consignmentInfo.screenshots as string[]).filter(
(item) => typeof item === 'string' && item.trim(),
)}
size={72}
/>
) : consignmentScreenshotUrls.length > 0 ? (
<ImagePreviewList imageUrls={consignmentScreenshotUrls} size={72} />
) : (
<Typography.Text type="secondary">未提交</Typography.Text>
)}
@@ -3329,8 +3294,7 @@ export default function WorkOrdersPanel() {
</div>
<div style={{ color: '#389e0d', marginTop: 4 }}>
若使用内部寄售库存履约,<strong>无需向外部打手支付佣金(打手支出 ¥0.00)</strong>
。系统会立刻占用库存,并跳过「待打手提交资料」,直接进入该商品规则设定的赠送冷静期(未单独配置时为
72 小时);冷静期结束后再用库存账号赠送并验收。
。系统会立刻占用库存,并跳过「待打手提交资料」,工单进入「待加好友」。加好买家好友后再点确认加好友,才会进入赠送冷却期。
</div>
</div>
}
@@ -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',
}}
>
免外派佣金 · 不扣打手押金 · 自动进冷静期
免外派佣金 · 不扣打手押金 · 进入待加好友
</div>
</div>
</div>
@@ -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 (
<Card title="好友赠送进度" size="small">
@@ -228,7 +232,7 @@ function GiftProgressCard({ order }: { order: WorkOrder }) {
)}
</Descriptions.Item>
</Descriptions>
{boosterItems.length > 0 || (gift.boosterMaterial?.screenshots || []).length > 0 ? (
{boosterItems.length > 0 || hasBoosterScreenshots || hasConsignmentScreenshots ? (
<div>
<Typography.Text type="secondary">已提交的本人资料</Typography.Text>
<div style={{ marginTop: 8 }}>
@@ -241,20 +245,13 @@ function GiftProgressCard({ order }: { order: WorkOrder }) {
))}
</Descriptions>
) : null}
{(gift.boosterMaterial?.screenshots || []).length > 0 ? (
{hasBoosterScreenshots ? (
<div style={{ marginTop: 8 }}>
<ImagePreviewList files={gift.boosterMaterial.screenshots} size={64} />
<ImagePreviewList files={gift.boosterMaterial?.screenshots} size={64} />
</div>
) : Array.isArray(
(order.material as Record<string, any> | undefined)?.consignment?.screenshots,
) &&
((order.material as Record<string, any>).consignment.screenshots as unknown[])
.length > 0 ? (
) : hasConsignmentScreenshots ? (
<div style={{ marginTop: 8 }}>
<ImagePreviewList
imageUrls={(order.material as Record<string, any>).consignment.screenshots}
size={64}
/>
<ImagePreviewList imageUrls={consignmentScreenshotUrls} size={64} />
</div>
) : null}
</div>
@@ -6,6 +6,27 @@ function asRecord(value: unknown): Record<string, unknown> {
: {}
}
/** 寄售截图可能是字符串 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<string, unknown>
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
@@ -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('进入赠送冷静期'), '成功提示不得再写成进入赠送冷静期')
})
@@ -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',
],
)
})
@@ -142,7 +142,7 @@
- 彻底解除 `selected.workerId` 强依赖与无法指派错误提示,详情卡片调整为展示供货来源(“供货打手”或“内部现货”);
- 文案统一调整为“确认内部现货履约”,明确告知“免外派佣金、不冻结重复押金、自动跳过打手资料”。
2. **工单发布拦截弹窗候选操作收敛为【内部现货直接履约】**:
- 在 `WorkOrdersPanel.tsx` 中将卡片操作按钮替换为【内部现货直接履约】高亮主按钮(绿色),副标题标注“免外派佣金 · 不扣打手押金 · 自动进冷静期”;
- 在 `WorkOrdersPanel.tsx` 中将卡片操作按钮替换为【内部现货直接履约】高亮主按钮(绿色),副标题标注“免外派佣金 · 不扣打手押金 · 进入待加好友”;
- 杜绝客服误点走旧指派流程。
3. **测试套件与类型风格全面达标**:
- `consignment-manual-match-mount.test.ts` 更新为断言手动匹配走内部现货直接履约;