diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java index f59fdf6b..266318e2 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java @@ -163,4 +163,15 @@ public class CcdiFileUploadController extends BaseController { CcdiFileUploadRecord record = fileUploadService.getById(id); return AjaxResult.success(record); } + + /** + * 删除上传记录 + */ + @DeleteMapping("/{id}") + @Operation(summary = "删除上传文件", description = "按上传记录ID删除文件并清理流水") + public AjaxResult deleteFile(@PathVariable Long id) { + Long userId = SecurityUtils.getUserId(); + String message = fileUploadService.deleteFileUploadRecord(id, userId); + return AjaxResult.success(message); + } } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java index bc106773..3fc66bc9 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java @@ -29,6 +29,9 @@ public class CcdiFileUploadStatisticsVO implements Serializable { /** 解析失败数量 */ private Long parsedFailed; + /** 已删除数量 */ + private Long deleted; + /** 总数量 */ private Long total; } diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java index 104cd149..01286d6e 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java @@ -52,6 +52,15 @@ public interface ICcdiFileUploadService { Long userId, String username); + /** + * 删除上传记录并清理关联数据 + * + * @param id 上传记录ID + * @param operatorUserId 当前操作用户ID + * @return 删除结果 + */ + String deleteFileUploadRecord(Long id, Long operatorUserId); + /** * 查询上传记录列表 * diff --git a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java index cbea371d..7d41c2db 100644 --- a/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java +++ b/ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java @@ -18,6 +18,7 @@ import com.ruoyi.lsfx.constants.LsfxConstants; import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest; import com.ruoyi.lsfx.domain.request.GetBankStatementRequest; import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest; +import com.ruoyi.lsfx.domain.request.DeleteFilesRequest; import com.ruoyi.lsfx.domain.response.*; import jakarta.annotation.Resource; import lombok.Data; @@ -207,6 +208,30 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { return batchId; } + @Override + public String deleteFileUploadRecord(Long id, Long operatorUserId) { + CcdiFileUploadRecord record = recordMapper.selectById(id); + validateDeleteRecord(record); + + DeleteFilesRequest request = new DeleteFilesRequest(); + request.setGroupId(record.getLsfxProjectId()); + request.setLogIds(new Integer[]{record.getLogId()}); + request.setUserId(toUploadUserId(operatorUserId)); + + DeleteFilesResponse response = lsfxClient.deleteFiles(request); + if (response == null || Boolean.FALSE.equals(response.getSuccessResponse())) { + throw new RuntimeException("流水分析平台删除文件失败"); + } + + bankStatementMapper.deleteByProjectIdAndBatchId(record.getProjectId(), record.getLogId()); + + CcdiFileUploadRecord update = new CcdiFileUploadRecord(); + update.setId(record.getId()); + update.setFileStatus("deleted"); + recordMapper.updateById(update); + return "删除成功"; + } + @Override public Page selectPage(Page page, CcdiFileUploadQueryDTO queryDTO) { @@ -249,6 +274,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { vo.setParsing(0L); vo.setParsedSuccess(0L); vo.setParsedFailed(0L); + vo.setDeleted(0L); long total = 0L; for (Map item : statusCounts) { @@ -261,6 +287,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { case "parsing" -> vo.setParsing(count); case "parsed_success" -> vo.setParsedSuccess(count); case "parsed_failed" -> vo.setParsedFailed(count); + case "deleted" -> vo.setDeleted(count); } } @@ -861,6 +888,24 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService { bankStatementMapper.deleteByProjectIdAndBatchId(projectId, logId); } + private void validateDeleteRecord(CcdiFileUploadRecord record) { + if (record == null) { + throw new RuntimeException("上传记录不存在"); + } + if (!"parsed_success".equals(record.getFileStatus())) { + if ("deleted".equals(record.getFileStatus())) { + throw new RuntimeException("文件已删除,请勿重复操作"); + } + throw new RuntimeException("仅支持删除解析成功文件"); + } + if (record.getLsfxProjectId() == null) { + throw new RuntimeException("缺少流水分析项目ID"); + } + if (record.getLogId() == null) { + throw new RuntimeException("缺少文件logId"); + } + } + private Integer toUploadUserId(Long userId) { if (userId == null) { throw new IllegalArgumentException("当前登录用户ID不能为空"); diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java index bdbcc32e..26e00f36 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java @@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets; import java.util.List; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; @@ -64,4 +65,19 @@ class CcdiFileUploadControllerTest { assertEquals(200, result.get("code")); } } + + @Test + void deleteFile_shouldUseCurrentLoginUserId() { + try (MockedStatic mocked = mockStatic(SecurityUtils.class)) { + mocked.when(SecurityUtils::getUserId).thenReturn(9527L); + when(fileUploadService.deleteFileUploadRecord(123L, 9527L)) + .thenReturn("删除成功"); + + AjaxResult result = controller.deleteFile(123L); + + assertEquals(200, result.get("code")); + assertEquals("删除成功", result.get("msg")); + verify(fileUploadService).deleteFileUploadRecord(123L, 9527L); + } + } } diff --git a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java index a2186c29..e744f20b 100644 --- a/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java +++ b/ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java @@ -5,6 +5,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.read.ListAppender; import com.alibaba.excel.EasyExcel; import com.ruoyi.ccdi.project.domain.CcdiProject; +import com.ruoyi.ccdi.project.domain.vo.CcdiFileUploadStatisticsVO; import com.ruoyi.ccdi.project.domain.entity.CcdiFileUploadRecord; import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper; import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper; @@ -12,6 +13,7 @@ import com.ruoyi.ccdi.project.mapper.CcdiProjectMapper; import com.ruoyi.lsfx.client.LsfxAnalysisClient; import com.ruoyi.lsfx.domain.request.GetBankStatementRequest; import com.ruoyi.lsfx.domain.response.CheckParseStatusResponse; +import com.ruoyi.lsfx.domain.response.DeleteFilesResponse; import com.ruoyi.lsfx.domain.response.FetchInnerFlowResponse; import com.ruoyi.lsfx.domain.response.GetBankStatementResponse; import com.ruoyi.lsfx.domain.response.GetFileUploadStatusResponse; @@ -37,6 +39,7 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicInteger; @@ -47,7 +50,9 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -255,6 +260,61 @@ class CcdiFileUploadServiceImplTest { ); } + @Test + void deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted() { + CcdiFileUploadRecord record = buildRecord(); + record.setProjectId(PROJECT_ID); + record.setLsfxProjectId(LSFX_PROJECT_ID); + record.setLogId(LOG_ID); + record.setFileStatus("parsed_success"); + + when(recordMapper.selectById(RECORD_ID)).thenReturn(record); + when(lsfxClient.deleteFiles(any())).thenReturn(buildDeleteFilesResponse()); + + String result = service.deleteFileUploadRecord(RECORD_ID, 9527L); + + assertEquals("删除成功", result); + verify(lsfxClient).deleteFiles(argThat(request -> + request.getGroupId().equals(LSFX_PROJECT_ID) + && request.getUserId().equals(9527) + && request.getLogIds().length == 1 + && request.getLogIds()[0].equals(LOG_ID) + )); + verify(bankStatementMapper).deleteByProjectIdAndBatchId(PROJECT_ID, LOG_ID); + verify(recordMapper).updateById(org.mockito.ArgumentMatchers.argThat(item -> + RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus()) + )); + } + + @Test + void deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus() { + CcdiFileUploadRecord record = buildRecord(); + record.setFileStatus("parsed_failed"); + when(recordMapper.selectById(RECORD_ID)).thenReturn(record); + + RuntimeException exception = assertThrows(RuntimeException.class, + () -> service.deleteFileUploadRecord(RECORD_ID, 9527L)); + + assertTrue(exception.getMessage().contains("仅支持删除解析成功文件")); + } + + @Test + void deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails() { + CcdiFileUploadRecord record = buildRecord(); + record.setFileStatus("parsed_success"); + record.setLogId(LOG_ID); + record.setLsfxProjectId(LSFX_PROJECT_ID); + when(recordMapper.selectById(RECORD_ID)).thenReturn(record); + when(lsfxClient.deleteFiles(any())).thenThrow(new RuntimeException("lsfx delete failed")); + + assertThrows(RuntimeException.class, () -> service.deleteFileUploadRecord(RECORD_ID, 9527L)); + + verify(bankStatementMapper, never()).deleteByProjectIdAndBatchId(any(), any()); + verify(recordMapper, never()).updateById(org.mockito.ArgumentMatchers.argThat(item -> + "deleted".equals(item.getFileStatus()) + )); + } + // @Test // void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() { // when(lsfxClient.fetchInnerFlow(any())).thenThrow(new RuntimeException("fetch inner flow failed")); @@ -414,6 +474,20 @@ class CcdiFileUploadServiceImplTest { assertFalse(events.stream().anyMatch(event -> event.endsWith("record:parsed_success"))); } + @Test + void countByStatus_shouldIncludeDeletedCount() { + when(recordMapper.countByStatus(PROJECT_ID)).thenReturn(List.of( + Map.of("status", "uploading", "count", 1), + Map.of("status", "deleted", "count", 2) + )); + + CcdiFileUploadStatisticsVO result = service.countByStatus(PROJECT_ID); + + assertEquals(1L, result.getUploading()); + assertEquals(2L, result.getDeleted()); + assertEquals(3L, result.getTotal()); + } + private void captureRecordStatus(List events, AtomicInteger sequence) { doAnswer(invocation -> { CcdiFileUploadRecord record = invocation.getArgument(0); @@ -493,6 +567,12 @@ class CcdiFileUploadServiceImplTest { return response; } + private DeleteFilesResponse buildDeleteFilesResponse() { + DeleteFilesResponse response = new DeleteFilesResponse(); + response.setSuccessResponse(Boolean.TRUE); + return response; + } + private GetFileUploadStatusResponse buildParsedSuccessStatusResponse(String uploadFileName) { GetFileUploadStatusResponse response = buildParsedSuccessStatusResponse(); response.getData().getLogs().get(0).setUploadFileName(uploadFileName);