fix(worker): 多份额验收优先唯一待提交份额并补充 shareId 防呆

This commit is contained in:
2026-11-01 12:27:55 +08:00
parent 26beaca0c9
commit 94a1436453
3 changed files with 95 additions and 11 deletions
@@ -801,22 +801,29 @@ async function supplementWorkerAcceptedSharingEvidence(
return { share: mapWorkOrderShare(updated || sharingShare) }
}
type WorkerTargetShareLike = { id: number; status: string }
export type WorkerTargetShareSelection<T> = {
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<T extends WorkerTargetShareLike>(
shares: T[],
requestedShareId: number,
): WorkerTargetShareSelection<T> {
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<WorkerTargetShareSelection<WorkOrderShareRow>> {
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'])
@@ -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)
})
@@ -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