refactor(crm): 重构文件上传功能,替换原有的fileUrls字段为统一的StorageAttachmentApi

- 移除CrmFollowUpCreateReqBO中的fileUrls字段
- 在CrmFollowUpRecordController中引入StorageAttachmentApi并实现附件列表查询
- 更新CrmFollowUpRecordDO移除fileUrls字段
- 修改CrmFollowUpRecordRespVO使用attachmentList替代fileUrls
- 调整CrmFollowUpRecordSaveReqVO增加blobIds字段
- 在CrmFollowUpRecordServiceImpl中集成StorageAttachmentApi进行附件绑定和删除
- 更新CrmSaleQuotationController获取附件列表功能
- 从CrmSaleQuotationDO移除fileUrl字段
- 修改CrmSaleQuotationRespVO使用attachmentList
- 调整CrmSaleQuotationSaveReqVO使用blobIds替代fileUrl
- 在CrmSaleQuotationServiceImpl中集成附件绑定逻辑
- 移除ERP模块中各DO的fileUrl字段
- 移除Hrm模块中AttendanceException相关的fileUrl字段
- 更新文档file_upload_guide.md说明统一使用Storage API
- 从数据库脚本移除各模块的file_url字段
已添加8个文件
已修改62个文件
1737 ■■■■ 文件已修改
.claude/rules/file-upload.md 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/attachment_upload_frontend_integration.md 306 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/attachment_upload_reform.md 168 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/file_upload_guide.md 324 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/mdm_item_sync_mes_optimization.md 97 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
docs/附件上传模块汇总.md 131 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/hrm_module_init.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java 6 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationSaveReqVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/followup/CrmFollowUpRecordDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/followup/bo/CrmFollowUpCreateReqBO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmResignationApplicationRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmResignationApplicationSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmTransferApplicationRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmTransferApplicationSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceExceptionController.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryPaymentController.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryPaymentRespVO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmLeaveApplicationDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmResignationApplicationDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmTransferApplicationDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceExceptionDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryPaymentDO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryPaymentServiceImpl.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java 17 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApi.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/archive/vo/MesPdArchiveDetailRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/MesPdDocumentController.java 7 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentRespVO.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentSaveReqVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pd/document/MesPdDocumentDO.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderBomDO.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemMapper.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderBomMapper.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java 27 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApi.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApiImpl.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/dto/StorageBlobRespDTO.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
.claude/rules/file-upload.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
# æ–‡ä»¶ä¸Šä¼ è§„则
## æ ¸å¿ƒåŽŸåˆ™ï¼ˆAI å¿…须严格遵守)
**所有文件上传必须走 `yudao-module-system` æ¨¡å—çš„ Storage API + Attachment ä¸­é—´è¡¨æœºåˆ¶ã€‚**
**禁止在任何业务模块自行设计文件上传方案,包括但不限于:添加 `file_url` åˆ—、新增 upload æŽ¥å£ã€æ³¨å…¥ FileApi。**
## æ–‡ä»¶ä¸Žä¸šåŠ¡çš„å…³è”æ–¹å¼
文件与业务记录的关联**只能通过 `system_storage_attachment` ä¸­é—´è¡¨**,不允许在业务表中添加 `file_url`、`file_urls`、`attachment_url` ç­‰å­—符串字段存储文件地址。
关联流程:
1. å‰ç«¯è°ƒç”¨ `POST /system/storage-blob/upload` ä¸Šä¼ æ–‡ä»¶ â†’ èŽ·å¾— `blobId`
2. å‰ç«¯è°ƒç”¨ `POST /system/storage-attachment/bind` ç»‘定文件到业务记录(传入 `recordType` + `recordId` + `blobItems`)
3. å‰ç«¯è°ƒç”¨ `GET /system/storage-attachment/list` æŸ¥è¯¢é™„ä»¶
4. å‰ç«¯è°ƒç”¨ `DELETE /system/storage-attachment/delete` åˆ é™¤é™„ä»¶
## æ­£ç¡®æŽ¥å£ï¼ˆsystem æ¨¡å—)
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/system/storage-blob/upload` | ä¸Šä¼ æ–‡ä»¶ï¼ˆ`files` å‚数,multipart) |
| POST | `/system/storage-blob/public-upload` | å…¬å…±æ–‡ä»¶ä¸Šä¼ ï¼ˆæ°¸ä¹…有效) |
| GET | `/system/storage-blob/preview/{fileName}` | æ–‡ä»¶é¢„览 |
| GET | `/system/storage-blob/download/{fileName}` | æ–‡ä»¶ä¸‹è½½ |
| GET | `/system/storage-attachment/list` | æŸ¥è¯¢é™„件列表 |
| POST | `/system/storage-attachment/bind` | ç»‘定附件到业务记录 |
| DELETE | `/system/storage-attachment/delete` | æ‰¹é‡åˆ é™¤é™„ä»¶ |
## ç¦æ­¢è¡Œä¸ºï¼ˆAI ä¸¥ç¦æ‰§è¡Œä»¥ä¸‹æ“ä½œï¼‰
### æ•°æ®åº“层面
- âŒ ç¦æ­¢åœ¨ä¸šåŠ¡è¡¨æ·»åŠ  `file_url`、`file_urls`、`attachment_url` ç­‰æ–‡ä»¶åœ°å€åˆ—
- âŒ ç¦æ­¢åœ¨ä¸šåŠ¡è¡¨æ·»åŠ ä»»ä½•ç”¨äºŽå­˜å‚¨æ–‡ä»¶ä¿¡æ¯çš„ JSON åˆ—
### Java åŽç«¯å±‚面
- âŒ ç¦æ­¢åœ¨ DO ç±»æ·»åŠ  `fileUrl`、`fileUrls`、`attachmentUrl` ç­‰å­—段
- âŒ ç¦æ­¢åœ¨ SaveReqVO / RespVO æ·»åŠ æ–‡ä»¶åœ°å€å­—æ®µ
- âŒ ç¦æ­¢åœ¨ MES/CRM/ERP/HRM ç­‰ä¸šåŠ¡æ¨¡å—æ–°å¢ž `@PostMapping("/upload")` æŽ¥å£
- âŒ ç¦æ­¢åœ¨ Controller ä¸­ä½¿ç”¨ `MultipartFile` å‚数(Excel å¯¼å…¥é™¤å¤–)
- âŒ ç¦æ­¢æ³¨å…¥æˆ–调用 `cn.iocoder.yudao.module.infra.api.file.FileApi`
### å‰ç«¯å±‚面
- âŒ ç¦æ­¢è°ƒç”¨ `/infra/file/upload` æˆ– `/infra/file/presigned-url`
- âŒ ç¦æ­¢è°ƒç”¨æ—§ç‰ˆ `src/api/infra/file/index.ts` ä¸­çš„ `uploadFile()` å‡½æ•°
## å¸¸è§ AI å¹»è§‰æ¨¡å¼ï¼ˆå¿…须识别并拒绝)
以下是 AI å®¹æ˜“幻觉生成的设计模式,遇到类似需求时直接拒绝:
1. **"加个 fileUrl å­—段就行"** â†’ é”™è¯¯ï¼Œå¿…须用 attachment ä¸­é—´è¡¨
2. **"参考其它模块的 fileUrl å­—段"** â†’ é”™è¯¯ï¼Œå…¶å®ƒæ¨¡å—çš„ fileUrl ä¹Ÿåº”被删除
3. **"在 Controller åŠ ä¸ª upload æŽ¥å£"** â†’ é”™è¯¯ï¼Œç»Ÿä¸€èµ° system æ¨¡å—
4. **"用 FileApi ä¸Šä¼ æ–‡ä»¶"** â†’ é”™è¯¯ï¼ŒFileApi æ˜¯ infra æ¨¡å—的旧接口
5. **"在表单保存接口加 MultipartFile å‚æ•°"** â†’ é”™è¯¯ï¼Œä¸Šä¼ å’Œä¿å­˜åˆ†ç¦»
## æ­£ç¡®ç¤ºä¾‹
### å‰ç«¯è°ƒç”¨
```typescript
import { uploadFile, bindAttachments, listAttachments } from '#/api/system/storage';
// 1. ä¸Šä¼ æ–‡ä»¶
const blobs = await uploadFile([file]);
const blobId = blobs[0].id;
// 2. ä¿å­˜ä¸šåŠ¡è®°å½•åŽï¼Œç»‘å®šé™„ä»¶
await bindAttachments({
  application: 'file',
  recordType: 'erp_purchase_order',
  recordId: savedRecord.id,
  blobItems: [{ blobId }],
});
// 3. æŸ¥è¯¢é™„ä»¶
const attachments = await listAttachments({
  recordType: 'erp_purchase_order',
  recordId: savedRecord.id,
});
```
### recordType å‘½åè§„范
格式:`{模块前缀}_{业务表名}`
| æ¨¡å— | recordType |
|------|------------|
| ERP é‡‡è´­è®¢å• | `erp_purchase_order` |
| ERP é‡‡è´­ç”³è¯· | `erp_purchase_request` |
| ERP é”€å”®è®¢å• | `erp_sale_order` |
| ERP æ”¶æ¬¾å• | `erp_finance_receipt` |
| ERP ä»˜æ¬¾å• | `erp_finance_payment` |
| HRM è¯·å‡ | `hrm_leave_application` |
| HRM ç¦»èŒ | `hrm_resignation_application` |
| HRM è°ƒåЍ | `hrm_transfer_application` |
| MES é‡‡è´­è®¢å• | `mes_purchase_order` |
| MES å·¥å• | `mes_pro_work_order` |
| CRM å®¢æˆ· | `crm_customer` |
## AI è‡ªæŸ¥æ¸…单
在实现任何文件上传相关需求前,逐项确认:
- [ ] æ˜¯å¦ä½¿ç”¨äº† `/system/storage-blob/upload` ä¸Šä¼ ï¼Ÿ
- [ ] æ˜¯å¦ä½¿ç”¨äº† `/system/storage-attachment/bind` å…³è”?
- [ ] æ˜¯å¦æ²¡æœ‰åœ¨ä¸šåŠ¡è¡¨ä¸­æ·»åŠ æ–‡ä»¶åœ°å€åˆ—ï¼Ÿ
- [ ] æ˜¯å¦æ²¡æœ‰åœ¨ DO/VO ä¸­æ·»åŠ æ–‡ä»¶åœ°å€å­—æ®µï¼Ÿ
- [ ] æ˜¯å¦æ²¡æœ‰åˆ›å»ºæ–°çš„ upload Controller æŽ¥å£ï¼Ÿ
- [ ] æ˜¯å¦æ²¡æœ‰ä½¿ç”¨ infra æ¨¡å—çš„ FileApi?
以上 6 é¡¹å…¨éƒ¨ä¸º"是"才可继续。
## ä¸ºä»€ä¹ˆ
- å­˜å‚¨é…ç½®ï¼ˆæœ¬åœ°/S3)在 system æ¨¡å—统一管理
- é™„件绑定机制(storage-attachment)提供 `recordType + recordId` çš„æ ‡å‡†å…³è”方式,支持多文件、用途分类
- infra æ¨¡å—çš„ `/infra/file/*` æ˜¯æ—§ç‰ˆæŽ¥å£ï¼Œå­˜åœ¨æ–‡ä»¶ä¸Žä¸šåŠ¡è®°å½•æ— æ³•å…³è”çš„é—®é¢˜
- ä¸šåŠ¡è¡¨å­˜å‚¨ file_url å­—符串无法支持多文件,且文件与业务记录之间缺乏标准化的关联管理
docs/attachment_upload_frontend_integration.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,306 @@
# æ–‡ä»¶ä¸Šä¼ æ”¹é€  - å‰ç«¯è”调方案
## åŽç«¯å˜æ›´æ‘˜è¦
所有模块的文件上传已从 `fileUrl` å­—符串字段改为 `system_storage_attachment` ä¸­é—´è¡¨æœºåˆ¶ï¼š
| å˜æ›´ | æ—§æ–¹å¼ | æ–°æ–¹å¼ |
|------|--------|--------|
| ä¸Šä¼  | ç›´æŽ¥å­˜ fileUrl | POST `/system/storage-blob/upload` â†’ èŽ·å– blobId |
| ä¿å­˜ | SaveReqVO.fileUrl | SaveReqVO.blobIds (List\<Long\>) |
| æŸ¥è¯¢ | RespVO.fileUrl | RespVO.attachmentList (List\<StorageBlob\>) |
| åˆ é™¤ | æ—  | åŽç«¯ 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
<script lang="ts" setup>
import { ref } from 'vue';
import { uploadFile } from '#/api/system/storage';
import type { UploadChangeParam } from 'ant-design-vue';
const blobIds = ref<number[]>([]);
const fileList = ref([]);
// æ–‡ä»¶ä¸Šä¼ å¤„理
async function handleUpload(info: UploadChangeParam) {
  if (info.file.status === 'done') {
    // æ–‡ä»¶ä¸Šä¼ æˆåŠŸï¼Œæ”¶é›† blobId
    const blobs = info.file.response;
    if (blobs?.length) {
      blobIds.value.push(blobs[0].id);
    }
  }
}
// ä¿å­˜æ—¶
async function handleSave() {
  const data = await formApi.getValues();
  data.blobIds = blobIds.value;  // å°† blobIds ä¼ ç»™åŽç«¯
  await createXxx(data);
}
// ç¼–辑时回显已有附件
async function loadDetail(id: number) {
  const detail = await getXxx(id);
  if (detail.attachmentList) {
    fileList.value = detail.attachmentList.map(item => ({
      uid: String(item.id),
      name: item.name || '',
      status: 'done',
      url: item.url,
    }));
    blobIds.value = detail.attachmentList.map(item => item.id);
  }
}
</script>
<template>
  <a-upload
    v-model:file-list="fileList"
    :action="`${baseURL}/system/storage-blob/upload`"
    multiple
    @change="handleUpload"
  >
    <a-button>
      <upload-outlined />
      ä¸Šä¼ é™„ä»¶
    </a-button>
  </a-upload>
</template>
```
**或者使用已经封装的 uploadFile å‡½æ•°é…åˆæ‰‹åŠ¨ä¸Šä¼ ï¼š**
- æŽ¨èæ–¹å¼ï¼šåœ¨æäº¤å‰å…ˆè°ƒç”¨ `uploadFile()` ä¸Šä¼ æ‰€æœ‰æ–‡ä»¶ï¼Œæ‹¿åˆ° blobIds åŽä¸€èµ·æäº¤
- å¤‡é€‰æ–¹å¼ï¼šä½¿ç”¨ a-upload ç»„件的自动上传,在 `@change` ä¸­æ”¶é›† blobId
### æ­¥éª¤ 3:修改详情展示
在详情/查看模式下展示附件列表(只读):
```vue
<template>
  <!-- é™„件展示 -->
  <div v-if="attachmentList?.length">
    <div class="text-gray-500 mb-2">附件</div>
    <div v-for="item in attachmentList" :key="item.id">
      <a :href="item.url" target="_blank">{{ item.name }}</a>
    </div>
  </div>
</template>
```
---
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| `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,只在详情接口返回
docs/attachment_upload_reform.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,168 @@
# é™„件上传模块改造 - å‰ç«¯è”调方案
## æ¦‚è¿°
本次改造统一所有模块的文件上传:
1. **后端**:删除业务表中的 `file_url` åˆ—,全部改用 `system_storage_attachment` ä¸­é—´è¡¨å…³è”
2. **前端**:FileUpload ç»„件统一使用 `/system/storage-blob/upload`,附件通过 attachment API å…³è”
## åŽç«¯å˜æ›´
### å·²åˆ é™¤ file_url åˆ—的表
| è¡¨å | æ¨¡å— |
|------|------|
| `erp_purchase_order` | ERP é‡‡è´­è®¢å• |
| `erp_purchase_request` | ERP é‡‡è´­ç”³è¯· |
| `erp_sale_order` | ERP é”€å”®è®¢å• |
| `hrm_leave_application` | HRM è¯·å‡ |
| `hrm_resignation_application` | HRM ç¦»èŒ |
| `hrm_transfer_application` | HRM è°ƒåЍ |
### å·²ä»Ž Java ç±»ä¸­åˆ é™¤ fileUrl å­—段
- 6 ä¸ª DO ç±»ï¼ˆErpPurchaseOrderDO、ErpPurchaseRequestDO、ErpSaleOrderDO、HrmLeaveApplicationDO、HrmResignationApplicationDO、HrmTransferApplicationDO)
- 6 ä¸ª SaveReqVO ç±»
- 6 ä¸ª RespVO ç±»
### æ–‡ä»¶å…³è”方式
文件只能通过 `system_storage_attachment` ä¸­é—´è¡¨å…³è”:
| API | è¯´æ˜Ž |
|-----|------|
| `POST /system/storage-blob/upload` | ä¸Šä¼ æ–‡ä»¶ï¼ˆ`files` å‚数,multipart) |
| `POST /system/storage-attachment/bind` | ç»‘定附件到业务记录 |
| `GET /system/storage-attachment/list` | æŸ¥è¯¢é™„件(按 recordType + recordId) |
| `DELETE /system/storage-attachment/delete` | æ‰¹é‡åˆ é™¤é™„ä»¶ |
## æ¶‰åŠé¡µé¢
### ä¸€ã€æ ¸å¿ƒä¸Šä¼ ç»„件改动
**文件**:`src/components/upload/use-upload.ts`
| æ”¹åЍ项 | æ—§å€¼ | æ–°å€¼ |
|--------|------|------|
| ä¸Šä¼ æŽ¥å£ | `POST /infra/file/upload` / S3 ç›´ä¼  | `POST /system/storage-blob/upload` |
| è¯·æ±‚方式 | è‡ªå®šä¹‰ `createFile0()` / PUT S3 | `FormData` + `requestClient.post()` |
| uploadUrl | `${apiURL}/infra/file/upload` | `${apiURL}/system/storage-blob/upload` |
### äºŒã€ä¸Šä¼ é™åˆ¶æ”¾å¼€
以下模块的 `FileUpload` / `ImageUpload` ç»„件已移除 `maxNumber`、`maxSize`、`accept` é™åˆ¶ï¼š
#### ERP æ¨¡å—(13 ä¸ªæ–‡ä»¶ï¼‰
| æ–‡ä»¶ | å­—段 |
|------|------|
| `src/views/erp/purchase/order/data.ts` | `fileUrl` |
| `src/views/erp/purchase/request/data.ts` | `fileUrl` |
| `src/views/erp/purchase/return/data.ts` | `fileUrl` |
| `src/views/erp/purchase/in/data.ts` | `fileUrl` |
| `src/views/erp/sale/order/data.ts` | `fileUrl` |
| `src/views/erp/sale/return/data.ts` | `fileUrl` |
| `src/views/erp/sale/out/data.ts` | `fileUrl` |
| `src/views/erp/stock/in/data.ts` | `fileUrl` |
| `src/views/erp/stock/out/data.ts` | `fileUrl` |
| `src/views/erp/stock/move/data.ts` | `fileUrl` |
| `src/views/erp/stock/check/data.ts` | `fileUrl` |
| `src/views/erp/finance/receipt/data.ts` | `fileUrl` |
| `src/views/erp/finance/payment/data.ts` | `fileUrl` |
#### ImageUpload é™åˆ¶æ”¾å¼€ï¼ˆ5 å¤„)
| æ–‡ä»¶ | å­—段 |
|------|------|
| `src/views/im/manager/face/pack/data.ts` | `icon` |
| `src/views/im/manager/face/pack/data.ts` | `url` |
| `src/views/im/manager/channel/material/data.ts` | `coverUrl` |
| `src/views/im/manager/channel/list/data.ts` | `avatar` |
| `src/views/mes/md/item/modules/product-media-list.vue` | `url` |
#### MES æ–‡æ¡£é™åˆ¶æ”¾å¼€
| æ–‡ä»¶ | å­—段 |
|------|------|
| `src/views/mes/pd/project/modules/document-form.vue` | `fileUrl` |
### ä¸‰ã€HRM é™„件上传修复
3 ä¸ª HRM æ¨¡å—çš„"附件"字段从 `Input` æ”¹ä¸º `FileUpload`:
| æ–‡ä»¶ | å­—段 | æ”¹åЍ |
|------|------|------|
| `src/views/hrm/leave/data.ts` | `fileUrl` | `component: 'Input'` â†’ `component: 'FileUpload'` |
| `src/views/hrm/resignation/data.ts` | `fileUrl` | `component: 'Input'` â†’ `component: 'FileUpload'` |
| `src/views/hrm/transfer/data.ts` | `fileUrl` | `component: 'Input'` â†’ `component: 'FileUpload'` |
## API
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž |
|------|------|------|
| POST | `/system/storage-blob/upload` | ä¸Šä¼ æ–‡ä»¶ï¼Œ`files`(multipart æ•°ç»„) |
| POST | `/system/storage-attachment/bind` | ç»‘定附件到业务记录 |
| GET | `/system/storage-attachment/list` | æŸ¥è¯¢ä¸šåŠ¡è®°å½•çš„é™„ä»¶åˆ—è¡¨ |
| DELETE | `/system/storage-attachment/delete` | æ‰¹é‡åˆ é™¤é™„ä»¶ |
**上传响应**:
```json
[
  {
    "id": 123,
    "url": "https://xxx.com/u_abc123.pdf",
    "previewURL": "https://xxx.com/preview/u_abc123.pdf",
    "name": "合同.pdf",
    "downloadURL": "https://xxx.com/download/u_abc123.pdf"
  }
]
```
**绑定请求**:
```json
{
  "application": "file",
  "recordType": "mes_purchase_order",
  "recordId": 1,
  "blobItems": [{ "blobId": 123 }]
}
```
## æ ‡å‡†ä¸Šä¼ æµç¨‹
1. ç”¨æˆ·é€‰æ‹©æ–‡ä»¶ â†’ `POST /system/storage-blob/upload` è¿”回 `[{ id, url, name }]`
2. ä¸šåŠ¡è®°å½•ä¿å­˜æ—¶ â†’ `POST /system/storage-attachment/bind` å…³è” blobId
3. è¯¦æƒ…/编辑页 â†’ `GET /system/storage-attachment/list` åŠ è½½å·²æœ‰é™„ä»¶
4. ç”¨æˆ·åˆ é™¤é™„ä»¶ â†’ `DELETE /system/storage-attachment/delete`
## å‰ç«¯ API å°è£…
新 API æ¨¡å—位于 `src/api/system/storage/index.ts`:
```typescript
import { uploadFile, listAttachments, bindAttachments, deleteAttachments } from '#/api/system/storage';
```
## recordType å‘½åè§„范
格式:`{模块前缀}_{业务表名}`
| æ¨¡å— | recordType | è¯´æ˜Ž |
|------|------------|------|
| MES é‡‡è´­è®¢å• | `mes_purchase_order` | é‡‡è´­è®¢å•附件 |
| MES å·¥å• | `mes_pro_work_order` | ç”Ÿäº§å·¥å•附件 |
| MES è®¾å¤‡ | `mes_dv_machinery` | è®¾å¤‡æ¡£æ¡ˆé™„ä»¶ |
| MES æ–‡æ¡£ | `mes_pd_document` | é¡¹ç›®æ–‡æ¡£é™„ä»¶ |
| CRM å®¢æˆ· | `crm_customer` | å®¢æˆ·é™„ä»¶ |
| ERP é‡‡è´­è®¢å• | `erp_purchase_order` | ERP é‡‡è´­è®¢å•附件 |
## æ³¨æ„äº‹é¡¹
- FileUpload ç»„件使用 `POST /system/storage-blob/upload` ä¸Šä¼ æ–‡ä»¶ï¼ŒFormData å­—段名为 `files`
- æ–‡ä»¶ä¸Šä¼ åŽä»…暂存,必须调用 bind æŽ¥å£å…³è”到业务记录
- æ–‡ä»¶é¢„览使用响应中的 `previewURL` æˆ– `url` å­—段
- ç¦æ­¢ä½¿ç”¨æ—§ç‰ˆ `/infra/file/*` æŽ¥å£
- HRM è¯·å‡/离职/调动页面的附件字段现在可以使用 FileUpload ç»„件上传附件
docs/file_upload_guide.md
@@ -2,243 +2,171 @@
## æ¦‚è¿°
本系统提供了**统一的文件上传服务**,支持多种存储方式(本地、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`
docs/mdm_item_sync_mes_optimization.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,97 @@
# MDM ç‰©æ–™åŒæ­¥ MES ä¼˜åŒ– - å‰ç«¯è”调方案
## æ¶‰åŠé¡µé¢
- åŸºç¡€æ•°æ® > MDM > ç‰©æ–™ç®¡ç†ï¼ˆåˆ—表页)
- åŸºç¡€æ•°æ® > MDM > ç‰©æ–™ç®¡ç†ï¼ˆæ–°å¢ž/编辑弹窗)
## å˜æ›´æ¦‚è¿°
1. **移除 `syncMes` å¤é€‰æ¡†**:创建/编辑物料时不再需要手动勾选"同步到 MES",后端自动同步
2. **列表新增同步状态列**:显示物料是否已同步到 MES
3. **状态/删除实时同步**:MDM ä¾§æ›´æ–°çŠ¶æ€æˆ–åˆ é™¤ç‰©æ–™æ—¶ï¼Œè‡ªåŠ¨åŒæ­¥åˆ° MES
## API å˜æ›´
### 1. GET `/mdm/item/page` - å“åº”新增字段
**响应字段新增:**
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| syncedMes | Boolean | æ˜¯å¦å·²åŒæ­¥åˆ° MES。true=已同步,false/null=未同步 |
**响应示例:**
```json
{
  "code": 0,
  "data": {
    "list": [
      {
        "id": 1,
        "code": "ITEM001",
        "name": "螺丝",
        "syncedMes": true,
        "...": "..."
      },
      {
        "id": 2,
        "code": "ITEM002",
        "name": "螺母",
        "syncedMes": false,
        "...": "..."
      }
    ],
    "total": 100
  }
}
```
### 2. GET `/mdm/item/get` - å“åº”新增字段
同上,`syncedMes` è¡¨ç¤ºè¯¥ç‰©æ–™æ˜¯å¦å·²åŒæ­¥åˆ° MES。
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| syncedMes | åˆ—表新增列 | true æ—¶æ˜¾ç¤ºç»¿è‰²"已同步"标签,false æ—¶æ˜¾ç¤ºç°è‰²"未同步"标签 |
## ä¸šåŠ¡è§„åˆ™è¯´æ˜Ž
| åœºæ™¯ | è§„则 |
|------|------|
| æ–°å¢žç‰©æ–™ | åŽç«¯è‡ªåŠ¨åŒæ­¥åˆ° MES,列表中 syncedMes ä¸º true |
| ç¼–辑物料 | åŽç«¯è‡ªåŠ¨åŒæ­¥åˆ° MES |
| æ›´æ–°ç‰©æ–™çŠ¶æ€ | åŽç«¯è‡ªåŠ¨åŒæ­¥çŠ¶æ€åˆ° MES(启用/禁用) |
| åˆ é™¤ç‰©æ–™ | åŽç«¯è‡ªåŠ¨åŒæ­¥åˆ é™¤ MES ä¸­çš„对应记录(如存在) |
| è¡¨å•中的 syncMes å­—段 | å‰ç«¯å¯ä¿ç•™ä½†ä¸å†ç”Ÿæ•ˆï¼Œå»ºè®®éšè—"同步到 MES"开关 |
## å‰ç«¯ä¿®æ”¹ç‚¹
### 1. TypeScript ç±»åž‹å®šä¹‰
`src/api/mdm/item/index.ts` ä¸­ `MdmItemApi.Item` æŽ¥å£æ–°å¢ž `syncedMes?: boolean` å­—段。
### 2. åˆ—表新增同步状态列
`src/views/basicData/mdm/data.ts` ä¸­ `useGridColumns()` æ–°å¢žä¸€åˆ—:
- field: `syncedMes`
- title: "同步状态"
- ä½¿ç”¨æ’槽渲染 Tag ç»„ä»¶
### 3. åˆ—表页模板
`src/views/basicData/mdm/index.vue` ä¸­æ–°å¢ž `syncedMes` æ’槽模板,展示同步状态标签。
### 4. è¡¨å•移除 syncMes å¼€å…³ï¼ˆå¯é€‰ï¼‰
`src/views/basicData/mdm/data.ts` ä¸­ `useFormSchema()` å¯éšè—æˆ–移除 `syncMes` è¡¨å•项,后端已忽略此参数。
## æ³¨æ„äº‹é¡¹
- åŽ†å²æ•°æ®ä¸­æœªåŒæ­¥åˆ° MES çš„物料,`syncedMes` ä¸º false;可通过编辑物料触发自动同步
- MES åŒæ­¥å¤±è´¥ä¼šå›žæ»š MDM æ“ä½œï¼ˆä¿æŒæ•°æ®ä¸€è‡´æ€§ï¼‰ï¼Œå‰ç«¯éœ€å…³æ³¨æŽ¥å£è¿”回的错误信息
- `syncedMes` å­—段在详情页(getItem)也会返回,可用于详情页显示同步状态
docs/¸½¼þÉÏ´«Ä£¿é»ã×Ü.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,131 @@
# é™„件上传模块汇总
## ä¸€ã€æ ¸å¿ƒä¸Šä¼ ç»„ä»¶
路径:`src/components/upload/`
| ç»„ä»¶ | æ–‡ä»¶ | ç”¨é€” |
|---|---|---|
| `FileUpload` | `file-upload.vue` | é€šç”¨æ–‡ä»¶ä¸Šä¼ ï¼Œæ–‡æœ¬åˆ—表展示,支持拖拽、类型/大小校验 |
| `ImageUpload` | `image-upload.vue` | å›¾ç‰‡ä¸Šä¼ ï¼Œå¡ç‰‡é¢„览模式,支持图片预览弹窗 |
| `InputUpload` | `input-upload.vue` | è¾“入框+上传组合,上传按钮在输入框后缀或文本域右下角 |
| `useUpload()` | `use-upload.ts` | æ ¸å¿ƒç»„合函数,统一使用 system æ¨¡å— Storage API(POST `/system/storage-blob/upload`) |
| `CropperAvatar` | `src/components/cropper/cropper-avatar.vue` | å¤´åƒè£å‰ªä¸Šä¼  |
### ä¸Šä¼ æ¨¡å¼
- **服务端上传**:POST `/system/storage-blob/upload`,统一使用 system æ¨¡å— Storage API
- **附件绑定**:POST `/system/storage-attachment/bind`,将上传的文件关联到业务记录
### è¡¨å•适配器注册
路径:`src/adapter/component/index.ts`
注册了 `Upload`、`FileUpload`、`ImageUpload`、`PreviewUpload` å››ç§è¡¨å•组件类型,可在表单schema中直接使用。
---
## äºŒã€FileUpload ä½¿ç”¨æ¨¡å—(附件上传)
### ERP æ¨¡å—
| æ¨¡å— | æ–‡ä»¶ | å­—段 | é™åˆ¶ |
|---|---|---|---|
| é‡‡è´­è®¢å• | `src/views/erp/purchase/order/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| é‡‡è´­ç”³è¯· | `src/views/erp/purchase/request/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| é‡‡è´­é€€è´§ | `src/views/erp/purchase/return/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| é‡‡è´­å…¥åº“ | `src/views/erp/purchase/in/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| é”€å”®è®¢å• | `src/views/erp/sale/order/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| é”€å”®é€€è´§ | `src/views/erp/sale/return/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| é”€å”®å‡ºåº“ | `src/views/erp/sale/out/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| åº“存入库 | `src/views/erp/stock/in/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| åº“存出库 | `src/views/erp/stock/out/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| åº“存调拨 | `src/views/erp/stock/move/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| åº“存盘点 | `src/views/erp/stock/check/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| æ”¶æ¬¾å• | `src/views/erp/finance/receipt/data.ts` | `fileUrl` | æ— é™åˆ¶ |
| ä»˜æ¬¾å• | `src/views/erp/finance/payment/data.ts` | `fileUrl` | æ— é™åˆ¶ |
### CRM æ¨¡å—
| æ¨¡å— | æ–‡ä»¶ | å­—段 | é™åˆ¶ |
|---|---|---|---|
| è·Ÿè¿›è®°å½• | `src/views/crm/followup/data.ts` | `fileUrls` | â€” |
### MES æ¨¡å—
| æ¨¡å— | æ–‡ä»¶ | å­—段 | é™åˆ¶ |
|---|---|---|---|
| é¡¹ç›®æ–‡æ¡£ | `src/views/mes/pd/project/modules/document-form.vue` | `fileUrl` | æ— é™åˆ¶, directory:mes/pd |
---
## ä¸‰ã€ImageUpload ä½¿ç”¨æ¨¡å—(图片上传)
| æ¨¡å— | æ–‡ä»¶ | å­—段 | ç”¨é€” |
|---|---|---|---|
| AI角色管理 | `src/views/ai/model/chatRole/data.ts` | `avatar` | è§’色头像 |
| CRM跟进记录 | `src/views/crm/followup/data.ts` | `picUrls` | å›¾ç‰‡ |
| åŸºç¡€æ¼”示 | `src/views/infra/demo/demo01/data.ts` | `avatar` | å¤´åƒ |
| OAuth2应用 | `src/views/system/oauth2/client/data.ts` | `logo` | åº”用图标 |
| IM表情包 | `src/views/im/manager/face/pack/data.ts` | `icon` | å°é¢ |
| IM表情包 | `src/views/im/manager/face/pack/data.ts` | `url` | è¡¨æƒ…图 |
| IM频道素材 | `src/views/im/manager/channel/material/data.ts` | `coverUrl` | å°é¢å›¾ |
| IM频道列表 | `src/views/im/manager/channel/list/data.ts` | `avatar` | é¢‘道头像 |
| MES产品媒体 | `src/views/mes/md/item/modules/product-media-list.vue` | `url` | äº§å“å›¾ç‰‡ |
| BPM流程表单 | `src/views/bpm/model/form/modules/basic-info.vue` | å›¾æ ‡å­—段 | è¡¨å•图标 |
---
## å››ã€Upload ä½¿ç”¨æ¨¡å—(Excel导入)
| æ¨¡å— | æ–‡ä»¶ | ç”¨é€” | API |
|---|---|---|---|
| ç³»ç»Ÿç”¨æˆ· | `src/views/system/user/modules/import-form.vue` | Excel导入用户 | `POST /system/user/import` |
| CRM客户 | `src/views/crm/customer/modules/import-form.vue` | Excel导入客户 | `POST /crm/customer/import` |
| MES物料 | `src/views/mes/md/item/modules/import-form.vue` | Excel导入物料 | `POST /mes/md/item/import` |
| MES设备 | `src/views/mes/dv/machinery/modules/import-form.vue` | Excel导入设备 | `POST /mes/dv/machinery/import` |
| HRM考勤 | `src/views/hrm/attendance/record/modules/import-form.vue` | Excel导入考勤 | `POST /hrm/attendance/record/import` |
---
## äº”、其他上传场景
| æ¨¡å— | æ–‡ä»¶ | ç”¨é€” | å®žçŽ°æ–¹å¼ |
|---|---|---|---|
| AI知识库 | `src/views/ai/knowledge/document/form/modules/upload-step.vue` | æ–‡æ¡£ä¸Šä¼ ï¼ˆTXT/PDF/DOC等,15MB) | `UploadDragger` |
| AI聊天 | `src/views/ai/chat/index/modules/message/file-upload.vue` | æ–‡ä»¶ä¸Šä¼ ï¼ˆ5个/10MB) | åŽŸç”Ÿinput + `useUpload().httpRequest()` |
| IM聊天 | `src/views/im/home/composables/useMediaUploader.ts` | å›¾ç‰‡/文件/语音/视频上传 | `uploadFile()` API |
| IM消息输入 | `src/views/im/home/pages/conversation/components/input/message-input.vue` | é€‰æ‹©æ–‡ä»¶å‘送 | åŽŸç”Ÿinput + `useMediaUploader` |
| IM表情上传 | `src/views/im/home/pages/conversation/components/input/face-picker.vue` | è‡ªå®šä¹‰è¡¨æƒ…上传 | `uploadFile()` API |
| BPM签名 | `src/views/bpm/processInstance/detail/modules/signature.vue` | æ‰‹å†™ç­¾åä¸Šä¼  | `uploadFile()` API(base64转文件) |
| ç”¨æˆ·å¤´åƒ | `src/views/_core/profile/modules/profile-user.vue` | å¤´åƒè£å‰ªä¸Šä¼  | `CropperAvatar` + `useUpload().httpRequest()` |
| TinyMCE | `src/components/tinymce/img-upload.vue` | å¯Œæ–‡æœ¬å›¾ç‰‡ä¸Šä¼  | `Upload`组件 |
| Tiptap | `src/packages/effects/plugins/src/tiptap/tiptap.vue` | å¯Œæ–‡æœ¬å›¾ç‰‡ä¸Šä¼  | Tiptap扩展 |
| MES供应商 | `src/api/mes/md/vendor/index.ts` | Excel导入供应商 | `POST /mes/md-vendor/import` |
| MES客户 | `src/api/mes/md/client/index.ts` | Excel导入MES客户 | `POST /mes/md-client/import` |
| MES项目 | `src/api/mes/pd/project/index.ts` | é¡¹ç›®æ–‡æ¡£ä¸Šä¼  | `PUT /mes/pd/project/upload` |
---
## å…­ã€HRM é™„件上传(已修复)
HRM æ¨¡å—çš„"附件"字段已从 `Input` æ”¹ä¸º `FileUpload` ç»„件。
| æ¨¡å— | æ–‡ä»¶ | å­—段 | ç»„ä»¶ | çŠ¶æ€ |
|---|---|---|---|---|
| è¯·å‡ | `src/views/hrm/leave/data.ts` | `fileUrl` | `FileUpload` | å·²ä¿®å¤ |
| ç¦»èŒ | `src/views/hrm/resignation/data.ts` | `fileUrl` | `FileUpload` | å·²ä¿®å¤ |
| è°ƒåЍ | `src/views/hrm/transfer/data.ts` | `fileUrl` | `FileUpload` | å·²ä¿®å¤ |
---
## ä¸ƒã€ä¸Šä¼ ç›¸å…³API
| å‡½æ•° | ç«¯ç‚¹ | ç”¨é€” |
|---|---|---|
| `uploadFile()` | `POST /system/storage-blob/upload` | ç»Ÿä¸€æ–‡ä»¶ä¸Šä¼  |
| `bindAttachments()` | `POST /system/storage-attachment/bind` | ç»‘定附件到业务记录 |
| `listAttachments()` | `GET /system/storage-attachment/list` | æŸ¥è¯¢é™„件列表 |
| `deleteAttachments()` | `DELETE /system/storage-attachment/delete` | æ‰¹é‡åˆ é™¤é™„ä»¶ |
API定义文件:`src/api/system/storage/index.ts`
sql/mysql/hrm_module_init.sql
@@ -122,7 +122,6 @@
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-草稿,10-审批中,20-通过,30-驳回,40-撤销',
    `process_instance_id` varchar(64) DEFAULT NULL COMMENT 'BPM流程实例ID',
    `approve_time` datetime DEFAULT NULL COMMENT '审批完成时间',
    `file_url` varchar(500) DEFAULT NULL COMMENT '附件地址',
    `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -150,7 +149,6 @@
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-草稿,10-审批中,20-通过,30-驳回,40-撤销',
    `process_instance_id` varchar(64) DEFAULT NULL COMMENT 'BPM流程实例ID',
    `approve_time` datetime DEFAULT NULL COMMENT '审批完成时间',
    `file_url` varchar(500) DEFAULT NULL COMMENT '附件地址',
    `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -178,7 +176,6 @@
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-草稿,10-审批中,20-通过,30-驳回,40-撤销',
    `process_instance_id` varchar(64) DEFAULT NULL COMMENT 'BPM流程实例ID',
    `approve_time` datetime DEFAULT NULL COMMENT '审批完成时间',
    `file_url` varchar(500) DEFAULT NULL COMMENT '附件地址',
    `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -255,7 +252,6 @@
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-草稿,10-审批中,20-通过,30-驳回,40-撤销',
    `process_instance_id` varchar(64) DEFAULT NULL COMMENT 'BPM流程实例ID',
    `approve_time` datetime DEFAULT NULL COMMENT '审批完成时间',
    `file_url` varchar(500) DEFAULT NULL COMMENT '附件地址',
    `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
@@ -359,7 +355,6 @@
    `payment_method` tinyint NOT NULL COMMENT '发放方式:1-银行转账,2-现金',
    `payment_time` datetime NOT NULL COMMENT '发放时间',
    `status` tinyint NOT NULL DEFAULT 0 COMMENT '状态:0-待发放,10-已发放',
    `file_url` varchar(500) DEFAULT NULL COMMENT '银行回单附件',
    `remark` varchar(500) DEFAULT NULL COMMENT '备注',
    `creator` varchar(64) DEFAULT '' COMMENT '创建者',
    `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/CrmFollowUpRecordController.java
@@ -17,6 +17,7 @@
import cn.iocoder.yudao.module.crm.service.contact.CrmContactService;
import cn.iocoder.yudao.module.crm.service.followup.CrmFollowUpRecordService;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@@ -58,6 +59,8 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建跟进记录")
@@ -85,7 +88,9 @@
                getLoginUserId(), CrmPermissionLevelEnum.READ)) {
            throw exception(CRM_PERMISSION_DENIED, CrmBizTypeEnum.getNameByType(followUpRecord.getBizType()));
        }
        return success(BeanUtils.toBean(followUpRecord, CrmFollowUpRecordRespVO.class));
        CrmFollowUpRecordRespVO respVO = BeanUtils.toBean(followUpRecord, CrmFollowUpRecordRespVO.class);
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("crm_follow_up_record", id));
        return success(respVO);
    }
    @GetMapping("/page")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.crm.controller.admin.followup.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.module.crm.controller.admin.business.vo.business.CrmBusinessRespVO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
@@ -48,8 +49,8 @@
    @Schema(description = "图片")
    private List<String> picUrls;
    @Schema(description = "附件")
    private List<String> fileUrls;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "创建人", example = "1024")
    @ExcelProperty("创建人")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/followup/vo/CrmFollowUpRecordSaveReqVO.java
@@ -35,6 +35,9 @@
    @NotNull(message = "下次联系时间不能为空")
    private LocalDateTime nextTime;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "关联的商机编号数组")
    private List<Long> businessIds;
    @Schema(description = "关联的联系人编号数组")
@@ -42,7 +45,4 @@
    @Schema(description = "图片 URL åˆ—表")
    private List<String> picUrls;
    @Schema(description = "附件 URL åˆ—表")
    private List<String> fileUrls;
}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/CrmSaleQuotationController.java
@@ -22,6 +22,7 @@
import cn.iocoder.yudao.module.mdm.api.unit.dto.MdmUnitMeasureRespDTO;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@@ -65,6 +66,8 @@
    private MdmUnitMeasureApi mdmUnitMeasureApi;
    @Resource
    private CrmCustomerService customerService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建销售报价单")
@@ -177,6 +180,8 @@
        }
        // 3. å¡«å……关联信息
        fillSaleQuotationRelatedFields(respVO);
        // 4. å¡«å……附件列表
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("crm_sale_quotation", saleQuotation.getId()));
        return respVO;
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.crm.controller.admin.quotation.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -103,8 +104,8 @@
    @ExcelProperty("关联合同单号")
    private String contractNo;
    @Schema(description = "附件地址", example = "https://www.baidu.com")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注", example = "备注信息")
    @ExcelProperty("备注")
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/controller/admin/quotation/vo/CrmSaleQuotationSaveReqVO.java
@@ -53,8 +53,8 @@
    @Schema(description = "税率", example = "13.00")
    private BigDecimal taxRate;
    @Schema(description = "附件地址", example = "https://www.baidu.com")
    private String fileUrl;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/followup/CrmFollowUpRecordDO.java
@@ -73,12 +73,6 @@
    @TableField(typeHandler = StringListTypeHandler.class)
    private List<String> picUrls;
    /**
     * é™„ä»¶
     */
    @TableField(typeHandler = StringListTypeHandler.class)
    private List<String> fileUrls;
    /**
     * å…³è”的商机编号数组
     *
     * å…³è” {@link CrmBusinessDO#getId()}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/dal/dataobject/quotation/CrmSaleQuotationDO.java
@@ -125,10 +125,6 @@
     */
    private String contractNo;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/followup/CrmFollowUpRecordServiceImpl.java
