fix(upload): 上传上限统一 150MB + 超时放宽至 600s(R225)
背景:教师组作品压缩包上传失败,根因是后端 config.multipart.fileSize 为 '100mb',而前端一直承诺 zip 200MB / 其他 100MB —— 两套阈值互不知情, 100~200MB 的文件必然「前端放行、后端拒收」。 改动: - utils/uploadCombo.js: 新增 MAX_MB=150 作为唯一数值源,ZIP_MAX / OTHER_MAX 均取 MAX_MB;validateSingleFile 的提示文案改为「150MB」。 原先 zip 200MB 是**无法兑现的承诺**(后端从来只收 100MB),收紧到 150 是让提示与事实一致,不是新增限制。 - views/Competition.vue: 删除本文件内另一份 ZIP_MAX=200MB/OTHER_MAX=100MB 的副本(本来就没被引用,但重复常量正是漂移温床),改为从 uploadCombo 导入 MAX_MB,三处文案同步为「单个文件 150MB」。 - api/upload.js: uploadWork 的 timeout 300s -> 600s。150MB 在服务器入口 约 1.25MB/s 且为全体考生共享,单传约 120s、数人并发可达 400s+;不放宽 超时的话,限额放开后失败方式只会从「文件太大」变成「超时」。 nginx /api/ 的 proxy_send_timeout / proxy_read_timeout 已是 600s,对齐。 产物验证:新包 Competition-C3sA4cVv.js 含「超过大小限制(150MB)」, 旧文案「zip 200MB / 其他 100MB」0 命中;timeout 压缩形态 timeout:6e5 (=600000)已确认落在 Competition chunk,Admin chunk 的超时未被波及。
This commit is contained in:
@@ -39,8 +39,12 @@ export function uploadWork(group, formData, onProgress) {
|
|||||||
// 注意:不要手动设置 Content-Type: multipart/form-data(不带 boundary 会导致
|
// 注意:不要手动设置 Content-Type: multipart/form-data(不带 boundary 会导致
|
||||||
// 后端 ctx.getFileStream() 报 Missing boundary 而 500)。交由 axios/浏览器自动
|
// 后端 ctx.getFileStream() 报 Missing boundary 而 500)。交由 axios/浏览器自动
|
||||||
// 补带 boundary 的 multipart 头。字段顺序:先文本字段(file_type/work_name)再 file。
|
// 补带 boundary 的 multipart 头。字段顺序:先文本字段(file_type/work_name)再 file。
|
||||||
|
// 🔴 R225:timeout 300s -> 600s。上传上限提至 150MB 后,服务器入口带宽约
|
||||||
|
// 1.25MB/s 且为全体考生共享,单传约 120s、数人并发时可达 400s+;
|
||||||
|
// 原 300s 会让「限额已放开」的文件改以超时失败。
|
||||||
|
// nginx 侧 /api/ 的 proxy_send_timeout/proxy_read_timeout 已是 600s,两者对齐。
|
||||||
return request.post(`/upload/${group}`, formData, {
|
return request.post(`/upload/${group}`, formData, {
|
||||||
timeout: 300000,
|
timeout: 600000,
|
||||||
onUploadProgress: (e) => {
|
onUploadProgress: (e) => {
|
||||||
if (onProgress && e.total) onProgress(Math.round((e.loaded / e.total) * 100))
|
if (onProgress && e.total) onProgress(Math.round((e.loaded / e.total) * 100))
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,8 +13,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
export const ALLOWED = ['.doc', '.docx', '.pdf', '.ppt', '.pptx', '.zip']
|
export const ALLOWED = ['.doc', '.docx', '.pdf', '.ppt', '.pptx', '.zip']
|
||||||
export const ZIP_MAX = 200 * 1024 * 1024
|
// 上传大小上限:统一 150MB
|
||||||
export const OTHER_MAX = 100 * 1024 * 1024
|
// 🔴 2026-10-23 R225:原 ZIP_MAX=200MB / OTHER_MAX=100MB,与后端
|
||||||
|
// config.multipart.fileSize(当时 '100mb')长期不一致 ⇒ 100~200MB 的 zip
|
||||||
|
// 前端放行、后端抛 MultipartFileTooLargeError,教师组因此报障。
|
||||||
|
// 现前后端统一为 150MB(后端已同步改为 '150mb'),保持「前端放行 == 后端接收」。
|
||||||
|
export const MAX_MB = 150
|
||||||
|
export const ZIP_MAX = MAX_MB * 1024 * 1024
|
||||||
|
export const OTHER_MAX = MAX_MB * 1024 * 1024
|
||||||
|
|
||||||
export function fileExt(name) {
|
export function fileExt(name) {
|
||||||
return (String(name).split('.').pop() || '').toLowerCase()
|
return (String(name).split('.').pop() || '').toLowerCase()
|
||||||
@@ -42,7 +48,7 @@ export function validateSingleFile(name, size) {
|
|||||||
const e = '.' + fileExt(name)
|
const e = '.' + fileExt(name)
|
||||||
if (!ALLOWED.includes(e)) return `不支持的格式:${name}`
|
if (!ALLOWED.includes(e)) return `不支持的格式:${name}`
|
||||||
const max = e === '.zip' ? ZIP_MAX : OTHER_MAX
|
const max = e === '.zip' ? ZIP_MAX : OTHER_MAX
|
||||||
if (size > max) return `${name} 超过大小限制(${e === '.zip' ? '200MB' : '100MB'})`
|
if (size > max) return `${name} 超过大小限制(${MAX_MB}MB)`
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,7 @@
|
|||||||
</template>
|
</template>
|
||||||
<el-alert type="info" :closable="false" show-icon style="margin-bottom:16px">
|
<el-alert type="info" :closable="false" show-icon style="margin-bottom:16px">
|
||||||
<template #title>本组别无客观题:先选择文件并确认无误,再点击「上传所选文件」提交</template>
|
<template #title>本组别无客观题:先选择文件并确认无误,再点击「上传所选文件」提交</template>
|
||||||
支持 docx / pdf / ppt / zip。规则:① zip 须单独上传(与任何其他文件互斥,自动覆盖);② pdf 与 docx 相互覆盖,只能保留其一;③ 若需同时提交文档与演示稿,请选「pdf + ppt」或「docx + ppt」组合。zip 上限 200MB,其余格式上限 100MB。
|
支持 docx / pdf / ppt / zip。规则:① zip 须单独上传(与任何其他文件互斥,自动覆盖);② pdf 与 docx 相互覆盖,只能保留其一;③ 若需同时提交文档与演示稿,请选「pdf + ppt」或「docx + ppt」组合。单个文件上限 150MB。
|
||||||
</el-alert>
|
</el-alert>
|
||||||
|
|
||||||
<!-- R190:小组作品状态(组内互斥)—— 组内已有人上传后,其他成员不能再传。
|
<!-- R190:小组作品状态(组内互斥)—— 组内已有人上传后,其他成员不能再传。
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
show-icon
|
show-icon
|
||||||
style="margin-top:10px"
|
style="margin-top:10px"
|
||||||
title="存在超出大小限制的文件"
|
title="存在超出大小限制的文件"
|
||||||
description="标红的文件已超过上传上限(zip 200MB / 其他 100MB),请删除后重新选择符合要求的文件再提交。"
|
description="标红的文件已超过上传上限(单个文件 150MB),请删除后重新选择符合要求的文件再提交。"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -432,6 +432,7 @@ import {
|
|||||||
validateSingleFile,
|
validateSingleFile,
|
||||||
finalComboCheck,
|
finalComboCheck,
|
||||||
reconcileFiles,
|
reconcileFiles,
|
||||||
|
MAX_MB,
|
||||||
} from '@/utils/uploadCombo'
|
} from '@/utils/uploadCombo'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@@ -472,8 +473,9 @@ const teamPanelVisible = computed(() => !!uploadStatus.value.teamNumber || uploa
|
|||||||
// 两处同列会重复且按钮语义不同,容易误删。
|
// 两处同列会重复且按钮语义不同,容易误删。
|
||||||
const teamSharedFiles = computed(() => (teamLocked.value ? (uploadStatus.value.teamFiles || []) : []))
|
const teamSharedFiles = computed(() => (teamLocked.value ? (uploadStatus.value.teamFiles || []) : []))
|
||||||
|
|
||||||
const ZIP_MAX = 200 * 1024 * 1024
|
// 大小上限唯一来源 = uploadCombo.MAX_MB(与后端 config.multipart.fileSize 对齐)
|
||||||
const OTHER_MAX = 100 * 1024 * 1024
|
// 🔴 R225:原先此处另存一份 ZIP_MAX=200MB / OTHER_MAX=100MB 的副本,已删除
|
||||||
|
// —— 重复常量是「前端放行、后端拒收」漂移的温床,改为单点引用。
|
||||||
|
|
||||||
function fileTypeOf(name) {
|
function fileTypeOf(name) {
|
||||||
const e = '.' + fileExt(name)
|
const e = '.' + fileExt(name)
|
||||||
@@ -493,7 +495,7 @@ const uploadComboValid = computed(() => {
|
|||||||
const pendingUploadFiles = computed(() => uploadFileList.value.filter(f => !f.uploaded && !isOverLimit(f)))
|
const pendingUploadFiles = computed(() => uploadFileList.value.filter(f => !f.uploaded && !isOverLimit(f)))
|
||||||
const hasOverLimitFile = computed(() => uploadFileList.value.some(f => isOverLimit(f)))
|
const hasOverLimitFile = computed(() => uploadFileList.value.some(f => isOverLimit(f)))
|
||||||
|
|
||||||
// 单文件是否超过上传大小限制(zip 200MB / 其他 100MB,来自 uploadCombo.validateSingleFile)
|
// 单文件是否超过上传大小限制(150MB,来自 uploadCombo.validateSingleFile)
|
||||||
function isOverLimit(file) {
|
function isOverLimit(file) {
|
||||||
const f = file.raw || file
|
const f = file.raw || file
|
||||||
return validateSingleFile(f.name, f.size) !== null
|
return validateSingleFile(f.name, f.size) !== null
|
||||||
@@ -511,7 +513,7 @@ function onUploadFileChange(file, fileList) {
|
|||||||
const over = uploadFileList.value.filter(f => isOverLimit(f))
|
const over = uploadFileList.value.filter(f => isOverLimit(f))
|
||||||
if (over.length) {
|
if (over.length) {
|
||||||
const names = over.map(f => (f.raw || f).name).join('、')
|
const names = over.map(f => (f.raw || f).name).join('、')
|
||||||
ElMessage.error(`文件「${names}」已超过大小限制(zip 200MB / 其他 100MB),已标红且无法提交`)
|
ElMessage.error(`文件「${names}」已超过大小限制(单个文件 ${MAX_MB}MB),已标红且无法提交`)
|
||||||
} else if (r.dropped.length) {
|
} else if (r.dropped.length) {
|
||||||
ElMessage.info('已按规则自动覆盖冲突文件(zip 独占 / pdf 与 docx 互斥)')
|
ElMessage.info('已按规则自动覆盖冲突文件(zip 独占 / pdf 与 docx 互斥)')
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user