task-101: 上传/解析/入队三道前端拦截,阻止坏数据卡死 Python 自动化
Build Backend JAR / build (push) Has been cancelled

新增 shared/dispatch-guard.ts(纯 TS 校验,可单测)与 dispatch-guard-ui.ts
(ElMessageBox 弹窗层),在数据流的三个位置设闸:

- 选文件:空选择、非 xlsx/xls/csv、空路径直接拦;重复文件弹确认
- 解析结果:taskId 非法、totalRows 为 0、整批行被丢弃、需要分组却无分组直接拦;
  部分行被丢弃弹确认后才允许推送
- 入队前:结构不符、必填字段缺失、items/groups/country_codes 为空数组、
  行数页数为 0、JSON 不安全值(NaN/Infinity/BigInt/循环引用/数组洞)

覆盖 11 个推 Python 队列的 Tab 与 dedupe/split/convert 三个纯 Java Tab。
闸位按 Tab 挑选以免留下孤儿后端状态:collect-data 校验前置到
activateCollectDataTask 之前(任务保持 PENDING 无需回滚)、product-risk 的国家
检查提到建任务之前;已建任务后才拦下的复用各 Tab 原有失败补偿路径。

两处边界:对象属性 undefined 放行(taskNo 这类可选字段是惯用写法),只拦数组
元素 undefined 与数组洞;price-track 的 asin_rows_by_country 仅在 mode=asin
时要求非空,status 模式下 loadAsinRowsForAppClient 本就返回 {}。

顺带修复 similar-asin / appearance-patent 的 selectFiles 缺少 try/catch,
上传失败会残留上一批文件与解析结果。

测试:tests/dispatch-guard.test.ts 新增 37 个 test_task_101_* 用例。
This commit is contained in:
2026-09-01 12:53:12 +08:00
parent e2607ab723
commit 759f0b15d8
18 changed files with 1603 additions and 14 deletions
@@ -151,6 +151,8 @@ import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunVo } from '@/shared/api/java-modules'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import { EXCEL_EXTENSIONS, checkSelectedFiles, guardBlocked } from '@/shared/dispatch-guard'
import { passGuard } from '@/shared/dispatch-guard-ui'
const cleanAvailableColumns = ref<string[]>([])
const cleanSelectedColumns = ref<string[]>([])
@@ -231,8 +233,11 @@ async function selectCleanFiles() {
try {
const paths = await api.select_brand_xlsx_files()
if (!paths?.length) return
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
await handleSelectedPaths(paths, `已选择 ${paths.length} 个待清洗文件`)
} catch (error) {
cleanSelectedPaths.value = []
cleanUploadedFiles.value = []
ElMessage.error(error instanceof Error ? error.message : '选择失败')
}
}
@@ -251,6 +256,7 @@ async function selectCleanFolder() {
const result = await expandBrandFolderRecursive(folder)
if (result.success && result.items?.length) {
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
cleanSelectedPaths.value = result.items.map((item) => item.relativePath)
cleanUploadedFiles.value = await uploadPathsToJava(result.items)
if (cleanUploadedFiles.value.length > 0) {
@@ -262,6 +268,8 @@ async function selectCleanFolder() {
ElMessage.warning(result.error || '该文件夹下没有 xlsx 文件')
} catch (error) {
cleanSelectedPaths.value = []
cleanUploadedFiles.value = []
ElMessage.error(error instanceof Error ? error.message : '选择失败')
}
}
@@ -296,6 +304,19 @@ async function submitCleanRun() {
cleanSummary.value = result
cleanResultItems.value = result.items || []
await loadCleanHistory()
// 全部文件都失败时不该报「完成」,让用户看清是哪一批没处理成功
if (result.total > 0 && result.successCount === 0) {
await passGuard(
guardBlocked(
'去重未成功',
`本次提交的 ${result.total} 个文件全部处理失败。\n` +
'常见原因是表头列名与所选保留列对不上,或文件内没有数据行。\n' +
'请在右侧结果列表查看每个文件的失败原因后重试。',
'dedupe.all-failed',
),
)
return
}
ElMessage.success('数据去重完成')
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '去重失败')