@@ -18,6 +18,7 @@
import cn.iocoder.yudao.module.crm.service.customer.CrmCustomerService;
import cn.iocoder.yudao.module.crm.service.followup.bo.CrmFollowUpCreateReqBO;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -60,6 +61,8 @@
    @Resource
    @Lazy
    private CrmCustomerService customerService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    @CrmPermission(bizTypeValue = "#createReqVO.bizType", bizId = "#createReqVO.bizId", level = CrmPermissionLevelEnum.WRITE)
@@ -67,6 +70,8 @@
        // 1. åˆ›å»ºæ›´è¿›è®°å½•
        CrmFollowUpRecordDO record = BeanUtils.toBean(createReqVO, CrmFollowUpRecordDO.class);
        crmFollowUpRecordMapper.insert(record);
        storageAttachmentApi.bindAttachments("file", "crm_follow_up_record",
                record.getId(), createReqVO.getBlobIds());
        // 2. æ›´æ–° bizId å¯¹åº”的记录
        if (ObjUtil.equal(CrmBizTypeEnum.CRM_CUSTOMER.getType(), record.getBizType())) { // æ›´æ–°å®¢æˆ·è·Ÿè¿›ä¿¡æ¯
@@ -113,12 +118,18 @@
            throw exception(FOLLOW_UP_RECORD_DELETE_DENIED);
        }
        // åˆ é™¤é™„ä»¶
        storageAttachmentApi.deleteAttachmentsByRecord("crm_follow_up_record", id);
        // åˆ é™¤
        crmFollowUpRecordMapper.deleteById(id);
    }
    @Override
    public void deleteFollowUpRecordByBiz(Integer bizType, Long bizId) {
        List<CrmFollowUpRecordDO> records = crmFollowUpRecordMapper.selectListByBiz(bizType, List.of(bizId));
        for (CrmFollowUpRecordDO record : records) {
            storageAttachmentApi.deleteAttachmentsByRecord("crm_follow_up_record", record.getId());
        }
        crmFollowUpRecordMapper.deleteByBiz(bizType, bizId);
    }
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/followup/bo/CrmFollowUpCreateReqBO.java
@@ -57,11 +57,6 @@
     */
    private List<String> picUrls;
    /**
     * é™„ä»¶
     */
    private List<String> fileUrls;
    /**
     * å…³è”的商机编号数组
     *
     * å…³è” {@link CrmBusinessDO#getId()}
yudao-module-crm/src/main/java/cn/iocoder/yudao/module/crm/service/quotation/CrmSaleQuotationServiceImpl.java
@@ -23,6 +23,7 @@
import cn.iocoder.yudao.module.crm.api.customer.CrmCustomerApi;
import cn.iocoder.yudao.module.crm.service.permission.CrmPermissionService;
import cn.iocoder.yudao.module.crm.service.permission.bo.CrmPermissionCreateReqBO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.mdm.api.item.MdmItemApi;
import cn.iocoder.yudao.module.mdm.api.item.dto.MdmItemRespDTO;
import jakarta.annotation.Resource;
@@ -70,6 +71,8 @@
    @Resource
    private CrmPermissionService permissionService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private CrmContractService contractService;
    @Override
@@ -100,6 +103,8 @@
                .setBizType(CrmBizTypeEnum.CRM_SALE_QUOTATION.getType())
                .setBizId(saleQuotation.getId())
                .setLevel(CrmPermissionLevelEnum.OWNER.getLevel()));
        storageAttachmentApi.bindAttachments("file", "crm_sale_quotation",
                saleQuotation.getId(), createReqVO.getBlobIds());
        return saleQuotation.getId();
    }
@@ -123,6 +128,11 @@
        saleQuotationMapper.updateById(updateObj);
        // 2.2 æ›´æ–°æŠ¥ä»·æ˜Žç»†
        updateQuotationProductList(updateReqVO.getId(), quotationProducts);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("crm_sale_quotation", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "crm_sale_quotation",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
@@ -141,6 +151,8 @@
        // 2. éåŽ†åˆ é™¤
        saleQuotations.forEach(saleQuotation -> {
            // 2.0 åˆ é™¤é™„件关联
            storageAttachmentApi.deleteAttachmentsByRecord("crm_sale_quotation", saleQuotation.getId());
            // 2.1 åˆ é™¤æŠ¥ä»·å•
            saleQuotationMapper.deleteById(saleQuotation.getId());
            // 2.2 åˆ é™¤æŠ¥ä»·æ˜Žç»†
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderRespVO.java
@@ -63,10 +63,6 @@
    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
    private BigDecimal depositPrice;
    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
    @ExcelProperty("附件地址")
    private String fileUrl;
    @Schema(description = "备注", example = "你猜")
    @ExcelProperty("备注")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/order/ErpPurchaseOrderSaveReqVO.java
@@ -32,9 +32,6 @@
    @Schema(description = "定金金额,单位:元", example = "7127")
    private BigDecimal depositPrice;
    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
    private String fileUrl;
    @Schema(description = "备注", example = "你猜")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestRespVO.java
@@ -64,9 +64,6 @@
    @Schema(description = "入库状态(0:未入库 1:部分入库 2:全部入库)", example = "0")
    private Integer inStatus;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/vo/request/ErpPurchaseRequestSaveReqVO.java
@@ -33,9 +33,6 @@
    @Schema(description = "定金金额", example = "1000.00")
    private BigDecimal depositPrice;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderRespVO.java
@@ -72,10 +72,6 @@
    @Schema(description = "定金金额,单位:元", requiredMode = Schema.RequiredMode.REQUIRED, example = "7127")
    private BigDecimal depositPrice;
    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
    @ExcelProperty("附件地址")
    private String fileUrl;
    @Schema(description = "备注", example = "你猜")
    @ExcelProperty("备注")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/sale/vo/order/ErpSaleOrderSaveReqVO.java
@@ -41,9 +41,6 @@
    @Schema(description = "定金金额,单位:元", example = "7127")
    private BigDecimal depositPrice;
    @Schema(description = "附件地址", example = "https://www.iocoder.cn")
    private String fileUrl;
    @Schema(description = "备注", example = "你猜")
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseOrderDO.java
@@ -92,10 +92,6 @@
    private BigDecimal depositPrice;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/purchase/ErpPurchaseRequestDO.java
@@ -105,11 +105,6 @@
    private BigDecimal depositPrice;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/dal/dataobject/sale/ErpSaleOrderDO.java
@@ -106,10 +106,6 @@
    private BigDecimal depositPrice;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationRespVO.java
@@ -3,8 +3,10 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è¯·å‡ç”³è¯· Response VO")
@Data
@@ -58,8 +60,8 @@
    @Schema(description = "审批完成时间", example = "2024-01-01 10:00:00")
    private LocalDateTime approveTime;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmLeaveApplicationSaveReqVO.java
@@ -7,6 +7,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è¯·å‡ç”³è¯·æ–°å¢ž/修改 Request VO")
@Data
@@ -41,8 +42,8 @@
    @NotEmpty(message = "请假原因不能为空")
    private String reason;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmResignationApplicationRespVO.java
@@ -3,8 +3,10 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - ç¦»èŒç”³è¯· Response VO")
@Data
@@ -70,8 +72,8 @@
    @Schema(description = "审批完成时间", example = "2024-01-01 10:00:00")
    private LocalDateTime approveTime;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmResignationApplicationSaveReqVO.java
@@ -6,6 +6,7 @@
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - ç¦»èŒç”³è¯·æ–°å¢ž/修改 Request VO")
@Data
@@ -38,8 +39,8 @@
    @Schema(description = "交接说明", example = "交接说明")
    private String handoverRemark;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmTransferApplicationRespVO.java
@@ -3,8 +3,10 @@
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è°ƒå²—申请 Response VO")
@Data
@@ -70,8 +72,8 @@
    @Schema(description = "审批完成时间", example = "2024-01-01 10:00:00")
    private LocalDateTime approveTime;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/approval/vo/HrmTransferApplicationSaveReqVO.java
@@ -6,6 +6,7 @@
import lombok.Data;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - è°ƒå²—申请新增/修改 Request VO")
@Data
@@ -43,8 +44,8 @@
    @NotEmpty(message = "调岗原因不能为空")
    private String reason;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.pdf")
    private String fileUrl;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/HrmAttendanceExceptionController.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.hrm.service.employee.HrmEmployeeService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -39,6 +40,8 @@
    private HrmEmployeeService employeeService;
    @Resource
    private DeptApi deptApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "新增考勤异常申请")
@@ -87,6 +90,7 @@
                respVO.setDeptName(dept.getName());
            }
        }
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_attendance_exception", id));
        return success(respVO);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
@@ -9,6 +10,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è€ƒå‹¤å¼‚常申请 Response VO")
@Data
@@ -66,8 +68,8 @@
    @ExcelProperty("审批完成时间")
    private LocalDateTime approveTime;
    @Schema(description = "附件地址")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注")
    @ExcelProperty("备注")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/attendance/vo/HrmAttendanceExceptionSaveReqVO.java
@@ -7,6 +7,7 @@
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è€ƒå‹¤å¼‚常申请新增/修改 Request VO")
@Data
@@ -30,8 +31,8 @@
    @NotEmpty(message = "申请原因不能为空")
    private String reason;
    @Schema(description = "附件地址", example = "https://xxx.com/xxx.jpg")
    private String fileUrl;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "备注", example = "备注信息")
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/HrmSalaryPaymentController.java
@@ -15,6 +15,7 @@
import cn.iocoder.yudao.module.hrm.service.salary.HrmSalaryPaymentService;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -41,6 +42,8 @@
    private HrmEmployeeService employeeService;
    @Resource
    private DeptApi deptApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @GetMapping("/page")
    @Operation(summary = "获得薪资发放分页")
@@ -56,7 +59,9 @@
    @PreAuthorize("@ss.hasPermission('hrm:salary:query')")
    public CommonResult<HrmSalaryPaymentRespVO> getSalaryPayment(@RequestParam("id") Long id) {
        HrmSalaryPaymentDO salaryPayment = salaryPaymentService.getSalaryPayment(id);
        return success(BeanUtils.toBean(salaryPayment, HrmSalaryPaymentRespVO.class));
        HrmSalaryPaymentRespVO respVO = BeanUtils.toBean(salaryPayment, HrmSalaryPaymentRespVO.class);
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_salary_payment", id));
        return success(respVO);
    }
    @PostMapping("/generate")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/controller/admin/salary/vo/HrmSalaryPaymentRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.hrm.controller.admin.salary.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
@@ -9,6 +10,7 @@
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - è–ªèµ„发放 Response VO")
@Data
@@ -49,8 +51,8 @@
    @DictFormat("hrm_payment_status")
    private Integer status;
    @Schema(description = "银行回单附件")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注")
    @ExcelProperty("备注")
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmLeaveApplicationDO.java
@@ -95,11 +95,6 @@
    private LocalDateTime approveTime;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmResignationApplicationDO.java
@@ -108,11 +108,6 @@
    private java.time.LocalDateTime approveTime;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/approval/HrmTransferApplicationDO.java
@@ -105,11 +105,6 @@
    private java.time.LocalDateTime approveTime;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/attendance/HrmAttendanceExceptionDO.java
@@ -83,11 +83,6 @@
    private LocalDateTime approveTime;
    /**
     * é™„件地址
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/dal/dataobject/salary/HrmSalaryPaymentDO.java
@@ -66,11 +66,6 @@
    private Integer status;
    /**
     * é“¶è¡Œå›žå•附件
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmLeaveApplicationServiceImpl.java
@@ -20,6 +20,7 @@
import cn.iocoder.yudao.module.hrm.enums.HrmLeaveTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
@@ -65,6 +66,8 @@
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
@@ -88,6 +91,9 @@
        application.setDeptId(createReqVO.getDeptId() != null ? createReqVO.getDeptId() : user.getDeptId());
        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        leaveApplicationMapper.insert(application);
        // ç»‘定附件
        storageAttachmentApi.bindAttachments("file", "hrm_leave_application",
                application.getId(), createReqVO.getBlobIds());
        return application.getId();
    }
@@ -115,6 +121,12 @@
        // æ›´æ–°
        HrmLeaveApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmLeaveApplicationDO.class);
        leaveApplicationMapper.updateById(updateObj);
        // é‡æ–°ç»‘定附件
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_leave_application", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_leave_application",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
@@ -125,6 +137,8 @@
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(LEAVE_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        // åˆ é™¤é™„件关联
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_leave_application", id);
        // åˆ é™¤
        leaveApplicationMapper.deleteById(id);
    }
@@ -167,6 +181,8 @@
        respVO.setLeaveTypeName(HrmLeaveTypeEnum.getNameByStatus(application.getLeaveType()));
        // å¡«å……审批状态名称
        respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
        // å¡«å……附件列表
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_leave_application", id));
        return respVO;
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmResignationApplicationServiceImpl.java
@@ -19,6 +19,7 @@
import cn.iocoder.yudao.module.hrm.enums.HrmResignationTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
@@ -69,6 +70,8 @@
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
@@ -95,6 +98,8 @@
        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        application.setHandoverStatus(HANDOVER_STATUS_NOT_STARTED);
        resignationApplicationMapper.insert(application);
        storageAttachmentApi.bindAttachments("file", "hrm_resignation_application",
                application.getId(), createReqVO.getBlobIds());
        return application.getId();
    }
@@ -118,6 +123,11 @@
        }
        HrmResignationApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmResignationApplicationDO.class);
        resignationApplicationMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_resignation_application", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_resignation_application",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
@@ -126,6 +136,7 @@
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(RESIGNATION_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_resignation_application", id);
        resignationApplicationMapper.deleteById(id);
    }
@@ -159,6 +170,7 @@
        respVO.setResignationTypeName(HrmResignationTypeEnum.getNameByStatus(application.getResignationType()));
        respVO.setHandoverStatusName(getHandoverStatusName(application.getHandoverStatus()));
        respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_resignation_application", id));
        return respVO;
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/approval/HrmTransferApplicationServiceImpl.java
@@ -19,6 +19,7 @@
import cn.iocoder.yudao.module.hrm.enums.HrmTransferTypeEnum;
import cn.iocoder.yudao.module.system.api.dept.DeptApi;
import cn.iocoder.yudao.module.system.api.dept.PostApi;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
import cn.iocoder.yudao.module.system.api.dept.dto.PostRespDTO;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
@@ -69,6 +70,8 @@
    @Resource
    private BpmCategoryService bpmCategoryService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
@@ -99,6 +102,8 @@
        application.setUserId(userId);
        application.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        transferApplicationMapper.insert(application);
        storageAttachmentApi.bindAttachments("file", "hrm_transfer_application",
                application.getId(), createReqVO.getBlobIds());
        return application.getId();
    }
@@ -129,6 +134,11 @@
        }
        HrmTransferApplicationDO updateObj = BeanUtils.toBean(updateReqVO, HrmTransferApplicationDO.class);
        transferApplicationMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_transfer_application", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_transfer_application",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
@@ -137,6 +147,7 @@
        if (!Objects.equals(application.getStatus(), HrmAuditStatusEnum.DRAFT.getStatus())) {
            throw exception(TRANSFER_APPLICATION_UPDATE_FAIL_NOT_DRAFT);
        }
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_transfer_application", id);
        transferApplicationMapper.deleteById(id);
    }
@@ -170,6 +181,7 @@
        fillPostNames(respVO, application);
        respVO.setTransferTypeName(HrmTransferTypeEnum.getNameByStatus(application.getTransferType()));
        respVO.setStatusName(HrmAuditStatusEnum.getNameByStatus(application.getStatus()));
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("hrm_transfer_application", id));
        return respVO;
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/attendance/HrmAttendanceExceptionServiceImpl.java
@@ -9,6 +9,7 @@
import cn.iocoder.yudao.module.hrm.controller.admin.attendance.vo.HrmAttendanceExceptionSaveReqVO;
import cn.iocoder.yudao.module.hrm.dal.dataobject.attendance.HrmAttendanceExceptionDO;
import cn.iocoder.yudao.module.hrm.dal.mysql.attendance.HrmAttendanceExceptionMapper;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.hrm.enums.HrmAuditStatusEnum;
import jakarta.annotation.Resource;
@@ -39,6 +40,8 @@
    @Resource
    private BpmProcessInstanceApi bpmProcessInstanceApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Resource
    private BpmProcessDefinitionService bpmProcessDefinitionService;
    @Override
@@ -47,6 +50,8 @@
        attendanceException.setNo(generateNo());
        attendanceException.setStatus(HrmAuditStatusEnum.DRAFT.getStatus());
        attendanceExceptionMapper.insert(attendanceException);
        storageAttachmentApi.bindAttachments("file", "hrm_attendance_exception",
                attendanceException.getId(), createReqVO.getBlobIds());
        return attendanceException.getId();
    }
@@ -59,11 +64,17 @@
        validateAttendanceExceptionExists(updateReqVO.getId());
        HrmAttendanceExceptionDO updateObj = BeanUtils.toBean(updateReqVO, HrmAttendanceExceptionDO.class);
        attendanceExceptionMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("hrm_attendance_exception", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "hrm_attendance_exception",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
    public void deleteAttendanceException(Long id) {
        validateAttendanceExceptionExists(id);
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_attendance_exception", id);
        attendanceExceptionMapper.deleteById(id);
    }
yudao-module-hrm/src/main/java/cn/iocoder/yudao/module/hrm/service/salary/HrmSalaryPaymentServiceImpl.java
@@ -11,6 +11,7 @@
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentDetailMapper;
import cn.iocoder.yudao.module.hrm.dal.mysql.salary.HrmSalaryPaymentMapper;
import cn.iocoder.yudao.module.hrm.dal.redis.HrmNoRedisDAO;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -40,6 +41,8 @@
    private HrmSalaryCalculationMapper salaryCalculationMapper;
    @Resource
    private HrmNoRedisDAO noRedisDAO;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    public PageResult<HrmSalaryPaymentDO> getSalaryPaymentPage(HrmSalaryPaymentPageReqVO pageReqVO) {
@@ -280,6 +283,8 @@
        for (HrmSalaryPaymentDetailDO detail : details) {
            salaryPaymentDetailMapper.deleteById(detail.getId());
        }
        // åˆ é™¤é™„件关联
        storageAttachmentApi.deleteAttachmentsByRecord("hrm_salary_payment", id);
        // åˆ é™¤å‘放台账
        salaryPaymentMapper.deleteById(id);
    }
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/MdmItemController.java
@@ -18,6 +18,7 @@
import cn.iocoder.yudao.module.mdm.service.item.MdmItemService;
import cn.iocoder.yudao.module.mdm.service.unit.MdmUnitMeasureService;
import cn.iocoder.yudao.module.mdm.service.warehouse.MdmWarehouseService;
import cn.iocoder.yudao.module.mes.api.item.MesMdItemApi;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
@@ -41,6 +42,9 @@
    @Resource
    private MdmItemService itemService;
    @Resource
    private MesMdItemApi mesMdItemApi;
    @Resource
    private MdmItemCategoryService itemCategoryService;
@@ -105,6 +109,8 @@
            enrichItemRespVO(resp);
            // å¡«å……批次属性配置
            enrichItemBatchConfig(resp);
            // å¡«å……同步状态
            resp.setSyncedMes(mesMdItemApi.getSyncedMdmItemIds(Set.of(id)).contains(id));
        }
        return success(resp);
    }
@@ -122,6 +128,9 @@
                convertSet(pageResult.getList(), MdmItemDO::getUnitMeasureId));
        // è½¬æ¢
        List<MdmItemRespVO> respList = BeanUtils.toBean(pageResult.getList(), MdmItemRespVO.class);
        // æ‰¹é‡æŸ¥è¯¢åŒæ­¥çŠ¶æ€
        Set<Long> allIds = convertSet(respList, MdmItemRespVO::getId);
        Set<Long> syncedIds = mesMdItemApi.getSyncedMdmItemIds(allIds);
        // æ‰¹é‡å…³è”查询辅单位名称
        Set<Long> unitIds = new HashSet<>();
        respList.forEach(resp -> {
@@ -161,6 +170,8 @@
            if (warehouse != null) {
                resp.setWarehouseName(warehouse.getName());
            }
            // åŒæ­¥çŠ¶æ€
            resp.setSyncedMes(syncedIds.contains(resp.getId()));
        });
        return success(new PageResult<>(respList, pageResult.getTotal()));
    }
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/controller/admin/item/vo/MdmItemRespVO.java
@@ -132,6 +132,9 @@
    @Schema(description = "备注", example = "备注信息")
    private String remark;
    @Schema(description = "是否已同步到 MES", example = "true")
    private Boolean syncedMes;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDateTime createTime;
yudao-module-mdm/src/main/java/cn/iocoder/yudao/module/mdm/service/item/MdmItemServiceImpl.java
@@ -77,11 +77,9 @@
        // ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆå¦‚果有)
        saveBatchConfig(item.getId(), createReqVO);
        // åŒæ­¥åˆ° MES(勾选 syncMes æ—¶ï¼‰
        // å§‹ç»ˆåŒæ­¥åˆ° MES
        createReqVO.setId(item.getId());
        if (Boolean.TRUE.equals(createReqVO.getSyncMes())) {
            syncItemToMes(createReqVO);
        }
        syncItemToMes(createReqVO);
        return item.getId();
    }
@@ -100,10 +98,8 @@
        // ä¿å­˜æ‰¹æ¬¡é…ç½®ï¼ˆå¦‚果有)
        saveBatchConfig(updateReqVO.getId(), updateReqVO);
        // åŒæ­¥åˆ° MES(勾选 syncMes æ—¶ï¼‰
        if (Boolean.TRUE.equals(updateReqVO.getSyncMes())) {
            syncItemToMes(updateReqVO);
        }
        // å§‹ç»ˆåŒæ­¥åˆ° MES
        syncItemToMes(updateReqVO);
    }
    private void validateItemSaveData(MdmItemSaveReqVO reqVO) {
@@ -165,11 +161,14 @@
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void updateItemStatus(Long id, Integer status) {
        // æ ¡éªŒå­˜åœ¨
        validateItemExists(id);
        // æ›´æ–°çŠ¶æ€
        itemMapper.updateById(new MdmItemDO().setId(id).setStatus(status));
        // åŒæ­¥çŠ¶æ€åˆ° MES
        mesMdItemApi.updateItemStatus(id, status);
    }
    @Override
@@ -181,6 +180,8 @@
        validateItemNoReference(id);
        // è½¯åˆ é™¤å½“前记录
        itemMapper.deleteById(id);
        // åŒæ­¥åˆ é™¤åˆ° MES
        mesMdItemApi.deleteItem(id);
    }
    /**
yudao-module-mes-api/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApi.java
@@ -2,6 +2,9 @@
import cn.iocoder.yudao.module.mes.api.item.dto.MesMdItemSyncReqDTO;
import java.util.Collection;
import java.util.Set;
/**
 * MES ç‰©æ–™äº§å“ API æŽ¥å£
 *
@@ -29,4 +32,31 @@
     */
    void updateItem(MesMdItemSyncReqDTO syncReqDTO);
    /**
     * åˆ é™¤ MES ç‰©æ–™äº§å“ï¼ˆåŒæ­¥ç”¨ï¼‰
     *
     * æŒ‰ MDM ç‰©æ–™ ID æŸ¥æ‰¾ MES è®°å½•并删除,不存在则忽略。
     *
     * @param mdmItemId MDM ç‰©æ–™ ID
     */
    void deleteItem(Long mdmItemId);
    /**
     * æ›´æ–° MES ç‰©æ–™äº§å“çŠ¶æ€ï¼ˆåŒæ­¥ç”¨ï¼‰
     *
     * æŒ‰ MDM ç‰©æ–™ ID æŸ¥æ‰¾ MES è®°å½•并更新状态,不存在则忽略。
     *
     * @param mdmItemId MDM ç‰©æ–™ ID
     * @param status    çŠ¶æ€
     */
    void updateItemStatus(Long mdmItemId, Integer status);
    /**
     * èŽ·å–å·²åŒæ­¥åˆ° MES çš„ MDM ç‰©æ–™ ID é›†åˆ
     *
     * @param mdmItemIds MDM ç‰©æ–™ ID åˆ—表
     * @return å­˜åœ¨äºŽ MES ä¸­çš„ MDM ç‰©æ–™ ID é›†åˆ
     */
    Set<Long> getSyncedMdmItemIds(Collection<Long> mdmItemIds);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/api/item/MesMdItemApiImpl.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemTypeDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.unitmeasure.MesMdUnitMeasureDO;
