From df90b67d8ec11ef7a6ba9a216fa7da98be68813b Mon Sep 17 00:00:00 2001 From: yml2213 Date: Wed, 19 Aug 2026 19:27:23 +0800 Subject: [PATCH] =?UTF-8?q?v5=E6=8B=BC=E5=8D=95=E5=85=8D=E5=AE=A1=E6=A0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../worker-platform/worker-service.ts | 139 +++++++++++++++++- .../src/components/WorkOrderEventTimeline.tsx | 21 +++ .../src/pages/worker/WorkerOrdersPage.tsx | 21 +-- 3 files changed, 162 insertions(+), 19 deletions(-) diff --git a/apps/backend/src/services/worker-platform/worker-service.ts b/apps/backend/src/services/worker-platform/worker-service.ts index bccab000..6f692d51 100644 --- a/apps/backend/src/services/worker-platform/worker-service.ts +++ b/apps/backend/src/services/worker-platform/worker-service.ts @@ -4,6 +4,7 @@ import { runtimeConfig } from '../../config/runtime.js' import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' import { acceptWorkOrderAndSettle, + acceptWorkOrderShareAndSettle, countWorkerAcceptedOrders, countWorkerActiveOrders, countWorkerTimeoutEvents, @@ -1875,11 +1876,9 @@ export async function supplementWorkerAcceptedOrderEvidence( requireActiveWorkerSession(session) const workOrder = await getRequiredWorkOrder(workOrderId) const worker = await getRequiredWorker(session.workerId) - if (workOrder.sharing_enabled) { - throw createHttpError('拼单订单暂不支持免审核后补图', { - statusCode: 409, - errorCode: 'work_order_vip_evidence_sharing_not_supported', - }) + const sharingShare = await getWorkOrderShare(workOrder.id, session.workerId) + if (sharingShare) { + return supplementWorkerAcceptedSharingEvidence(workOrder, sharingShare, payload, session) } if (Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)) { throw createHttpError('只能补充自己的订单验收图片', { @@ -1985,11 +1984,13 @@ async function submitWorkerSharingAcceptance( }) } const now = nowIso() + const worker = await getRequiredWorker(session.workerId) + const canAutoAcceptWithoutEvidence = resolveWorkerPermissions(worker).autoAcceptWithoutEvidence const { files, imageUrls, note } = resolveAcceptanceImages( payload, sharingShare.draft_acceptance_json, ) - if (imageUrls.length === 0) { + if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) { throw createHttpError('请上传验收图片', { statusCode: 400, errorCode: 'work_order_acceptance_image_required', @@ -2000,6 +2001,14 @@ async function submitWorkerSharingAcceptance( files, imageUrls: [...new Set(imageUrls)], submittedAt: now, + ...(canAutoAcceptWithoutEvidence + ? { + reviewMode: 'vip_auto', + evidenceStatus: imageUrls.length > 0 ? 'complete' : 'pending', + evidenceDueAt: + imageUrls.length > 0 ? null : addHours(now, VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS), + } + : {}), } const result = await submitWorkOrderShareAcceptance({ workOrderId: Number(workOrder.id), @@ -2019,6 +2028,30 @@ async function submitWorkerSharingAcceptance( errorCode: 'work_order_sharing_share_not_found', }) } + if (canAutoAcceptWithoutEvidence) { + const accepted = await acceptWorkOrderShareAndSettle({ + workOrderId: Number(workOrder.id), + shareId: Number(result.share.id), + unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays, + now, + actorType: 'worker', + actorName: worker.display_name || worker.username || '', + }) + if (!accepted.share || !accepted.order) { + throw createHttpError('VIP 免审核验收失败,请稍后重试', { + statusCode: 409, + errorCode: 'work_order_sharing_vip_auto_accept_failed', + }) + } + await publishWorkOrderParticipantChange(Number(workOrder.id), { + workerIds: [Number(session.workerId)], + walletChangedWorkerIds: [Number(session.workerId)], + }) + return { + share: mapWorkOrderShare(accepted.share), + sharingPendingCount: await countWorkOrderPendingSharingSubmissions(Number(workOrder.id)), + } + } await publishWorkOrderParticipantChange(Number(workOrder.id), { workerIds: [Number(session.workerId)], }) @@ -2028,6 +2061,100 @@ async function submitWorkerSharingAcceptance( } } +/** VIP 拼单份额在免审核后 24 小时内补充验收图片。 */ +async function supplementWorkerAcceptedSharingEvidence( + workOrder: WorkOrderRow, + sharingShare: WorkOrderShareRow, + payload: JsonObject, + session: WorkerSession, +) { + if (sharingShare.status !== 'accepted') { + throw createHttpError('只有已验收的拼单份额可以补充验收图片', { + statusCode: 409, + errorCode: 'work_order_sharing_evidence_supplement_status_invalid', + }) + } + + const now = nowIso() + const previous = safeParseJson(sharingShare.acceptance_json) + if (String(previous.reviewMode || '').trim() !== 'vip_auto') { + throw createHttpError('该拼单份额不支持验收后补图', { + statusCode: 403, + errorCode: 'work_order_sharing_evidence_supplement_not_allowed', + }) + } + const evidenceDueAt = String(previous.evidenceDueAt || '').trim() + if (!evidenceDueAt || Date.parse(evidenceDueAt) <= Date.now()) { + throw createHttpError('补充验收图片已超过 24 小时时限', { + statusCode: 409, + errorCode: 'work_order_sharing_evidence_supplement_expired', + }) + } + + const existingFiles = normalizeUploadedFiles(previous.files) + const uploadedFiles = normalizeUploadedFiles(payload.files) + const mergedFiles = [...existingFiles] + const fileIdentities = new Set( + existingFiles.map((file) => file.objectKey || file.url || file.mediumUrl || file.thumbnailUrl), + ) + for (const file of uploadedFiles) { + const identity = file.objectKey || file.url || file.mediumUrl || file.thumbnailUrl + if (!identity || fileIdentities.has(identity)) continue + fileIdentities.add(identity) + mergedFiles.push(file) + } + + const previousImageUrls = normalizeStringArray(previous.imageUrls) + const imageUrls = [ + ...new Set([ + ...previousImageUrls, + ...mergedFiles.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean), + ]), + ] + if ( + mergedFiles.length === existingFiles.length && + imageUrls.length === previousImageUrls.length + ) { + throw createHttpError('请上传新的验收图片', { + statusCode: 400, + errorCode: 'work_order_sharing_evidence_supplement_image_required', + }) + } + + const acceptance = { + ...previous, + files: mergedFiles, + imageUrls, + evidenceStatus: 'complete', + evidenceSupplementedAt: now, + updatedAt: now, + } + const updated = await updateWorkOrderShareAcceptanceRecord({ + workOrderId: Number(workOrder.id), + workerId: Number(session.workerId), + acceptanceJson: JSON.stringify(acceptance), + now, + }) + await createWorkOrderEvent({ + workOrderId: workOrder.id, + actorType: 'worker', + actorId: String(session.workerId), + eventType: 'sharing_acceptance_evidence_supplemented', + fromStatus: WORK_ORDER_STATUS.ACCEPTED, + toStatus: WORK_ORDER_STATUS.ACCEPTED, + payloadJson: JSON.stringify({ + workerId: Number(session.workerId), + addedImageCount: mergedFiles.length - existingFiles.length, + totalImageCount: imageUrls.length, + }), + now, + }) + await publishWorkOrderParticipantChange(Number(workOrder.id), { + workerIds: [Number(session.workerId)], + }) + return { share: mapWorkOrderShare(updated || sharingShare) } +} + /** * 从提交/保存验收的载荷中解析图片与说明。 * 载荷未带图片时回退到暂存草稿(上传图片与提交验收分离:先暂存、后提交)。 diff --git a/apps/frontend/src/components/WorkOrderEventTimeline.tsx b/apps/frontend/src/components/WorkOrderEventTimeline.tsx index 75f0115f..f6245974 100644 --- a/apps/frontend/src/components/WorkOrderEventTimeline.tsx +++ b/apps/frontend/src/components/WorkOrderEventTimeline.tsx @@ -142,6 +142,27 @@ export function formatWorkOrderEvent(event: WorkOrderEvent): EventNodeStyle { color: 'purple', } } + case 'sharing_acceptance_submitted': { + const quantity = readPayload(event, 'quantity') + return { + title: `提交拼单份额验收${quantity ? `(${quantity} 份)` : ''}`, + color: 'purple', + } + } + case 'sharing_share_accepted': { + const workerId = readPayload(event, 'workerId') + return { + title: `拼单份额验收通过并已结算${workerId ? `(打手 #${workerId})` : ''}`, + color: 'purple', + } + } + case 'sharing_acceptance_evidence_supplemented': { + const addedImageCount = readPayload(event, 'addedImageCount') + return { + title: `拼单份额已验收后补充图片${addedImageCount ? `(新增 ${addedImageCount} 张)` : ''}`, + color: 'purple', + } + } case 'accepted': case 'accepted_by_admin': return { diff --git a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx index 64d1d613..490b0bdc 100644 --- a/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx +++ b/apps/frontend/src/pages/worker/WorkerOrdersPage.tsx @@ -1369,26 +1369,21 @@ function canRemindAcceptance(order: WorkOrder) { } function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) { - return ( - workerCanAutoAcceptWithoutEvidence && - !order.myShare && - order.sharing?.enabled !== true && - ['in_progress', 'problem'].includes(order.status) - ) + if (!workerCanAutoAcceptWithoutEvidence) return false + if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted' + return order.sharing?.enabled !== true && ['in_progress', 'problem'].includes(order.status) } function canSupplementAcceptedEvidence(order: WorkOrder) { - if (order.myShare || order.status !== 'accepted' || order.acceptance?.reviewMode !== 'vip_auto') { - return false - } - const dueAt = Date.parse(String(order.acceptance.evidenceDueAt || '')) + const acceptance = getWorkerAcceptance(order) + if (order.status !== 'accepted' || acceptance?.reviewMode !== 'vip_auto') return false + const dueAt = Date.parse(String(acceptance.evidenceDueAt || '')) return Number.isFinite(dueAt) && dueAt > Date.now() } function isVipEvidencePending(order: WorkOrder) { - return ( - order.acceptance?.reviewMode === 'vip_auto' && order.acceptance?.evidenceStatus === 'pending' - ) + const acceptance = getWorkerAcceptance(order) + return acceptance?.reviewMode === 'vip_auto' && acceptance?.evidenceStatus === 'pending' } /** 是否可编辑验收图片:未提交时可暂存,已提交(待验收)时可补充/修改 */