diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 92e91eca..152f777e 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -19,12 +19,17 @@ export * from "./types/modules/appearance-patent.ts"; export * from "./types/modules/delete-brand.ts"; export * from "./types/modules/price-track.ts"; export * from "./types/modules/shop-match.ts"; +export * from "./types/modules/query-asin.ts"; import type { ProductRiskCandidateVo, ProductRiskDashboardVo, ProductRiskMatchShopsVo, ProductRiskShopQueueItem, } from "./types/modules/shop-match.ts"; +import type { + QueryAsinCandidateVo, + QueryAsinDashboardVo, +} from "./types/modules/query-asin.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -418,363 +423,6 @@ export function deletePatrolDeleteHistory(resultId: number) { ); } -export type QueryAsinCandidateVo = ProductRiskCandidateVo; -export type QueryAsinDashboardVo = ProductRiskDashboardVo; -export type QueryAsinShopQueueItem = ProductRiskShopQueueItem; - -export interface QueryAsinCountryAsins { - country: string; - asins: string[]; -} - -export interface QueryAsinStatusItem { - asin?: string; - status?: string; -} - -export interface QueryAsinCountryResult { - country: string; - items: QueryAsinStatusItem[]; -} - -export interface QueryAsinTaskItem { - shopName?: string; - matched?: boolean; - shopId?: string; - platform?: string; - companyName?: string; - matchStatus?: string; - matchMessage?: string; - queryAsins: QueryAsinCountryAsins[]; -} - -export interface QueryAsinHistoryItem { - resultId?: number; - taskId?: number; - shopName?: string; - shopId?: string; - platform?: string; - companyName?: string; - matched?: boolean; - matchStatus?: string; - matchMessage?: string; - taskStatus?: string; - success?: boolean; - error?: string; - createdAt?: string; - finishedAt?: string; - outputFilename?: string; - downloadUrl?: string; - fileJobId?: number; - fileStatus?: string; - fileError?: string; - fileReady?: boolean; - queryAsins: QueryAsinCountryAsins[]; - countryResults?: QueryAsinCountryResult[]; -} - -export interface QueryAsinHistoryVo { - items: QueryAsinHistoryItem[]; -} - -export interface QueryAsinTaskBatchVo { - items: QueryAsinHistoryItem[]; - missingTaskIds: number[]; -} - -export interface QueryAsinCreateTaskVo { - taskId: number; - items: QueryAsinHistoryItem[]; -} - -export function listQueryAsinCandidates() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/query-asin/candidates`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function addQueryAsinCandidate(shopName: string) { - return unwrapJavaResponse( - post, { user_id: number; shop_name: string }>( - `${JAVA_API_PREFIX}/query-asin/candidates`, - { user_id: getCurrentUserId(), shop_name: shopName }, - ), - ); -} - -export function deleteQueryAsinCandidate(id: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/query-asin/candidates/${id}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function matchQueryAsinShops(shopNames: string[]) { - return unwrapJavaResponse( - post, { user_id: number; shop_names: string[] }>( - `${JAVA_API_PREFIX}/query-asin/match-shops`, - { user_id: getCurrentUserId(), shop_names: shopNames }, - ), - ); -} - -export function getQueryAsinDashboard() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/query-asin/dashboard`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function getQueryAsinHistory() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/query-asin/history`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function getQueryAsinTaskProgressBatch(taskIds: number[]) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/query-asin/tasks/progress/batch`, - taskIds, - ); -} - -export function createQueryAsinTask(items: QueryAsinTaskItem[]) { - return unwrapJavaResponse( - post, { user_id: number; items: QueryAsinTaskItem[] }>( - `${JAVA_API_PREFIX}/query-asin/tasks`, - { user_id: getCurrentUserId(), items }, - ), - ); -} - -export function submitQueryAsinTaskResult( - taskId: number, - payload: { - shops: Array<{ - shopName: string; - error?: string; - countryResults?: QueryAsinCountryResult[]; - shopDone?: boolean; - submissionId?: string; - }>; - }, -) { - return unwrapJavaResponse( - post, typeof payload>(`${JAVA_API_PREFIX}/query-asin/tasks/${taskId}/result`, payload), - ); -} - -export function getQueryAsinResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/query-asin/results/${resultId}/download`); -} - -export function deleteQueryAsinTask(taskId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/query-asin/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function deleteQueryAsinHistory(resultId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/query-asin/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -// ========== 店铺数据抓取 ========== - -export interface ShopDataCrawlCandidateVo { - id: number; - shop_name: string; - created_at?: string; -} - -export interface ShopDataCrawlCountryPreferenceVo { - country_codes: string[]; -} - -export interface ShopDataCrawlShopItem { - shopName: string; - matched: boolean; - shopId?: string; - platform?: string; - companyName?: string; - openStoreUrl?: string; - matchedUserId?: number; - matchStatus?: string; - matchMessage?: string; -} - -export interface ShopDataCrawlMatchVo { - items: ShopDataCrawlShopItem[]; -} - -export interface ShopDataCrawlDashboardVo { - candidateCount: number; - processedTaskCount: number; - successTaskCount: number; - failedTaskCount: number; -} - -export interface ShopDataCrawlHistoryItem extends ShopDataCrawlShopItem { - resultId?: number; - taskId?: number; - taskStatus?: string; - success?: boolean; - error?: string; - fileReady?: boolean; - fileStatus?: string; - downloadUrl?: string; - outputFilename?: string; - createdAt?: string; - finishedAt?: string; -} - -export interface ShopDataCrawlHistoryVo { - items: ShopDataCrawlHistoryItem[]; -} - -export interface ShopDataCrawlTaskSummary { - id?: number; - status?: string; - errorMessage?: string; - createdAt?: string; - finishedAt?: string; - countryCodes?: string[]; -} - -export interface ShopDataCrawlTaskDetailVo { - task?: ShopDataCrawlTaskSummary; - items?: ShopDataCrawlHistoryItem[]; -} - -export interface ShopDataCrawlTaskBatchVo { - items: Array; - missingTaskIds?: number[]; -} - -export interface ShopDataCrawlCreateTaskVo { - taskId: number; - items: ShopDataCrawlHistoryItem[]; -} - -export function listShopDataCrawlCandidates() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/shop-data-crawl/candidates`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function addShopDataCrawlCandidate(shopName: string) { - return unwrapJavaResponse( - post, { user_id: number; shop_name: string }>( - `${JAVA_API_PREFIX}/shop-data-crawl/candidates`, - { user_id: getCurrentUserId(), shop_name: shopName }, - ), - ); -} - -export function deleteShopDataCrawlCandidate(id: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/shop-data-crawl/candidates/${id}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function getShopDataCrawlCountryPreference() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/shop-data-crawl/country-preference`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function putShopDataCrawlCountryPreference(countryCodes: string[]) { - return unwrapJavaResponse( - put, { user_id: number; country_codes: string[] }>( - `${JAVA_API_PREFIX}/shop-data-crawl/country-preference`, - { user_id: getCurrentUserId(), country_codes: countryCodes }, - ), - ); -} - -export function matchShopDataCrawlShops(shopNames: string[]) { - return unwrapJavaResponse( - post, { user_id: number; shop_names: string[] }>( - `${JAVA_API_PREFIX}/shop-data-crawl/match-shops`, - { user_id: getCurrentUserId(), shop_names: shopNames }, - ), - ); -} - -export function getShopDataCrawlDashboard() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/shop-data-crawl/dashboard`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function getShopDataCrawlHistory() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/shop-data-crawl/history`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function createShopDataCrawlTask(items: ShopDataCrawlShopItem[], countryCodes: string[]) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; items: ShopDataCrawlShopItem[]; country_codes: string[] } - >(`${JAVA_API_PREFIX}/shop-data-crawl/tasks`, { - user_id: getCurrentUserId(), - items, - country_codes: countryCodes, - }), - ); -} - -export function getShopDataCrawlTaskProgressBatch(taskIds: number[]) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/shop-data-crawl/tasks/progress/batch`, - taskIds, - ); -} - -export function getShopDataCrawlResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/shop-data-crawl/results/${resultId}/download`); -} - -export function deleteShopDataCrawlTask(taskId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/shop-data-crawl/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function deleteShopDataCrawlHistory(resultId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/shop-data-crawl/history/${resultId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - // ========== 取款 ========== export type WithdrawCandidateVo = QueryAsinCandidateVo; diff --git a/frontend-vue/src/shared/api/types/modules/query-asin.ts b/frontend-vue/src/shared/api/types/modules/query-asin.ts new file mode 100644 index 00000000..780d8f5d --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/query-asin.ts @@ -0,0 +1,429 @@ +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 type { + ProductRiskCandidateVo, + ProductRiskDashboardVo, + ProductRiskMatchShopsVo, + ProductRiskShopQueueItem, +} from './shop-match.ts' +import { API_ENDPOINTS } from '../../endpoints.ts' + +export type QueryAsinCandidateVo = ProductRiskCandidateVo; +export type QueryAsinDashboardVo = ProductRiskDashboardVo; +export type QueryAsinShopQueueItem = ProductRiskShopQueueItem; + +export interface QueryAsinCountryAsins { + country: string; + asins: string[]; +} + +export interface QueryAsinStatusItem { + asin?: string; + status?: string; +} + +export interface QueryAsinCountryResult { + country: string; + items: QueryAsinStatusItem[]; +} + +export interface QueryAsinTaskItem { + shopName?: string; + matched?: boolean; + shopId?: string; + platform?: string; + companyName?: string; + matchStatus?: string; + matchMessage?: string; + queryAsins: QueryAsinCountryAsins[]; +} + +export interface QueryAsinHistoryItem { + resultId?: number; + taskId?: number; + shopName?: string; + shopId?: string; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + taskStatus?: string; + success?: boolean; + error?: string; + createdAt?: string; + finishedAt?: string; + outputFilename?: string; + downloadUrl?: string; + fileJobId?: number; + fileStatus?: string; + fileError?: string; + fileReady?: boolean; + queryAsins: QueryAsinCountryAsins[]; + countryResults?: QueryAsinCountryResult[]; +} + +export interface QueryAsinHistoryVo { + items: QueryAsinHistoryItem[]; +} + +export interface QueryAsinTaskBatchVo { + items: QueryAsinHistoryItem[]; + missingTaskIds: number[]; +} + +export interface QueryAsinCreateTaskVo { + taskId: number; + items: QueryAsinHistoryItem[]; +} + +export interface ShopDataCrawlCandidateVo { + id: number; + shop_name: string; + created_at?: string; +} + +export interface ShopDataCrawlCountryPreferenceVo { + country_codes: string[]; +} + +export interface ShopDataCrawlShopItem { + shopName: string; + matched: boolean; + shopId?: string; + platform?: string; + companyName?: string; + openStoreUrl?: string; + matchedUserId?: number; + matchStatus?: string; + matchMessage?: string; +} + +export interface ShopDataCrawlMatchVo { + items: ShopDataCrawlShopItem[]; +} + +export interface ShopDataCrawlDashboardVo { + candidateCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; +} + +export interface ShopDataCrawlHistoryItem extends ShopDataCrawlShopItem { + resultId?: number; + taskId?: number; + taskStatus?: string; + success?: boolean; + error?: string; + fileReady?: boolean; + fileStatus?: string; + downloadUrl?: string; + outputFilename?: string; + createdAt?: string; + finishedAt?: string; +} + +export interface ShopDataCrawlHistoryVo { + items: ShopDataCrawlHistoryItem[]; +} + +export interface ShopDataCrawlTaskSummary { + id?: number; + status?: string; + errorMessage?: string; + createdAt?: string; + finishedAt?: string; + countryCodes?: string[]; +} + +export interface ShopDataCrawlTaskDetailVo { + task?: ShopDataCrawlTaskSummary; + items?: ShopDataCrawlHistoryItem[]; +} + +export interface ShopDataCrawlTaskBatchVo { + items: Array; + missingTaskIds?: number[]; +} + +export interface ShopDataCrawlCreateTaskVo { + taskId: number; + items: ShopDataCrawlHistoryItem[]; +} + +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 listQueryAsinCandidates() { + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/query-asin/candidates`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function addQueryAsinCandidate(shopName: string) { + return unwrapJavaResponse( + post, { user_id: number; shop_name: string }>( + `${JAVA_API_PREFIX}/query-asin/candidates`, + { user_id: getCurrentUserId(), shop_name: shopName }, + ), + ); +} + +export function deleteQueryAsinCandidate(id: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/query-asin/candidates/${id}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function matchQueryAsinShops(shopNames: string[]) { + return unwrapJavaResponse( + post, { user_id: number; shop_names: string[] }>( + `${JAVA_API_PREFIX}/query-asin/match-shops`, + { user_id: getCurrentUserId(), shop_names: shopNames }, + ), + ); +} + +export function getQueryAsinDashboard() { + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/query-asin/dashboard`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function getQueryAsinHistory() { + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/query-asin/history`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function getQueryAsinTaskProgressBatch(taskIds: number[]) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/query-asin/tasks/progress/batch`, + taskIds, + ); +} + +export function createQueryAsinTask(items: QueryAsinTaskItem[]) { + return unwrapJavaResponse( + post, { user_id: number; items: QueryAsinTaskItem[] }>( + `${JAVA_API_PREFIX}/query-asin/tasks`, + { user_id: getCurrentUserId(), items }, + ), + ); +} + +export function submitQueryAsinTaskResult( + taskId: number, + payload: { + shops: Array<{ + shopName: string; + error?: string; + countryResults?: QueryAsinCountryResult[]; + shopDone?: boolean; + submissionId?: string; + }>; + }, +) { + return unwrapJavaResponse( + post, typeof payload>(`${JAVA_API_PREFIX}/query-asin/tasks/${taskId}/result`, payload), + ); +} + +export function getQueryAsinResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.queryAsin.resultDownload.replace('{resultId}', String(resultId)), + ); +} + +export function deleteQueryAsinTask(taskId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/query-asin/tasks/${taskId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function deleteQueryAsinHistory(resultId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/query-asin/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function listShopDataCrawlCandidates() { + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/shop-data-crawl/candidates`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function addShopDataCrawlCandidate(shopName: string) { + return unwrapJavaResponse( + post, { user_id: number; shop_name: string }>( + `${JAVA_API_PREFIX}/shop-data-crawl/candidates`, + { user_id: getCurrentUserId(), shop_name: shopName }, + ), + ); +} + +export function deleteShopDataCrawlCandidate(id: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/shop-data-crawl/candidates/${id}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function getShopDataCrawlCountryPreference() { + return unwrapJavaResponse( + get>( + `${JAVA_API_PREFIX}/shop-data-crawl/country-preference`, + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function putShopDataCrawlCountryPreference(countryCodes: string[]) { + return unwrapJavaResponse( + put, { user_id: number; country_codes: string[] }>( + `${JAVA_API_PREFIX}/shop-data-crawl/country-preference`, + { user_id: getCurrentUserId(), country_codes: countryCodes }, + ), + ); +} + +export function matchShopDataCrawlShops(shopNames: string[]) { + return unwrapJavaResponse( + post, { user_id: number; shop_names: string[] }>( + `${JAVA_API_PREFIX}/shop-data-crawl/match-shops`, + { user_id: getCurrentUserId(), shop_names: shopNames }, + ), + ); +} + +export function getShopDataCrawlDashboard() { + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/shop-data-crawl/dashboard`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function getShopDataCrawlHistory() { + return unwrapJavaResponse( + get>(`${JAVA_API_PREFIX}/shop-data-crawl/history`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function createShopDataCrawlTask(items: ShopDataCrawlShopItem[], countryCodes: string[]) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; items: ShopDataCrawlShopItem[]; country_codes: string[] } + >(`${JAVA_API_PREFIX}/shop-data-crawl/tasks`, { + user_id: getCurrentUserId(), + items, + country_codes: countryCodes, + }), + ); +} + +export function getShopDataCrawlTaskProgressBatch(taskIds: number[]) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/shop-data-crawl/tasks/progress/batch`, + taskIds, + ); +} + +export function getShopDataCrawlResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.shopDataCrawl.resultDownload.replace('{resultId}', String(resultId)), + ); +} + +export function deleteShopDataCrawlTask(taskId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/shop-data-crawl/tasks/${taskId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function deleteShopDataCrawlHistory(resultId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/shop-data-crawl/history/${resultId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} diff --git a/frontend-vue/src/shared/api/types/modules/shop-match.ts b/frontend-vue/src/shared/api/types/modules/shop-match.ts index 345a4fbc..43bf1e27 100644 --- a/frontend-vue/src/shared/api/types/modules/shop-match.ts +++ b/frontend-vue/src/shared/api/types/modules/shop-match.ts @@ -5,12 +5,7 @@ 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' - -/** 与 query-asin 模块同构的共享行结构(避免跨模块循环依赖,类型内联) */ -export interface QueryAsinCountryAsins { - country: string; - asins: string[]; -} +import type { QueryAsinCountryAsins } from './query-asin.ts' /** 商品风险处理:备选店铺与匹配相关数据结构。 */ export interface ProductRiskCandidateVo { diff --git a/frontend-vue/tests/modules-query-asin.test.ts b/frontend-vue/tests/modules-query-asin.test.ts new file mode 100644 index 00000000..e481c450 --- /dev/null +++ b/frontend-vue/tests/modules-query-asin.test.ts @@ -0,0 +1,249 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + listShopDataCrawlCandidates, + addShopDataCrawlCandidate, + deleteShopDataCrawlCandidate, + getShopDataCrawlCountryPreference, + putShopDataCrawlCountryPreference, + matchShopDataCrawlShops, + getShopDataCrawlDashboard, + getShopDataCrawlHistory, + createShopDataCrawlTask, + getShopDataCrawlTaskProgressBatch, + getShopDataCrawlResultDownloadUrl, + deleteShopDataCrawlTask, + deleteShopDataCrawlHistory, + listQueryAsinCandidates, + addQueryAsinCandidate, + deleteQueryAsinCandidate, + matchQueryAsinShops, + getQueryAsinDashboard, + getQueryAsinHistory, + getQueryAsinTaskProgressBatch, + createQueryAsinTask, + submitQueryAsinTaskResult, + getQueryAsinResultDownloadUrl, + deleteQueryAsinTask, + deleteQueryAsinHistory, +} from '../src/shared/api/types/modules/query-asin.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_shop_data_crawl_candidates', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ id: 1, shop_name: 's1' }) + }) + await listShopDataCrawlCandidates() + await addShopDataCrawlCandidate('店铺A') + await deleteShopDataCrawlCandidate(3) + assert.equal(calls[0].url, '/newApi/api/shop-data-crawl/candidates') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/shop-data-crawl/candidates') + assert.equal(calls[1].method, 'POST') + assert.deepEqual(calls[1].data, { user_id: 42, shop_name: '店铺A' }) + assert.equal(calls[2].url, '/newApi/api/shop-data-crawl/candidates/3') + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_shop_data_crawl_preference_match', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ country_codes: ['US'] }) + }) + await getShopDataCrawlCountryPreference() + await putShopDataCrawlCountryPreference(['US']) + await matchShopDataCrawlShops(['店铺A']) + assert.equal(calls[0].url, '/newApi/api/shop-data-crawl/country-preference') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/shop-data-crawl/country-preference') + assert.equal(calls[1].method, 'PUT') + assert.deepEqual(calls[1].data, { user_id: 42, country_codes: ['US'] }) + assert.equal(calls[2].url, '/newApi/api/shop-data-crawl/match-shops') + assert.deepEqual(calls[2].data, { user_id: 42, shop_names: ['店铺A'] }) +}) + +test('test_shop_data_crawl_task_apis', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 1, items: [] }) + }) + await createShopDataCrawlTask([{ shopName: '店铺A', matched: true }], ['US']) + await deleteShopDataCrawlTask(7) + await deleteShopDataCrawlHistory(9) + assert.equal(calls[0].url, '/newApi/api/shop-data-crawl/tasks') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { user_id: 42, items: [{ shopName: '店铺A', matched: true }], country_codes: ['US'] }) + assert.equal(calls[1].url, '/newApi/api/shop-data-crawl/tasks/7') + assert.equal(calls[1].method, 'DELETE') + assert.deepEqual(calls[1].params, { user_id: 42 }) + assert.equal(calls[2].url, '/newApi/api/shop-data-crawl/history/9') + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_shop_data_crawl_dashboard_history', async (t) => { + setupWindow() + const calls: { url?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ candidateCount: 0, processedTaskCount: 0, successTaskCount: 1, failedTaskCount: 0 }) + }) + await getShopDataCrawlDashboard() + await getShopDataCrawlHistory() + assert.equal(calls[0].url, '/newApi/api/shop-data-crawl/dashboard') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/shop-data-crawl/history') + assert.deepEqual(calls[1].params, { user_id: 42 }) + assert.equal( + getShopDataCrawlResultDownloadUrl(7), + 'http://localhost/newApi/api/shop-data-crawl/results/7/download?user_id=42', + ) +}) + +test('test_shop_data_crawl_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 getShopDataCrawlTaskProgressBatch([5, 3]) + assert.equal(captured.url, '/newApi/api/shop-data-crawl/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_query_asin_candidates', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ id: 1, shop_name: 's1' }) + }) + await listQueryAsinCandidates() + await addQueryAsinCandidate('店铺A') + await deleteQueryAsinCandidate(3) + assert.equal(calls[0].url, '/newApi/api/query-asin/candidates') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/query-asin/candidates') + assert.equal(calls[1].method, 'POST') + assert.deepEqual(calls[1].data, { user_id: 42, shop_name: '店铺A' }) + assert.equal(calls[2].url, '/newApi/api/query-asin/candidates/3') + assert.equal(calls[2].method, 'DELETE') +}) + +test('test_query_asin_match_dashboard_history', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ items: [] }) + }) + await matchQueryAsinShops(['店铺A']) + await getQueryAsinDashboard() + await getQueryAsinHistory() + assert.equal(calls[0].url, '/newApi/api/query-asin/match-shops') + assert.deepEqual(calls[0].data, { user_id: 42, shop_names: ['店铺A'] }) + assert.equal(calls[1].url, '/newApi/api/query-asin/dashboard') + assert.deepEqual(calls[1].params, { user_id: 42 }) + assert.equal(calls[2].url, '/newApi/api/query-asin/history') + assert.deepEqual(calls[2].params, { user_id: 42 }) +}) + +test('test_query_asin_task_apis', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ taskId: 1, items: [] }) + }) + await createQueryAsinTask([{ queryAsins: [{ country: 'US', asins: ['B001'] }] }]) + const payload = { + shops: [{ shopName: '店铺A', countryResults: [{ country: 'US', items: [{ asin: 'B001', status: 'ok' }] }], shopDone: true }], + } + await submitQueryAsinTaskResult(7, payload) + assert.equal(calls[0].url, '/newApi/api/query-asin/tasks') + assert.equal(calls[0].method, 'POST') + assert.deepEqual(calls[0].data, { user_id: 42, items: [{ queryAsins: [{ country: 'US', asins: ['B001'] }] }] }) + assert.equal(calls[1].url, '/newApi/api/query-asin/tasks/7/result') + assert.deepEqual(calls[1].data, payload) +}) + +test('test_query_asin_progress_delete_download', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; data?: unknown; timeout?: number }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ items: [], missingTaskIds: [] }) + }) + await getQueryAsinTaskProgressBatch([5, 3]) + await deleteQueryAsinTask(7) + await deleteQueryAsinHistory(9) + assert.equal(calls[0].url, '/newApi/api/query-asin/tasks/progress/batch') + assert.deepEqual(calls[0].data, { taskIds: [3, 5] }) + assert.equal(calls[0].timeout, 10000) + assert.equal(calls[1].url, '/newApi/api/query-asin/tasks/7') + assert.equal(calls[1].method, 'DELETE') + assert.equal(calls[2].url, '/newApi/api/query-asin/history/9') + assert.equal(calls[2].method, 'DELETE') + assert.equal( + getQueryAsinResultDownloadUrl(7), + 'http://localhost/newApi/api/query-asin/results/7/download?user_id=42', + ) +}) + +test('test_query_asin_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromQueryAsin = await import('../src/shared/api/types/modules/query-asin.ts') + const names = [ + 'listShopDataCrawlCandidates', 'addShopDataCrawlCandidate', 'deleteShopDataCrawlCandidate', + 'getShopDataCrawlCountryPreference', 'putShopDataCrawlCountryPreference', 'matchShopDataCrawlShops', + 'getShopDataCrawlDashboard', 'getShopDataCrawlHistory', 'createShopDataCrawlTask', + 'getShopDataCrawlTaskProgressBatch', 'getShopDataCrawlResultDownloadUrl', + 'deleteShopDataCrawlTask', 'deleteShopDataCrawlHistory', + 'listQueryAsinCandidates', 'addQueryAsinCandidate', 'deleteQueryAsinCandidate', + 'matchQueryAsinShops', 'getQueryAsinDashboard', 'getQueryAsinHistory', + 'getQueryAsinTaskProgressBatch', 'createQueryAsinTask', 'submitQueryAsinTaskResult', + 'getQueryAsinResultDownloadUrl', 'deleteQueryAsinTask', 'deleteQueryAsinHistory', + ] + for (const name of names) { + assert.equal(fromJavaModules[name], fromQueryAsin[name], `${name} 应为同一引用`) + } + const item = { resultId: 1, taskId: 2, shopName: 's1', queryAsins: [{ country: 'US', asins: ['B001'] }] } + assert.equal(item.queryAsins[0].asins[0], 'B001') +}) + +test('test_query_asin_unwrap_and_error', async (t) => { + setupWindow() + mockRequest(t, () => okResponse({ taskId: 1, items: [] })) + const result = await createQueryAsinTask([]) + assert.equal(result.taskId, 1) + + mockRequest(t, () => Promise.resolve({ data: { success: false, message: '店铺不存在' } })) + await assert.rejects(addQueryAsinCandidate('x'), /店铺不存在/) +})