import { MAX_LOGICAL_UPLOAD_BYTES } from '@/lib/material-upload-chunk-policy' export const TASK_DETAIL_UPLOAD_LIMITS = { /** 单个图片原图的保护上限,避免浏览器解码/服务器兜底处理一次吞掉过多内存。 */ maxSingleImageBytes: 100 * 1024 * 1024, /** 一次任务允许的图片数量上限;详情长图的切片不计入此门禁。生产存量素材 SKU 图最多 220 张,上限需覆盖真实业务。 */ maxImagesPerGroup: 300, maxTotalImageCount: 600, /** PSD/PSB/TIFF 等源文件不是图片计数的一部分,单独限制,防止小文件堆积绕过图片上限。 */ maxPsdFileCount: 20, /** 原图与源文件的总条目数上限。 */ maxTotalSourceFileCount: 700, /** 仅图片原图总大小。PSD/压缩包不算进这项,避免把源文件误卡在 1GB。 */ maxTotalSourceBytes: 1024 * 1024 * 1024, /** 单个 PSD/压缩包与分片上传单文件上限对齐。 */ maxSinglePsdBytes: MAX_LOGICAL_UPLOAD_BYTES, /** 一次提交的源文件总量:大约够主图/SKU/详情各一份大源文件。 */ maxTotalPsdBytes: 3 * MAX_LOGICAL_UPLOAD_BYTES, maxPreviewCountPerImage: 60, maxTotalPreviewCount: 2_000, maxSinglePreviewBytes: 10 * 1024 * 1024, maxTotalPreviewBytes: 512 * 1024 * 1024, /** 普通预览为 800x1200,详情切片最大 1600x1500。 */ maxPreviewWidth: 800, maxPreviewHeight: 1_200, maxPreviewPixels: 800 * 1_200, maxSliceWidth: 1_600, maxSliceHeight: 1_500, maxSlicePixels: 1_600 * 1_500, maxPreviewAnimationFrames: 1, /** 单个用户最多排队 3 个,防止一个账号长期占满唯一图片 worker。 */ maxQueuedJobsPerUser: 3, /** 全站最多等待 20 个任务;processing 任务不计入等待容量。 */ maxQueuedJobsGlobal: 20, /** 合法最大提交:140 个源文件 + 1000 个客户端预览。 */ maxTotalUploadSessionCount: 1_140, } as const export type TaskDetailUploadFileGroups = { main: File[] sku: File[] detail: File[] psd?: File[] } export function validateTaskDetailUploadFiles(groups: TaskDetailUploadFileGroups): string | null { const imageGroups: Array<[string, File[]]> = [ ['主图', groups.main], ['SKU 图', groups.sku], ['详情图', groups.detail], ] for (const [label, files] of imageGroups) { if (files.length > TASK_DETAIL_UPLOAD_LIMITS.maxImagesPerGroup) { return `${label}最多上传 ${TASK_DETAIL_UPLOAD_LIMITS.maxImagesPerGroup} 张,请分批提交` } const oversized = files.find((file) => file.size > TASK_DETAIL_UPLOAD_LIMITS.maxSingleImageBytes) if (oversized) { return `${label}「${oversized.name}」超过单图 ${Math.round(TASK_DETAIL_UPLOAD_LIMITS.maxSingleImageBytes / 1024 / 1024)}MB 限制,请先压缩或拆分` } } const imageFiles = imageGroups.flatMap(([, files]) => files) if (imageFiles.length > TASK_DETAIL_UPLOAD_LIMITS.maxTotalImageCount) { return `本次图片最多上传 ${TASK_DETAIL_UPLOAD_LIMITS.maxTotalImageCount} 张,请分批提交` } const psdFiles = groups.psd || [] if (psdFiles.length > TASK_DETAIL_UPLOAD_LIMITS.maxPsdFileCount) { return `PSD 源文件最多上传 ${TASK_DETAIL_UPLOAD_LIMITS.maxPsdFileCount} 个,请分批提交` } const allFiles = [...imageFiles, ...psdFiles] if (allFiles.length > TASK_DETAIL_UPLOAD_LIMITS.maxTotalSourceFileCount) { return `本次原图和源文件最多上传 ${TASK_DETAIL_UPLOAD_LIMITS.maxTotalSourceFileCount} 个,请分批提交` } const totalImageBytes = imageFiles.reduce((sum, file) => sum + Math.max(0, file.size), 0) if (totalImageBytes > TASK_DETAIL_UPLOAD_LIMITS.maxTotalSourceBytes) { return `本次图片总大小超过 ${formatTaskDetailByteLimit(TASK_DETAIL_UPLOAD_LIMITS.maxTotalSourceBytes)},请分批提交` } const oversizedPsd = psdFiles.find((file) => file.size > TASK_DETAIL_UPLOAD_LIMITS.maxSinglePsdBytes) if (oversizedPsd) { return `源文件「${oversizedPsd.name}」超过 ${formatTaskDetailByteLimit(TASK_DETAIL_UPLOAD_LIMITS.maxSinglePsdBytes)},请先拆分或压缩` } const totalPsdBytes = psdFiles.reduce((sum, file) => sum + Math.max(0, file.size), 0) if (totalPsdBytes > TASK_DETAIL_UPLOAD_LIMITS.maxTotalPsdBytes) { return `本次源文件总大小超过 ${formatTaskDetailByteLimit(TASK_DETAIL_UPLOAD_LIMITS.maxTotalPsdBytes)},请分批提交` } return null } export function formatTaskDetailByteLimit(bytes: number): string { const gib = 1024 * 1024 * 1024 const mib = 1024 * 1024 if (bytes >= gib) { const value = bytes / gib return Number.isInteger(value) ? `${value}GB` : `${value.toFixed(1)}GB` } if (bytes >= mib) { const value = bytes / mib return Number.isInteger(value) ? `${value}MB` : `${value.toFixed(1)}MB` } return `${bytes}B` }