diff --git a/superpowers/specs/2026-06-07-file-storage-service-design.md b/superpowers/specs/2026-06-07-file-storage-service-design.md index 9e463ec..adebffd 100644 --- a/superpowers/specs/2026-06-07-file-storage-service-design.md +++ b/superpowers/specs/2026-06-07-file-storage-service-design.md @@ -275,25 +275,89 @@ Authorization: Bearer # 网关已注入,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) (可选,默认 false;true 时若文件是 .zip 自动解压为多文件入库,详见 7.5) -Response (Result): +Response (Result>): { "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=true(ZIP 自动解压) + +**适用**:批量上传场景(应用多证书、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_UPLOAD,type 都是 "SYS_APP_CERT_UPLOAD",bizType=SYS_APP_CERT +``` + ### 7.2 查询文件 ```http