import cn.iocoder.yudao.module.mes.dal.mysql.md.item.MesMdItemMapper;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemBatchConfigService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemTypeService;
@@ -15,6 +16,9 @@
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.util.Collection;
import java.util.Set;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
@@ -34,6 +38,8 @@
    @Resource
    private MesMdItemService itemService;
    @Resource
    private MesMdItemMapper itemMapper;
    @Resource
    private MesMdItemTypeService itemTypeService;
    @Resource
@@ -106,6 +112,29 @@
        return saveReqVO;
    }
    @Override
    public void deleteItem(Long mdmItemId) {
        log.info("[deleteItem] åŒæ­¥åˆ é™¤ MES ç‰©æ–™äº§å“ï¼ŒmdmItemId={}", mdmItemId);
        MesMdItemDO existItem = itemService.getItemByMdmItemId(mdmItemId);
        if (existItem != null) {
            itemService.deleteItem(existItem.getId());
        }
    }
    @Override
    public void updateItemStatus(Long mdmItemId, Integer status) {
        log.info("[updateItemStatus] åŒæ­¥æ›´æ–° MES ç‰©æ–™äº§å“çŠ¶æ€ï¼ŒmdmItemId={}, status={}", mdmItemId, status);
        MesMdItemDO existItem = itemService.getItemByMdmItemId(mdmItemId);
        if (existItem != null) {
            itemService.updateItemStatus(existItem.getId(), status);
        }
    }
    @Override
    public Set<Long> getSyncedMdmItemIds(Collection<Long> mdmItemIds) {
        return itemMapper.selectMdmItemIdsByMdmItemIds(mdmItemIds);
    }
    /**
     * ä¿å­˜æ‰¹æ¬¡é…ç½®
     */
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/archive/vo/MesPdArchiveDetailRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.controller.admin.pd.archive.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
@@ -122,8 +123,8 @@
        @Schema(description = "文件名", example = "design.pdf")
        private String fileName;
        @Schema(description = "文件URL", example = "/files/design.pdf")
        private String fileUrl;
        @Schema(description = "附件列表")
        private List<StorageBlobRespDTO> attachmentList;
        @Schema(description = "上传人", example = "1")
        private Long uploadUserId;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/MesPdDocumentController.java
