diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 87d49cba..f8dbae53 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -17,6 +17,7 @@ export * from "./types/modules/publish.ts"; export * from "./types/modules/similar-asin.ts"; export * from "./types/modules/appearance-patent.ts"; export * from "./types/modules/delete-brand.ts"; +export * from "./types/modules/price-track.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -1444,195 +1445,6 @@ export function deleteWithdrawHistory(resultId: number) { ); } -// ========== 跟价 ========== - -export interface PriceTrackCandidateVo { - id: number; - shopName: string; -} - -export interface PriceTrackShopQueueItem { - shopName?: string; - shopMallName?: string; - shopId?: number | string | null; - matched?: boolean; - matchStatus?: string; - matchMessage?: string; - platform?: string; - companyName?: string; - skipAsins?: Record; -} - -export interface PriceTrackMatchShopsVo { - items: PriceTrackShopQueueItem[]; - skipAsinsByCountry?: Record; - skipAsinDetailsByCountry?: Record>; - asinRowsByCountry?: Record; - minimumPriceByCountryAndAsin?: Record>; -} - -export interface PriceTrackCountryPreferenceVo { - userId: number; - countryCodes: string[]; -} - -export interface PriceTrackDashboardVo { - candidateCount: number; - processedTaskCount: number; - successTaskCount: number; - failedTaskCount: number; -} - -export interface PriceTrackHistoryItem { - resultId?: number; - taskId?: number; - loopRunId?: number; - roundIndex?: number; - shopName?: string; - shopMallName?: string; - shopId?: number | string | null; - platform?: string; - companyName?: string; - matched?: boolean; - matchStatus?: string; - matchMessage?: string; - taskStatus?: string; - outputFilename?: string; - downloadUrl?: string; - fileJobId?: number; - fileStatus?: string; - fileError?: string; - fileReady?: boolean; - error?: string; - success?: boolean; -} - -export interface PriceTrackHistoryVo { - items: PriceTrackHistoryItem[]; -} - -export interface PriceTrackAsinParsedRow { - shopMallName?: string; - asin?: string; - price?: string; - recommendedPrice?: string; - shippingFee?: string; - minimumPrice?: string; - firstPlace?: string; - firstShop?: string; - secondPlace?: string; - secondShop?: string; - cartShopName?: string; - priceChangeStatus?: string; - modifyCount?: string; - status?: string; -} - -export interface PriceTrackCreateTaskVo { - taskId: number; - items: PriceTrackHistoryItem[]; - skipAsinsByCountry?: Record; - skipAsinDetailsByCountry?: Record>; - asinRowsByCountry?: Record; - minimumPriceByCountryAndAsin?: Record>; -} - -export interface PriceTrackTaskSummary { - id?: number; - taskNo?: string; - status?: string; - loopRunId?: number; - roundIndex?: number; - errorMessage?: string; - createdAt?: string; - updatedAt?: string; - finishedAt?: string; -} - -export interface PriceTrackTaskDetailVo { - task?: PriceTrackTaskSummary; - items?: PriceTrackHistoryItem[]; -} - -export interface PriceTrackTaskBatchVo { - items: PriceTrackTaskDetailVo[]; - missingTaskIds?: number[]; -} - -export interface PriceTrackPendingDeleteVo { - removed: boolean; -} - -export type PriceTrackCreateTaskPayload = Omit< - PriceTrackCreateTaskRequest, - "userId" ->; - -export function listPriceTrackCandidates() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/candidates?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function addPriceTrackCandidate(shopName: string) { - return unwrapJavaResponse( - post, { userId: number; shopName: string }>( - `${JAVA_API_PREFIX}/price-track/candidates`, - { userId: getCurrentUserId(), shopName }, - ), - ); -} - -export function deletePriceTrackCandidate(id: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/price-track/candidates/${id}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function getPriceTrackCountryPreference() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/country-preference?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function putPriceTrackCountryPreference(countryCodes: string[]) { - return unwrapJavaResponse( - put, { userId: number; countryCodes: string[] }>( - `${JAVA_API_PREFIX}/price-track/country-preference`, - { userId: getCurrentUserId(), countryCodes }, - ), - ); -} - -export function matchPriceTrackShops( - shopNames: string[], - options?: { - asinFiles?: string[]; - countryCodes?: string[]; - }, -) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { userId: number; shopNames: string[]; asinFiles?: string[]; countryCodes?: string[] } - >( - `${JAVA_API_PREFIX}/price-track/match-shops`, - { - userId: getCurrentUserId(), - shopNames, - asinFiles: options?.asinFiles || [], - countryCodes: options?.countryCodes || [], - }, - ), - ); -} - export function getShopMatchTaskProgressBatch(taskIds: number[]) { return postTaskProgressBatch( `${JAVA_API_PREFIX}/shop-match/tasks/progress/batch`, @@ -1647,231 +1459,6 @@ export function getProductRiskTaskProgressBatch(taskIds: number[]) { ); } -export function getPriceTrackDashboard() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/dashboard?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function getPriceTrackHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/history?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function deletePriceTrackHistory(resultId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/price-track/history/${resultId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export interface PriceTrackCreateTaskRequest { - userId: number; - statusMode: boolean; - asinMode: boolean; - items: PriceTrackShopQueueItem[]; - asinFiles: string[]; - countryCodes: string[]; - loopRunId?: number; - roundIndex?: number; - shopIndex?: number; -} - -export type PriceTrackExecutionMode = "FINITE" | "INFINITE"; - -export interface PriceTrackLoopRunCreateRequest { - userId: number; - statusMode: boolean; - asinMode: boolean; - items: PriceTrackShopQueueItem[]; - asinFiles: string[]; - countryCodes: string[]; - executionMode: PriceTrackExecutionMode; - targetRounds?: number; -} - -export interface PriceTrackLoopRunVo { - id: number; - status?: string; - executionMode?: PriceTrackExecutionMode; - targetRounds?: number; - currentRound?: number; - currentShopIndex?: number; - totalShopCount?: number; - activeTaskId?: number; - activeTaskStatus?: string; - stopRequested?: boolean; - errorMessage?: string; - statusMode?: boolean; - asinMode?: boolean; - asinFiles?: string[]; - countryCodes?: string[]; -} - -export interface PriceTrackLoopRunDispatchVo { - loopRun?: PriceTrackLoopRunVo; - childTaskRequest?: PriceTrackCreateTaskRequest; -} - -export function createPriceTrackTask( - request: PriceTrackCreateTaskPayload | PriceTrackCreateTaskRequest, -) { - return unwrapJavaResponse( - post, PriceTrackCreateTaskRequest>( - `${JAVA_API_PREFIX}/price-track/tasks`, - { ...request, userId: getCurrentUserId() }, - ), - ); -} - -export function deletePriceTrackTask(taskId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/price-track/tasks/${taskId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function getPriceTrackTasksBatch(taskIds: number[]) { - return unwrapJavaResponse( - post, { taskIds: number[] }>( - `${JAVA_API_PREFIX}/price-track/tasks/batch`, - { taskIds }, - ), - ); -} - -export function createPriceTrackLoopRun( - request: Omit | PriceTrackLoopRunCreateRequest, -) { - return unwrapJavaResponse( - post, PriceTrackLoopRunCreateRequest>( - `${JAVA_API_PREFIX}/price-track/loop-runs`, - { ...request, userId: getCurrentUserId() }, - ), - ); -} - -export function getPriceTrackLoopRun(loopRunId: number) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - ), - ); -} - -export function dispatchNextPriceTrackLoopRun(loopRunId: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/dispatch-next?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - undefined, - ), - ); -} - -export function completePriceTrackLoopChild(loopRunId: number, childTaskId: number) { - return unwrapJavaResponse( - post, { childTaskId: number }>( - `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/child-finished?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - { childTaskId }, - ), - ); -} - -export function stopPriceTrackLoopRun(loopRunId: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/stop?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - undefined, - ), - ); -} - -export function getPriceTrackTaskProgressBatch(taskIds: number[]) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/price-track/tasks/progress/batch`, - taskIds, - ); -} - -export function getPriceTrackResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/price-track/results/${resultId}/download`); -} - -export function getTaskSkipPriceAsinsPaginated( - taskId: number, - page: number = 1, - pageSize: number = 1000, - options: { - shopName?: string | string[]; - countryCode?: string | string[]; - } = {}, -) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/tasks/${taskId}/skip-asins/paginated`, - { - params: { - page, - page_size: pageSize, - ...(options.shopName ? { shop_name: options.shopName } : {}), - ...(options.countryCode ? { country_code: options.countryCode } : {}), - }, - }, - ), - ); -} - -export function markPriceTrackDispatchFailed(taskId: number, errorMessage: string) { - return unwrapJavaResponse( - post, { errorMessage: string }>( - `${JAVA_API_PREFIX}/price-track/tasks/${taskId}/dispatch-failed?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - { errorMessage }, - ), - ); -} - -export function checkTaskSkipPriceAsin(taskId: number, country: string, asin: string) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/price-track/tasks/${taskId}/skip-asin/check`, - { - params: { - country, - asin, - }, - }, - ), - ); -} - -export interface SkipPriceAsinCheckVo { - country: string; - asin: string; - exists: boolean; - minimumPrice?: string; -} - -export interface SkipPriceAsinPageVo { - page: number; - pageSize: number; - total: number; - totalPages: number; - skipAsinsByCountry: Record; - skipAsinDetailsByCountry: Record>; - skip_asins?: Record; - skip_asins_by_country?: Record; - skip_asin_details_by_country?: Record>; - asin_rows_by_country?: Record>; - minimum_price_by_country_and_asin?: Record>; -} - export interface ShopMatchSkipAsinPageVo { page: number; pageSize: number; @@ -1882,14 +1469,6 @@ export interface ShopMatchSkipAsinPageVo { skipAsinDetailsByCountry: Record>; } -export function deletePendingPriceTrackShopResult(shopName: string) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/price-track/pending-shop-result?user_id=${encodeURIComponent(String(getCurrentUserId()))}&shop_name=${encodeURIComponent(shopName)}`, - ), - ); -} - // ========== 视频复刻 / 图生视频 ========== export interface ImageVideoDouyinCopyVo { diff --git a/frontend-vue/src/shared/api/types/modules/price-track.ts b/frontend-vue/src/shared/api/types/modules/price-track.ts new file mode 100644 index 00000000..67da99e4 --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/price-track.ts @@ -0,0 +1,486 @@ +import { get, post, put, del, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts' +import { buildJavaUrl, JAVA_API_PREFIX } from '../../url.ts' +import { getCurrentUserId } from '../../user.ts' +import { getJavaDownloadUrl } from '../../download-url.ts' +import { getTaskProgressCacheTtlMs, getTaskProgressTimeoutMs } from '../../../task-progress-config.ts' +import { createTaskProgressRequestCache } from '../../../task-progress-request-cache.ts' +import { API_ENDPOINTS } from '../../endpoints.ts' + +export interface PriceTrackCandidateVo { + id: number; + shopName: string; +} + +export interface PriceTrackShopQueueItem { + shopName?: string; + shopMallName?: string; + shopId?: number | string | null; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + platform?: string; + companyName?: string; + skipAsins?: Record; +} + +export interface PriceTrackMatchShopsVo { + items: PriceTrackShopQueueItem[]; + skipAsinsByCountry?: Record; + skipAsinDetailsByCountry?: Record>; + asinRowsByCountry?: Record; + minimumPriceByCountryAndAsin?: Record>; +} + +export interface PriceTrackCountryPreferenceVo { + userId: number; + countryCodes: string[]; +} + +export interface PriceTrackDashboardVo { + candidateCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; +} + +export interface PriceTrackHistoryItem { + resultId?: number; + taskId?: number; + loopRunId?: number; + roundIndex?: number; + shopName?: string; + shopMallName?: string; + shopId?: number | string | null; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + taskStatus?: string; + outputFilename?: string; + downloadUrl?: string; + fileJobId?: number; + fileStatus?: string; + fileError?: string; + fileReady?: boolean; + error?: string; + success?: boolean; +} + +export interface PriceTrackHistoryVo { + items: PriceTrackHistoryItem[]; +} + +export interface PriceTrackAsinParsedRow { + shopMallName?: string; + asin?: string; + price?: string; + recommendedPrice?: string; + shippingFee?: string; + minimumPrice?: string; + firstPlace?: string; + firstShop?: string; + secondPlace?: string; + secondShop?: string; + cartShopName?: string; + priceChangeStatus?: string; + modifyCount?: string; + status?: string; +} + +export interface PriceTrackCreateTaskVo { + taskId: number; + items: PriceTrackHistoryItem[]; + skipAsinsByCountry?: Record; + skipAsinDetailsByCountry?: Record>; + asinRowsByCountry?: Record; + minimumPriceByCountryAndAsin?: Record>; +} + +export interface PriceTrackTaskSummary { + id?: number; + taskNo?: string; + status?: string; + loopRunId?: number; + roundIndex?: number; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; +} + +export interface PriceTrackTaskDetailVo { + task?: PriceTrackTaskSummary; + items?: PriceTrackHistoryItem[]; +} + +export interface PriceTrackTaskBatchVo { + items: PriceTrackTaskDetailVo[]; + missingTaskIds?: number[]; +} + +export interface PriceTrackPendingDeleteVo { + removed: boolean; +} + +export type PriceTrackCreateTaskPayload = Omit< + PriceTrackCreateTaskRequest, + "userId" +>; + +export interface PriceTrackCreateTaskRequest { + userId: number; + statusMode: boolean; + asinMode: boolean; + items: PriceTrackShopQueueItem[]; + asinFiles: string[]; + countryCodes: string[]; + loopRunId?: number; + roundIndex?: number; + shopIndex?: number; +} + +export type PriceTrackExecutionMode = "FINITE" | "INFINITE"; + +export interface PriceTrackLoopRunCreateRequest { + userId: number; + statusMode: boolean; + asinMode: boolean; + items: PriceTrackShopQueueItem[]; + asinFiles: string[]; + countryCodes: string[]; + executionMode: PriceTrackExecutionMode; + targetRounds?: number; +} + +export interface PriceTrackLoopRunVo { + id: number; + status?: string; + executionMode?: PriceTrackExecutionMode; + targetRounds?: number; + currentRound?: number; + currentShopIndex?: number; + totalShopCount?: number; + activeTaskId?: number; + activeTaskStatus?: string; + stopRequested?: boolean; + errorMessage?: string; + statusMode?: boolean; + asinMode?: boolean; + asinFiles?: string[]; + countryCodes?: string[]; +} + +export interface PriceTrackLoopRunDispatchVo { + loopRun?: PriceTrackLoopRunVo; + childTaskRequest?: PriceTrackCreateTaskRequest; +} + +export interface SkipPriceAsinCheckVo { + country: string; + asin: string; + exists: boolean; + minimumPrice?: string; +} + +export interface SkipPriceAsinPageVo { + page: number; + pageSize: number; + total: number; + totalPages: number; + skipAsinsByCountry: Record; + skipAsinDetailsByCountry: Record>; + skip_asins?: Record; + skip_asins_by_country?: Record; + skip_asin_details_by_country?: Record>; + asin_rows_by_country?: Record>; + minimum_price_by_country_and_asin?: Record>; +} + +const taskProgressResponseCache = createTaskProgressRequestCache({ + ttlMs: () => getTaskProgressCacheTtlMs(), + maxEntries: 100, + maxInflight: 16, +}); + +function normalizeTaskIds(taskIds: number[]) { + return Array.from( + new Set( + (taskIds || []) + .map((taskId) => Number(taskId)) + .filter((taskId) => Number.isFinite(taskId) && taskId > 0), + ), + ).sort((left, right) => left - right); +} + +function buildTaskProgressRequestKey(path: string, taskIds: number[]) { + return `${path}::${taskIds.join(",")}`; +} + +async function postTaskProgressBatch( + path: string, + taskIds: number[], +) { + const normalizedTaskIds = normalizeTaskIds(taskIds); + if (!normalizedTaskIds.length) { + return { items: [], missingTaskIds: [] } as T; + } + + const cacheKey = buildTaskProgressRequestKey(path, normalizedTaskIds); + const cached = taskProgressResponseCache.get(cacheKey); + if (cached !== undefined) { + return cached as T; + } + + const inflight = taskProgressResponseCache.getInflight(cacheKey); + if (inflight) { + return (await inflight) as T; + } + + const requestPromise = unwrapJavaResponse( + post, { taskIds: number[] }>(path, { taskIds: normalizedTaskIds }, { + timeout: getTaskProgressTimeoutMs(), + }), + ) + .then((data) => { + taskProgressResponseCache.set(cacheKey, data); + return data; + }) + .finally(() => { + taskProgressResponseCache.endInflight(cacheKey); + }); + + taskProgressResponseCache.startInflight(cacheKey, requestPromise); + return requestPromise; +} + +export function listPriceTrackCandidates() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/candidates?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function addPriceTrackCandidate(shopName: string) { + return unwrapJavaResponse( + post, { userId: number; shopName: string }>( + `${JAVA_API_PREFIX}/price-track/candidates`, + { userId: getCurrentUserId(), shopName }, + ), + ); +} + +export function deletePriceTrackCandidate(id: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/price-track/candidates/${id}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function getPriceTrackCountryPreference() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/country-preference?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function putPriceTrackCountryPreference(countryCodes: string[]) { + return unwrapJavaResponse( + put, { userId: number; countryCodes: string[] }>( + `${JAVA_API_PREFIX}/price-track/country-preference`, + { userId: getCurrentUserId(), countryCodes }, + ), + ); +} + +export function matchPriceTrackShops( + shopNames: string[], + options?: { + asinFiles?: string[]; + countryCodes?: string[]; + }, +) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { userId: number; shopNames: string[]; asinFiles?: string[]; countryCodes?: string[] } + >( + `${JAVA_API_PREFIX}/price-track/match-shops`, + { + userId: getCurrentUserId(), + shopNames, + asinFiles: options?.asinFiles || [], + countryCodes: options?.countryCodes || [], + }, + ), + ); +} + +export function createPriceTrackTask( + request: PriceTrackCreateTaskPayload | PriceTrackCreateTaskRequest, +) { + return unwrapJavaResponse( + post, PriceTrackCreateTaskRequest>( + `${JAVA_API_PREFIX}/price-track/tasks`, + { ...request, userId: getCurrentUserId() }, + ), + ); +} + +export function deletePriceTrackTask(taskId: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/price-track/tasks/${taskId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function getPriceTrackTasksBatch(taskIds: number[]) { + return unwrapJavaResponse( + post, { taskIds: number[] }>( + `${JAVA_API_PREFIX}/price-track/tasks/batch`, + { taskIds }, + ), + ); +} + +export function createPriceTrackLoopRun( + request: Omit | PriceTrackLoopRunCreateRequest, +) { + return unwrapJavaResponse( + post, PriceTrackLoopRunCreateRequest>( + `${JAVA_API_PREFIX}/price-track/loop-runs`, + { ...request, userId: getCurrentUserId() }, + ), + ); +} + +export function getPriceTrackLoopRun(loopRunId: number) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function dispatchNextPriceTrackLoopRun(loopRunId: number) { + return unwrapJavaResponse( + post, undefined>( + `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/dispatch-next?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + undefined, + ), + ); +} + +export function completePriceTrackLoopChild(loopRunId: number, childTaskId: number) { + return unwrapJavaResponse( + post, { childTaskId: number }>( + `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/child-finished?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + { childTaskId }, + ), + ); +} + +export function stopPriceTrackLoopRun(loopRunId: number) { + return unwrapJavaResponse( + post, undefined>( + `${JAVA_API_PREFIX}/price-track/loop-runs/${loopRunId}/stop?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + undefined, + ), + ); +} + +export function getPriceTrackTaskProgressBatch(taskIds: number[]) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/price-track/tasks/progress/batch`, + taskIds, + ); +} + +export function getPriceTrackResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.priceTrack.resultDownload.replace('{resultId}', String(resultId)), + ); +} + +export function getTaskSkipPriceAsinsPaginated( + taskId: number, + page: number = 1, + pageSize: number = 1000, + options: { + shopName?: string | string[]; + countryCode?: string | string[]; + } = {}, +) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/tasks/${taskId}/skip-asins/paginated`, + { + params: { + page, + page_size: pageSize, + ...(options.shopName ? { shop_name: options.shopName } : {}), + ...(options.countryCode ? { country_code: options.countryCode } : {}), + }, + }, + ), + ); +} + +export function markPriceTrackDispatchFailed(taskId: number, errorMessage: string) { + return unwrapJavaResponse( + post, { errorMessage: string }>( + `${JAVA_API_PREFIX}/price-track/tasks/${taskId}/dispatch-failed?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + { errorMessage }, + ), + ); +} + +export function checkTaskSkipPriceAsin(taskId: number, country: string, asin: string) { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/tasks/${taskId}/skip-asin/check`, + { + params: { + country, + asin, + }, + }, + ), + ); +} + +export function deletePendingPriceTrackShopResult(shopName: string) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/price-track/pending-shop-result?user_id=${encodeURIComponent(String(getCurrentUserId()))}&shop_name=${encodeURIComponent(shopName)}`, + ), + ); +} + +export function getPriceTrackDashboard() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/dashboard?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function getPriceTrackHistory() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/price-track/history?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} + +export function deletePriceTrackHistory(resultId: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/price-track/history/${resultId}?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + ), + ); +} diff --git a/frontend-vue/tests/modules-price-track.test.ts b/frontend-vue/tests/modules-price-track.test.ts new file mode 100644 index 00000000..195f5fa7 --- /dev/null +++ b/frontend-vue/tests/modules-price-track.test.ts @@ -0,0 +1,252 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + listPriceTrackCandidates, + addPriceTrackCandidate, + deletePriceTrackCandidate, + getPriceTrackCountryPreference, + putPriceTrackCountryPreference, + matchPriceTrackShops, + createPriceTrackTask, + deletePriceTrackTask, + getPriceTrackTasksBatch, + createPriceTrackLoopRun, + getPriceTrackLoopRun, + dispatchNextPriceTrackLoopRun, + completePriceTrackLoopChild, + stopPriceTrackLoopRun, + getPriceTrackTaskProgressBatch, + getPriceTrackResultDownloadUrl, + getTaskSkipPriceAsinsPaginated, + markPriceTrackDispatchFailed, + checkTaskSkipPriceAsin, + deletePendingPriceTrackShopResult, + getPriceTrackDashboard, + getPriceTrackHistory, + deletePriceTrackHistory, + type PriceTrackCreateTaskRequest, + type PriceTrackLoopRunCreateRequest, +} from '../src/shared/api/types/modules/price-track.ts' + +function setupWindow() { + ;(globalThis as Record).window = { + localStorage: { getItem: () => '42' }, + location: { origin: 'http://localhost' }, + } +} + +function mockRequest( + t: Parameters[1] extends (t: infer T) => unknown ? T : never, + impl: (config: { url?: string; method?: string; params?: Record; data?: unknown; timeout?: number }) => Promise, +) { + t.mock.method(http, 'request', impl as never) +} + +const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } }) + +test('test_price_track_candidates_crud', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ id: 1, shopName: 's1' }) + }) + await listPriceTrackCandidates() + await addPriceTrackCandidate('店铺A') + await deletePriceTrackCandidate(3) + assert.equal(calls[0].url, '/newApi/api/price-track/candidates?user_id=42') + assert.equal(calls[0].method, 'GET') + assert.equal(calls[1].url, '/newApi/api/price-track/candidates') + assert.equal(calls[1].method, 'POST') + assert.deepEqual(calls[1].data, { userId: 42, shopName: '店铺A' }) + assert.equal(calls[2].url, '/newApi/api/price-track/candidates/3?user_id=42') + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_price_track_country_preference', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ userId: 42, countryCodes: ['US'] }) + }) + const result = await getPriceTrackCountryPreference() + await putPriceTrackCountryPreference(['US', 'CA']) + assert.equal(calls[0].url, '/newApi/api/price-track/country-preference?user_id=42') + assert.equal(result.countryCodes[0], 'US') + assert.equal(calls[1].url, '/newApi/api/price-track/country-preference') + assert.equal(calls[1].method, 'PUT') + assert.deepEqual(calls[1].data, { userId: 42, countryCodes: ['US', 'CA'] }) +}) + +test('test_price_track_match_shops', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [] }) + }) + await matchPriceTrackShops(['店铺A'], { asinFiles: ['k1'], countryCodes: ['US'] }) + assert.equal(captured.url, '/newApi/api/price-track/match-shops') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { userId: 42, shopNames: ['店铺A'], asinFiles: ['k1'], countryCodes: ['US'] }) +}) + +test('test_price_track_create_task', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ taskId: 7, items: [] }) + }) + const request: Omit = { + statusMode: true, + asinMode: false, + items: [{ shopName: '店铺A' }], + asinFiles: [], + countryCodes: ['US'], + } + await createPriceTrackTask(request) + assert.equal(captured.url, '/newApi/api/price-track/tasks') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { ...request, userId: 42 }) +}) + +test('test_price_track_tasks_batch_delete', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ items: [], missingTaskIds: [] }) + }) + await getPriceTrackTasksBatch([5, 3]) + await deletePriceTrackTask(7) + assert.equal(calls[0].url, '/newApi/api/price-track/tasks/batch') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { taskIds: [5, 3] }) + assert.equal(calls[1].url, '/newApi/api/price-track/tasks/7?user_id=42') + assert.equal(calls[1].method, 'DELETE') +}) + +test('test_price_track_loop_run', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ id: 1, status: 'RUNNING' }) + }) + const request: Omit = { + statusMode: true, + asinMode: false, + items: [], + asinFiles: [], + countryCodes: ['US'], + executionMode: 'FINITE', + targetRounds: 3, + } + await createPriceTrackLoopRun(request) + await getPriceTrackLoopRun(1) + await dispatchNextPriceTrackLoopRun(1) + await completePriceTrackLoopChild(1, 7) + await stopPriceTrackLoopRun(1) + assert.equal(calls[0].url, '/newApi/api/price-track/loop-runs') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { ...request, userId: 42 }) + assert.equal(calls[1].url, '/newApi/api/price-track/loop-runs/1?user_id=42') + assert.equal(calls[1].method, 'GET') + assert.equal(calls[2].url, '/newApi/api/price-track/loop-runs/1/dispatch-next?user_id=42') + assert.equal(calls[2].method, 'POST') + assert.equal(calls[3].url, '/newApi/api/price-track/loop-runs/1/child-finished?user_id=42') + assert.deepEqual(calls[3].data, { childTaskId: 7 }) + assert.equal(calls[4].url, '/newApi/api/price-track/loop-runs/1/stop?user_id=42') + assert.equal(calls[4].method, 'POST') +}) + +test('test_price_track_progress_batch_timeout', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown; timeout?: number } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [], missingTaskIds: [] }) + }) + const result = await getPriceTrackTaskProgressBatch([5, 3]) + assert.equal(captured.url, '/newApi/api/price-track/tasks/progress/batch') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { taskIds: [3, 5] }) + assert.equal(captured.timeout, 10000) + assert.deepEqual(result, { items: [], missingTaskIds: [] }) +}) + +test('test_price_track_skip_asin_apis', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ page: 1, pageSize: 1000, total: 0, totalPages: 0, skipAsinsByCountry: {} }) + }) + await getTaskSkipPriceAsinsPaginated(7, 1, 1000, { shopName: '店铺A', countryCode: 'US' }) + await markPriceTrackDispatchFailed(7, '超时') + await checkTaskSkipPriceAsin(7, 'US', 'B001') + await deletePendingPriceTrackShopResult('店铺A') + assert.equal(calls[0].url, '/newApi/api/price-track/tasks/7/skip-asins/paginated') + assert.deepEqual(calls[0].params, { page: 1, page_size: 1000, shop_name: '店铺A', country_code: 'US' }) + assert.equal(calls[1].url, '/newApi/api/price-track/tasks/7/dispatch-failed?user_id=42') + assert.deepEqual(calls[1].data, { errorMessage: '超时' }) + assert.equal(calls[2].url, '/newApi/api/price-track/tasks/7/skip-asin/check') + assert.deepEqual(calls[2].params, { country: 'US', asin: 'B001' }) + assert.equal(calls[3].url, '/newApi/api/price-track/pending-shop-result?user_id=42&shop_name=%E5%BA%97%E9%93%BAA') +}) + +test('test_price_track_dashboard_history', async (t) => { + setupWindow() + const calls: { url?: string; method?: string }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 1, failedTaskCount: 0 }) + }) + await getPriceTrackDashboard() + await getPriceTrackHistory() + await deletePriceTrackHistory(9) + assert.equal(calls[0].url, '/newApi/api/price-track/dashboard?user_id=42') + assert.equal(calls[1].url, '/newApi/api/price-track/history?user_id=42') + assert.equal(calls[2].url, '/newApi/api/price-track/history/9?user_id=42') + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_price_track_download_url', async (t) => { + setupWindow() + const url = getPriceTrackResultDownloadUrl(7) + assert.equal(url, 'http://localhost/newApi/api/price-track/results/7/download?user_id=42') +}) + +test('test_price_track_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromPriceTrack = await import('../src/shared/api/types/modules/price-track.ts') + const names = [ + 'listPriceTrackCandidates', 'addPriceTrackCandidate', 'deletePriceTrackCandidate', + 'getPriceTrackCountryPreference', 'putPriceTrackCountryPreference', 'matchPriceTrackShops', + 'createPriceTrackTask', 'deletePriceTrackTask', 'getPriceTrackTasksBatch', + 'createPriceTrackLoopRun', 'getPriceTrackLoopRun', 'dispatchNextPriceTrackLoopRun', + 'completePriceTrackLoopChild', 'stopPriceTrackLoopRun', 'getPriceTrackTaskProgressBatch', + 'getPriceTrackResultDownloadUrl', 'getTaskSkipPriceAsinsPaginated', 'markPriceTrackDispatchFailed', + 'checkTaskSkipPriceAsin', 'deletePendingPriceTrackShopResult', 'getPriceTrackDashboard', + 'getPriceTrackHistory', 'deletePriceTrackHistory', + ] + for (const name of names) { + assert.equal(fromJavaModules[name], fromPriceTrack[name], `${name} 应为同一引用`) + } + const item = { id: 1, shopName: 's1', matched: true } + assert.equal(item.matched, true) +}) + +test('test_price_track_unwrap_and_error', async (t) => { + setupWindow() + mockRequest(t, () => okResponse({ taskId: 1, items: [] })) + const result = await createPriceTrackTask({ statusMode: true, asinMode: false, items: [], asinFiles: [], countryCodes: [] }) + assert.equal(result.taskId, 1) + + mockRequest(t, () => Promise.resolve({ data: { success: false, message: '店铺不存在' } })) + await assert.rejects(addPriceTrackCandidate('x'), /店铺不存在/) +})