Files
ccdi/docs/plans/backend/2026-03-16-project-upload-file-delete-backend-implementation.md

13 KiB
Raw Blame History

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_statusdeleted;统计 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

@Test
void deleteFile_shouldUseCurrentLoginUserId() {
    try (MockedStatic<SecurityUtils> 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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest#deleteFile_shouldUseCurrentLoginUserId

Expected:

  • FAIL
  • 原因是 CcdiFileUploadController 中还没有 deleteFile 方法或 ICcdiFileUploadService 中还没有 deleteFileUploadRecord 方法

Step 3: Write minimal implementation

在接口和控制器中补最小实现:

String deleteFileUploadRecord(Long id, Long operatorUserId);
@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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest#deleteFile_shouldUseCurrentLoginUserId

Expected:

  • PASS

Step 5: Commit

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 中新增成功链路测试:

@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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted

Expected:

  • FAIL
  • 原因是 CcdiFileUploadServiceImpl 中还没有 deleteFileUploadRecord 实现

Step 3: Write minimal implementation

CcdiFileUploadServiceImpl 中实现删除主链路:

@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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldDeletePlatformFileBankStatementsAndMarkDeleted

Expected:

  • PASS

Step 5: Commit

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

为关键边界新增失败测试,至少覆盖“状态不允许删除”和“平台删除失败时不应更新本地状态”:

@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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus,CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails

Expected:

  • FAIL
  • 原因是现有实现还没有完整的前置校验和失败保护

Step 3: Write minimal implementation

补全 Service 校验和异常处理:

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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldRejectNonParsedSuccessStatus,CcdiFileUploadServiceImplTest#deleteFileUploadRecord_shouldStopWhenLsfxDeleteFails

Expected:

  • PASS

Step 5: Commit

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

@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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#countByStatus_shouldIncludeDeletedCount

Expected:

  • FAIL
  • 原因是 CcdiFileUploadStatisticsVO 还没有 deleted 字段,或 countByStatus 还没有映射该状态

Step 3: Write minimal implementation

在 VO 和 Service 中补 deleted 字段及映射:

private Long deleted;
vo.setDeleted(0L);
case "deleted" -> vo.setDeleted(count);

Step 4: Run test to verify it passes

Run:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadServiceImplTest#countByStatus_shouldIncludeDeletedCount

Expected:

  • PASS

Step 5: Commit

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:

mvn test -pl ccdi-project -am -Dtest=CcdiFileUploadControllerTest,CcdiFileUploadServiceImplTest

Expected:

  • 全部 PASS

Step 2: Run module compile

Run:

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

git add .
git commit -m "test: 完成上传文件删除后端回归验证"