@@ -13,6 +13,7 @@
import cn.iocoder.yudao.module.mes.controller.admin.pd.document.vo.MesPdDocumentSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
import cn.iocoder.yudao.module.mes.service.pd.document.MesPdDocumentService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import io.swagger.v3.oas.annotations.Operation;
@@ -45,6 +46,8 @@
    @Resource
    private AdminUserApi adminUserApi;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @PostMapping("/create")
    @Operation(summary = "创建设计资料")
@@ -79,7 +82,9 @@
        if (document == null) {
            return success(null);
        }
        return success(buildDocumentRespVOList(Collections.singletonList(document)).get(0));
        MesPdDocumentRespVO respVO = buildDocumentRespVOList(Collections.singletonList(document)).get(0);
        respVO.setAttachmentList(storageAttachmentApi.listAttachments("mes_pd_document", id));
        return success(respVO);
    }
    @GetMapping("/page")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentRespVO.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.controller.admin.pd.document.vo;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
@@ -9,6 +10,7 @@
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES è®¾è®¡èµ„æ–™ Response VO")
@Data
@@ -50,9 +52,8 @@
    @ExcelProperty("上传时间")
    private LocalDateTime uploadTime;
    @Schema(description = "文件URL", example = "https://xxx/file.pdf")
    @ExcelProperty("文件URL")
    private String fileUrl;
    @Schema(description = "附件列表")
    private List<StorageBlobRespDTO> attachmentList;
    @Schema(description = "备注", example = "测试备注")
    @ExcelProperty("备注")
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pd/document/vo/MesPdDocumentSaveReqVO.java
@@ -6,6 +6,7 @@
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES è®¾è®¡èµ„料新增/修改 Request VO")
@Data
@@ -37,8 +38,8 @@
    @Schema(description = "上传时间")
    private LocalDateTime uploadTime;
    @Schema(description = "文件URL", example = "https://xxx/file.pdf")
    private String fileUrl;
    @Schema(description = "附件blobId列表", example = "[1, 2, 3]")
    private List<Long> blobIds;
    @Schema(description = "备注", example = "测试备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/MesProTaskController.java
@@ -254,15 +254,19 @@
                convertSet(itemMap.values(), MesMdItemDO::getUnitMeasureId));
        Map<Long, CrmCustomerRespDTO> customerMap = customerApi.getCustomerMap(
                convertSet(list, MesProTaskDO::getClientId));
        // å·¥åºçš„ checkFlag:批量查询后构建 routeId -> processId -> checkFlag çš„双层 Map
        // å·¥åºçš„ checkFlag + backflushFlag:批量查询后构建 routeId -> processId -> flag çš„双层 Map
        Set<Long> routeIds = convertSet(list, MesProTaskDO::getRouteId);
        Map<Long, Map<Long, Boolean>> routeProcessCheckFlagMap = new HashMap<>();
        Map<Long, Map<Long, Boolean>> routeProcessBackflushFlagMap = new HashMap<>();
        if (CollUtil.isNotEmpty(routeIds)) {
            List<MesProRouteProcessDO> allRouteProcesses = routeProcessService.getRouteProcessListByRouteIds(routeIds);
            for (MesProRouteProcessDO rp : allRouteProcesses) {
                routeProcessCheckFlagMap
                        .computeIfAbsent(rp.getRouteId(), k -> new HashMap<>())
                        .put(rp.getProcessId(), Boolean.TRUE.equals(rp.getCheckFlag()));
                routeProcessBackflushFlagMap
                        .computeIfAbsent(rp.getRouteId(), k -> new HashMap<>())
                        .put(rp.getProcessId(), Boolean.TRUE.equals(rp.getBackflushFlag()));
            }
        }
        // æŠ¥å·¥åœ¨é€”量:按任务聚合 feedbackQuantity(草稿/审批中/待检验 = åœ¨é€”;已完成 = å·²å®¡æ‰¹ï¼‰
