前端上传 OBS 显式附带 filename,避免部分环境 400
ci/woodpecker/tag/build Pipeline was successful

对真实 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:
becomer
2026-10-18 18:58:24 +08:00
parent 91b8d7bfd4
commit 72dd2645ec
3 changed files with 58 additions and 2 deletions
+47
View File
@@ -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}`
}
+8 -1
View File
@@ -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',
+3 -1
View File
@@ -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',