fix:打手端/我的订单/验收:验收视频和验收图片至少上传一个就好了,其他逻辑按照上传图片走

This commit is contained in:
2026-10-25 16:45:38 +08:00
parent 6993c36efb
commit 60fe00b28c
4 changed files with 55 additions and 34 deletions
@@ -77,6 +77,8 @@ export async function createWorkerAcceptanceAdminNotification(input: {
productName: string productName: string
workerName: string workerName: string
imageCount: number imageCount: number
/** 是否提交了验收视频:视频与图片至少一个,通知里要体现,避免显示成“0 张图片”。 */
hasVideo?: boolean
/** 拼单份额:每份提交各生成一条待办(按 shareId 去重) */ /** 拼单份额:每份提交各生成一条待办(按 shareId 去重) */
shareId?: number | null shareId?: number | null
}) { }) {
@@ -97,6 +99,7 @@ export async function createWorkerAcceptanceAdminNotification(input: {
`打手:${input.workerName || '-'}`, `打手:${input.workerName || '-'}`,
...(shareId > 0 ? [`份额:#${shareId}`] : []), ...(shareId > 0 ? [`份额:#${shareId}`] : []),
`验收图片:${input.imageCount} 张`, `验收图片:${input.imageCount} 张`,
...(input.hasVideo ? ['验收视频:已上传'] : []),
].join(';'), ].join(';'),
actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=orders`, actionPath: `${ADMIN_WORKER_PLATFORM_PATH}?tab=orders`,
now: new Date().toISOString(), now: new Date().toISOString(),
@@ -1,3 +1,4 @@
import { hasAcceptanceEvidence } from '../../domain/work-order-acceptance-evidence.js'
import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js' import { WORK_ORDER_STATUS } from '../../domain/work-order-status.js'
import { import {
isFriendGiftAcceptanceMode, isFriendGiftAcceptanceMode,
@@ -155,8 +156,10 @@ export async function submitWorkerOrderAcceptance(
workOrder.draft_acceptance_json, workOrder.draft_acceptance_json,
) )
const video = resolveAcceptanceVideo(payload, workOrder.draft_acceptance_json) const video = resolveAcceptanceVideo(payload, workOrder.draft_acceptance_json)
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) { // 验收图片与验收视频至少上传一个;有视频时视同有图片,后续口径完全一致。
throw createHttpError('请上传验收图片', { const hasEvidence = hasAcceptanceEvidence({ imageUrls, video })
if (!hasEvidence && !canAutoAcceptWithoutEvidence) {
throw createHttpError('请上传验收图片或验收视频', {
statusCode: 400, statusCode: 400,
errorCode: 'work_order_acceptance_image_required', errorCode: 'work_order_acceptance_image_required',
}) })
@@ -170,9 +173,8 @@ export async function submitWorkerOrderAcceptance(
...(canAutoAcceptWithoutEvidence ...(canAutoAcceptWithoutEvidence
? { ? {
reviewMode: 'vip_auto', reviewMode: 'vip_auto',
evidenceStatus: imageUrls.length > 0 ? 'complete' : 'pending', evidenceStatus: hasEvidence ? 'complete' : 'pending',
evidenceDueAt: evidenceDueAt: hasEvidence ? null : addHours(now, VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS),
imageUrls.length > 0 ? null : addHours(now, VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS),
} }
: {}), : {}),
} }
@@ -252,6 +254,7 @@ export async function submitWorkerOrderAcceptance(
productName: workOrder.product_name, productName: workOrder.product_name,
workerName: worker.display_name || worker.username, workerName: worker.display_name || worker.username,
imageCount: imageUrls.length, imageCount: imageUrls.length,
hasVideo: Boolean(video),
}) })
publishWorkOrderRealtimeChange({ publishWorkOrderRealtimeChange({
workOrderId: Number(workOrder.id), workOrderId: Number(workOrder.id),
@@ -303,6 +306,7 @@ export async function remindWorkerOrderAcceptance(
productName: workOrder.product_name, productName: workOrder.product_name,
workerName: worker.display_name || worker.username, workerName: worker.display_name || worker.username,
imageCount: normalizeStringArray(safeParseJson(workOrder.acceptance_json).imageUrls).length, imageCount: normalizeStringArray(safeParseJson(workOrder.acceptance_json).imageUrls).length,
hasVideo: Boolean(safeParseJson(workOrder.acceptance_json).video),
}) })
notification = await getWorkerAcceptanceReminder(Number(workOrder.id)) notification = await getWorkerAcceptanceReminder(Number(workOrder.id))
} }
@@ -396,7 +400,7 @@ export async function supplementWorkerAcceptedOrderEvidence(
const payloadVideo = resolveAcceptanceVideoFromPayload(payload) const payloadVideo = resolveAcceptanceVideoFromPayload(payload)
const video = payloadVideo || readAcceptanceVideoLenient(previous) const video = payloadVideo || readAcceptanceVideoLenient(previous)
if (!mergedEvidence.changed && !payloadVideo) { if (!mergedEvidence.changed && !payloadVideo) {
throw createHttpError('请上传新的验收图片', { throw createHttpError('请上传新的验收图片或新的验收视频', {
statusCode: 400, statusCode: 400,
errorCode: 'work_order_evidence_supplement_image_required', errorCode: 'work_order_evidence_supplement_image_required',
}) })
@@ -485,8 +489,10 @@ async function submitWorkerSharingAcceptance(
sharingShare.draft_acceptance_json, sharingShare.draft_acceptance_json,
) )
const video = resolveAcceptanceVideo(payload, sharingShare.draft_acceptance_json) const video = resolveAcceptanceVideo(payload, sharingShare.draft_acceptance_json)
if (imageUrls.length === 0 && !canAutoAcceptWithoutEvidence) { // 验收图片与验收视频至少上传一个;有视频时视同有图片,后续口径完全一致。
throw createHttpError('请上传验收图片', { const hasEvidence = hasAcceptanceEvidence({ imageUrls, video })
if (!hasEvidence && !canAutoAcceptWithoutEvidence) {
throw createHttpError('请上传验收图片或验收视频', {
statusCode: 400, statusCode: 400,
errorCode: 'work_order_acceptance_image_required', errorCode: 'work_order_acceptance_image_required',
}) })
@@ -500,9 +506,8 @@ async function submitWorkerSharingAcceptance(
...(canAutoAcceptWithoutEvidence ...(canAutoAcceptWithoutEvidence
? { ? {
reviewMode: 'vip_auto', reviewMode: 'vip_auto',
evidenceStatus: imageUrls.length > 0 ? 'complete' : 'pending', evidenceStatus: hasEvidence ? 'complete' : 'pending',
evidenceDueAt: evidenceDueAt: hasEvidence ? null : addHours(now, VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS),
imageUrls.length > 0 ? null : addHours(now, VIP_AUTO_ACCEPT_EVIDENCE_WINDOW_HOURS),
} }
: {}), : {}),
} }
@@ -562,6 +567,7 @@ async function submitWorkerSharingAcceptance(
productName: workOrder.product_name, productName: workOrder.product_name,
workerName: worker.display_name || worker.username, workerName: worker.display_name || worker.username,
imageCount: acceptance.imageUrls.length, imageCount: acceptance.imageUrls.length,
hasVideo: Boolean(acceptance.video),
shareId: Number(sharingShare.id), shareId: Number(sharingShare.id),
}) })
await publishWorkOrderParticipantChange(Number(workOrder.id), { await publishWorkOrderParticipantChange(Number(workOrder.id), {
@@ -593,7 +599,7 @@ async function supplementWorkerAcceptedSharingEvidence(
const payloadVideo = resolveAcceptanceVideoFromPayload(payload) const payloadVideo = resolveAcceptanceVideoFromPayload(payload)
const video = payloadVideo || readAcceptanceVideoLenient(previous) const video = payloadVideo || readAcceptanceVideoLenient(previous)
if (!mergedEvidence.changed && !payloadVideo) { if (!mergedEvidence.changed && !payloadVideo) {
throw createHttpError('请上传新的验收图片', { throw createHttpError('请上传新的验收图片或新的验收视频', {
statusCode: 400, statusCode: 400,
errorCode: 'work_order_sharing_evidence_supplement_image_required', errorCode: 'work_order_sharing_evidence_supplement_image_required',
}) })
@@ -846,15 +852,16 @@ export async function saveWorkerOrderAcceptanceDraft(
const worker = await getRequiredWorker(session.workerId) const worker = await getRequiredWorker(session.workerId)
if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) { if (workOrder.status === WORK_ORDER_STATUS.PENDING_ACCEPTANCE) {
if (imageUrls.length === 0) { const previous = safeParseJson(workOrder.acceptance_json)
throw createHttpError('请至少保留一张验收图片', { // 已提交后只允许替换视频,不允许删除证据。
const video = resolveAcceptanceVideo(payload, previous)
// 验收图片与验收视频至少保留一个。
if (!hasAcceptanceEvidence({ imageUrls, video })) {
throw createHttpError('请至少保留一张验收图片或一段验收视频', {
statusCode: 400, statusCode: 400,
errorCode: 'work_order_acceptance_image_required', errorCode: 'work_order_acceptance_image_required',
}) })
} }
const previous = safeParseJson(workOrder.acceptance_json)
// 已提交后只允许替换视频,不允许删除证据。
const video = resolveAcceptanceVideo(payload, previous)
const acceptance = { const acceptance = {
note: String(payload.note ?? previous.note ?? '').trim(), note: String(payload.note ?? previous.note ?? '').trim(),
files, files,
@@ -944,15 +951,16 @@ async function saveWorkerSharingAcceptanceDraft(
const { files, imageUrls } = resolveAcceptanceImages(payload, {}) const { files, imageUrls } = resolveAcceptanceImages(payload, {})
if (sharingShare.status === 'submitted') { if (sharingShare.status === 'submitted') {
if (imageUrls.length === 0) { const previous = safeParseJson(sharingShare.acceptance_json)
throw createHttpError('请至少保留一张验收图片', { // 已提交后只允许替换视频,不允许删除证据。
const video = resolveAcceptanceVideo(payload, previous)
// 验收图片与验收视频至少保留一个。
if (!hasAcceptanceEvidence({ imageUrls, video })) {
throw createHttpError('请至少保留一张验收图片或一段验收视频', {
statusCode: 400, statusCode: 400,
errorCode: 'work_order_acceptance_image_required', errorCode: 'work_order_acceptance_image_required',
}) })
} }
const previous = safeParseJson(sharingShare.acceptance_json)
// 已提交后只允许替换视频,不允许删除证据。
const video = resolveAcceptanceVideo(payload, previous)
const acceptance = { const acceptance = {
note: String(payload.note ?? previous.note ?? '').trim(), note: String(payload.note ?? previous.note ?? '').trim(),
files, files,
@@ -267,17 +267,18 @@ export function WorkerOrderModals({
<Form.Item <Form.Item
label="验收图片" label="验收图片"
required={ required={
!editingOrder || !acceptanceVideo &&
canSupplementAcceptedEvidence(editingOrder) || (!editingOrder ||
!canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence) canSupplementAcceptedEvidence(editingOrder) ||
!canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence))
} }
extra={ extra={
editingOrder && canSupplementAcceptedEvidence(editingOrder) editingOrder && canSupplementAcceptedEvidence(editingOrder)
? '仅可追加新图片,原有验收图片会保留。' ? '仅可追加新图片,原有验收图片会保留。'
: editingOrder && : editingOrder &&
canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence) canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
? 'VIP 免审核可先提交,验收后需在 24 小时内补充图片。' ? 'VIP 免审核可先提交,验收后需在 24 小时内补充图片或视频。'
: undefined : '验收图片与验收视频至少上传一个,已上传视频时图片可不填。'
} }
> >
<ImageUpload <ImageUpload
@@ -290,8 +291,8 @@ export function WorkerOrderModals({
/> />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
label="验收视频(选填)" label="验收视频"
extra="选片后会在本机压到 720p 再上传;已提交的验收只能替换视频,不能删除。" extra="与验收图片至少上传一个;选片后会在本机压到 720p 再上传;已提交的验收只能替换视频,不能删除。"
> >
<VideoUpload value={acceptanceVideo} onChange={onAcceptanceVideoChange} /> <VideoUpload value={acceptanceVideo} onChange={onAcceptanceVideoChange} />
</Form.Item> </Form.Item>
@@ -518,8 +518,10 @@ export default function WorkerOrdersPage() {
} }
const canAutoAccept = canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence) const canAutoAccept = canAutoAcceptOrder(editingOrder, workerCanAutoAcceptWithoutEvidence)
if (acceptanceFiles.length === 0 && !canAutoAccept) { // 验收图片与验收视频至少上传一个;有视频时视同有图片,后续口径完全一致。
message.error('请上传验收图片') const hasEvidence = acceptanceFiles.length > 0 || Boolean(acceptanceVideo)
if (!hasEvidence && !canAutoAccept) {
message.error('请上传验收图片或验收视频')
return return
} }
try { try {
@@ -529,7 +531,14 @@ export default function WorkerOrdersPage() {
video: acceptanceVideo, video: acceptanceVideo,
...(editingShareId ? { shareId: editingShareId } : {}), ...(editingShareId ? { shareId: editingShareId } : {}),
}) })
message.success(canAutoAccept ? '已自动验收,可在 24 小时内补充图片' : '已提交验收') // 已有图片或视频时证据即算齐备,不再提示补图。
message.success(
canAutoAccept
? hasEvidence
? '已自动验收'
: '已自动验收,可在 24 小时内补充图片或视频'
: '已提交验收',
)
setEditingOrder(null) setEditingOrder(null)
setAcceptanceFiles([]) setAcceptanceFiles([])
setAcceptanceVideo(null) setAcceptanceVideo(null)
@@ -543,8 +552,8 @@ export default function WorkerOrdersPage() {
async function saveAcceptanceImages(values: AcceptanceFormValues) { async function saveAcceptanceImages(values: AcceptanceFormValues) {
if (!editingOrder) return if (!editingOrder) return
if (hasSubmittedAcceptance(editingOrder) && acceptanceFiles.length === 0) { if (hasSubmittedAcceptance(editingOrder) && acceptanceFiles.length === 0 && !acceptanceVideo) {
message.error('请至少保留一张验收图片') message.error('请至少保留一张验收图片或一段验收视频')
return return
} }
try { try {