完成匹配、跟价、权限部分

This commit is contained in:
super
2026-04-17 12:52:16 +08:00
parent ecc8b6ce6c
commit d5764f3b50
63 changed files with 8858 additions and 1473 deletions

View File

@@ -216,6 +216,7 @@ import {
deleteDeleteBrandHistory,
getDeleteBrandHistory,
getDeleteBrandTaskDetails,
getDeleteBrandTaskProgress,
getDeleteBrandTaskDownloadUrl,
runDeleteBrand,
type DeleteBrandPreviewRow,
@@ -853,7 +854,7 @@ async function refreshTaskDetails(taskIds?: number[]) {
if (!ids.length) return
try {
const batch = await getDeleteBrandTaskDetails(ids)
const batch = await getDeleteBrandTaskProgress(ids)
let changed = false
for (const detail of batch.items || []) {
const id = detail?.task?.id
@@ -876,7 +877,7 @@ async function refreshTaskDetails(taskIds?: number[]) {
}
function getPollIntervalMs() {
return document.visibilityState === 'visible' ? 4500 : 10000
return document.visibilityState === 'visible' ? 6000 : 20000
}
function scheduleNextPoll(immediate = false) {

View File

@@ -8,12 +8,12 @@
<div class="section-title">跟价模式</div>
<div class="mode-zone">
<label class="mode-check-row">
<input type="checkbox" class="mode-check-input" v-model="statusModeEnabled" />
<input type="radio" name="price-track-mode" class="mode-check-input" :checked="statusModeEnabled" @change="selectMode('status')" />
<span class="mode-check-text">按商品状态全量</span>
<span class="mode-hint-inline"> 获取店铺所有在售ASIN跑跟价</span>
</label>
<label class="mode-check-row">
<input type="checkbox" class="mode-check-input" v-model="asinModeEnabled" />
<input type="radio" name="price-track-mode" class="mode-check-input" :checked="asinModeEnabled" @change="selectMode('asin')" />
<span class="mode-check-text">按指定ASIN文档自定义</span>
<span class="mode-hint-inline"> 上传ASIN表格按文档内ASIN跑跟价</span>
</label>
@@ -26,6 +26,7 @@
<div class="hint">上传包含ASIN的表格文件按文档内的ASIN进行跟价</div>
<div class="btns">
<button type="button" class="opt-btn" @click="selectAsinFile">选择ASIN文件</button>
<button type="button" class="opt-btn" @click="selectAsinFolder">选择文件夹</button>
</div>
<div class="selected-files clean-placeholder">
<template v-if="asinFiles.length">
@@ -225,6 +226,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
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 {
addPriceTrackCandidate,
@@ -237,11 +239,14 @@ import {
getPriceTrackDashboard,
getPriceTrackHistory,
getPriceTrackResultDownloadUrl,
getPriceTrackTaskProgressBatch,
getPriceTrackTasksBatch,
listPriceTrackCandidates,
matchPriceTrackShops,
putPriceTrackCountryPreference,
type PriceTrackAsinParsedRow,
type PriceTrackCandidateVo,
type PriceTrackCreateTaskVo,
type PriceTrackDashboardVo,
type PriceTrackHistoryItem,
type PriceTrackShopQueueItem,
@@ -267,6 +272,7 @@ const matching = ref(false)
const pushing = ref(false)
const queuePushResult = ref('')
const queuePayloadText = ref('')
const matchAsinRowsByCountry = ref<Record<string, PriceTrackAsinParsedRow[]>>({})
// 跟价模式(可同时启用)
const statusModeEnabled = ref(false) // 按商品状态(全量)
@@ -404,20 +410,56 @@ function saveMatchedItemsToStorage() {
// ========== 文件上传 ==========
async function selectAsinFile() {
const api = getPywebviewApi()
if (!api?.select_files) {
ElMessage.error('当前环境不支持选择文件')
if (api?.select_files) {
const result = await api.select_files({
filters: [{ name: 'Excel/CSV', extensions: 'xlsx,xls,csv' }],
multiple: true,
})
if (result?.paths?.length) {
asinFiles.value = result.paths
ElMessage.success(`已选择 ${result.paths.length} 个ASIN文件`)
}
return
}
const result = await api.select_files({ filters: [{ name: 'Excel/CSV', extensions: 'xlsx,xls,csv' }] })
if (result?.paths?.length) {
asinFiles.value = result.paths
ElMessage.success(`已选择 ${result.paths.length} 个ASIN文件`)
if (api?.select_brand_xlsx_files) {
const paths = await api.select_brand_xlsx_files()
if (!paths?.length) return
asinFiles.value = paths
ElMessage.success(`已选择 ${paths.length} 个ASIN文件`)
return
}
ElMessage.warning('当前环境不支持文件选择,请在本地客户端中打开')
}
async function selectAsinFolder() {
const api = getPywebviewApi()
if (!api?.select_brand_folder) {
ElMessage.warning('当前环境不支持文件夹选择,请在本地客户端中打开')
return
}
try {
const folder = await api.select_brand_folder()
if (!folder) return
const result = await expandBrandFolderRecursive(folder)
if (!result.success || !result.items?.length) {
ElMessage.warning(result.error || '该文件夹下没有可用的 xlsx 文件')
return
}
asinFiles.value = result.items.map((item) => item.absolutePath)
ElMessage.success(`已选择文件夹内 ${result.items.length} 个ASIN文件`)
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : '选择失败')
}
}
// ========== 国家顺序相关 ==========
function selectMode(mode: 'status' | 'asin') {
statusModeEnabled.value = mode === 'status'
asinModeEnabled.value = mode === 'asin'
}
function countryLabel(code: string) {
const row = COUNTRY_OPTIONS.find((o) => o.code === code)
return row?.label ?? code
@@ -595,6 +637,15 @@ function collectShopNamesForMatch(): string[] {
for (const r of selectedCandidates.value) {
if (r?.shopName?.trim()) set.add(r.shopName.trim())
}
if (asinModeEnabled.value) {
for (const filePath of asinFiles.value) {
const normalizedPath = (filePath || '').trim()
if (!normalizedPath) continue
const filename = normalizedPath.split(/[/\\]/).filter(Boolean).pop() || ''
const shopName = filename.replace(/\.[^.]+$/, '').trim()
if (shopName) set.add(shopName)
}
}
return [...set]
}
@@ -604,11 +655,25 @@ async function runMatch() {
ElMessage.warning('请勾选备选店铺,或在输入框中填写店名')
return
}
if (asinModeEnabled.value && !asinFiles.value.length) {
ElMessage.warning('请先选择 ASIN 文件')
return
}
matching.value = true
try {
const res = await matchPriceTrackShops(names)
const res = await matchPriceTrackShops(names, {
asinFiles: asinModeEnabled.value ? asinFiles.value : [],
countryCodes: [...orderedCountryCodes.value],
})
const batch = res.items || []
matchedItems.value = [...matchedItems.value, ...batch.filter((n) => !matchedItems.value.some((e) => e.shopName === n.shopName))]
matchAsinRowsByCountry.value = res.asinRowsByCountry || {}
const nextByShop = new Map(
matchedItems.value.map((item) => [((item.shopName || '').trim()), item] as const),
)
for (const item of batch) {
nextByShop.set((item.shopName || '').trim(), item)
}
matchedItems.value = [...nextByShop.values()]
saveMatchedItemsToStorage()
ElMessage.success(`匹配 ${batch.length} 个店铺`)
} catch (e) {
@@ -668,11 +733,15 @@ async function removeMatchedRow(row: PriceTrackShopQueueItem) {
}
}
async function pushToPythonQueue() {
async function pushToPythonQueueLegacy() {
if (!hasValidMode.value) {
ElMessage.warning('请至少选择一个跟价模式')
return
}
if (asinModeEnabled.value && !asinFiles.value.length) {
ElMessage.warning('请先选择 ASIN 文件')
return
}
const api = getPywebviewApi()
if (!api?.enqueue_json) {
ElMessage.error('当前环境未启用 pywebview enqueue_json')
@@ -742,15 +811,146 @@ async function pushToPythonQueue() {
}
// ========== 任务状态轮询 ==========
function buildQueuePayload(taskVo: PriceTrackCreateTaskVo, row: PriceTrackShopQueueItem) {
const shopName = (row.shopName || '').trim()
const skipAsinsByCountry = row.skipAsins || taskVo.skipAsinsByCountry || {}
return {
type: 'price-track-run',
ts: Date.now(),
data: {
task_id: taskVo.taskId,
shop_name: shopName,
shop_id: row.shopId ?? null,
country_codes: [...orderedCountryCodes.value],
mode: statusModeEnabled.value ? 'status' : 'asin',
skip_asins: skipAsinsByCountry,
skip_asins_by_country: skipAsinsByCountry,
asin_rows_by_country: Object.keys(matchAsinRowsByCountry.value).length
? matchAsinRowsByCountry.value
: (taskVo.asinRowsByCountry || {}),
},
}
}
async function waitForTaskTerminal(taskId: number) {
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
}
await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) })
}
}
async function pushToPythonQueue() {
if (!hasValidMode.value) {
ElMessage.warning('请至少选择一个跟价模式')
return
}
if (asinModeEnabled.value && !asinFiles.value.length) {
ElMessage.warning('请先选择 ASIN 文件')
return
}
const api = getPywebviewApi()
if (!api?.enqueue_json) {
ElMessage.error('当前环境未启用 pywebview enqueue_json')
return
}
const matchedRows = matchedItems.value.filter((i) => i.matched)
if (!matchedRows.length) {
ElMessage.warning('无可用匹配店铺')
return
}
pushing.value = true
queuePayloadText.value = ''
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],
})
taskSnapshots.value = {
...taskSnapshots.value,
[taskVo.taskId]: {
task: { id: taskVo.taskId, status: 'RUNNING' },
items: taskVo.items,
},
}
taskDetails.value = {
...taskDetails.value,
[taskVo.taskId]: 'RUNNING',
}
saveTaskSnapshotsToStorage()
saveTaskDetailsToStorage()
const queuePayload = buildQueuePayload(taskVo, row)
queuePayloadText.value = JSON.stringify(queuePayload, null, 2)
const pushResult = await api.enqueue_json(queuePayload)
if (!pushResult?.success) {
throw new Error(pushResult?.error || `任务 ${taskVo.taskId} 推送失败`)
}
addPollingTask(taskVo.taskId)
scheduleNextPoll(true)
removeMatchedRowsLocally([row])
queuePushResult.value = matchedRows.length > 1
? `任务 ${taskVo.taskId} 已入队,等待完成后继续下一条(${index + 1}/${matchedRows.length}`
: `任务 ${taskVo.taskId} 已入队,等待执行完成`
const finalStatus = await waitForTaskTerminal(taskVo.taskId)
if (finalStatus !== 'SUCCESS') {
throw new Error(`任务 ${taskVo.taskId} 执行失败,已停止后续任务`)
}
queuePushResult.value = index + 1 < matchedRows.length
? `任务 ${taskVo.taskId} 已完成,继续推送下一条(${index + 1}/${matchedRows.length}`
: `任务 ${taskVo.taskId} 已完成`
}
ElMessage.success(`已串行完成 ${matchedRows.length} 条店铺任务推送`)
} catch (e) {
queuePushResult.value = e instanceof Error ? e.message : '推送异常'
ElMessage.error(queuePushResult.value)
} finally {
pushing.value = false
}
}
function getPollIntervalMs() {
return document.visibilityState === 'visible' ? 4500 : 10000
return document.visibilityState === 'visible' ? 6000 : 20000
}
async function refreshTaskBatch() {
const ids = pollingTaskIds.value.filter((id) => id > 0)
if (!ids.length) return
try {
const batch = await getPriceTrackTasksBatch(ids)
const batch = await getPriceTrackTaskProgressBatch(ids)
let changed = false
const nextSnapshots = { ...taskSnapshots.value }
for (const missingId of batch.missingTaskIds || []) {
@@ -761,7 +961,8 @@ async function refreshTaskBatch() {
const taskId = detail.task?.id
const status = detail.task?.status
if (typeof taskId !== 'number' || taskId <= 0) continue
nextSnapshots[taskId] = detail
const prev = nextSnapshots[taskId]
nextSnapshots[taskId] = prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail
if (status) {
taskDetails.value[taskId] = status
if (status === 'SUCCESS' || status === 'FAILED') {

View File

@@ -208,6 +208,7 @@ import {
getProductRiskDashboard,
getProductRiskHistory,
getProductRiskResultDownloadUrl,
getProductRiskTaskProgressBatch,
getProductRiskTasksBatch,
listProductRiskCandidates,
matchProductRiskShops,
@@ -641,14 +642,15 @@ async function refreshTaskBatch() {
const ids = pollingTaskIds.value.filter((id) => id > 0)
if (!ids.length) return
try {
const batch = await getProductRiskTasksBatch(ids)
const batch = await getProductRiskTaskProgressBatch(ids)
let changed = false
const nextSnapshots = { ...taskSnapshots.value }
for (const d of batch.items || []) {
const id = d.task?.id
const st = d.task?.status
if (typeof id === 'number' && id > 0) {
nextSnapshots[id] = d
const prev = nextSnapshots[id]
nextSnapshots[id] = prev ? { ...prev, task: { ...(prev.task || {}), ...(d.task || {}) } } : d
if (st) {
taskDetails.value[id] = st
if (st === 'SUCCESS' || st === 'FAILED') {
@@ -671,7 +673,7 @@ async function refreshTaskBatch() {
}
function getPollIntervalMs() {
return document.visibilityState === 'visible' ? 4500 : 10000
return document.visibilityState === 'visible' ? 6000 : 20000
}
function scheduleNextPoll(immediate = false) {
@@ -717,14 +719,15 @@ function stopPolling() {
async function waitForTaskTerminal(taskId: number) {
while (true) {
const batch = await getProductRiskTasksBatch([taskId])
const batch = await getProductRiskTaskProgressBatch([taskId])
const detail = (batch.items || []).find((d) => d.task?.id === taskId)
const status = detail?.task?.status || ''
if (detail) {
const prev = taskSnapshots.value[taskId]
taskSnapshots.value = {
...taskSnapshots.value,
[taskId]: detail,
[taskId]: prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail,
}
saveTaskSnapshotsToStorage()
}

View File

@@ -49,11 +49,11 @@
<label class="schedule-switch"><input v-model="scheduleEnabled" type="checkbox" :disabled="pushing" @change="onScheduleToggle" /><span>启用多时间点调度</span></label>
<div v-if="scheduleEnabled" class="schedule-config">
<div v-for="(value, index) in schedulePickerValues" :key="`schedule-${index}`" class="schedule-row">
<el-time-picker :model-value="value" value-format="HH:mm:ss" format="HH:mm" placeholder="选择执行时间" :disabled="pushing" :disabled-hours="disablePastScheduleHours" :disabled-minutes="disablePastScheduleMinutes" :disabled-seconds="disableScheduleSeconds" class="schedule-picker" @update:model-value="updateScheduleValue(index, $event)" />
<el-date-picker :model-value="value" type="datetime" value-format="YYYY-MM-DD HH:mm:ss" format="MM-DD HH:mm" placeholder="选择执行时间" :disabled="pushing" class="schedule-picker" @update:model-value="updateScheduleValue(index, $event)" />
<button type="button" class="schedule-remove" :disabled="pushing || schedulePickerValues.length <= 1" @click="removeScheduleValue(index)">删除</button>
</div>
<button type="button" class="schedule-add" :disabled="pushing" @click="addScheduleValue">添加时间点</button>
<p class="hint schedule-hint">默认开启定时只需选择 24 小时制的时分系统会自动按最近可执行时间顺序处理</p>
<p class="hint schedule-hint">默认开启定时支持按 `MM-DD HH:mm` 配置多个时间点前端不再限制选择更早时间实际是否可执行以后端校验为准</p>
</div>
<div class="run-row">
<button type="button" class="btn-run" :disabled="matching" @click="runMatch">{{ matching ? '匹配中...' : '匹配店铺' }}</button>
@@ -108,7 +108,7 @@ import { computed, onMounted, onUnmounted, ref } from 'vue'
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 { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
import { activateShopMatchTask, addShopMatchCandidate, createShopMatchTask, deleteShopMatchCandidate, deleteShopMatchHistory, deleteShopMatchTask, getShopMatchCountryPreference, getShopMatchDashboard, getShopMatchHistory, getShopMatchResultDownloadUrl, getShopMatchTaskProgressBatch, getShopMatchTasksBatch, listShopMatchCandidates, matchShopMatchShops, putShopMatchCountryPreference, type ShopMatchCandidateVo, type ShopMatchDashboardVo, type ShopMatchHistoryItem, type ShopMatchShopQueueItem, type ShopMatchTaskDetailVo } from '@/shared/api/java-modules'
import { getPywebviewApi } from '@/shared/bridges/pywebview'
const COUNTRY_OPTIONS = [{ code: 'DE', label: '德国' }, { code: 'UK', label: '英国' }, { code: 'FR', label: '法国' }, { code: 'IT', label: '意大利' }, { code: 'ES', label: '西班牙' }] as const
@@ -128,7 +128,7 @@ const countryPrefSaving = ref(false)
const countryPrefUserTouched = ref(false)
let countryPrefSaveTimer: ReturnType<typeof setTimeout> | null = null
const scheduleEnabled = ref(true)
const schedulePickerValues = ref<string[]>([getMinimumScheduleTime()])
const schedulePickerValues = ref<string[]>([getDefaultScheduleValue()])
const dashboard = ref<ShopMatchDashboardVo>({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 0, failedTaskCount: 0 })
const historyItems = ref<ShopMatchHistoryItem[]>([])
const pollingTaskIds = ref<number[]>([])
@@ -158,20 +158,27 @@ function rowKeyForMatch(row: ShopMatchShopQueueItem) { return `${(row.shopName |
function taskRowKey(item: ShopMatchHistoryItem) { return `${normalizeTaskId(item.taskId)}-${item.resultId || 0}-${item.shopName || ''}` }
function countryLabel(code: string) { return COUNTRY_OPTIONS.find((item) => item.code === code)?.label || code }
function formatScheduleDateTime(date: Date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}` }
function formatScheduleTime(date: Date) { const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); const seconds = String(date.getSeconds()).padStart(2, '0'); return `${hours}:${minutes}:${seconds}` }
function getMinimumScheduleDate() { const date = new Date(); date.setSeconds(0, 0); date.setMinutes(date.getMinutes() + 1); return date }
function getMinimumScheduleTime() { return formatScheduleTime(getMinimumScheduleDate()) }
function parseTimeParts(value: string) { const matched = value.match(/^(\d{2}):(\d{2})(?::(\d{2}))?$/); if (!matched) return null; const hours = Number(matched[1]); const minutes = Number(matched[2]); const seconds = Number(matched[3] || '0'); if (hours > 23 || minutes > 59 || seconds > 59) return null; return { hours, minutes, seconds } }
function buildTodayTime(parts: { hours: number; minutes: number; seconds: number }) { const candidate = new Date(); candidate.setHours(parts.hours, parts.minutes, parts.seconds, 0); return candidate }
function resolveScheduleDateTime(value: string) { const parts = parseTimeParts(value); if (!parts) return null; const minimum = getMinimumScheduleDate(); const candidate = new Date(); candidate.setHours(parts.hours, parts.minutes, parts.seconds, 0); if (candidate.getTime() < minimum.getTime()) candidate.setDate(candidate.getDate() + 1); return candidate }
function nextMinuteString(offsetMinutes: number) { const date = getMinimumScheduleDate(); date.setMinutes(date.getMinutes() + Math.max(0, offsetMinutes - 1)); return formatScheduleTime(date) }
function nextMinuteAfter(value: string, offsetMinutes: number) { const date = resolveScheduleDateTime(value); if (!date) return nextMinuteString(offsetMinutes); date.setMinutes(date.getMinutes() + offsetMinutes); return formatScheduleTime(date) }
function normalizeScheduleValue(value: string | null | undefined, showWarning = false) { if (!value) return ''; const parts = parseTimeParts(value); if (!parts) return value; const minimum = getMinimumScheduleDate(); const candidate = buildTodayTime(parts); if (candidate.getTime() < minimum.getTime()) { if (showWarning) ElMessage.warning('执行时间不能早于当前时间,已自动调整到当前时间后 1 分钟'); return formatScheduleTime(minimum) } return formatScheduleTime(candidate) }
function disablePastScheduleHours() { const minimum = getMinimumScheduleDate(); return Array.from({ length: minimum.getHours() }, (_, index) => index) }
function disablePastScheduleMinutes(selectedHour: number) { const minimum = getMinimumScheduleDate(); if (selectedHour !== minimum.getHours()) return []; return Array.from({ length: minimum.getMinutes() }, (_, index) => index) }
function disableScheduleSeconds() { return Array.from({ length: 59 }, (_, index) => index + 1) }
function formatPickerDateTime(date: Date) { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}:00` }
function getDefaultScheduleValue() { const date = new Date(); date.setSeconds(0, 0); date.setMinutes(date.getMinutes() + 1); return formatPickerDateTime(date) }
function parseScheduleDateTime(value?: string | null) {
if (!value) return null
const normalized = value.trim().replace('T', ' ')
const matched = normalized.match(/^(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2})(?::(\d{2}))?$/)
if (!matched) return null
const year = Number(matched[1])
const month = Number(matched[2])
const day = Number(matched[3])
const hours = Number(matched[4])
const minutes = Number(matched[5])
const seconds = Number(matched[6] || '0')
const date = new Date(year, month - 1, day, hours, minutes, seconds, 0)
if (Number.isNaN(date.getTime())) return null
if (date.getFullYear() !== year || date.getMonth() !== month - 1 || date.getDate() !== day) return null
return date
}
function nextScheduleValueAfter(value: string, offsetMinutes: number) { const date = parseScheduleDateTime(value) || new Date(); date.setSeconds(0, 0); date.setMinutes(date.getMinutes() + offsetMinutes); return formatPickerDateTime(date) }
function formatDateTime(value?: string) { return value ? value.replace('T', ' ').slice(0, 19) : '-' }
function formatTimeOnly(value?: string) { return value ? value.slice(11, 16) : '-' }
function formatMonthDayTime(value?: string) { const date = parseScheduleDateTime(value); if (!date) return '-'; const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${month}-${day} ${hours}:${minutes}` }
function uniqueTaskRows(rows: ShopMatchHistoryItem[], snapshots: Record<number, ShopMatchTaskDetailVo>, section: 'current' | 'history') { const map = new Map<string, ShopMatchHistoryItem>(); for (const row of rows) map.set(taskRowKey(row), row); for (const [taskIdText, snapshot] of Object.entries(snapshots)) { const taskId = normalizeTaskId(taskIdText); const first = snapshot.items?.[0]; if (!first || !taskId) continue; const terminal = isTaskTerminalById(taskId); if ((section === 'current' && terminal) || (section === 'history' && !terminal)) continue; const key = taskRowKey({ ...first, taskId }); if (!map.has(key)) map.set(key, { ...first, taskId }) } return Array.from(map.values()) }
function loadPollingIdsFromStorage() { try { const raw = window.localStorage.getItem(sessionKey()); const parsed = raw ? JSON.parse(raw) : []; pollingTaskIds.value = Array.isArray(parsed) ? parsed.filter((item): item is number => typeof item === 'number' && item > 0) : [] } catch { pollingTaskIds.value = [] } }
function savePollingIds() { setStorageJson(sessionKey(), pollingTaskIds.value, pollingTaskIds.value.length === 0) }
@@ -228,36 +235,37 @@ function onCountryToggle(code: string, checked: boolean) { countryPrefUserTouche
function onCountryDragStart(index: number) { dragCountryIndex.value = index }
function onCountryDragEnd() { dragCountryIndex.value = null }
function onCountryDrop(toIndex: number) { const from = dragCountryIndex.value; dragCountryIndex.value = null; if (from == null || from === toIndex) return; countryPrefUserTouched.value = true; const list = [...orderedCountryCodes.value]; const [item] = list.splice(from, 1); list.splice(toIndex, 0, item); orderedCountryCodes.value = list; scheduleSaveCountryPreference() }
function onScheduleToggle() { if (scheduleEnabled.value && schedulePickerValues.value.length === 0) schedulePickerValues.value = [getMinimumScheduleTime()]; if (!scheduleEnabled.value) schedulePickerValues.value = [] }
function addScheduleValue() { const last = schedulePickerValues.value[schedulePickerValues.value.length - 1]; const nextValue = last ? nextMinuteAfter(last, 60) : getMinimumScheduleTime(); schedulePickerValues.value = [...schedulePickerValues.value, normalizeScheduleValue(nextValue)] }
function onScheduleToggle() { if (scheduleEnabled.value && schedulePickerValues.value.length === 0) schedulePickerValues.value = [getDefaultScheduleValue()]; if (!scheduleEnabled.value) schedulePickerValues.value = [] }
function addScheduleValue() { const last = schedulePickerValues.value[schedulePickerValues.value.length - 1]; const nextValue = last ? nextScheduleValueAfter(last, 60) : getDefaultScheduleValue(); schedulePickerValues.value = [...schedulePickerValues.value, nextValue] }
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 ? normalizeScheduleValue(value, true) : ''; schedulePickerValues.value = list }
function updateScheduleValue(index: number, value: string | null) { const list = [...schedulePickerValues.value]; list[index] = value || ''; schedulePickerValues.value = list }
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 } }
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() { if (!scheduleEnabled.value) return undefined; const values = schedulePickerValues.value.map((item) => item.trim()).filter(Boolean); if (!values.length) throw new Error('请至少选择一个执行时间'); const withTime = values.map((value) => { const normalized = normalizeScheduleValue(value); const date = resolveScheduleDateTime(normalized); if (!date) throw new Error(`时间格式无效: ${value}`); return { raw: formatScheduleDateTime(date), time: date.getTime() } }).sort((a, b) => a.time - b.time); for (let i = 1; i < withTime.length; i += 1) if (withTime[i].time === withTime[i - 1].time) throw new Error('执行时间不能重复'); return withTime.map((item) => item.raw) }
function parseScheduleValues() { if (!scheduleEnabled.value) return undefined; const values = schedulePickerValues.value.map((item) => item.trim()).filter(Boolean); if (!values.length) throw new Error('请至少选择一个执行时间'); const withTime = values.map((value) => { const date = parseScheduleDateTime(value); if (!date) throw new Error(`时间格式无效: ${value}`); return { raw: formatScheduleDateTime(date), time: date.getTime() } }).sort((a, b) => a.time - b.time); for (let i = 1; i < withTime.length; i += 1) if (withTime[i].time === withTime[i - 1].time) throw new Error('执行时间不能重复'); return withTime.map((item) => item.raw) }
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 activateShopMatchTask(taskId, stage.stageIndex); 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 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 = new Date(stage.scheduledAt).getTime(); 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 }
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 scheduleDispatch(taskId: number, scheduledAt?: string) { if (!scheduledAt) return; clearDispatchTimer(taskId); const time = new Date(scheduledAt).getTime(); 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 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) }
function stopScheduleHeartbeat() { if (!scheduleHeartbeatTimer) return; window.clearInterval(scheduleHeartbeatTimer); scheduleHeartbeatTimer = null }
function handleScheduleRecovery() { restoreScheduledDispatches(); if (pollingTaskIds.value.length) ensurePolling(true) }
async function refreshTaskBatch() { if (!pollingTaskIds.value.length) return { settledTaskIds: [] as number[] }; try { const batch = await getShopMatchTasksBatch(pollingTaskIds.value); const nextSnapshots = { ...taskSnapshots.value }; const settledTaskIds = new Set<number>(); for (const missingId of batch.missingTaskIds || []) { removeTaskLocally(missingId); delete nextSnapshots[missingId]; settledTaskIds.add(missingId) } for (const detail of batch.items || []) { const taskId = detail.task?.id; if (!taskId) continue; nextSnapshots[taskId] = detail; const status = detail.task?.status || ''; if (!status) continue; taskDetails.value[taskId] = status; if (isTaskTerminalStatus(status)) { removePollingTask(taskId); settledTaskIds.add(taskId); continue } if (status === 'SCHEDULED') { stopPollingTask(taskId); settledTaskIds.add(taskId) } } taskSnapshots.value = nextSnapshots; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); restoreScheduledDispatches(); return { settledTaskIds: Array.from(settledTaskIds) } } catch { return { settledTaskIds: [] as number[] } } }
function getPollIntervalMs() { return document.visibilityState === 'visible' ? 4000 : 10000 }
async function refreshTaskBatch() { if (!pollingTaskIds.value.length) return { settledTaskIds: [] as number[] }; try { const batch = await getShopMatchTaskProgressBatch(pollingTaskIds.value); const nextSnapshots = { ...taskSnapshots.value }; const settledTaskIds = new Set<number>(); for (const missingId of batch.missingTaskIds || []) { removeTaskLocally(missingId); delete nextSnapshots[missingId]; settledTaskIds.add(missingId) } for (const detail of batch.items || []) { const taskId = detail.task?.id; if (!taskId) continue; const prev = nextSnapshots[taskId]; nextSnapshots[taskId] = prev ? { ...prev, task: { ...(prev.task || {}), ...(detail.task || {}) } } : detail; const status = detail.task?.status || ''; if (!status) continue; taskDetails.value[taskId] = status; if (isTaskTerminalStatus(status)) { removePollingTask(taskId); settledTaskIds.add(taskId); continue } if (status === 'SCHEDULED') { stopPollingTask(taskId); settledTaskIds.add(taskId) } } taskSnapshots.value = nextSnapshots; saveTaskSnapshotsToStorage(); saveTaskDetailsToStorage(); restoreScheduledDispatches(); return { settledTaskIds: Array.from(settledTaskIds) } } catch { return { settledTaskIds: [] as number[] } } }
function getPollIntervalMs() { return document.visibilityState === 'visible' ? 6000 : 20000 }
function scheduleNextPoll(immediate = false) { if (pollTimer.value) { if (!immediate) return; window.clearTimeout(pollTimer.value); pollTimer.value = null } const run = async () => { pollTimer.value = null; if (pollingInFlight.value || !pollingTaskIds.value.length) { if (pollingTaskIds.value.length) scheduleNextPoll(); return } pollingInFlight.value = true; try { const { settledTaskIds } = await refreshTaskBatch(); if (settledTaskIds.length) await Promise.allSettled([loadHistory(), loadDashboard()]) } finally { pollingInFlight.value = false } if (pollingTaskIds.value.length) pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }; if (immediate) void run(); else pollTimer.value = window.setTimeout(run, getPollIntervalMs()) }
function ensurePolling(immediate = false) { scheduleNextPoll(immediate) }
function stopPolling() { if (pollTimer.value) { window.clearTimeout(pollTimer.value); pollTimer.value = null } }
async function waitForTaskTerminal(taskId: number) { while (true) { const batch = await getShopMatchTasksBatch([taskId]); if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return 'FAILED' } const detail = (batch.items || []).find((item) => item.task?.id === taskId); const status = detail?.task?.status || ''; if (detail) { taskSnapshots.value = { ...taskSnapshots.value, [taskId]: detail }; saveTaskSnapshotsToStorage() } if (status) { taskDetails.value[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) }) } }
async function waitForTaskTerminal(taskId: number) { while (true) { const batch = await getShopMatchTaskProgressBatch([taskId]); if ((batch.missingTaskIds || []).includes(taskId)) { removeTaskLocally(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); 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[taskId] = status; saveTaskDetailsToStorage() } if (isTaskTerminalStatus(status)) { removePollingTask(taskId); await refreshTaskViewsBestEffort(); restoreScheduledDispatches(); return status } await new Promise<void>((resolve) => { window.setTimeout(() => resolve(), getPollIntervalMs()) }) } }
function resolvedTaskStatus(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); if (!taskId) return item.taskStatus || ''; return taskDetails.value[taskId] || taskSnapshots.value[taskId]?.task?.status || item.taskStatus || '' }
function taskSnapshotOf(item: ShopMatchHistoryItem) { const taskId = normalizeTaskId(item.taskId); return taskId ? taskSnapshots.value[taskId] : undefined }
function currentTaskStageText(item: ShopMatchHistoryItem) { const snapshot = taskSnapshotOf(item); const task = snapshot?.task; const stages = task?.scheduleStages || []; if (!stages.length) return ''; const total = stages.length; const activeIndex = typeof task?.activeStageIndex === 'number' ? task.activeStageIndex : undefined; const currentIndex = typeof task?.currentStageIndex === 'number' ? task.currentStageIndex : undefined; if (typeof activeIndex === 'number') return `执行进度: 第 ${activeIndex + 1}/${total} 次执行中`; if (typeof currentIndex === 'number') return `执行进度: 等待第 ${currentIndex + 1}/${total}`; return `执行进度: 共 ${total}`}
function nextScheduledDisplay(item: ShopMatchHistoryItem) { const snapshot = taskSnapshotOf(item); const task = snapshot?.task; const stages = task?.scheduleStages || []; if (typeof task?.currentStageIndex === 'number') { const stage = stages.find((entry) => entry.stageIndex === task.currentStageIndex); if (stage?.scheduledAt) return formatTimeOnly(stage.scheduledAt) } if (item.scheduledAt) return formatTimeOnly(item.scheduledAt); return '' }
function nextScheduledDisplay(item: ShopMatchHistoryItem) { const snapshot = taskSnapshotOf(item); const task = snapshot?.task; const stages = task?.scheduleStages || []; if (typeof task?.currentStageIndex === 'number') { const stage = stages.find((entry) => entry.stageIndex === task.currentStageIndex); if (stage?.scheduledAt) return formatMonthDayTime(stage.scheduledAt) } if (item.scheduledAt) return formatMonthDayTime(item.scheduledAt); return '' }
function statusText(item: ShopMatchHistoryItem) { const status = resolvedTaskStatus(item); if (status === 'SCHEDULED') return '待执行'; if (status === 'RUNNING') return '执行中'; if (status === 'SUCCESS' || status === 'COMPLETED') return '已完成'; if (status === 'FAILED') return '失败'; return item.success ? '已完成' : '未知' }
function statusClass(item: ShopMatchHistoryItem) { const status = resolvedTaskStatus(item); return status === 'SUCCESS' || status === 'COMPLETED' ? 'success' : status === 'FAILED' ? 'failed' : 'running' }
function canDownload(item: ShopMatchHistoryItem) { const status = resolvedTaskStatus(item); return !!item.resultId && !!item.downloadUrl && (status === 'SUCCESS' || status === 'COMPLETED') }

View File

@@ -931,10 +931,13 @@ export interface PriceTrackShopQueueItem {
matchMessage?: string;
platform?: string;
companyName?: string;
skipAsins?: Record<string, string[]>;
}
export interface PriceTrackMatchShopsVo {
items: PriceTrackShopQueueItem[];
skipAsinsByCountry?: Record<string, string[]>;
asinRowsByCountry?: Record<string, PriceTrackAsinParsedRow[]>;
}
export interface PriceTrackCountryPreferenceVo {
@@ -970,9 +973,25 @@ export interface PriceTrackHistoryVo {
items: PriceTrackHistoryItem[];
}
export interface PriceTrackAsinParsedRow {
shopMallName?: string;
asin?: string;
price?: string;
recommendedPrice?: string;
minimumPrice?: string;
firstPlace?: string;
secondPlace?: string;
cartShopName?: string;
priceChangeStatus?: string;
modifyCount?: string;
status?: string;
}
export interface PriceTrackCreateTaskVo {
taskId: number;
items: PriceTrackHistoryItem[];
skipAsinsByCountry?: Record<string, string[]>;
asinRowsByCountry?: Record<string, PriceTrackAsinParsedRow[]>;
}
export interface PriceTrackTaskSummary {
@@ -1046,11 +1065,52 @@ export function putPriceTrackCountryPreference(countryCodes: string[]) {
);
}
export function matchPriceTrackShops(shopNames: string[]) {
export function matchPriceTrackShops(
shopNames: string[],
options?: {
asinFiles?: string[];
countryCodes?: string[];
},
) {
return unwrapJavaResponse(
post<JavaApiResponse<PriceTrackMatchShopsVo>, { userId: number; shopNames: string[] }>(
post<
JavaApiResponse<PriceTrackMatchShopsVo>,
{ userId: number; shopNames: string[]; asinFiles?: string[]; countryCodes?: string[] }
>(
`${JAVA_API_PREFIX}/price-track/match-shops`,
{ userId: getCurrentUserId(), shopNames },
{
userId: getCurrentUserId(),
shopNames,
asinFiles: options?.asinFiles || [],
countryCodes: options?.countryCodes || [],
},
),
);
}
export function getShopMatchTaskProgressBatch(taskIds: number[]) {
return unwrapJavaResponse(
post<JavaApiResponse<ShopMatchTaskBatchVo>, { taskIds: number[] }>(
`${JAVA_API_PREFIX}/shop-match/tasks/progress/batch`,
{ taskIds },
),
);
}
export function getProductRiskTaskProgressBatch(taskIds: number[]) {
return unwrapJavaResponse(
post<JavaApiResponse<ProductRiskTaskBatchVo>, { taskIds: number[] }>(
`${JAVA_API_PREFIX}/product-risk-resolve/tasks/progress/batch`,
{ taskIds },
),
);
}
export function getDeleteBrandTaskProgress(taskIds: number[]) {
return unwrapJavaResponse(
post<JavaApiResponse<DeleteBrandTaskBatchVo>, { taskIds: number[] }>(
`${JAVA_API_PREFIX}/delete-brand/tasks/progress/batch`,
{ taskIds },
),
);
}
@@ -1116,6 +1176,15 @@ export function getPriceTrackTasksBatch(taskIds: number[]) {
);
}
export function getPriceTrackTaskProgressBatch(taskIds: number[]) {
return unwrapJavaResponse(
post<JavaApiResponse<PriceTrackTaskBatchVo>, { taskIds: number[] }>(
`${JAVA_API_PREFIX}/price-track/tasks/progress/batch`,
{ taskIds },
),
);
}
export function getPriceTrackResultDownloadUrl(resultId: number) {
return getJavaDownloadUrl(`/price-track/results/${resultId}/download`);
}