v5拼单免审核

This commit is contained in:
yml2213
2026-08-19 19:27:23 +08:00
parent 4ddc4021a2
commit df90b67d8e
3 changed files with 162 additions and 19 deletions
@@ -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) }
}
/**
* 从提交/保存验收的载荷中解析图片与说明。
* 载荷未带图片时回退到暂存草稿(上传图片与提交验收分离:先暂存、后提交)。