diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 5eb08391..b7cac90f 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -15,6 +15,7 @@ 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"; +export * from "./types/modules/appearance-patent.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -1668,204 +1669,6 @@ export function deleteWithdrawHistory(resultId: number) { ); } -// ========== 外观专利检测 ========== - -export interface AppearancePatentParsedRow { - rowIndex: number; - sourceFileKey?: string; - sourceFilename?: string; - sourceId: string; - displayId: string; - rowToken?: string; - groupKey?: string; - asin: string; - country: string; - price?: string; - sku?: string; - url?: string; - title?: string; -} - -export interface AppearancePatentParsedGroup { - sourceFileKey?: string; - sourceFilename?: string; - groupKey?: string; - baseId?: string; - displayId?: string; - itemCount?: number; - items: AppearancePatentParsedRow[]; -} - -export interface AppearancePatentParseVo { - taskId: number; - sourceFilename?: string; - sourceFileCount?: number; - totalRows: number; - acceptedRows: number; - droppedRows: number; - groupCount?: number; - aiPrompt?: string; - items: AppearancePatentParsedRow[]; - groups?: AppearancePatentParsedGroup[]; -} - -export interface AppearancePatentParsedPayloadDto { - aiPrompt?: string; - apiKey?: string; - patentToken?: string; - sourceFiles?: UploadedFileRef[]; - headers?: string[]; - items?: AppearancePatentParsedRow[]; - groups?: AppearancePatentParsedGroup[]; - allItems?: AppearancePatentParsedRow[]; -} - -export interface AppearancePatentDashboardVo { - pendingTaskCount: number; - processedTaskCount: number; - successTaskCount: number; - failedTaskCount: number; -} - -export interface AppearancePatentHistoryItem { - 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 AppearancePatentHistoryVo { - items: AppearancePatentHistoryItem[]; -} - -export interface AppearancePatentTaskSummary { - id?: number; - taskNo?: string; - status?: string; - errorMessage?: string; - createdAt?: string; - updatedAt?: string; - finishedAt?: string; -} - -export interface AppearancePatentTaskDetailVo { - task?: AppearancePatentTaskSummary; - items?: AppearancePatentHistoryItem[]; -} - -export interface AppearancePatentTaskBatchVo { - items: AppearancePatentTaskDetailVo[]; - missingTaskIds?: number[]; -} - -export function parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string, apiKey?: string, patentToken?: string) { - return unwrapJavaResponse( - post< - JavaApiResponse, - { user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string; patent_token?: string } - >(`${JAVA_API_PREFIX}/appearance-patent/parse`, { - user_id: getCurrentUserId(), - files, - ai_prompt: aiPrompt, - api_key: apiKey, - patent_token: patentToken, - }), - ); -} - -export function getAppearancePatentParsedPayload(taskId: number) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}/parsed-payload`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getAppearancePatentQueuePayload(taskId: number) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}/queue-payload`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getAppearancePatentDashboard() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/appearance-patent/dashboard`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getAppearancePatentHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/appearance-patent/history`, - { params: { user_id: getCurrentUserId(), limit: 50 } }, - ), - ); -} - -export function getAppearancePatentTaskProgressBatch( - taskIds: number[], - options: TaskProgressBatchOptions = {}, -) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/appearance-patent/tasks/progress/batch`, - taskIds, - options, - ); -} - -export function activateAppearancePatentTask(taskId: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, - undefined, - ), - ); -} - -export function deleteAppearancePatentTask(taskId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function deleteAppearancePatentHistory(resultId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/appearance-patent/history/${resultId}`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getAppearancePatentResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/appearance-patent/results/${resultId}/download`); -} - // ========== 跟价 ========== export interface PriceTrackCandidateVo { diff --git a/frontend-vue/src/shared/api/types/modules/appearance-patent.ts b/frontend-vue/src/shared/api/types/modules/appearance-patent.ts new file mode 100644 index 00000000..e8752e0c --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/appearance-patent.ts @@ -0,0 +1,268 @@ +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 AppearancePatentParsedRow { + rowIndex: number; + sourceFileKey?: string; + sourceFilename?: string; + sourceId: string; + displayId: string; + rowToken?: string; + groupKey?: string; + asin: string; + country: string; + price?: string; + sku?: string; + url?: string; + title?: string; +} + +export interface AppearancePatentParsedGroup { + sourceFileKey?: string; + sourceFilename?: string; + groupKey?: string; + baseId?: string; + displayId?: string; + itemCount?: number; + items: AppearancePatentParsedRow[]; +} + +export interface AppearancePatentParseVo { + taskId: number; + sourceFilename?: string; + sourceFileCount?: number; + totalRows: number; + acceptedRows: number; + droppedRows: number; + groupCount?: number; + aiPrompt?: string; + items: AppearancePatentParsedRow[]; + groups?: AppearancePatentParsedGroup[]; +} + +export interface AppearancePatentParsedPayloadDto { + aiPrompt?: string; + apiKey?: string; + patentToken?: string; + sourceFiles?: UploadedFileRef[]; + headers?: string[]; + items?: AppearancePatentParsedRow[]; + groups?: AppearancePatentParsedGroup[]; + allItems?: AppearancePatentParsedRow[]; +} + +export interface AppearancePatentDashboardVo { + pendingTaskCount: number; + processedTaskCount: number; + successTaskCount: number; + failedTaskCount: number; +} + +export interface AppearancePatentHistoryItem { + 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 AppearancePatentHistoryVo { + items: AppearancePatentHistoryItem[]; +} + +export interface AppearancePatentTaskSummary { + id?: number; + taskNo?: string; + status?: string; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; +} + +export interface AppearancePatentTaskDetailVo { + task?: AppearancePatentTaskSummary; + items?: AppearancePatentHistoryItem[]; +} + +export interface AppearancePatentTaskBatchVo { + items: AppearancePatentTaskDetailVo[]; + 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 parseAppearancePatent(files: UploadedFileRef[], aiPrompt: string, apiKey?: string, patentToken?: string) { + return unwrapJavaResponse( + post< + JavaApiResponse, + { user_id: number; files: UploadedFileRef[]; ai_prompt: string; api_key?: string; patent_token?: string } + >(buildJavaUrl(API_ENDPOINTS.appearancePatent.parse), { + user_id: getCurrentUserId(), + files, + ai_prompt: aiPrompt, + api_key: apiKey, + patent_token: patentToken, + }), + ); +} + +export function getAppearancePatentParsedPayload(taskId: number) { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.appearancePatent.parsedPayload.replace('{taskId}', String(taskId))), + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getAppearancePatentQueuePayload(taskId: number) { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.appearancePatent.queuePayload.replace('{taskId}', String(taskId))), + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getAppearancePatentDashboard() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.appearancePatent.dashboard), + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getAppearancePatentHistory() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.appearancePatent.history), + { params: { user_id: getCurrentUserId(), limit: 50 } }, + ), + ); +} + +export function getAppearancePatentTaskProgressBatch( + taskIds: number[], + options: TaskProgressBatchOptions = {}, +) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/appearance-patent/tasks/progress/batch`, + taskIds, + options, + ); +} + +export function activateAppearancePatentTask(taskId: number) { + return unwrapJavaResponse( + post, undefined>( + `${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}/activate?user_id=${encodeURIComponent(String(getCurrentUserId()))}`, + undefined, + ), + ); +} + +export function deleteAppearancePatentTask(taskId: number) { + return unwrapJavaResponse( + del>(`${JAVA_API_PREFIX}/appearance-patent/tasks/${taskId}`, { + params: { user_id: getCurrentUserId() }, + }), + ); +} + +export function deleteAppearancePatentHistory(resultId: number) { + return unwrapJavaResponse( + del>( + `${JAVA_API_PREFIX}/appearance-patent/history/${resultId}`, + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getAppearancePatentResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.appearancePatent.resultDownload.replace('{resultId}', String(resultId)), + ); +} diff --git a/frontend-vue/tests/modules-appearance-patent.test.ts b/frontend-vue/tests/modules-appearance-patent.test.ts new file mode 100644 index 00000000..a22a7c12 --- /dev/null +++ b/frontend-vue/tests/modules-appearance-patent.test.ts @@ -0,0 +1,158 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + parseAppearancePatent, + getAppearancePatentParsedPayload, + getAppearancePatentQueuePayload, + getAppearancePatentDashboard, + getAppearancePatentHistory, + getAppearancePatentTaskProgressBatch, + activateAppearancePatentTask, + deleteAppearancePatentTask, + deleteAppearancePatentHistory, + getAppearancePatentResultDownloadUrl, +} from '../src/shared/api/types/modules/appearance-patent.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_appearance_patent_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 parseAppearancePatent([{ fileKey: 'k1' }], '请识别', 'api-1', 'tk-1') + assert.equal(captured.url, '/newApi/api/appearance-patent/parse') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { + user_id: 42, + files: [{ fileKey: 'k1' }], + ai_prompt: '请识别', + api_key: 'api-1', + patent_token: 'tk-1', + }) +}) + +test('test_appearance_patent_parsed_payload', async (t) => { + setupWindow() + let captured: { url?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [] }) + }) + const result = await getAppearancePatentParsedPayload(7) + assert.equal(captured.url, '/newApi/api/appearance-patent/tasks/7/parsed-payload') + assert.deepEqual(captured.params, { user_id: 42 }) + assert.deepEqual(result.items, []) +}) + +test('test_appearance_patent_queue_payload', async (t) => { + setupWindow() + let captured: { url?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ aiPrompt: '请识别' }) + }) + const result = await getAppearancePatentQueuePayload(7) + assert.equal(captured.url, '/newApi/api/appearance-patent/tasks/7/queue-payload') + assert.deepEqual(captured.params, { user_id: 42 }) + assert.equal(result.aiPrompt, '请识别') +}) + +test('test_appearance_patent_dashboard_history', async (t) => { + setupWindow() + const calls: { url?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ pendingTaskCount: 0, processedTaskCount: 0, successTaskCount: 1, failedTaskCount: 0 }) + }) + await getAppearancePatentDashboard() + await getAppearancePatentHistory() + assert.equal(calls[0].url, '/newApi/api/appearance-patent/dashboard') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/appearance-patent/history') + assert.deepEqual(calls[1].params, { user_id: 42, limit: 50 }) +}) + +test('test_appearance_patent_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 getAppearancePatentTaskProgressBatch([5, 3]) + assert.equal(captured.url, '/newApi/api/appearance-patent/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_appearance_patent_activate', async (t) => { + setupWindow() + let captured: { url?: string; method?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await activateAppearancePatentTask(7) + assert.equal(captured.url, '/newApi/api/appearance-patent/tasks/7/activate?user_id=42') + assert.equal(captured.method, 'POST') +}) + +test('test_appearance_patent_delete_task_history', async (t) => { + setupWindow() + const calls: { url?: string; method?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse(null) + }) + await deleteAppearancePatentTask(7) + await deleteAppearancePatentHistory(9) + assert.equal(calls[0].url, '/newApi/api/appearance-patent/tasks/7') + assert.equal(calls[0].method, 'DELETE') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/appearance-patent/history/9') + assert.equal(calls[1].method, 'DELETE') + assert.deepEqual(calls[1].params, { user_id: 42 }) +}) + +test('test_appearance_patent_download_url', async (t) => { + setupWindow() + const url = getAppearancePatentResultDownloadUrl(7) + assert.equal(url, 'http://localhost/newApi/api/appearance-patent/results/7/download?user_id=42') +}) + +test('test_appearance_patent_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromAppearance = await import('../src/shared/api/types/modules/appearance-patent.ts') + const names = [ + 'parseAppearancePatent', 'getAppearancePatentParsedPayload', 'getAppearancePatentQueuePayload', + 'getAppearancePatentDashboard', 'getAppearancePatentHistory', 'getAppearancePatentTaskProgressBatch', + 'activateAppearancePatentTask', 'deleteAppearancePatentTask', 'deleteAppearancePatentHistory', + 'getAppearancePatentResultDownloadUrl', + ] + for (const name of names) { + assert.equal(fromJavaModules[name], fromAppearance[name], `${name} 应为同一引用`) + } + const row = { rowIndex: 1, sourceId: 's1', displayId: 'd1', asin: 'B001', country: 'US' } + assert.equal(row.country, 'US') +})