拆分打手验收服务
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-realtime-service.js'
|
||||
export * from './worker-collect-service.js'
|
||||
export * from './worker-acceptance-service.js'
|
||||
export * from './worker-platform-defaults.js'
|
||||
export * from './work-order-timeout-policy.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 { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
|
||||
import {
|
||||
acceptWorkOrderAndSettle,
|
||||
acceptWorkOrderShareAndSettle,
|
||||
createWorkerCancelRequest,
|
||||
createWorkerOrderFeedback,
|
||||
countWorkerActiveOrders,
|
||||
countWorkOrderPendingSharingSubmissions,
|
||||
consumeWorkerSmsCode,
|
||||
createWorkOrderEvent,
|
||||
getLatestWorkerSmsCode,
|
||||
recordWorkerSmsCodeFailure,
|
||||
settleOverdueWorkOrder,
|
||||
getWorkerUserById,
|
||||
getWorkerUserByPhone,
|
||||
getWorkerUserByUsername,
|
||||
@@ -34,24 +30,16 @@ import {
|
||||
listWorkOrders,
|
||||
listWorkOrderEventsByOrderId,
|
||||
getWorkerCancelRequestRisk,
|
||||
submitWorkOrderShareAcceptance,
|
||||
updateWorkOrder,
|
||||
updateWorkOrderAcceptanceRecord,
|
||||
updateWorkOrderDraftAcceptance,
|
||||
updateWorkOrderShareAcceptanceRecord,
|
||||
updateWorkOrderShareDraftAcceptance,
|
||||
upsertWorkerWorkOrderNote,
|
||||
markWorkerWorkOrderViewed,
|
||||
type WorkOrderRow,
|
||||
type WorkOrderShareRow,
|
||||
type WorkerSessionRow,
|
||||
} 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 { normalizePage, normalizePageSize, safeParseJson } from '../admin/admin-query-utils.js'
|
||||
import { nowIso } from '../../utils/time.js'
|
||||
import { normalizePage, normalizePageSize } from '../admin/admin-query-utils.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 {
|
||||
getRequiredWorkOrder,
|
||||
@@ -60,11 +48,8 @@ import {
|
||||
type WorkerSession,
|
||||
} from './worker-session-context-service.js'
|
||||
import {
|
||||
createWorkerAcceptanceAdminNotification,
|
||||
createWorkerCancelRequestAdminNotification,
|
||||
createWorkerOrderFeedbackAdminNotification,
|
||||
getWorkerAcceptanceReminder,
|
||||
remindWorkerAcceptanceAdminNotification,
|
||||
} from '../admin/admin-notification-service.js'
|
||||
import {
|
||||
publishWorkerWalletRealtimeChange,
|
||||
@@ -81,11 +66,8 @@ import {
|
||||
normalizeOptionalId,
|
||||
normalizePositiveInteger,
|
||||
normalizeStatuses,
|
||||
normalizeStringArray,
|
||||
normalizeUploadedFiles,
|
||||
resolveFreezeDepositAmount,
|
||||
resolveWorkerPermissions,
|
||||
canWorkerAutoAcceptWholeOrder,
|
||||
throwGrabWorkOrderFailure,
|
||||
} from './mappers.js'
|
||||
import {
|
||||
@@ -119,8 +101,6 @@ import { normalizeWorkOrderTimeoutPolicy } from './work-order-timeout-policy.js'
|
||||
import { publishWorkOrderParticipantChange } from './worker-order-realtime-service.js'
|
||||
import { settleOverdueWorkOrders } from './worker-order-timeout-service.js'
|
||||
|
||||
const VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS = 24
|
||||
|
||||
export {
|
||||
getRequiredWorkOrder,
|
||||
getRequiredWorker,
|
||||
@@ -638,733 +618,12 @@ function resolveWorkOrderShareJoinError(
|
||||
})
|
||||
}
|
||||
|
||||
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 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 {
|
||||
remindWorkerOrderAcceptance,
|
||||
saveWorkerOrderAcceptanceDraft,
|
||||
submitWorkerOrderAcceptance,
|
||||
supplementWorkerAcceptedOrderEvidence,
|
||||
} from './worker-acceptance-service.js'
|
||||
|
||||
export {
|
||||
collectLookupWorkOrder,
|
||||
|
||||
Reference in New Issue
Block a user