v5拼单免审核
This commit is contained in:
@@ -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) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 从提交/保存验收的载荷中解析图片与说明。
|
||||
* 载荷未带图片时回退到暂存草稿(上传图片与提交验收分离:先暂存、后提交)。
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
/** 是否可编辑验收图片:未提交时可暂存,已提交(待验收)时可补充/修改 */
|
||||
|
||||
Reference in New Issue
Block a user