feat(consignment): 支持多数量拆分与份额级赠送,并对齐待抢单看板

- 匹配引擎支持一条库存按剩余数量拆给多张整单/拼单,份额数量、报酬、分配数量按实际占用计算
- 「我的寄卖」改为按占用单/份额一行 + 剩余库存行,卖家按 allocation/share/workOrder 定位确认加好友与提交凭证
- 解绑/撤销/超时按 allocation 精确回补,修复单绑定字段在多占用下被误清的问题
- 拼单好友赠送改为份额级 friend_added_at / gift_available_at(迁移 146),新增后台按份额确认加好友入口与寄卖赠送列
- 待抢单匹配看板改用新引擎:覆盖全部寄卖分类、拼单余量、精确/非精确/冲突明细,去掉手枪品类写死
- 发布押金改为该提报单全部售出/作废后才释放,避免多件库存分多单时提前退款
- 补齐集成测试与单测:拆分、多占用、份额级独立冷却与凭证、看板新旧引擎对齐
This commit is contained in:
2026-11-06 12:23:55 +08:00
parent 3f16b15c5c
commit e0a85359b6
28 changed files with 1758 additions and 125 deletions
@@ -95,6 +95,11 @@ export default function ConsignmentPendingMatchModal({
<Text type="secondary" style={{ fontSize: 11 }}>
录单 {record.workOrderCreatedAt ? formatDateTime(record.workOrderCreatedAt) : '-'}
</Text>
{Number(record.totalQuantity || 1) > 1 ? (
<Text type="secondary" style={{ fontSize: 11 }}>
拼单剩余 {record.remainingQuantity ?? 0} / {record.totalQuantity} 份
</Text>
) : null}
</Space>
),
},
@@ -166,6 +171,15 @@ export default function ConsignmentPendingMatchModal({
{record.conflicts.join(';')}
</Text>
) : null}
{(record.matchDetails || [])
.filter((detail) => detail.status !== 'match' && detail.status !== 'ignored')
.slice(0, 4)
.map((detail) => (
<Text key={detail.key} type="secondary" style={{ fontSize: 11 }}>
{detail.label}:订单「{detail.orderValue || '-'}」/ 库存「
{detail.inventoryValue || '-'}」
</Text>
))}
</Space>
),
},
@@ -173,7 +187,7 @@ export default function ConsignmentPendingMatchModal({
return (
<Modal
title="匹配 · 待抢单手枪 × 在库现货"
title="匹配 · 待抢单 × 在库现货"
open={open}
width="min(1320px, 94vw)"
footer={null}
@@ -184,8 +198,8 @@ export default function ConsignmentPendingMatchModal({
<Alert
type="info"
showIcon
message="只查询非拼单的待抢单手枪单 × 在库待匹配现货,不直接改单。"
description="分数 0-100:系统 20 / 区服 20 / 系列 20 / 钻级 25 / 璀璨 15。非精确项仍需按现有规则确认;不按录单、入库时间先后过滤。"
message="按寄卖配置匹配全部待抢单 × 在库待匹配现货,不直接改单。"
description="有寄卖配置的订单按该分类的规格逐项判定(精确 / 非精确 / 不匹配);拼单订单按剩余份数展示。未配置寄卖的订单仍按旧手枪口径兜底。"
/>
<Space wrap size={12}>
@@ -52,6 +52,7 @@ import VideoPreview, { formatAcceptanceVideoDuration } from '@/components/files/
import {
acceptAdminWorkOrder,
acceptAdminWorkOrderShare,
confirmAdminWorkOrderShareFriendAdded,
acceptAdminWorkOrders,
assignAdminWorkOrderToWorker,
cancelAdminWorkOrder,
@@ -3734,6 +3735,22 @@ export default function WorkOrdersPanel() {
</Space>
),
},
{
title: '寄卖赠送',
width: 120,
render: (_, share) => {
if (!share.consignmentInventoryId) return '-'
if (share.status === 'accepted') return <Tag color="green">已完成</Tag>
if (share.status === 'submitted')
return <Tag color="processing">待审核</Tag>
if (!share.friendAddedAt) return <Tag>待加好友</Tag>
const availableAt = Date.parse(String(share.giftAvailableAt || ''))
if (Number.isFinite(availableAt) && availableAt > Date.now()) {
return <Tag color="orange">冷却中</Tag>
}
return <Tag color="green">待赠送</Tag>
},
},
{
title: '问题',
width: 120,
@@ -3906,6 +3923,29 @@ export default function WorkOrdersPanel() {
)
) : null
const consignmentGiftAction =
sharingOrder?.acceptanceMode === 'friend_gift' &&
share.consignmentInventoryId &&
share.status === 'joined' &&
!share.friendAddedAt ? (
<Button
size="small"
title="只确认该卖家的份额,其他卖家的冷却不受影响"
onClick={() => {
void runAction(
() =>
confirmAdminWorkOrderShareFriendAdded(
sharingOrder.workOrderId,
share.shareId,
),
'已确认该份额加好友,赠送冷却开始',
)
}}
>
确认加好友
</Button>
) : null
return (
<Space size={6} className="admin-share-actions">
{share.status === 'submitted' ? (
@@ -3969,6 +4009,7 @@ export default function WorkOrdersPanel() {
撤份额
</Button>
) : null}
{consignmentGiftAction}
{problemAction}
</Space>
)
@@ -530,6 +530,14 @@ export default function WorkerConsignmentPage() {
return null
}
function resolveActionTarget(item: WorkerConsignmentRecord) {
return {
workOrderId: item.workOrderId,
shareId: item.shareId ?? null,
allocationId: item.allocationId ?? null,
}
}
function handleConfirmFriend(item: WorkerConsignmentRecord) {
const submissionId = resolveActionSubmissionId(item)
if (submissionId == null) return
@@ -554,7 +562,7 @@ export default function WorkerConsignmentPage() {
centered: true,
onOk: async () => {
try {
await confirmWorkerConsignmentFriendAdded(submissionId)
await confirmWorkerConsignmentFriendAdded(submissionId, resolveActionTarget(item))
message.success('已确认加好友,赠送冷却开始')
await myConsignmentsQuery.refetch()
} catch (err: any) {
@@ -578,7 +586,8 @@ export default function WorkerConsignmentPage() {
}
async function handleEvidenceSubmit() {
const submissionId = evidenceItem ? resolveActionSubmissionId(evidenceItem) : null
if (!evidenceItem) return
const submissionId = resolveActionSubmissionId(evidenceItem)
if (submissionId == null) return
const urls = evidenceFiles.map((file) => file.url).filter(Boolean)
if (urls.length === 0) {
@@ -594,7 +603,10 @@ export default function WorkerConsignmentPage() {
imageUrls: urls,
...(note ? { note } : {}),
}
await submitWorkerConsignmentGiftEvidence(submissionId, payload)
await submitWorkerConsignmentGiftEvidence(submissionId, {
...resolveActionTarget(evidenceItem),
...payload,
})
message.success('赠送截图已提交,等待客服验收')
closeEvidenceModal()
await myConsignmentsQuery.refetch()
@@ -1137,7 +1149,7 @@ export default function WorkerConsignmentPage() {
<div className="worker-consignment-record-list">
{records.map((record) => (
<ConsignmentRecordCard
key={record.consignmentNo}
key={`${record.submissionId ?? 'x'}-${record.inventoryId ?? 'x'}-${record.allocationId ?? 'stock'}-${record.workOrderId ?? 0}`}
record={record}
now={cooldownNow}
onConfirmFriend={handleConfirmFriend}
@@ -1526,6 +1538,11 @@ function ConsignmentRecordCard({
寄售价{' '}
<strong>¥{((record.salePriceFen ?? record.askingPriceFen) / 100).toFixed(2)}</strong>
</span>
{record.allocationId && Number(record.allocatedQuantity || 0) > 0 ? (
<span>
本单占用 <strong>{record.allocatedQuantity} 件</strong>
</span>
) : null}
{Number(record.quantity || 1) > 1 ? (
<span>
数量{' '}
@@ -981,6 +981,14 @@ export function acceptAdminWorkOrderShare(workOrderId: number, shareId: number)
)
}
/** 寄卖拼单:按份额确认已加好友,只影响该卖家的冷却。 */
export function confirmAdminWorkOrderShareFriendAdded(workOrderId: number, shareId: number) {
return apiPost<{ share: WorkOrderShare | null }>(
`/api/v1/admin/worker-platform/orders/${workOrderId}/shares/${shareId}/confirm-friend`,
{},
)
}
export function cancelAdminWorkOrderShare(
workOrderId: number,
shareId: number,
+18 -3
View File
@@ -447,7 +447,13 @@ export type WorkerConsignmentRecord = {
consignmentNo: string
submissionId: number | null
inventoryId: number | null
/** 一库存拆给多张订单时,本行对应的占用记录 */
allocationId?: number | null
/** 拼单份额 id(本行对应的卖家份额) */
shareId?: number | null
workOrderId: number | null
/** 本行占用的数量;库存剩余行为剩余数量 */
allocatedQuantity?: number
itemName: string
system: string
serverZone: string
@@ -558,21 +564,30 @@ export function fetchWorkerConsignments(params: WorkerConsignmentListParams = {}
)
}
export type WorkerConsignmentActionTarget = {
workOrderId?: number | null
shareId?: number | null
allocationId?: number | null
}
/**
* 卖家确认已在游戏内加好买家好友 → 进入赠送冷却期。
* 注意:确认后若未完成赠送,履约押金不退。
*/
export function confirmWorkerConsignmentFriendAdded(submissionId: number | string) {
export function confirmWorkerConsignmentFriendAdded(
submissionId: number | string,
target: WorkerConsignmentActionTarget = {},
) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/worker/consignment/items/${submissionId}/confirm-friend`,
{},
target,
)
}
/** 卖家提交赠送完成截图(后端复用打手验收链路,工单转「待验收」交客服审核)。 */
export function submitWorkerConsignmentGiftEvidence(
submissionId: number | string,
payload: { imageUrls: string[]; note?: string },
payload: { imageUrls: string[]; note?: string } & WorkerConsignmentActionTarget,
) {
return apiPost<{ order: WorkOrder }>(
`/api/v1/worker/consignment/items/${submissionId}/gift-evidence`,
+15 -1
View File
@@ -300,7 +300,7 @@ export type WorkOrderInventoryMatchCheck = {
matches: ConsignmentMatchResult[]
}
/** 后台「寄售仓库 → 匹配」:待抢单手枪 × 在库待匹配现货的配对明细 */
/** 后台「寄售仓库 → 匹配」:待抢单 × 在库待匹配现货的配对明细 */
export type PendingPistolMatchPair = {
workOrderId: number
workOrderNo: string
@@ -316,6 +316,20 @@ export type PendingPistolMatchPair = {
score: number
reason: string
conflicts: string[]
/** 自定义规格逐字段对比;内置五字段为空,沿用旧评分原因 */
matchDetails?: Array<{
key: string
label: string
orderValue: string
inventoryValue: string
status: 'match' | 'partial' | 'conflict' | 'unknown' | 'ignored'
}>
/** 拼单订单还差多少份;非拼单为 1 */
remainingQuantity?: number
/** 拼单订单总份数;非拼单为 1 */
totalQuantity?: number
/** 命中的寄卖配置 id;无配置时为 null(旧手枪兜底) */
profileId?: number | null
}
export type PendingPistolMatchResult = {
@@ -608,6 +608,10 @@ export type WorkOrderShare = {
cancelNote?: string
/** 被撤掉的份数(撤份额累计 / 单人撤单为整份份数;0 = 未撤过) */
cancelQuantity?: number
/** 寄卖拼单:本份额对应的库存与加好友/赠送冷却状态 */
consignmentInventoryId?: number | null
friendAddedAt?: string | null
giftAvailableAt?: string | null
/** 份额级问题单:只影响该份额持有人,母单状态不变 */
problemMarked?: boolean
problemNote?: string