feat: 支持验收图片暂存

This commit is contained in:
yml2213
2026-08-15 14:20:58 +08:00
parent 21211bd760
commit af323af48e
9 changed files with 599 additions and 75 deletions
@@ -394,6 +394,7 @@ export function mapWorkOrderAdmin(workOrder: WorkOrderRow) {
fields: resolveRequirementFields(workOrder),
},
acceptance: mapAcceptanceForResponse(workOrder.acceptance_json),
draftAcceptance: mapAcceptanceForResponse(workOrder.draft_acceptance_json),
problemNote: workOrder.problem_note || '',
worker: workOrder.assigned_worker_id
? {
@@ -443,6 +444,7 @@ export function mapWorkOrderShare(share: WorkOrderShareRow) {
shareDeposit: Number(share.share_deposit || 0),
status: share.status,
acceptance: safeParseJson(share.acceptance_json),
draftAcceptance: safeParseJson(share.draft_acceptance_json),
submittedAt: share.submitted_at,
acceptedAt: share.accepted_at,
createdAt: share.created_at,
@@ -63,6 +63,10 @@ import {
settleOverdueWorkOrder,
submitWorkOrderShareAcceptance,
updateWorkOrder,
updateWorkOrderAcceptanceRecord,
updateWorkOrderDraftAcceptance,
updateWorkOrderShareAcceptanceRecord,
updateWorkOrderShareDraftAcceptance,
updateWorkerPassword,
updateWorkerUser,
upsertWorkCategory,
@@ -1111,11 +1115,10 @@ export async function submitWorkerOrderAcceptance(
})
}
const files = normalizeUploadedFiles(payload.files)
const imageUrls = [
...files.map((file) => file.url || file.mediumUrl || file.thumbnailUrl).filter(Boolean),
...normalizeStringArray(payload.imageUrls),
]
const { files, imageUrls, note } = resolveAcceptanceImages(
payload,
workOrder.draft_acceptance_json,
)
if (imageUrls.length === 0) {
throw createHttpError('请上传验收图片', {
statusCode: 400,
@@ -1123,7 +1126,7 @@ export async function submitWorkerOrderAcceptance(
})
}
const acceptance = {
note: String(payload.note || '').trim(),
note,
files,
imageUrls: [...new Set(imageUrls)],
submittedAt: now,
@@ -1131,6 +1134,7 @@ export async function submitWorkerOrderAcceptance(
const updated = await updateWorkOrder(workOrder.id, {
status: WORK_ORDER_STATUS.PENDING_ACCEPTANCE,
acceptance_json: acceptance,
draft_acceptance_json: {},
submitted_at: now,
updated_at: now,
})
@@ -1198,11 +1202,10 @@ async function submitWorkerSharingAcceptance(
})
}
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 { files, imageUrls, note } = resolveAcceptanceImages(
payload,
sharingShare.draft_acceptance_json,
)
if (imageUrls.length === 0) {
throw createHttpError('请上传验收图片', {
statusCode: 400,
@@ -1210,7 +1213,7 @@ async function submitWorkerSharingAcceptance(
})
}
const acceptance = {
note: String(payload.note || '').trim(),
note,
files,
imageUrls: [...new Set(imageUrls)],
submittedAt: now,
@@ -1239,6 +1242,221 @@ async function submitWorkerSharingAcceptance(
}
}
/**
* 从提交/保存验收的载荷中解析图片与说明。
* 载荷未带图片时回退到暂存草稿(上传图片与提交验收分离:先暂存、后提交)。
*/
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,
})
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,
})
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,
})
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,
})
return { share: share ? mapWorkOrderShare(share) : mapWorkOrderShare(sharingShare) }
}
export async function collectLookupWorkOrder(payload: JsonObject = {}) {
const orderNo = String(payload.orderNo || payload.platformOrderId || '').trim()
if (!orderNo) {