diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 34cf90be..0ddb2ad2 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -13,6 +13,7 @@ import { createTaskProgressRequestCache } from "../task-progress-request-cache.t export * from "./types/modules/dedupe.ts"; export * from "./types/modules/split.ts"; export * from "./types/modules/convert.ts"; +export * from "./types/modules/publish.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -3018,277 +3019,6 @@ export function failCollectDataTask(taskId: number, error?: string) { ); } -// ========== 上架 ========== - -export interface PublishSourceFile { - fileKey: string; - originalFilename?: string; - relativePath?: string; -} - -export interface PublishParseRequest { - user_id: number; - files: PublishSourceFile[]; - publish_country: string; - sync_countries: string[]; -} - -export interface PublishFileItem { - fileId: number; - fileKey?: string; - sourceFilename?: string; - shopName?: string; - shopId?: string; - matchedUserId?: number; - platform?: string; - companyName?: string; - matched?: boolean; - matchStatus?: string; - matchMessage?: string; - status?: string; - error?: string; - errorMessage?: string; - totalRows?: number; - processedRows?: number; - progressCurrent?: number; - progressTotal?: number; - progressPercent?: number; - percent?: number; - progressMessage?: string; - pageSize?: number; - totalPages?: number; - pageUrl?: string; -} - -export interface PublishTaskResult { - resultId?: number; - downloadUrl?: string; - resultFilename?: string; - fileReady?: boolean; - fileJobId?: number; - fileJobStatus?: string; - fileJobRetryCount?: number; - fileJobError?: string; - errorMessage?: string; -} - -export interface PublishTaskSummary { - id: number; - taskNo?: string; - status?: string; - sourceFileCount?: number; - totalRows?: number; - successFileCount?: number; - failedFileCount?: number; - completedFileCount?: number; - errorMessage?: string; - processedRows?: number; - percent?: number; - createdAt?: string; - updatedAt?: string; - startedAt?: string; - finishedAt?: string; - downloadUrl?: string; - resultFilename?: string; -} - -export interface PublishTaskDetailVo { - task: PublishTaskSummary; - files: PublishFileItem[]; - result?: PublishTaskResult; -} - -export interface PublishParseVo { - taskId: number; - taskNo?: string; - sourceFileCount?: number; - totalRows?: number; - pageSize?: number; - files: PublishFileItem[]; - result?: PublishTaskResult; -} - -export interface PublishTaskBatchVo { - items: PublishTaskDetailVo[]; - missingTaskIds?: number[]; -} - -export interface PublishHistoryVo { - total?: number; - items: PublishTaskDetailVo[]; -} - -export interface PublishDashboardVo { - pendingCount: number; - runningCount: number; - successCount: number; - failedCount: number; - recent?: PublishTaskDetailVo[]; -} - -export interface PublishItemVo { - id?: number; - rowIndex?: number; - fileId?: number; - fileKey?: string; - sourceFilename?: string; - values?: Record; - data?: Record; -} - -export interface PublishItemsPageVo { - taskId?: number; - fileId?: number; - taskStatus?: string; - fileStatus?: string; - shopName?: string; - shopId?: string; - platform?: string; - companyName?: string; - publishCountry?: string; - syncCountries?: string[]; - page: number; - pageSize: number; - total: number; - totalPages: number; - count?: number; - items: PublishItemVo[]; -} - -export interface PublishTaskResultFileRequest { - fileId: number; - fileKey?: string; - sourceFilename?: string; - error?: string; - rows: unknown[]; -} - -export interface PublishTaskResultRequest { - user_id?: number; - files: PublishTaskResultFileRequest[]; -} - -export function parsePublish( - request: Omit | PublishParseRequest, -) { - return unwrapJavaResponse( - post, PublishParseRequest>( - `${JAVA_API_PREFIX}/publish/parse`, - { ...request, user_id: getCurrentUserId() }, - { timeout: 180000 }, - ), - ); -} - -export function activatePublishTask(taskId: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/publish/tasks/${taskId}/activate`, - undefined, - ), - ); -} - -export function activatePublishFile(taskId: number, fileId: number) { - return unwrapJavaResponse( - post, undefined>( - `${JAVA_API_PREFIX}/publish/tasks/${taskId}/files/${fileId}/activate`, - undefined, - ), - ); -} - -export function getPublishItemsPage( - taskId: number, - fileId: number, - page: number = 1, - pageSize: number = 100, -) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/publish/tasks/${taskId}/items`, - { - params: { - file_id: fileId, - page, - page_size: pageSize, - }, - }, - ), - ); -} - -export function getPublishItemsPageUrl( - taskId: number, - fileId: number, - pageSize: number = 100, - page: number = 1, -) { - let raw = `${JAVA_API_PREFIX}/publish/tasks/${taskId}/items`; - if (typeof window !== "undefined") { - raw = `${window.location.origin}${raw}`; - } - const params = new URLSearchParams({ - file_id: String(fileId), - page: String(page), - page_size: String(pageSize), - }); - return `${raw}?${params.toString()}`; -} - -export function getPublishTaskProgressBatch(taskIds: number[]) { - const normalizedTaskIds = normalizeTaskIds(taskIds); - if (!normalizedTaskIds.length) { - return Promise.resolve({ items: [], missingTaskIds: [] } as PublishTaskBatchVo); - } - return unwrapJavaResponse( - post< - JavaApiResponse, - { task_ids: number[] } - >(`${JAVA_API_PREFIX}/publish/tasks/progress/batch`, { - task_ids: normalizedTaskIds, - }), - ); -} - -export function submitPublishTaskResult( - taskId: number, - request: PublishTaskResultRequest, -) { - return unwrapJavaResponse( - post, PublishTaskResultRequest>( - `${JAVA_API_PREFIX}/publish/tasks/${taskId}/result`, - { ...request }, - ), - ); -} - -export function getPublishDashboard() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/publish/dashboard`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function getPublishHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/publish/history`, - { params: { user_id: getCurrentUserId() } }, - ), - ); -} - -export function deletePublishTask(taskId: number) { - return unwrapJavaResponse( - del>(`${JAVA_API_PREFIX}/publish/tasks/${taskId}`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - export function getJavaDownloadUrl(path: string) { let raw = path.startsWith("http://") || path.startsWith("https://") diff --git a/frontend-vue/src/shared/api/types/modules/publish.ts b/frontend-vue/src/shared/api/types/modules/publish.ts new file mode 100644 index 00000000..ff5aec22 --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/publish.ts @@ -0,0 +1,284 @@ +import { get, post, del, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts' +import { buildJavaUrl, JAVA_API_PREFIX } from '../../url.ts' +import { getCurrentUserId } from '../../user.ts' +import { getTaskProgressTimeoutMs } from '../../../task-progress-config.ts' +import { API_ENDPOINTS } from '../../endpoints.ts' + +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); +} + +export interface PublishSourceFile { + fileKey: string; + originalFilename?: string; + relativePath?: string; +} + +export interface PublishParseRequest { + user_id: number; + files: PublishSourceFile[]; + publish_country: string; + sync_countries: string[]; +} + +export interface PublishFileItem { + fileId: number; + fileKey?: string; + sourceFilename?: string; + shopName?: string; + shopId?: string; + matchedUserId?: number; + platform?: string; + companyName?: string; + matched?: boolean; + matchStatus?: string; + matchMessage?: string; + status?: string; + error?: string; + errorMessage?: string; + totalRows?: number; + processedRows?: number; + progressCurrent?: number; + progressTotal?: number; + progressPercent?: number; + percent?: number; + progressMessage?: string; + pageSize?: number; + totalPages?: number; + pageUrl?: string; +} + +export interface PublishTaskResult { + resultId?: number; + downloadUrl?: string; + resultFilename?: string; + fileReady?: boolean; + fileJobId?: number; + fileJobStatus?: string; + fileJobRetryCount?: number; + fileJobError?: string; + errorMessage?: string; +} + +export interface PublishTaskSummary { + id: number; + taskNo?: string; + status?: string; + sourceFileCount?: number; + totalRows?: number; + successFileCount?: number; + failedFileCount?: number; + completedFileCount?: number; + errorMessage?: string; + processedRows?: number; + percent?: number; + createdAt?: string; + updatedAt?: string; + startedAt?: string; + finishedAt?: string; + downloadUrl?: string; + resultFilename?: string; +} + +export interface PublishTaskDetailVo { + task: PublishTaskSummary; + files: PublishFileItem[]; + result?: PublishTaskResult; +} + +export interface PublishParseVo { + taskId: number; + taskNo?: string; + sourceFileCount?: number; + totalRows?: number; + pageSize?: number; + files: PublishFileItem[]; + result?: PublishTaskResult; +} + +export interface PublishTaskBatchVo { + items: PublishTaskDetailVo[]; + missingTaskIds?: number[]; +} + +export interface PublishHistoryVo { + total?: number; + items: PublishTaskDetailVo[]; +} + +export interface PublishDashboardVo { + pendingCount: number; + runningCount: number; + successCount: number; + failedCount: number; + recent?: PublishTaskDetailVo[]; +} + +export interface PublishItemVo { + id?: number; + rowIndex?: number; + fileId?: number; + fileKey?: string; + sourceFilename?: string; + values?: Record; + data?: Record; +} + +export interface PublishItemsPageVo { + taskId?: number; + fileId?: number; + taskStatus?: string; + fileStatus?: string; + shopName?: string; + shopId?: string; + platform?: string; + companyName?: string; + publishCountry?: string; + syncCountries?: string[]; + page: number; + pageSize: number; + total: number; + totalPages: number; + count?: number; + items: PublishItemVo[]; +} + +export interface PublishTaskResultFileRequest { + fileId: number; + fileKey?: string; + sourceFilename?: string; + error?: string; + rows: unknown[]; +} + +export interface PublishTaskResultRequest { + user_id?: number; + files: PublishTaskResultFileRequest[]; +} + +export function parsePublish( + request: Omit | PublishParseRequest, +) { + return unwrapJavaResponse( + post, PublishParseRequest>( + buildJavaUrl(API_ENDPOINTS.publish.parse), + { ...request, user_id: getCurrentUserId() }, + { timeout: 180000 }, + ), + ); +} + +export function activatePublishTask(taskId: number) { + return unwrapJavaResponse( + post, undefined>( + buildJavaUrl(API_ENDPOINTS.publish.taskActivate.replace('{taskId}', String(taskId))), + undefined, + ), + ); +} + +export function activatePublishFile(taskId: number, fileId: number) { + return unwrapJavaResponse( + post, undefined>( + buildJavaUrl(API_ENDPOINTS.publish.fileActivate.replace('{taskId}', String(taskId)).replace('{fileId}', String(fileId))), + undefined, + ), + ); +} + +export function getPublishItemsPage( + taskId: number, + fileId: number, + page: number = 1, + pageSize: number = 100, +) { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.publish.items.replace('{taskId}', String(taskId))), + { + params: { + file_id: fileId, + page, + page_size: pageSize, + }, + }, + ), + ); +} + +export function getPublishItemsPageUrl( + taskId: number, + fileId: number, + pageSize: number = 100, + page: number = 1, +) { + let raw = `${JAVA_API_PREFIX}/publish/tasks/${taskId}/items`; + if (typeof window !== "undefined") { + raw = `${window.location.origin}${raw}`; + } + const params = new URLSearchParams({ + file_id: String(fileId), + page: String(page), + page_size: String(pageSize), + }); + return `${raw}?${params.toString()}`; +} + +export function getPublishTaskProgressBatch(taskIds: number[]) { + const normalizedTaskIds = normalizeTaskIds(taskIds); + if (!normalizedTaskIds.length) { + return Promise.resolve({ items: [], missingTaskIds: [] } as PublishTaskBatchVo); + } + return unwrapJavaResponse( + post< + JavaApiResponse, + { task_ids: number[] } + >(buildJavaUrl(API_ENDPOINTS.publish.progressBatch), { + task_ids: normalizedTaskIds, + }, { timeout: getTaskProgressTimeoutMs() }), + ); +} + +export function submitPublishTaskResult( + taskId: number, + request: PublishTaskResultRequest, +) { + return unwrapJavaResponse( + post, PublishTaskResultRequest>( + buildJavaUrl(API_ENDPOINTS.publish.result.replace('{taskId}', String(taskId))), + { ...request }, + ), + ); +} + +export function getPublishDashboard() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.publish.dashboard), + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function getPublishHistory() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.publish.history), + { params: { user_id: getCurrentUserId() } }, + ), + ); +} + +export function deletePublishTask(taskId: number) { + return unwrapJavaResponse( + del>(buildJavaUrl(API_ENDPOINTS.publish.taskDelete.replace('{taskId}', String(taskId))), { + params: { user_id: getCurrentUserId() }, + }), + ); +} diff --git a/frontend-vue/tests/modules-publish.test.ts b/frontend-vue/tests/modules-publish.test.ts new file mode 100644 index 00000000..aa3acbc3 --- /dev/null +++ b/frontend-vue/tests/modules-publish.test.ts @@ -0,0 +1,174 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + parsePublish, + activatePublishTask, + activatePublishFile, + getPublishItemsPage, + getPublishItemsPageUrl, + getPublishTaskProgressBatch, + submitPublishTaskResult, + getPublishDashboard, + getPublishHistory, + deletePublishTask, + type PublishParseRequest, + type PublishTaskResultRequest, + type PublishTaskBatchVo, +} from '../src/shared/api/types/modules/publish.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 } }) + +const parseRequest: Omit = { + files: [{ fileKey: 'k1', originalFilename: 'a.xlsx' }], + publish_country: 'US', + sync_countries: ['CA'], +} + +test('test_publish_parse_url_payload', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown; timeout?: number } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ taskId: 1, files: [] }) + }) + await parsePublish(parseRequest) + assert.equal(captured.url, '/newApi/api/publish/parse') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { ...parseRequest, user_id: 42 }) + assert.equal(captured.timeout, 180000) +}) + +test('test_publish_activate_task', async (t) => { + setupWindow() + let captured: { url?: string; method?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await activatePublishTask(7) + assert.equal(captured.url, '/newApi/api/publish/tasks/7/activate') + assert.equal(captured.method, 'POST') +}) + +test('test_publish_activate_file', async (t) => { + setupWindow() + let captured: { url?: string; method?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await activatePublishFile(7, 9) + assert.equal(captured.url, '/newApi/api/publish/tasks/7/files/9/activate') + assert.equal(captured.method, 'POST') +}) + +test('test_publish_items_page_params', async (t) => { + setupWindow() + let captured: { url?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ taskId: 7, fileId: 9, page: 2, pageSize: 50, total: 0, totalPages: 0, items: [] }) + }) + await getPublishItemsPage(7, 9, 2, 50) + assert.equal(captured.url, '/newApi/api/publish/tasks/7/items') + assert.deepEqual(captured.params, { file_id: 9, page: 2, page_size: 50 }) +}) + +test('test_publish_items_page_url', async (t) => { + setupWindow() + const url = getPublishItemsPageUrl(7, 9, 100, 1) + assert.equal(url, 'http://localhost/newApi/api/publish/tasks/7/items?file_id=9&page=1&page_size=100') +}) + +test('test_publish_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 getPublishTaskProgressBatch([5, 3, 5]) + assert.equal(captured.url, '/newApi/api/publish/tasks/progress/batch') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { task_ids: [3, 5] }) + assert.equal(captured.timeout, 10000) + assert.deepEqual(result, { items: [], missingTaskIds: [] }) + + const empty = await getPublishTaskProgressBatch([]) + assert.deepEqual(empty as PublishTaskBatchVo, { items: [], missingTaskIds: [] }) +}) + +test('test_publish_submit_result_body', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + const body: PublishTaskResultRequest = { files: [{ fileId: 9, rows: [{ a: 1 }] }] } + await submitPublishTaskResult(7, body) + assert.equal(captured.url, '/newApi/api/publish/tasks/7/result') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, body) +}) + +test('test_publish_dashboard_history', async (t) => { + setupWindow() + const calls: { url?: string; params?: Record }[] = [] + mockRequest(t, (config) => { + calls.push(config) + return okResponse({ pendingCount: 0, runningCount: 0, successCount: 1, failedCount: 0 }) + }) + await getPublishDashboard() + await getPublishHistory() + assert.equal(calls[0].url, '/newApi/api/publish/dashboard') + assert.deepEqual(calls[0].params, { user_id: 42 }) + assert.equal(calls[1].url, '/newApi/api/publish/history') + assert.deepEqual(calls[1].params, { user_id: 42 }) +}) + +test('test_publish_delete_task', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await deletePublishTask(7) + assert.equal(captured.url, '/newApi/api/publish/tasks/7') + assert.equal(captured.method, 'DELETE') + assert.deepEqual(captured.params, { user_id: 42 }) +}) + +test('test_publish_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromPublish = await import('../src/shared/api/types/modules/publish.ts') + const names = [ + 'parsePublish', 'activatePublishTask', 'activatePublishFile', 'getPublishItemsPage', + 'getPublishItemsPageUrl', 'getPublishTaskProgressBatch', 'submitPublishTaskResult', + 'getPublishDashboard', 'getPublishHistory', 'deletePublishTask', + ] + for (const name of names) { + assert.equal(fromJavaModules[name], fromPublish[name], `${name} 应为同一引用`) + } + const summary = { id: 1, taskNo: 'T1', status: 'RUNNING' } + assert.equal(summary.status, 'RUNNING') + const itemsPage = { taskId: 1, fileId: 2, page: 1, pageSize: 100, total: 0, totalPages: 0, items: [] } + assert.equal(itemsPage.totalPages, 0) +})