task-200: 巡检报表任务落地(InspectionSqlReportTask 跑 6 张只读报表 SQL:默认 disabled、分布式锁、单条失败不阻断、只 query)+ 8 条测试

This commit is contained in:
2026-09-05 00:08:44 +08:00
parent d6b223ba25
commit 1b27a3f1ca
2 changed files with 289 additions and 0 deletions
@@ -0,0 +1,108 @@
package com.nanri.aiimage.modules.task.service;
import com.nanri.aiimage.common.service.DistributedJobLockService;
import com.nanri.aiimage.config.InspectionProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.List;
import java.util.Map;
/**
* 巡检只读 SQL 报表任务(task-200)。
*
* 与 InspectionScheduler 同开关/调度风格:aiimage.inspection.enabled 默认 false;启用后按
* aiimage.inspection.cron 执行 6 张只读报表 SQLresources/inspection/*.sqltask-194..199),
* 逐条输出行数与耗时;单条失败记录日志不阻断其余;分布式锁防双实例重复;只 query 不改数据。
*/
@Slf4j
@Service
public class InspectionSqlReportTask {
private static final Duration REPORT_LOCK_TTL = Duration.ofMinutes(10);
/** 与巡检 SQL 文件一一对应(顺序执行)。 */
static final List<String> REPORT_FILES = List.of(
"01_orphan_job.sql", "02_orphan_result.sql", "03_task_missing_result.sql",
"04_result_missing_file.sql", "05_terminal_task_active_job.sql",
"06_over_retention_active_job.sql");
private final InspectionProperties inspectionProperties;
private final DistributedJobLockService distributedJobLockService;
private final ObjectProvider<JdbcTemplate> jdbcTemplateProvider;
/** 测试/无锁构造。 */
public InspectionSqlReportTask(InspectionProperties inspectionProperties,
ObjectProvider<JdbcTemplate> jdbcTemplateProvider) {
this(inspectionProperties, null, jdbcTemplateProvider);
}
@Autowired
public InspectionSqlReportTask(InspectionProperties inspectionProperties,
DistributedJobLockService distributedJobLockService,
ObjectProvider<JdbcTemplate> jdbcTemplateProvider) {
this.inspectionProperties = inspectionProperties;
this.distributedJobLockService = distributedJobLockService;
this.jdbcTemplateProvider = jdbcTemplateProvider;
}
@Scheduled(cron = "${aiimage.inspection.cron:0 0 3 * * *}")
public void runIfEnabled() {
if (!inspectionProperties.isEnabled()) {
return;
}
if (distributedJobLockService == null) {
return;
}
DistributedJobLockService.LockHandle lockHandle =
distributedJobLockService.tryLock("inspection-sql-report", REPORT_LOCK_TTL);
if (lockHandle == null) {
log.info("[inspection-sql] skip because another instance holds the report lock");
return;
}
try (lockHandle) {
runAll();
}
}
/** 执行全部只读报表 SQL;返回成功执行的报表数(disabled/无 JDBC 返回 0,失败单条不阻断)。 */
public int runAll() {
if (!inspectionProperties.isEnabled()) {
return 0;
}
JdbcTemplate jdbcTemplate = jdbcTemplateProvider.getIfAvailable();
if (jdbcTemplate == null) {
log.warn("[inspection-sql] enabled but no JdbcTemplate bean, skip reports");
return 0;
}
int ok = 0;
for (String file : REPORT_FILES) {
long startedAt = System.nanoTime();
try {
String sql = loadSql(file);
List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql);
long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000;
log.info("[inspection-sql] report {} rows={} elapsedMs={}",
file, rows.size(), elapsedMs);
ok++;
} catch (Exception ex) {
long elapsedMs = (System.nanoTime() - startedAt) / 1_000_000;
log.error("[inspection-sql] report {} failed elapsedMs={} cause={}",
file, elapsedMs, ex.toString());
}
}
return ok;
}
private String loadSql(String file) throws java.io.IOException {
ClassPathResource resource = new ClassPathResource("inspection/" + file);
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
}