# 文件上传改造 - 前端联调方案 ## 后端变更摘要 所有模块的文件上传已从 `fileUrl` 字符串字段改为 `system_storage_attachment` 中间表机制: | 变更 | 旧方式 | 新方式 | |------|--------|--------| | 上传 | 直接存 fileUrl | POST `/system/storage-blob/upload` → 获取 blobId | | 保存 | SaveReqVO.fileUrl | SaveReqVO.blobIds (List\) | | 查询 | RespVO.fileUrl | RespVO.attachmentList (List\) | | 删除 | 无 | 后端 Service 自动同步删除附件 | ## 涉及页面 ### HRM 模块(5个页面) | 页面 | 目录 | recordType | |------|------|------------| | 请假申请 | `src/views/hrm/leave/` | `hrm_leave_application` | | 离职申请 | `src/views/hrm/resignation/` | `hrm_resignation_application` | | 调岗申请 | `src/views/hrm/transfer/` | `hrm_transfer_application` | | 考勤异常申请 | `src/views/hrm/attendance/exception/` | `hrm_attendance_exception` | | 薪资发放 | `src/views/hrm/salary/payment/` | `hrm_salary_payment` | ### CRM 模块(2个页面) | 页面 | 目录 | recordType | |------|------|------------| | 销售报价单 | `src/views/crm/saleQuotation/` | `crm_sale_quotation` | | 跟进记录 | `src/views/crm/followup/` | `crm_follow_up_record` | ### MES 模块(1个页面) | 页面 | 目录 | recordType | |------|------|------------| | 设计资料 | `src/views/mes/pd/project/modules/document-form.vue` | `mes_pd_document` | --- ## API 说明 ### 通用文件 API(已有,无需新增) | 方法 | 路径 | 说明 | |------|------|------| | POST | `/system/storage-blob/upload` | 上传文件(multipart,`files` 参数) | | GET | `/system/storage-attachment/list` | 查询附件列表(`recordType` + `recordId`) | | POST | `/system/storage-attachment/bind` | 绑定附件到业务记录 | | DELETE | `/system/storage-attachment/delete` | 批量删除附件 | 前端已有对应的 API 封装在 `src/api/system/storage/index.ts`: - `uploadFile(files)` → `StorageBlob[]` - `listAttachments({ recordType, recordId })` → `StorageBlob[]` - `bindAttachments({ application, recordType, recordId, blobItems })` → `boolean` - `deleteAttachments(ids)` → `boolean` ### 各接口 SaveReqVO 新增字段 所有模块的创建/更新接口均新增 `blobIds` 参数: | 接口 | 新增参数 | |------|----------| | 所有模块 create/update | `blobIds: number[]` | ### 各接口 RespVO 新增字段 所有模块的详情查询接口均新增 `attachmentList` 字段: | 接口 | 新增字段 | 类型 | |------|----------|------| | GET /get(详情) | `attachmentList` | `StorageBlob[]` | **StorageBlob 结构:** ```typescript interface StorageBlob { id: number; // blob ID url?: string; // 文件访问 URL previewURL?: string; // 预览地址 name?: string; // 原文件名 downloadURL?: string; // 下载地址 application?: string; // 文件用途 storageAttachmentId?: number; // 附件关联 ID } ``` --- ## 前端修改步骤(每个模块通用) ### 步骤 1:修改 API 类型定义 修改 `src/api/{module}/index.ts` 中的接口定义: ```typescript // ✅ 引入 StorageBlob 类型 import type { SystemStorageApi } from '#/api/system/storage'; export namespace XxxApi { export interface Xxx { // ... 其他字段不变 ... // ❌ 删除:fileUrl?: string; // ✅ 新增: blobIds?: number[]; // 保存时传入的附件 blobId 列表 attachmentList?: SystemStorageApi.StorageBlob[]; // 查询时返回的附件列表 } } ``` 涉及的文件: | 文件 | 删除字段 | 新增字段 | |------|---------|---------| | `src/api/hrm/leave/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | | `src/api/hrm/resignation/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | | `src/api/hrm/transfer/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | | `src/api/hrm/attendance/exception/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | | `src/api/hrm/salary/payment/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | | `src/api/crm/saleQuotation/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | | `src/api/crm/followup/index.ts` | `fileUrls` | `blobIds`, `attachmentList` | | `src/api/mes/pd/document/index.ts` | `fileUrl` | `blobIds`, `attachmentList` | ### 步骤 2:修改表单页面(form.vue) **新增/编辑模式**需要添加文件上传组件,流程: ``` 用户选择文件 → uploadFile(files) 上传 → 获得 blobId[] → 保存时将 blobIds 发给后端 → 后端自动 bindAttachments ``` 核心代码模式: ```vue ``` **或者使用已经封装的 uploadFile 函数配合手动上传:** - 推荐方式:在提交前先调用 `uploadFile()` 上传所有文件,拿到 blobIds 后一起提交 - 备选方式:使用 a-upload 组件的自动上传,在 `@change` 中收集 blobId ### 步骤 3:修改详情展示 在详情/查看模式下展示附件列表(只读): ```vue ``` --- ## 字段展示规则 | 字段 | 展示位置 | 说明 | |------|----------|------| | `blobIds` | 表单(不展示) | 隐藏字段,保存时传给后端 | | `attachmentList` | 详情页、表单编辑回显 | 显示文件列表,支持预览/下载 | ## 业务规则说明 | 场景 | 规则 | |------|------| | 创建时 | 先调用 `uploadFile()` 上传文件拿 blobId,再把 blobIds 传给 create 接口 | | 更新时 | 可以新增/删除附件,后端收到 blobIds 不为 null 时先删后绑 | | 查看时 | 详情接口返回 `attachmentList`,直接渲染 | | 删除时 | 后端自动处理附件删除,前端无需额外操作 | | 列表/分页 | 不返回 `attachmentList`(性能考虑),仅详情接口返回 | ## 各模块特殊说明 ### HRM 请假/离职/调岗 - 表单页:`src/views/hrm/xxx/modules/form.vue` - 这三个模块结构相同,参考请假已完成的集成方式 - 创建时 `blobIds` 随表单数据一起发送 - 更新时 `blobIds` 不为 null 时后端重新绑定 ### HRM 考勤异常申请 - 表单页:`src/views/hrm/attendance/exception/modules/form.vue` - 目前只有 create 和 update、delete,注意 update 时也需要处理附件重绑 ### HRM 薪资发放 - 列表页:`src/views/hrm/salary/payment/index.vue` - 详情页:`src/views/hrm/salary/payment/modules/detail.vue` - 薪资发放是后端自动生成的,没有创建/编辑表单 - **只需在详情页展示 attachmentList** - 删除时后端自动同步删除附件 ### CRM 销售报价单 - 表单页:`src/views/crm/saleQuotation/modules/form.vue` - 列表页的 items 是物料明细,附件展示在详情/编辑弹窗中 - 表单已经使用 `useVbenModal` + `useVbenForm`,需要在上传区域加入 a-upload ### CRM 跟进记录 - 表单页:`src/views/crm/followup/modules/form.vue` - 之前使用 `fileUrls: string[]` 存储附件地址,现在改为 blobIds - 原来可能有图片/附件上传区域,需要改为 storage API ### MES 设计资料 - 表单页:`src/views/mes/pd/project/modules/document-form.vue` - 设计资料本身就是文件上传场景(资料类型 + 文件上传) - 注意:设计资料的"文件"是业务内容,而 attachment 是附加的参考文件 - 原有的 `fileUrl` 是资料文件自身的地址,需确认是否也需要改为 attachment 方式 --- ## 文件上传流程时序 ``` ┌─────────┐ ┌──────────┐ ┌──────────┐ │ 前端 │ │ system │ │ 业务模块 │ │ │ │ 模块 │ │ │ └────┬────┘ └────┬─────┘ └────┬─────┘ │ │ │ │ 1.选择文件 │ │ │──────────────>│ │ │ 2.返回 blobId │ │ │<──────────────│ │ │ │ │ │ 3.save(blobIds) │ │──────────────────────────────>│ │ │ │ │ │ 4.bind(blobIds)──> 内部调用 │ │<──────────────│ │ │ │ │ 5.返回成功 │ │ │<──────────────────────────────│ │ │ │ │ 6.查看详情 │ │ │──────────────────────────────>│ │ │ │ │ 7.返回 attachmentList │ │<──────────────────────────────│ ``` ## 注意事项 1. **上传时机**:文件上传应独立于业务保存,先上传拿 blobId,再保存业务数据 2. **编辑回显**:编辑时必须通过 GET /get 接口拿到 attachmentList 回显到上传组件 3. **大文件处理**:如果文件较大,需要在保存前等待上传完成(loading 状态) 4. **删除附件**:前端可以通过 `deleteAttachments([attachmentId])` 删除单个附件,但不强制,因为保存时 blobIds 不为 null 会触发后端重新绑定 5. **薪资发放特殊**:薪资发放无创建/编辑表单,只需在其详情页展示 attachmentList 6. **MES 设计资料特殊**:`MesPdDocumentDO` 原有的 `fileUrl` 字段已被删除,如果这个字段是作为业务文件地址(而非附件),可能需要单独评估是否需要保留文件存储 7. **跟进记录特殊**:跟进记录之前用 `fileUrls: string[]`(数组),现在改为 `blobIds: number[]`,接口层面变化较大 8. **列表页不展示附件**:为性能考虑,列表/分页接口不返回 attachmentList,只在详情接口返回