@@ -303,6 +307,8 @@
                    vo.setClientName(customer.getName()));
            findAndThen(routeProcessCheckFlagMap, task.getRouteId(), processCheckMap ->
                    findAndThen(processCheckMap, task.getProcessId(), vo::setCheckFlag));
            findAndThen(routeProcessBackflushFlagMap, task.getRouteId(), processBackflushMap ->
                    findAndThen(processBackflushMap, task.getProcessId(), vo::setBackflushFlag));
            return vo;
        });
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/task/vo/MesProTaskRespVO.java
@@ -121,6 +121,9 @@
    @Schema(description = "是否质检(派生自工艺路线工序 checkFlag)", example = "true")
    private Boolean checkFlag;
    @Schema(description = "是否倒冲(派生自工艺路线工序 backflushFlag)", example = "true")
    private Boolean backflushFlag;
    @Schema(description = "备注", example = "备注")
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/MesProWorkOrderProcessController.java
@@ -6,14 +6,17 @@
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProProcessBomItemVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.pro.workorder.vo.process.MesProWorkOrderProcessSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.item.MesMdItemDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderBomDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.pro.workorder.MesProWorkOrderProcessDO;
import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService;
import cn.iocoder.yudao.module.mes.service.pro.process.MesProProcessService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderBomService;
import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderProcessService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
@@ -37,6 +40,8 @@
    @Resource
    private MesProWorkOrderProcessService workOrderProcessService;
    @Resource
    private MesProWorkOrderBomService workOrderBomService;
    @Resource
    private MesProProcessService processService;
    @Resource
