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:
@@ -0,0 +1,61 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { dirname, join } from 'node:path'
|
||||
|
||||
/**
|
||||
* 「行数据按需拉取」接线守卫(审查 F5 后续):
|
||||
* 5 个品牌工具页用轻量端点的结果行版本跳过重型 batch;这里守住两件最容易写错的事
|
||||
* ——①接线没被误删;②跳过分支的返回值必须符合调用方契约(曾经把 shopDataCrawl 写成裸 return,
|
||||
* 而调用方 waitForTerminal 把 falsy 当「任务已消失 → FAILED」,会误杀正在跑的任务)。
|
||||
*/
|
||||
const PAGES_DIR = join(dirname(fileURLToPath(import.meta.url)), '../src/pages/brand/components')
|
||||
|
||||
const WIRED_PAGES = [
|
||||
'BrandQueryAsinTab.vue',
|
||||
'BrandWithdrawTab.vue',
|
||||
'BrandPatrolDeleteTab.vue',
|
||||
'BrandShopDataCrawlTab.vue',
|
||||
'BrandAppearancePatentTab.vue',
|
||||
]
|
||||
|
||||
function sourceOf(page: string) {
|
||||
return readFileSync(join(PAGES_DIR, page), 'utf8')
|
||||
}
|
||||
|
||||
test('已接线的品牌工具页仍使用结果行版本跳过重型 batch', () => {
|
||||
for (const page of WIRED_PAGES) {
|
||||
const source = sourceOf(page)
|
||||
assert.ok(source.includes('createRowsVersionTracker'), `${page} 缺少版本跟踪器声明`)
|
||||
assert.ok(source.includes('selectTasksNeedingRows'), `${page} 缺少按需拉取判断`)
|
||||
assert.ok(source.includes('getModuleProgressLight'), `${page} 缺少轻量端点调用`)
|
||||
}
|
||||
})
|
||||
|
||||
test('轻量请求失败必须回退为全量拉取(不得因此少拉任务)', () => {
|
||||
// 多任务页:catch 里把 heavyIds 还原成全部任务
|
||||
for (const page of ['BrandQueryAsinTab.vue', 'BrandWithdrawTab.vue', 'BrandPatrolDeleteTab.vue']) {
|
||||
assert.match(sourceOf(page), /catch\s*\{[\s\S]{0,200}?heavyIds = ids/, `${page} 缺少轻量失败回退`)
|
||||
}
|
||||
assert.match(sourceOf('BrandAppearancePatentTab.vue'), /catch\s*\{[\s\S]{0,200}?heavyIds = taskIds/,
|
||||
'BrandAppearancePatentTab 缺少轻量失败回退')
|
||||
|
||||
// 单任务页:catch 里不能有 return(直接落到批量拉取)
|
||||
const shopDataCrawl = sourceOf('BrandShopDataCrawlTab.vue')
|
||||
const catchBlock = shopDataCrawl.match(/catch\s*\{([^}]*)\}/)?.[1] ?? ''
|
||||
assert.ok(catchBlock.length > 0, 'BrandShopDataCrawlTab 未找到 catch 分支')
|
||||
assert.ok(!catchBlock.includes('return'), 'BrandShopDataCrawlTab 轻量失败时不得提前返回,必须照旧拉批量')
|
||||
})
|
||||
|
||||
test('跳过分支的返回值符合调用方契约', () => {
|
||||
const shopDataCrawl = sourceOf('BrandShopDataCrawlTab.vue')
|
||||
// waitForTerminal: `if (!exists) return 'FAILED'` —— 任务仍在时必须返回 true
|
||||
assert.match(shopDataCrawl, /selectTasksNeedingRows\([\s\S]{0,200}?return true/,
|
||||
'BrandShopDataCrawlTab 跳过分支必须 return true(false/undefined 会被当成任务已消失)')
|
||||
|
||||
const patrolDelete = sourceOf('BrandPatrolDeleteTab.vue')
|
||||
// waitForTaskTerminal: `missingTaskIds.includes(taskId)` —— 未消失时返回空数组
|
||||
assert.match(patrolDelete, /selectTasksNeedingRows\([\s\S]{0,300}?return \[\]/,
|
||||
'BrandPatrolDeleteTab 跳过分支必须返回空 missingTaskIds 数组')
|
||||
})
|
||||
@@ -7,6 +7,10 @@ import {
|
||||
type ProgressLightModule,
|
||||
type TaskProgressLightItem,
|
||||
} from '../src/shared/api/progress-light.ts'
|
||||
import {
|
||||
getPollingProgressBatch,
|
||||
lightClaimsAllTasksMissing,
|
||||
} from '../src/shared/api/task-progress-polling.ts'
|
||||
import type { TaskProgressBatchVo } from '../src/shared/api/types/task.ts'
|
||||
|
||||
function setupWindow() {
|
||||
@@ -164,3 +168,58 @@ test('test_light_reexport', async () => {
|
||||
assert.equal(javaModules.getModuleProgressLight, direct.getModuleProgressLight, 'java-modules 应 re-export getModuleProgressLight')
|
||||
assert.equal(javaModules.TaskProgressLightItem, direct.TaskProgressLightItem, '类型应同一引用')
|
||||
})
|
||||
|
||||
test('test_light_all_missing_needs_heavy_recheck', async (t) => {
|
||||
setupWindow()
|
||||
// light 恒判 missing(2026-09-14 线上真实故障形态)→ 必须用重型端点复核,不能直接采信
|
||||
mockRequest(t, () => okResponse({ items: [], missingTaskIds: [9101] }))
|
||||
let heavyCalls = 0
|
||||
const heavy = { items: [{ taskId: 9101, status: 'RUNNING' }], missingTaskIds: [] }
|
||||
|
||||
const batch = await getPollingProgressBatch<{ items: unknown[]; missingTaskIds: number[] }>(
|
||||
'priceTrack',
|
||||
[9101],
|
||||
{ fallback: () => { heavyCalls += 1; return Promise.resolve(heavy) } },
|
||||
)
|
||||
|
||||
assert.equal(heavyCalls, 1, '整体性 missing 结论必须复核')
|
||||
assert.deepEqual(batch, heavy, '以重型端点结果为准')
|
||||
})
|
||||
|
||||
test('test_light_all_missing_confirmed_by_heavy', async (t) => {
|
||||
setupWindow()
|
||||
mockRequest(t, () => okResponse({ items: [], missingTaskIds: [9102] }))
|
||||
const heavy = { items: [], missingTaskIds: [9102] }
|
||||
|
||||
const batch = await getPollingProgressBatch<{ items: unknown[]; missingTaskIds: number[] }>(
|
||||
'withdraw',
|
||||
[9102],
|
||||
{ fallback: () => Promise.resolve(heavy) },
|
||||
)
|
||||
|
||||
assert.deepEqual(batch, heavy, '重型端点也判不存在时结论一致')
|
||||
})
|
||||
|
||||
test('test_light_with_items_skips_recheck', async (t) => {
|
||||
setupWindow()
|
||||
mockRequest(t, () => okResponse({ items: [{ taskId: 9103, status: 'RUNNING' }], missingTaskIds: [] }))
|
||||
let heavyCalls = 0
|
||||
|
||||
const batch = await getPollingProgressBatch<{ items: unknown[]; missingTaskIds: number[] }>(
|
||||
'shopMatch',
|
||||
[9103],
|
||||
{ fallback: () => { heavyCalls += 1; return Promise.resolve({ items: [], missingTaskIds: [] }) } },
|
||||
)
|
||||
|
||||
assert.equal(heavyCalls, 0, '有条目时不做复核,保持省流')
|
||||
assert.equal((batch.items || []).length, 1)
|
||||
})
|
||||
|
||||
test('test_light_claims_all_missing_semantics', () => {
|
||||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [1] }, [1]), true)
|
||||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [1] }, [1, 2]), false, '部分缺失不算整体性结论')
|
||||
assert.equal(lightClaimsAllTasksMissing({ items: [{ taskId: 1 }], missingTaskIds: [] }, [1]), false, '有条目即不成立')
|
||||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [] }, [1]), false)
|
||||
assert.equal(lightClaimsAllTasksMissing(null, [1]), false)
|
||||
assert.equal(lightClaimsAllTasksMissing({ items: [], missingTaskIds: [1] }, []), false)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,89 @@
|
||||
import { test } from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { createRowsVersionTracker } from '../src/shared/api/task-progress-polling.ts'
|
||||
|
||||
test('首次见到的任务需要拉取行数据', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
|
||||
const need = tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '100' }], [1])
|
||||
|
||||
assert.deepEqual(need, [1])
|
||||
})
|
||||
|
||||
test('版本未变时不再拉取,版本变化后重新拉取', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '100' }], [1])
|
||||
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '100' }], [1]), [], '版本相同跳过')
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '101' }], [1]), [1], '版本变化要拉')
|
||||
})
|
||||
|
||||
test('拿不到版本(老后端/字段缺失)时照旧拉取', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 1 }], [1]), [1])
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: null }], [1]), [1])
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([], [7]), [7], '响应里没有该任务也拉')
|
||||
})
|
||||
|
||||
test('多任务各自独立判断', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
tracker.selectTasksNeedingRows(
|
||||
[{ taskId: 1, rowsVersion: 'a' }, { taskId: 2, rowsVersion: 'b' }],
|
||||
[1, 2],
|
||||
)
|
||||
|
||||
const need = tracker.selectTasksNeedingRows(
|
||||
[{ taskId: 1, rowsVersion: 'a' }, { taskId: 2, rowsVersion: 'b2' }],
|
||||
[1, 2],
|
||||
)
|
||||
|
||||
assert.deepEqual(need, [2])
|
||||
})
|
||||
|
||||
test('forget 后该任务会被重新拉取(任务重跑场景)', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
tracker.selectTasksNeedingRows([{ taskId: 5, rowsVersion: 'v1' }], [5])
|
||||
|
||||
tracker.forget(5)
|
||||
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 5, rowsVersion: 'v1' }], [5]), [5])
|
||||
})
|
||||
|
||||
test('任务状态推进(行没变)也要重新拉取', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '100', status: 'RUNNING' }], [1])
|
||||
|
||||
assert.deepEqual(
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '100', status: 'RUNNING' }], [1]),
|
||||
[],
|
||||
'行版本与状态都没变:跳过',
|
||||
)
|
||||
assert.deepEqual(
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, rowsVersion: '100', status: 'SUCCESS' }], [1]),
|
||||
[1],
|
||||
'行版本没变但任务已成功:必须拉取,否则界面卡在运行中',
|
||||
)
|
||||
})
|
||||
|
||||
test('结果文件状态变化也要重新拉取', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, status: 'RUNNING', fileStatus: 'RUNNING', fileReady: false }], [1])
|
||||
|
||||
assert.deepEqual(
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, status: 'RUNNING', fileStatus: 'RUNNING', fileReady: false }], [1]),
|
||||
[],
|
||||
)
|
||||
assert.deepEqual(
|
||||
tracker.selectTasksNeedingRows([{ taskId: 1, status: 'RUNNING', fileStatus: 'SUCCESS', fileReady: true }], [1]),
|
||||
[1],
|
||||
'文件就绪变化要能触发拉取(下载按钮依赖它)',
|
||||
)
|
||||
})
|
||||
|
||||
test('四项信号全空视为拿不到信号:每轮都拉取', () => {
|
||||
const tracker = createRowsVersionTracker()
|
||||
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 1 }], [1]), [1])
|
||||
assert.deepEqual(tracker.selectTasksNeedingRows([{ taskId: 1 }], [1]), [1], '老后端不给信号时保持旧行为')
|
||||
})
|
||||
Reference in New Issue
Block a user