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 { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||||
import {
|
import {
|
||||||
acceptWorkOrderAndSettle,
|
acceptWorkOrderAndSettle,
|
||||||
|
acceptWorkOrderShareAndSettle,
|
||||||
countWorkerAcceptedOrders,
|
countWorkerAcceptedOrders,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
countWorkerTimeoutEvents,
|
countWorkerTimeoutEvents,
|
||||||
@@ -1875,11 +1876,9 @@ export async function supplementWorkerAcceptedOrderEvidence(
|
|||||||
requireActiveWorkerSession(session)
|
requireActiveWorkerSession(session)
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
if (workOrder.sharing_enabled) {
|
const sharingShare = await getWorkOrderShare(workOrder.id, session.workerId)
|
||||||
throw createHttpError('拼单订单暂不支持免审核后补图', {
|
if (sharingShare) {
|
||||||
statusCode: 409,
|
return supplementWorkerAcceptedSharingEvidence(workOrder, sharingShare, payload, session)
|
||||||
errorCode: 'work_order_vip_evidence_sharing_not_supported',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
if (Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)) {
|
if (Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)) {
|
||||||
throw createHttpError('只能补充自己的订单验收图片', {
|
throw createHttpError('只能补充自己的订单验收图片', {
|
||||||
@@ -1985,11 +1984,13 @@ async function submitWorkerSharingAcceptance(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
const now = nowIso()
|
const now = nowIso()
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
const canAutoAcceptWithoutEvidence = resolveWorkerPermissions(worker).autoAcceptWithoutEvidence
|
||||||
const { files, imageUrls, note } = resolveAcceptanceImages(
|
const { files, imageUrls, note } = resolveAcceptanceImages(
|
||||||
payload,
|
payload,
|
||||||
sharingShare.draft_acceptance_json,
|
sharingShare.draft_acceptance_json,
|
||||||
)
|
)
|
||||||
if (imageUrls.length === 0) {
|
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) {
|
||||||
throw createHttpError('请上传验收图片', {
|
throw createHttpError('请上传验收图片', {
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
errorCode: 'work_order_acceptance_image_required',
|
errorCode: 'work_order_acceptance_image_required',
|
||||||
@@ -2000,6 +2001,14 @@ async function submitWorkerSharingAcceptance(
|
|||||||
files,
|
files,
|
||||||
imageUrls: [...new Set(imageUrls)],
|
imageUrls: [...new Set(imageUrls)],
|
||||||
submittedAt: now,
|
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({
|
const result = await submitWorkOrderShareAcceptance({
|
||||||
workOrderId: Number(workOrder.id),
|
workOrderId: Number(workOrder.id),
|
||||||
@@ -2019,6 +2028,30 @@ async function submitWorkerSharingAcceptance(
|
|||||||
errorCode: 'work_order_sharing_share_not_found',
|
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), {
|
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
||||||
workerIds: [Number(session.workerId)],
|
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',
|
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':
|
||||||
case 'accepted_by_admin':
|
case 'accepted_by_admin':
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -1369,26 +1369,21 @@ function canRemindAcceptance(order: WorkOrder) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
function canAutoAcceptOrder(order: WorkOrder, workerCanAutoAcceptWithoutEvidence: boolean) {
|
||||||
return (
|
if (!workerCanAutoAcceptWithoutEvidence) return false
|
||||||
workerCanAutoAcceptWithoutEvidence &&
|
if (order.myShare) return order.myShare.status === 'joined' && order.status !== 'accepted'
|
||||||
!order.myShare &&
|
return order.sharing?.enabled !== true && ['in_progress', 'problem'].includes(order.status)
|
||||||
order.sharing?.enabled !== true &&
|
|
||||||
['in_progress', 'problem'].includes(order.status)
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function canSupplementAcceptedEvidence(order: WorkOrder) {
|
function canSupplementAcceptedEvidence(order: WorkOrder) {
|
||||||
if (order.myShare || order.status !== 'accepted' || order.acceptance?.reviewMode !== 'vip_auto') {
|
const acceptance = getWorkerAcceptance(order)
|
||||||
return false
|
if (order.status !== 'accepted' || acceptance?.reviewMode !== 'vip_auto') return false
|
||||||
}
|
const dueAt = Date.parse(String(acceptance.evidenceDueAt || ''))
|
||||||
const dueAt = Date.parse(String(order.acceptance.evidenceDueAt || ''))
|
|
||||||
return Number.isFinite(dueAt) && dueAt > Date.now()
|
return Number.isFinite(dueAt) && dueAt > Date.now()
|
||||||
}
|
}
|
||||||
|
|
||||||
function isVipEvidencePending(order: WorkOrder) {
|
function isVipEvidencePending(order: WorkOrder) {
|
||||||
return (
|
const acceptance = getWorkerAcceptance(order)
|
||||||
order.acceptance?.reviewMode === 'vip_auto' && order.acceptance?.evidenceStatus === 'pending'
|
return acceptance?.reviewMode === 'vip_auto' && acceptance?.evidenceStatus === 'pending'
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/** 是否可编辑验收图片:未提交时可暂存,已提交(待验收)时可补充/修改 */
|
/** 是否可编辑验收图片:未提交时可暂存,已提交(待验收)时可补充/修改 */
|
||||||
|
|||||||
Reference in New Issue
Block a user