# Project Upload File Delete Backend Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** 为项目上传文件列表新增后端删除能力,支持删除已解析成功的文件、清理本地流水,并把上传记录状态更新为 `deleted`。 **Architecture:** 在 `CcdiFileUploadController` 新增按记录 ID 删除接口,由 Controller 获取当前登录用户 ID 并传给 `ICcdiFileUploadService`。Service 负责查询记录、校验状态、调用 `LsfxAnalysisClient.deleteFiles`、删除 `ccdi_bank_statement` 中对应 `logId` 的流水,并更新 `ccdi_file_upload_record.file_status` 为 `deleted`;统计 VO 同步扩展 `deleted` 状态。 **Tech Stack:** Java 21, Spring Boot 3, MyBatis Plus, JUnit 5, Mockito, Maven --- ### Task 1: 补齐删除接口控制器契约 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java` **Step 1: Write the failing test** 在 `CcdiFileUploadControllerTest` 中新增测试,验证删除接口会读取当前登录用户 ID 并调用 Service: ```java @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); } } ``` **Step 2: Run test to verify it fails** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest#deleteFile_shouldUseCurrentLoginUserId ``` Expected: - `FAIL` - 原因是 `CcdiFileUploadController` 中还没有 `deleteFile` 方法或 `ICcdiFileUploadService` 中还没有 `deleteFileUploadRecord` 方法 **Step 3: Write minimal implementation** 在接口和控制器中补最小实现: ```java String deleteFileUploadRecord(Long id, Long operatorUserId); ``` ```java @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); } ``` **Step 4: Run test to verify it passes** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest#deleteFile_shouldUseCurrentLoginUserId ``` Expected: - `PASS` **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/ICcdiFileUploadService.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java git commit -m "test: 补充上传文件删除接口控制器契约" ``` ### Task 2: 实现删除成功主链路 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` **Step 1: Write the failing test** 在 `CcdiFileUploadServiceImplTest` 中新增成功链路测试: ```java @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(argThat(item -> RECORD_ID.equals(item.getId()) && "deleted".equals(item.getFileStatus()) )); } ``` **Step 2: Run test to verify it fails** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted ``` Expected: - `FAIL` - 原因是 `CcdiFileUploadServiceImpl` 中还没有 `deleteFileUploadRecord` 实现 **Step 3: Write minimal implementation** 在 `CcdiFileUploadServiceImpl` 中实现删除主链路: ```java @Override @Transactional 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 "删除成功"; } ``` 同时补一个私有校验方法,仅先满足成功路径所需字段。 **Step 4: Run test to verify it passes** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted ``` Expected: - `PASS` **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "feat: 打通上传文件删除成功主链路" ``` ### Task 3: 补齐删除前置校验与失败保护 **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` **Step 1: Write the failing test** 为关键边界新增失败测试,至少覆盖“状态不允许删除”和“平台删除失败时不应更新本地状态”: ```java @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, org.mockito.Mockito.never()).deleteByProjectIdAndBatchId(any(), any()); verify(recordMapper, org.mockito.Mockito.never()).updateById(argThat(item -> "deleted".equals(item.getFileStatus()) )); } ``` **Step 2: Run test to verify it fails** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus,CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails ``` Expected: - `FAIL` - 原因是现有实现还没有完整的前置校验和失败保护 **Step 3: Write minimal implementation** 补全 Service 校验和异常处理: ```java 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"); } } ``` 不要吞掉平台删除异常,让事务直接回滚。 **Step 4: Run test to verify it passes** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus,CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails ``` Expected: - `PASS` **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "test: 补齐上传文件删除校验与失败保护" ``` ### Task 4: 扩展统计口径支持 `deleted` **Files:** - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java` - Modify: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java` - Test: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` **Step 1: Write the failing test** 新增统计测试,验证 `deleted` 状态会被正确映射到 VO: ```java @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()); } ``` **Step 2: Run test to verify it fails** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#countByStatus_shouldIncludeDeletedCount ``` Expected: - `FAIL` - 原因是 `CcdiFileUploadStatisticsVO` 还没有 `deleted` 字段,或 `countByStatus` 还没有映射该状态 **Step 3: Write minimal implementation** 在 VO 和 Service 中补 `deleted` 字段及映射: ```java private Long deleted; ``` ```java vo.setDeleted(0L); case "deleted" -> vo.setDeleted(count); ``` **Step 4: Run test to verify it passes** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#countByStatus_shouldIncludeDeletedCount ``` Expected: - `PASS` **Step 5: Commit** ```bash git add ccdi-project/src/main/java/com/ruoyi/ccdi/project/domain/vo/CcdiFileUploadStatisticsVO.java ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java git commit -m "feat: 扩展上传文件统计支持已删除状态" ``` ### Task 5: 跑后端回归验证 **Files:** - Verify only: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadController.java` - Verify only: `ccdi-project/src/main/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImpl.java` - Verify only: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/controller/CcdiFileUploadControllerTest.java` - Verify only: `ccdi-project/src/test/java/com/ruoyi/ccdi/project/service/impl/CcdiFileUploadServiceImplTest.java` **Step 1: Run focused tests** Run: ```bash mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest,CcdiFileUploadServiceImplTest ``` Expected: - 全部 `PASS` **Step 2: Run module compile** Run: ```bash mvn clean compile -pl ccdi-project -am ``` Expected: - `BUILD SUCCESS` **Step 3: Record manual API smoke checklist** 手工检查以下接口行为: - `DELETE /ccdi/file-upload/{id}` 删除 `parsed_success` 记录返回成功 - 删除 `parsed_failed` 记录返回错误 - 重复删除 `deleted` 记录返回错误 **Step 4: Commit** ```bash git add . git commit -m "test: 完成上传文件删除后端回归验证" ```