task-156: 孤立 Job 巡检报表(task/result 缺失孤儿检出、重试耗尽同样报、只读不修改、可重复)+ 8 条测试

This commit is contained in:
2026-09-02 06:53:06 +08:00
parent d64ef5284a
commit d773748ae9
2 changed files with 266 additions and 0 deletions
@@ -0,0 +1,91 @@
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.mapper.TaskFileJobMapper;
import com.nanri.aiimage.modules.task.model.entity.TaskFileJobEntity;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* 孤立 Job 巡检(task-156)。
*
* 只读巡检:task_file_job 无对应 task 或 result 的孤儿 Job 清单输出报表
* (日志 + 返回对象);绝不修改 Job 状态。巡检可重复执行。
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class OrphanJobInspector {
private final TaskFileJobMapper taskFileJobMapper;
private final FileTaskMapper fileTaskMapper;
private final FileResultMapper fileResultMapper;
public record OrphanJobEntry(Long jobId, Long taskId, Long resultId, String moduleType,
String status, String updatedAt) {
}
public record OrphanJobReport(List<OrphanJobEntry> entries) {
public boolean isEmpty() {
return entries == null || entries.isEmpty();
}
}
public OrphanJobReport inspectOrphanJobs(int limit) {
List<TaskFileJobEntity> jobs = taskFileJobMapper.selectList(new LambdaQueryWrapper<TaskFileJobEntity>()
.orderByAsc(TaskFileJobEntity::getUpdatedAt)
.last("limit " + Math.max(1, Math.min(limit, 500))));
if (jobs == null || jobs.isEmpty()) {
return new OrphanJobReport(List.of());
}
Set<Long> taskIds = jobs.stream()
.map(TaskFileJobEntity::getTaskId)
.filter(id -> id != null && id > 0)
.collect(Collectors.toSet());
Set<Long> resultIds = jobs.stream()
.map(TaskFileJobEntity::getResultId)
.filter(id -> id != null && id > 0)
.collect(Collectors.toSet());
Set<Long> existingTaskIds = taskIds.isEmpty() ? Set.of()
: fileTaskMapper.selectBatchIds(taskIds).stream()
.map(task -> task.getId())
.collect(Collectors.toSet());
Set<Long> existingResultIds = resultIds.isEmpty() ? Set.of()
: fileResultMapper.selectBatchIds(resultIds).stream()
.map(result -> result.getId())
.collect(Collectors.toSet());
List<OrphanJobEntry> entries = new ArrayList<>();
for (TaskFileJobEntity job : jobs) {
boolean taskMissing = job.getTaskId() != null && job.getTaskId() > 0
&& !existingTaskIds.contains(job.getTaskId());
boolean resultMissing = job.getResultId() != null && job.getResultId() > 0
&& !existingResultIds.contains(job.getResultId());
if (taskMissing || resultMissing) {
entries.add(new OrphanJobEntry(
job.getId(), job.getTaskId(), job.getResultId(), job.getModuleType(),
job.getStatus(),
job.getUpdatedAt() == null ? null : job.getUpdatedAt().toString()));
}
}
OrphanJobReport report = new OrphanJobReport(List.copyOf(entries));
if (!report.isEmpty()) {
log.info("orphan job report: count={}", report.entries().size());
for (OrphanJobEntry entry : report.entries()) {
log.info("orphan job: jobId={} taskId={} resultId={} moduleType={} status={} updatedAt={}",
entry.jobId(), entry.taskId(), entry.resultId(), entry.moduleType(),
entry.status(), entry.updatedAt());
}
}
return report;
}
}