@@ -135,8 +140,32 @@
        Map<Long, MesMdItemDO> itemMap = CollUtil.isNotEmpty(outputItemIds)
                ? itemService.getItemMap(outputItemIds) : Collections.emptyMap();
        // 2. æ‹¼æŽ¥ VO
        return BeanUtils.toBean(list, MesProWorkOrderProcessRespVO.class, vo -> {
        // 2. æŸ¥è¯¢æŠ•æ–™BOM(按工序分组)
        Set<Long> workOrderIds = convertSet(list, MesProWorkOrderProcessDO::getWorkOrderId);
        workOrderIds.removeAll(Collections.singleton(null));
        Map<Long, List<MesProProcessBomItemVO>> bomItemsByProcessId = new HashMap<>();
        Set<Long> allBomItemIds = new HashSet<>();
        for (Long woId : workOrderIds) {
            List<MesProWorkOrderBomDO> bomList = workOrderBomService.getWorkOrderBomListByWorkOrderId(woId);
            if (CollUtil.isNotEmpty(bomList)) {
                allBomItemIds.addAll(convertSet(bomList, MesProWorkOrderBomDO::getItemId));
                for (MesProWorkOrderBomDO bom : bomList) {
                    bomItemsByProcessId.computeIfAbsent(bom.getProcessId(), k -> new ArrayList<>())
                            .add(BeanUtils.toBean(bom, MesProProcessBomItemVO.class));
                }
            }
        }
        // å¡«å……投料物料编码和名称
        Map<Long, MesMdItemDO> bomItemMap = CollUtil.isNotEmpty(allBomItemIds)
                ? itemService.getItemMap(allBomItemIds) : Collections.emptyMap();
        bomItemsByProcessId.values().forEach(vos -> vos.forEach(vo -> {
            MapUtils.findAndThen(bomItemMap, vo.getItemId(), item -> {
                vo.setItemCode(item.getCode()).setItemName(item.getName());
            });
        }));
        // 3. æ‹¼æŽ¥ VO
        List<MesProWorkOrderProcessRespVO> result = BeanUtils.toBean(list, MesProWorkOrderProcessRespVO.class, vo -> {
            MapUtils.findAndThen(processMap, vo.getProcessId(), process -> {
                vo.setProcessCode(process.getCode()).setProcessName(process.getName());
            });
@@ -147,6 +176,9 @@
                vo.setOutputItemCode(item.getCode()).setOutputItemName(item.getName());
            });
        });
        // è®¾ç½®æ¯ä¸ªå·¥åºå„自的投料BOM
        result.forEach(vo -> vo.setBomItems(bomItemsByProcessId.getOrDefault(vo.getProcessId(), Collections.emptyList())));
        return result;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/pro/workorder/vo/process/MesProWorkOrderProcessRespVO.java
