完成后端架构重构等
This commit is contained in:
@@ -358,6 +358,7 @@ const pendingQueue = ref<PatrolDeleteShopQueueItem[]>([]);
|
||||
const activeTaskId = ref<number | null>(null);
|
||||
const activeQueueItem = ref<PatrolDeleteShopQueueItem | null>(null);
|
||||
const queueWorkerRunning = ref(false);
|
||||
const autoQueueEnabled = ref(false);
|
||||
let historyPollTimer: number | null = null;
|
||||
|
||||
const matchedRunnableItems = computed(() =>
|
||||
@@ -774,6 +775,11 @@ async function runMatch() {
|
||||
const data = await matchPatrolDeleteShops(names);
|
||||
matchedItems.value = mergeMatchedItems(matchedItems.value, data.items || []);
|
||||
saveMatchedItems();
|
||||
if (autoQueueEnabled.value) {
|
||||
pendingQueue.value = mergeQueueItems(pendingQueue.value, matchedRunnableItems.value);
|
||||
saveQueueState();
|
||||
void processQueue();
|
||||
}
|
||||
ElMessage.success(`本次返回 ${data.items?.length || 0} 条匹配结果`);
|
||||
} catch (error) {
|
||||
ElMessage.error(error instanceof Error ? error.message : "匹配失败");
|
||||
@@ -963,6 +969,7 @@ async function processQueue() {
|
||||
}
|
||||
|
||||
async function pushToPythonQueue() {
|
||||
autoQueueEnabled.value = true;
|
||||
const runnable = matchedRunnableItems.value;
|
||||
if (!runnable.length) {
|
||||
ElMessage.warning("请先匹配可用店铺");
|
||||
@@ -1016,6 +1023,7 @@ onMounted(async () => {
|
||||
await Promise.all([loadCandidates(), loadDashboard(), loadHistory()]);
|
||||
|
||||
if (activeTaskId.value || pendingQueue.value.length) {
|
||||
autoQueueEnabled.value = true;
|
||||
queuePushResult.value =
|
||||
activeTaskId.value != null
|
||||
? `检测到未完成队列,继续等待任务 ${activeTaskId.value} 完成并自动接续后续店铺`
|
||||
|
||||
@@ -312,6 +312,8 @@ const matchedItems = ref<PriceTrackShopQueueItem[]>([])
|
||||
const adding = ref(false)
|
||||
const matching = ref(false)
|
||||
const pushing = ref(false)
|
||||
const queueWorkerRunning = ref(false)
|
||||
const autoQueueEnabled = ref(false)
|
||||
const queuePushResult = ref('')
|
||||
const queuePayloadText = ref('')
|
||||
const matchAsinRowsByCountry = ref<Record<string, PriceTrackAsinParsedRow[]>>({})
|
||||
@@ -787,6 +789,9 @@ async function runMatch() {
|
||||
}
|
||||
matchedItems.value = [...nextByShop.values()]
|
||||
saveMatchedItemsToStorage()
|
||||
if (autoQueueEnabled.value) {
|
||||
void processMatchedQueue()
|
||||
}
|
||||
ElMessage.success(`匹配 ${batch.length} 个店铺`)
|
||||
} catch (e) {
|
||||
ElMessage.error(e instanceof Error ? e.message : '匹配失败')
|
||||
@@ -813,6 +818,19 @@ function rowKeyForMatch(row: { shopName?: string; shopId?: number | string | nul
|
||||
return `${(row.shopName || '').trim()}\u0001${row.shopId ?? ''}`
|
||||
}
|
||||
|
||||
function buildSkipAsinDeletePolicy() {
|
||||
const enabled = statusModeEnabled.value && !asinModeEnabled.value
|
||||
return {
|
||||
enabled,
|
||||
mode: enabled ? 'DELETE_WHEN_PRICE_BELOW_MINIMUM' : 'NONE',
|
||||
deleteWhenPriceBelowMinimum: enabled,
|
||||
compareField: 'price',
|
||||
thresholdField: 'minimumPrice',
|
||||
target: 'skip_asin',
|
||||
source: 'frontend-price-track',
|
||||
}
|
||||
}
|
||||
|
||||
function removeMatchedRowsLocally(rows: Array<{ shopName?: string; shopId?: number | string | null }>) {
|
||||
if (!rows.length) return
|
||||
const keys = new Set(rows.map((row) => rowKeyForMatch(row)))
|
||||
@@ -903,6 +921,9 @@ async function pushToPythonQueueLegacy() {
|
||||
// 店铺简要信息(用于展示)
|
||||
shop_names: matchedRows.map((i) => i.shopName).filter(Boolean),
|
||||
country_codes: [...orderedCountryCodes.value],
|
||||
skip_asin_delete_policy: buildSkipAsinDeletePolicy(),
|
||||
delete_skip_asin_when_price_below_minimum: statusModeEnabled.value && !asinModeEnabled.value,
|
||||
deleteSkipAsinWhenPriceBelowMinimum: statusModeEnabled.value && !asinModeEnabled.value,
|
||||
},
|
||||
}
|
||||
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
|
||||
@@ -930,6 +951,9 @@ function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQu
|
||||
const taskItem = taskVo.items?.[0]
|
||||
const shopName = (row.shopName || '').trim()
|
||||
const skipAsinsByCountry = row.skipAsins || taskVo.skipAsinsByCountry || {}
|
||||
const skipAsinDetailsByCountry = taskVo.skipAsinDetailsByCountry || {}
|
||||
const minimumPriceByCountryAndAsin = taskVo.minimumPriceByCountryAndAsin || {}
|
||||
const skipAsinDeletePolicy = buildSkipAsinDeletePolicy()
|
||||
const resultId = taskItem?.resultId ?? null
|
||||
const loopRunId = taskItem?.loopRunId ?? null
|
||||
const roundIndex = taskItem?.roundIndex ?? null
|
||||
@@ -963,6 +987,11 @@ function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQu
|
||||
mode: statusModeEnabled.value ? 'status' : 'asin',
|
||||
skip_asins: skipAsinsByCountry,
|
||||
skip_asins_by_country: skipAsinsByCountry,
|
||||
skip_asin_details_by_country: skipAsinDetailsByCountry,
|
||||
minimum_price_by_country_and_asin: minimumPriceByCountryAndAsin,
|
||||
skip_asin_delete_policy: skipAsinDeletePolicy,
|
||||
delete_skip_asin_when_price_below_minimum: skipAsinDeletePolicy.deleteWhenPriceBelowMinimum,
|
||||
deleteSkipAsinWhenPriceBelowMinimum: skipAsinDeletePolicy.deleteWhenPriceBelowMinimum,
|
||||
asin_rows_by_country: Object.keys(matchAsinRowsByCountry.value).length
|
||||
? matchAsinRowsByCountry.value
|
||||
: (taskVo.asinRowsByCountry || {}),
|
||||
@@ -1039,6 +1068,18 @@ async function waitForTaskTerminal(taskId: number) {
|
||||
}
|
||||
|
||||
async function pushToPythonQueue() {
|
||||
autoQueueEnabled.value = true
|
||||
await processMatchedQueue()
|
||||
}
|
||||
|
||||
function nextMatchedQueueItem() {
|
||||
return matchedItems.value.find((item) => item.matched)
|
||||
}
|
||||
|
||||
async function processMatchedQueue() {
|
||||
if (queueWorkerRunning.value) {
|
||||
return
|
||||
}
|
||||
if (!hasValidMode.value) {
|
||||
ElMessage.warning('请至少选择一个跟价模式')
|
||||
return
|
||||
@@ -1057,13 +1098,19 @@ async function pushToPythonQueue() {
|
||||
ElMessage.warning('无可用匹配店铺')
|
||||
return
|
||||
}
|
||||
queueWorkerRunning.value = true
|
||||
pushing.value = true
|
||||
queuePayloadText.value = ''
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
try {
|
||||
for (let index = 0; index < matchedRows.length; index += 1) {
|
||||
const row = matchedRows[index]
|
||||
let index = 0
|
||||
while (autoQueueEnabled.value) {
|
||||
const row = nextMatchedQueueItem()
|
||||
if (!row) {
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
try {
|
||||
let attempt = 0
|
||||
const taskVo = await (async () => {
|
||||
@@ -1107,37 +1154,38 @@ async function pushToPythonQueue() {
|
||||
failedCount += 1
|
||||
queuePushResult.value = '任务 ' + taskVo.taskId + ' 推送失败,已自动继续下一条:' + (pushResult?.error || '未知错误')
|
||||
ElMessage.error(queuePushResult.value)
|
||||
removeMatchedRowsLocally([row])
|
||||
continue
|
||||
}
|
||||
addPollingTask(taskVo.taskId)
|
||||
scheduleNextPoll(true)
|
||||
removeMatchedRowsLocally([row])
|
||||
queuePushResult.value = matchedRows.length > 1
|
||||
? '任务 ' + taskVo.taskId + ' 已入队,等待完成后继续下一条(' + (index + 1) + '/' + matchedRows.length + ')'
|
||||
: '任务 ' + taskVo.taskId + ' 已入队,等待执行完成'
|
||||
queuePushResult.value = '任务 ' + taskVo.taskId + ' 已入队,等待执行完成'
|
||||
const finalStatus = await waitForTaskTerminal(taskVo.taskId)
|
||||
if (finalStatus !== 'SUCCESS') {
|
||||
failedCount += 1
|
||||
queuePushResult.value = '任务 ' + taskVo.taskId + ' 执行失败,已自动继续下一条(' + (index + 1) + '/' + matchedRows.length + ')'
|
||||
queuePushResult.value = '任务 ' + taskVo.taskId + ' 执行失败,已自动继续下一条'
|
||||
ElMessage.error(queuePushResult.value)
|
||||
continue
|
||||
}
|
||||
successCount += 1
|
||||
queuePushResult.value = index + 1 < matchedRows.length
|
||||
? '任务 ' + taskVo.taskId + ' 已完成,继续推送下一条(' + (index + 1) + '/' + matchedRows.length + ')'
|
||||
: '任务 ' + taskVo.taskId + ' 已完成'
|
||||
queuePushResult.value = '任务 ' + taskVo.taskId + ' 已完成'
|
||||
} catch (error) {
|
||||
failedCount += 1
|
||||
queuePushResult.value = '第 ' + (index + 1) + '/' + matchedRows.length + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
||||
queuePushResult.value = '第 ' + index + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
||||
ElMessage.error(queuePushResult.value)
|
||||
removeMatchedRowsLocally([row])
|
||||
continue
|
||||
}
|
||||
}
|
||||
ElMessage.success('店铺任务推送已完成:成功 ' + successCount + ' 条,失败 ' + failedCount + ' 条')
|
||||
if (successCount > 0 || failedCount > 0) {
|
||||
ElMessage.success('店铺任务推送已完成:成功 ' + successCount + ' 条,失败 ' + failedCount + ' 条')
|
||||
}
|
||||
} catch (e) {
|
||||
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
||||
ElMessage.error(queuePushResult.value)
|
||||
} finally {
|
||||
queueWorkerRunning.value = false
|
||||
pushing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -229,6 +229,8 @@ const matchedItems = ref<ProductRiskShopQueueItem[]>([])
|
||||
const adding = ref(false)
|
||||
const matching = ref(false)
|
||||
const pushing = ref(false)
|
||||
const queueWorkerRunning = ref(false)
|
||||
const autoQueueEnabled = ref(false)
|
||||
const queuePushResult = ref('')
|
||||
const queuePayloadText = ref('')
|
||||
|
||||
@@ -897,6 +899,9 @@ async function runMatch() {
|
||||
const batch = res.items || []
|
||||
matchedItems.value = mergeMatchedItems(matchedItems.value, batch)
|
||||
saveMatchedItemsToStorage()
|
||||
if (autoQueueEnabled.value) {
|
||||
void processMatchedQueue()
|
||||
}
|
||||
if (!batch.length) {
|
||||
ElMessage.warning('未返回匹配结果')
|
||||
} else {
|
||||
@@ -982,6 +987,18 @@ function formatMatchRemark(row: ProductRiskShopQueueItem) {
|
||||
}
|
||||
|
||||
async function pushToPythonQueue() {
|
||||
autoQueueEnabled.value = true
|
||||
await processMatchedQueue()
|
||||
}
|
||||
|
||||
function nextMatchedQueueItem() {
|
||||
return matchedItems.value.find((item) => item.matched)
|
||||
}
|
||||
|
||||
async function processMatchedQueue() {
|
||||
if (queueWorkerRunning.value) {
|
||||
return
|
||||
}
|
||||
if (!matchedItems.value.length) {
|
||||
ElMessage.warning('请先匹配店铺')
|
||||
return
|
||||
@@ -996,13 +1013,19 @@ async function pushToPythonQueue() {
|
||||
ElMessage.warning('没有已匹配的店铺可推送')
|
||||
return
|
||||
}
|
||||
queueWorkerRunning.value = true
|
||||
pushing.value = true
|
||||
queuePayloadText.value = ''
|
||||
let successCount = 0
|
||||
let failedCount = 0
|
||||
try {
|
||||
for (let i = 0; i < toPush.length; i++) {
|
||||
const item = toPush[i]
|
||||
let index = 0
|
||||
while (autoQueueEnabled.value) {
|
||||
const item = nextMatchedQueueItem()
|
||||
if (!item) {
|
||||
break
|
||||
}
|
||||
index += 1
|
||||
try {
|
||||
const created = await createProductRiskTaskWithRetry([item])
|
||||
const taskId = created.taskId
|
||||
@@ -1031,39 +1054,40 @@ async function pushToPythonQueue() {
|
||||
if (!pushResult?.success) {
|
||||
removePollingTask(taskId)
|
||||
failedCount += 1
|
||||
queuePushResult.value = '第 ' + (i + 1) + '/' + toPush.length + ' 条推送失败:' + (pushResult?.error || '未知错误') + ',已自动继续下一条'
|
||||
queuePushResult.value = '第 ' + index + ' 条推送失败:' + (pushResult?.error || '未知错误') + ',已自动继续下一条'
|
||||
ElMessage.error(queuePushResult.value)
|
||||
removeMatchedRowsLocally([item])
|
||||
continue
|
||||
}
|
||||
removeMatchedRowsLocally([item])
|
||||
queuePushResult.value = toPush.length > 1
|
||||
? '任务 ' + taskId + ':第 ' + (i + 1) + '/' + toPush.length + ' 条店铺已入队,等待执行完成...'
|
||||
: '任务 ' + taskId + ' 已入队,等待执行完成...'
|
||||
queuePushResult.value = '任务 ' + taskId + ' 已入队,等待执行完成...'
|
||||
const finalStatus = await waitForTaskTerminal(taskId)
|
||||
if (finalStatus !== 'SUCCESS') {
|
||||
failedCount += 1
|
||||
queuePushResult.value = '任务 ' + taskId + ' 执行失败,已自动继续下一条(' + (i + 1) + '/' + toPush.length + ')'
|
||||
queuePushResult.value = '任务 ' + taskId + ' 执行失败,已自动继续下一条'
|
||||
ElMessage.error(queuePushResult.value)
|
||||
continue
|
||||
}
|
||||
successCount += 1
|
||||
queuePushResult.value = i + 1 < toPush.length
|
||||
? '任务 ' + taskId + ' 已完成,继续推送下一条(' + (i + 1) + '/' + toPush.length + ')'
|
||||
: '任务 ' + taskId + ' 已完成'
|
||||
queuePushResult.value = '任务 ' + taskId + ' 已完成'
|
||||
} catch (error) {
|
||||
failedCount += 1
|
||||
queuePushResult.value = '第 ' + (i + 1) + '/' + toPush.length + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
||||
queuePushResult.value = '第 ' + index + ' 条任务处理失败,已自动继续下一条:' + (error instanceof Error ? error.message : '未知错误')
|
||||
ElMessage.error(queuePushResult.value)
|
||||
removeMatchedRowsLocally([item])
|
||||
continue
|
||||
}
|
||||
}
|
||||
ElMessage.success('店铺推送已完成:成功 ' + successCount + ' 条,失败 ' + failedCount + ' 条')
|
||||
if (successCount > 0 || failedCount > 0) {
|
||||
ElMessage.success('店铺推送已完成:成功 ' + successCount + ' 条,失败 ' + failedCount + ' 条')
|
||||
}
|
||||
await loadHistory()
|
||||
ensurePolling(true)
|
||||
} catch (e) {
|
||||
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
|
||||
ElMessage.error(queuePushResult.value)
|
||||
} finally {
|
||||
queueWorkerRunning.value = false
|
||||
pushing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -121,6 +121,8 @@ const shopMatchListingFilter = ref<ListingFilterValue>('Active')
|
||||
const adding = ref(false)
|
||||
const matching = ref(false)
|
||||
const pushing = ref(false)
|
||||
const queueWorkerRunning = ref(false)
|
||||
const autoQueueEnabled = ref(false)
|
||||
const queuePushResult = ref('')
|
||||
const queuePayloadText = ref('')
|
||||
const orderedCountryCodes = ref<string[]>(['DE', 'UK', 'FR', 'IT', 'ES'])
|
||||
@@ -241,7 +243,7 @@ function addScheduleValue() { const last = schedulePickerValues.value[schedulePi
|
||||
function removeScheduleValue(index: number) { if (schedulePickerValues.value.length <= 1) return; schedulePickerValues.value = schedulePickerValues.value.filter((_, idx) => idx !== index) }
|
||||
function updateScheduleValue(index: number, value: string | null) { const list = [...schedulePickerValues.value]; list[index] = value || ''; schedulePickerValues.value = sanitizeSchedulePickerValues(list, false) }
|
||||
function mergeMatchedItems(base: ShopMatchShopQueueItem[], incoming: ShopMatchShopQueueItem[]) { const map = new Map<string, ShopMatchShopQueueItem>(); for (const item of base) map.set(rowKeyForMatch(item), item); for (const item of incoming) map.set(rowKeyForMatch(item), item); return Array.from(map.values()) }
|
||||
async function runMatch() { const names = selectedCandidates.value.map((item) => item.shop_name).filter(Boolean); if (!names.length) { ElMessage.warning('请先选择候选店铺'); return } matching.value = true; try { const data = await matchShopMatchShops(names); matchedItems.value = mergeMatchedItems(matchedItems.value, data.items || []); saveMatchedItemsToStorage(); ElMessage.success(`本次返回 ${data.items?.length || 0} 条匹配结果`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '匹配失败') } finally { matching.value = false } }
|
||||
async function runMatch() { const names = selectedCandidates.value.map((item) => item.shop_name).filter(Boolean); if (!names.length) { ElMessage.warning('请先选择候选店铺'); return } matching.value = true; try { const data = await matchShopMatchShops(names); matchedItems.value = mergeMatchedItems(matchedItems.value, data.items || []); saveMatchedItemsToStorage(); if (autoQueueEnabled.value) void processMatchedQueue(); ElMessage.success(`本次返回 ${data.items?.length || 0} 条匹配结果`) } catch (error) { ElMessage.error(error instanceof Error ? error.message : '匹配失败') } finally { matching.value = false } }
|
||||
function removeMatchedRow(row: ShopMatchShopQueueItem) { matchedItems.value = matchedItems.value.filter((item) => rowKeyForMatch(item) !== rowKeyForMatch(row)); saveMatchedItemsToStorage() }
|
||||
function removeMatchedRowsLocally(rows: ShopMatchHistoryItem[] | ShopMatchShopQueueItem[]) { const keys = new Set(rows.map((row) => rowKeyForMatch(row as ShopMatchShopQueueItem))); matchedItems.value = matchedItems.value.filter((item) => !keys.has(rowKeyForMatch(item))); saveMatchedItemsToStorage() }
|
||||
function parseScheduleValues() {
|
||||
@@ -289,10 +291,9 @@ function sanitizeSchedulePickerValues(values: string[], ensureOne: boolean) {
|
||||
function buildQueuePayload(taskId: number, item: Pick<ShopMatchHistoryItem, 'shopName' | 'shopId' | 'platform' | 'companyName' | 'matchStatus' | 'matchMessage'>, countryCodes: string[], stageIndex?: number, finalStage = true) { return { type: 'shop-match-run', ts: Date.now(), data: { taskId, user_id: currentUserId(), items: [{ shopName: item.shopName, shopId: item.shopId, platform: item.platform, companyName: item.companyName, matched: true, matchStatus: item.matchStatus, matchMessage: item.matchMessage }], country_codes: [...countryCodes], risk_listing_filter: shopMatchListingFilter.value, stage_index: stageIndex, final_stage: finalStage } } }
|
||||
function nextScheduleStage(taskId: number) { const task = taskSnapshots.value[taskId]?.task; const stages = task?.scheduleStages || []; const currentStageIndex = task?.currentStageIndex; if (typeof currentStageIndex !== 'number') return null; const stage = stages.find((item) => item.stageIndex === currentStageIndex); if (!stage?.scheduledAt) return null; return { stageIndex: currentStageIndex, scheduledAt: stage.scheduledAt, finalStage: currentStageIndex === stages.length - 1 } }
|
||||
async function dispatchScheduledTask(taskId: number, item: ShopMatchHistoryItem) { const api = getPywebviewApi(); if (!api?.enqueue_json) throw new Error('当前客户端未提供 enqueue_json'); const stage = nextScheduleStage(taskId); const snapshot = taskSnapshots.value[taskId]; const countryCodes = snapshot?.task?.countryCodes || orderedCountryCodes.value; if (!stage) throw new Error(`任务 ${taskId} 缺少待执行阶段`); clearDispatchTimer(taskId); await withTransientRetry(() => activateShopMatchTask(taskId, stage.stageIndex), (attempt, maxAttempts) => { queuePushResult.value = `任务 ${taskId} 等待后端恢复后再激活执行(${attempt}/${maxAttempts})...` }); taskDetails.value = { ...taskDetails.value, [taskId]: 'RUNNING' }; if (snapshot?.task) taskSnapshots.value = { ...taskSnapshots.value, [taskId]: { ...snapshot, task: { ...snapshot.task, status: 'RUNNING', activeStageIndex: stage.stageIndex } } }; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); const payload = buildQueuePayload(taskId, item, countryCodes, stage.stageIndex, stage.finalStage); queuePayloadText.value = JSON.stringify(payload, null, 2); const result = await api.enqueue_json(payload); if (!result?.success) throw new Error(result?.error || `任务 ${taskId} 推送失败`); addPollingTask(taskId); queuePushResult.value = `任务 ${taskId} 已推送第 ${stage.stageIndex + 1} 轮`; ensurePolling(true) }
|
||||
function hasRunningTask() { return Object.values(taskDetails.value).some((status) => status === 'RUNNING') || Object.values(taskSnapshots.value).some((detail) => detail.task?.status === 'RUNNING') }
|
||||
function scheduledTimestamp(value?: string) { const date = parseScheduleDateTime(value); return date ? date.getTime() : Number.NaN }
|
||||
function findNextReadyScheduledTask() { const taskIds = new Set<number>(pollingTaskIds.value); for (const key of Object.keys(taskSnapshots.value)) { const id = normalizeTaskId(key); if (id > 0) taskIds.add(id) } for (const item of historyItems.value) { const id = normalizeTaskId(item.taskId); if (id > 0) taskIds.add(id) } const ready = Array.from(taskIds).map((taskId) => { const snapshot = taskSnapshots.value[taskId]; const taskStatus = taskDetails.value[taskId] || snapshot?.task?.status; const item = snapshot?.items?.[0] || historyItems.value.find((row) => normalizeTaskId(row.taskId) === taskId); const stage = nextScheduleStage(taskId); if (taskStatus !== 'SCHEDULED' || !item || !stage?.scheduledAt) return null; const scheduledTime = scheduledTimestamp(stage.scheduledAt); if (!Number.isFinite(scheduledTime) || scheduledTime > Date.now()) return null; return { taskId, item, scheduledTime } }).filter((item): item is { taskId: number; item: ShopMatchHistoryItem; scheduledTime: number } => !!item); ready.sort((a, b) => a.scheduledTime - b.scheduledTime || a.taskId - b.taskId); return ready[0] || null }
|
||||
async function pumpScheduledQueue() { if (scheduledDispatchInFlight.value || hasRunningTask()) return; const readyTask = findNextReadyScheduledTask(); if (!readyTask) return; scheduledDispatchInFlight.value = true; try { await dispatchScheduledTask(readyTask.taskId, readyTask.item) } catch (error) { const message = error instanceof Error ? error.message : `任务 ${readyTask.taskId} 调度失败`; const key = `${readyTask.taskId}:${message}`; const now = Date.now(); queuePushResult.value = message; if (lastScheduledErrorKey !== key || now - lastScheduledErrorAt > 10000) { lastScheduledErrorKey = key; lastScheduledErrorAt = now; ElMessage.error(message) } await Promise.allSettled([loadHistory(), refreshTaskBatch()]) } finally { scheduledDispatchInFlight.value = false } }
|
||||
function findReadyScheduledTasks() { const taskIds = new Set<number>(pollingTaskIds.value); for (const key of Object.keys(taskSnapshots.value)) { const id = normalizeTaskId(key); if (id > 0) taskIds.add(id) } for (const item of historyItems.value) { const id = normalizeTaskId(item.taskId); if (id > 0) taskIds.add(id) } const ready = Array.from(taskIds).map((taskId) => { const snapshot = taskSnapshots.value[taskId]; const taskStatus = taskDetails.value[taskId] || snapshot?.task?.status; const item = snapshot?.items?.[0] || historyItems.value.find((row) => normalizeTaskId(row.taskId) === taskId); const stage = nextScheduleStage(taskId); if (taskStatus !== 'SCHEDULED' || !item || !stage?.scheduledAt) return null; const scheduledTime = scheduledTimestamp(stage.scheduledAt); if (!Number.isFinite(scheduledTime) || scheduledTime > Date.now()) return null; return { taskId, item, scheduledTime } }).filter((item): item is { taskId: number; item: ShopMatchHistoryItem; scheduledTime: number } => !!item); ready.sort((a, b) => a.scheduledTime - b.scheduledTime || a.taskId - b.taskId); return ready }
|
||||
async function pumpScheduledQueue() { if (scheduledDispatchInFlight.value) return; const readyTasks = findReadyScheduledTasks(); if (!readyTasks.length) return; scheduledDispatchInFlight.value = true; try { for (const readyTask of readyTasks) { try { await dispatchScheduledTask(readyTask.taskId, readyTask.item) } catch (error) { const message = error instanceof Error ? error.message : `任务 ${readyTask.taskId} 调度失败`; const key = `${readyTask.taskId}:${message}`; const now = Date.now(); queuePushResult.value = message; if (lastScheduledErrorKey !== key || now - lastScheduledErrorAt > 10000) { lastScheduledErrorKey = key; lastScheduledErrorAt = now; ElMessage.error(message) } await Promise.allSettled([loadHistory(), refreshTaskBatch()]) } } } finally { scheduledDispatchInFlight.value = false } }
|
||||
function scheduleDispatch(taskId: number, scheduledAt?: string) { if (!scheduledAt) return; clearDispatchTimer(taskId); const time = scheduledTimestamp(scheduledAt); if (!Number.isFinite(time)) return; const delay = Math.max(0, time - Date.now()); const timer = window.setTimeout(() => { dispatchTimers.delete(taskId); void pumpScheduledQueue() }, delay); dispatchTimers.set(taskId, timer) }
|
||||
function restoreScheduledDispatches() { const taskIds = new Set<number>(pollingTaskIds.value); for (const key of Object.keys(taskSnapshots.value)) { const id = normalizeTaskId(key); if (id > 0) taskIds.add(id) } for (const item of historyItems.value) { const id = normalizeTaskId(item.taskId); if (id > 0) taskIds.add(id) } for (const taskId of taskIds) { const snapshot = taskSnapshots.value[taskId]; const taskStatus = snapshot?.task?.status || taskDetails.value[taskId]; const stage = nextScheduleStage(taskId); if (taskStatus === 'SCHEDULED' && stage?.scheduledAt) scheduleDispatch(taskId, stage.scheduledAt); else clearDispatchTimer(taskId) } void pumpScheduledQueue() }
|
||||
function startScheduleHeartbeat() { if (scheduleHeartbeatTimer) return; scheduleHeartbeatTimer = window.setInterval(() => { void pumpScheduledQueue() }, 1000) }
|
||||
@@ -344,7 +345,9 @@ async function deleteTaskRecord(item: ShopMatchHistoryItem) {
|
||||
}
|
||||
function formatMatchStatus(status?: string) { const value = (status || '').trim(); return { MATCHED: '已匹配', PENDING: '待匹配', CONFLICT: '需人工确认', INDEX_STALE: '索引过期' }[value] || value || '—' }
|
||||
function formatMatchRemark(row: ShopMatchShopQueueItem) { const message = (row.matchMessage || '').trim(); if (message) return message; if (row.matched && row.matchStatus === 'MATCHED') return '紫鸟索引已命中,可推送队列'; if (row.matched) return '已关联索引,请结合状态列查看是否可推送'; return '未命中或未就绪,请检查店铺名与索引刷新' }
|
||||
async function pushToPythonQueue() { const api = getPywebviewApi(); if (!api?.enqueue_json) { ElMessage.error('当前客户端未提供 enqueue_json'); return } const matched = matchedItems.value.filter((item) => item.matched); if (!matched.length) { ElMessage.warning('请先匹配可用店铺'); return } if (!orderedCountryCodes.value.length) { ElMessage.warning('请至少勾选一个国家'); return } let scheduleValues: string[] | undefined; try { scheduleValues = parseScheduleValues() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '定时配置无效'); return } pushing.value = true; queuePayloadText.value = ''; let successCount = 0; let failedCount = 0; try { for (let index = 0; index < matched.length; index += 1) { const item = matched[index]; try { const created = await withTransientRetry(() => createShopMatchTask([item], orderedCountryCodes.value, scheduleValues), (attempt, maxAttempts) => { queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts})...` }); const taskId = created.taskId; const initialStatus = scheduleValues?.length ? 'SCHEDULED' : 'RUNNING'; const snapshot: ShopMatchTaskDetailVo = { task: { id: taskId, status: initialStatus, scheduledAt: scheduleValues?.[0], countryCodes: [...orderedCountryCodes.value], currentStageIndex: scheduleValues?.length ? 0 : undefined, activeStageIndex: undefined, scheduleStages: (scheduleValues || []).map((value, stageIndex) => ({ stageIndex, scheduledAt: value, status: stageIndex === 0 ? 'SCHEDULED' : 'PENDING' })) }, items: created.items }; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: snapshot }; taskDetails.value = { ...taskDetails.value, [taskId]: initialStatus }; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); const createdItem = created.items?.[0]; if (!createdItem) continue; if (scheduleValues?.length) { successCount += 1; restoreScheduledDispatches(); queuePushResult.value = `任务 ${taskId} 已创建,共 ${scheduleValues.length} 个执行时间点`; removeMatchedRowsLocally([item]); continue } const payload = buildQueuePayload(taskId, item, orderedCountryCodes.value); queuePayloadText.value = JSON.stringify(payload, null, 2); const result = await api.enqueue_json(payload); if (!result?.success) { failedCount += 1; queuePushResult.value = `任务 ${taskId} 推送失败,已自动继续下一条:${result?.error || '未知错误'}`; ElMessage.error(queuePushResult.value); continue } addPollingTask(taskId); ensurePolling(true); removeMatchedRowsLocally([item]); queuePushResult.value = matched.length > 1 ? `任务 ${taskId} 已入队,等待完成后继续推送下一条(${index + 1}/${matched.length})` : `任务 ${taskId} 已入队,等待执行完成`; const finalStatus = await waitForTaskTerminal(taskId); if (finalStatus !== 'SUCCESS') { failedCount += 1; queuePushResult.value = `任务 ${taskId} 执行失败,已自动继续下一条(${index + 1}/${matched.length})`; ElMessage.error(queuePushResult.value); continue } successCount += 1; queuePushResult.value = index + 1 < matched.length ? `任务 ${taskId} 已完成,继续推送下一条(${index + 1}/${matched.length})` : `任务 ${taskId} 已完成` } catch (error) { failedCount += 1; queuePushResult.value = `第 ${index + 1}/${matched.length} 条任务处理失败,已自动继续下一条:${error instanceof Error ? error.message : '未知错误'}`; ElMessage.error(queuePushResult.value); continue } } restoreScheduledDispatches(); if (!scheduleValues?.length) ensurePolling(true); ElMessage.success(scheduleValues?.length ? `定时任务已创建:成功 ${successCount} 条,失败 ${failedCount} 条` : `店铺推送已完成:成功 ${successCount} 条,失败 ${failedCount} 条`) } catch (error) { const message = error instanceof Error ? error.message : '推送失败'; queuePushResult.value = message; ElMessage.error(message) } finally { pushing.value = false } }
|
||||
async function pushToPythonQueue() { autoQueueEnabled.value = true; await processMatchedQueue() }
|
||||
function nextMatchedQueueItem() { return matchedItems.value.find((item) => item.matched) }
|
||||
async function processMatchedQueue() { if (queueWorkerRunning.value) return; const api = getPywebviewApi(); if (!api?.enqueue_json) { ElMessage.error('当前客户端未提供 enqueue_json'); return } const matched = matchedItems.value.filter((item) => item.matched); if (!matched.length) { ElMessage.warning('请先匹配可用店铺'); return } if (!orderedCountryCodes.value.length) { ElMessage.warning('请至少勾选一个国家'); return } let scheduleValues: string[] | undefined; try { scheduleValues = parseScheduleValues() } catch (error) { ElMessage.error(error instanceof Error ? error.message : '定时配置无效'); return } queueWorkerRunning.value = true; pushing.value = true; queuePayloadText.value = ''; let successCount = 0; let failedCount = 0; let index = 0; try { while (autoQueueEnabled.value) { const item = nextMatchedQueueItem(); if (!item) break; index += 1; try { const created = await withTransientRetry(() => createShopMatchTask([item], orderedCountryCodes.value, scheduleValues), (attempt, maxAttempts) => { queuePushResult.value = `后端服务暂时不可用,正在重试创建任务(${attempt}/${maxAttempts})...` }); const taskId = created.taskId; const initialStatus = scheduleValues?.length ? 'SCHEDULED' : 'RUNNING'; const snapshot: ShopMatchTaskDetailVo = { task: { id: taskId, status: initialStatus, scheduledAt: scheduleValues?.[0], countryCodes: [...orderedCountryCodes.value], currentStageIndex: scheduleValues?.length ? 0 : undefined, activeStageIndex: undefined, scheduleStages: (scheduleValues || []).map((value, stageIndex) => ({ stageIndex, scheduledAt: value, status: stageIndex === 0 ? 'SCHEDULED' : 'PENDING' })) }, items: created.items }; taskSnapshots.value = { ...taskSnapshots.value, [taskId]: snapshot }; taskDetails.value = { ...taskDetails.value, [taskId]: initialStatus }; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); const createdItem = created.items?.[0]; if (!createdItem) { removeMatchedRowsLocally([item]); continue } if (scheduleValues?.length) { successCount += 1; restoreScheduledDispatches(); queuePushResult.value = `任务 ${taskId} 已创建,共 ${scheduleValues.length} 个执行时间点`; removeMatchedRowsLocally([item]); continue } const payload = buildQueuePayload(taskId, item, orderedCountryCodes.value); queuePayloadText.value = JSON.stringify(payload, null, 2); const result = await api.enqueue_json(payload); if (!result?.success) { failedCount += 1; queuePushResult.value = `任务 ${taskId} 推送失败,已自动继续下一条:${result?.error || '未知错误'}`; ElMessage.error(queuePushResult.value); removeMatchedRowsLocally([item]); continue } addPollingTask(taskId); ensurePolling(true); removeMatchedRowsLocally([item]); queuePushResult.value = `任务 ${taskId} 已入队,等待执行完成`; const finalStatus = await waitForTaskTerminal(taskId); if (finalStatus !== 'SUCCESS') { failedCount += 1; queuePushResult.value = `任务 ${taskId} 执行失败,已自动继续下一条`; ElMessage.error(queuePushResult.value); continue } successCount += 1; queuePushResult.value = `任务 ${taskId} 已完成` } catch (error) { failedCount += 1; queuePushResult.value = `第 ${index} 条任务处理失败,已自动继续下一条:${error instanceof Error ? error.message : '未知错误'}`; ElMessage.error(queuePushResult.value); removeMatchedRowsLocally([item]); continue } } restoreScheduledDispatches(); if (!scheduleValues?.length) ensurePolling(true); if (successCount > 0 || failedCount > 0) ElMessage.success(scheduleValues?.length ? `定时任务已创建:成功 ${successCount} 条,失败 ${failedCount} 条` : `店铺推送已完成:成功 ${successCount} 条,失败 ${failedCount} 条`) } catch (error) { const message = error instanceof Error ? error.message : '推送失败'; queuePushResult.value = message; ElMessage.error(message) } finally { queueWorkerRunning.value = false; pushing.value = false } }
|
||||
onMounted(async () => { loadPollingIdsFromStorage(); loadTaskDetailsFromStorage(); loadTaskSnapshotsFromStorage(); loadMatchedItemsFromStorage(); schedulePickerValues.value = sanitizeSchedulePickerValues(schedulePickerValues.value, scheduleEnabled.value); syncPollingIdsWithTaskState(); await Promise.allSettled([loadCandidates(), loadCountryPreference(), loadHistory(), loadDashboard()]); startScheduleHeartbeat(); window.addEventListener('focus', handleScheduleRecovery); document.addEventListener('visibilitychange', handleScheduleRecovery); restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) })
|
||||
onUnmounted(() => { stopPolling(); stopScheduleHeartbeat(); window.removeEventListener('focus', handleScheduleRecovery); document.removeEventListener('visibilitychange', handleScheduleRecovery); if (countryPrefSaveTimer) clearTimeout(countryPrefSaveTimer); for (const timer of dispatchTimers.values()) window.clearTimeout(timer); dispatchTimers.clear() })
|
||||
</script>
|
||||
|
||||
@@ -1211,7 +1211,9 @@ export interface PriceTrackShopQueueItem {
|
||||
export interface PriceTrackMatchShopsVo {
|
||||
items: PriceTrackShopQueueItem[];
|
||||
skipAsinsByCountry?: Record<string, string[]>;
|
||||
skipAsinDetailsByCountry?: Record<string, Array<{ asin?: string; minimumPrice?: string }>>;
|
||||
asinRowsByCountry?: Record<string, PriceTrackAsinParsedRow[]>;
|
||||
minimumPriceByCountryAndAsin?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface PriceTrackCountryPreferenceVo {
|
||||
@@ -1268,7 +1270,9 @@ export interface PriceTrackCreateTaskVo {
|
||||
taskId: number;
|
||||
items: PriceTrackHistoryItem[];
|
||||
skipAsinsByCountry?: Record<string, string[]>;
|
||||
skipAsinDetailsByCountry?: Record<string, Array<{ asin?: string; minimumPrice?: string }>>;
|
||||
asinRowsByCountry?: Record<string, PriceTrackAsinParsedRow[]>;
|
||||
minimumPriceByCountryAndAsin?: Record<string, Record<string, string>>;
|
||||
}
|
||||
|
||||
export interface PriceTrackTaskSummary {
|
||||
|
||||
Reference in New Issue
Block a user