diff --git a/changelogs/2026-05/04_frontend_bug_attachment_upload_multipart.md b/changelogs/2026-05/04_frontend_bug_attachment_upload_multipart.md new file mode 100644 index 0000000..4d7020e --- /dev/null +++ b/changelogs/2026-05/04_frontend_bug_attachment_upload_multipart.md @@ -0,0 +1,97 @@ +--- +date: 2026-05-04 +type: frontend-bug +module: contract-scheme-attachment +priority: high +notify: ["@mmg"] +status: pending +--- + +# 合同方案附件上传 MultipartException — 前端没用 FormData + +## 现象 + +POST `/admin/contract/scheme/{schemeId}/attachments` 报错: + +```json +{"code":500,"message":"服务器内部错误[MultipartException]: Current request is not a multipart request","data":null,"success":false} +``` + +## 根因 + +后端 round-trip 5MB / 中文名 / 30MB PDF curl 上传**全部成功**(已验证),后端 100% OK。 + +错误是 **`org.springframework.web.multipart.MultipartException`** — Spring 在期望 multipart 请求体时拿到的不是 multipart 格式。这是前端发请求时 **Content-Type / body 包装错了**。 + +最常见原因(按概率): +1. **axios 默认 Content-Type=application/json,没让 browser 自动设成 multipart** +2. **传了普通 object 不是 FormData,axios JSON.stringify 了** +3. **代码里写了 `Content-Type: application/json` 强制覆盖** + +## 修复(前端) + +### 错误写法 ❌ + +```js +// 错: 直接传对象 → axios 自动 JSON.stringify +http.post(`/admin/contract/scheme/${schemeId}/attachments`, { + file: fileObject, + fileType: 'PDF', + sortOrder: 0 +}) + +// 错: 强制 Content-Type +http.post(url, formData, { + headers: { 'Content-Type': 'application/json' } +}) +``` + +### 正确写法 ✅ + +```js +const formData = new FormData() +formData.append('file', file) // file 必须是 File / Blob 对象 +formData.append('fileType', 'PDF') // PDF / IMAGE / OTHER +formData.append('sortOrder', 0) + +// 关键: Content-Type 不要手动设 +// 不传 headers, 让 browser 自动加 'multipart/form-data; boundary=----WebKitFormBoundary...' +http.post(`/admin/contract/scheme/${schemeId}/attachments`, formData) + +// 或显式覆盖默认 application/json: +http.post(url, formData, { + headers: { 'Content-Type': 'multipart/form-data' } // 让 axios 自动加 boundary +}) +``` + +## 后端接口规约(不变) + +| 字段 | 类型 | 必填 | 说明 | +|------|------|------|------| +| `file` | File (multipart) | ✅ | 文件本体 | +| `fileType` | String (form field) | 否 | `PDF` / `IMAGE` / `OTHER`,不传按扩展名推断 | +| `sortOrder` | Integer (form field) | 否 | 默认 0 | + +## 验证 curl(后端确认 OK 的请求) + +```bash +curl -X POST "https://api.test.1814.love:9443/admin/contract/scheme/3/attachments" \ + -H "Authorization: Bearer $TOKEN" \ + -F "file=@文明旅游倡议书.pdf;type=application/pdf" \ + -F "fileType=PDF" \ + -F "sortOrder=0" +``` + +返回: +```json +{"code":200,"data":{"attachmentId":"...","ossUrl":"...","fileSize":...},"success":true} +``` + +## 后端配套(已部署测试服) + +| PR | 改动 | +|----|------| +| #1640 | multipart 上限 50MB;超限友好提示 `code=100100` | +| #1641 | generic 500 错误暴露 exception class(本次定位就靠这个) | + +@mmg 看下 hl-ui 里调 `/admin/contract/scheme/.../attachments` 的代码,把对象传参改成 FormData 即可。