diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClient.java b/backend-java/src/main/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClient.java index 0196a90e..2fe49281 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClient.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClient.java @@ -44,23 +44,27 @@ public class MaixiangConsoleClient { return hasText(properties.getPriceTrackApiUrl()) && hasText(properties.getMaixiangConsoleToken()); } - /** 批量任务列表(all_task)。createdBefore 非空时只取创建时间不晚于该时刻的任务(SQL 字符串比较)。 */ - public BatchTaskPage batchTasks(int status, LocalDateTime createdBefore, int pageSize) { + /** + * 批量任务列表(all_task)。updateTimeBefore 非空时只取「最后更新」不晚于该时刻的任务 + * (SQL 字符串比较)。用 update_time 过滤而不是 create_time:desc(id) 分页下按创建时间过滤 + * 只能看到最新创建的一批,积压深处的老任务(真正停滞的)永远看不到 —— 2026-09-15 滞留漏报的根因。 + */ + public BatchTaskPage batchTasks(int status, LocalDateTime updateTimeBefore, int pageSize) { String url = buildUrl("/api/console/batch/tasks", "status=" + status, "page=1", "page_size=" + pageSize, - createdBefore == null ? null : "end_time=" + TIME_FORMAT.format(createdBefore)); + updateTimeBefore == null ? null : "update_time_end=" + TIME_FORMAT.format(updateTimeBefore)); return parseBatchTaskPage(fetch(url, "批量任务列表 status=" + status)); } - /** 单任务列表(task_record)。 */ - public SingleTaskPage singleTasks(int status, LocalDateTime createdBefore, int pageSize) { + /** 单任务列表(task_record)。updateTimeBefore 语义同 {@link #batchTasks(int, LocalDateTime, int)}。 */ + public SingleTaskPage singleTasks(int status, LocalDateTime updateTimeBefore, int pageSize) { String url = buildUrl("/api/console/tasks", "status=" + status, "page=1", "page_size=" + pageSize, - createdBefore == null ? null : "end_time=" + TIME_FORMAT.format(createdBefore)); + updateTimeBefore == null ? null : "update_time_end=" + TIME_FORMAT.format(updateTimeBefore)); return parseSingleTaskPage(fetch(url, "单任务列表 status=" + status)); } diff --git a/backend-java/src/main/java/com/nanri/aiimage/modules/notification/service/MaixiangAnomalyScanner.java b/backend-java/src/main/java/com/nanri/aiimage/modules/notification/service/MaixiangAnomalyScanner.java index 655e3647..33c19ab1 100644 --- a/backend-java/src/main/java/com/nanri/aiimage/modules/notification/service/MaixiangAnomalyScanner.java +++ b/backend-java/src/main/java/com/nanri/aiimage/modules/notification/service/MaixiangAnomalyScanner.java @@ -104,7 +104,7 @@ public class MaixiangAnomalyScanner { } } - /** 批量任务停滞:创建早于阈值、仍处 status=0/1 且 update_time 不再推进。 */ + /** 批量任务停滞:最后更新早于阈值、仍处 status=0/1(服务端按 update_time 过滤后取回)。 */ void checkStuckBatchTasks(NotificationDispatchService.AdminAudience audience, LocalDateTime now) { int thresholdMinutes = Math.max(1, properties.getMaixiangStuckMinutes()); LocalDateTime cutoff = now.minusMinutes(thresholdMinutes); @@ -112,7 +112,7 @@ public class MaixiangAnomalyScanner { for (int status : new int[]{0, 1}) { candidates.addAll(consoleClient.batchTasks(status, cutoff, PAGE_SIZE).items()); } - // 创建时间早于 cutoff 但仍在正常推进的大任务(update_time 晚于 cutoff)不算停滞 + // 服务端已按 update_time ≤ cutoff 过滤;这里再兜一层,防止接口参数被忽略时误报正常推进的大任务 List stuck = new ArrayList<>(); for (BatchTaskItem task : candidates) { if (task.updateTime() != null && !task.updateTime().isAfter(cutoff)) { @@ -135,7 +135,7 @@ public class MaixiangAnomalyScanner { push(audience, "麦象批量任务停滞", content, "maixiang_stuck_batch:" + now.format(DAY_FORMAT)); } - /** 单任务滞留:创建早于阈值、仍处 status=0/1(跟价等单任务应秒级完成)。 */ + /** 单任务滞留:最后更新早于阈值、仍处 status=0/1(跟价等单任务应秒级完成)。 */ void checkStuckSingleTasks(NotificationDispatchService.AdminAudience audience, LocalDateTime now) { int thresholdMinutes = Math.max(1, properties.getMaixiangSingleStuckMinutes()); LocalDateTime cutoff = now.minusMinutes(thresholdMinutes); diff --git a/backend-java/src/test/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClientTest.java b/backend-java/src/test/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClientTest.java index 98a27651..52c6f481 100644 --- a/backend-java/src/test/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClientTest.java +++ b/backend-java/src/test/java/com/nanri/aiimage/modules/notification/client/MaixiangConsoleClientTest.java @@ -132,7 +132,7 @@ class MaixiangConsoleClientTest { @Test void buildUrlEncodesTimeParameterExactlyOnce() { - // 回归:end_time 里的空格必须编码成 %20(单次),否则提交时会被二次编码成 %2520, + // 回归:update_time_end 里的空格必须编码成 %20(单次),否则提交时会被二次编码成 %2520, // 服务端拿到字面 "%20" 直接报 Incorrect DATETIME value(联调时实际踩到过) NotificationProperties properties = new NotificationProperties(); properties.setPriceTrackApiUrl("http://192.168.0.170:18960"); @@ -140,13 +140,13 @@ class MaixiangConsoleClientTest { MaixiangConsoleClient underTest = new MaixiangConsoleClient(properties, new ObjectMapper()); String url = underTest.buildUrl("/api/console/tasks", - "status=0", "page=1", "page_size=100", "end_time=2026-09-15 10:58:39"); + "status=0", "page=1", "page_size=100", "update_time_end=2026-09-15 10:58:39"); assertThat(url).startsWith("http://192.168.0.170:18960/api/console/tasks?"); - assertThat(url).contains("end_time=2026-09-15%2010:58:39"); + assertThat(url).contains("update_time_end=2026-09-15%2010:58:39"); assertThat(url).doesNotContain("%2520"); assertThat(url).contains("token=tok%20en+%E7%89%B9%E6%AE%8A"); // 组装结果必须是可直接提交的合法 URI - assertThat(java.net.URI.create(url).getRawQuery()).contains("end_time=2026-09-15%2010:58:39"); + assertThat(java.net.URI.create(url).getRawQuery()).contains("update_time_end=2026-09-15%2010:58:39"); } }