perf(F5+): 行数据按需拉取(结果行版本信号)+ 修复 progress/light 恒判 missing

行数据按需拉取(审查 F5 后续):
- V125 给 biz_file_result 补 updated_at(DEFAULT/ON UPDATE 由数据库维护,
  实体标注 insertStrategy/updateStrategy=NEVER —— 否则 selectById→updateById 的
  写回会把旧值写回去、ON UPDATE 不触发,版本信号静默冻结)
- 装配器回传 rowsVersion=「最后变更时间毫秒#行数」,5 个品牌工具页版本未变即跳过
  带行明细的重型 batch;前端变更信号为 rowsVersion + status/fileStatus/fileReady 复合
  (任务收尾常见「行早写完、之后才置成功」,只看行版本会把界面卡在旧状态)

修复线上缺陷(同一功能验证时暴露):
- TaskProgressLightAssembler 列裁剪漏选 module_type 却用它做模块过滤 →
  getModuleType() 恒为 null → light 恒把任务判成 missing;第七批把 light 接进
  跟价/定时匹配/商品风险的轮询后,消费方会把运行中任务判为 FAILED
- 补选中列 + 守卫用例 taskQueryMustSelectModuleType(已反向验证:去掉修复即红)
- 前端 lightClaimsAllTasksMissing:整体性 missing 结论用重型端点复核后再采信

