|
|
|
@@ -62,7 +62,7 @@ import {
|
|
|
|
|
} from '../../repositories/worker-platform/index.js'
|
|
|
|
|
import type { JsonObject } from '../../types/json.js'
|
|
|
|
|
import { createHttpError } from '../../utils/http.js'
|
|
|
|
|
import { nowIso } from '../../utils/time.js'
|
|
|
|
|
import { addHours, nowIso } from '../../utils/time.js'
|
|
|
|
|
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
|
|
|
|
import { getSmsProvider } from '../sms/index.js'
|
|
|
|
|
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
|
|
|
@@ -115,6 +115,7 @@ import {
|
|
|
|
|
} from './mappers.js'
|
|
|
|
|
|
|
|
|
|
const WORKER_DAILY_WITHDRAW_LIMIT = 3
|
|
|
|
|
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
|
|
|
|
|
|
|
|
|
export type WorkerSession = {
|
|
|
|
|
sessionId: string
|
|
|
|
@@ -1266,10 +1267,13 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
requireActiveWorkerSession(session)
|
|
|
|
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
|
|
|
const workerId = Number(session.workerId)
|
|
|
|
|
const worker = await getRequiredWorker(session.workerId)
|
|
|
|
|
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
|
|
|
|
if (sharingShare) {
|
|
|
|
|
return submitWorkerSharingAcceptance(workOrder, sharingShare, payload, session)
|
|
|
|
|
}
|
|
|
|
|
const canAutoAcceptWithoutEvidence =
|
|
|
|
|
!workOrder.sharing_enabled && resolveWorkerPermissions(worker).autoAcceptWithoutEvidence
|
|
|
|
|
const now = nowIso()
|
|
|
|
|
if (
|
|
|
|
|
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS &&
|
|
|
|
@@ -1305,7 +1309,7 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
payload,
|
|
|
|
|
workOrder.draft_acceptance_json,
|
|
|
|
|
)
|
|
|
|
|
if (imageUrls.length === 0) {
|
|
|
|
|
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) {
|
|
|
|
|
throw createHttpError('请上传验收图片', {
|
|
|
|
|
statusCode: 400,
|
|
|
|
|
errorCode: 'work_order_acceptance_image_required',
|
|
|
|
@@ -1316,6 +1320,14 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
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 updated = await updateWorkOrder(workOrder.id, {
|
|
|
|
|
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
|
|
@@ -1331,11 +1343,14 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
eventType: 'acceptance_submitted',
|
|
|
|
|
fromStatus: workOrder.status,
|
|
|
|
|
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
|
|
|
payloadJson: JSON.stringify(acceptance),
|
|
|
|
|
payloadJson: JSON.stringify({
|
|
|
|
|
imageCount: imageUrls.length,
|
|
|
|
|
autoAccepted: canAutoAcceptWithoutEvidence,
|
|
|
|
|
evidenceDueAt: acceptance.evidenceDueAt || null,
|
|
|
|
|
}),
|
|
|
|
|
now,
|
|
|
|
|
})
|
|
|
|
|
const worker = await getRequiredWorker(session.workerId)
|
|
|
|
|
if (worker.worker_type === 'internal') {
|
|
|
|
|
if (worker.worker_type === 'internal' || canAutoAcceptWithoutEvidence) {
|
|
|
|
|
const { order: acceptedOrder, failureReason } = await acceptWorkOrderAndSettle({
|
|
|
|
|
workOrderId: workOrder.id,
|
|
|
|
|
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
|
|
|
@@ -1350,7 +1365,7 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (!acceptedOrder) {
|
|
|
|
|
throw createHttpError('内部打手免审核验收失败,请稍后重试', {
|
|
|
|
|
throw createHttpError('免审核验收失败,请稍后重试', {
|
|
|
|
|
statusCode: 409,
|
|
|
|
|
errorCode: 'work_order_internal_auto_accept_failed',
|
|
|
|
|
})
|
|
|
|
@@ -1359,10 +1374,14 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
workOrderId: workOrder.id,
|
|
|
|
|
actorType: 'worker',
|
|
|
|
|
actorId: String(session.workerId),
|
|
|
|
|
eventType: 'accepted_by_internal',
|
|
|
|
|
eventType: worker.worker_type === 'internal' ? 'accepted_by_internal' : 'accepted_by_vip',
|
|
|
|
|
fromStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
|
|
|
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
|
|
|
|
payloadJson: JSON.stringify({ autoAccepted: true }),
|
|
|
|
|
payloadJson: JSON.stringify({
|
|
|
|
|
autoAccepted: true,
|
|
|
|
|
evidenceStatus: acceptance.evidenceStatus || 'complete',
|
|
|
|
|
evidenceDueAt: acceptance.evidenceDueAt || null,
|
|
|
|
|
}),
|
|
|
|
|
now,
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
@@ -1374,6 +1393,111 @@ export async function submitWorkerOrderAcceptance(
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** VIP 自动验收订单在 24 小时内追加验收图片,不改变订单状态和结算结果。 */
|
|
|
|
|
export async function supplementWorkerAcceptedOrderEvidence(
|
|
|
|
|
workOrderId: number | string,
|
|
|
|
|
payload: JsonObject = {},
|
|
|
|
|
session: WorkerSession,
|
|
|
|
|
) {
|
|
|
|
|
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',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)) {
|
|
|
|
|
throw createHttpError('只能补充自己的订单验收图片', {
|
|
|
|
|
statusCode: 403,
|
|
|
|
|
errorCode: 'work_order_owner_required',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
if (workOrder.status !== WORK_ORDER_STATUS.ACCEPTED) {
|
|
|
|
|
throw createHttpError('只有已验收订单可以补充验收图片', {
|
|
|
|
|
statusCode: 409,
|
|
|
|
|
errorCode: 'work_order_evidence_supplement_status_invalid',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const now = nowIso()
|
|
|
|
|
const previous = safeParseJson(workOrder.acceptance_json)
|
|
|
|
|
if (String(previous.reviewMode || '').trim() !== 'vip_auto') {
|
|
|
|
|
throw createHttpError('该订单不支持验收后补图', {
|
|
|
|
|
statusCode: 403,
|
|
|
|
|
errorCode: 'work_order_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_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_evidence_supplement_image_required',
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const acceptance = {
|
|
|
|
|
...previous,
|
|
|
|
|
files: mergedFiles,
|
|
|
|
|
imageUrls,
|
|
|
|
|
evidenceStatus: 'complete',
|
|
|
|
|
evidenceSupplementedAt: now,
|
|
|
|
|
updatedAt: now,
|
|
|
|
|
}
|
|
|
|
|
const updated = await updateWorkOrderAcceptanceRecord({
|
|
|
|
|
workOrderId: workOrder.id,
|
|
|
|
|
acceptanceJson: JSON.stringify(acceptance),
|
|
|
|
|
now,
|
|
|
|
|
})
|
|
|
|
|
await createWorkOrderEvent({
|
|
|
|
|
workOrderId: workOrder.id,
|
|
|
|
|
actorType: 'worker',
|
|
|
|
|
actorId: String(session.workerId),
|
|
|
|
|
eventType: 'acceptance_evidence_supplemented',
|
|
|
|
|
fromStatus: WORK_ORDER_STATUS.ACCEPTED,
|
|
|
|
|
toStatus: WORK_ORDER_STATUS.ACCEPTED,
|
|
|
|
|
payloadJson: JSON.stringify({
|
|
|
|
|
addedImageCount: mergedFiles.length - existingFiles.length,
|
|
|
|
|
totalImageCount: imageUrls.length,
|
|
|
|
|
}),
|
|
|
|
|
now,
|
|
|
|
|
})
|
|
|
|
|
return {
|
|
|
|
|
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function submitWorkerSharingAcceptance(
|
|
|
|
|
workOrder: WorkOrderRow,
|
|
|
|
|
sharingShare: WorkOrderShareRow,
|
|
|
|
|