feat(consignment): 一个寄卖单号锚定全流程 + 统一状态机 + 卖家侧确认加好友/提交赠送截图
问题:一件货身上挂着三套互不相通的状态(提报单审核态、仓库库存态、工单赠送态),
my-items 同时返回两个数组、卖家端分两个列表渲染,同一件货显示两行两个号;
且卖家端只映射了前两套状态,"已出库"之后流程就断了,卖家不知道该干什么。
- 提报单号 SUB… 作为唯一「寄卖单号」从发布锚定到结算(零迁移);
listWorkerConsignments 改成一件货一条记录,库存/工单信息挂上去,
客服手工入库的现货(无提报单)才回退用库存号当锚点
- 新增 domain/consignment-item-stage.ts:把三套底层状态推导成卖家看得懂的一个阶段
(待审核/已通过待匹配/已匹配-待加好友/冷却中/待赠送/已赠送待审核/已完成/驳回/作废/取消)
+ 一句"下一步做什么" + 能力位(谁能确认加好友、谁该传截图),列表筛选与 KPI 也改由它驱动
- 卖家侧新增动作(以前只有 publish + my-items,卖家只能干看):
POST /worker/consignment/items/:submissionId/{confirm-friend,gift-evidence}
POST /worker/consignment/inventories/:inventoryId/{confirm-friend,gift-evidence}(手工入库现货)
复用既有工单链路 confirmWorkOrderFriendAdded / submitWorkerOrderAcceptance,
只加归属校验(不是本人履约的订单 403),顺带白拿"冷却没结束不许提交凭证""必须带图"
- 客服端用同一个号:库存列表/详情补 sourceSubmissionNo(LEFT JOIN 提报单),
履约/指派写进工单的寄售信息也带上;关键字搜索支持按寄卖单号搜(计数 SQL 同步补 join)
真实库端到端验证:确认加好友 → gift_phase=friend_countdown / friend_added_at 落库 /
gift_available_at 正好 +72h;冷却期内提交截图被拒(work_order_gift_cooldown_active);
越权操作被拒(consignment_item_not_found);按寄卖单号搜索命中正确。临时数据已清理。
This commit is contained in:
@@ -92,6 +92,14 @@ const BUYER_COLORS: Record<string, string> = {
|
||||
客服B: 'magenta',
|
||||
}
|
||||
|
||||
/**
|
||||
* 寄卖单号 = 来源提报单号(卖家提报时生成的提报单号就是唯一寄卖单号)。
|
||||
* 客服手工入库的现货没有来源提报,回退显示内部仓库号,保证客服对单口径一致。
|
||||
*/
|
||||
function resolveConsignmentNo(record: ConsignmentInventory) {
|
||||
return record.sourceSubmissionNo || record.inventoryNo
|
||||
}
|
||||
|
||||
export default function ConsignmentInventoryPanel() {
|
||||
const { message } = App.useApp()
|
||||
|
||||
@@ -297,7 +305,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
async function handleArchive(record: ConsignmentInventory) {
|
||||
Modal.confirm({
|
||||
title: '确认下架作废该物品?',
|
||||
content: `下架后该物品 [${record.itemName} - ${record.inventoryNo}] 将不再参与匹配。`,
|
||||
content: `下架后该物品 [${record.itemName} - 寄卖单号 ${resolveConsignmentNo(record)}] 将不再参与匹配。`,
|
||||
okText: '确认作废',
|
||||
okType: 'danger',
|
||||
cancelText: '取消',
|
||||
@@ -342,16 +350,28 @@ export default function ConsignmentInventoryPanel() {
|
||||
|
||||
const columns: TableColumnsType<ConsignmentInventory> = [
|
||||
{
|
||||
title: '物品/库存编号',
|
||||
title: '物品/寄卖单号',
|
||||
key: 'item',
|
||||
width: 220,
|
||||
width: 240,
|
||||
render: (_, record) => (
|
||||
<div>
|
||||
<div style={{ fontWeight: 600, fontSize: 14 }}>{record.itemName}</div>
|
||||
<Space size={4}>
|
||||
<Text type="secondary" copyable style={{ fontSize: 12 }}>
|
||||
{record.inventoryNo}
|
||||
<Space size={4} align="center">
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
寄卖单号
|
||||
</Text>
|
||||
<Text copyable style={{ fontSize: 12, fontWeight: 600 }}>
|
||||
{resolveConsignmentNo(record)}
|
||||
</Text>
|
||||
</Space>
|
||||
{record.sourceSubmissionNo ? (
|
||||
<div>
|
||||
<Text type="secondary" style={{ fontSize: 11, color: '#bfbfbf' }}>
|
||||
仓库号 {record.inventoryNo}
|
||||
</Text>
|
||||
</div>
|
||||
) : null}
|
||||
<div style={{ marginTop: 2 }}>
|
||||
{record.sourceType === 'worker_portal' ? (
|
||||
<Tag color="purple" style={{ fontSize: 11, padding: '0 4px' }}>
|
||||
打手端提报
|
||||
@@ -365,7 +385,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
人工入库
|
||||
</Tag>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
@@ -1176,7 +1196,7 @@ export default function ConsignmentInventoryPanel() {
|
||||
|
||||
{/* 查看详情与凭据弹窗 */}
|
||||
<Modal
|
||||
title={`物品详情 [${viewingItem?.inventoryNo}]`}
|
||||
title={`物品详情 [${viewingItem ? resolveConsignmentNo(viewingItem) : ''}]`}
|
||||
open={Boolean(viewingItem)}
|
||||
width={680}
|
||||
footer={[
|
||||
@@ -1191,8 +1211,20 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Descriptions.Item label="物品名称" span={2}>
|
||||
<strong>{viewingItem.itemName}</strong>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="库存编号">
|
||||
<Text copyable>{viewingItem.inventoryNo}</Text>
|
||||
<Descriptions.Item label="寄卖单号">
|
||||
<Text copyable strong>
|
||||
{resolveConsignmentNo(viewingItem)}
|
||||
</Text>
|
||||
{viewingItem.sourceSubmissionNo ? null : (
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||
(客服手工入库,无来源提报单号)
|
||||
</Text>
|
||||
)}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="仓库号">
|
||||
<Text type="secondary" copyable>
|
||||
{viewingItem.inventoryNo}
|
||||
</Text>
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="当前状态">
|
||||
{viewingItem.status === 'in_stock' ? (
|
||||
@@ -1289,7 +1321,8 @@ export default function ConsignmentInventoryPanel() {
|
||||
<Text>
|
||||
确认将该物品{' '}
|
||||
<strong>
|
||||
[{returnItem?.itemName} - {returnItem?.inventoryNo}]
|
||||
[{returnItem?.itemName} - 寄卖单号{' '}
|
||||
{returnItem ? resolveConsignmentNo(returnItem) : ''}]
|
||||
</strong>{' '}
|
||||
退回复库吗?
|
||||
</Text>
|
||||
|
||||
@@ -80,7 +80,7 @@ import {
|
||||
revokeWorkOrderConsignmentFulfillment,
|
||||
type WorkOrderVoucherConsumeResult,
|
||||
} from '@/services/admin/consignment'
|
||||
import type { WorkOrderInventoryMatchCheck } from '@/types/consignment'
|
||||
import type { ConsignmentFulfillmentInfo, WorkOrderInventoryMatchCheck } from '@/types/consignment'
|
||||
import type { UploadedFile, WorkOrder, WorkOrderShare } from '@/types/worker-platform'
|
||||
import { AdminRangePicker } from '@/components/admin/AdminDatePicker'
|
||||
import dayjs, { ADMIN_DATE_FORMAT } from '@/lib/dayjs'
|
||||
@@ -174,6 +174,20 @@ function isInternalConsignmentOrder(row: WorkOrder) {
|
||||
return Boolean(consignment.inventoryId) && !row.worker
|
||||
}
|
||||
|
||||
/** 工单 `material.consignment`(寄售履约信息);没有寄售履约信息时返回 null。 */
|
||||
function getConsignmentFulfillment(row: WorkOrder): ConsignmentFulfillmentInfo | null {
|
||||
const consignment = asRecord((row.material as Record<string, unknown> | undefined)?.consignment)
|
||||
return Object.keys(consignment).length > 0 ? (consignment as ConsignmentFulfillmentInfo) : null
|
||||
}
|
||||
|
||||
/**
|
||||
* 寄卖单号 = 来源提报单号(提报单号即寄卖单号)。
|
||||
* 历史老单的 consignment 没有 sourceSubmissionNo,回退到内部仓库号 inventoryNo。
|
||||
*/
|
||||
function resolveConsignmentNo(info: ConsignmentFulfillmentInfo) {
|
||||
return info.sourceSubmissionNo || info.inventoryNo || ''
|
||||
}
|
||||
|
||||
/**
|
||||
* 只有「未验收且未取消」的工单可以撤销寄售履约。
|
||||
* 已验收工单撤销会造成同一件现货二次出货、并重新冻结已退还的押金,后端一律拒绝。
|
||||
@@ -330,6 +344,9 @@ export default function WorkOrdersPanel() {
|
||||
submitting: false,
|
||||
})
|
||||
|
||||
// 详情抽屉里的寄售履约信息(有寄售履约才展示寄卖单号)
|
||||
const detailConsignment = detailOrder ? getConsignmentFulfillment(detailOrder) : null
|
||||
|
||||
// 仅单独筛选「待验收」时提供批量验收入口(多选状态下不启用勾选,避免误操作)
|
||||
const isPendingAcceptanceOnly = statuses.length === 1 && statuses[0] === 'pending_acceptance'
|
||||
const selectedWorkOrderStatuses = statuses.filter(
|
||||
@@ -1210,6 +1227,7 @@ export default function WorkOrdersPanel() {
|
||||
const source = asRecord(row.material?.source)
|
||||
const shopName = String(source.shopName || '').trim()
|
||||
const provider = String(source.provider || '').trim()
|
||||
const consignment = getConsignmentFulfillment(row)
|
||||
return (
|
||||
<div className="cell-stack">
|
||||
<ProductNameEditableCell
|
||||
@@ -1255,10 +1273,10 @@ export default function WorkOrdersPanel() {
|
||||
{shopName ? `店铺:${shopName}` : ''}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
{(row.material as any)?.consignment ? (
|
||||
{consignment ? (
|
||||
<Tag color="purple" style={{ marginTop: 2, width: 'fit-content' }}>
|
||||
寄售履约: {(row.material as any).consignment.inventoryNo} (成本: ¥
|
||||
{(((row.material as any).consignment.costPriceFen || 0) / 100).toFixed(2)})
|
||||
寄卖单号: {resolveConsignmentNo(consignment) || '-'} (成本: ¥
|
||||
{((Number(consignment.costPriceFen) || 0) / 100).toFixed(2)})
|
||||
</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
@@ -1869,6 +1887,13 @@ export default function WorkOrdersPanel() {
|
||||
<Descriptions.Item label="分类">
|
||||
{detailOrder.categoryName || '-'}
|
||||
</Descriptions.Item>
|
||||
{detailConsignment ? (
|
||||
<Descriptions.Item label="寄卖单号">
|
||||
<Typography.Text copyable={{ text: resolveConsignmentNo(detailConsignment) }}>
|
||||
{resolveConsignmentNo(detailConsignment) || '-'}
|
||||
</Typography.Text>
|
||||
</Descriptions.Item>
|
||||
) : null}
|
||||
<Descriptions.Item label="状态">
|
||||
<Tag color={resolveStatusColor(detailOrder.status)}>
|
||||
{formatStatus(detailOrder.status)}
|
||||
@@ -2999,9 +3024,14 @@ export default function WorkOrdersPanel() {
|
||||
) : (
|
||||
<Tag color="gold">【部分匹配 / 高配充低配】</Tag>
|
||||
)}
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
{inv.inventoryNo}
|
||||
<Typography.Text style={{ fontSize: 12, fontWeight: 500 }}>
|
||||
寄卖单号 {inv.sourceSubmissionNo || inv.inventoryNo}
|
||||
</Typography.Text>
|
||||
{inv.sourceSubmissionNo ? (
|
||||
<Typography.Text type="secondary" style={{ fontSize: 12 }}>
|
||||
仓库号 {inv.inventoryNo}
|
||||
</Typography.Text>
|
||||
) : null}
|
||||
</Space>
|
||||
|
||||
<div style={{ marginTop: 4 }}>
|
||||
|
||||
Reference in New Issue
Block a user