docs(spec): 上传 API 增加 fileName + extract=zip 自动解压

- §7.1 补充 fileName (可选)、extract (bool)
- 响应统一为 Result<List<SysFileUploadVO>>:单文件也是长度 1 的数组
- §7.4 fileName 行为说明 (不传/传/校验)
- §7.5 extract=true (ZIP 解压):
  - 行为 + 路径规则 (bizType/zipBaseName/entryName)
  - 安全护栏 (总 entry ≤ 100, 单 entry ≤ maxSize, entry 名正则)
  - 请求/响应/订阅方事件示例
- 对应后端 commit: c4a5d5f
This commit is contained in:
2026-06-07 23:17:14 +08:00
parent 3aebe0b5a5
commit f26c67368c
@@ -275,25 +275,89 @@ Authorization: Bearer <JWT> # 网关已注入,storage 服务再校
file : MultipartFile ()
bizType : string (form) ()
storage : string (query) (aliyun/tencent/local active)
storage : string (form) (aliyun/tencent/local active)
fileName : string (form) ( [A-Za-z0-9][A-Za-z0-9._-]{<=200} 7.4)
extract : bool (form) ( falsetrue .zip 7.5)
Response (Result<SysFileUploadVO>):
Response (Result<List<SysFileUploadVO>>):
{
"error": 0,
"message": "success",
"data": {
"id": 1001,
"name": "a1b2c3d4.pem",
"originalName": "wechat.pem",
"url": "https://oss.../cert/2026/06/a1b2c3d4.pem",
"size": 2048,
"contentType": "application/x-pem-file",
"storageType": "ALIYUN",
"bizType": "SYS_APP_CERT"
}
"data": [
{
"id": 1001,
"name": "a1b2c3d4.pem",
"originalName": "wechat.pem",
"path": "sys-app-cert/2026/06/a1b2c3d4.pem", //
"url": "https://oss.../sys-app-cert/2026/06/a1b2c3d4.pem",
"size": 2048,
"contentType": "application/x-pem-file",
"storageType": "ALIYUN",
"bizType": "SYS_APP_CERT"
}
]
}
```
> **响应统一是数组**:单文件上传长度为 1,zip 解压上传长度为 N。前端按 `data.length` 即可区分。
### 7.4 fileName 参数
| 行为 | 说明 |
|------|------|
| 不传 | 默认 `bizType/yyyy/MM/{uuid}{ext}`,分布式不冲突 |
| 传 | 存储路径为 `bizType/{fileName}`,**会覆盖同名文件**(适用固定路径场景,如 `avatar-{userId}.jpg`|
| 校验 | `^[A-Za-z0-9][A-Za-z0-9._-]{0,199}$`,不合规 400 |
### 7.5 extract=trueZIP 自动解压)
**适用**:批量上传场景(应用多证书、UI 主题包、字体包、翻译文件等)。
```http
POST /storage/upload
Content-Type: multipart/form-data
file : certs.zip (.zip)
bizType : SYS_APP_CERT
fileName : wechat ()
extract : true
```
**行为**
- zip 本身**不**存到后端;解压每个 entry 单独存、单独推 `ON_UPLOAD` 事件
- 存储路径:`bizType/{zipBaseName}/{entryName}`zipBaseName 优先级 `fileName` > 原文件名去 `.zip` > UUID 前 12 位
- 响应:`data` 数组长度 = zip 中文件 entry 数(不含目录)
- 非 .zip 文件传 `extract=true` → 400
**安全护栏**`ZipExtractor` 强制):
| 项 | 限制 | 失败行为 |
|----|------|---------|
| 总 entry 数 | ≤ 100 | 400 防 zip bomb / 百万小文件 |
| 单 entry 大小 | ≤ `bizType` 配置的 `maxSize` | 400 |
| entry 名 | 须匹配 `^[A-Za-z0-9][A-Za-z0-9._-]*(/[A-Za-z0-9][A-Za-z0-9._-]*)*$` | 400(防 Zip Slip / 绝对路径 / Windows 盘符)|
| entry 名长度 | ≤ 200 | 400 |
**典型场景**
```jsonc
// 请求
file = wechat.zip // 内部: wechat/apiclient_cert.pem, wechat/apiclient_key.pem
bizType = "SYS_APP_CERT"
fileName = "wechat"
extract = true
// 响应
{
"error": 0,
"data": [
{ "id": 1001, "name": "wechat/apiclient_cert.pem", "path": "sys-app-cert/wechat/apiclient_cert.pem", ... },
{ "id": 1002, "name": "wechat/apiclient_key.pem", "path": "sys-app-cert/wechat/apiclient_key.pem", ... }
]
}
// 订阅方收到 2 条 ON_UPLOADtype 都是 "SYS_APP_CERT_UPLOAD"bizType=SYS_APP_CERT
```
### 7.2 查询文件
```http