diff --git a/apps/backend/src/services/worker-platform/worker-acceptance-service.ts b/apps/backend/src/services/worker-platform/worker-acceptance-service.ts index e5de93fb..25e4e1a3 100644 --- a/apps/backend/src/services/worker-platform/worker-acceptance-service.ts +++ b/apps/backend/src/services/worker-platform/worker-acceptance-service.ts @@ -801,22 +801,29 @@ async function supplementWorkerAcceptedSharingEvidence( return { share: mapWorkOrderShare(updated || sharingShare) } } +type WorkerTargetShareLike = { id: number; status: string } + +export type WorkerTargetShareSelection = { + share: T | null + error: 'required' | 'not_found' | null +} + /** * 定位本次操作要处理的份额: * - 传了 shareId:必须是本人该工单下的有效份额(否则 not_found) * - 没传且只有 1 条:直接用 - * - 没传且有多条:要求前端明确指定(required) + * - 没传且有多条:优先唯一一条「进行中(joined)」份额;仍有多条才要求前端指定(required) * - 没有任何份额:返回空(走整单逻辑) + * + * 多条时优先 joined 是因为 submitted / accepted 的份额已经不能再提交验收: + * 第一份提交后只剩第二份待提交时,前端不传 shareId 也应该能直接定位到它。 */ -async function resolveWorkerTargetShare( - workOrderId: number, - workerId: number | string, - payload: JsonObject, -): Promise<{ share: WorkOrderShareRow | null; error: 'required' | 'not_found' | null }> { - const requested = Number(payload.shareId ?? payload.workOrderShareId ?? 0) - const shares = await listWorkOrderSharesByWorkerInOrder(workOrderId, workerId) - if (Number.isFinite(requested) && requested > 0) { - const found = shares.find((share) => Number(share.id) === requested) || null +export function chooseWorkerTargetShare( + shares: T[], + requestedShareId: number, +): WorkerTargetShareSelection { + if (Number.isFinite(requestedShareId) && requestedShareId > 0) { + const found = shares.find((share) => Number(share.id) === requestedShareId) || null return { share: found, error: found ? null : 'not_found' } } if (shares.length === 0) { @@ -825,9 +832,23 @@ async function resolveWorkerTargetShare( if (shares.length === 1) { return { share: shares[0] ?? null, error: null } } + const pendingShares = shares.filter((share) => String(share.status) === 'joined') + if (pendingShares.length === 1) { + return { share: pendingShares[0] ?? null, error: null } + } return { share: null, error: 'required' } } +async function resolveWorkerTargetShare( + workOrderId: number, + workerId: number | string, + payload: JsonObject, +): Promise> { + const requested = Number(payload.shareId ?? payload.workOrderShareId ?? 0) + const shares = await listWorkOrderSharesByWorkerInOrder(workOrderId, workerId) + return chooseWorkerTargetShare(shares, requested) +} + const ACCEPTANCE_VIDEO_SCENE = 'worker-acceptance-video' const ACCEPTANCE_VIDEO_CONTENT_TYPES = new Set(['video/mp4', 'video/webm']) diff --git a/apps/backend/src/services/worker-platform/worker-acceptance-target-share.test.ts b/apps/backend/src/services/worker-platform/worker-acceptance-target-share.test.ts new file mode 100644 index 00000000..e9b082fd --- /dev/null +++ b/apps/backend/src/services/worker-platform/worker-acceptance-target-share.test.ts @@ -0,0 +1,51 @@ +import assert from 'node:assert/strict' +import test from 'node:test' + +import { chooseWorkerTargetShare } from './worker-acceptance-service.js' + +const submitted = { id: 11, status: 'submitted' } +const joined = { id: 12, status: 'joined' } +const accepted = { id: 13, status: 'accepted' } + +test('份额选择:指定 shareId 命中本人份额', () => { + const result = chooseWorkerTargetShare([submitted, joined], 12) + assert.equal(result.error, null) + assert.equal(result.share?.id, 12) +}) + +test('份额选择:指定了不属于自己的 shareId 返回 not_found', () => { + const result = chooseWorkerTargetShare([submitted, joined], 99) + assert.equal(result.error, 'not_found') + assert.equal(result.share, null) +}) + +test('份额选择:没有任何份额时走整单逻辑', () => { + const result = chooseWorkerTargetShare([], 0) + assert.equal(result.error, null) + assert.equal(result.share, null) +}) + +test('份额选择:只有一条份额时直接使用,不要求 shareId', () => { + const result = chooseWorkerTargetShare([joined], 0) + assert.equal(result.error, null) + assert.equal(result.share?.id, joined.id) +}) + +test('份额选择:多条中只剩一条 joined 时自动选中,不再要求 shareId', () => { + const result = chooseWorkerTargetShare([submitted, joined, accepted], 0) + assert.equal(result.error, null) + assert.equal(result.share?.id, joined.id) +}) + +test('份额选择:多条 joined 时仍要求显式指定', () => { + const secondJoined = { id: 14, status: 'joined' } + const result = chooseWorkerTargetShare([joined, secondJoined, submitted], 0) + assert.equal(result.error, 'required') + assert.equal(result.share, null) +}) + +test('份额选择:全部已提交/已验收时不能自动选中', () => { + const result = chooseWorkerTargetShare([submitted, accepted], 0) + assert.equal(result.error, 'required') + assert.equal(result.share, null) +}) diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index f3aac03a..ee6d022f 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -323,8 +323,12 @@ export default function WorkerOrdersPage() { } function openAcceptanceModal(order: WorkOrder) { - markOrderAsRead(order) const shares = getOrderShares(order) + if (shares.length === 0) { + message.warning('未找到可验收的拼单份额,请刷新后重试') + return + } + markOrderAsRead(order) if (shares.length > 1) { setPickedShareId(shares[0]?.shareId || 0) setSharePicker({ order, action: 'accept' }) @@ -487,6 +491,10 @@ export default function WorkerOrdersPage() { async function submitAcceptance(values: AcceptanceFormValues) { if (!editingOrder) return + if (getOrderShares(editingOrder).length > 1 && !editingShareId) { + message.warning('请先选择要验收的拼单份额') + return + } const isAcceptedEvidenceSupplement = canSupplementAcceptedEvidence(editingOrder) if (isAcceptedEvidenceSupplement) { if (acceptanceFiles.length === 0 && !acceptanceVideoDirty) { @@ -552,6 +560,10 @@ export default function WorkerOrdersPage() { async function saveAcceptanceImages(values: AcceptanceFormValues) { if (!editingOrder) return + if (getOrderShares(editingOrder).length > 1 && !editingShareId) { + message.warning('请先选择要操作的拼单份额') + return + } if (hasSubmittedAcceptance(editingOrder) && acceptanceFiles.length === 0 && !acceptanceVideo) { message.error('请至少保留一张验收图片或一段验收视频') return