更新跟价相关内容修改
This commit is contained in:
@@ -261,7 +261,7 @@ import { ElMessage } from 'element-plus'
|
||||
import BrandTopBar from '@/pages/brand/components/BrandTopBar.vue'
|
||||
import { LISTING_FILTER_OPTIONS, type ListingFilterValue } from '@/pages/brand/components/listingFilters'
|
||||
import { expandBrandFolderRecursive } from '@/shared/api/brand'
|
||||
import { getPywebviewApi } from '@/shared/bridges/pywebview'
|
||||
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
|
||||
import {
|
||||
addPriceTrackCandidate,
|
||||
completePriceTrackLoopChild,
|
||||
@@ -327,6 +327,7 @@ const asinModeEnabled = ref(false) // 按指定ASIN文档(自定义)
|
||||
|
||||
// 上传文件
|
||||
const asinFiles = ref<string[]>([]) // ASIN文档
|
||||
const asinUploadedFiles = ref<UploadedJavaFile[]>([])
|
||||
|
||||
// 是否有有效模式
|
||||
const hasValidMode = computed(() => statusModeEnabled.value || asinModeEnabled.value)
|
||||
@@ -499,6 +500,7 @@ async function selectAsinFile() {
|
||||
})
|
||||
if (result?.paths?.length) {
|
||||
asinFiles.value = result.paths
|
||||
asinUploadedFiles.value = await uploadAsinPathsToJava(result.paths)
|
||||
ElMessage.success(`已选择 ${result.paths.length} 个ASIN文件`)
|
||||
}
|
||||
return
|
||||
@@ -507,6 +509,7 @@ async function selectAsinFile() {
|
||||
const paths = await api.select_brand_xlsx_files()
|
||||
if (!paths?.length) return
|
||||
asinFiles.value = paths
|
||||
asinUploadedFiles.value = await uploadAsinPathsToJava(paths)
|
||||
ElMessage.success(`已选择 ${paths.length} 个ASIN文件`)
|
||||
return
|
||||
}
|
||||
@@ -527,7 +530,8 @@ async function selectAsinFolder() {
|
||||
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
|
||||
return
|
||||
}
|
||||
asinFiles.value = result.items.map((item) => item.absolutePath)
|
||||
asinFiles.value = result.items.map((item) => item.relativePath || item.absolutePath)
|
||||
asinUploadedFiles.value = await uploadAsinPathsToJava(result.items)
|
||||
ElMessage.success(`已选择文件夹内 ${result.items.length} 个ASIN文件`)
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : '选择失败')
|
||||
@@ -537,6 +541,31 @@ async function selectAsinFolder() {
|
||||
|
||||
|
||||
// ========== 国家顺序相关 ==========
|
||||
async function uploadAsinPathsToJava(paths: Array<string | { absolutePath: string; relativePath?: string }>) {
|
||||
const api = getPywebviewApi()
|
||||
if (!api?.upload_file_to_java) {
|
||||
return []
|
||||
}
|
||||
const uploaded: UploadedJavaFile[] = []
|
||||
for (const item of paths) {
|
||||
const filePath = typeof item === 'string' ? item : item.absolutePath
|
||||
const relativePath = typeof item === 'string' ? undefined : item.relativePath
|
||||
const result = await api.upload_file_to_java(filePath, relativePath)
|
||||
if (!result?.success || !result.data) {
|
||||
throw new Error(result?.error || result?.message || `上传失败:${filePath}`)
|
||||
}
|
||||
uploaded.push(result.data)
|
||||
}
|
||||
return uploaded
|
||||
}
|
||||
|
||||
function resolveAsinRequestPaths() {
|
||||
if (asinUploadedFiles.value.length) {
|
||||
return asinUploadedFiles.value.map((item) => item.localPath).filter((path) => !!path)
|
||||
}
|
||||
return asinFiles.value
|
||||
}
|
||||
|
||||
function selectMode(mode: 'status' | 'asin') {
|
||||
statusModeEnabled.value = mode === 'status'
|
||||
asinModeEnabled.value = mode === 'asin'
|
||||
@@ -744,7 +773,7 @@ async function runMatch() {
|
||||
matching.value = true
|
||||
try {
|
||||
const res = await matchPriceTrackShops(names, {
|
||||
asinFiles: asinModeEnabled.value ? asinFiles.value : [],
|
||||
asinFiles: asinModeEnabled.value ? resolveAsinRequestPaths() : [],
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
})
|
||||
const batch = res.items || []
|
||||
@@ -846,7 +875,7 @@ async function pushToPythonQueueLegacy() {
|
||||
statusMode: statusModeEnabled.value,
|
||||
asinMode: asinModeEnabled.value,
|
||||
items: matchedRows as unknown as Record<string, unknown>[],
|
||||
asinFiles: asinFiles.value,
|
||||
asinFiles: resolveAsinRequestPaths(),
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
}
|
||||
const taskVo = await createPriceTrackTask(taskReq)
|
||||
@@ -906,6 +935,7 @@ function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQu
|
||||
task_id: taskVo.taskId,
|
||||
shop_name: shopName,
|
||||
shop_id: row.shopId ?? null,
|
||||
shop_mall_name: row.shopMallName || '',
|
||||
country_codes: [...orderedCountryCodes.value],
|
||||
mode: statusModeEnabled.value ? 'status' : 'asin',
|
||||
skip_asins: skipAsinsByCountry,
|
||||
@@ -918,32 +948,51 @@ function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQu
|
||||
}
|
||||
|
||||
async function waitForTaskTerminal(taskId: number) {
|
||||
let transientErrorCount = 0
|
||||
const maxTransientErrors = 30
|
||||
while (true) {
|
||||
const batch = await getPriceTrackTaskProgressBatch([taskId])
|
||||
if ((batch.missingTaskIds || []).includes(taskId)) {
|
||||
removePollingTask(taskId)
|
||||
await loadHistory()
|
||||
await loadDashboard()
|
||||
syncPollingIdsWithHistory()
|
||||
return 'FAILED'
|
||||
}
|
||||
const detail = (batch.items || []).find((item) => item.task?.id === taskId)
|
||||
const status = detail?.task?.status || ''
|
||||
if (detail) {
|
||||
const prev = taskSnapshots.value[taskId]
|
||||
taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }
|
||||
saveTaskSnapshotsToStorage()
|
||||
}
|
||||
if (status) {
|
||||
taskDetails.value = { ...taskDetails.value, [taskId]: status }
|
||||
saveTaskDetailsToStorage()
|
||||
}
|
||||
if (status === 'SUCCESS' || status === 'FAILED') {
|
||||
removePollingTask(taskId)
|
||||
await loadHistory()
|
||||
await loadDashboard()
|
||||
syncPollingIdsWithHistory()
|
||||
return status
|
||||
try {
|
||||
const batch = await getPriceTrackTaskProgressBatch([taskId])
|
||||
transientErrorCount = 0
|
||||
if ((batch.missingTaskIds || []).includes(taskId)) {
|
||||
removePollingTask(taskId)
|
||||
await loadHistory()
|
||||
await loadDashboard()
|
||||
syncPollingIdsWithHistory()
|
||||
return 'FAILED'
|
||||
}
|
||||
const detail = (batch.items || []).find((item) => item.task?.id === taskId)
|
||||
const status = detail?.task?.status || ''
|
||||
if (detail) {
|
||||
const prev = taskSnapshots.value[taskId]
|
||||
taskSnapshots.value = { ...taskSnapshots.value, [taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail }
|
||||
saveTaskSnapshotsToStorage()
|
||||
}
|
||||
if (status) {
|
||||
taskDetails.value = { ...taskDetails.value, [taskId]: status }
|
||||
saveTaskDetailsToStorage()
|
||||
}
|
||||
if (status === 'SUCCESS' || status === 'FAILED') {
|
||||
removePollingTask(taskId)
|
||||
await loadHistory()
|
||||
await loadDashboard()
|
||||
syncPollingIdsWithHistory()
|
||||
return status
|
||||
}
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
|
||||
const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
|
||||
if (!transient) throw error
|
||||
transientErrorCount += 1
|
||||
if (transientErrorCount >= maxTransientErrors) {
|
||||
throw new Error(`任务 ${taskId} 等待后端恢复超时,请稍后手动刷新查看状态`)
|
||||
}
|
||||
queuePushResult.value = `任务 ${taskId} 运行中,正在等待后端服务恢复(${transientErrorCount}/${maxTransientErrors})...`
|
||||
if (transientErrorCount === 1 || transientErrorCount % 5 === 0) {
|
||||
ElMessage.warning(`任务 ${taskId} 运行中,后端服务暂时不可用,正在自动重试`)
|
||||
}
|
||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
||||
continue
|
||||
}
|
||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
||||
}
|
||||
@@ -973,14 +1022,28 @@ async function pushToPythonQueue() {
|
||||
try {
|
||||
for (let index = 0; index < matchedRows.length; index += 1) {
|
||||
const row = matchedRows[index]
|
||||
const taskVo = await createPriceTrackTask({
|
||||
userId: 0,
|
||||
statusMode: statusModeEnabled.value,
|
||||
asinMode: asinModeEnabled.value,
|
||||
items: [row] as unknown as Record<string, unknown>[],
|
||||
asinFiles: asinFiles.value,
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
})
|
||||
let attempt = 0
|
||||
const taskVo = await (async () => {
|
||||
while (true) {
|
||||
try {
|
||||
return await createPriceTrackTask({
|
||||
userId: 0,
|
||||
statusMode: statusModeEnabled.value,
|
||||
asinMode: asinModeEnabled.value,
|
||||
items: [row] as unknown as Record<string, unknown>[],
|
||||
asinFiles: resolveAsinRequestPaths(),
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
})
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
|
||||
const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
|
||||
attempt += 1
|
||||
if (!transient || attempt >= 10) throw error
|
||||
queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/10)...`
|
||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
||||
}
|
||||
}
|
||||
})()
|
||||
taskSnapshots.value = {
|
||||
...taskSnapshots.value,
|
||||
[taskVo.taskId]: {
|
||||
@@ -1050,7 +1113,10 @@ async function syncActiveLoopRun() {
|
||||
setLoopRun(loop)
|
||||
clearLoopRunIfTerminal()
|
||||
return activeLoopRun.value
|
||||
} catch {
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
|
||||
const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
|
||||
if (transient) return activeLoopRun.value
|
||||
activeLoopRun.value = null
|
||||
activeLoopRunId.value = null
|
||||
saveActiveLoopRunId()
|
||||
@@ -1107,10 +1173,25 @@ async function runLoopExecution(loopId?: number) {
|
||||
if (!loop || loop.status === 'SUCCESS' || loop.status === 'FAILED' || loop.status === 'STOPPED') break
|
||||
|
||||
if (loop.activeTaskId) {
|
||||
addPollingTask(loop.activeTaskId)
|
||||
const activeTaskId = loop.activeTaskId
|
||||
addPollingTask(activeTaskId)
|
||||
scheduleNextPoll(true)
|
||||
const finalStatus = await waitForTaskTerminal(loop.activeTaskId)
|
||||
const updatedLoop = await completePriceTrackLoopChild(loop.id, loop.activeTaskId)
|
||||
const finalStatus = await waitForTaskTerminal(activeTaskId)
|
||||
let completeAttempt = 0
|
||||
const updatedLoop = await (async () => {
|
||||
while (true) {
|
||||
try {
|
||||
return await completePriceTrackLoopChild(loop.id, activeTaskId)
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
|
||||
const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
|
||||
completeAttempt += 1
|
||||
if (!transient || completeAttempt >= 10) throw error
|
||||
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后确认子任务完成(${completeAttempt}/10)...`
|
||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
||||
}
|
||||
}
|
||||
})()
|
||||
setLoopRun(updatedLoop)
|
||||
await loadHistory()
|
||||
await loadDashboard()
|
||||
@@ -1122,15 +1203,44 @@ async function runLoopExecution(loopId?: number) {
|
||||
continue
|
||||
}
|
||||
|
||||
const dispatch = await dispatchNextPriceTrackLoopRun(loop.id)
|
||||
let dispatchAttempt = 0
|
||||
const dispatch = await (async () => {
|
||||
while (true) {
|
||||
try {
|
||||
return await dispatchNextPriceTrackLoopRun(loop.id)
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
|
||||
const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
|
||||
dispatchAttempt += 1
|
||||
if (!transient || dispatchAttempt >= 10) throw error
|
||||
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后继续派发(${dispatchAttempt}/10)...`
|
||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
||||
}
|
||||
}
|
||||
})()
|
||||
setLoopRun(dispatch.loopRun || loop)
|
||||
if (!dispatch.childTaskRequest) {
|
||||
clearLoopRunIfTerminal()
|
||||
break
|
||||
}
|
||||
const row = dispatch.childTaskRequest.items?.[0]
|
||||
const childTaskRequest = dispatch.childTaskRequest
|
||||
const row = childTaskRequest.items?.[0]
|
||||
if (!row) throw new Error('循环任务缺少待执行店铺')
|
||||
const taskVo = await createPriceTrackTask(dispatch.childTaskRequest)
|
||||
let createAttempt = 0
|
||||
const taskVo = await (async () => {
|
||||
while (true) {
|
||||
try {
|
||||
return await createPriceTrackTask(childTaskRequest)
|
||||
} catch (error) {
|
||||
const message = (error instanceof Error ? error.message : String(error || '')).toLowerCase()
|
||||
const transient = ['无法连接到后端服务', 'bad gateway', 'gateway timeout', 'network error', 'timeout', '502', '503', '504'].some((pattern) => message.includes(pattern.toLowerCase()))
|
||||
createAttempt += 1
|
||||
if (!transient || createAttempt >= 10) throw error
|
||||
queuePushResult.value = `循环任务 ${loop.id} 等待后端恢复后重试创建子任务(${createAttempt}/10)...`
|
||||
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
|
||||
}
|
||||
}
|
||||
})()
|
||||
recordCreatedTask(taskVo)
|
||||
const queuePayload = buildQueuePayload(taskVo, row)
|
||||
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
|
||||
@@ -1185,7 +1295,7 @@ async function pushToPythonLoopQueue() {
|
||||
statusMode: statusModeEnabled.value,
|
||||
asinMode: asinModeEnabled.value,
|
||||
items: matchedRows,
|
||||
asinFiles: asinFiles.value,
|
||||
asinFiles: resolveAsinRequestPaths(),
|
||||
countryCodes: [...orderedCountryCodes.value],
|
||||
executionMode: executionMode.value,
|
||||
targetRounds: executionMode.value === 'FINITE' ? Math.max(1, Number(roundCount.value) || 1) : undefined,
|
||||
|
||||
Reference in New Issue
Block a user