拆分打手验收服务
This commit is contained in:
@@ -13,6 +13,7 @@ export * from './worker-hall-service.js'
|
|||||||
export * from './worker-order-timeout-service.js'
|
export * from './worker-order-timeout-service.js'
|
||||||
export * from './worker-order-realtime-service.js'
|
export * from './worker-order-realtime-service.js'
|
||||||
export * from './worker-collect-service.js'
|
export * from './worker-collect-service.js'
|
||||||
|
export * from './worker-acceptance-service.js'
|
||||||
export * from './worker-platform-defaults.js'
|
export * from './worker-platform-defaults.js'
|
||||||
export * from './work-order-timeout-policy.js'
|
export * from './work-order-timeout-policy.js'
|
||||||
export * from './after-sales-service.js'
|
export * from './after-sales-service.js'
|
||||||
|
|||||||
@@ -0,0 +1,744 @@
|
|||||||
|
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||||
|
import {
|
||||||
|
acceptWorkOrderAndSettle,
|
||||||
|
acceptWorkOrderShareAndSettle,
|
||||||
|
countWorkOrderPendingSharingSubmissions,
|
||||||
|
createWorkOrderEvent,
|
||||||
|
getWorkOrderShare,
|
||||||
|
settleOverdueWorkOrder,
|
||||||
|
submitWorkOrderShareAcceptance,
|
||||||
|
updateWorkOrder,
|
||||||
|
updateWorkOrderAcceptanceRecord,
|
||||||
|
updateWorkOrderDraftAcceptance,
|
||||||
|
updateWorkOrderShareAcceptanceRecord,
|
||||||
|
updateWorkOrderShareDraftAcceptance,
|
||||||
|
type WorkOrderRow,
|
||||||
|
type WorkOrderShareRow,
|
||||||
|
} from '../../repositories/worker-platform/index.js'
|
||||||
|
import type { JsonObject } from '../../types/json.js'
|
||||||
|
import { createHttpError } from '../../utils/http.js'
|
||||||
|
import { addHours, nowIso } from '../../utils/time.js'
|
||||||
|
import { safeParseJson } from '../admin/admin-query-utils.js'
|
||||||
|
import {
|
||||||
|
createWorkerAcceptanceAdminNotification,
|
||||||
|
getWorkerAcceptanceReminder,
|
||||||
|
remindWorkerAcceptanceAdminNotification,
|
||||||
|
} from '../admin/admin-notification-service.js'
|
||||||
|
import {
|
||||||
|
publishWorkerWalletRealtimeChange,
|
||||||
|
publishWorkOrderRealtimeChange,
|
||||||
|
} from '../realtime/realtime-event-service.js'
|
||||||
|
import {
|
||||||
|
canWorkerAutoAcceptWholeOrder,
|
||||||
|
mapWorkOrderForWorker,
|
||||||
|
mapWorkOrderShare,
|
||||||
|
normalizeStringArray,
|
||||||
|
normalizeUploadedFiles,
|
||||||
|
resolveWorkerPermissions,
|
||||||
|
} from './mappers.js'
|
||||||
|
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
||||||
|
import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js'
|
||||||
|
import {
|
||||||
|
getRequiredWorkOrder,
|
||||||
|
getRequiredWorker,
|
||||||
|
requireActiveWorkerSession,
|
||||||
|
type WorkerSession,
|
||||||
|
} from './worker-session-context-service.js'
|
||||||
|
import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||||
|
|
||||||
|
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
||||||
|
|
||||||
|
export async function submitWorkerOrderAcceptance(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
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 = canWorkerAutoAcceptWholeOrder({
|
||||||
|
worker,
|
||||||
|
hasPersonalSharingShare: false,
|
||||||
|
})
|
||||||
|
const now = nowIso()
|
||||||
|
if (
|
||||||
|
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS &&
|
||||||
|
workOrder.deadline_at &&
|
||||||
|
new Date(workOrder.deadline_at).getTime() <= Date.now()
|
||||||
|
) {
|
||||||
|
await settleOverdueWorkOrder({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
policy: normalizeWorkOrderTimeoutPolicy(workOrder.timeout_policy),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
throw createHttpError('任务已超时,系统已按超时策略处置', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_timeout_expired',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Number(workOrder.assigned_worker_id || 0) !== workerId) {
|
||||||
|
throw createHttpError('只能提交自己的订单', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'work_order_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(workOrder.status as never)
|
||||||
|
) {
|
||||||
|
throw createHttpError('当前状态不能提交验收', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_submit_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const { files, imageUrls, note } = resolveAcceptanceImages(
|
||||||
|
payload,
|
||||||
|
workOrder.draft_acceptance_json,
|
||||||
|
)
|
||||||
|
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) {
|
||||||
|
throw createHttpError('请上传验收图片', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_acceptance_image_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const acceptance = {
|
||||||
|
note,
|
||||||
|
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,
|
||||||
|
acceptance_json: acceptance,
|
||||||
|
draft_acceptance_json: {},
|
||||||
|
submitted_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(session.workerId),
|
||||||
|
eventType: 'acceptance_submitted',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
imageCount: imageUrls.length,
|
||||||
|
autoAccepted: canAutoAcceptWithoutEvidence,
|
||||||
|
evidenceDueAt: acceptance.evidenceDueAt || null,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (worker.worker_type === 'internal' || canAutoAcceptWithoutEvidence) {
|
||||||
|
const { order: acceptedOrder, failureReason } = await acceptWorkOrderAndSettle({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
||||||
|
now,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorName: worker.display_name || worker.username || '',
|
||||||
|
})
|
||||||
|
if (failureReason === 'sharing_pending_submissions') {
|
||||||
|
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_pending_submissions',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!acceptedOrder) {
|
||||||
|
throw createHttpError('免审核验收失败,请稍后重试', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_internal_auto_accept_failed',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(session.workerId),
|
||||||
|
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,
|
||||||
|
evidenceStatus: acceptance.evidenceStatus || 'complete',
|
||||||
|
evidenceDueAt: acceptance.evidenceDueAt || null,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerIds: [Number(worker.id)],
|
||||||
|
})
|
||||||
|
publishWorkerWalletRealtimeChange(Number(worker.id))
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(acceptedOrder, resolveWorkerPermissions(worker)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await createWorkerAcceptanceAdminNotification({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workOrderNo: workOrder.work_order_no,
|
||||||
|
productName: workOrder.product_name,
|
||||||
|
workerName: worker.display_name || worker.username,
|
||||||
|
imageCount: imageUrls.length,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerIds: [Number(worker.id)],
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 打手在提交验收后催促后台处理;频率和次数均由服务端控制。 */
|
||||||
|
export async function remindWorkerOrderAcceptance(
|
||||||
|
workOrderId: number | string,
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
if (
|
||||||
|
workOrder.sharing_enabled ||
|
||||||
|
Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)
|
||||||
|
) {
|
||||||
|
throw createHttpError('只能催促自己提交的整单验收', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
||||||
|
throw createHttpError('当前订单无需催验收', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const submittedAt = Date.parse(String(workOrder.submitted_at || ''))
|
||||||
|
const now = Date.now()
|
||||||
|
const firstReminderAt = submittedAt + 10 * 60 * 1000
|
||||||
|
if (!Number.isFinite(submittedAt) || now < firstReminderAt) {
|
||||||
|
throw createHttpError('提交验收满 10 分钟后才可催验收', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_too_early',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let notification = await getWorkerAcceptanceReminder(Number(workOrder.id))
|
||||||
|
if (!notification) {
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
await createWorkerAcceptanceAdminNotification({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workOrderNo: workOrder.work_order_no,
|
||||||
|
productName: workOrder.product_name,
|
||||||
|
workerName: worker.display_name || worker.username,
|
||||||
|
imageCount: normalizeStringArray(safeParseJson(workOrder.acceptance_json).imageUrls).length,
|
||||||
|
})
|
||||||
|
notification = await getWorkerAcceptanceReminder(Number(workOrder.id))
|
||||||
|
}
|
||||||
|
if (!notification) {
|
||||||
|
throw createHttpError('催验收待办创建失败,请稍后重试', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_notification_missing',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (Number(notification.reminder_count || 0) >= 3) {
|
||||||
|
throw createHttpError('该订单最多催验收 3 次', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_limit_reached',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const lastRemindedAt = Date.parse(String(notification.last_reminded_at || ''))
|
||||||
|
const nextRemindAt = lastRemindedAt + 30 * 60 * 1000
|
||||||
|
if (Number.isFinite(lastRemindedAt) && now < nextRemindAt) {
|
||||||
|
throw createHttpError('每 30 分钟可催验收一次,请稍后再试', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_cooldown',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const reminded = await remindWorkerAcceptanceAdminNotification(Number(workOrder.id))
|
||||||
|
if (!reminded) {
|
||||||
|
throw createHttpError('订单状态已变化,请刷新后重试', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_acceptance_reminder_conflict',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const remindedAt = new Date(reminded.last_reminded_at || new Date().toISOString()).getTime()
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(session.workerId),
|
||||||
|
eventType: 'acceptance_reminded',
|
||||||
|
fromStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
|
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
||||||
|
payloadJson: JSON.stringify({ reminderCount: reminded.reminder_count }),
|
||||||
|
now: new Date(reminded.last_reminded_at || new Date().toISOString()).toISOString(),
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
reminderCount: Number(reminded.reminder_count || 0),
|
||||||
|
nextRemindAt: new Date(remindedAt + 30 * 60 * 1000).toISOString(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已验收订单可追加验收图片,不改变订单状态和结算结果。 */
|
||||||
|
export async function supplementWorkerAcceptedOrderEvidence(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
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('只能补充自己的订单验收图片', {
|
||||||
|
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)
|
||||||
|
const mergedEvidence = mergeAcceptanceEvidence(previous, payload.files)
|
||||||
|
if (!mergedEvidence.changed) {
|
||||||
|
throw createHttpError('请上传新的验收图片', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_evidence_supplement_image_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const acceptance = {
|
||||||
|
...previous,
|
||||||
|
files: mergedEvidence.files,
|
||||||
|
imageUrls: mergedEvidence.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: mergedEvidence.addedFileCount,
|
||||||
|
totalImageCount: mergedEvidence.imageUrls.length,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id) })
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitWorkerSharingAcceptance(
|
||||||
|
workOrder: WorkOrderRow,
|
||||||
|
sharingShare: WorkOrderShareRow,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
if (workOrder.status === WORK_ORDER_STATUS.ACCEPTED) {
|
||||||
|
throw createHttpError('该拼单已验收完成', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_accepted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
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 && !canAutoAcceptWithoutEvidence) {
|
||||||
|
throw createHttpError('请上传验收图片', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_acceptance_image_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const acceptance = {
|
||||||
|
note,
|
||||||
|
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),
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
acceptanceJson: JSON.stringify(acceptance),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
if (result.failureReason === 'share_status_invalid') {
|
||||||
|
throw createHttpError('当前拼单份额已提交,请勿重复提交', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_share_submitted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (!result.share) {
|
||||||
|
throw createHttpError('未找到拼单份额', {
|
||||||
|
statusCode: 404,
|
||||||
|
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)],
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
share: mapWorkOrderShare(result.share),
|
||||||
|
sharingPendingCount: await countWorkOrderPendingSharingSubmissions(Number(workOrder.id)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 已验收拼单份额可追加验收图片,不改变订单状态和结算结果。 */
|
||||||
|
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)
|
||||||
|
const mergedEvidence = mergeAcceptanceEvidence(previous, payload.files)
|
||||||
|
if (!mergedEvidence.changed) {
|
||||||
|
throw createHttpError('请上传新的验收图片', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_sharing_evidence_supplement_image_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const acceptance = {
|
||||||
|
...previous,
|
||||||
|
files: mergedEvidence.files,
|
||||||
|
imageUrls: mergedEvidence.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: mergedEvidence.addedFileCount,
|
||||||
|
totalImageCount: mergedEvidence.imageUrls.length,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return { share: mapWorkOrderShare(updated || sharingShare) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 从提交或草稿载荷中解析验收图片与说明。 */
|
||||||
|
function resolveAcceptanceImages(payload: JsonObject, draftValue: unknown) {
|
||||||
|
const files = normalizeUploadedFiles(payload.files)
|
||||||
|
const imageUrls = [
|
||||||
|
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
||||||
|
...normalizeStringArray(payload.imageUrls),
|
||||||
|
]
|
||||||
|
if (imageUrls.length > 0) {
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
imageUrls: [...new Set(imageUrls)],
|
||||||
|
note: String(payload.note ?? '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const draft = safeParseJson(draftValue)
|
||||||
|
return {
|
||||||
|
files: normalizeUploadedFiles(draft.files),
|
||||||
|
imageUrls: [...new Set(normalizeStringArray(draft.imageUrls))],
|
||||||
|
note: String(payload.note ?? draft.note ?? '').trim(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 暂存验收图片或说明;已提交验收时更新已提交资料。 */
|
||||||
|
export async function saveWorkerOrderAcceptanceDraft(
|
||||||
|
workOrderId: number | string,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
requireActiveWorkerSession(session)
|
||||||
|
const workOrder = await getRequiredWorkOrder(workOrderId)
|
||||||
|
const workerId = Number(session.workerId)
|
||||||
|
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
||||||
|
if (sharingShare) {
|
||||||
|
return saveWorkerSharingAcceptanceDraft(workOrder, sharingShare, payload, session)
|
||||||
|
}
|
||||||
|
if (Number(workOrder.assigned_worker_id || 0) !== workerId) {
|
||||||
|
throw createHttpError('只能操作自己的订单', {
|
||||||
|
statusCode: 403,
|
||||||
|
errorCode: 'work_order_owner_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const { files, imageUrls } = resolveAcceptanceImages(payload, {})
|
||||||
|
const worker = await getRequiredWorker(session.workerId)
|
||||||
|
|
||||||
|
if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
||||||
|
if (imageUrls.length === 0) {
|
||||||
|
throw createHttpError('请至少保留一张验收图片', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_acceptance_image_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const previous = safeParseJson(workOrder.acceptance_json)
|
||||||
|
const acceptance = {
|
||||||
|
note: String(payload.note ?? previous.note ?? '').trim(),
|
||||||
|
files,
|
||||||
|
imageUrls: [...new Set(imageUrls)],
|
||||||
|
submittedAt: previous.submittedAt || workOrder.submitted_at || 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_updated',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({ imageCount: imageUrls.length }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(workOrder.status as never)
|
||||||
|
) {
|
||||||
|
throw createHttpError('当前状态不能保存验收图片', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_draft_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const draft = {
|
||||||
|
note: String(payload.note || '').trim(),
|
||||||
|
files,
|
||||||
|
imageUrls: [...new Set(imageUrls)],
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
const updated = await updateWorkOrderDraftAcceptance({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
draftJson: JSON.stringify(draft),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(session.workerId),
|
||||||
|
eventType: 'acceptance_draft_saved',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({ imageCount: imageUrls.length }),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function saveWorkerSharingAcceptanceDraft(
|
||||||
|
workOrder: WorkOrderRow,
|
||||||
|
sharingShare: WorkOrderShareRow,
|
||||||
|
payload: JsonObject = {},
|
||||||
|
session: WorkerSession,
|
||||||
|
) {
|
||||||
|
if (workOrder.status === WORK_ORDER_STATUS.ACCEPTED) {
|
||||||
|
throw createHttpError('该拼单已验收完成,不能再修改验收图片', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_accepted',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const now = nowIso()
|
||||||
|
const { files, imageUrls } = resolveAcceptanceImages(payload, {})
|
||||||
|
|
||||||
|
if (sharingShare.status === 'submitted') {
|
||||||
|
if (imageUrls.length === 0) {
|
||||||
|
throw createHttpError('请至少保留一张验收图片', {
|
||||||
|
statusCode: 400,
|
||||||
|
errorCode: 'work_order_acceptance_image_required',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const previous = safeParseJson(sharingShare.acceptance_json)
|
||||||
|
const acceptance = {
|
||||||
|
note: String(payload.note ?? previous.note ?? '').trim(),
|
||||||
|
files,
|
||||||
|
imageUrls: [...new Set(imageUrls)],
|
||||||
|
submittedAt: previous.submittedAt || sharingShare.submitted_at || now,
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
const share = 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_updated',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
imageCount: imageUrls.length,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (sharingShare.status !== 'joined') {
|
||||||
|
throw createHttpError('当前拼单状态不能保存验收图片', {
|
||||||
|
statusCode: 409,
|
||||||
|
errorCode: 'work_order_sharing_draft_status_invalid',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
const draft = {
|
||||||
|
note: String(payload.note || '').trim(),
|
||||||
|
files,
|
||||||
|
imageUrls: [...new Set(imageUrls)],
|
||||||
|
updatedAt: now,
|
||||||
|
}
|
||||||
|
const share = await updateWorkOrderShareDraftAcceptance({
|
||||||
|
workOrderId: Number(workOrder.id),
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
draftJson: JSON.stringify(draft),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await createWorkOrderEvent({
|
||||||
|
workOrderId: workOrder.id,
|
||||||
|
actorType: 'worker',
|
||||||
|
actorId: String(session.workerId),
|
||||||
|
eventType: 'sharing_acceptance_draft_saved',
|
||||||
|
fromStatus: workOrder.status,
|
||||||
|
toStatus: workOrder.status,
|
||||||
|
payloadJson: JSON.stringify({
|
||||||
|
workerId: Number(session.workerId),
|
||||||
|
imageCount: imageUrls.length,
|
||||||
|
}),
|
||||||
|
now,
|
||||||
|
})
|
||||||
|
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
||||||
|
workerIds: [Number(session.workerId)],
|
||||||
|
})
|
||||||
|
return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) }
|
||||||
|
}
|
||||||
|
|
||||||
|
function mergeAcceptanceEvidence(previous: Record<string, unknown>, uploadedValue: unknown) {
|
||||||
|
const existingFiles = normalizeUploadedFiles(previous.files)
|
||||||
|
const uploadedFiles = normalizeUploadedFiles(uploadedValue)
|
||||||
|
const files = [...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)
|
||||||
|
files.push(file)
|
||||||
|
}
|
||||||
|
const previousImageUrls = normalizeStringArray(previous.imageUrls)
|
||||||
|
const imageUrls = [
|
||||||
|
...new Set([
|
||||||
|
...previousImageUrls,
|
||||||
|
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
||||||
|
]),
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
files,
|
||||||
|
imageUrls,
|
||||||
|
addedFileCount: files.length - existingFiles.length,
|
||||||
|
changed: files.length !== existingFiles.length || imageUrls.length !== previousImageUrls.length,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,13 @@
|
|||||||
import { runtimeConfig } from '../../config/runtime.js'
|
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,
|
|
||||||
acceptWorkOrderShareAndSettle,
|
|
||||||
createWorkerCancelRequest,
|
createWorkerCancelRequest,
|
||||||
createWorkerOrderFeedback,
|
createWorkerOrderFeedback,
|
||||||
countWorkerActiveOrders,
|
countWorkerActiveOrders,
|
||||||
countWorkOrderPendingSharingSubmissions,
|
|
||||||
consumeWorkerSmsCode,
|
consumeWorkerSmsCode,
|
||||||
createWorkOrderEvent,
|
createWorkOrderEvent,
|
||||||
getLatestWorkerSmsCode,
|
getLatestWorkerSmsCode,
|
||||||
recordWorkerSmsCodeFailure,
|
recordWorkerSmsCodeFailure,
|
||||||
settleOverdueWorkOrder,
|
|
||||||
getWorkerUserById,
|
getWorkerUserById,
|
||||||
getWorkerUserByPhone,
|
getWorkerUserByPhone,
|
||||||
getWorkerUserByUsername,
|
getWorkerUserByUsername,
|
||||||
@@ -34,24 +30,16 @@ import {
|
|||||||
listWorkOrders,
|
listWorkOrders,
|
||||||
listWorkOrderEventsByOrderId,
|
listWorkOrderEventsByOrderId,
|
||||||
getWorkerCancelRequestRisk,
|
getWorkerCancelRequestRisk,
|
||||||
submitWorkOrderShareAcceptance,
|
|
||||||
updateWorkOrder,
|
|
||||||
updateWorkOrderAcceptanceRecord,
|
|
||||||
updateWorkOrderDraftAcceptance,
|
|
||||||
updateWorkOrderShareAcceptanceRecord,
|
|
||||||
updateWorkOrderShareDraftAcceptance,
|
|
||||||
upsertWorkerWorkOrderNote,
|
upsertWorkerWorkOrderNote,
|
||||||
markWorkerWorkOrderViewed,
|
markWorkerWorkOrderViewed,
|
||||||
type WorkOrderRow,
|
type WorkOrderRow,
|
||||||
type WorkOrderShareRow,
|
|
||||||
type WorkerSessionRow,
|
type WorkerSessionRow,
|
||||||
} from '../../repositories/worker-platform/index.js'
|
} from '../../repositories/worker-platform/index.js'
|
||||||
import type { JsonObject } from '../../types/json.js'
|
import type { JsonObject } from '../../types/json.js'
|
||||||
import { createHttpError } from '../../utils/http.js'
|
import { createHttpError } from '../../utils/http.js'
|
||||||
import { addHours, nowIso } from '../../utils/time.js'
|
import { nowIso } from '../../utils/time.js'
|
||||||
import { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.js'
|
||||||
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
import { getWorkerAnnouncementConfig } from './worker-announcement-config-service.js'
|
||||||
import { getWorkerFinanceConfig } from './worker-finance-config-service.js'
|
|
||||||
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
import { getWorkerHallConfig } from './worker-hall-config-service.js'
|
||||||
import {
|
import {
|
||||||
getRequiredWorkOrder,
|
getRequiredWorkOrder,
|
||||||
@@ -60,11 +48,8 @@ import {
|
|||||||
type WorkerSession,
|
type WorkerSession,
|
||||||
} from './worker-session-context-service.js'
|
} from './worker-session-context-service.js'
|
||||||
import {
|
import {
|
||||||
createWorkerAcceptanceAdminNotification,
|
|
||||||
createWorkerCancelRequestAdminNotification,
|
createWorkerCancelRequestAdminNotification,
|
||||||
createWorkerOrderFeedbackAdminNotification,
|
createWorkerOrderFeedbackAdminNotification,
|
||||||
getWorkerAcceptanceReminder,
|
|
||||||
remindWorkerAcceptanceAdminNotification,
|
|
||||||
} from '../admin/admin-notification-service.js'
|
} from '../admin/admin-notification-service.js'
|
||||||
import {
|
import {
|
||||||
publishWorkerWalletRealtimeChange,
|
publishWorkerWalletRealtimeChange,
|
||||||
@@ -81,11 +66,8 @@ import {
|
|||||||
normalizeOptionalId,
|
normalizeOptionalId,
|
||||||
normalizePositiveInteger,
|
normalizePositiveInteger,
|
||||||
normalizeStatuses,
|
normalizeStatuses,
|
||||||
normalizeStringArray,
|
|
||||||
normalizeUploadedFiles,
|
|
||||||
resolveFreezeDepositAmount,
|
resolveFreezeDepositAmount,
|
||||||
resolveWorkerPermissions,
|
resolveWorkerPermissions,
|
||||||
canWorkerAutoAcceptWholeOrder,
|
|
||||||
throwGrabWorkOrderFailure,
|
throwGrabWorkOrderFailure,
|
||||||
} from './mappers.js'
|
} from './mappers.js'
|
||||||
import {
|
import {
|
||||||
@@ -119,8 +101,6 @@ import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
|||||||
import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js'
|
import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js'
|
||||||
import { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
import { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
||||||
|
|
||||||
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
getRequiredWorkOrder,
|
getRequiredWorkOrder,
|
||||||
getRequiredWorker,
|
getRequiredWorker,
|
||||||
@@ -638,733 +618,12 @@ function resolveWorkOrderShareJoinError(
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function submitWorkerOrderAcceptance(
|
export {
|
||||||
workOrderId: number | string,
|
remindWorkerOrderAcceptance,
|
||||||
payload: JsonObject = {},
|
saveWorkerOrderAcceptanceDraft,
|
||||||
session: WorkerSession,
|
submitWorkerOrderAcceptance,
|
||||||
) {
|
supplementWorkerAcceptedOrderEvidence,
|
||||||
requireActiveWorkerSession(session)
|
} from './worker-acceptance-service.js'
|
||||||
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 = canWorkerAutoAcceptWholeOrder({
|
|
||||||
worker,
|
|
||||||
hasPersonalSharingShare: false,
|
|
||||||
})
|
|
||||||
const now = nowIso()
|
|
||||||
if (
|
|
||||||
workOrder.status === WORK_ORDER_STATUS.IN_PROGRESS &&
|
|
||||||
workOrder.deadline_at &&
|
|
||||||
new Date(workOrder.deadline_at).getTime() <= Date.now()
|
|
||||||
) {
|
|
||||||
await settleOverdueWorkOrder({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
policy: normalizeWorkOrderTimeoutPolicy(workOrder.timeout_policy),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
throw createHttpError('任务已超时,系统已按超时策略处置', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_timeout_expired',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (Number(workOrder.assigned_worker_id || 0) !== workerId) {
|
|
||||||
throw createHttpError('只能提交自己的订单', {
|
|
||||||
statusCode: 403,
|
|
||||||
errorCode: 'work_order_owner_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(workOrder.status as never)
|
|
||||||
) {
|
|
||||||
throw createHttpError('当前状态不能提交验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_submit_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const { files, imageUrls, note } = resolveAcceptanceImages(
|
|
||||||
payload,
|
|
||||||
workOrder.draft_acceptance_json,
|
|
||||||
)
|
|
||||||
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) {
|
|
||||||
throw createHttpError('请上传验收图片', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_acceptance_image_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const acceptance = {
|
|
||||||
note,
|
|
||||||
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,
|
|
||||||
acceptance_json: acceptance,
|
|
||||||
draft_acceptance_json: {},
|
|
||||||
submitted_at: now,
|
|
||||||
updated_at: now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(session.workerId),
|
|
||||||
eventType: 'acceptance_submitted',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
imageCount: imageUrls.length,
|
|
||||||
autoAccepted: canAutoAcceptWithoutEvidence,
|
|
||||||
evidenceDueAt: acceptance.evidenceDueAt || null,
|
|
||||||
}),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
if (worker.worker_type === 'internal' || canAutoAcceptWithoutEvidence) {
|
|
||||||
const { order: acceptedOrder, failureReason } = await acceptWorkOrderAndSettle({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
unfreezeDays: getWorkerFinanceConfig().depositUnfreezeDays,
|
|
||||||
now,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorName: worker.display_name || worker.username || '',
|
|
||||||
})
|
|
||||||
if (failureReason === 'sharing_pending_submissions') {
|
|
||||||
throw createHttpError('还有拼单打手未提交验收资料,暂不能统一验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_pending_submissions',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!acceptedOrder) {
|
|
||||||
throw createHttpError('免审核验收失败,请稍后重试', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_internal_auto_accept_failed',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(session.workerId),
|
|
||||||
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,
|
|
||||||
evidenceStatus: acceptance.evidenceStatus || 'complete',
|
|
||||||
evidenceDueAt: acceptance.evidenceDueAt || null,
|
|
||||||
}),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: [Number(worker.id)],
|
|
||||||
})
|
|
||||||
publishWorkerWalletRealtimeChange(Number(worker.id))
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderForWorker(acceptedOrder, resolveWorkerPermissions(worker)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await createWorkerAcceptanceAdminNotification({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workOrderNo: workOrder.work_order_no,
|
|
||||||
productName: workOrder.product_name,
|
|
||||||
workerName: worker.display_name || worker.username,
|
|
||||||
imageCount: imageUrls.length,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerIds: [Number(worker.id)],
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 打手在提交验收后催促后台处理;频率和次数均由服务端控制。 */
|
|
||||||
export async function remindWorkerOrderAcceptance(
|
|
||||||
workOrderId: number | string,
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
if (
|
|
||||||
workOrder.sharing_enabled ||
|
|
||||||
Number(workOrder.assigned_worker_id || 0) !== Number(session.workerId)
|
|
||||||
) {
|
|
||||||
throw createHttpError('只能催促自己提交的整单验收', {
|
|
||||||
statusCode: 403,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_owner_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (workOrder.status !== WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
|
||||||
throw createHttpError('当前订单无需催验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const submittedAt = Date.parse(String(workOrder.submitted_at || ''))
|
|
||||||
const now = Date.now()
|
|
||||||
const firstReminderAt = submittedAt + 10 * 60 * 1000
|
|
||||||
if (!Number.isFinite(submittedAt) || now < firstReminderAt) {
|
|
||||||
throw createHttpError('提交验收满 10 分钟后才可催验收', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_too_early',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let notification = await getWorkerAcceptanceReminder(Number(workOrder.id))
|
|
||||||
if (!notification) {
|
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
|
||||||
await createWorkerAcceptanceAdminNotification({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workOrderNo: workOrder.work_order_no,
|
|
||||||
productName: workOrder.product_name,
|
|
||||||
workerName: worker.display_name || worker.username,
|
|
||||||
imageCount: normalizeStringArray(safeParseJson(workOrder.acceptance_json).imageUrls).length,
|
|
||||||
})
|
|
||||||
notification = await getWorkerAcceptanceReminder(Number(workOrder.id))
|
|
||||||
}
|
|
||||||
if (!notification) {
|
|
||||||
throw createHttpError('催验收待办创建失败,请稍后重试', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_notification_missing',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (Number(notification.reminder_count || 0) >= 3) {
|
|
||||||
throw createHttpError('该订单最多催验收 3 次', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_limit_reached',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const lastRemindedAt = Date.parse(String(notification.last_reminded_at || ''))
|
|
||||||
const nextRemindAt = lastRemindedAt + 30 * 60 * 1000
|
|
||||||
if (Number.isFinite(lastRemindedAt) && now < nextRemindAt) {
|
|
||||||
throw createHttpError('每 30 分钟可催验收一次,请稍后再试', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_cooldown',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const reminded = await remindWorkerAcceptanceAdminNotification(Number(workOrder.id))
|
|
||||||
if (!reminded) {
|
|
||||||
throw createHttpError('订单状态已变化,请刷新后重试', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_acceptance_reminder_conflict',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const remindedAt = new Date(reminded.last_reminded_at || new Date().toISOString()).getTime()
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(session.workerId),
|
|
||||||
eventType: 'acceptance_reminded',
|
|
||||||
fromStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
||||||
toStatus: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
|
|
||||||
payloadJson: JSON.stringify({ reminderCount: reminded.reminder_count }),
|
|
||||||
now: new Date(reminded.last_reminded_at || new Date().toISOString()).toISOString(),
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
reminderCount: Number(reminded.reminder_count || 0),
|
|
||||||
nextRemindAt: new Date(remindedAt + 30 * 60 * 1000).toISOString(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 已验收订单可追加验收图片,不改变订单状态和结算结果。 */
|
|
||||||
export async function supplementWorkerAcceptedOrderEvidence(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
|
||||||
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('只能补充自己的订单验收图片', {
|
|
||||||
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)
|
|
||||||
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,
|
|
||||||
})
|
|
||||||
publishWorkOrderRealtimeChange({ workOrderId: Number(workOrder.id) })
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function submitWorkerSharingAcceptance(
|
|
||||||
workOrder: WorkOrderRow,
|
|
||||||
sharingShare: WorkOrderShareRow,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
if (workOrder.status === WORK_ORDER_STATUS.ACCEPTED) {
|
|
||||||
throw createHttpError('该拼单已验收完成', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_accepted',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
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 && !canAutoAcceptWithoutEvidence) {
|
|
||||||
throw createHttpError('请上传验收图片', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_acceptance_image_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const acceptance = {
|
|
||||||
note,
|
|
||||||
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),
|
|
||||||
workerId: Number(session.workerId),
|
|
||||||
acceptanceJson: JSON.stringify(acceptance),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
if (result.failureReason === 'share_status_invalid') {
|
|
||||||
throw createHttpError('当前拼单份额已提交,请勿重复提交', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_share_submitted',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if (!result.share) {
|
|
||||||
throw createHttpError('未找到拼单份额', {
|
|
||||||
statusCode: 404,
|
|
||||||
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)],
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
share: mapWorkOrderShare(result.share),
|
|
||||||
sharingPendingCount: await countWorkOrderPendingSharingSubmissions(Number(workOrder.id)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 已验收拼单份额可追加验收图片,不改变订单状态和结算结果。 */
|
|
||||||
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)
|
|
||||||
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) }
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 从提交/保存验收的载荷中解析图片与说明。
|
|
||||||
* 载荷未带图片时回退到暂存草稿(上传图片与提交验收分离:先暂存、后提交)。
|
|
||||||
*/
|
|
||||||
function resolveAcceptanceImages(payload: JsonObject, draftValue: unknown) {
|
|
||||||
const files = normalizeUploadedFiles(payload.files)
|
|
||||||
const imageUrls = [
|
|
||||||
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
|
||||||
...normalizeStringArray(payload.imageUrls),
|
|
||||||
]
|
|
||||||
if (imageUrls.length > 0) {
|
|
||||||
return {
|
|
||||||
files,
|
|
||||||
imageUrls: [...new Set(imageUrls)],
|
|
||||||
note: String(payload.note ?? '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const draft = safeParseJson(draftValue)
|
|
||||||
return {
|
|
||||||
files: normalizeUploadedFiles(draft.files),
|
|
||||||
imageUrls: [...new Set(normalizeStringArray(draft.imageUrls))],
|
|
||||||
note: String(payload.note ?? draft.note ?? '').trim(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 暂存验收图片/说明(草稿)。
|
|
||||||
* - 未提交(in_progress / problem / 拼单 joined):写入草稿,不改变工单状态;
|
|
||||||
* - 已提交(pending_acceptance / 拼单 submitted):直接更新已提交的验收资料(保留提交时间)。
|
|
||||||
*/
|
|
||||||
export async function saveWorkerOrderAcceptanceDraft(
|
|
||||||
workOrderId: number | string,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
requireActiveWorkerSession(session)
|
|
||||||
const workOrder = await getRequiredWorkOrder(workOrderId)
|
|
||||||
const workerId = Number(session.workerId)
|
|
||||||
const sharingShare = await getWorkOrderShare(workOrder.id, workerId)
|
|
||||||
if (sharingShare) {
|
|
||||||
return saveWorkerSharingAcceptanceDraft(workOrder, sharingShare, payload, session)
|
|
||||||
}
|
|
||||||
if (Number(workOrder.assigned_worker_id || 0) !== workerId) {
|
|
||||||
throw createHttpError('只能操作自己的订单', {
|
|
||||||
statusCode: 403,
|
|
||||||
errorCode: 'work_order_owner_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const files = normalizeUploadedFiles(payload.files)
|
|
||||||
const imageUrls = [
|
|
||||||
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
|
||||||
...normalizeStringArray(payload.imageUrls),
|
|
||||||
]
|
|
||||||
const worker = await getRequiredWorker(session.workerId)
|
|
||||||
|
|
||||||
if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
|
|
||||||
if (imageUrls.length === 0) {
|
|
||||||
throw createHttpError('请至少保留一张验收图片', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_acceptance_image_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const previous = safeParseJson(workOrder.acceptance_json)
|
|
||||||
const acceptance = {
|
|
||||||
note: String(payload.note ?? previous.note ?? '').trim(),
|
|
||||||
files,
|
|
||||||
imageUrls: [...new Set(imageUrls)],
|
|
||||||
submittedAt: previous.submittedAt || workOrder.submitted_at || 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_updated',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify({ imageCount: imageUrls.length }),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
|
||||||
workerIds: [Number(session.workerId)],
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
![WORK_ORDER_STATUS.IN_PROGRESS, WORK_ORDER_STATUS.PROBLEM].includes(workOrder.status as never)
|
|
||||||
) {
|
|
||||||
throw createHttpError('当前状态不能保存验收图片', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_draft_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const draft = {
|
|
||||||
note: String(payload.note || '').trim(),
|
|
||||||
files,
|
|
||||||
imageUrls: [...new Set(imageUrls)],
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
const updated = await updateWorkOrderDraftAcceptance({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
draftJson: JSON.stringify(draft),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(session.workerId),
|
|
||||||
eventType: 'acceptance_draft_saved',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify({ imageCount: imageUrls.length }),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
|
||||||
workerIds: [Number(session.workerId)],
|
|
||||||
})
|
|
||||||
return {
|
|
||||||
order: mapWorkOrderForWorker(updated || workOrder, resolveWorkerPermissions(worker)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveWorkerSharingAcceptanceDraft(
|
|
||||||
workOrder: WorkOrderRow,
|
|
||||||
sharingShare: WorkOrderShareRow,
|
|
||||||
payload: JsonObject = {},
|
|
||||||
session: WorkerSession,
|
|
||||||
) {
|
|
||||||
if (workOrder.status === WORK_ORDER_STATUS.ACCEPTED) {
|
|
||||||
throw createHttpError('该拼单已验收完成,不能再修改验收图片', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_accepted',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const now = nowIso()
|
|
||||||
const files = normalizeUploadedFiles(payload.files)
|
|
||||||
const imageUrls = [
|
|
||||||
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
|
|
||||||
...normalizeStringArray(payload.imageUrls),
|
|
||||||
]
|
|
||||||
|
|
||||||
if (sharingShare.status === 'submitted') {
|
|
||||||
if (imageUrls.length === 0) {
|
|
||||||
throw createHttpError('请至少保留一张验收图片', {
|
|
||||||
statusCode: 400,
|
|
||||||
errorCode: 'work_order_acceptance_image_required',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const previous = safeParseJson(sharingShare.acceptance_json)
|
|
||||||
const acceptance = {
|
|
||||||
note: String(payload.note ?? previous.note ?? '').trim(),
|
|
||||||
files,
|
|
||||||
imageUrls: [...new Set(imageUrls)],
|
|
||||||
submittedAt: previous.submittedAt || sharingShare.submitted_at || now,
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
const share = 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_updated',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
workerId: Number(session.workerId),
|
|
||||||
imageCount: imageUrls.length,
|
|
||||||
}),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
|
||||||
workerIds: [Number(session.workerId)],
|
|
||||||
})
|
|
||||||
return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sharingShare.status !== 'joined') {
|
|
||||||
throw createHttpError('当前拼单状态不能保存验收图片', {
|
|
||||||
statusCode: 409,
|
|
||||||
errorCode: 'work_order_sharing_draft_status_invalid',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
const draft = {
|
|
||||||
note: String(payload.note || '').trim(),
|
|
||||||
files,
|
|
||||||
imageUrls: [...new Set(imageUrls)],
|
|
||||||
updatedAt: now,
|
|
||||||
}
|
|
||||||
const share = await updateWorkOrderShareDraftAcceptance({
|
|
||||||
workOrderId: Number(workOrder.id),
|
|
||||||
workerId: Number(session.workerId),
|
|
||||||
draftJson: JSON.stringify(draft),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
await createWorkOrderEvent({
|
|
||||||
workOrderId: workOrder.id,
|
|
||||||
actorType: 'worker',
|
|
||||||
actorId: String(session.workerId),
|
|
||||||
eventType: 'sharing_acceptance_draft_saved',
|
|
||||||
fromStatus: workOrder.status,
|
|
||||||
toStatus: workOrder.status,
|
|
||||||
payloadJson: JSON.stringify({
|
|
||||||
workerId: Number(session.workerId),
|
|
||||||
imageCount: imageUrls.length,
|
|
||||||
}),
|
|
||||||
now,
|
|
||||||
})
|
|
||||||
await publishWorkOrderParticipantChange(Number(workOrder.id), {
|
|
||||||
workerIds: [Number(session.workerId)],
|
|
||||||
})
|
|
||||||
return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) }
|
|
||||||
}
|
|
||||||
|
|
||||||
export {
|
export {
|
||||||
collectLookupWorkOrder,
|
collectLookupWorkOrder,
|
||||||
|
|||||||
Reference in New Issue
Block a user