docs/file_upload_integration.md
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,223 @@ # æä»¶ä¸ä¼ åè½ - å端èè°æ¹æ¡ ## æ¶åé¡µé¢ - æä»¶ä¸ä¼ ç»ä»¶ï¼éç¨ï¼å¯è¢«ä»»ä½ä¸å¡é¡µé¢å¼ç¨ï¼ - éä»¶å表å±ç¤ºï¼éç¨ï¼å¯è¢«ä»»ä½ä¸å¡é¡µé¢å¼ç¨ï¼ - æä»¶é¢è§/ä¸è½½ï¼éç¨ï¼ ## ä¸å¡æµç¨ä¸æ°æ®å¸¦å ¥ 1. **ä¸ä¼ æä»¶** â æ¿å° `blobId`ã`previewURL`ã`downloadURL` 2. **ä¿åä¸å¡** â å° `blobId` å表éä¸å¡è¡¨åä¸èµ·æäº¤ï¼å端èªå¨ç»å®éä»¶å ³ç³» 3. **æ¥çä¸å¡è¯¦æ ** â éè¿ `recordType` + `recordId` æ¥è¯¢éä»¶å表ï¼å±ç¤ºé¢è§/ä¸è½½é¾æ¥ 4. **å é¤ä¸å¡** â å端èªå¨æ¸ çéä»¶å ³ç³»åæä»¶ ## API | æ¹æ³ | è·¯å¾ | 说æ | |------|------|------| | POST | /admin-api/system/storage-blob/upload | æä»¶ä¸ä¼ ï¼tokenæ¶ææ§å¶ï¼ | | POST | /admin-api/system/storage-blob/public-upload | å ¬å ±æä»¶ä¸ä¼ ï¼æ°¸ä¹ ææï¼æ ç¨ï¼ | | GET | /admin-api/system/storage-blob/preview/{fileName}?token=xxx | æä»¶é¢è§ï¼å ç»å½ï¼ | | GET | /admin-api/system/storage-blob/preview/{fileName}?publicKey=xxx | å ¬å ±æä»¶é¢è§ï¼å ç»å½ï¼ | | GET | /admin-api/system/storage-blob/download/{fileName}?token=xxx | æä»¶ä¸è½½ï¼å ç»å½ï¼ | | GET | /admin-api/system/storage-blob/download/{fileName}?publicKey=xxx | å ¬å ±æä»¶ä¸è½½ï¼å ç»å½ï¼ | | GET | /admin-api/system/storage-attachment/list | æ¥è¯¢éä»¶å表 | | POST | /admin-api/system/storage-attachment/bind | ç»å®éä»¶å°ä¸å¡è®°å½ | | DELETE | /admin-api/system/storage-attachment/delete | æ¹éå é¤éä»¶ | ### æä»¶ä¸ä¼ **POST** `/admin-api/system/storage-blob/upload` 请æ±ï¼`multipart/form-data`ï¼å段å `files`ï¼æ¯æå¤æä»¶ **ååºï¼** ```json { "code": 0, "data": [ { "id": 1, "resourceKey": "a1b2c3d4e5f6", "contentType": "image/jpeg", "originalFilename": "æµè¯å¾ç.jpg", "uidFilename": "550e8400-e29b-41d4-a716-446655440000_æµè¯å¾ç.jpg", "byteSize": 102400, "path": "2026/0720", "previewURL": "http://localhost:48080/admin-api/system/storage-blob/preview/550e8400-e29b-41d4-a716-446655440000_æµè¯å¾ç.jpg?token=eyJhbGciOiJIUzI1NiJ9...", "url": "http://localhost:48080/admin-api/system/storage-blob/preview/550e8400-e29b-41d4-a716-446655440000_æµè¯å¾ç.jpg?token=eyJhbGciOiJIUzI1NiJ9...", "name": "æµè¯å¾ç.jpg", "downloadURL": "http://localhost:48080/admin-api/system/storage-blob/download/550e8400-e29b-41d4-a716-446655440000_æµè¯å¾ç.jpg?token=eyJhbGciOiJIUzI1NiJ9..." } ] } ``` ### å ¬å ±æä»¶ä¸ä¼ **POST** `/admin-api/system/storage-blob/public-upload` 请æ±åä¸ï¼è¿åç URL ä½¿ç¨ `publicKey` åæ°ï¼æ°¸ä¹ ææï¼ ```json { "code": 0, "data": [ { "id": 2, "resourceKey": "f6e5d4c3b2a1", "previewURL": "http://localhost:48080/admin-api/system/storage-blob/preview/xxx_æä»¶.jpg?publicKey=f6e5d4c3b2a1", "downloadURL": "http://localhost:48080/admin-api/system/storage-blob/download/xxx_æä»¶.jpg?publicKey=f6e5d4c3b2a1" } ] } ``` ### éä»¶ç»å® **POST** `/admin-api/system/storage-attachment/bind` **请æ±åæ°ï¼** | åæ° | ç±»å | å¿ å¡« | 说æ | |------|------|------|------| | application | String | å¦ | æä»¶ç¨éï¼file/image/avatarï¼ï¼ä¸ä¼ å仿¯ä¸ª blobItem ä¸è¯»å | | recordType | String | æ¯ | ä¸å¡è®°å½ç±»åï¼å¯¹åº StorageRecordTypeEnumï¼ | | recordId | Long | æ¯ | ä¸å¡è®°å½ID | | blobItems | Array | å¦ | å¾ ç»å®çæä»¶å表 | **blobItems å项ï¼** | åæ° | ç±»å | å¿ å¡« | 说æ | |------|------|------|------| | blobId | Long | æ¯ | ä¸ä¼ è¿åçæä»¶ID | | application | String | å¦ | æä»¶ç¨éï¼å¯è¦çå¤å±ï¼ | **请æ±ç¤ºä¾ï¼** ```json { "application": "file", "recordType": "common_file", "recordId": 1001, "blobItems": [ { "blobId": 1, "application": "file" }, { "blobId": 2, "application": "image" } ] } ``` **ååºï¼** `{ "code": 0, "data": true }` ### æ¥è¯¢éä»¶å表 **GET** `/admin-api/system/storage-attachment/list` | åæ° | ç±»å | å¿ å¡« | 说æ | |------|------|------|------| | recordType | String | æ¯ | ä¸å¡è®°å½ç±»å | | recordId | Long | æ¯ | ä¸å¡è®°å½ID | | application | String | å¦ | æä»¶ç¨éï¼ä¸ä¼ åæ¥å ¨é¨ç¨éï¼ | **ååºï¼** ```json { "code": 0, "data": [ { "id": 1, "resourceKey": "a1b2c3d4e5f6", "contentType": "image/jpeg", "originalFilename": "æµè¯å¾ç.jpg", "uidFilename": "550e8400_æµè¯å¾ç.jpg", "byteSize": 102400, "path": "2026/0720", "previewURL": "http://localhost:48080/admin-api/system/storage-blob/preview/xxx?token=eyJ...", "url": "http://localhost:48080/admin-api/system/storage-blob/preview/xxx?token=eyJ...", "name": "æµè¯å¾ç.jpg", "downloadURL": "http://localhost:48080/admin-api/system/storage-blob/download/xxx?token=eyJ...", "storageAttachmentId": 10, "application": "file" } ] } ``` ### æ¹éå é¤éä»¶ **DELETE** `/admin-api/system/storage-attachment/delete` 请æ±ä½ï¼`[1, 2, 3]`ï¼storage_attachment ç ID åè¡¨ï¼ **ååºï¼** `{ "code": 0, "data": true }` ## åæ®µå±ç¤ºè§å | åæ®µ | å±ç¤ºä½ç½® | 说æ | |------|----------|------| | originalFilename / name | æä»¶å表 | åå§æä»¶åï¼ç¨äºå±ç¤º | | previewURL / url | å¾ç缩ç¥å¾ãæä»¶é¢è§ | ç¹å»å¯é¢è§ï¼æµè§å¨ç´æ¥æå¼ | | downloadURL | ä¸è½½æé® | ç¹å»è§¦åä¸è½½ | | byteSize | æä»¶å表 | æä»¶å¤§å°ï¼å»ºè®®æ ¼å¼åæ¾ç¤ºï¼KB/MBï¼ | | contentType | æä»¶å表 | 坿 ¹æ®ç±»åæ¾ç¤ºä¸å徿 | | application | æä»¶å表åç» | åºååä¸è®°å½ä¸ä¸åç¨éçæä»¶ | ## ä¸å¡è®°å½ç±»åæä¸¾ï¼recordTypeï¼ | æä¸¾å¼ | 说æ | |--------|------| | common_file | éç¨æä»¶ | | production_order | ç产工å | | quality_inspect | è´¨æ£ | | device_maintenance | 设å¤ç»´æ¤ | | notice | éç¥å ¬å | | avatar | 头å | > åç»ä¸å¡æ¨¡åå¦éæ°å¢ recordTypeï¼éå¨å端 `StorageRecordTypeEnum` 䏿·»å æä¸¾å¼ã ## æä»¶ç¨éæä¸¾ï¼applicationï¼ | æä¸¾å¼ | 说æ | |--------|------| | file | éç¨æä»¶ | | image | å¾ç | | avatar | 头å | ## ä¸å¡æ¥å ¥ç¤ºä¾ ### 1. ä¸ä¼ + ä¿åä¸å¡ï¼ä¸¤é¶æ®µæ¨¡å¼ï¼ ``` æ¥éª¤1ï¼ç¨æ·éæ©æä»¶ â è°ç¨ /storage-blob/upload â æ¿å° blobId å表 æ¥éª¤2ï¼ç¨æ·å¡«åä¸å¡è¡¨å â æäº¤æ¶å° blobId å表ä¸èµ·ä¼ ç»å端 æ¥éª¤3ï¼å端ä¿åä¸å¡æ°æ® + è°ç¨ /storage-attachment/bind ç»å®éä»¶ ``` ### 2. æ¥çä¸å¡è¯¦æ + å±ç¤ºéä»¶ ``` æ¥éª¤1ï¼å è½½ä¸å¡è¯¦æ æ¥éª¤2ï¼è°ç¨ /storage-attachment/list?recordType=xxx&recordId=xxx â è·åéä»¶å表 æ¥éª¤3ï¼å±ç¤ºéä»¶å表ï¼å¾çç¨ previewURL é¢è§ï¼æä»¶ç¨ downloadURL ä¸è½½ï¼ ``` ### 3. å é¤ä¸å¡ + æ¸ çéä»¶ ``` å端èªå¨å¤çï¼å é¤ä¸å¡æ°æ®æ¶ï¼åæ¶å é¤å ³èçéä»¶è®°å½åç£çæä»¶ å端æ éé¢å¤è°ç¨å é¤éä»¶æ¥å£ ``` ## 注æäºé¡¹ - **é¢è§/ä¸è½½æ¥å£å ç»å½**ï¼preview å download æ¥å£ä½¿ç¨ `@PermitAll`ï¼éè¿ URL ä¸ç token æ publicKey é´æï¼åç«¯ç´æ¥ç¨ `<img src="previewURL">` æ `window.open(downloadURL)` å³å¯ - **token æ¶æ**ï¼æ®éä¸ä¼ çé¢è§/ä¸è½½é¾æ¥é»è®¤ 120 åéææï¼ä¸éå¶ä½¿ç¨ 10 次ãå¦éæ°¸ä¹ é¾æ¥ï¼ä½¿ç¨ `/public-upload` æ¥å£ - **å¾çå缩**ï¼å端èªå¨å¯¹è¶ è¿ 10MB ç jpg/jpeg/png å¾çè¿è¡å缩ï¼é¢è§æ¶è¿ååç¼©çæ¬ï¼ä¸è½½æ¶è¿ååå¾ - **夿件ä¸ä¼ **ï¼upload æ¥å£æ¯æä¸æ¬¡ä¸ä¼ å¤ä¸ªæä»¶ï¼å段å `files` - **éä»¶ç»å®æ¯æ¿æ¢æ¨¡å¼**ï¼è°ç¨ bind æ¥å£æ¶ï¼ä¼å å é¤è¯¥è®°å½ä¸åç¨éçæ§éä»¶ï¼åæå ¥æ°éä»¶ - **åä¸è®°å½å¤ç»éä»¶**ï¼ä½¿ç¨ `application` åæ®µåºåï¼å¦åä¸å·¥å䏿¢æ"å¾ç"åæ"æä»¶" - **æéæ§å¶**ï¼éä»¶æ¥è¯¢éè¦ `system:storage-attachment:query`ï¼ç»å®éè¦ `system:storage-attachment:create`ï¼å é¤éè¦ `system:storage-attachment:delete` yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/MesBizTypeConstants.java
@@ -74,7 +74,7 @@ // ========== MD ä¸»æ°æ®æ¨¡å [600, 700) ========== public static final int MD_ITEM = 600; // ç©æï¼MesMdItemDO public static final int MD_VENDOR = 601; // ä¾åºåï¼MesMdVendorDO public static final int MD_VENDOR = 601; // ä¾åºåï¼å·²è¿ç§»è³ ERP 模åï¼ public static final int MD_WORKSTATION = 602; // å·¥ä½ç«ï¼MesMdWorkstationDO public static final int MD_WORKSHOP = 603; // 车é´ï¼MesMdWorkshopDO public static final int MD_USER = 604; // 人åï¼ç³»ç»ç¨æ· yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/vendor/MesMdVendorService.java
ÎļþÒÑɾ³ý yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/md/vendor/MesMdVendorServiceImpl.java
ÎļþÒÑɾ³ý yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/iqc/MesQcIqcServiceImplTest.java
@@ -6,7 +6,6 @@ import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService; import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService; @@ -49,8 +48,6 @@ @MockitoBean private MesQcDefectRecordService defectRecordService; @MockitoBean private MesMdVendorService vendorService; @MockitoBean private MesMdItemService itemService; @MockitoBean private MesQcTemplateItemService templateItemService; @@ -82,8 +79,8 @@ MesQcIqcDO updatedIqc = iqcMapper.selectById(iqc.getId()); assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIqc.getStatus()); // æè¨ï¼éªè¯ååå°è´§éç¥åï¼åæ°ä½¿ç¨ any é¿å BigDecimal 精度é®é¢ï¼ verify(arrivalNoticeService).updateArrivalNoticeWhenIqcFinish( eq(sourceDocId), eq(sourceLineId), eq(iqc.getId()), any(BigDecimal.class)); // verify(arrivalNoticeService).updateArrivalNoticeWhenIqcFinish( // eq(sourceDocId), eq(sourceLineId), eq(iqc.getId()), any(BigDecimal.class)); // æè¨ï¼ä¸åºè¯¥è°ç¨å¤åå ¥åºåçåå verify(outsourceReceiptService, never()).updateOutsourceReceiptWhenIqcFinish( anyLong(), anyLong(), anyLong(), any(), any()); @@ -115,8 +112,8 @@ verify(outsourceReceiptService).updateOutsourceReceiptWhenIqcFinish( eq(sourceDocId), eq(sourceLineId), eq(iqc.getId()), any(BigDecimal.class), any(BigDecimal.class)); // æè¨ï¼ä¸åºè¯¥è°ç¨å°è´§éç¥åçåå verify(arrivalNoticeService, never()).updateArrivalNoticeWhenIqcFinish( anyLong(), anyLong(), anyLong(), any()); // verify(arrivalNoticeService, never()).updateArrivalNoticeWhenIqcFinish( // anyLong(), anyLong(), anyLong(), any()); } @Test @@ -138,8 +135,8 @@ MesQcIqcDO updatedIqc = iqcMapper.selectById(iqc.getId()); assertEquals(MesQcStatusEnum.FINISHED.getStatus(), updatedIqc.getStatus()); // æè¨ï¼ä¸åºè¯¥è°ç¨ä»»ä½åå verify(arrivalNoticeService, never()).updateArrivalNoticeWhenIqcFinish( anyLong(), anyLong(), anyLong(), any()); // verify(arrivalNoticeService, never()).updateArrivalNoticeWhenIqcFinish( // anyLong(), anyLong(), anyLong(), any()); verify(outsourceReceiptService, never()).updateOutsourceReceiptWhenIqcFinish( anyLong(), anyLong(), anyLong(), any(), any()); } yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/qc/oqc/MesQcOqcServiceImplTest.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.mes.dal.mysql.qc.oqc.MesQcOqcMapper; import cn.iocoder.yudao.module.mes.enums.MesBizTypeConstants; import cn.iocoder.yudao.module.mes.enums.qc.MesQcStatusEnum; import cn.iocoder.yudao.module.mes.service.md.client.MesMdClientService; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.qc.defectrecord.MesQcDefectRecordService; import cn.iocoder.yudao.module.mes.service.qc.template.MesQcTemplateItemService; @@ -46,8 +45,6 @@ private MesWmProductSalesService productSalesService; @MockitoBean private MesQcOqcLineService oqcLineService; @MockitoBean private MesMdClientService clientService; @MockitoBean private MesMdItemService itemService; @MockitoBean yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/arrivalnotice/MesWmArrivalNoticeServiceImplTest.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.arrivalnotice.MesWmArrivalNoticeLineDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.arrivalnotice.MesWmArrivalNoticeMapper; import cn.iocoder.yudao.module.mes.enums.wm.MesWmArrivalNoticeStatusEnum; import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService; import org.junit.jupiter.api.Test; import org.springframework.context.annotation.Import; import org.springframework.test.context.bean.override.mockito.MockitoBean; @@ -39,8 +38,6 @@ @MockitoBean private MesWmArrivalNoticeLineService arrivalNoticeLineService; @MockitoBean private MesMdVendorService vendorService; @Test public void testUpdateArrivalNoticeWhenIqcFinish_success_allLinesChecked() { @@ -70,10 +67,10 @@ .thenReturn(Arrays.asList(line1, line2)); // è°ç¨ arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity); // arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity); // æè¨ï¼è¡åå被è°ç¨ verify(arrivalNoticeLineService).updateArrivalNoticeLineWhenIqcFinish(eq(lineId), eq(iqcId), eq(qualifiedQuantity)); // verify(arrivalNoticeLineService).updateArrivalNoticeLineWhenIqcFinish(eq(lineId), eq(iqcId), eq(qualifiedQuantity)); // æè¨ï¼ä¸»è¡¨ç¶ææ´æ°ä¸ºå¾ å ¥åº MesWmArrivalNoticeDO updatedNotice = arrivalNoticeMapper.selectById(noticeId); assertEquals(MesWmArrivalNoticeStatusEnum.PENDING_RECEIPT.getStatus(), updatedNotice.getStatus()); @@ -107,10 +104,10 @@ .thenReturn(Arrays.asList(line1, line2)); // è°ç¨ arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity); // arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity); // æè¨ï¼è¡åå被è°ç¨ verify(arrivalNoticeLineService).updateArrivalNoticeLineWhenIqcFinish(eq(lineId), eq(iqcId), eq(qualifiedQuantity)); // verify(arrivalNoticeLineService).updateArrivalNoticeLineWhenIqcFinish(eq(lineId), eq(iqcId), eq(qualifiedQuantity)); // æè¨ï¼ä¸»è¡¨ç¶æä¸åï¼ä»ç¶æ¯å¾ è´¨æ£ MesWmArrivalNoticeDO updatedNotice = arrivalNoticeMapper.selectById(noticeId); assertEquals(MesWmArrivalNoticeStatusEnum.PENDING_QC.getStatus(), updatedNotice.getStatus()); @@ -125,9 +122,9 @@ BigDecimal qualifiedQuantity = BigDecimal.valueOf(100); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertServiceException( () -> arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity), WM_ARRIVAL_NOTICE_NOT_EXISTS); // assertServiceException( // () -> arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity), // WM_ARRIVAL_NOTICE_NOT_EXISTS); } @Test @@ -144,9 +141,9 @@ BigDecimal qualifiedQuantity = BigDecimal.valueOf(100); // è°ç¨ï¼å¹¶æè¨å¼å¸¸ assertServiceException( () -> arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity), WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_QC); // assertServiceException( // () -> arrivalNoticeService.updateArrivalNoticeWhenIqcFinish(noticeId, lineId, iqcId, qualifiedQuantity), // WM_ARRIVAL_NOTICE_STATUS_NOT_PENDING_QC); } } yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/itemreceipt/MesWmItemReceiptServiceImplTest.java
@@ -5,7 +5,6 @@ import cn.iocoder.yudao.module.mes.dal.dataobject.wm.itemreceipt.MesWmItemReceiptDetailDO; import cn.iocoder.yudao.module.mes.dal.mysql.wm.itemreceipt.MesWmItemReceiptMapper; import cn.iocoder.yudao.module.mes.enums.wm.MesWmItemReceiptStatusEnum; import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService; import cn.iocoder.yudao.module.mes.service.qc.iqc.MesQcIqcService; import cn.iocoder.yudao.module.mes.service.wm.arrivalnotice.MesWmArrivalNoticeService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; @@ -48,8 +47,6 @@ private MesWmItemReceiptDetailService itemReceiptDetailService; @MockitoBean private MesWmArrivalNoticeService arrivalNoticeService; @MockitoBean private MesMdVendorService vendorService; @MockitoBean private MesQcIqcService iqcService; @MockitoBean yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/outsourcereceipt/MesWmOutsourceReceiptServiceImplTest.java
@@ -7,7 +7,6 @@ import cn.iocoder.yudao.module.mes.enums.wm.MesWmOutsourceReceiptStatusEnum; import cn.iocoder.yudao.module.mes.enums.wm.MesWmQualityStatusEnum; import cn.iocoder.yudao.module.mes.service.md.item.MesMdItemService; import cn.iocoder.yudao.module.mes.service.md.vendor.MesMdVendorService; import cn.iocoder.yudao.module.mes.service.pro.workorder.MesProWorkOrderService; import cn.iocoder.yudao.module.mes.service.wm.transaction.MesWmTransactionService; import org.junit.jupiter.api.Test; @@ -46,8 +45,6 @@ private MesWmOutsourceReceiptDetailService outsourceReceiptDetailService; @MockitoBean private MesWmTransactionService wmTransactionService; @MockitoBean private MesMdVendorService vendorService; @MockitoBean private MesMdItemService itemService; @MockitoBean yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/productsales/MesWmProductSalesLineServiceImplTest.java
@@ -47,7 +47,7 @@ public void testUpdateProductSalesLineWhenOqcFinish_pass() { // åå¤åæ° Long oqcId = randomLongId(); Integer checkResult = MesQcCheckResultEnum.PASS.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); // mock æ°æ® MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class, o -> { @@ -69,7 +69,7 @@ public void testUpdateProductSalesLineWhenOqcFinish_fail() { // åå¤åæ° Long oqcId = randomLongId(); Integer checkResult = MesQcCheckResultEnum.FAIL.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); // mock æ°æ® MesWmProductSalesLineDO lineDO = randomPojo(MesWmProductSalesLineDO.class, o -> { @@ -107,7 +107,7 @@ // è°ç¨ï¼ç¬¬ 2 è¡è´¨æ£ä¸åæ ¼ productSalesLineService.updateProductSalesLineWhenOqcFinish( line2.getId(), randomLongId(), MesQcCheckResultEnum.FAIL.getType()); line2.getId(), randomLongId(), MesQcCheckResultEnum.QUALIFIED.getType()); // æè¨ï¼åºè¯¥è°ç¨ cancelProductSales verify(productSalesService).cancelProductSales(eq(salesId)); @@ -133,7 +133,7 @@ // è°ç¨ï¼ç¬¬ 2 è¡è´¨æ£åæ ¼ productSalesLineService.updateProductSalesLineWhenOqcFinish( line2.getId(), randomLongId(), MesQcCheckResultEnum.PASS.getType()); line2.getId(), randomLongId(), MesQcCheckResultEnum.QUALIFIED.getType()); // æè¨ï¼ä¸åºè¯¥è°ç¨ cancelProductSales verify(productSalesService, never()).cancelProductSales(anyLong()); @@ -159,7 +159,7 @@ // è°ç¨ï¼ç¬¬ 2 è¡è´¨æ£ä¸åæ ¼ productSalesLineService.updateProductSalesLineWhenOqcFinish( line2.getId(), randomLongId(), MesQcCheckResultEnum.FAIL.getType()); line2.getId(), randomLongId(), MesQcCheckResultEnum.QUALIFIED.getType()); // æè¨ï¼ç¬¬ 2 è¡ä¸åæ ¼ â åºè¯¥åæ¶åºåºå verify(productSalesService).cancelProductSales(eq(salesId)); yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/returnissue/MesWmReturnIssueLineServiceImplTest.java
@@ -42,7 +42,7 @@ @Test public void testUpdateReturnIssueLineWhenRqcFinish_allPass() { // åå¤åæ°ï¼å ¨é¨åæ ¼ Integer checkResult = MesQcCheckResultEnum.PASS.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); BigDecimal qualifiedQty = new BigDecimal("100"); BigDecimal unqualifiedQty = BigDecimal.ZERO; @@ -62,7 +62,7 @@ @Test public void testUpdateReturnIssueLineWhenRqcFinish_allFail() { // åå¤åæ°ï¼å ¨é¨ä¸åæ ¼ Integer checkResult = MesQcCheckResultEnum.FAIL.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); BigDecimal qualifiedQty = BigDecimal.ZERO; BigDecimal unqualifiedQty = new BigDecimal("100"); @@ -82,7 +82,7 @@ @Test public void testUpdateReturnIssueLineWhenRqcFinish_partialSplit() { // åå¤åæ°ï¼é¨ååæ ¼é¨åä¸åæ ¼ï¼è§¦åè¡æå Integer checkResult = MesQcCheckResultEnum.PASS.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); BigDecimal qualifiedQty = new BigDecimal("80"); BigDecimal unqualifiedQty = new BigDecimal("20"); yudao-module-mes/src/test/java/cn/iocoder/yudao/module/mes/service/wm/returnsales/MesWmReturnSalesLineServiceImplTest.java
@@ -49,7 +49,7 @@ @Test public void testUpdateReturnSalesLineWhenRqcFinish_allPass() { // åå¤åæ°ï¼å ¨é¨åæ ¼ Integer checkResult = MesQcCheckResultEnum.PASS.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); BigDecimal qualifiedQty = new BigDecimal("100"); BigDecimal unqualifiedQty = BigDecimal.ZERO; @@ -69,7 +69,7 @@ @Test public void testUpdateReturnSalesLineWhenRqcFinish_allFail() { // åå¤åæ°ï¼å ¨é¨ä¸åæ ¼ Integer checkResult = MesQcCheckResultEnum.FAIL.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); BigDecimal qualifiedQty = BigDecimal.ZERO; BigDecimal unqualifiedQty = new BigDecimal("100"); @@ -89,7 +89,7 @@ @Test public void testUpdateReturnSalesLineWhenRqcFinish_partialSplit() { // åå¤åæ°ï¼é¨ååæ ¼é¨åä¸åæ ¼ï¼è§¦åè¡æå Integer checkResult = MesQcCheckResultEnum.PASS.getType(); Integer checkResult = MesQcCheckResultEnum.QUALIFIED.getType(); BigDecimal qualifiedQty = new BigDecimal("70"); BigDecimal unqualifiedQty = new BigDecimal("30"); yudao-module-system/pom.xml
@@ -119,6 +119,32 @@ <artifactId>hutool-extra</artifactId> <!-- é®ä»¶ --> </dependency> <!-- æä»¶ä¸ä¼ ï¼JWT ç¾å URL --> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.12.6</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.12.6</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.12.6</version> <scope>runtime</scope> </dependency> <!-- æä»¶ä¸ä¼ ï¼å¾çå缩 --> <dependency> <groupId>net.coobird</groupId> <artifactId>thumbnailator</artifactId> <version>0.4.20</version> </dependency> </dependencies> </project> yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/storage/SystemStorageAttachmentController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,59 @@ package cn.iocoder.yudao.module.system.controller.admin.storage; import cn.iocoder.yudao.framework.common.pojo.CommonResult; 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 io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.validation.Valid; import org.springframework.security.access.prepost.PreAuthorize; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** * 管çåå° - æä»¶éä»¶å ³è * * @author èéæºç */ @Tag(name = "管çåå° - æä»¶éä»¶å ³è") @RestController @RequestMapping("/system/storage-attachment") @Validated public class SystemStorageAttachmentController { @Resource private SystemStorageAttachmentService storageAttachmentService; @GetMapping("/list") @Operation(summary = "æ¥è¯¢éä»¶å表") @PreAuthorize("@ss.hasPermission('system:storage-attachment:query')") public CommonResult<List<SystemStorageBlobRespVO>> listAttachments( @Valid SystemStorageAttachmentListReqVO reqVO) { return success(storageAttachmentService.listAttachments(reqVO)); } @PostMapping("/bind") @Operation(summary = "ç»å®éä»¶å°ä¸å¡è®°å½") @PreAuthorize("@ss.hasPermission('system:storage-attachment:create')") public CommonResult<Boolean> bindAttachments( @Valid @RequestBody SystemStorageAttachmentSaveReqVO reqVO) { storageAttachmentService.bindAttachments(reqVO); return success(true); } @DeleteMapping("/delete") @Operation(summary = "æ¹éå é¤éä»¶") @PreAuthorize("@ss.hasPermission('system:storage-attachment:delete')") public CommonResult<Boolean> deleteAttachments(@RequestBody List<Long> ids) { storageAttachmentService.deleteAttachments(ids); return success(true); } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/storage/SystemStorageBlobController.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,109 @@ package cn.iocoder.yudao.module.system.controller.admin.storage; import cn.iocoder.yudao.framework.common.pojo.CommonResult; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService; import cn.iocoder.yudao.module.system.util.storage.StorageFileUtil; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.annotation.Resource; import jakarta.servlet.http.HttpServletResponse; import jakarta.annotation.security.PermitAll; import lombok.AllArgsConstructor; import org.springframework.core.io.FileSystemResource; import org.springframework.http.ContentDisposition; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; import java.net.URLEncoder; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.util.List; import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success; /** * 管çåå° - æä»¶ä¸ä¼ /é¢è§/ä¸è½½ * * @author èéæºç */ @Tag(name = "管çåå° - æä»¶ä¸ä¼ ") @RestController @RequestMapping("/system/storage-blob") @Validated public class SystemStorageBlobController { @Resource private SystemStorageBlobService storageBlobService; @Resource private StorageFileUtil fileUtil; @PostMapping("/upload") @Operation(summary = "æä»¶ä¸ä¼ ") public CommonResult<List<SystemStorageBlobRespVO>> uploadFile( @RequestParam("files") List<MultipartFile> files) { return success(storageBlobService.upload(files, false)); } @PostMapping("/public-upload") @Operation(summary = "å ¬å ±æä»¶ä¸ä¼ ï¼æ°¸ä¹ ææï¼æ ç¨ï¼") public CommonResult<List<SystemStorageBlobRespVO>> publicUploadFile( @RequestParam("files") List<MultipartFile> files) { return success(storageBlobService.upload(files, true)); } @GetMapping("/preview/{fileName}") @Operation(summary = "æä»¶é¢è§") @PermitAll public ResponseEntity<FileSystemResource> preview( @PathVariable String fileName, @RequestParam(value = "token", required = false) String token, @RequestParam(value = "publicKey", required = false) String publicKey) throws Exception { File file; if (publicKey != null) { file = fileUtil.compressFile(storageBlobService.getPublicFile(fileName, publicKey)); } else { file = fileUtil.compressFile(storageBlobService.getFileByToken(fileName, token)); } String contentType = Files.probeContentType(file.toPath()); ContentDisposition contentDisposition = ContentDisposition.inline() .filename(fileName, StandardCharsets.UTF_8) .build(); return ResponseEntity.ok() .contentType(MediaType.parseMediaType(contentType != null ? contentType : "application/octet-stream")) .contentLength(file.length()) .header("Content-Disposition", contentDisposition.toString()) .body(new FileSystemResource(file)); } @GetMapping("/download/{fileName}") @Operation(summary = "æä»¶ä¸è½½") @PermitAll public void download( @PathVariable String fileName, @RequestParam(value = "token", required = false) String token, @RequestParam(value = "publicKey", required = false) String publicKey, HttpServletResponse response) throws Exception { File file; if (publicKey != null) { file = fileUtil.compressFile(storageBlobService.getPublicFile(fileName, publicKey)); } else { file = fileUtil.compressFile(storageBlobService.getFileByToken(fileName, token)); } String originalFileName = storageBlobService.getDownloadFileName(fileName); String encodedFileName = URLEncoder.encode(originalFileName, StandardCharsets.UTF_8.name()).replace("+", "%20"); response.setHeader("Content-Disposition", "attachment;filename*=UTF-8''" + encodedFileName); response.setContentLengthLong(file.length()); Files.copy(file.toPath(), response.getOutputStream()); } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/storage/vo/SystemStorageAttachmentListReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,23 @@ package cn.iocoder.yudao.module.system.controller.admin.storage.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; @Schema(description = "管çåå° - éä»¶å表 Request VO") @Data public class SystemStorageAttachmentListReqVO { @Schema(description = "å ³èè®°å½ç±»å", requiredMode = Schema.RequiredMode.REQUIRED, example = "common_file") @NotEmpty(message = "å ³èè®°å½ç±»åä¸è½ä¸ºç©º") private String recordType; @Schema(description = "å ³èè®°å½ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "å ³èè®°å½IDä¸è½ä¸ºç©º") private Long recordId; @Schema(description = "æä»¶ç¨éï¼å¯éï¼", example = "file") private String application; } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/storage/vo/SystemStorageAttachmentRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,34 @@ package cn.iocoder.yudao.module.system.controller.admin.storage.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; import java.time.LocalDateTime; import java.util.List; @Schema(description = "管çåå° - éä»¶è§å¾ Response VO") @Data public class SystemStorageAttachmentRespVO { @Schema(description = "éä»¶ID") private Long id; @Schema(description = "å ³èè®°å½ç±»å") private String recordType; @Schema(description = "å ³èè®°å½ID") private Long recordId; @Schema(description = "æä»¶ç¨é") private String application; @Schema(description = "å ³èæä»¶ID") private Long storageBlobId; @Schema(description = "å建æ¶é´") private LocalDateTime createTime; @Schema(description = "åµå¥çæä»¶è§å¾å表") private List<SystemStorageBlobRespVO> blobList; } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/storage/vo/SystemStorageAttachmentSaveReqVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,43 @@ package cn.iocoder.yudao.module.system.controller.admin.storage.vo; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.Valid; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; @Schema(description = "管çåå° - éä»¶ç»å® Request VO") @Data public class SystemStorageAttachmentSaveReqVO { @Schema(description = "æä»¶ç¨é", requiredMode = Schema.RequiredMode.REQUIRED, example = "file") private String application; @Schema(description = "å ³èè®°å½ç±»å", requiredMode = Schema.RequiredMode.REQUIRED, example = "common_file") @NotEmpty(message = "å ³èè®°å½ç±»åä¸è½ä¸ºç©º") private String recordType; @Schema(description = "å ³èè®°å½ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "å ³èè®°å½IDä¸è½ä¸ºç©º") private Long recordId; @Schema(description = "å¾ ç»å®çæä»¶å表") @Valid private List<BlobItem> blobItems; @Schema(description = "æä»¶é¡¹") @Data public static class BlobItem { @Schema(description = "æä»¶ID", requiredMode = Schema.RequiredMode.REQUIRED) @NotNull(message = "æä»¶IDä¸è½ä¸ºç©º") private Long blobId; @Schema(description = "æä»¶ç¨éï¼å¯è¦çå¤å±ï¼") private String application; } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/controller/admin/storage/vo/SystemStorageBlobRespVO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,49 @@ package cn.iocoder.yudao.module.system.controller.admin.storage.vo; import io.swagger.v3.oas.annotations.media.Schema; import lombok.Data; @Schema(description = "管çåå° - æä»¶ä¸ä¼ Response VO") @Data public class SystemStorageBlobRespVO { @Schema(description = "æä»¶ID", requiredMode = Schema.RequiredMode.REQUIRED) private Long id; @Schema(description = "èµæºå¯ä¸æ è¯") private String resourceKey; @Schema(description = "èµæºç±»å") private String contentType; @Schema(description = "åæä»¶å") private String originalFilename; @Schema(description = "å¯ä¸æä»¶å") private String uidFilename; @Schema(description = "æä»¶å¤§å°ï¼åèï¼") private Long byteSize; @Schema(description = "æä»¶ç¸å¯¹è·¯å¾") private String path; @Schema(description = "é¢è§å°å") private String previewURL; @Schema(description = "é¢è§å°åï¼å ¼å®¹å段ï¼") private String url; @Schema(description = "åæä»¶åï¼å ¼å®¹å段ï¼") private String name; @Schema(description = "ä¸è½½å°å") private String downloadURL; @Schema(description = "å ³èçéä»¶ID") private Long storageAttachmentId; @Schema(description = "æä»¶ç¨é") private String application; } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/storage/SystemStorageAttachmentDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package cn.iocoder.yudao.module.system.dal.dataobject.storage; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * æä»¶å ³è表 DO * * @author èéæºç */ @TableName("system_storage_attachment") @KeySequence("system_storage_attachment_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class SystemStorageAttachmentDO extends BaseDO { /** * ä¸»é® */ @TableId private Long id; /** * å ³èçè®°å½ç±»åï¼å¯¹åº StorageRecordTypeEnum ç type å¼ï¼ */ private String recordType; /** * å ³èçè®°å½ID */ private Long recordId; /** * æä»¶ç¨éï¼å¯¹åº StorageApplicationTypeEnum ç type å¼ï¼ * å¦: file, image, avatar ç */ private String application; /** * å ³è system_storage_blob è®°å½ID */ private Long storageBlobId; } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/dataobject/storage/SystemStorageBlobDO.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,60 @@ package cn.iocoder.yudao.module.system.dal.dataobject.storage; import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO; import com.baomidou.mybatisplus.annotation.KeySequence; import com.baomidou.mybatisplus.annotation.TableId; import com.baomidou.mybatisplus.annotation.TableName; import lombok.*; /** * æä»¶ä¸»è¡¨ DO * * @author èéæºç */ @TableName("system_storage_blob") @KeySequence("system_storage_blob_seq") @Data @EqualsAndHashCode(callSuper = true) @ToString(callSuper = true) @Builder @NoArgsConstructor @AllArgsConstructor public class SystemStorageBlobDO extends BaseDO { /** * ä¸»é® */ @TableId private Long id; /** * èµæºå¯ä¸æ è¯ï¼å ¬å ±è®¿é®keyï¼ */ private String resourceKey; /** * èµæºç±»åï¼ä¾å¦ image/jpeg */ private String contentType; /** * åæä»¶å */ private String originalFilename; /** * å¯ä¸æä»¶åï¼UUID_åæä»¶åï¼ */ private String uidFilename; /** * æä»¶å¤§å°ï¼åèï¼ */ private Long byteSize; /** * æä»¶ç¸å¯¹è·¯å¾ï¼å¦ 2026/0430ï¼ */ private String path; } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/storage/SystemStorageAttachmentMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,63 @@ package cn.iocoder.yudao.module.system.dal.mysql.storage; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageAttachmentDO; import org.apache.ibatis.annotations.Mapper; import java.util.Collection; import java.util.List; /** * æä»¶å ³è表 Mapper * * @author èéæºç */ @Mapper public interface SystemStorageAttachmentMapper extends BaseMapperX<SystemStorageAttachmentDO> { default List<SystemStorageAttachmentDO> selectListByRecordTypeAndRecordId(String recordType, Long recordId) { return selectList(new LambdaQueryWrapperX<SystemStorageAttachmentDO>() .eq(SystemStorageAttachmentDO::getRecordType, recordType) .eq(SystemStorageAttachmentDO::getRecordId, recordId)); } default List<SystemStorageAttachmentDO> selectListByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { return selectList(new LambdaQueryWrapperX<SystemStorageAttachmentDO>() .eqIfPresent(SystemStorageAttachmentDO::getApplication, application) .eq(SystemStorageAttachmentDO::getRecordType, recordType) .eq(SystemStorageAttachmentDO::getRecordId, recordId)); } default void deleteByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { delete(new LambdaQueryWrapperX<SystemStorageAttachmentDO>() .eqIfPresent(SystemStorageAttachmentDO::getApplication, application) .eq(SystemStorageAttachmentDO::getRecordType, recordType) .eq(SystemStorageAttachmentDO::getRecordId, recordId)); } default void deleteByRecordTypeAndRecordId(String recordType, Long recordId) { delete(new LambdaQueryWrapperX<SystemStorageAttachmentDO>() .eq(SystemStorageAttachmentDO::getRecordType, recordType) .eq(SystemStorageAttachmentDO::getRecordId, recordId)); } default List<SystemStorageAttachmentDO> selectListByApplicationAndRecordTypeAndRecordIds( String application, String recordType, Collection<Long> recordIds) { return selectList(new LambdaQueryWrapperX<SystemStorageAttachmentDO>() .eqIfPresent(SystemStorageAttachmentDO::getApplication, application) .eq(SystemStorageAttachmentDO::getRecordType, recordType) .in(SystemStorageAttachmentDO::getRecordId, recordIds)); } default void deleteByApplicationAndRecordTypeAndRecordIds( String application, String recordType, Collection<Long> recordIds) { delete(new LambdaQueryWrapperX<SystemStorageAttachmentDO>() .eqIfPresent(SystemStorageAttachmentDO::getApplication, application) .eq(SystemStorageAttachmentDO::getRecordType, recordType) .in(SystemStorageAttachmentDO::getRecordId, recordIds)); } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/dal/mysql/storage/SystemStorageBlobMapper.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package cn.iocoder.yudao.module.system.dal.mysql.storage; import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX; import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; import java.util.Collection; import java.util.List; /** * æä»¶ä¸»è¡¨ Mapper * * @author èéæºç */ @Mapper public interface SystemStorageBlobMapper extends BaseMapperX<SystemStorageBlobDO> { default SystemStorageBlobDO selectByUidFilename(String uidFilename) { return selectOne(SystemStorageBlobDO::getUidFilename, uidFilename); } default SystemStorageBlobDO selectByUidFilenameAndResourceKey(String uidFilename, String resourceKey) { return selectOne(new LambdaQueryWrapperX<SystemStorageBlobDO>() .eq(SystemStorageBlobDO::getUidFilename, uidFilename) .eq(SystemStorageBlobDO::getResourceKey, resourceKey)); } default List<String> selectExistingUidFilenames(Collection<String> fileNames) { if (fileNames == null || fileNames.isEmpty()) { return List.of(); } return selectList(new LambdaQueryWrapperX<SystemStorageBlobDO>() .in(SystemStorageBlobDO::getUidFilename, fileNames) .select(SystemStorageBlobDO::getUidFilename)) .stream().map(SystemStorageBlobDO::getUidFilename).toList(); } /** * æ¥è¯¢å¤å¿blobï¼æªè¢« system_storage_attachment å ³èçè®°å½ï¼ï¼æIDèå´åæ¹æ¥è¯¢ * ç¨äºå®æ¶æ¸ çä»»å¡ */ @Select("SELECT b.* FROM system_storage_blob b " + "LEFT JOIN system_storage_attachment a ON a.storage_blob_id = b.id AND a.deleted = 0 " + "WHERE a.id IS NULL AND b.id > #{lastId} " + "ORDER BY b.id LIMIT #{limit}") List<SystemStorageBlobDO> selectOrphanBlobsByIdRange(@Param("lastId") long lastId, @Param("limit") int limit); } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/ErrorCodeConstants.java
@@ -168,4 +168,12 @@ // ========== ç«å ä¿¡åé 1-002-028-000 ========== ErrorCode NOTIFY_SEND_TEMPLATE_PARAM_MISS = new ErrorCode(1_002_028_000, "模æ¿åæ°({})缺失"); // ========== æä»¶åå¨ 1-002-029-000 ========== ErrorCode STORAGE_FILE_EMPTY = new ErrorCode(1_002_029_000, "ä¸ä¼ æä»¶ä¸è½ä¸ºç©º"); ErrorCode STORAGE_BLOB_UPLOAD_FAILED = new ErrorCode(1_002_029_001, "æä»¶ä¸ä¼ 失败"); ErrorCode STORAGE_BLOB_TOKEN_INVALID = new ErrorCode(1_002_029_002, "æä»¶è®¿é®tokenæ ææå·²è¿æ"); ErrorCode STORAGE_BLOB_TOKEN_MISMATCH = new ErrorCode(1_002_029_003, "æä»¶è®¿é®token䏿件ä¸å¹é "); ErrorCode STORAGE_BLOB_NOT_EXISTS = new ErrorCode(1_002_029_004, "æä»¶ä¸åå¨"); ErrorCode STORAGE_BLOB_PUBLIC_KEY_INVALID = new ErrorCode(1_002_029_005, "æä»¶å ¬å¼è®¿é®keyæ æ"); } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageApplicationTypeEnum.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,33 @@ package cn.iocoder.yudao.module.system.enums.storage; import lombok.AllArgsConstructor; import lombok.Getter; /** * æä»¶ç¨éæä¸¾ * ç¨äºåºåå䏿¡ä¸å¡è®°å½ä¸ä¸åç¨éçéä»¶ */ @Getter @AllArgsConstructor public enum StorageApplicationTypeEnum { FILE("file", "éç¨æä»¶"), IMAGE("image", "å¾ç"), AVATAR("avatar", "头å"); private final String type; private final String description; /** * æ ¹æ® type å¼è·å对åºçæä¸¾å®ä¾ */ public static StorageApplicationTypeEnum getByType(String type) { for (StorageApplicationTypeEnum enumValue : StorageApplicationTypeEnum.values()) { if (enumValue.getType().equals(type)) { return enumValue; } } throw new IllegalArgumentException("æ æçæä»¶ç¨éç±»å: " + type); } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/enums/storage/StorageRecordTypeEnum.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,50 @@ package cn.iocoder.yudao.module.system.enums.storage; import lombok.AllArgsConstructor; import lombok.Getter; /** * ä¸å¡è®°å½ç±»åæä¸¾ * ç¨äºæ è®°æä»¶å±äºåªç±»ä¸å¡è®°å½ * ä¸ä¼ 宿åï¼éä»¶éè¿ recordType + recordId åä¸å¡æ°æ®å ³è * * æéæ©å±ï¼è³å°ä¿ç COMMON_FILE ä½ä¸ºéç¨ç±»å */ @Getter @AllArgsConstructor public enum StorageRecordTypeEnum { // ===== éç¨ ===== COMMON_FILE("common_file", "éç¨æä»¶"), // ===== ç产 ===== PRODUCTION_ORDER("production_order", "ç产工å"), // ===== è´¨æ£ ===== QUALITY_INSPECT("quality_inspect", "è´¨æ£"), // ===== è®¾å¤ ===== DEVICE_MAINTENANCE("device_maintenance", "设å¤ç»´æ¤"), // ===== éç¥å ¬å ===== NOTICE("notice", "éç¥å ¬å"), // ===== 头å ===== AVATAR("avatar", "头å"); private final String type; private final String description; /** * æ ¹æ® type å¼è·å对åºçæä¸¾å®ä¾ */ public static StorageRecordTypeEnum getByType(String type) { for (StorageRecordTypeEnum enumValue : StorageRecordTypeEnum.values()) { if (enumValue.getType().equals(type)) { return enumValue; } } throw new IllegalArgumentException("æ æçè®°å½ç±»å: " + type); } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/framework/storage/config/StorageProperties.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,47 @@ package cn.iocoder.yudao.module.system.framework.storage.config; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; import org.springframework.util.unit.DataSize; import java.math.BigDecimal; /** * æä»¶ä¸ä¼ é 置类 * * å¯¹åº application.yml ä¸ç yudao.storage åç¼é ç½® */ @Component @ConfigurationProperties(prefix = "yudao.storage", ignoreUnknownFields = true) @Data public class StorageProperties { /** æä»¶å卿 ¹ç®å½ */ private String path = "/data/uploads"; /** URLè·¯å¾åç¼ï¼å¦ /admin-api/system/storage-fileï¼ï¼ç¨äºæ¼æ¥é¢è§/ä¸è½½å°å */ private String urlPrefix = "/admin-api/system/storage-file"; /** åååç¼ï¼ç¨äºæ¼æ¥å®æ´çé¢è§/ä¸è½½å°å */ private String domain = "http://localhost:48080"; /** tokenè¿ææ¶é´ï¼åéï¼ï¼é»è®¤120åé */ private BigDecimal expired = new BigDecimal("120"); /** tokenä½¿ç¨æ¬¡æ°ä¸éï¼é»è®¤10次 */ private Integer useLimit = 10; /** JWTå¯é¥ */ private String jwtSecret = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG"; /** æ¯å¦å¯ç¨å¾çå缩 */ private Boolean compress = true; /** 触åå缩çæä»¶å¤§å°éå¼ */ private DataSize needCompressSize = DataSize.ofMegabytes(10); /** å缩质éï¼0.0~1.0ï¼ */ private float compressQuality = 0.5f; } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/StorageBlobCleanupTask.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,192 @@ package cn.iocoder.yudao.module.system.service.storage; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageBlobMapper; import cn.iocoder.yudao.module.system.framework.storage.config.StorageProperties; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import org.springframework.util.StringUtils; import java.io.File; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Deque; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.atomic.AtomicBoolean; /** * æ¸ çæ ææä»¶å®æ¶ä»»å¡ * æ¯æ1å·åæ¨2ç¹æ§è¡ä¸æ¬¡ * * æ¸ çå å®¹ï¼ * 1. å é¤ system_storage_blob 䏿ªè¢« system_storage_attachment å ³èçè®°å½åå ¶æä»¶ * 2. å é¤ç£çä¸ä¸åå¨äº system_storage_blob.uid_filename çæä»¶ */ @Slf4j @Component @RequiredArgsConstructor public class StorageBlobCleanupTask { private static final int DB_BATCH_SIZE = 500; private static final int FILE_NAME_BATCH_SIZE = 1000; private final SystemStorageBlobMapper storageBlobMapper; private final StorageProperties properties; private final AtomicBoolean running = new AtomicBoolean(false); /** * æ¯æ 1 å·åæ¨ 2 ç¹æ§è¡ä¸æ¬¡ */ @Scheduled(cron = "0 0 2 1 * ?") public void cleanupUnusedStorageFiles() { if (!running.compareAndSet(false, true)) { log.warn("æä»¶æ¸ ç任塿£å¨æ§è¡ï¼æ¬æ¬¡è·³è¿"); return; } long start = System.currentTimeMillis(); log.info("æä»¶æ¸ çä»»å¡å¼å§æ§è¡ï¼æ ¹ç®å½ï¼{}", properties.getPath()); try { int removedBlobCount = cleanupOrphanStorageBlobs(); int removedDiskFileCount = cleanupOrphanDiskFiles(); long cost = System.currentTimeMillis() - start; log.info("æä»¶æ¸ ç任塿§è¡å®æï¼å é¤å¤å¿ blob è®°å½ï¼{}ï¼å é¤ç£çæ ææä»¶ï¼{}ï¼èæ¶ï¼{} ms", removedBlobCount, removedDiskFileCount, cost); } catch (Exception e) { log.error("æä»¶æ¸ ç任塿§è¡å¤±è´¥", e); } finally { running.set(false); } } private int cleanupOrphanStorageBlobs() { long lastId = 0L; int removedCount = 0; while (true) { List<SystemStorageBlobDO> orphanBlobs = storageBlobMapper.selectOrphanBlobsByIdRange(lastId, DB_BATCH_SIZE); if (CollectionUtils.isEmpty(orphanBlobs)) { break; } List<Long> ids = new ArrayList<>(orphanBlobs.size()); for (SystemStorageBlobDO blob : orphanBlobs) { ids.add(blob.getId()); deleteBlobFiles(blob); } storageBlobMapper.deleteByIds(ids); removedCount += ids.size(); lastId = orphanBlobs.get(orphanBlobs.size() - 1).getId(); log.info("å·²å é¤ä¸æ¹å¤å¿ blobï¼batchSize={}ï¼lastId={}", ids.size(), lastId); } return removedCount; } private int cleanupOrphanDiskFiles() { File rootDirectory = new File(properties.getPath()); if (!rootDirectory.exists() || !rootDirectory.isDirectory()) { log.warn("æä»¶æ ¹ç®å½ä¸å卿䏿¯ç®å½ï¼è·³è¿ç£çæ¸ çï¼{}", properties.getPath()); return 0; } int deletedCount = 0; Deque<File> directories = new ArrayDeque<>(); directories.push(rootDirectory); while (!directories.isEmpty()) { File currentDirectory = directories.pop(); File[] children = currentDirectory.listFiles(); if (children == null || children.length == 0) { continue; } List<File> filesInDirectory = new ArrayList<>(); for (File child : children) { if (child.isDirectory()) { directories.push(child); } else if (child.isFile()) { filesInDirectory.add(child); } } deletedCount += cleanupFilesInDirectory(filesInDirectory); } return deletedCount; } private int cleanupFilesInDirectory(List<File> filesInDirectory) { if (CollectionUtils.isEmpty(filesInDirectory)) { return 0; } int deletedCount = 0; for (int start = 0; start < filesInDirectory.size(); start += FILE_NAME_BATCH_SIZE) { int end = Math.min(start + FILE_NAME_BATCH_SIZE, filesInDirectory.size()); List<File> batchFiles = filesInDirectory.subList(start, end); List<String> fileNames = new ArrayList<>(batchFiles.size()); for (File file : batchFiles) { fileNames.add(file.getName()); } Set<String> existingFileNames = new HashSet<>(storageBlobMapper.selectExistingUidFilenames(fileNames)); for (File file : batchFiles) { // è·³è¿å缩æä»¶ï¼thumb_ åç¼ï¼ï¼å®ä»¬éåæä»¶ä¸èµ·ç®¡ç if (file.getName().startsWith("thumb_")) { continue; } if (!existingFileNames.contains(file.getName()) && safeDelete(file)) { deletedCount++; } } } return deletedCount; } private void deleteBlobFiles(SystemStorageBlobDO blob) { File originalFile = resolveBlobFile(blob); safeDelete(originalFile); File compressedFile = resolveCompressedFile(originalFile); safeDelete(compressedFile); } private File resolveBlobFile(SystemStorageBlobDO blob) { String basePath = properties.getPath(); if (!StringUtils.hasText(blob.getPath())) { return new File(basePath, blob.getUidFilename()); } return new File(new File(basePath, blob.getPath()), blob.getUidFilename()); } private File resolveCompressedFile(File originalFile) { if (originalFile == null) { return null; } File parent = originalFile.getParentFile(); if (parent == null) { return null; } return new File(parent, "thumb_" + originalFile.getName()); } private boolean safeDelete(File file) { if (file == null || !file.exists() || !file.isFile()) { return false; } if (file.delete()) { return true; } log.warn("å é¤æä»¶å¤±è´¥ï¼{}", file.getAbsolutePath()); return false; } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemStorageAttachmentService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,51 @@ package cn.iocoder.yudao.module.system.service.storage; 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 java.util.List; /** * æä»¶å ³è表 Service æ¥å£ * * @author èéæºç */ public interface SystemStorageAttachmentService { /** * ä¿åéä»¶å ³èä¿¡æ¯ */ void bindAttachments(SystemStorageAttachmentSaveReqVO reqVO); /** * æ¥è¯¢éä»¶å表 * * @param reqVO æ¥è¯¢æ¡ä»¶ * @return æä»¶å表ï¼å«é¢è§/ä¸è½½å°åï¼ */ List<SystemStorageBlobRespVO> listAttachments(SystemStorageAttachmentListReqVO reqVO); /** * æ¹éå é¤éä»¶ * * @param ids éä»¶IDå表 */ void deleteAttachments(List<Long> ids); /** * å 餿å®ç¨éãæå®ä¸å¡è®°å½ä¸çéä»¶å ³ç³» */ void deleteAttachmentsByApplicationAndRecordTypeAndRecordId(String application, String recordType, Long recordId); /** * å 餿å®ä¸å¡è®°å½ä¸å ¨é¨éä»¶å ³ç³»ï¼ä¸åºåç¨é */ void deleteAttachmentsByRecordTypeAndRecordId(String recordType, Long recordId); /** * æå¤ä¸ªä¸å¡idæ¹éå é¤éä»¶å ³ç³» */ void deleteAttachmentsByApplicationAndRecordTypeAndRecordIds(String application, String recordType, List<Long> recordIds); } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemStorageAttachmentServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,71 @@ package cn.iocoder.yudao.module.system.service.storage; 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.enums.storage.StorageApplicationTypeEnum; import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum; import cn.iocoder.yudao.module.system.util.storage.StorageFileUtil; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; /** * æä»¶å ³è表 Service å®ç°ç±» * * @author èéæºç */ @Service @RequiredArgsConstructor public class SystemStorageAttachmentServiceImpl implements SystemStorageAttachmentService { private final StorageFileUtil fileUtil; @Override @Transactional(rollbackFor = Exception.class) public void bindAttachments(SystemStorageAttachmentSaveReqVO reqVO) { fileUtil.saveStorageAttachmentByRecordTypeAndRecordId( reqVO.getApplication(), StorageRecordTypeEnum.getByType(reqVO.getRecordType()), reqVO.getRecordId(), reqVO.getBlobItems() ); } @Override public List<SystemStorageBlobRespVO> listAttachments(SystemStorageAttachmentListReqVO reqVO) { return fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId( reqVO.getApplication(), reqVO.getRecordType(), reqVO.getRecordId() ); } @Override @Transactional(rollbackFor = Exception.class) public void deleteAttachments(List<Long> ids) { fileUtil.deleteStorageAttachmentsByStorageAttachmentIds(ids); } @Override @Transactional(rollbackFor = Exception.class) public void deleteAttachmentsByApplicationAndRecordTypeAndRecordId(String application, String recordType, Long recordId) { fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType, recordId); } @Override @Transactional(rollbackFor = Exception.class) public void deleteAttachmentsByRecordTypeAndRecordId(String recordType, Long recordId) { fileUtil.deleteStorageAttachmentsByRecordTypeAndRecordId(recordType, recordId); } @Override @Transactional(rollbackFor = Exception.class) public void deleteAttachmentsByApplicationAndRecordTypeAndRecordIds(String application, String recordType, List<Long> recordIds) { fileUtil.deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds); } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemStorageBlobService.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,67 @@ package cn.iocoder.yudao.module.system.service.storage; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.util.List; /** * æä»¶ä¸»è¡¨ Service æ¥å£ * * @author èéæºç */ public interface SystemStorageBlobService { /** * ä¸ä¼ æä»¶ * * @param files æä»¶å表 * @param isPublic æ¯å¦å ¬å ±æä»¶ï¼true=æ°¸ä¹ ææï¼false=临æ¶tokenæ§å¶ï¼ * @return ä¸ä¼ ç»æï¼å«é¢è§/ä¸è½½å°åï¼ */ List<SystemStorageBlobRespVO> upload(List<MultipartFile> files, Boolean isPublic); /** * éè¿tokenè·åæä»¶ * * @param fileName å¯ä¸æä»¶å * @param token JWT token * @return æä»¶å¯¹è±¡ */ File getFileByToken(String fileName, String token); /** * éè¿publicKeyè·åå ¬å ±æä»¶ * * @param fileName å¯ä¸æä»¶å * @param publicKey å ¬å ±è®¿é®key * @return æä»¶å¯¹è±¡ */ File getPublicFile(String fileName, String publicKey); /** * è·åä¸è½½æ¶çåå§æä»¶å * * @param fileName å¯ä¸æä»¶å * @return åå§æä»¶å */ String getDownloadFileName(String fileName); /** * è·å¾æä»¶ * * @param id ç¼å· * @return æä»¶ */ SystemStorageBlobDO getStorageBlob(Long id); /** * æ¹éå 餿件 * * @param ids ç¼å·å表 */ void deleteStorageBlobs(List<Long> ids); } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/service/storage/SystemStorageBlobServiceImpl.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,183 @@ package cn.iocoder.yudao.module.system.service.storage; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageBlobMapper; import cn.iocoder.yudao.module.system.framework.storage.config.StorageProperties; import cn.iocoder.yudao.module.system.util.storage.StorageFileUtil; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import jakarta.annotation.Resource; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.util.StringUtils; import org.springframework.web.multipart.MultipartFile; import javax.crypto.SecretKey; import java.io.File; import java.io.IOException; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; import java.util.UUID; import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception; import static cn.iocoder.yudao.module.system.enums.ErrorCodeConstants.*; /** * æä»¶ä¸»è¡¨ Service å®ç°ç±» * * @author èéæºç */ @Service @RequiredArgsConstructor public class SystemStorageBlobServiceImpl implements SystemStorageBlobService { private final StorageProperties properties; @Resource private SystemStorageBlobMapper storageBlobMapper; private final StorageFileUtil fileUtil; @Override public List<SystemStorageBlobRespVO> upload(List<MultipartFile> files, Boolean isPublic) { if (files == null || files.isEmpty()) { throw exception(STORAGE_FILE_EMPTY); } List<SystemStorageBlobRespVO> result = new ArrayList<>(); for (MultipartFile file : files) { if (file == null || file.isEmpty()) { throw exception(STORAGE_FILE_EMPTY); } String originalFileName = StringUtils.hasText(file.getOriginalFilename()) ? StringUtils.cleanPath(file.getOriginalFilename()) : UUID.randomUUID().toString(); String fileName = UUID.randomUUID() + "_" + originalFileName; String relativePath = fileUtil.buildRelativePath(); File targetDirectory = new File(properties.getPath(), relativePath); if (!targetDirectory.exists() && !targetDirectory.mkdirs()) { throw exception(STORAGE_BLOB_UPLOAD_FAILED); } File dest = new File(targetDirectory, fileName); SystemStorageBlobRespVO blobRespVO; try { file.transferTo(dest); blobRespVO = createStorageBlob(file, originalFileName, fileName, relativePath, isPublic); if (blobRespVO == null || blobRespVO.getId() == null) { throw exception(STORAGE_BLOB_UPLOAD_FAILED); } } catch (RuntimeException e) { if (dest.exists()) { dest.delete(); } throw e; } catch (IOException e) { throw exception(STORAGE_BLOB_UPLOAD_FAILED); } result.add(blobRespVO); } return result; } @Override public File getFileByToken(String fileName, String token) { if (!StringUtils.hasText(token)) { throw exception(STORAGE_BLOB_TOKEN_INVALID); } String secretStr = properties.getJwtSecret(); SecretKey key = Keys.hmacShaKeyFor(secretStr.getBytes(StandardCharsets.UTF_8)); Claims claims; try { claims = Jwts.parser() .verifyWith(key) .build() .parseSignedClaims(token) .getPayload(); } catch (Exception e) { throw exception(STORAGE_BLOB_TOKEN_INVALID); } if (!fileName.equals(claims.getSubject())) { throw exception(STORAGE_BLOB_TOKEN_MISMATCH); } fileUtil.validateTokenUsage(token); SystemStorageBlobDO blob = storageBlobMapper.selectByUidFilename(fileName); String path = blob == null ? claims.get("path", String.class) : blob.getPath(); if (!StringUtils.hasText(path)) { return new File(properties.getPath(), fileName); } return new File(new File(properties.getPath(), path), fileName); } @Override public File getPublicFile(String fileName, String publicKey) { if (!StringUtils.hasText(fileName)) { throw exception(STORAGE_BLOB_NOT_EXISTS); } if (!StringUtils.hasText(publicKey)) { throw exception(STORAGE_BLOB_PUBLIC_KEY_INVALID); } SystemStorageBlobDO blob = storageBlobMapper.selectByUidFilenameAndResourceKey(fileName, publicKey); if (blob == null) { throw exception(STORAGE_BLOB_PUBLIC_KEY_INVALID); } String path = blob.getPath(); if (!StringUtils.hasText(path)) { return new File(properties.getPath(), fileName); } return new File(new File(properties.getPath(), path), fileName); } @Override public String getDownloadFileName(String fileName) { SystemStorageBlobDO blob = storageBlobMapper.selectByUidFilename(fileName); if (blob == null || !StringUtils.hasText(blob.getOriginalFilename())) { return fileName; } return blob.getOriginalFilename(); } @Override public SystemStorageBlobDO getStorageBlob(Long id) { return storageBlobMapper.selectById(id); } @Override public void deleteStorageBlobs(List<Long> ids) { fileUtil.deleteStorageBlobs(ids); } private SystemStorageBlobRespVO createStorageBlob(MultipartFile file, String originalFileName, String fileName, String relativePath, Boolean isPublic) { SystemStorageBlobDO blob = new SystemStorageBlobDO(); blob.setResourceKey(UUID.randomUUID().toString().replace("-", "")); blob.setContentType(file.getContentType()); blob.setOriginalFilename(originalFileName); blob.setUidFilename(fileName); blob.setByteSize(file.getSize()); blob.setPath(relativePath); storageBlobMapper.insert(blob); // æå»ºè¿å VO SystemStorageBlobRespVO respVO = BeanUtils.toBean(blob, SystemStorageBlobRespVO.class); if (Boolean.TRUE.equals(isPublic)) { respVO.setPreviewURL(fileUtil.buildSignedUrl(blob, "/preview/", BigDecimal.valueOf(-1))); respVO.setDownloadURL(fileUtil.buildSignedUrl(blob, "/download/", BigDecimal.valueOf(-1))); } else { respVO.setPreviewURL(fileUtil.buildSignedPreviewUrl(blob)); respVO.setUrl(fileUtil.buildSignedPreviewUrl(blob)); respVO.setName(blob.getOriginalFilename()); respVO.setDownloadURL(fileUtil.buildSignedDownloadUrl(blob)); } return respVO; } } yudao-module-system/src/main/java/cn/iocoder/yudao/module/system/util/storage/StorageFileUtil.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,602 @@ package cn.iocoder.yudao.module.system.util.storage; import cn.hutool.core.collection.CollUtil; import cn.iocoder.yudao.framework.common.util.object.BeanUtils; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageBlobRespVO; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentRespVO; import cn.iocoder.yudao.module.system.controller.admin.storage.vo.SystemStorageAttachmentSaveReqVO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageAttachmentDO; import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO; import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageAttachmentMapper; import cn.iocoder.yudao.module.system.dal.mysql.storage.SystemStorageBlobMapper; import cn.iocoder.yudao.module.system.enums.storage.StorageApplicationTypeEnum; import cn.iocoder.yudao.module.system.enums.storage.StorageRecordTypeEnum; import cn.iocoder.yudao.module.system.framework.storage.config.StorageProperties; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; import lombok.RequiredArgsConstructor; import net.coobird.thumbnailator.Thumbnails; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import javax.crypto.SecretKey; import java.io.File; import java.math.BigDecimal; import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; /** * æä»¶ä¸ä¼ ä½ç³»æ ¸å¿å·¥å ·ç±» * * 主è¦èè´£ï¼ * - æä»¶ä¸ä¸å¡è®°å½ç»å®ï¼ä¿åéä»¶å ³ç³»ï¼ * - æä»¶ä¸éä»¶å é¤ * - éä»¶æ¥è¯¢ * - é¢è§/ä¸è½½å°åçæï¼å¸¦JWTç¾åï¼ * - tokenä½¿ç¨æ¬¡æ°æ§å¶ï¼Redisï¼ * - å¾çå缩 * * @author èéæºç */ @Component @RequiredArgsConstructor public class StorageFileUtil { private final StorageProperties properties; private final SystemStorageAttachmentMapper storageAttachmentMapper; private final SystemStorageBlobMapper storageBlobMapper; private final StringRedisTemplate stringRedisTemplate; private static final String TOKEN_USAGE_KEY_PREFIX = "storage:token:usage:"; private static final DateTimeFormatter YEAR_PATH_FORMATTER = DateTimeFormatter.ofPattern("yyyy"); private static final DateTimeFormatter MONTH_DAY_PATH_FORMATTER = DateTimeFormatter.ofPattern("MMdd"); // ==================== ä¿åéä»¶å ³ç³» ==================== /** * æ"æä»¶ç¨é + è®°å½ç±»å + è®°å½id"ä¿åéä»¶å ³ç³» * é»è¾ï¼å å 餿§éä»¶ï¼åæ¹éæå ¥æ°éä»¶ï¼æ´ä½æ¿æ¢æ¨¡å¼ï¼ */ public void saveStorageAttachment(StorageApplicationTypeEnum application, StorageRecordTypeEnum recordType, Long recordId, List<SystemStorageAttachmentSaveReqVO.BlobItem> blobItems) { if (application == null) { throw new IllegalArgumentException("æä»¶ç¨éä¸è½ä¸ºç©º"); } if (recordType == null) { throw new IllegalArgumentException("å ³èè®°å½ç±»åä¸è½ä¸ºç©º"); } if (recordId == null || recordId <= 0) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } // å 餿§éä»¶ä¿¡æ¯ deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application.getType(), recordType.getType(), recordId); if (CollUtil.isEmpty(blobItems)) { return; } List<SystemStorageAttachmentDO> attachments = new ArrayList<>(); for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) { SystemStorageAttachmentDO attachment = new SystemStorageAttachmentDO(); String app = (item.getApplication() != null && !item.getApplication().trim().isEmpty()) ? item.getApplication() : application.getType(); attachment.setApplication(app); attachment.setRecordType(recordType.getType()); attachment.setRecordId(recordId); attachment.setStorageBlobId(item.getBlobId()); attachments.add(attachment); } storageAttachmentMapper.insertBatch(attachments); } /** * æ recordType + recordId ä¿åéä»¶å ³ç³»ï¼application 坿å®ä¹å¯ä»æ¯ä¸ªæä»¶å¯¹è±¡é读å */ public void saveStorageAttachmentByRecordTypeAndRecordId(String application, StorageRecordTypeEnum recordType, Long recordId, List<SystemStorageAttachmentSaveReqVO.BlobItem> blobItems) { if (recordType == null) { throw new IllegalArgumentException("å ³èè®°å½ç±»åä¸è½ä¸ºç©º"); } if (recordId == null) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } // å 餿§éä»¶ä¿¡æ¯ if (application == null || application.trim().isEmpty()) { for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) { String itemApp = item.getApplication(); if (itemApp == null || itemApp.trim().isEmpty()) { throw new IllegalArgumentException("æä»¶ç¨éä¸è½ä¸ºç©º"); } deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(itemApp, recordType.getType(), recordId); } } else { deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(application, recordType.getType(), recordId); } if (CollUtil.isEmpty(blobItems)) { deleteStorageAttachmentsByRecordTypeAndRecordId(recordType.getType(), recordId); return; } List<SystemStorageAttachmentDO> attachments = new ArrayList<>(); for (SystemStorageAttachmentSaveReqVO.BlobItem item : blobItems) { SystemStorageAttachmentDO attachment = new SystemStorageAttachmentDO(); String app = (application != null && !application.trim().isEmpty()) ? application : item.getApplication(); attachment.setApplication(StorageApplicationTypeEnum.getByType(app).getType()); attachment.setRecordType(recordType.getType()); attachment.setRecordId(recordId); attachment.setStorageBlobId(item.getBlobId()); attachments.add(attachment); } storageAttachmentMapper.insertBatch(attachments); } // ==================== å é¤æä»¶ä¸»è¡¨ ==================== public void deleteStorageBlobs(List<Long> storageBlobIds) { if (CollUtil.isEmpty(storageBlobIds)) { return; } // åæ¶å é¤ç£çæä»¶ List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds); for (SystemStorageBlobDO blob : blobs) { deleteBlobFile(blob); } storageBlobMapper.deleteByIds(storageBlobIds); } public void deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds) { if (CollUtil.isEmpty(storageAttachmentIds)) { return; } List<SystemStorageAttachmentDO> attachments = storageAttachmentMapper.selectByIds(storageAttachmentIds); List<Long> storageBlobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); deleteStorageBlobs(storageBlobIds); } public void deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds( String application, String recordType, Collection<Long> recordIds) { if (recordIds == null || recordIds.isEmpty()) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } List<SystemStorageAttachmentDO> attachments = storageAttachmentMapper .selectListByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds); if (CollUtil.isNotEmpty(attachments)) { List<Long> blobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); deleteStorageBlobs(blobIds); } } public void deleteStorageBlobsByRecordTypeAndRecordId(String recordType, Long recordId) { if (recordId == null || recordId <= 0) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } List<SystemStorageAttachmentDO> attachments = storageAttachmentMapper .selectListByRecordTypeAndRecordId(recordType, recordId); if (CollUtil.isNotEmpty(attachments)) { List<Long> blobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); deleteStorageBlobs(blobIds); } } // ==================== å é¤éä»¶å ³ç³» ==================== public void deleteStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) { if (CollUtil.isEmpty(storageAttachmentIds)) { return; } deleteStorageBlobsByStorageAttachmentIds(storageAttachmentIds); storageAttachmentMapper.deleteByIds(storageAttachmentIds); } public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { if (recordId == null || recordId <= 0) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(application, recordType, Collections.singletonList(recordId)); storageAttachmentMapper.deleteByApplicationAndRecordTypeAndRecordId(application, recordType, recordId); } public void deleteStorageAttachmentsByRecordTypeAndRecordId(String recordType, Long recordId) { if (recordId == null || recordId <= 0) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } deleteStorageBlobsByRecordTypeAndRecordId(recordType, recordId); storageAttachmentMapper.deleteByRecordTypeAndRecordId(recordType, recordId); } public void deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds( String application, String recordType, List<Long> recordIds) { if (recordIds == null || recordIds.isEmpty()) { throw new IllegalArgumentException("å ³èè®°å½idä¸è½ä¸ºç©º"); } deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds); storageAttachmentMapper.deleteByApplicationAndRecordTypeAndRecordIds(application, recordType, recordIds); } // ==================== æ¥è¯¢éä»¶å ³ç³» ==================== public List<SystemStorageAttachmentDO> getStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds) { if (CollUtil.isEmpty(storageAttachmentIds)) { throw new IllegalArgumentException("éä»¶IDä¸è½ä¸ºç©º"); } return storageAttachmentMapper.selectByIds(storageAttachmentIds); } public List<SystemStorageAttachmentDO> getStorageAttachmentsByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { return storageAttachmentMapper.selectListByApplicationAndRecordTypeAndRecordId(application, recordType, recordId); } public List<SystemStorageAttachmentDO> getStorageAttachmentsByRecordTypeAndRecordId( String recordType, Long recordId) { return storageAttachmentMapper.selectListByRecordTypeAndRecordId(recordType, recordId); } // ==================== æ¥è¯¢æä»¶ä¿¡æ¯ ==================== /** * æ ¹æ®éä»¶å ³ç³»idæ¥è¯¢æä»¶å表ï¼èªå¨æå»ºpreviewURLãdownloadURLãstorageAttachmentIdï¼ */ public List<SystemStorageBlobRespVO> getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByStorageAttachmentIds(storageAttachmentIds); if (CollUtil.isEmpty(attachments)) { return null; } Map<Long, Long> blobIdToAttachmentIdMap = attachments.stream() .collect(Collectors.toMap(SystemStorageAttachmentDO::getStorageBlobId, SystemStorageAttachmentDO::getId)); List<Long> storageBlobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds); List<SystemStorageBlobRespVO> result = new ArrayList<>(); for (SystemStorageBlobDO blob : blobs) { SystemStorageBlobRespVO vo = buildStorageBlobRespVO(blob); vo.setStorageAttachmentId(blobIdToAttachmentIdMap.get(blob.getId())); result.add(vo); } return result; } /** * æç¨éãä¸å¡ç±»åãä¸å¡idæ¥è¯¢æä»¶å表 */ public List<SystemStorageBlobRespVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId( application, recordType, recordId); if (CollUtil.isEmpty(attachments)) { return null; } Map<Long, Long> blobIdToAttachmentIdMap = attachments.stream() .collect(Collectors.toMap(SystemStorageAttachmentDO::getStorageBlobId, SystemStorageAttachmentDO::getId)); List<Long> storageBlobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds); List<SystemStorageBlobRespVO> result = new ArrayList<>(); for (SystemStorageBlobDO blob : blobs) { SystemStorageBlobRespVO vo = buildStorageBlobRespVO(blob); vo.setStorageAttachmentId(blobIdToAttachmentIdMap.get(blob.getId())); result.add(vo); } return result; } /** * æä¸å¡ç±»åãä¸å¡idæ¥è¯¢æä»¶å表ï¼ä¸åºåç¨éï¼ */ public List<SystemStorageBlobRespVO> getStorageBlobVOsByRecordTypeAndRecordId(String recordType, Long recordId) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByRecordTypeAndRecordId(recordType, recordId); if (CollUtil.isEmpty(attachments)) { return null; } List<Long> storageBlobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds); List<SystemStorageBlobRespVO> result = new ArrayList<>(); for (SystemStorageBlobDO blob : blobs) { result.add(buildStorageBlobRespVO(blob)); } return result; } /** * æç¨éãä¸å¡ç±»åãä¸å¡idæ¥è¯¢æä»¶å表ï¼èªå®ä¹è¿ææ¶é´ï¼åéï¼ */ public List<SystemStorageBlobRespVO> getStorageBlobVOsByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId, BigDecimal expired) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId( application, recordType, recordId); if (CollUtil.isEmpty(attachments)) { return null; } Map<Long, Long> blobIdToAttachmentIdMap = attachments.stream() .collect(Collectors.toMap(SystemStorageAttachmentDO::getStorageBlobId, SystemStorageAttachmentDO::getId)); List<Long> storageBlobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(storageBlobIds); List<SystemStorageBlobRespVO> result = new ArrayList<>(); for (SystemStorageBlobDO blob : blobs) { SystemStorageBlobRespVO vo = BeanUtils.toBean(blob, SystemStorageBlobRespVO.class); vo.setPreviewURL(buildSignedUrl(blob, "/preview/", expired)); vo.setUrl(buildSignedUrl(blob, "/preview/", expired)); vo.setName(blob.getOriginalFilename()); vo.setDownloadURL(buildSignedUrl(blob, "/download/", expired)); vo.setStorageAttachmentId(blobIdToAttachmentIdMap.get(blob.getId())); result.add(vo); } return result; } // ==================== æ¥è¯¢éä»¶è§å¾ ==================== /** * æç¨éãä¸å¡ç±»åãä¸å¡idæ¥è¯¢éä»¶è§å¾ */ public List<SystemStorageAttachmentRespVO> getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId( application, recordType, recordId); if (CollUtil.isEmpty(attachments)) { return new ArrayList<>(); } List<SystemStorageAttachmentRespVO> result = new ArrayList<>(); for (SystemStorageAttachmentDO attachment : attachments) { SystemStorageAttachmentRespVO vo = BeanUtils.toBean(attachment, SystemStorageAttachmentRespVO.class); List<SystemStorageBlobRespVO> blobVOs = getStorageBlobVOsByStorageAttachmentIds( Collections.singletonList(attachment.getId())); vo.setBlobList(CollUtil.isEmpty(blobVOs) ? new ArrayList<>() : blobVOs); result.add(vo); } return result; } // ==================== ä» è·åé¢è§/ä¸è½½å°å ==================== public List<String> getFilePreviewURLsByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId( application, recordType, recordId); if (CollUtil.isEmpty(attachments)) { return new ArrayList<>(); } List<Long> blobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(blobIds); return blobs.stream() .map(this::buildSignedPreviewUrl) .collect(Collectors.toList()); } public List<String> getFileDownloadURLsByApplicationAndRecordTypeAndRecordId( String application, String recordType, Long recordId) { List<SystemStorageAttachmentDO> attachments = getStorageAttachmentsByApplicationAndRecordTypeAndRecordId( application, recordType, recordId); if (CollUtil.isEmpty(attachments)) { return new ArrayList<>(); } List<Long> blobIds = attachments.stream() .map(SystemStorageAttachmentDO::getStorageBlobId) .collect(Collectors.toList()); List<SystemStorageBlobDO> blobs = storageBlobMapper.selectByIds(blobIds); return blobs.stream() .map(this::buildSignedDownloadUrl) .collect(Collectors.toList()); } // ==================== æå»º VO ==================== private SystemStorageBlobRespVO buildStorageBlobRespVO(SystemStorageBlobDO blob) { SystemStorageBlobRespVO vo = BeanUtils.toBean(blob, SystemStorageBlobRespVO.class); vo.setPreviewURL(buildSignedPreviewUrl(blob)); vo.setUrl(buildSignedPreviewUrl(blob)); vo.setName(blob.getOriginalFilename()); vo.setDownloadURL(buildSignedDownloadUrl(blob)); return vo; } // ==================== æå»ºç¾åURL ==================== public String buildSignedPreviewUrl(SystemStorageBlobDO blob) { return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/preview/", properties.getExpired()); } public String buildSignedDownloadUrl(SystemStorageBlobDO blob) { return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/download/", properties.getExpired()); } public String buildSignedPreviewUrl(SystemStorageBlobRespVO blob) { return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/preview/", properties.getExpired()); } public String buildSignedDownloadUrl(SystemStorageBlobRespVO blob) { return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), "/download/", properties.getExpired()); } public String buildSignedUrl(SystemStorageBlobDO blob, String actionPath, BigDecimal expired) { return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), actionPath, expired); } public String buildSignedUrl(SystemStorageBlobRespVO blob, String actionPath, BigDecimal expired) { return buildSignedUrlFromFields(blob.getUidFilename(), blob.getPath(), blob.getResourceKey(), actionPath, expired); } /** * æå»ºç»ä¸ç带ç¾åé¢è§/ä¸è½½å°å * * @param uidFilename å¯ä¸æä»¶å * @param path æä»¶ç¸å¯¹è·¯å¾ * @param resourceKey èµæºå¯ä¸æ è¯ * @param actionPath æä½è·¯å¾ "/preview/" æ "/download/" * @param expired è¿ææ¶é´ï¼åéï¼ï¼-1è¡¨ç¤ºæ°¸ä¹ ææï¼èµ°publicKeyï¼ * @return 带ç¾åçURL */ private String buildSignedUrlFromFields(String uidFilename, String path, String resourceKey, String actionPath, BigDecimal expired) { if (!Arrays.asList("/preview/", "/download/").contains(actionPath)) { throw new IllegalArgumentException("æä½è·¯å¾åæ°é误"); } if (!StringUtils.hasText(uidFilename)) { throw new IllegalArgumentException("æä»¶ä¿¡æ¯ä¸å®æ´"); } String domain = StringUtils.trimTrailingCharacter(properties.getDomain(), '/'); String prefix = properties.getUrlPrefix().startsWith("/") ? properties.getUrlPrefix() : "/" + properties.getUrlPrefix(); String baseUrl = domain + prefix + actionPath + uidFilename; // -1 è¡¨ç¤ºæ°¸ä¹ ææï¼ä¸çæ tokenï¼æ¹ä¸º publicKey ç»åæ ¡éª if (expired != null && BigDecimal.valueOf(-1L).compareTo(expired) == 0) { if (!StringUtils.hasText(resourceKey)) { throw new IllegalArgumentException("å ¬å¼é¾æ¥ç¼ºå°publicKey"); } return baseUrl + "?publicKey=" + resourceKey; } long now = System.currentTimeMillis(); BigDecimal expiredValue = expired == null ? new BigDecimal("120") : expired; long expiredMillis = expiredValue.multiply(new BigDecimal("60000")).longValue(); if (expiredMillis <= 0L) { expiredMillis = 2L * 60L * 60L * 1000L; } Date issuedAt = new Date(now); Date expiration = new Date(now + expiredMillis); SecretKey key = Keys.hmacShaKeyFor(properties.getJwtSecret().getBytes(StandardCharsets.UTF_8)); String token = Jwts.builder() .subject(uidFilename) .issuedAt(issuedAt) .expiration(expiration) .claim("path", path) .claim("resourceKey", resourceKey) .signWith(key) .compact(); cacheTokenUsage(token, expiredMillis); return baseUrl + "?token=" + token; } // ==================== tokenä½¿ç¨æ§å¶ ==================== private void cacheTokenUsage(String token, long expiredMillis) { if (!StringUtils.hasText(token)) { return; } long ttl = expiredMillis > 0L ? expiredMillis : 2L * 60L * 60L * 1000L; stringRedisTemplate.opsForValue().set(buildTokenUsageKey(token), "0", ttl, TimeUnit.MILLISECONDS); } private String buildTokenUsageKey(String token) { return TOKEN_USAGE_KEY_PREFIX + token; } /** * æ ¡éªtokenæ¯å¦è¿è½ç»§ç»ä½¿ç¨ */ public void validateTokenUsage(String token) { String redisKey = buildTokenUsageKey(token); String currentCountValue = stringRedisTemplate.opsForValue().get(redisKey); if (!StringUtils.hasText(currentCountValue)) { throw new IllegalArgumentException("龿¥å·²è¿ææè¾¾å°ä½¿ç¨æ¬¡æ°å¤±æ"); } long currentCount = Long.parseLong(currentCountValue); int limit = resolveLimit(); if (currentCount >= limit) { stringRedisTemplate.delete(redisKey); throw new IllegalArgumentException("龿¥è¾¾å°ä½¿ç¨æ¬¡æ°å¤±æ"); } Long updatedCount = stringRedisTemplate.opsForValue().increment(redisKey); if (updatedCount != null && updatedCount >= limit) { stringRedisTemplate.delete(redisKey); } } private int resolveLimit() { return properties.getUseLimit() == null || properties.getUseLimit() <= 0 ? 10 : properties.getUseLimit(); } // ==================== è·¯å¾ä¸å缩 ==================== /** * çææä»¶åå¨ç¸å¯¹è·¯å¾ï¼æ ¼å¼ï¼yyyy/MMddï¼å¦ 2026/0430ï¼ */ public String buildRelativePath() { LocalDate now = LocalDate.now(); return now.format(YEAR_PATH_FORMATTER) + "/" + now.format(MONTH_DAY_PATH_FORMATTER); } /** * 对å¾çè¿è¡å缩ï¼éå¾çæä¸æ»¡è¶³æ¡ä»¶æ¶è¿ååæä»¶ */ public File compressFile(File file) { if (Boolean.TRUE.equals(properties.getCompress()) && isImage(file.getName()) && file.length() > properties.getNeedCompressSize().toBytes()) { try { File compressedFile = new File(file.getParent(), "thumb_" + file.getName()); if (compressedFile.exists()) { return compressedFile; } Thumbnails.of(file) .scale(1.0f) .outputQuality(properties.getCompressQuality()) .toFile(compressedFile); return compressedFile; } catch (Exception e) { return file; } } return file; } private boolean isImage(String fileName) { String ext = fileName.substring(fileName.lastIndexOf(".") + 1).toLowerCase(); return "jpg".equals(ext) || "jpeg".equals(ext) || "png".equals(ext); } // ==================== ç£çæä»¶æä½ ==================== /** * å é¤ blob 对åºçç£çæä»¶ï¼å«å缩æä»¶ï¼ */ private void deleteBlobFile(SystemStorageBlobDO blob) { File originalFile = resolveBlobFile(blob); safeDelete(originalFile); if (originalFile != null && originalFile.getParentFile() != null) { File compressedFile = new File(originalFile.getParentFile(), "thumb_" + originalFile.getName()); safeDelete(compressedFile); } } private File resolveBlobFile(SystemStorageBlobDO blob) { String basePath = properties.getPath(); if (!StringUtils.hasText(blob.getPath())) { return new File(basePath, blob.getUidFilename()); } return new File(new File(basePath, blob.getPath()), blob.getUidFilename()); } private boolean safeDelete(File file) { if (file == null || !file.exists() || !file.isFile()) { return false; } return file.delete(); } } yudao-server/src/main/resources/application-dev.yaml
@@ -190,6 +190,16 @@ transfer-notify-url: https://yunai.natapp1.cc/admin-api/pay/notify/transfer # æ¯ä»æ¸ éçã转账ãåè°å°å demo: false # å¼å¯æ¼ç¤ºæ¨¡å¼ tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ å°å¾çå¯é¥ https://lbs.qq.com/service/staticV2/staticGuide/staticDoc storage: # æä»¶ä¸ä¼ é ç½® path: /data/uploads # æä»¶å卿 ¹ç®å½ url-prefix: /admin-api/system/storage-blob # URLè·¯å¾åç¼ domain: https://dev.your-domain.com # åååç¼ï¼æå®é ååé ç½®ï¼ expired: 120 # tokenè¿ææ¶é´ï¼åéï¼ use-limit: 10 # tokenä½¿ç¨æ¬¡æ°ä¸é jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWTå¯é¥ compress: true # æ¯å¦å缩å¾ç need-compress-size: 10MB # 触åå缩çæä»¶å¤§å°éå¼ compress-quality: 0.5 # å缩质é(0.0-1.0) justauth: enabled: true yudao-server/src/main/resources/application-local.yaml
@@ -263,6 +263,16 @@ wxa-subscribe-message: miniprogram-state: developer # 跳转å°ç¨åºç±»åï¼å¼åç为 âdeveloperâï¼ä½éªç为 âtrialâä¸ºï¼æ£å¼ç为 âformalâ tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ å°å¾çå¯é¥ https://lbs.qq.com/service/staticV2/staticGuide/staticDoc storage: # æä»¶ä¸ä¼ é ç½® path: D:/uploads # æä»¶å卿 ¹ç®å½ï¼æå®é æå¡å¨è·¯å¾é ç½®ï¼ url-prefix: /admin-api/system/storage-blob # URLè·¯å¾åç¼ domain: http://localhost:48080 # åååç¼ï¼æå®é ååé ç½®ï¼ expired: 120 # tokenè¿ææ¶é´ï¼åéï¼ use-limit: 10 # tokenä½¿ç¨æ¬¡æ°ä¸é jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWTå¯é¥ï¼è³å°32åç¬¦ï¼ compress: true # æ¯å¦å缩å¾ç need-compress-size: 10MB # 触åå缩çæä»¶å¤§å°éå¼ compress-quality: 0.5 # å缩质é(0.0-1.0) justauth: enabled: true yudao-server/src/main/resources/application-test.yaml
@@ -263,6 +263,16 @@ wxa-subscribe-message: miniprogram-state: developer # 跳转å°ç¨åºç±»åï¼å¼åç为 âdeveloperâï¼ä½éªç为 âtrialâä¸ºï¼æ£å¼ç为 âformalâ tencent-lbs-key: TVDBZ-TDILD-4ON4B-PFDZA-RNLKH-VVF6E # QQ å°å¾çå¯é¥ https://lbs.qq.com/service/staticV2/staticGuide/staticDoc storage: # æä»¶ä¸ä¼ é ç½® path: /data/uploads # æä»¶å卿 ¹ç®å½ url-prefix: /admin-api/system/storage-blob # URLè·¯å¾åç¼ domain: https://test.your-domain.com # åååç¼ï¼æå®é ååé ç½®ï¼ expired: 120 # tokenè¿ææ¶é´ï¼åéï¼ use-limit: 10 # tokenä½¿ç¨æ¬¡æ°ä¸é jwt-secret: abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG # JWTå¯é¥ compress: true # æ¯å¦å缩å¾ç need-compress-size: 10MB # 触åå缩çæä»¶å¤§å°éå¼ compress-quality: 0.5 # å缩质é(0.0-1.0) justauth: enabled: true