对真实 OBS 做了 5 组对照(真实用户 cookie + 真实策略 + 真实上传):
filename=file.jpg CT=image/jpeg → 204 成功
无 filename CT=image/jpeg → 400 失败
filename=blob CT=image/jpeg → 204 成功
无 filename CT=octet-stream → 400 失败
filename=image.jpg CT=octet-stream → 204 成功
结论:file 部分必须带 filename,否则 OBS 直接 400;与 Content-Type 无关。
原代码 `obsForm.append('file', file)` 只传两个参数。浏览器对
append(name, Blob) 会按规范自动补 filename="blob",所以多数桌面/移动
浏览器能通过;但自动补的名字无扩展名,且部分环境(老 WebView、
App 内置浏览器)不补,就会命中上表的 400 分支。
改动:
- 新增 utils/obsUpload.ts:resolveUploadFileName 优先用原始文件名
(无扩展名时按 MIME 补),无 name 时按 MIME + 时间戳生成;
非图片/未知 MIME 回退 jpg。
- HomeView.vue(任务表单上传)与 LeaveView.vue(请假上传)两处
append 均改为显式传文件名。
说明:本次未定位到用户 400 的确切链路(后端复现 return_register 得到的是
cookie_expired 而非 400),此项为消除已知的 filename 差异,
使前端行为在任何环境下都稳定。
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* 上传到今日校园 OBS 时的文件名处理。
|
||||
*
|
||||
* 背景(对真实 OBS 的 5 组对照实测):
|
||||
* file 部分带 filename → 204 成功
|
||||
* file 部分无 filename → 400 失败
|
||||
* 与 Content-Type 无关
|
||||
*
|
||||
* 浏览器对 FormData.append(name, Blob) 会按规范自动补 filename="blob",
|
||||
* 因此 `append('file', file)` 在多数浏览器能通过;但自动补出的名字无扩展名,
|
||||
* 且部分环境(老 WebView、某些 App 内置浏览器)不补,直接导致 400。
|
||||
* 所以这里统一显式给出带扩展名的文件名。
|
||||
*/
|
||||
|
||||
const MIME_EXT: Record<string, string> = {
|
||||
'image/jpeg': 'jpg',
|
||||
'image/jpg': 'jpg',
|
||||
'image/png': 'png',
|
||||
'image/webp': 'webp',
|
||||
'image/gif': 'gif',
|
||||
'image/bmp': 'bmp',
|
||||
'image/heic': 'heic',
|
||||
}
|
||||
|
||||
/** 从 MIME 推断扩展名,未知则回退 jpg(后端默认按 image/jpeg 处理)。 */
|
||||
export function extFromMime(mime?: string): string {
|
||||
if (!mime) return 'jpg'
|
||||
const key = mime.split(';')[0].trim().toLowerCase()
|
||||
return MIME_EXT[key] || 'jpg'
|
||||
}
|
||||
|
||||
/**
|
||||
* 为上传到 OBS 的文件生成文件名。
|
||||
* 优先用原始文件名(保留扩展名),否则按 MIME 生成。
|
||||
*/
|
||||
export function resolveUploadFileName(file: File | Blob, fallbackPrefix = 'image'): string {
|
||||
const anyFile = file as File
|
||||
const original = typeof anyFile?.name === 'string' ? anyFile.name.trim() : ''
|
||||
if (original) {
|
||||
// 有扩展名直接用;否则补一个
|
||||
if (/\.[a-zA-Z0-9]{1,8}$/.test(original)) return original
|
||||
return `${original}.${extFromMime(file.type)}`
|
||||
}
|
||||
const ext = extFromMime(file.type)
|
||||
const stamp = Date.now().toString(36)
|
||||
return `${fallbackPrefix}_${stamp}.${ext}`
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import AppDialog from '@/components/ui/dialog/AppDialog.vue'
|
||||
import PinnedNotice from '@/components/PinnedNotice.vue'
|
||||
import QrScanner from '@/components/QrScanner.vue'
|
||||
import jsQR from 'jsqr'
|
||||
import { resolveUploadFileName } from '@/utils/obsUpload'
|
||||
import * as authApi from '@/api/auth'
|
||||
import * as announceApi from '@/api/announcement'
|
||||
import { CPD_PASSWORD_ERROR } from '@/api'
|
||||
@@ -812,7 +813,13 @@ async function handleImageUpload(index: number, isEdit: boolean) {
|
||||
obsForm.append('policy', policy.policy || policy.Policy)
|
||||
obsForm.append('content-type', file.type || 'image/jpeg')
|
||||
}
|
||||
obsForm.append('file', file)
|
||||
// file 部分必须显式带 filename。
|
||||
// 实测(对真实 OBS 的 5 组对照):file 无 filename 时 OBS 直接返回 400,
|
||||
// 有 filename 才返回 204;与 Content-Type 无关。
|
||||
// 浏览器对 append(name, Blob) 虽会自动补 filename="blob",但部分环境
|
||||
// 不补或补成无扩展名,故这里显式给出带扩展名的文件名。
|
||||
const fileName = resolveUploadFileName(file)
|
||||
obsForm.append('file', file, fileName)
|
||||
|
||||
const obsRes = await fetch(policy.host || policy.Host, {
|
||||
method: 'POST',
|
||||
|
||||
@@ -797,6 +797,7 @@
|
||||
import { ref, computed, watch, onMounted, onUnmounted } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { CalendarCheck, CalendarPlus, CalendarClock, LoaderCircle, Trash2, X, MapPin, Upload, MessageSquare, Smartphone, Edit3 } from 'lucide-vue-next'
|
||||
import { resolveUploadFileName } from '@/utils/obsUpload'
|
||||
import * as leaveApi from '@/api/leave'
|
||||
import * as authApi from '@/api/auth'
|
||||
import type { LeaveItem, LeaveReportPlace, LeaveType } from '@/api/leave'
|
||||
@@ -1031,7 +1032,8 @@ async function uploadImageToObs(file: File): Promise<string | null> {
|
||||
obsForm.append('policy', policy.policy || policy.Policy)
|
||||
obsForm.append('content-type', file.type || 'image/jpeg')
|
||||
}
|
||||
obsForm.append('file', file)
|
||||
// file 部分必须显式带 filename,否则 OBS 返回 400(实测结论,与 Content-Type 无关)
|
||||
obsForm.append('file', file, resolveUploadFileName(file))
|
||||
|
||||
const obsRes = await fetch(policy.host || policy.Host, {
|
||||
method: 'POST',
|
||||
|
||||
Reference in New Issue
Block a user