新增 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:
@@ -164,6 +164,13 @@ import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { getStoredApiSecret } from '@/shared/utils/api-secret-store'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import {
|
||||
EXCEL_EXTENSIONS,
|
||||
checkParseResult,
|
||||
checkQueuePayload,
|
||||
checkSelectedFiles,
|
||||
} from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
|
||||
const selectedFileNames = ref<string[]>([])
|
||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||
@@ -363,12 +370,23 @@ async function selectFiles() {
|
||||
}
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
const files = await uploadAppearancePathsToJava(paths)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
try {
|
||||
const files = await uploadAppearancePathsToJava(paths)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
} catch (error) {
|
||||
// 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪
|
||||
uploadedFiles.value = []
|
||||
selectedFileNames.value = []
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
ElMessage.error(error instanceof Error ? error.message : '文件上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFolder() {
|
||||
@@ -385,6 +403,7 @@ async function selectFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
|
||||
return
|
||||
}
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
const files = await uploadAppearancePathsToJava(result.items)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
@@ -393,6 +412,9 @@ async function selectFolder() {
|
||||
queuePayloadText.value = ''
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||
} catch (error) {
|
||||
uploadedFiles.value = []
|
||||
selectedFileNames.value = []
|
||||
parseResult.value = null
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -414,6 +436,17 @@ async function parseFiles() {
|
||||
relativePath: f.relativePath,
|
||||
}))
|
||||
const res = await parseAppearancePatent(files, effectiveAiPrompt(), effectiveLlmApiKey(), effectivePatentToken())
|
||||
// 本模块按主 ID 分组执行,没有分组就没有可跑的批次,一并拦下
|
||||
const guard = checkParseResult(res, {
|
||||
requireGroups: true,
|
||||
requiredColumnsHint: 'id / ASIN / 国家',
|
||||
})
|
||||
if (!(await passGuard(guard))) {
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
return
|
||||
}
|
||||
parseResult.value = res
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
@@ -479,6 +512,13 @@ async function pushToPythonQueue() {
|
||||
},
|
||||
}
|
||||
queuePayloadText.value = `任务 ${taskId} 已准备推送,共 ${groups.length} 组`
|
||||
// 后端返回的 groups 为空时推送出去,Python 端没有可执行批次会一直停在执行中
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: 'appearance-patent-run',
|
||||
requiredDataKeys: ['taskId', 'api_key'],
|
||||
nonEmptyArrayKeys: ['groups'],
|
||||
})
|
||||
if (!(await passGuard(guard))) return
|
||||
await activateAppearancePatentTask(taskId)
|
||||
const result = await api.enqueue_json(payload)
|
||||
if (!result?.success) {
|
||||
|
||||
@@ -219,6 +219,13 @@ import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import { useTaskProgressLoop } from '@/shared/composables/useTaskProgressLoop'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import {
|
||||
EXCEL_EXTENSIONS,
|
||||
checkParseResult,
|
||||
checkQueuePayload,
|
||||
checkSelectedFiles,
|
||||
} from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import {
|
||||
parseCollectData,
|
||||
activateCollectDataTask,
|
||||
@@ -396,12 +403,18 @@ async function selectFiles() {
|
||||
try {
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
const files = await uploadPathsToJava(paths)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||
lastTaskId.value = null
|
||||
lastParseVo.value = null
|
||||
} catch (error) {
|
||||
// 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪
|
||||
uploadedFiles.value = []
|
||||
selectedFileNames.value = []
|
||||
lastTaskId.value = null
|
||||
lastParseVo.value = null
|
||||
ElMessage.error(error instanceof Error ? error.message : '文件选择失败')
|
||||
}
|
||||
}
|
||||
@@ -420,6 +433,7 @@ async function selectFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
|
||||
return
|
||||
}
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
const files = await uploadPathsToJava(result.items)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
@@ -427,6 +441,10 @@ async function selectFolder() {
|
||||
lastParseVo.value = null
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||
} catch (error) {
|
||||
uploadedFiles.value = []
|
||||
selectedFileNames.value = []
|
||||
lastTaskId.value = null
|
||||
lastParseVo.value = null
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -475,6 +493,14 @@ async function submitCollect() {
|
||||
})
|
||||
lastParseVo.value = vo
|
||||
lastTaskId.value = vo.taskId ?? null
|
||||
// 0 行落库的任务推给 Python 后拉不到任何明细,会一直停在执行中
|
||||
const guard = checkParseResult(vo, { requiredColumnsHint: 'ASIN / 国家' })
|
||||
if (!(await passGuard(guard))) {
|
||||
lastParseVo.value = null
|
||||
lastTaskId.value = null
|
||||
await Promise.all([loadDashboard(), loadHistory()])
|
||||
return
|
||||
}
|
||||
ElMessage.success(`提交成功:任务 ${vo.taskNo || vo.taskId},落库 ${vo.acceptedRows ?? 0} 行`)
|
||||
await Promise.all([loadDashboard(), loadHistory()])
|
||||
} catch (error) {
|
||||
@@ -497,8 +523,6 @@ async function pushToPythonQueue() {
|
||||
pushing.value = true
|
||||
let activated = false
|
||||
try {
|
||||
await activateCollectDataTask(lastTaskId.value)
|
||||
activated = true
|
||||
const payload = {
|
||||
type: 'collect-data-run',
|
||||
ts: Date.now(),
|
||||
@@ -511,6 +535,15 @@ async function pushToPythonQueue() {
|
||||
filters: buildFiltersPayload(),
|
||||
},
|
||||
}
|
||||
// 先校验再激活:拦下来时任务还是 PENDING,不需要回滚成 FAILED
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: 'collect-data-run',
|
||||
requiredDataKeys: ['taskId', 'pageSize'],
|
||||
nonEmptyObjectKeys: ['filters'],
|
||||
})
|
||||
if (!(await passGuard(guard))) return
|
||||
await activateCollectDataTask(lastTaskId.value)
|
||||
activated = true
|
||||
const result = await api.enqueue_json(payload)
|
||||
if (!result?.success) {
|
||||
throw new Error(result?.error || '入队失败')
|
||||
|
||||
@@ -190,6 +190,8 @@ import {
|
||||
} 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 convertSelectedPaths = ref<string[]>([])
|
||||
const convertArchiveName = ref('')
|
||||
@@ -272,8 +274,11 @@ async function selectConvertFiles() {
|
||||
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) {
|
||||
convertSelectedPaths.value = []
|
||||
convertUploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -292,6 +297,7 @@ async function selectConvertFolder() {
|
||||
|
||||
const result = await expandBrandFolderRecursive(folder)
|
||||
if (result.success && result.items?.length) {
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
convertSelectedPaths.value = result.items.map((item) => item.relativePath)
|
||||
convertUploadedFiles.value = await uploadPathsToJava(result.items)
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 xlsx 文件`)
|
||||
@@ -300,6 +306,8 @@ async function selectConvertFolder() {
|
||||
|
||||
ElMessage.warning(result.error || '该文件夹下没有 xlsx 文件')
|
||||
} catch (error) {
|
||||
convertSelectedPaths.value = []
|
||||
convertUploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -317,9 +325,25 @@ async function handleTemplateFileChange(event: Event) {
|
||||
return
|
||||
}
|
||||
|
||||
// 浏览器侧直接读内容的唯一入口:扩展名与内容都在这里拦,空模板传上去后
|
||||
// 转换阶段每个文件都会失败,不如在上传时就说清楚。
|
||||
if (!(await passGuard(checkSelectedFiles([file.name], { allowedExtensions: ['.txt'] })))) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
templateUploading.value = true
|
||||
const templateContent = await file.text()
|
||||
if (!templateContent.trim()) {
|
||||
await passGuard(
|
||||
guardBlocked(
|
||||
'模板内容为空',
|
||||
`${file.name} 里没有任何内容。\n空模板无法用于格式转换,请填好列映射后重新上传。`,
|
||||
'convert.empty-template',
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
const templateName = file.name.replace(/\.[^.]+$/, '') || file.name
|
||||
|
||||
const template = await importConvertTemplate({
|
||||
@@ -436,6 +460,18 @@ async function submitConvertRun() {
|
||||
convertSummary.value = result
|
||||
convertResultItems.value = result.items || []
|
||||
await loadConvertHistory()
|
||||
if (result.total > 0 && result.successCount === 0) {
|
||||
await passGuard(
|
||||
guardBlocked(
|
||||
'格式转换未成功',
|
||||
`本次提交的 ${result.total} 个文件全部转换失败。\n` +
|
||||
'常见原因是源文件表头与所选模板不匹配,或文件内没有数据行。\n' +
|
||||
'请在右侧结果列表查看每个文件的失败原因,或换一个模板后重试。',
|
||||
'convert.all-failed',
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
ElMessage.success('格式转换完成')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '格式转换失败')
|
||||
|
||||
@@ -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 : '去重失败')
|
||||
|
||||
@@ -235,6 +235,13 @@ import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebvi
|
||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import {
|
||||
EXCEL_EXTENSIONS,
|
||||
checkQueuePayload,
|
||||
checkSelectedFiles,
|
||||
guardBlocked,
|
||||
} from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
|
||||
|
||||
interface SessionDeleteBrandItem extends DeleteBrandResultItem {
|
||||
@@ -1054,10 +1061,14 @@ async function selectFiles() {
|
||||
try {
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
selectedPaths.value = paths
|
||||
uploadedFiles.value = await uploadPathsToJava(paths)
|
||||
ElMessage.success(`已选择 ${paths.length} 个删除品牌文件`)
|
||||
} catch (error) {
|
||||
// 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪
|
||||
selectedPaths.value = []
|
||||
uploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -1077,10 +1088,13 @@ async function selectFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有 xlsx 文件')
|
||||
return
|
||||
}
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
selectedPaths.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
uploadedFiles.value = await uploadPathsToJava(result.items)
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 xlsx 文件`)
|
||||
} catch (error) {
|
||||
selectedPaths.value = []
|
||||
uploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -1100,6 +1114,21 @@ async function submitRun() {
|
||||
})),
|
||||
})
|
||||
const normalizedItems = normalizeDeleteBrandItems(result.items || [])
|
||||
// 一条都没解析出来说明文件根本没读懂,直接拦下,不写入会话与历史
|
||||
if (!normalizedItems.length) {
|
||||
await passGuard(
|
||||
guardBlocked(
|
||||
'解析结果无法执行',
|
||||
'这批文件里没有解析出任何删除品牌记录。\n' +
|
||||
'请确认文件名能对应到店铺名、表内有数据行且表头与模板一致,修正后重新上传。',
|
||||
'delete-brand.empty-result',
|
||||
),
|
||||
)
|
||||
queuePushResult.value = '解析完成,但没有解析出任何记录'
|
||||
queuePayloadText.value = ''
|
||||
await loadHistory()
|
||||
return
|
||||
}
|
||||
const hasRunnableItems = normalizedItems.some((item) => isUsableMatchedItem(item))
|
||||
const hasStaleMatchedItems = normalizedItems.some((item) => item.matchStatus === 'INDEX_STALE' && item.matched)
|
||||
const hasBlockedItems = normalizedItems.some((item) => !isUsableMatchedItem(item))
|
||||
@@ -1322,6 +1351,16 @@ async function runItem(item: DeleteBrandResultItem, options?: { auto?: boolean }
|
||||
}
|
||||
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: 'delete-brand-run',
|
||||
requiredDataKeys: ['taskId'],
|
||||
nonEmptyArrayKeys: ['items'],
|
||||
})
|
||||
if (!(await passGuard(guard))) {
|
||||
queuePushResult.value = `文件 ${item.sourceFilename || taskId} 数据校验未通过,已阻止推送`
|
||||
saveSessionTask(taskId, sessionTask.items, queuePushResult.value, queuePayloadText.value)
|
||||
return { success: false, retryable: false }
|
||||
}
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
let qpr = ''
|
||||
if (pushResult?.success) {
|
||||
|
||||
@@ -395,6 +395,8 @@ import { getPywebviewApi } from "@/shared/bridges/pywebview";
|
||||
import { getTaskPollIntervalMs } from "@/shared/task-progress-config";
|
||||
import { createCategorizedTimers } from "@/shared/utils/categorized-timers";
|
||||
import { saveUrlWithProgress } from "@/shared/utils/download-progress";
|
||||
import { checkQueuePayload } from "@/shared/dispatch-guard";
|
||||
import { passGuard } from "@/shared/dispatch-guard-ui";
|
||||
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
|
||||
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
|
||||
|
||||
@@ -1119,6 +1121,31 @@ async function processQueue() {
|
||||
const payload = buildQueuePayload(created.taskId, createdItems);
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2);
|
||||
|
||||
// 删除条件为空时 Python 端没有可执行的判定规则,任务会一直停在执行中
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: "patrol-delete-run",
|
||||
requiredDataKeys: ["taskId"],
|
||||
nonEmptyArrayKeys: ["items", "delete_conditions"],
|
||||
});
|
||||
if (!(await passGuard(guard))) {
|
||||
await submitPatrolDeleteTaskResult(created.taskId, {
|
||||
shops: createdItems.map((createdItem) => ({
|
||||
shopName: createdItem.shopName || "",
|
||||
error: `任务 ${created.taskId} 数据校验未通过,已阻止推送`,
|
||||
countrySections: createdItem.countrySections || [],
|
||||
cartRatios: createdItem.cartRatios || [],
|
||||
shopDone: true,
|
||||
submissionId: createSubmissionId(created.taskId, createdItem.shopName),
|
||||
chunkIndex: 1,
|
||||
chunkTotal: 1,
|
||||
})),
|
||||
});
|
||||
await refreshTaskViews();
|
||||
queuePushResult.value = `任务 ${created.taskId} 数据校验未通过,已阻止推送`;
|
||||
clearActiveQueueTask();
|
||||
return;
|
||||
}
|
||||
|
||||
const pushResult = await api.enqueue_json(payload);
|
||||
if (!pushResult?.success) {
|
||||
await submitPatrolDeleteTaskResult(created.taskId, {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -224,6 +224,8 @@ import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import { checkQueuePayload, guardBlocked } from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import ZiniaoVersionSetting from '@/shared/components/ZiniaoVersionSetting.vue'
|
||||
import { useZiniaoVersion } from '@/shared/utils/ziniao-version'
|
||||
|
||||
@@ -1060,6 +1062,15 @@ function shouldBatchProductRiskQueue() {
|
||||
return productRiskListingFilter.value === 'SearchSuppressed' || productRiskListingFilter.value === 'DetailPageRemoved'
|
||||
}
|
||||
|
||||
/** 入队前的结构校验:店铺列表或国家列表为空、payload 里混进 NaN,都会让 Python 端空转 */
|
||||
function productRiskPayloadGuard(payload: unknown) {
|
||||
return checkQueuePayload(payload, {
|
||||
expectedType: 'product-risk-resolve-run',
|
||||
requiredDataKeys: ['taskId'],
|
||||
nonEmptyArrayKeys: ['items', 'country_codes'],
|
||||
})
|
||||
}
|
||||
|
||||
async function processMatchedBatchQueue(toPush: ProductRiskShopQueueItem[]) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.enqueue_json) {
|
||||
@@ -1090,6 +1101,10 @@ async function processMatchedBatchQueue(toPush: ProductRiskShopQueueItem[]) {
|
||||
},
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
if (!(await passGuard(productRiskPayloadGuard(payload)))) {
|
||||
removePollingTask(taskId)
|
||||
throw new Error('任务 ' + taskId + ' 数据校验未通过,已阻止推送')
|
||||
}
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
if (!pushResult?.success) {
|
||||
removePollingTask(taskId)
|
||||
@@ -1122,6 +1137,17 @@ async function processMatchedQueue() {
|
||||
ElMessage.warning('没有已匹配的店铺可推送')
|
||||
return
|
||||
}
|
||||
// 一个国家都没选时 Python 端没有可遍历的站点,任务会停在执行中;在建任务之前就拦掉
|
||||
if (!orderedCountryCodes.value.length) {
|
||||
await passGuard(
|
||||
guardBlocked(
|
||||
'还没有选择国家',
|
||||
'当前一个国家都没有勾选,推送后 Python 端没有可遍历的站点,任务会一直停在执行中。\n请先在左侧勾选至少 1 个国家。',
|
||||
'product-risk.no-country',
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
queueWorkerRunning.value = true
|
||||
pushing.value = true
|
||||
queuePayloadText.value = ''
|
||||
@@ -1168,6 +1194,10 @@ async function processMatchedQueue() {
|
||||
},
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2)
|
||||
if (!(await passGuard(productRiskPayloadGuard(payload)))) {
|
||||
removePollingTask(taskId)
|
||||
throw new Error('任务 ' + taskId + ' 数据校验未通过,已阻止推送')
|
||||
}
|
||||
const pushResult = await api.enqueue_json(payload)
|
||||
if (!pushResult?.success) {
|
||||
removePollingTask(taskId)
|
||||
|
||||
@@ -227,6 +227,13 @@ import { useTaskProgressLoop } from '@/shared/composables/useTaskProgressLoop'
|
||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import {
|
||||
EXCEL_EXTENSIONS,
|
||||
checkParseResult,
|
||||
checkQueuePayload,
|
||||
checkSelectedFiles,
|
||||
} from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import { useZiniaoVersion, type ZiniaoVersion } from '@/shared/utils/ziniao-version'
|
||||
|
||||
const COUNTRY_OPTIONS = [
|
||||
@@ -463,12 +470,16 @@ async function selectFiles() {
|
||||
try {
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
uploading.value = true
|
||||
const uploaded = await uploadPathsToJava(paths)
|
||||
selectedPaths.value = paths
|
||||
uploadedFiles.value = uploaded
|
||||
ElMessage.success(`已上传 ${uploaded.length} 个待上架文件`)
|
||||
} catch (error) {
|
||||
// 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪
|
||||
selectedPaths.value = []
|
||||
uploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
@@ -489,12 +500,15 @@ async function selectFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有 xlsx 文件')
|
||||
return
|
||||
}
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
uploading.value = true
|
||||
const uploaded = await uploadPathsToJava(result.items)
|
||||
selectedPaths.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
uploadedFiles.value = uploaded
|
||||
ElMessage.success(`已上传文件夹内 ${uploaded.length} 个 xlsx 文件`)
|
||||
} catch (error) {
|
||||
selectedPaths.value = []
|
||||
uploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
} finally {
|
||||
uploading.value = false
|
||||
@@ -532,6 +546,12 @@ async function submitRun() {
|
||||
sync_countries: syncCountries.value.filter((country) => country !== publishCountry.value),
|
||||
})
|
||||
if (!parsed.taskId) throw new Error('后端未返回有效任务标识')
|
||||
// publish 的 Vo 用 files 承载明细,没有 acceptedRows;这里先卡住 taskId 与总行数
|
||||
const guard = checkParseResult(
|
||||
{ taskId: parsed.taskId, totalRows: parsed.totalRows, acceptedRows: parsed.totalRows },
|
||||
{ requiredColumnsHint: '店铺名 / 商品行' },
|
||||
)
|
||||
if (!(await passGuard(guard))) return
|
||||
|
||||
const options: PublishDispatchOptions = {
|
||||
publishCountry: publishCountry.value,
|
||||
@@ -851,6 +871,15 @@ async function processQueue() {
|
||||
await activatePublishFile(taskId, nextFileId)
|
||||
updateCurrentFile(nextFileId, { status: 'RUNNING', progressMessage: '已派发到 Python 队列' })
|
||||
const payload = buildQueuePayload(taskId, file)
|
||||
// totalRows/totalPages 为 0 时 Python 端翻不到任何明细,会一直停在执行中
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: 'publish-run',
|
||||
requiredDataKeys: ['taskId', 'fileId', 'shopName', 'publish_country', 'paginationUrl'],
|
||||
positiveNumberKeys: ['totalRows', 'totalPages'],
|
||||
})
|
||||
if (!(await passGuard(guard))) {
|
||||
throw new Error(`文件 ${file.sourceFilename || nextFileId} 数据校验未通过,已阻止推送`)
|
||||
}
|
||||
const result = await api.enqueue_json(payload)
|
||||
if (!result?.success) throw new Error(result?.error || 'Python 队列拒绝接收任务')
|
||||
queueMessage.value = pendingFileIds.value.length
|
||||
|
||||
@@ -343,6 +343,8 @@ import { getPywebviewApi } from "@/shared/bridges/pywebview";
|
||||
import { getTaskPollIntervalMs } from "@/shared/task-progress-config";
|
||||
import { createCategorizedTimers } from "@/shared/utils/categorized-timers";
|
||||
import { saveUrlWithProgress } from "@/shared/utils/download-progress";
|
||||
import { checkQueuePayload } from "@/shared/dispatch-guard";
|
||||
import { passGuard } from "@/shared/dispatch-guard-ui";
|
||||
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
|
||||
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
|
||||
|
||||
@@ -1050,6 +1052,32 @@ async function processQueue() {
|
||||
const payload = buildQueuePayload(created.taskId, createdItem);
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2);
|
||||
|
||||
// 模板里一个有效 ASIN 都没解析出来时,Python 端拿到空列表会空转,任务停在执行中
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: "query-asin-run",
|
||||
requiredDataKeys: ["taskId"],
|
||||
nonEmptyArrayKeys: ["items", "query_asins"],
|
||||
});
|
||||
if (!(await passGuard(guard))) {
|
||||
await submitQueryAsinTaskResult(created.taskId, {
|
||||
shops: [
|
||||
{
|
||||
shopName: createdItem.shopName || nextItem.shopName || "",
|
||||
error: `任务 ${created.taskId} 数据校验未通过,已阻止推送`,
|
||||
shopDone: true,
|
||||
submissionId: createSubmissionId(
|
||||
created.taskId,
|
||||
createdItem.shopName || nextItem.shopName,
|
||||
),
|
||||
},
|
||||
],
|
||||
});
|
||||
await refreshTaskViews();
|
||||
queuePushResult.value = `任务 ${created.taskId} 数据校验未通过,已阻止推送并继续下一个任务`;
|
||||
clearActiveQueueTask();
|
||||
continue;
|
||||
}
|
||||
|
||||
const pushResult = await api.enqueue_json(payload);
|
||||
if (!pushResult?.success) {
|
||||
await submitQueryAsinTaskResult(created.taskId, {
|
||||
|
||||
@@ -127,6 +127,8 @@ import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import { getTaskPollIntervalMs } from '@/shared/task-progress-config'
|
||||
import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import { checkQueuePayload } from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
import { mergeHistoryItems } from '@/shared/merge-history-items'
|
||||
import {
|
||||
addShopDataCrawlCandidate,
|
||||
@@ -429,6 +431,15 @@ async function dispatchActiveTask(api: NonNullable<ReturnType<typeof getPywebvie
|
||||
country_codes: [...activeCountryCodes.value],
|
||||
},
|
||||
}
|
||||
// country_codes 为空时 Python 端没有可遍历的站点,任务会一直停在执行中
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: 'shop-data-crawl-run',
|
||||
requiredDataKeys: ['taskId'],
|
||||
nonEmptyArrayKeys: ['items', 'country_codes'],
|
||||
})
|
||||
if (!(await passGuard(guard))) {
|
||||
throw new Error(`任务 ${activeTaskId.value} 数据校验未通过,已阻止推送`)
|
||||
}
|
||||
const pushed = await api.enqueue_json(payload)
|
||||
if (!pushed?.success) throw new Error(pushed?.error || `任务 ${activeTaskId.value} 入队失败`)
|
||||
activeDispatched.value = true
|
||||
|
||||
@@ -213,6 +213,13 @@ import { createCategorizedTimers } from '@/shared/utils/categorized-timers'
|
||||
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
|
||||
import { createAsinForceThrottle } from '@/shared/asin-force-throttle'
|
||||
import { toParsePreview, type ParsePreviewOptions } from '@/shared/parse-preview'
|
||||
import {
|
||||
EXCEL_EXTENSIONS,
|
||||
checkParseResult,
|
||||
checkQueuePayload,
|
||||
checkSelectedFiles,
|
||||
} from '@/shared/dispatch-guard'
|
||||
import { passGuard } from '@/shared/dispatch-guard-ui'
|
||||
|
||||
const selectedFileNames = ref<string[]>([])
|
||||
const uploadedFiles = ref<UploadFileVo[]>([])
|
||||
@@ -421,12 +428,23 @@ async function selectFiles() {
|
||||
}
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
const files = await uploadAppearancePathsToJava(paths)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
if (!(await passGuard(checkSelectedFiles(paths, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
try {
|
||||
const files = await uploadAppearancePathsToJava(paths)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = files.map((f) => f.relativePath || f.originalFilename || f.fileKey)
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
} catch (error) {
|
||||
// 上传失败时清空选择,避免残留上一批文件让用户误以为新文件已就绪
|
||||
uploadedFiles.value = []
|
||||
selectedFileNames.value = []
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
ElMessage.error(error instanceof Error ? error.message : '文件上传失败')
|
||||
}
|
||||
}
|
||||
|
||||
async function selectFolder() {
|
||||
@@ -443,6 +461,7 @@ async function selectFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
|
||||
return
|
||||
}
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
const files = await uploadAppearancePathsToJava(result.items)
|
||||
uploadedFiles.value = files
|
||||
selectedFileNames.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
@@ -451,6 +470,9 @@ async function selectFolder() {
|
||||
queuePayloadText.value = ''
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个 Excel 文件`)
|
||||
} catch (error) {
|
||||
uploadedFiles.value = []
|
||||
selectedFileNames.value = []
|
||||
parseResult.value = null
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -473,6 +495,14 @@ async function parseFiles() {
|
||||
relativePath: f.relativePath,
|
||||
}))
|
||||
const res = await parseSimilarAsin(files, effectiveLlmApiKey(), imgSwitch.value, categorySwitch.value)
|
||||
// 0 有效行的任务推给 Python 只会一直停在 RUNNING,这里拦下来不写入 parseResult
|
||||
const guard = checkParseResult(res, { requiredColumnsHint: 'id / ASIN / 国家' })
|
||||
if (!(await passGuard(guard))) {
|
||||
parseResult.value = null
|
||||
queuedTaskSummary.value = null
|
||||
queuePayloadText.value = ''
|
||||
return
|
||||
}
|
||||
// 只保留摘要字段与有界预览行,避免数千行 items/groups 进入响应式对象
|
||||
const previewOptions: ParsePreviewOptions = { previewRowLimit: 200 }
|
||||
parseResult.value = toParsePreview(res, previewOptions) as unknown as SimilarAsinParseVo
|
||||
@@ -551,6 +581,12 @@ async function pushToPythonQueue() {
|
||||
},
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(payloadForDisplay(payload), null, 2)
|
||||
// 入队前最后一道闸:缺字段或含 NaN 的 payload 到 Python 侧会变成 None,任务卡住
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: 'similar-asin-run',
|
||||
requiredDataKeys: ['taskId', 'api_key', 'aliprice_usename', 'aliprice_pwd'],
|
||||
})
|
||||
if (!(await passGuard(guard))) return
|
||||
await activateSimilarAsinTask(taskId)
|
||||
const result = await api.enqueue_json(payload)
|
||||
if (!result?.success) {
|
||||
|
||||
@@ -170,6 +170,8 @@ import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared
|
||||
import { deleteSplitHistory, getExcelInfo, getSplitHistory, getSplitResultDownloadUrl, runSplit, type SplitResultItem, type SplitRunVo } 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 splitSelectedPaths = ref<string[]>([])
|
||||
const splitArchiveName = ref('')
|
||||
@@ -239,8 +241,11 @@ async function selectSplitFiles() {
|
||||
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) {
|
||||
splitSelectedPaths.value = []
|
||||
splitUploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -259,6 +264,7 @@ async function selectSplitFolder() {
|
||||
|
||||
const result = await expandBrandFolderRecursive(folder)
|
||||
if (result.success && result.items?.length) {
|
||||
if (!(await passGuard(checkSelectedFiles(result.items, { allowedExtensions: EXCEL_EXTENSIONS })))) return
|
||||
splitSelectedPaths.value = result.items.map((item) => item.relativePath)
|
||||
splitUploadedFiles.value = await uploadPathsToJava(result.items)
|
||||
if (splitUploadedFiles.value.length > 0) {
|
||||
@@ -270,6 +276,8 @@ async function selectSplitFolder() {
|
||||
|
||||
ElMessage.warning(result.error || '该文件夹下没有 xlsx 文件')
|
||||
} catch (error) {
|
||||
splitSelectedPaths.value = []
|
||||
splitUploadedFiles.value = []
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
}
|
||||
}
|
||||
@@ -308,6 +316,18 @@ async function submitSplitRun() {
|
||||
splitSummary.value = result
|
||||
splitResultItems.value = result.items || []
|
||||
await loadSplitHistory()
|
||||
if (result.total > 0 && result.successCount === 0) {
|
||||
await passGuard(
|
||||
guardBlocked(
|
||||
'拆分未成功',
|
||||
`本次提交的 ${result.total} 个文件全部处理失败。\n` +
|
||||
'常见原因是表头列名与所选保留列对不上,或文件内没有数据行。\n' +
|
||||
'请在右侧结果列表查看每个文件的失败原因后重试。',
|
||||
'split.all-failed',
|
||||
),
|
||||
)
|
||||
return
|
||||
}
|
||||
ElMessage.success('数据拆分完成')
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '数据拆分失败')
|
||||
|
||||
@@ -356,6 +356,8 @@ import { getPywebviewApi } from "@/shared/bridges/pywebview";
|
||||
import { getTaskPollIntervalMs } from "@/shared/task-progress-config";
|
||||
import { createCategorizedTimers } from "@/shared/utils/categorized-timers";
|
||||
import { saveUrlWithProgress } from "@/shared/utils/download-progress";
|
||||
import { checkQueuePayload } from "@/shared/dispatch-guard";
|
||||
import { passGuard } from "@/shared/dispatch-guard-ui";
|
||||
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
|
||||
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
|
||||
|
||||
@@ -1161,6 +1163,27 @@ async function processQueue() {
|
||||
const payload = buildQueuePayload(created.taskId, nextBatch);
|
||||
queuePayloadText.value = JSON.stringify(payload, null, 2);
|
||||
|
||||
const guard = checkQueuePayload(payload, {
|
||||
expectedType: "withdraw-run",
|
||||
requiredDataKeys: ["task_id"],
|
||||
nonEmptyArrayKeys: ["items"],
|
||||
});
|
||||
if (!(await passGuard(guard))) {
|
||||
await submitWithdrawTaskResult(created.taskId, {
|
||||
shops: nextBatch.items.map((item) => ({
|
||||
shopName: item.shopName || "",
|
||||
error: `任务 ${created.taskId} 数据校验未通过,已阻止推送`,
|
||||
shopDone: true,
|
||||
submissionId: createSubmissionId(created.taskId, item.shopName),
|
||||
rows: [],
|
||||
})),
|
||||
});
|
||||
await refreshTaskViews();
|
||||
queuePushResult.value = `任务 ${created.taskId} 数据校验未通过,已阻止推送并继续下一个任务`;
|
||||
clearActiveQueueTask();
|
||||
continue;
|
||||
}
|
||||
|
||||
const pushResult = await api.enqueue_json(payload);
|
||||
if (!pushResult?.success) {
|
||||
await submitWithdrawTaskResult(created.taskId, {
|
||||
|
||||
Reference in New Issue
Block a user