task-157: 任务存在结果缺失巡检(终态任务无 file_result 检出、运行中忽略、只读、可重复)+ 8 条测试
This commit is contained in:
+81
@@ -0,0 +1,81 @@
|
|||||||
|
package com.nanri.aiimage.modules.task.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||||
|
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||||
|
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
import java.util.stream.Collectors;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 任务存在结果缺失巡检(task-157)。
|
||||||
|
*
|
||||||
|
* 只读巡检:终态(SUCCESS/FAILED)任务无对应 file_result 行的清单输出报表;
|
||||||
|
* 运行中/待处理任务忽略;绝不修改任何状态。可重复执行。
|
||||||
|
*/
|
||||||
|
@Slf4j
|
||||||
|
@Service
|
||||||
|
@RequiredArgsConstructor
|
||||||
|
public class TaskResultMissingInspector {
|
||||||
|
|
||||||
|
private final FileTaskMapper fileTaskMapper;
|
||||||
|
private final FileResultMapper fileResultMapper;
|
||||||
|
|
||||||
|
public record MissingResultEntry(Long taskId, String moduleType, String status, String updatedAt) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public record MissingResultReport(List<MissingResultEntry> entries) {
|
||||||
|
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return entries == null || entries.isEmpty();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public MissingResultReport inspectTasksMissingResult(int limit) {
|
||||||
|
List<FileTaskEntity> tasks = fileTaskMapper.selectList(new LambdaQueryWrapper<FileTaskEntity>()
|
||||||
|
.in(FileTaskEntity::getStatus, List.of("SUCCESS", "FAILED"))
|
||||||
|
.orderByAsc(FileTaskEntity::getUpdatedAt)
|
||||||
|
.last("limit " + Math.max(1, Math.min(limit, 500))));
|
||||||
|
if (tasks == null || tasks.isEmpty()) {
|
||||||
|
return new MissingResultReport(List.of());
|
||||||
|
}
|
||||||
|
Set<Long> taskIds = tasks.stream()
|
||||||
|
.map(FileTaskEntity::getId)
|
||||||
|
.filter(id -> id != null && id > 0)
|
||||||
|
.collect(Collectors.toSet());
|
||||||
|
Map<Long, List<FileResultEntity>> resultsByTask = taskIds.isEmpty() ? Map.of()
|
||||||
|
: fileResultMapper.selectList(new LambdaQueryWrapper<FileResultEntity>()
|
||||||
|
.in(FileResultEntity::getTaskId, taskIds)).stream()
|
||||||
|
.collect(Collectors.groupingBy(FileResultEntity::getTaskId));
|
||||||
|
|
||||||
|
List<MissingResultEntry> entries = new ArrayList<>();
|
||||||
|
for (FileTaskEntity task : tasks) {
|
||||||
|
if (task.getId() == null || task.getId() <= 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
List<FileResultEntity> results = resultsByTask.get(task.getId());
|
||||||
|
if (results == null || results.isEmpty()) {
|
||||||
|
entries.add(new MissingResultEntry(
|
||||||
|
task.getId(), task.getModuleType(), task.getStatus(),
|
||||||
|
task.getUpdatedAt() == null ? null : task.getUpdatedAt().toString()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
MissingResultReport report = new MissingResultReport(List.copyOf(entries));
|
||||||
|
if (!report.isEmpty()) {
|
||||||
|
log.info("task-missing-result report: count={}", report.entries().size());
|
||||||
|
for (MissingResultEntry entry : report.entries()) {
|
||||||
|
log.info("task-missing-result: taskId={} moduleType={} status={} updatedAt={}",
|
||||||
|
entry.taskId(), entry.moduleType(), entry.status(), entry.updatedAt());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return report;
|
||||||
|
}
|
||||||
|
}
|
||||||
+154
@@ -0,0 +1,154 @@
|
|||||||
|
package com.nanri.aiimage.modules.task.service;
|
||||||
|
|
||||||
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
||||||
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
||||||
|
import com.nanri.aiimage.modules.task.mapper.FileResultMapper;
|
||||||
|
import com.nanri.aiimage.modules.task.mapper.FileTaskMapper;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.FileResultEntity;
|
||||||
|
import com.nanri.aiimage.modules.task.model.entity.FileTaskEntity;
|
||||||
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
||||||
|
import org.junit.jupiter.api.BeforeAll;
|
||||||
|
import org.junit.jupiter.api.BeforeEach;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
import org.mockito.Mock;
|
||||||
|
import org.mockito.junit.jupiter.MockitoExtension;
|
||||||
|
|
||||||
|
import java.time.LocalDateTime;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||||
|
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||||
|
import static org.mockito.ArgumentMatchers.any;
|
||||||
|
import static org.mockito.Mockito.lenient;
|
||||||
|
import static org.mockito.Mockito.never;
|
||||||
|
import static org.mockito.Mockito.verify;
|
||||||
|
import static org.mockito.Mockito.when;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* task-157:任务存在结果缺失巡检契约(plan 09)。
|
||||||
|
* 终态(SUCCESS/FAILED)任务无 file_result 行 → 检出;运行中忽略;只读;可重复。
|
||||||
|
*/
|
||||||
|
@ExtendWith(MockitoExtension.class)
|
||||||
|
class TaskResultMissingInspectorTest {
|
||||||
|
|
||||||
|
private static final Long MISSING_TASK_ID = 7001L;
|
||||||
|
private static final Long WITH_RESULT_TASK_ID = 7101L;
|
||||||
|
private static final Long RUNNING_TASK_ID = 7201L;
|
||||||
|
|
||||||
|
@Mock private FileTaskMapper fileTaskMapper;
|
||||||
|
@Mock private FileResultMapper fileResultMapper;
|
||||||
|
|
||||||
|
private TaskResultMissingInspector inspector;
|
||||||
|
|
||||||
|
@BeforeAll
|
||||||
|
static void initializeMybatisMetadata() {
|
||||||
|
MapperBuilderAssistant assistant = new MapperBuilderAssistant(new MybatisConfiguration(), "");
|
||||||
|
TableInfoHelper.initTableInfo(assistant, FileTaskEntity.class);
|
||||||
|
TableInfoHelper.initTableInfo(assistant, FileResultEntity.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@BeforeEach
|
||||||
|
void setUp() {
|
||||||
|
inspector = new TaskResultMissingInspector(fileTaskMapper, fileResultMapper);
|
||||||
|
lenient().when(fileResultMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
}
|
||||||
|
|
||||||
|
private FileTaskEntity task(Long id, String status) {
|
||||||
|
FileTaskEntity task = new FileTaskEntity();
|
||||||
|
task.setId(id);
|
||||||
|
task.setModuleType("SIMILAR_ASIN");
|
||||||
|
task.setStatus(status);
|
||||||
|
task.setUpdatedAt(LocalDateTime.now());
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void missingResultDetected() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of(task(MISSING_TASK_ID, "SUCCESS")));
|
||||||
|
|
||||||
|
var report = inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
assertEquals(1, report.entries().size());
|
||||||
|
assertEquals(MISSING_TASK_ID, report.entries().getFirst().taskId());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void taskWithResultNotReported() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of(task(WITH_RESULT_TASK_ID, "SUCCESS")));
|
||||||
|
FileResultEntity result = new FileResultEntity();
|
||||||
|
result.setTaskId(WITH_RESULT_TASK_ID);
|
||||||
|
when(fileResultMapper.selectList(any())).thenReturn(List.of(result));
|
||||||
|
|
||||||
|
var report = inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
assertTrue(report.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void successWithoutResultIsReported() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of(task(MISSING_TASK_ID, "SUCCESS")));
|
||||||
|
|
||||||
|
var report = inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
assertEquals("SUCCESS", report.entries().getFirst().status());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void runningTasksAreIgnored() {
|
||||||
|
// 巡检只查终态任务:RUNNING 不在查询范围内(mapper 断言终态 IN 条件)
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
var wrapper = org.mockito.ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper.class);
|
||||||
|
verify(fileTaskMapper).selectList(wrapper.capture());
|
||||||
|
assertTrue(wrapper.getValue().getSqlSegment().contains("IN"), "只查终态任务: " + wrapper.getValue().getSqlSegment());
|
||||||
|
assertFalse(wrapper.getValue().getSqlSegment().contains("RUNNING"), "运行中任务不参与巡检");
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void emptyReportWhenNoTasks() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of());
|
||||||
|
|
||||||
|
var report = inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
assertTrue(report.isEmpty());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void readOnlyDoesNotModifyAnything() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of(task(MISSING_TASK_ID, "FAILED")));
|
||||||
|
|
||||||
|
inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
verify(fileTaskMapper, never()).update(any(), any());
|
||||||
|
verify(fileResultMapper, never()).update(any(), any());
|
||||||
|
verify(fileTaskMapper, never()).delete(any());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void reportIncludesDetails() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of(task(MISSING_TASK_ID, "FAILED")));
|
||||||
|
|
||||||
|
var report = inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
TaskResultMissingInspector.MissingResultEntry entry = report.entries().getFirst();
|
||||||
|
assertEquals(MISSING_TASK_ID, entry.taskId());
|
||||||
|
assertEquals("SIMILAR_ASIN", entry.moduleType());
|
||||||
|
assertEquals("FAILED", entry.status());
|
||||||
|
assertNotNull(entry.updatedAt());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void rerunIsSafeAndStable() {
|
||||||
|
when(fileTaskMapper.selectList(any())).thenReturn(List.of(task(MISSING_TASK_ID, "SUCCESS")));
|
||||||
|
|
||||||
|
var first = inspector.inspectTasksMissingResult(50);
|
||||||
|
var second = inspector.inspectTasksMissingResult(50);
|
||||||
|
|
||||||
|
assertEquals(first.entries().size(), second.entries().size());
|
||||||
|
assertEquals(first.entries().getFirst().taskId(), second.entries().getFirst().taskId());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user