新增 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:
@@ -272,6 +272,12 @@ import { getPywebviewApi, type PywebviewApi, type UploadedJavaFile } from '@/sha
|
||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import {
|
||||
EXCEL_CSV_EXTENSIONS,
|
||||
checkQueuePayload,
|
||||
checkSelectedFiles,
|
||||
} from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
|
||||
import {
|
||||
addPriceTrackCandidate,
|
||||
@@ -527,6 +533,7 @@ async function selectAsinFile() {
|
||||
multiple: true,
|
||||
})
|
||||
if (result?.paths?.length) {
|
||||
if (!(await passGuard(checkSelectedFiles(result.paths, { allowedExtensions: EXCEL_CSV_EXTENSIONS })))) return
|
||||
asinFiles.value = result.paths
|
||||
asinUploadedFiles.value = await uploadAsinPathsToJava(result.paths)
|
||||
ElMessage.success(`已选择 ${result.paths.length} 个ASIN文件`)
|
||||
@@ -536,6 +543,7 @@ async function selectAsinFile() {
|
||||
if (api?.select_brand_xlsx_files) {
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_CSV_EXTENSIONS })))) return
|
||||
asinFiles.value = paths
|
||||
asinUploadedFiles.value = await uploadAsinPathsToJava(paths)
|
||||
ElMessage.success(`已选择 ${paths.length} 个ASIN文件`)
|
||||
@@ -558,10 +566,14 @@ async function selectAsinFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
|
||||
return
|
||||
}
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_CSV_EXTENSIONS })))) return
|
||||
asinFiles.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
asinUploadedFiles.value = await uploadAsinPathsToJava(result.items)
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个ASIN文件`)
|
||||
} catch (error) {
|
||||
// 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪
|
||||
asinFiles.value = []
|
||||
asinUploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -1072,6 +1084,22 @@ async function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrack
|
||||
|
||||
async function enqueueCreatedTask(api: PywebviewApi, taskId: number, queuePayload: unknown) {
|
||||
if (!api.enqueue_json) throw new Error('当前环境未启用 pywebview enqueue_json')
|
||||
// 三条推送路径(手动/串行队列/循环任务)都走这里,闸设在这一处即可全覆盖。
|
||||
// 空 country_codes 会让 Python 端没有可遍历的站点,任务永远停在 RUNNING。
|
||||
// asin_rows_by_country 只在 asin 模式下要求非空——status 模式下
|
||||
// loadAsinRowsForAppClient 本就返回 {},那是正常的。
|
||||
const mode = (queuePayload as { data?: { mode?: unknown } } | null)?.data?.mode
|
||||
const guard = checkQueuePayload(queuePayload, {
|
||||
expectedType: 'price-track-run',
|
||||
requiredDataKeys: ['task_id', 'shop_name'],
|
||||
nonEmptyArrayKeys: ['country_codes'],
|
||||
nonEmptyObjectKeys: mode === 'asin' ? ['asin_rows_by_country'] : [],
|
||||
})
|
||||
if (!(await passGuard(guard))) {
|
||||
const reason = `任务 ${taskId} 数据校验未通过,已阻止推送到 Python 队列`
|
||||
await compensateDispatchFailure(taskId, reason)
|
||||
throw new Error(reason)
|
||||
}
|
||||
let pushResult: Awaited<ReturnType<NonNullable<PywebviewApi['enqueue_json']>>>
|
||||
try {
|
||||
pushResult = await api.enqueue_json(queuePayload)
|
||||
|
||||
Reference in New Issue
Block a user