| | |
| | | |
| | | ## 概述 |
| | | |
| | | 本系统提供了**统一的文件上传服务**,支持多种存储方式(本地、S3/OSS、数据库、FTP/SFTP)。 |
| | | 本系统使用 `yudao-module-system` 模块的 **Storage API** 作为统一的文件上传服务,支持多种存储方式(本地、S3/OSS)。 |
| | | |
| | | > **重要**:所有业务模块必须使用通用文件上传接口,**禁止为每个模块新增上传接口**。 |
| | | > **重要**:所有业务模块必须使用 system 模块的 Storage API,**禁止使用 infra 模块的 `/infra/file/*` 接口,禁止为每个模块新增上传接口**。 |
| | | |
| | | --- |
| | | |
| | | ## 一、通用上传接口 |
| | | ## 一、通用接口 |
| | | |
| | | ### 1.1 接口信息 |
| | | ### 1.1 文件上传 |
| | | |
| | | | 方法 | 路径 | 说明 | |
| | | |------|------|------| |
| | | | POST | `/infra/file/upload` | 通用文件上传 | |
| | | | POST | `/system/storage-blob/upload` | 通用文件上传 | |
| | | |
| | | ### 1.2 请求参数 |
| | | **请求参数**:`files`(MultipartFile 数组) |
| | | |
| | | **响应**: |
| | | |
| | | ```json |
| | | [ |
| | | { |
| | | "id": 123, |
| | | "resourceKey": "abc123", |
| | | "contentType": "application/pdf", |
| | | "originalFilename": "合同.pdf", |
| | | "uidFilename": "u_abc123.pdf", |
| | | "byteSize": 102400, |
| | | "path": "/storage/2024/01/abc123.pdf", |
| | | "previewURL": "https://xxx.com/preview/u_abc123.pdf", |
| | | "url": "https://xxx.com/u_abc123.pdf", |
| | | "name": "合同.pdf", |
| | | "downloadURL": "https://xxx.com/download/u_abc123.pdf" |
| | | } |
| | | ] |
| | | ``` |
| | | |
| | | ### 1.2 附件绑定 |
| | | |
| | | | 方法 | 路径 | 说明 | |
| | | |------|------|------| |
| | | | POST | `/system/storage-attachment/bind` | 将已上传文件绑定到业务记录 | |
| | | |
| | | **请求参数**: |
| | | |
| | | | 参数 | 类型 | 必填 | 说明 | |
| | | |------|------|------|------| |
| | | | file | MultipartFile | 是 | 上传的文件 | |
| | | | directory | String | 否 | 存储目录,如 `purchase-request`、`contract` | |
| | | | application | String | 否 | 文件用途,如 `file` | |
| | | | recordType | String | 是 | 关联记录类型,如 `mes_purchase_order` | |
| | | | recordId | Long | 是 | 关联记录 ID | |
| | | | blobItems | List\<BlobItem\> | 是 | 待绑定文件列表 | |
| | | |
| | | ### 1.3 响应示例 |
| | | `BlobItem`: |
| | | |
| | | ```json |
| | | { |
| | | "code": 0, |
| | | "data": "https://xxx.com/xxx.pdf", |
| | | "msg": "操作成功" |
| | | } |
| | | ``` |
| | | | 参数 | 类型 | 必填 | 说明 | |
| | | |------|------|------|------| |
| | | | blobId | Long | 是 | 上传返回的文件 ID | |
| | | | application | String | 否 | 覆盖外层的文件用途 | |
| | | |
| | | 返回值 `data` 即为文件访问 URL,可直接存储到数据库。 |
| | | ### 1.3 查询附件列表 |
| | | |
| | | | 方法 | 路径 | 说明 | |
| | | |------|------|------| |
| | | | GET | `/system/storage-attachment/list` | 查询业务记录的附件 | |
| | | |
| | | **请求参数**: |
| | | |
| | | | 参数 | 类型 | 必填 | 说明 | |
| | | |------|------|------|------| |
| | | | recordType | String | 是 | 关联记录类型 | |
| | | | recordId | Long | 是 | 关联记录 ID | |
| | | | application | String | 否 | 按用途过滤 | |
| | | |
| | | ### 1.4 删除附件 |
| | | |
| | | | 方法 | 路径 | 说明 | |
| | | |------|------|------| |
| | | | DELETE | `/system/storage-attachment/delete` | 批量删除附件 | |
| | | |
| | | **请求体**:文件 ID 数组 `[1, 2, 3]` |
| | | |
| | | --- |
| | | |
| | | ## 二、前端集成 |
| | | |
| | | ### 2.1 上传组件示例 |
| | | ### 2.1 使用封装好的 API |
| | | |
| | | ```html |
| | | <el-upload |
| | | :action="uploadUrl" |
| | | :headers="headers" |
| | | :data="{ directory: 'purchase-request' }" |
| | | :on-success="handleUploadSuccess" |
| | | :before-upload="beforeUpload" |
| | | > |
| | | <el-button type="primary">点击上传</el-button> |
| | | </el-upload> |
| | | ```typescript |
| | | import { bindAttachments, listAttachments, uploadFile } from '#/api/system/storage'; |
| | | |
| | | // 1. 上传文件 |
| | | const handleUpload = async (file: File) => { |
| | | const blobs = await uploadFile([file]); |
| | | // 获取 blob ID |
| | | const blobId = blobs[0]!.id; |
| | | return blobId; |
| | | }; |
| | | |
| | | // 2. 绑定到业务记录 |
| | | const handleBind = async (blobIds: number[], recordId: number) => { |
| | | await bindAttachments({ |
| | | application: 'file', |
| | | recordType: 'mes_pro_work_order', |
| | | recordId, |
| | | blobItems: blobIds.map(id => ({ blobId: id })), |
| | | }); |
| | | }; |
| | | |
| | | // 3. 查询附件列表 |
| | | const handleLoad = async (recordId: number) => { |
| | | const attachments = await listAttachments({ |
| | | recordType: 'mes_pro_work_order', |
| | | recordId, |
| | | }); |
| | | return attachments; |
| | | }; |
| | | ``` |
| | | |
| | | ### 2.2 配置说明 |
| | | ### 2.2 标准流程 |
| | | |
| | | ```js |
| | | data() { |
| | | return { |
| | | // 通用上传接口 |
| | | uploadUrl: process.env.VUE_APP_BASE_API + '/infra/file/upload', |
| | | // 认证头 |
| | | headers: { Authorization: 'Bearer ' + getToken() }, |
| | | } |
| | | }, |
| | | methods: { |
| | | handleUploadSuccess(response) { |
| | | if (response.code === 0) { |
| | | this.form.fileUrl = response.data // 保存文件 URL |
| | | } |
| | | }, |
| | | beforeUpload(file) { |
| | | // 业务层校验文件类型和大小 |
| | | const isLt10M = file.size / 1024 / 1024 < 10 |
| | | if (!isLt10M) { |
| | | this.$message.error('文件大小不能超过 10MB!') |
| | | } |
| | | return isLt10M |
| | | } |
| | | } |
| | | ``` |
| | | 1. 用户选择文件 → 调用 `POST /system/storage-blob/upload` 上传 |
| | | 2. 业务记录保存时 → 调用 `POST /system/storage-attachment/bind` 绑定附件到记录 |
| | | 3. 详情/编辑页 → 调用 `GET /system/storage-attachment/list` 加载已有附件 |
| | | 4. 用户删除附件 → 调用 `DELETE /system/storage-attachment/delete` |
| | | |
| | | ### 2.3 displayUpload 组件 |
| | | |
| | | 项目中已有文件上传显示组件,可直接使用系统 Storage API。 |
| | | |
| | | --- |
| | | |
| | | ## 三、目录规范 |
| | | ## 三、recordType 命名规范 |
| | | |
| | | 各业务模块通过 `directory` 参数区分存储目录,**禁止新增业务上传接口**: |
| | | 格式:`{模块前缀}_{业务表名}` |
| | | |
| | | | 模块 | directory | 说明 | |
| | | |------|-----------|------| |
| | | | 用户头像 | `avatar` | 用户头像图片 | |
| | | | 合同附件 | `contract` | 合同相关文件 | |
| | | | 采购申请 | `purchase-request` | 采购申请附件 | |
| | | | 采购订单 | `purchase-order` | 采购订单附件 | |
| | | | 产品图片 | `product` | 产品相关图片 | |
| | | | 通用附件 | `attachment` | 通用业务附件 | |
| | | |
| | | > **命名规范**:使用小写字母和连字符,如 `purchase-request`,与模块名保持一致。 |
| | | | 模块 | recordType | 说明 | |
| | | |------|------------|------| |
| | | | MES 采购订单 | `mes_purchase_order` | 采购订单附件 | |
| | | | MES 工单 | `mes_pro_work_order` | 生产工单附件 | |
| | | | MES 设备 | `mes_dv_machinery` | 设备档案附件 | |
| | | | MES 文档 | `mes_pd_document` | 项目文档附件 | |
| | | | CRM 客户 | `crm_customer` | 客户附件 | |
| | | | ERP 产品 | `erp_product` | 产品附件 | |
| | | |
| | | --- |
| | | |
| | | ## 四、后端集成(特殊场景) |
| | | ## 四、禁止行为 |
| | | |
| | | ### 4.1 后端需要主动上传文件时 |
| | | |
| | | 极少数场景下,后端需要主动上传文件(如生成报表后保存),可注入 `FileApi`: |
| | | |
| | | ```java |
| | | @Resource |
| | | private FileApi fileApi; |
| | | |
| | | // 上传文件 |
| | | String url = fileApi.createFile(bytes, "报表.xlsx", "report", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); |
| | | ``` |
| | | |
| | | ### 4.2 获取私有文件预签名地址 |
| | | |
| | | ```java |
| | | // 生成临时访问地址,1小时有效 |
| | | String presignedUrl = fileApi.presignGetUrl(fileUrl, 3600); |
| | | ``` |
| | | - ❌ 禁止调用 `/infra/file/upload` 或 `/infra/file/presigned-url` |
| | | - ❌ 禁止在 MES/CRM/ERP 等业务模块 Controller 新增 `@PostMapping("/upload")` |
| | | - ❌ 禁止注入 `cn.iocoder.yudao.module.infra.api.file.FileApi` |
| | | - ❌ 禁止使用旧版 `src/api/infra/file/index.ts` 中的 `uploadFile()` 函数 |
| | | |
| | | --- |
| | | |
| | | ## 五、存储配置 |
| | | ## 五、前端 API 模块位置 |
| | | |
| | | 存储配置通过管理后台动态配置,无需修改代码: |
| | | `src/api/system/storage/index.ts` |
| | | |
| | | 1. 访问 **基础设施 > 文件管理 > 文件配置** |
| | | 2. 新增配置,选择存储类型并填写参数 |
| | | 3. 点击"设置为主配置"激活使用 |
| | | |
| | | ### 支持的存储类型 |
| | | |
| | | | 类型 | 说明 | 适用场景 | |
| | | |------|------|----------| |
| | | | DB | 存储到数据库 | 小文件、临时文件 | |
| | | | LOCAL | 本地磁盘存储 | 内网部署、开发环境 | |
| | | | FTP/SFTP | FTP 服务器 | 兼容老系统 | |
| | | | S3 | 阿里云 OSS / 腾讯云 COS / MinIO | 生产环境 | |
| | | 导出函数:`uploadFile`、`listAttachments`、`bindAttachments`、`deleteAttachments` |
| | | |
| | | --- |
| | | |
| | | ## 六、文件存储方式 |
| | | ## 六、注意事项 |
| | | |
| | | ### 6.1 多文件存储(推荐) |
| | | |
| | | 使用 `List<String>` + `StringListTypeHandler`,参考 CRM 跟进记录模块: |
| | | |
| | | **实体类:** |
| | | |
| | | ```java |
| | | @TableName(value = "crm_follow_up_record", autoResultMap = true) |
| | | public class CrmFollowUpRecordDO extends BaseDO { |
| | | |
| | | /** |
| | | * 图片 |
| | | */ |
| | | @TableField(typeHandler = StringListTypeHandler.class) |
| | | private List<String> picUrls; |
| | | |
| | | /** |
| | | * 附件 |
| | | */ |
| | | @TableField(typeHandler = StringListTypeHandler.class) |
| | | private List<String> fileUrls; |
| | | } |
| | | ``` |
| | | |
| | | **数据库字段**:`VARCHAR`,存储 JSON 数组格式如 `["url1","url2","url3"]` |
| | | |
| | | **VO 类:** |
| | | |
| | | ```java |
| | | @Schema(description = "附件") |
| | | private List<String> fileUrls; |
| | | ``` |
| | | |
| | | **前端使用:** |
| | | |
| | | ```js |
| | | // 上传成功后追加 |
| | | handleUploadSuccess(response) { |
| | | if (response.code === 0) { |
| | | if (!this.form.fileUrls) { |
| | | this.form.fileUrls = [] |
| | | } |
| | | this.form.fileUrls.push(response.data) |
| | | } |
| | | } |
| | | |
| | | // 删除文件 |
| | | handleRemove(index) { |
| | | this.form.fileUrls.splice(index, 1) |
| | | } |
| | | ``` |
| | | |
| | | ### 6.2 单文件存储 |
| | | |
| | | ```java |
| | | // 实体类 |
| | | private String fileUrl; |
| | | |
| | | // VO 类 |
| | | @Schema(description = "附件地址") |
| | | private String fileUrl; |
| | | ``` |
| | | |
| | | ### 6.3 `infra_file` 表的作用 |
| | | |
| | | 系统 `infra_file` 表记录所有上传文件的元数据(name、path、url、type、size),不与业务关联,仅用于: |
| | | - 文件管理后台查看上传记录 |
| | | - 统计存储使用情况 |
| | | |
| | | --- |
| | | |
| | | ## 七、注意事项 |
| | | |
| | | 1. **使用通用接口**:所有文件上传必须使用 `/infra/file/upload`,禁止新增业务上传接口 |
| | | 2. **区分目录**:通过 `directory` 参数区分业务模块,便于管理和清理 |
| | | 3. **文件大小**:默认限制 10MB,可在 Nginx 或应用层调整 |
| | | 4. **类型校验**:前端应校验允许的文件类型,防止上传恶意文件 |
| | | 5. **URL 存储**:上传成功后,将返回的 URL 存储到业务表的 `fileUrl` 字段 |
| | | |
| | | --- |
| | | |
| | | ## 七、常见问题 |
| | | |
| | | **Q: 为什么不能为每个模块新增上传接口?** |
| | | |
| | | A: 统一接口便于: |
| | | - 统一管理存储配置 |
| | | - 统一权限控制和审计 |
| | | - 避免代码重复 |
| | | - 后续维护和迁移 |
| | | |
| | | **Q: 如何区分不同模块的文件?** |
| | | |
| | | A: 使用 `directory` 参数,文件会存储在对应目录下。 |
| | | |
| | | **Q: 前端如何限制文件类型?** |
| | | |
| | | A: 在 `beforeUpload` 方法中校验 `file.type` 或 `file.name` 后缀。 |
| | | 1. **使用通用接口**:所有上传走 system 模块,通过 `recordType` 区分业务 |
| | | 2. **上传后绑定**:文件上传只是暂存,必须调用 bind 接口关联到业务记录 |
| | | 3. **文件预览**:使用响应中的 `previewURL` 或 `url` 字段 |
| | | 4. **文件下载**:使用 `GET /system/storage-blob/download/{fileName}?token=xxx` |