From d7104d35da46afa770757a14dce0d6dc457dd412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Mon, 31 Aug 2026 19:34:56 +0800 Subject: [PATCH] =?UTF-8?q?task-15:=20=E6=8B=86=E5=88=86=20similar-asin=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20API=20=E8=87=B3=20types/modules/similar-as?= =?UTF-8?q?in.ts=EF=BC=8Cjava-modules.ts=20re-export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - parseSimilarAsin/getSimilarAsinDashboard/getSimilarAsinHistory/getSimilarAsinTaskProgressBatch/activateSimilarAsinTask/deleteSimilarAsinTask/deleteSimilarAsinHistory/getSimilarAsinResultDownloadUrl - similar-asin 全套类型随模块迁移;progressBatch 内联缓存+超时实现(与 java-modules 同构) - 9 个测试:parse URL/payload/默认值/dashboard/history/progressBatch 去重+超时/activate/delete/download/export compat --- frontend-vue/src/shared/api/java-modules.ts | 183 +------------ .../shared/api/types/modules/similar-asin.ts | 252 ++++++++++++++++++ .../tests/modules-similar-asin.test.ts | 161 +++++++++++ 3 files changed, 414 insertions(+), 182 deletions(-) create mode 100644 frontend-vue/src/shared/api/types/modules/similar-asin.ts create mode 100644 frontend-vue/tests/modules-similar-asin.test.ts diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 0ddb2ad2..5eb08391 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -14,6 +14,7 @@ export * from "./types/modules/dedupe.ts"; export * from "./types/modules/split.ts"; export * from "./types/modules/convert.ts"; export * from "./types/modules/publish.ts"; +export * from "./types/modules/similar-asin.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -1865,188 +1866,6 @@ export function getAppearancePatentResultDownloadUrl(resultId: number) { return getJavaDownloadUrl(`/appearance-patent/results/${resultId}/download`); } -// ========== 货源查询 ========== - -export interface SimilarAsinParsedRow { - rowIndex: number; - sourceFileKey?: string; - sourceFilename?: string; - sourceId: string; - displayId: string; - rowToken?: string; - groupKey?: string; - asin: string; - country: string; - sku?: string; - price?: string; - url?: string; - title?: string; -} - -export interface SimilarAsinParsedGroup { - sourceFileKey?: string; - sourceFilename?: string; - groupKey?: string; - baseId?: string; - displayId?: string; - itemCount?: number; - items: SimilarAsinParsedRow[]; -} - -export interface SimilarAsinParseVo { - taskId: number; - sourceFilename?: string; - sourceFileCount?: number; - totalRows: number; - acceptedRows: number; - droppedRows: number; - groupCount?: number; - aiPrompt?: string; - imgSwitch?: boolean; - categorySwitch?: boolean; - items: SimilarAsinParsedRow[]; - groups?: SimilarAsinParsedGroup[]; -} - -export interface SimilarAsinDashboardVo { - pendingTaskCount: number; - processedTaskCount: number; - successTaskCount: number; - failedTaskCount: number; -} - -export interface SimilarAsinHistoryItem { - resultId?: number; - taskId?: number; - sourceFilename?: string; - resultFilename?: string; - downloadUrl?: string; - fileJobId?: number; - fileStatus?: string; - fileError?: string; - fileReady?: boolean; - fileProgressPercent?: number; - fileProgressCurrent?: number; - fileProgressTotal?: number; - fileProgressMessage?: string; - taskStatus?: string; - success?: boolean; - error?: string; - rowCount?: number; - createdAt?: string; - startedAt?: string; - finishedAt?: string; -} - -export interface SimilarAsinHistoryVo { - items: SimilarAsinHistoryItem[]; -} - -export interface SimilarAsinTaskSummary { - id?: number; - taskNo?: string; - status?: string; - errorMessage?: string; - createdAt?: string; - updatedAt?: string; - finishedAt?: string; -} - -export interface SimilarAsinTaskDetailVo { - task?: SimilarAsinTaskSummary; - items?: SimilarAsinHistoryItem[]; -} - -export interface SimilarAsinTaskBatchVo { - items: SimilarAsinTaskDetailVo[]; - missingTaskIds?: number[]; -} - -export function parseSimilarAsin( - files: UploadedFileRef[], - apiKey?: string, - imgSwitch = false, - categorySwitch = false, -) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { - user_id: number; - files: UploadedFileRef[]; - api_key?: string; - img_switch: boolean; - category_switch: boolean; - } - >(`${JAVA_API_PREFIX}/similar-asin/parse`, { - user_id: getCurrentUserId(), - files, - api_key: apiKey, - img_switch: imgSwitch, - category_switch: categorySwitch, - }), - ); -} - -export function getSimilarAsinDashboard() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/similar-asin/dashboard`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getSimilarAsinHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/similar-asin/history`, - { params: { user_id: getCurrentUserId(), limit: 50 } }, - ), - ); -} - -export function getSimilarAsinTaskProgressBatch( - taskIds: number[], - options: TaskProgressBatchOptions = {}, -) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/similar-asin/tasks/progress/batch`, - taskIds, - options, - ); -} - -export function activateSimilarAsinTask(taskId: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/similar-asin/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - undefined, - ), - ); -} - -export function deleteSimilarAsinTask(taskId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/similar-asin/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function deleteSimilarAsinHistory(resultId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/similar-asin/history/${resultId}`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getSimilarAsinResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/similar-asin/results/${resultId}/download`); -} - // ========== 跟价 ========== export interface PriceTrackCandidateVo { diff --git a/frontend-vue/src/shared/api/types/modules/similar-asin.ts b/frontend-vue/src/shared/api/types/modules/similar-asin.ts new file mode 100644 index 00000000..cff0772d --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/similar-asin.ts @@ -0,0 +1,252 @@ +import { get, post, 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 { UploadedFileRef } from '../upload.ts' +import { API_ENDPOINTS } from '../../endpoints.ts' + +export interface SimilarAsinParsedRow { + rowIndex: number; + sourceFileKey?: string; + sourceFilename?: string; + sourceId: string; + displayId: string; + rowToken?: string; + groupKey?: string; + asin: string; + country: string; + sku?: string; + price?: string; + url?: string; + title?: string; +} + +export interface SimilarAsinParsedGroup { + sourceFileKey?: string; + sourceFilename?: string; + groupKey?: string; + baseId?: string; + displayId?: string; + itemCount?: number; + items: SimilarAsinParsedRow[]; +} + +export interface SimilarAsinParseVo { + taskId: number; + sourceFilename?: string; + sourceFileCount?: number; + totalRows: number; + acceptedRows: number; + droppedRows: number; + groupCount?: number; + aiPrompt?: string; + imgSwitch?: boolean; + categorySwitch?: boolean; + items: SimilarAsinParsedRow[]; + groups?: SimilarAsinParsedGroup[]; +} + +export interface SimilarAsinDashboardVo { + pendingTaskCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; +} + +export interface SimilarAsinHistoryItem { + resultId?: number; + taskId?: number; + sourceFilename?: string; + resultFilename?: string; + downloadUrl?: string; + fileJobId?: number; + fileStatus?: string; + fileError?: string; + fileReady?: boolean; + fileProgressPercent?: number; + fileProgressCurrent?: number; + fileProgressTotal?: number; + fileProgressMessage?: string; + taskStatus?: string; + success?: boolean; + error?: string; + rowCount?: number; + createdAt?: string; + startedAt?: string; + finishedAt?: string; +} + +export interface SimilarAsinHistoryVo { + items: SimilarAsinHistoryItem[]; +} + +export interface SimilarAsinTaskSummary { + id?: number; + taskNo?: string; + status?: string; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; +} + +export interface SimilarAsinTaskDetailVo { + task?: SimilarAsinTaskSummary; + items?: SimilarAsinHistoryItem[]; +} + +export interface SimilarAsinTaskBatchVo { + items: SimilarAsinTaskDetailVo[]; + missingTaskIds?: number[]; +} + +interface TaskProgressBatchOptions { + force?: boolean; +} + +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[], + options: TaskProgressBatchOptions = {}, +) { + const normalizedTaskIds = normalizeTaskIds(taskIds); + if (!normalizedTaskIds.length) { + return { items: [], missingTaskIds: [] } as T; + } + + const cacheKey = buildTaskProgressRequestKey(path, normalizedTaskIds); + const cached = taskProgressResponseCache.get(cacheKey); + if (!options.force && cached !== undefined) { + return cached as T; + } + + const inflight = taskProgressResponseCache.getInflight(cacheKey); + if (!options.force && 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 parseSimilarAsin( + files: UploadedFileRef[], + apiKey?: string, + imgSwitch = false, + categorySwitch = false, +) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { + user_id: number; + files: UploadedFileRef[]; + api_key?: string; + img_switch: boolean; + category_switch: boolean; + } + >(buildJavaUrl(API_ENDPOINTS.similarAsin.parse), { + user_id: getCurrentUserId(), + files, + api_key: apiKey, + img_switch: imgSwitch, + category_switch: categorySwitch, + }), + ); +} + +export function getSimilarAsinDashboard() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.similarAsin.dashboard), + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getSimilarAsinHistory() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.similarAsin.history), + { params: { user_id: getCurrentUserId(), limit: 50 } }, + ), + ); +} + +export function getSimilarAsinTaskProgressBatch( + taskIds: number[], + options: TaskProgressBatchOptions = {}, +) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/similar-asin/tasks/progress/batch`, + taskIds, + options, + ); +} + +export function activateSimilarAsinTask(taskId: number) { + return unwrapJavaResponse( + post, undefined>( + `${JAVA_API_PREFIX}/similar-asin/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + undefined, + ), + ); +} + +export function deleteSimilarAsinTask(taskId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/similar-asin/tasks/${taskId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function deleteSimilarAsinHistory(resultId: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/similar-asin/history/${resultId}`, + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getSimilarAsinResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.similarAsin.resultDownload.replace('{resultId}', String(resultId)), + ); +} diff --git a/frontend-vue/tests/modules-similar-asin.test.ts b/frontend-vue/tests/modules-similar-asin.test.ts new file mode 100644 index 00000000..45539c14 --- /dev/null +++ b/frontend-vue/tests/modules-similar-asin.test.ts @@ -0,0 +1,161 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + parseSimilarAsin, + getSimilarAsinDashboard, + getSimilarAsinHistory, + getSimilarAsinTaskProgressBatch, + activateSimilarAsinTask, + deleteSimilarAsinTask, + deleteSimilarAsinHistory, + getSimilarAsinResultDownloadUrl, + type SimilarAsinTaskBatchVo, +} from '../src/shared/api/types/modules/similar-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_similar_asin_parse_url_payload', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ taskId: 1, totalRows: 1, acceptedRows: 1, droppedRows: 0, items: [] }) + }) + await parseSimilarAsin([{ fileKey: 'k1' }], 'api-key-1', true, true) + assert.equal(captured.url, '/newApi/api/similar-asin/parse') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { + user_id: 42, + files: [{ fileKey: 'k1' }], + api_key: 'api-key-1', + img_switch: true, + category_switch: true, + }) +}) + +test('test_similar_asin_parse_defaults', async (t) => { + setupWindow() + let captured: { data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ taskId: 1, totalRows: 1, acceptedRows: 1, droppedRows: 0, items: [] }) + }) + await parseSimilarAsin([{ fileKey: 'k1' }]) + assert.deepEqual(captured.data, { + user_id: 42, + files: [{ fileKey: 'k1' }], + api_key: undefined, + img_switch: false, + category_switch: false, + }) +}) + +test('test_similar_asin_dashboard', async (t) => { + setupWindow() + let captured: { url?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ pendingTaskCount: 1, processedTaskCount: 2, successTaskCount: 1, failedTaskCount: 1 }) + }) + const result = await getSimilarAsinDashboard() + assert.equal(captured.url, '/newApi/api/similar-asin/dashboard') + assert.deepEqual(captured.params, { user_id: 42 }) + assert.equal(result.successTaskCount, 1) +}) + +test('test_similar_asin_history', async (t) => { + setupWindow() + let captured: { url?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [] }) + }) + await getSimilarAsinHistory() + assert.equal(captured.url, '/newApi/api/similar-asin/history') + assert.deepEqual(captured.params, { user_id: 42, limit: 50 }) +}) + +test('test_similar_asin_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 getSimilarAsinTaskProgressBatch([5, 3]) + assert.equal(captured.url, '/newApi/api/similar-asin/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: [] }) + + const empty = await getSimilarAsinTaskProgressBatch([]) + assert.deepEqual(empty as SimilarAsinTaskBatchVo, { items: [], missingTaskIds: [] }) +}) + +test('test_similar_asin_activate', async (t) => { + setupWindow() + let captured: { url?: string; method?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await activateSimilarAsinTask(7) + assert.equal(captured.url, '/newApi/api/similar-asin/tasks/7/activate?user_id=42') + assert.equal(captured.method, 'POST') +}) + +test('test_similar_asin_delete_task_history', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse(null) + }) + await deleteSimilarAsinTask(7) + await deleteSimilarAsinHistory(9) + assert.equal(calls[0].url, '/newApi/api/similar-asin/tasks/7') + assert.equal(calls[0].method, 'DELETE') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/similar-asin/history/9') + assert.equal(calls[1].method, 'DELETE') + assert.deepEqual(calls[1].params, { user_id: 42 }) +}) + +test('test_similar_asin_download_url', async (t) => { + setupWindow() + const url = getSimilarAsinResultDownloadUrl(7) + assert.equal(url, 'http://localhost/newApi/api/similar-asin/results/7/download?user_id=42') +}) + +test('test_similar_asin_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromSimilar = await import('../src/shared/api/types/modules/similar-asin.ts') + const names = [ + 'parseSimilarAsin', 'getSimilarAsinDashboard', 'getSimilarAsinHistory', + 'getSimilarAsinTaskProgressBatch', 'activateSimilarAsinTask', + 'deleteSimilarAsinTask', 'deleteSimilarAsinHistory', 'getSimilarAsinResultDownloadUrl', + ] + for (const name of names) { + assert.equal(fromJavaModules[name], fromSimilar[name], `${name} 应为同一引用`) + } + const row = { rowIndex: 1, sourceId: 's1', displayId: 'd1', asin: 'B001', country: 'US' } + assert.equal(row.asin, 'B001') + assert.equal(fromJavaModules.UploadedFileRef, undefined) +})