契约与文档:light 白名单补 rowsVersion(Java 契约测试 / spec 06 §2 / 12 个端点描述)
测试:mvn test 2901 全绿;前端 npm test 765 全绿
This commit is contained in:
2026-09-14 12:08:25 +08:00
parent 9166656673
commit 5ea52e5291
32 changed files with 641 additions and 36 deletions
@@ -113,6 +113,8 @@ import { passGuard } from '@/shared/dispatch-guard-ui'
import { formatDateTime } from '@/shared/utils/datetime'
import { uploadPathsToJava } from '@/shared/utils/upload-to-java'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { getModuleProgressLight } from '@/shared/api/progress-light.ts'
import { createRowsVersionTracker } from '@/shared/api/task-progress-polling.ts'
const selectedFileNames = ref<string[]>([])
const uploadedFiles = ref<UploadFileVo[]>([])
@@ -545,6 +547,9 @@ function scheduleNextPoll(immediate = false) {
else pollTimer.value = timers.setTimeout('task-poll', run, getTaskPollIntervalMs())
}
/** 结果行版本跟踪(审查 F5 后续):版本未变的任务跳过行明细拉取 */
const rowsVersionTracker = createRowsVersionTracker()
async function refreshTaskProgress() {
if (pollingInFlight.value || (!pollingTaskIds.value.length && !pendingFileTaskIds.value.length)) {
if (!pollingTaskIds.value.length && !pendingFileTaskIds.value.length) stopPolling()
@@ -556,7 +561,23 @@ async function refreshTaskProgress() {
let shouldRefreshHistory = false
const taskIds = Array.from(new Set([...pollingTaskIds.value, ...pendingFileTaskIds.value]))
if (taskIds.length) {
const batch = await getAppearancePatentTaskProgressBatch(taskIds, { force: pendingFileTaskIds.value.length > 0 })
// 行数据按需拉取(审查 F5 后续):等结果文件(pending)时必须强刷;其余情况版本未变的任务跳过
let heavyIds = taskIds
const mustForce = pendingFileTaskIds.value.length > 0
if (!mustForce) {
try {
const light = await getModuleProgressLight('appearancePatent', taskIds)
heavyIds = rowsVersionTracker.selectTasksNeedingRows(light.items || [], taskIds)
if (!heavyIds.length && !(light.missingTaskIds || []).length) {
// 行数据与状态都没变:本轮不拉行明细,直接结束
return
}
} catch {
// 轻量端点不可用(老后端/网络)→ 照旧全量拉取,行为与原来一致
heavyIds = taskIds
}
}
const batch = await getAppearancePatentTaskProgressBatch(heavyIds, { force: mustForce })
for (const detail of batch.items || []) {
const task = detail.task
if (!task?.id) continue
@@ -285,6 +285,8 @@ import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
import { getModuleProgressLight } from '@/shared/api/progress-light.ts'
import { createRowsVersionTracker } from '@/shared/api/task-progress-polling.ts'
const MAX_TRANSIENT_ERRORS = 30;
/** 任务终态后等待结果文件(Java 侧异步生成)的最大轮次,12 × 10s ≈ 2 分钟 */
@@ -797,6 +799,9 @@ function upsertHistoryItems(incoming: PatrolDeleteHistoryItem[]) {
mergeHistoryProgressItems(incoming);
}
/** 结果行版本跟踪:版本未变的任务不再拉取行明细(F5 后续) */
const rowsVersionTracker = createRowsVersionTracker()
async function refreshActiveTaskProgress(taskIds?: number[]) {
const ids = Array.from(
new Set(
@@ -809,9 +814,27 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
if (!ids.length) {
return [];
}
const batch = await getPatrolDeleteTaskProgressBatch(ids);
// 行数据按需拉取(审查 F5 后续):先问轻量端点拿"结果行版本",版本未变的任务跳过重型 batch
let heavyIds = ids;
let missingIds: number[] = [];
try {
const light = await getModuleProgressLight('patrolDelete', ids);
heavyIds = rowsVersionTracker.selectTasksNeedingRows(light.items || [], ids);
missingIds = light.missingTaskIds || [];
if (!heavyIds.length) {
if (missingIds.length) {
await loadHistory();
}
return [];
}
} catch {
// 轻量端点不可用(老后端/网络)→ 照旧全量拉取,行为与原来一致
heavyIds = ids;
}
const batch = await getPatrolDeleteTaskProgressBatch(heavyIds);
missingIds = Array.from(new Set([...missingIds, ...(batch.missingTaskIds || [])]));
mergeHistoryProgressItems(batch.items || []);
if ((batch.missingTaskIds || []).length) {
if (missingIds.length) {
await loadHistory();
}
return batch.missingTaskIds || [];
@@ -227,6 +227,8 @@ import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError, parseTaskStartTimes, removeTaskStartTime, upsertTaskStartTime } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
import { getModuleProgressLight } from '@/shared/api/progress-light.ts'
import { createRowsVersionTracker } from '@/shared/api/task-progress-polling.ts'
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
@@ -705,6 +707,9 @@ function mergeHistoryProgressItems(incoming: QueryAsinHistoryItem[]) {
historyItems.value = merged;
}
/** 结果行版本跟踪:版本未变的任务不再拉取行明细(F5 后续) */
const rowsVersionTracker = createRowsVersionTracker()
async function refreshActiveTaskProgress(taskIds?: number[]) {
const ids = Array.from(
new Set(
@@ -717,9 +722,28 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
if (!ids.length) {
return;
}
const batch = await getQueryAsinTaskProgressBatch(ids);
// 行数据按需拉取(审查 F5 后续):先问轻量端点拿"结果行版本",版本未变的任务跳过重型 batch
let heavyIds = ids;
let missingIds: number[] = [];
try {
const light = await getModuleProgressLight('queryAsin', ids);
heavyIds = rowsVersionTracker.selectTasksNeedingRows(light.items || [], ids);
missingIds = light.missingTaskIds || [];
if (!heavyIds.length) {
if (missingIds.length) {
await loadHistory();
reconcileActiveQueueTaskWithHistory();
}
return;
}
} catch {
// 轻量端点不可用(老后端/网络)→ 照旧全量拉取,行为与原来一致
heavyIds = ids;
}
const batch = await getQueryAsinTaskProgressBatch(heavyIds);
missingIds = Array.from(new Set([...missingIds, ...(batch.missingTaskIds || [])]));
mergeHistoryProgressItems(batch.items || []);
if ((batch.missingTaskIds || []).length) {
if (missingIds.length) {
await loadHistory();
reconcileActiveQueueTaskWithHistory();
}
@@ -162,6 +162,8 @@ import {
} from '@/shared/api/java-modules'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
import { getModuleProgressLight } from '@/shared/api/progress-light.ts'
import { createRowsVersionTracker } from '@/shared/api/task-progress-polling.ts'
const COUNTRY_OPTIONS = [
{ code: 'UK', label: '英国' },
@@ -399,7 +401,21 @@ function mergeProgress(detail: ShopDataCrawlTaskDetailVo) {
function isTaskDetail(row: ShopDataCrawlTaskDetailVo | ShopDataCrawlHistoryItem): row is ShopDataCrawlTaskDetailVo {
return 'task' in row || 'items' in row
}
/** 结果行版本跟踪(F5 后续):版本未变的任务跳过行明细拉取 */
const rowsVersionTracker = createRowsVersionTracker()
async function refreshTaskProgress(taskId: number) {
// 行数据按需拉取(审查 F5 后续):该任务结果行版本未变时跳过重型 batch;任务已被删除则照旧走原路径
try {
const light = await getModuleProgressLight('shopDataCrawl', [taskId])
const missing = light.missingTaskIds || []
if (!missing.length && !rowsVersionTracker.selectTasksNeedingRows(light.items || [], [taskId]).length) {
// 返回 true = 任务仍存在(调用方以此为「任务消失→FAILED」判据,不能返回 undefined
return true
}
} catch {
/* 轻量端点不可用 → 照旧全量拉取 */
}
const batch = await getShopDataCrawlTaskProgressBatch([taskId])
for (const row of batch.items || []) {
if (isTaskDetail(row)) {
@@ -267,6 +267,8 @@ import { useHistoryPolling } from '@/shared/composables/useHistoryPolling'
import { runBatchDelete } from '@/shared/utils/batch-delete'
import { isRecordMissingError, parseTaskStartTimes, removeTaskStartTime, upsertTaskStartTime } from '@/shared/utils/task-queue-state.ts'
import { useTablePaging } from '@/shared/composables/useTablePaging.ts'
import { getModuleProgressLight } from '@/shared/api/progress-light.ts'
import { createRowsVersionTracker } from '@/shared/api/task-progress-polling.ts'
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
@@ -829,6 +831,9 @@ function mergeHistoryProgressItems(incoming: WithdrawHistoryItem[]) {
historyItems.value = merged;
}
/** 结果行版本跟踪:版本未变的任务不再拉取行明细(F5 后续) */
const rowsVersionTracker = createRowsVersionTracker()
async function refreshActiveTaskProgress(taskIds?: number[]) {
const ids = Array.from(
new Set(
@@ -841,9 +846,28 @@ async function refreshActiveTaskProgress(taskIds?: number[]) {
if (!ids.length) {
return;
}
const batch = await getWithdrawTaskProgressBatch(ids);
// 行数据按需拉取(审查 F5 后续):先问轻量端点拿"结果行版本",版本未变的任务跳过重型 batch
let heavyIds = ids;
let missingIds: number[] = [];
try {
const light = await getModuleProgressLight('withdraw', ids);
heavyIds = rowsVersionTracker.selectTasksNeedingRows(light.items || [], ids);
missingIds = light.missingTaskIds || [];
if (!heavyIds.length) {
if (missingIds.length) {
await loadHistory();
reconcileActiveQueueTaskWithHistory();
}
return;
}
} catch {
// 轻量端点不可用(老后端/网络)→ 照旧全量拉取,行为与原来一致
heavyIds = ids;
}
const batch = await getWithdrawTaskProgressBatch(heavyIds);
missingIds = Array.from(new Set([...missingIds, ...(batch.missingTaskIds || [])]));
mergeHistoryProgressItems(batch.items || []);
if ((batch.missingTaskIds || []).length) {
if (missingIds.length) {
await loadHistory();
reconcileActiveQueueTaskWithHistory();
}