@@ -2,10 +2,12 @@
import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
import cn.idev.excel.annotation.ExcelProperty;
import cn.iocoder.yudao.module.mes.controller.admin.pro.route.vo.process.MesProProcessBomItemVO;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.time.LocalDateTime;
import java.util.List;
@Schema(description = "管理后台 - MES ç”Ÿäº§å·¥å•工序 Response VO")
@Data
@@ -79,6 +81,9 @@
    @ExcelProperty("备注")
    private String remark;
    @Schema(description = "工序投料BOM明细")
    private List<MesProProcessBomItemVO> bomItems;
    @Schema(description = "创建时间", requiredMode = Schema.RequiredMode.REQUIRED)
    @ExcelProperty("创建时间")
    private LocalDateTime createTime;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pd/document/MesPdDocumentDO.java
@@ -65,10 +65,6 @@
     */
    private LocalDateTime uploadTime;
    /**
     * æ–‡ä»¶URL
     */
    private String fileUrl;
    /**
     * å¤‡æ³¨
     */
    private String remark;
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/pro/workorder/MesProWorkOrderBomDO.java
@@ -36,6 +36,12 @@
     */
    private Long workOrderId;
    /**
     * å·¥åºç¼–号
     *
     * å…³è” {@link cn.iocoder.yudao.module.mes.dal.dataobject.pro.process.MesProProcessDO#getId()}
     */
    private Long processId;
    /**
     * BOM ç‰©æ–™ç¼–号
     *
     * å…³è” {@link MesMdItemDO#getId()}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/md/item/MesMdItemMapper.java
@@ -1,5 +1,6 @@
package cn.iocoder.yudao.module.mes.dal.mysql.md.item;
import cn.hutool.core.collection.CollUtil;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
@@ -8,6 +9,12 @@
import org.apache.ibatis.annotations.Mapper;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
/**
 * MES ç‰©æ–™äº§å“ Mapper
@@ -47,4 +54,18 @@
        return selectOne(MesMdItemDO::getMdmItemId, mdmItemId);
    }
    /**
     * æ‰¹é‡æŸ¥è¯¢å·²å­˜åœ¨çš„ MDM ç‰©æ–™ ID é›†åˆ
     *
     * @param mdmItemIds MDM ç‰©æ–™ ID åˆ—表
     * @return å­˜åœ¨äºŽ mes_md_item ä¸­çš„ mdmItemId é›†åˆ
     */
    default Set<Long> selectMdmItemIdsByMdmItemIds(Collection<Long> mdmItemIds) {
        if (CollUtil.isEmpty(mdmItemIds)) {
            return Collections.emptySet();
        }
        List<MesMdItemDO> list = selectList(MesMdItemDO::getMdmItemId, mdmItemIds);
        return convertSet(list, MesMdItemDO::getMdmItemId);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/pro/workorder/MesProWorkOrderBomMapper.java
@@ -27,6 +27,12 @@
        return selectList(MesProWorkOrderBomDO::getWorkOrderId, workOrderId);
    }
    default List<MesProWorkOrderBomDO> selectListByWorkOrderIdAndProcessId(Long workOrderId, Long processId) {
        return selectList(new LambdaQueryWrapperX<MesProWorkOrderBomDO>()
                .eq(MesProWorkOrderBomDO::getWorkOrderId, workOrderId)
                .eqIfPresent(MesProWorkOrderBomDO::getProcessId, processId));
    }
    default void deleteByWorkOrderId(Long workOrderId) {
        delete(MesProWorkOrderBomDO::getWorkOrderId, workOrderId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pd/document/MesPdDocumentServiceImpl.java
@@ -7,6 +7,7 @@
import cn.iocoder.yudao.module.mes.dal.dataobject.pd.document.MesPdDocumentDO;
import cn.iocoder.yudao.module.mes.dal.mysql.pd.document.MesPdDocumentMapper;
import cn.iocoder.yudao.module.mes.service.pd.project.MesPdProjectService;
import cn.iocoder.yudao.module.system.api.storage.StorageAttachmentApi;
import jakarta.annotation.Resource;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
@@ -32,6 +33,8 @@
    @Resource
    @Lazy
    private MesPdProjectService projectService;
    @Resource
    private StorageAttachmentApi storageAttachmentApi;
    @Override
    public Long createDocument(MesPdDocumentSaveReqVO createReqVO) {
@@ -40,6 +43,8 @@
        // æ’å…¥
        MesPdDocumentDO document = BeanUtils.toBean(createReqVO, MesPdDocumentDO.class);
        documentMapper.insert(document);
        storageAttachmentApi.bindAttachments("file", "mes_pd_document",
                document.getId(), createReqVO.getBlobIds());
        return document.getId();
    }
@@ -50,12 +55,19 @@
        // æ›´æ–°
        MesPdDocumentDO updateObj = BeanUtils.toBean(updateReqVO, MesPdDocumentDO.class);
        documentMapper.updateById(updateObj);
        if (updateReqVO.getBlobIds() != null) {
            storageAttachmentApi.deleteAttachmentsByRecord("mes_pd_document", updateReqVO.getId());
            storageAttachmentApi.bindAttachments("file", "mes_pd_document",
                    updateReqVO.getId(), updateReqVO.getBlobIds());
        }
    }
    @Override
    public void deleteDocument(Long id) {
        // æ ¡éªŒå­˜åœ¨
        validateDocumentExists(id);
        // åˆ é™¤é™„ä»¶
        storageAttachmentApi.deleteAttachmentsByRecord("mes_pd_document", id);
        // åˆ é™¤
        documentMapper.deleteById(id);
    }
@@ -77,6 +89,10 @@
    @Override
    public void deleteByProjectId(Long projectId) {
        List<MesPdDocumentDO> documents = documentMapper.selectListByProjectId(projectId);
        for (MesPdDocumentDO document : documents) {
            storageAttachmentApi.deleteAttachmentsByRecord("mes_pd_document", document.getId());
        }
        documentMapper.deleteByProjectId(projectId);
    }
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/pro/workorder/MesProWorkOrderProcessServiceImpl.java
@@ -191,32 +191,27 @@
    }
    /**
     * ä»Žå·¥åºåˆ—表中提取 BOM æŠ•料,合并去重后保存到工单 BOM è¡¨
     * ä»Žå·¥åºåˆ—表中提取 BOM æŠ•料,按工序分别保存到工单 BOM è¡¨
     */
    private void saveWorkOrderBomFromProcesses(Long workOrderId, List<MesProWorkOrderProcessSaveReqVO> list) {
        // æ”¶é›†æ‰€æœ‰å·¥åºçš„ bomItems,按 itemId åŽ»é‡ï¼ˆä¿ç•™ç¬¬ä¸€ä¸ªå‡ºçŽ°çš„ quantity)
        java.util.Map<Long, MesProWorkOrderProcessSaveReqVO.BomItem> merged = new java.util.LinkedHashMap<>();
        workOrderBomService.deleteWorkOrderBomByWorkOrderId(workOrderId);
        List<MesProWorkOrderBomDO> bomList = new ArrayList<>();
        for (MesProWorkOrderProcessSaveReqVO item : list) {
            if (CollUtil.isNotEmpty(item.getBomItems())) {
                for (MesProWorkOrderProcessSaveReqVO.BomItem bomItem : item.getBomItems()) {
                    if (bomItem.getItemId() != null) {
                        merged.putIfAbsent(bomItem.getItemId(), bomItem);
                        bomList.add(MesProWorkOrderBomDO.builder()
                                .workOrderId(workOrderId)
                                .processId(item.getProcessId())
                                .itemId(bomItem.getItemId())
                                .quantity(bomItem.getQuantity())
                                .remark(bomItem.getRemark())
                                .build());
                    }
                }
            }
        }
        // åˆ é™¤æ—§ BOM,批量插入新 BOM
        workOrderBomService.deleteWorkOrderBomByWorkOrderId(workOrderId);
        if (CollUtil.isNotEmpty(merged)) {
            List<MesProWorkOrderBomDO> bomList = new ArrayList<>(merged.size());
            for (MesProWorkOrderProcessSaveReqVO.BomItem bomItem : merged.values()) {
                bomList.add(MesProWorkOrderBomDO.builder()
                        .workOrderId(workOrderId)
                        .itemId(bomItem.getItemId())
                        .quantity(bomItem.getQuantity())
                        .remark(bomItem.getRemark())
                        .build());
            }
        if (CollUtil.isNotEmpty(bomList)) {
            workOrderBomMapper.insertBatch(bomList);
        }
    }
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApi.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package cn.iocoder.yudao.module.system.api.storage;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import java.util.List;
public interface StorageAttachmentApi {
    void bindAttachments(String application, String recordType, Long recordId, List<Long> blobIds);
    List<StorageBlobRespDTO> listAttachments(String recordType, Long recordId);
    void deleteAttachments(List<Long> attachmentIds);
    void deleteAttachmentsByRecord(String recordType, Long recordId);
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/StorageAttachmentApiImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,62 @@
package cn.iocoder.yudao.module.system.api.storage;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.system.api.storage.dto.StorageBlobRespDTO;
import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentListReqVO;
import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentSaveReqVO;
import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO;
import cn.iocoder.yudao.module.system.service.storage.SystemStorageAttachmentService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
@Service
public class StorageAttachmentApiImpl implements StorageAttachmentApi {
    @Resource
    private SystemStorageAttachmentService systemStorageAttachmentService;
    @Override
    public void bindAttachments(String application, String recordType, Long recordId, List<Long> blobIds) {
        if (blobIds == null || blobIds.isEmpty()) {
            return;
        }
        SystemStorageAttachmentSaveReqVO reqVO = new SystemStorageAttachmentSaveReqVO();
        reqVO.setApplication(application != null ? application : "file");
        reqVO.setRecordType(recordType);
        reqVO.setRecordId(recordId);
        List<SystemStorageAttachmentSaveReqVO.BlobItem> blobItems = new ArrayList<>();
        for (Long blobId : blobIds) {
            SystemStorageAttachmentSaveReqVO.BlobItem item = new SystemStorageAttachmentSaveReqVO.BlobItem();
            item.setBlobId(blobId);
            blobItems.add(item);
        }
        reqVO.setBlobItems(blobItems);
        systemStorageAttachmentService.bindAttachments(reqVO);
    }
    @Override
    public List<StorageBlobRespDTO> listAttachments(String recordType, Long recordId) {
        SystemStorageAttachmentListReqVO reqVO = new SystemStorageAttachmentListReqVO();
        reqVO.setRecordType(recordType);
        reqVO.setRecordId(recordId);
        List<SystemStorageBlobRespVO> list = systemStorageAttachmentService.listAttachments(reqVO);
        return BeanUtils.toBean(list, StorageBlobRespDTO.class);
    }
    @Override
    public void deleteAttachments(List<Long> attachmentIds) {
        if (attachmentIds == null || attachmentIds.isEmpty()) {
            return;
        }
        systemStorageAttachmentService.deleteAttachments(attachmentIds);
    }
    @Override
    public void deleteAttachmentsByRecord(String recordType, Long recordId) {
        systemStorageAttachmentService.deleteAttachmentsByRecordTypeAndRecordId(recordType, recordId);
    }
}
yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/api/storage/dto/StorageBlobRespDTO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package cn.iocoder.yudao.module.system.api.storage.dto;
import lombok.Data;
@Data
public class StorageBlobRespDTO {
    private Long id;
    private String url;
    private String previewURL;
    private String name;
    private String downloadURL;
    private String application;
    private Long storageAttachmentId;
}