diff --git a/apps/backend/src/services/worker-platform/consignment-action-target.test.ts b/apps/backend/src/services/worker-platform/consignment-action-target.test.ts new file mode 100644 index 00000000..d9ddeef8 --- /dev/null +++ b/apps/backend/src/services/worker-platform/consignment-action-target.test.ts @@ -0,0 +1,44 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { isLegacyBoundConsignmentTarget } from './consignment-action-target.js' + +function baseInput() { + return { + inventoryStatus: 'consumed', + boundWorkOrderId: 204855, + allocationId: 0, + shareId: 0, + workOrderId: 204855, + hasActiveAllocation: false, + } +} + +test('历史整单履约:没有占用记录且指向库存绑定工单时允许继续', () => { + assert.equal(isLegacyBoundConsignmentTarget(baseInput()), true) +}) + +test('历史兜底只接受 consumed 库存', () => { + assert.equal( + isLegacyBoundConsignmentTarget({ ...baseInput(), inventoryStatus: 'in_stock' }), + false, + ) +}) + +test('有新的有效占用时不得回退到库存绑定工单', () => { + assert.equal(isLegacyBoundConsignmentTarget({ ...baseInput(), hasActiveAllocation: true }), false) +}) + +test('显式指定 allocation/share 但没有匹配记录时不得兜底', () => { + assert.equal(isLegacyBoundConsignmentTarget({ ...baseInput(), allocationId: 11 }), false) + assert.equal(isLegacyBoundConsignmentTarget({ ...baseInput(), shareId: 22 }), false) +}) + +test('工单号与库存绑定工单不一致时不得兜底', () => { + assert.equal(isLegacyBoundConsignmentTarget({ ...baseInput(), workOrderId: 204856 }), false) + assert.equal(isLegacyBoundConsignmentTarget({ ...baseInput(), boundWorkOrderId: null }), false) +}) + +test('没有传工单号时不需要历史兜底', () => { + assert.equal(isLegacyBoundConsignmentTarget({ ...baseInput(), workOrderId: 0 }), false) +}) diff --git a/apps/backend/src/services/worker-platform/consignment-action-target.ts b/apps/backend/src/services/worker-platform/consignment-action-target.ts new file mode 100644 index 00000000..7bec4500 --- /dev/null +++ b/apps/backend/src/services/worker-platform/consignment-action-target.ts @@ -0,0 +1,21 @@ +/** + * 历史整单履约兼容判断。 + * + * 旧的后台「内部库存履约」只写 consignment_inventories.bound_work_order_id, + * 不会生成 consignment_inventory_allocations。卖家端按绑定工单发起操作时, + * 只要库存仍是 consumed 且没有新的有效占用,就允许按整单继续。 + */ +export function isLegacyBoundConsignmentTarget(input: { + inventoryStatus: string + boundWorkOrderId: number | string | null | undefined + allocationId: number + shareId: number + workOrderId: number + hasActiveAllocation: boolean +}): boolean { + if (input.hasActiveAllocation) return false + if (input.allocationId > 0 || input.shareId > 0) return false + if (input.workOrderId <= 0) return false + if (String(input.inventoryStatus || '').trim() !== 'consumed') return false + return Number(input.boundWorkOrderId || 0) === input.workOrderId +} 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 9377a5ab..d9d8620d 100644 --- a/apps/backend/src/services/worker-platform/consignment-worker-service.ts +++ b/apps/backend/src/services/worker-platform/consignment-worker-service.ts @@ -32,6 +32,7 @@ import { resolvePriceDropSuggestion, } from './consignment-profile-service.js' import { suggestConsignmentPriceFromRules } from './consignment-matching-service.js' +import { isLegacyBoundConsignmentTarget } from './consignment-action-target.js' import { getEnabledWorkProductRuleByKey, parseTemplateSpecs, @@ -1010,7 +1011,19 @@ async function resolveSellerConsignmentWorkOrder( errorCode: 'consignment_allocation_selection_required', }) } - if ((allocationIdInput > 0 || shareIdInput > 0 || workOrderIdInput > 0) && !allocation) { + const isLegacyBoundTarget = isLegacyBoundConsignmentTarget({ + inventoryStatus: inventory.status, + boundWorkOrderId: inventory.bound_work_order_id, + allocationId: allocationIdInput, + shareId: shareIdInput, + workOrderId: workOrderIdInput, + hasActiveAllocation: allocations.length > 0, + }) + if ( + (allocationIdInput > 0 || shareIdInput > 0 || workOrderIdInput > 0) && + !allocation && + !isLegacyBoundTarget + ) { throw createHttpError('未找到该寄卖单对应的订单占用,请刷新后重试', { statusCode: 404, errorCode: 'consignment_allocation_not_found', diff --git a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx index 1334a2c9..a363f41b 100644 --- a/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerConsignmentPage.tsx @@ -63,6 +63,7 @@ import type { } from '@/types/consignment' import type { UploadedFile } from '@/types/worker-platform' import { DEFAULT_CONSIGNMENT_INTAKE_FIELDS, buildSpecItemName } from '@/utils/consignment-spec' +import { resolveConsignmentActionTarget } from '@/utils/consignment-action-target' import { buildConsignmentCategoryGroups } from '@/utils/consignment-category-tree' import { formatConsignmentContactSupporter } from '@/utils/consignment-contact' import { formatGiftCooldownRemaining } from '@/utils/consignment-cooldown' @@ -757,14 +758,6 @@ 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 @@ -789,7 +782,10 @@ export default function WorkerConsignmentPage() { centered: true, onOk: async () => { try { - await confirmWorkerConsignmentFriendAdded(submissionId, resolveActionTarget(item)) + await confirmWorkerConsignmentFriendAdded( + submissionId, + resolveConsignmentActionTarget(item), + ) message.success('已确认加好友,赠送冷却开始') await myConsignmentsQuery.refetch() } catch (err: any) { @@ -831,7 +827,7 @@ export default function WorkerConsignmentPage() { ...(note ? { note } : {}), } await submitWorkerConsignmentGiftEvidence(submissionId, { - ...resolveActionTarget(evidenceItem), + ...resolveConsignmentActionTarget(evidenceItem), ...payload, }) message.success('赠送截图已提交,等待客服验收') diff --git a/apps/frontend/src/utils/consignment-action-target.ts b/apps/frontend/src/utils/consignment-action-target.ts new file mode 100644 index 00000000..f8a9676b --- /dev/null +++ b/apps/frontend/src/utils/consignment-action-target.ts @@ -0,0 +1,29 @@ +export type ConsignmentActionTargetInput = { + workOrderId?: number | null + shareId?: number | null + allocationId?: number | null +} + +export type ConsignmentActionTarget = { + workOrderId?: number | null + shareId?: number | null + allocationId?: number | null +} + +/** + * 卖家侧操作定位参数。 + * + * 有 allocation/share 时按新占用模型传参;历史整单履约没有占用记录, + * 这时不要传 workOrderId,让后端回退到库存自身的 bound_work_order_id。 + */ +export function resolveConsignmentActionTarget( + item: ConsignmentActionTargetInput, +): ConsignmentActionTarget { + const hasAllocationTarget = Number(item.allocationId || 0) > 0 || Number(item.shareId || 0) > 0 + if (!hasAllocationTarget) return {} + return { + workOrderId: item.workOrderId ?? null, + shareId: item.shareId ?? null, + allocationId: item.allocationId ?? null, + } +} diff --git a/apps/frontend/tests/consignment-action-target.test.ts b/apps/frontend/tests/consignment-action-target.test.ts new file mode 100644 index 00000000..72e2d79d --- /dev/null +++ b/apps/frontend/tests/consignment-action-target.test.ts @@ -0,0 +1,48 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { resolveConsignmentActionTarget } from '../src/utils/consignment-action-target.ts' + +test('历史整单履约没有占用/份额时不传 workOrderId', () => { + assert.deepEqual( + resolveConsignmentActionTarget({ + workOrderId: 204855, + shareId: null, + allocationId: null, + }), + {}, + ) +}) + +test('allocationId/shareId 为 0 也按无占用处理', () => { + assert.deepEqual( + resolveConsignmentActionTarget({ + workOrderId: 204855, + shareId: 0, + allocationId: 0, + }), + {}, + ) +}) + +test('有占用记录时携带完整定位参数', () => { + assert.deepEqual( + resolveConsignmentActionTarget({ + workOrderId: 204855, + shareId: null, + allocationId: 99, + }), + { workOrderId: 204855, shareId: null, allocationId: 99 }, + ) +}) + +test('拼单份额同样保留定位参数', () => { + assert.deepEqual( + resolveConsignmentActionTarget({ + workOrderId: 204855, + shareId: 88, + allocationId: null, + }), + { workOrderId: 204855, shareId: 88, allocationId: null }, + ) +})