整理docs目录并补充文档规范

This commit is contained in:
wkc
2026-03-17 15:06:59 +08:00
parent 9cb77b096e
commit 2fd93463b8
111 changed files with 706 additions and 100 deletions

View File

@@ -0,0 +1,410 @@
# 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<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:
```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: 完成上传文件删除后端回归验证"
```