Merge branch 'codex/project-upload-file-delete-backend' into dev

This commit is contained in:
wkc
2026-03-16 14:57:21 +08:00
6 changed files with 164 additions and 0 deletions

View File

@@ -163,4 +163,15 @@ public class CcdiFileUploadController extends BaseController {
CcdiFileUploadRecord record = fileUploadService.getById(id); CcdiFileUploadRecord record = fileUploadService.getById(id);
return AjaxResult.success(record); 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);
}
} }

View File

@@ -29,6 +29,9 @@ public class CcdiFileUploadStatisticsVO implements Serializable {
/** 解析失败数量 */ /** 解析失败数量 */
private Long parsedFailed; private Long parsedFailed;
/** 已删除数量 */
private Long deleted;
/** 总数量 */ /** 总数量 */
private Long total; private Long total;
} }

View File

@@ -52,6 +52,15 @@ public interface ICcdiFileUploadService {
Long userId, Long userId,
String username); String username);
/**
* 删除上传记录并清理关联数据
*
* @param id 上传记录ID
* @param operatorUserId 当前操作用户ID
* @return 删除结果
*/
String deleteFileUploadRecord(Long id, Long operatorUserId);
/** /**
* 查询上传记录列表 * 查询上传记录列表
* *

View File

@@ -18,6 +18,7 @@ import com.ruoyi.lsfx.constants.LsfxConstants;
import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest; import com.ruoyi.lsfx.domain.request.FetchInnerFlowRequest;
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest; import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest; import com.ruoyi.lsfx.domain.request.GetFileUploadStatusRequest;
import com.ruoyi.lsfx.domain.request.DeleteFilesRequest;
import com.ruoyi.lsfx.domain.response.*; import com.ruoyi.lsfx.domain.response.*;
import jakarta.annotation.Resource; import jakarta.annotation.Resource;
import lombok.Data; import lombok.Data;
@@ -207,6 +208,30 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
return batchId; 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 @Override
public Page<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> page, public Page<CcdiFileUploadRecord> selectPage(Page<CcdiFileUploadRecord> page,
CcdiFileUploadQueryDTO queryDTO) { CcdiFileUploadQueryDTO queryDTO) {
@@ -249,6 +274,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
vo.setParsing(0L); vo.setParsing(0L);
vo.setParsedSuccess(0L); vo.setParsedSuccess(0L);
vo.setParsedFailed(0L); vo.setParsedFailed(0L);
vo.setDeleted(0L);
long total = 0L; long total = 0L;
for (Map<String, Object> item : statusCounts) { for (Map<String, Object> item : statusCounts) {
@@ -261,6 +287,7 @@ public class CcdiFileUploadServiceImpl implements ICcdiFileUploadService {
case "parsing" -> vo.setParsing(count); case "parsing" -> vo.setParsing(count);
case "parsed_success" -> vo.setParsedSuccess(count); case "parsed_success" -> vo.setParsedSuccess(count);
case "parsed_failed" -> vo.setParsedFailed(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); 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) { private Integer toUploadUserId(Long userId) {
if (userId == null) { if (userId == null) {
throw new IllegalArgumentException("当前登录用户ID不能为空"); throw new IllegalArgumentException("当前登录用户ID不能为空");

View File

@@ -16,6 +16,7 @@ import java.nio.charset.StandardCharsets;
import java.util.List; import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals; 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.mockStatic;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
@@ -64,4 +65,19 @@ class CcdiFileUploadControllerTest {
assertEquals(200, result.get("code")); assertEquals(200, result.get("code"));
} }
} }
@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);
}
}
} }

View File

@@ -5,6 +5,7 @@ import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender; import ch.qos.logback.core.read.ListAppender;
import com.alibaba.excel.EasyExcel; import com.alibaba.excel.EasyExcel;
import com.ruoyi.ccdi.project.domain.CcdiProject; 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.domain.entity.CcdiFileUploadRecord;
import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper; import com.ruoyi.ccdi.project.mapper.CcdiBankStatementMapper;
import com.ruoyi.ccdi.project.mapper.CcdiFileUploadRecordMapper; 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.client.LsfxAnalysisClient;
import com.ruoyi.lsfx.domain.request.GetBankStatementRequest; import com.ruoyi.lsfx.domain.request.GetBankStatementRequest;
import com.ruoyi.lsfx.domain.response.CheckParseStatusResponse; 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.FetchInnerFlowResponse;
import com.ruoyi.lsfx.domain.response.GetBankStatementResponse; import com.ruoyi.lsfx.domain.response.GetBankStatementResponse;
import com.ruoyi.lsfx.domain.response.GetFileUploadStatusResponse; import com.ruoyi.lsfx.domain.response.GetFileUploadStatusResponse;
@@ -37,6 +39,7 @@ import java.nio.file.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map;
import java.util.concurrent.Executor; import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicReference; import java.util.concurrent.atomic.AtomicReference;
import java.util.concurrent.atomic.AtomicInteger; 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.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.doAnswer;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; 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.<CcdiFileUploadRecord>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.<CcdiFileUploadRecord>argThat(item ->
"deleted".equals(item.getFileStatus())
));
}
// @Test // @Test
// void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() { // void processPullBankInfoAsync_shouldMarkParsedFailedWhenFetchInnerFlowThrows() {
// when(lsfxClient.fetchInnerFlow(any())).thenThrow(new RuntimeException("fetch inner flow failed")); // 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"))); 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<String> events, AtomicInteger sequence) { private void captureRecordStatus(List<String> events, AtomicInteger sequence) {
doAnswer(invocation -> { doAnswer(invocation -> {
CcdiFileUploadRecord record = invocation.getArgument(0); CcdiFileUploadRecord record = invocation.getArgument(0);
@@ -493,6 +567,12 @@ class CcdiFileUploadServiceImplTest {
return response; return response;
} }
private DeleteFilesResponse buildDeleteFilesResponse() {
DeleteFilesResponse response = new DeleteFilesResponse();
response.setSuccessResponse(Boolean.TRUE);
return response;
}
private GetFileUploadStatusResponse buildParsedSuccessStatusResponse(String uploadFileName) { private GetFileUploadStatusResponse buildParsedSuccessStatusResponse(String uploadFileName) {
GetFileUploadStatusResponse response = buildParsedSuccessStatusResponse(); GetFileUploadStatusResponse response = buildParsedSuccessStatusResponse();
response.getData().getLogs().get(0).setUploadFileName(uploadFileName); response.getData().getLogs().get(0).setUploadFileName(uploadFileName);