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:
2026-10-23 11:19:18 +08:00
parent 138592155f
commit 1a6707cac3
3 changed files with 22 additions and 10 deletions

View File

@@ -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))
}, },

View File

@@ -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
} }

View File

@@ -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 互斥)')
} }