From f8dad2795761a625ffa3f45799261c9c24f51131 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:37:19 +0800 Subject: [PATCH] =?UTF-8?q?task-17:=20=E6=8B=86=E5=88=86=20delete-brand=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20API=20=E8=87=B3=20types/modules/delete-bra?= =?UTF-8?q?nd.ts=EF=BC=8Cjava-modules.ts=20re-export?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - runDeleteBrand/getDeleteBrandHistory/deleteDeleteBrandHistory/getDeleteBrandTaskDetail/getDeleteBrandTaskDetails/getDeleteBrandTaskDownloadUrl/getDeleteBrandResultDownloadUrl/submitDeleteBrandResult/getDeleteBrandTaskProgress - delete-brand 全套类型(含 MatchStatus union)随模块迁移;progressBatch 内联缓存+超时实现 - 10 个测试:run URL/超时/history/delete/taskDetail/taskDetails/download×2/submitResult 请求体/progressBatch/export compat --- frontend-vue/src/shared/api/java-modules.ts | 234 +------------ .../shared/api/types/modules/delete-brand.ts | 307 ++++++++++++++++++ .../tests/modules-delete-brand.test.ts | 167 ++++++++++ 3 files changed, 475 insertions(+), 233 deletions(-) create mode 100644 frontend-vue/src/shared/api/types/modules/delete-brand.ts create mode 100644 frontend-vue/tests/modules-delete-brand.test.ts diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index b7cac90f..87d49cba 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -16,6 +16,7 @@ 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"; +export * from "./types/modules/delete-brand.ts"; const JAVA_API_PREFIX = "/newApi/api"; @@ -150,232 +151,6 @@ export async function uploadTempFileToJava( } -export interface DeleteBrandPreviewRow { - rowIndex: number; - country: string; - asin: string; - status: string; -} - -export interface DeleteBrandCountryAsinItem { - rowIndex: number; - asin: string; - status: string; -} - -export interface DeleteBrandCountryGroup { - country: string; - asinCount: number; - items: DeleteBrandCountryAsinItem[]; -} - -export interface DeleteBrandCountryResultItem { - asin: string; - status?: string; -} - -export interface DeleteBrandProcessedCountry { - country: string; - items: DeleteBrandCountryResultItem[]; -} - -export interface DeleteBrandResultFile { - fileKey?: string; - sourceFilename: string; - fileIndex?: number; - fileTotal?: number; - chunkIndex?: number; - chunkTotal?: number; - processedRows?: number; - totalRows?: number; - currentCountry?: string; - currentAsin?: string; - countries: DeleteBrandProcessedCountry[]; -} - -export interface DeleteBrandSubmitResultRequest { - submissionId?: string; - files: DeleteBrandResultFile[]; -} - -export interface DeleteBrandLineProgressInfo { - file_index?: number; - file_total?: number; - file_name?: string; - current_line?: number; - total_lines?: number; - current_country?: string; - current_asin?: string; - finished_files?: number; - phase?: string; -} - -export interface DeleteBrandLineProgress { - has_progress: boolean; - info?: DeleteBrandLineProgressInfo; -} - -export interface DeleteBrandTaskItem { - id: number; - taskNo?: string; - status?: string; - sourceFileCount?: number; - successFileCount?: number; - failedFileCount?: number; - errorMessage?: string; - createdAt?: string; - updatedAt?: string; - finishedAt?: string; - downloadUrl?: string; - downloadFilename?: string; -} - -export interface DeleteBrandTaskFileProgress { - fileKey?: string; - sourceFilename?: string; - processedRows?: number; - totalRows?: number; - percent?: number; - status?: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED"; -} - -export interface DeleteBrandTaskDetailVo { - task: DeleteBrandTaskItem; - line_progress: DeleteBrandLineProgress; - items?: DeleteBrandResultItem[]; - fileProgress?: DeleteBrandTaskFileProgress[]; -} - -export interface DeleteBrandTaskBatchVo { - items: DeleteBrandTaskDetailVo[]; - missingTaskIds: number[]; -} - -export type DeleteBrandMatchStatus = - | "MATCHED" - | "PENDING" - | "CONFLICT" - | "INDEX_STALE"; - -export interface DeleteBrandResultItem { - resultId?: number; - fileKey?: string; - sourceFilename: string; - shopName?: string; - companyName?: string; - matched?: boolean; - matchStatus?: DeleteBrandMatchStatus; - matchMessage?: string; - shopId?: string; - platform?: string; - openStoreUrl?: string; - outputFilename?: string; - downloadUrl?: string; - fileJobId?: number; - fileStatus?: string; - fileError?: string; - fileReady?: boolean; - taskId?: number; - countryCount?: number; - countries?: DeleteBrandCountryGroup[]; - totalRows?: number; - truncated?: boolean; - previewRows?: DeleteBrandPreviewRow[]; - success: boolean; - error?: string; - taskStatus?: string; -} - -export interface DeleteBrandRunVo { - total: number; - successCount: number; - failedCount: number; - items: DeleteBrandResultItem[]; -} - -export interface DeleteBrandHistoryVo { - items: DeleteBrandResultItem[]; -} - -export interface DeleteBrandRunRequest { - files: UploadedFileRef[]; - user_id: number; -} - -export function runDeleteBrand( - request: Omit | DeleteBrandRunRequest, -) { - return unwrapJavaResponse( - post, DeleteBrandRunRequest>( - `${JAVA_API_PREFIX}/delete-brand/run`, - { - ...request, - user_id: getCurrentUserId(), - }, - { timeout: 180000 }, - ), - ); -} - -export function getDeleteBrandHistory() { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/delete-brand/history`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function deleteDeleteBrandHistory(resultId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/delete-brand/history/${resultId}`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function getDeleteBrandTaskDetail(taskId: number) { - return unwrapJavaResponse( - get>( - `${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}`, - ), - ); -} - -export function getDeleteBrandTaskDetails(taskIds: number[]) { - return unwrapJavaResponse( - post, { taskIds: number[] }>( - `${JAVA_API_PREFIX}/delete-brand/tasks/batch`, - { taskIds }, - ), - ); -} - -export function getDeleteBrandTaskDownloadUrl(taskId: number) { - return getJavaDownloadUrl(`/delete-brand/tasks/${taskId}/download`); -} - -export function getDeleteBrandResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/delete-brand/results/${resultId}/download`); -} - -export function submitDeleteBrandResult( - taskId: number, - request: DeleteBrandSubmitResultRequest, -) { - return unwrapJavaResponse( - post, DeleteBrandSubmitResultRequest>( - `${JAVA_API_PREFIX}/delete-brand/tasks/${taskId}/result`, - request, - ), - ); -} - /** 商品风险处理:备选店铺与匹配相关数据结构。 */ export interface ProductRiskCandidateVo { id: number; @@ -1872,13 +1647,6 @@ export function getProductRiskTaskProgressBatch(taskIds: number[]) { ); } -export function getDeleteBrandTaskProgress(taskIds: number[]) { - return postTaskProgressBatch( - `${JAVA_API_PREFIX}/delete-brand/tasks/progress/batch`, - taskIds, - ); -} - export function getPriceTrackDashboard() { return unwrapJavaResponse( get>( diff --git a/frontend-vue/src/shared/api/types/modules/delete-brand.ts b/frontend-vue/src/shared/api/types/modules/delete-brand.ts new file mode 100644 index 00000000..890086ff --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/delete-brand.ts @@ -0,0 +1,307 @@ +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 DeleteBrandPreviewRow { + rowIndex: number; + country: string; + asin: string; + status: string; +} + +export interface DeleteBrandCountryAsinItem { + rowIndex: number; + asin: string; + status: string; +} + +export interface DeleteBrandCountryGroup { + country: string; + asinCount: number; + items: DeleteBrandCountryAsinItem[]; +} + +export interface DeleteBrandCountryResultItem { + asin: string; + status?: string; +} + +export interface DeleteBrandProcessedCountry { + country: string; + items: DeleteBrandCountryResultItem[]; +} + +export interface DeleteBrandResultFile { + fileKey?: string; + sourceFilename: string; + fileIndex?: number; + fileTotal?: number; + chunkIndex?: number; + chunkTotal?: number; + processedRows?: number; + totalRows?: number; + currentCountry?: string; + currentAsin?: string; + countries: DeleteBrandProcessedCountry[]; +} + +export interface DeleteBrandSubmitResultRequest { + submissionId?: string; + files: DeleteBrandResultFile[]; +} + +export interface DeleteBrandLineProgressInfo { + file_index?: number; + file_total?: number; + file_name?: string; + current_line?: number; + total_lines?: number; + current_country?: string; + current_asin?: string; + finished_files?: number; + phase?: string; +} + +export interface DeleteBrandLineProgress { + has_progress: boolean; + info?: DeleteBrandLineProgressInfo; +} + +export interface DeleteBrandTaskItem { + id: number; + taskNo?: string; + status?: string; + sourceFileCount?: number; + successFileCount?: number; + failedFileCount?: number; + errorMessage?: string; + createdAt?: string; + updatedAt?: string; + finishedAt?: string; + downloadUrl?: string; + downloadFilename?: string; +} + +export interface DeleteBrandTaskFileProgress { + fileKey?: string; + sourceFilename?: string; + processedRows?: number; + totalRows?: number; + percent?: number; + status?: "PENDING" | "RUNNING" | "COMPLETED" | "FAILED"; +} + +export interface DeleteBrandTaskDetailVo { + task: DeleteBrandTaskItem; + line_progress: DeleteBrandLineProgress; + items?: DeleteBrandResultItem[]; + fileProgress?: DeleteBrandTaskFileProgress[]; +} + +export interface DeleteBrandTaskBatchVo { + items: DeleteBrandTaskDetailVo[]; + missingTaskIds: number[]; +} + +export type DeleteBrandMatchStatus = + | "MATCHED" + | "PENDING" + | "CONFLICT" + | "INDEX_STALE"; + +export interface DeleteBrandResultItem { + resultId?: number; + fileKey?: string; + sourceFilename: string; + shopName?: string; + companyName?: string; + matched?: boolean; + matchStatus?: DeleteBrandMatchStatus; + matchMessage?: string; + shopId?: string; + platform?: string; + openStoreUrl?: string; + outputFilename?: string; + downloadUrl?: string; + fileJobId?: number; + fileStatus?: string; + fileError?: string; + fileReady?: boolean; + taskId?: number; + countryCount?: number; + countries?: DeleteBrandCountryGroup[]; + totalRows?: number; + truncated?: boolean; + previewRows?: DeleteBrandPreviewRow[]; + success: boolean; + error?: string; + taskStatus?: string; +} + +export interface DeleteBrandRunVo { + total: number; + successCount: number; + failedCount: number; + items: DeleteBrandResultItem[]; +} + +export interface DeleteBrandHistoryVo { + items: DeleteBrandResultItem[]; +} + +export interface DeleteBrandRunRequest { + files: UploadedFileRef[]; + user_id: 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 runDeleteBrand( + request: Omit | DeleteBrandRunRequest, +) { + return unwrapJavaResponse( + post, DeleteBrandRunRequest>( + buildJavaUrl(API_ENDPOINTS.deleteBrand.run), + { + ...request, + user_id: getCurrentUserId(), + }, + { timeout: 180000 }, + ), + ); +} + +export function getDeleteBrandHistory() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.deleteBrand.history), + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function deleteDeleteBrandHistory(resultId: number) { + return unwrapJavaResponse( + del>( + buildJavaUrl(API_ENDPOINTS.deleteBrand.historyDelete.replace('{resultId}', String(resultId))), + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function getDeleteBrandTaskDetail(taskId: number) { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.deleteBrand.taskDetail.replace('{taskId}', String(taskId))), + ), + ); +} + +export function getDeleteBrandTaskDetails(taskIds: number[]) { + return unwrapJavaResponse( + post, { taskIds: number[] }>( + buildJavaUrl(API_ENDPOINTS.deleteBrand.taskDetails), + { taskIds }, + ), + ); +} + +export function getDeleteBrandTaskDownloadUrl(taskId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.deleteBrand.taskDownload.replace('{taskId}', String(taskId)), + ); +} + +export function getDeleteBrandResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.deleteBrand.resultDownload.replace('{resultId}', String(resultId)), + ); +} + +export function submitDeleteBrandResult( + taskId: number, + request: DeleteBrandSubmitResultRequest, +) { + return unwrapJavaResponse( + post, DeleteBrandSubmitResultRequest>( + buildJavaUrl(API_ENDPOINTS.deleteBrand.resultSubmit.replace('{taskId}', String(taskId))), + request, + ), + ); +} + +export function getDeleteBrandTaskProgress(taskIds: number[]) { + return postTaskProgressBatch( + `${JAVA_API_PREFIX}/delete-brand/tasks/progress/batch`, + taskIds, + ); +} diff --git a/frontend-vue/tests/modules-delete-brand.test.ts b/frontend-vue/tests/modules-delete-brand.test.ts new file mode 100644 index 00000000..119fc2e0 --- /dev/null +++ b/frontend-vue/tests/modules-delete-brand.test.ts @@ -0,0 +1,167 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + runDeleteBrand, + getDeleteBrandHistory, + deleteDeleteBrandHistory, + getDeleteBrandTaskDetail, + getDeleteBrandTaskDetails, + getDeleteBrandTaskDownloadUrl, + getDeleteBrandResultDownloadUrl, + submitDeleteBrandResult, + getDeleteBrandTaskProgress, + type DeleteBrandRunRequest, + type DeleteBrandSubmitResultRequest, +} from '../src/shared/api/types/modules/delete-brand.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 runRequest: Omit = { + files: [{ fileKey: 'k1' }], +} + +test('test_delete_brand_run_url_payload', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown; timeout?: number } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ total: 1, successCount: 0, failedCount: 1, items: [] }) + }) + await runDeleteBrand(runRequest) + assert.equal(captured.url, '/newApi/api/delete-brand/run') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { ...runRequest, user_id: 42 }) + assert.equal(captured.timeout, 180000) +}) + +test('test_delete_brand_history', async (t) => { + setupWindow() + let captured: { url?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [] }) + }) + await getDeleteBrandHistory() + assert.equal(captured.url, '/newApi/api/delete-brand/history') + assert.deepEqual(captured.params, { user_id: 42 }) +}) + +test('test_delete_brand_delete_history', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await deleteDeleteBrandHistory(7) + assert.equal(captured.url, '/newApi/api/delete-brand/history/7') + assert.equal(captured.method, 'DELETE') + assert.deepEqual(captured.params, { user_id: 42 }) +}) + +test('test_delete_brand_task_detail', async (t) => { + setupWindow() + let captured: { url?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ task: { id: 7 }, line_progress: { has_progress: true }, items: [] }) + }) + const result = await getDeleteBrandTaskDetail(7) + assert.equal(captured.url, '/newApi/api/delete-brand/tasks/7') + assert.equal(result.task.id, 7) + assert.equal(result.line_progress.has_progress, true) +}) + +test('test_delete_brand_task_details', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [], missingTaskIds: [] }) + }) + await getDeleteBrandTaskDetails([5, 3]) + assert.equal(captured.url, '/newApi/api/delete-brand/tasks/batch') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { taskIds: [5, 3] }) +}) + +test('test_delete_brand_task_download_url', async (t) => { + setupWindow() + assert.equal( + getDeleteBrandTaskDownloadUrl(7), + 'http://localhost/newApi/api/delete-brand/tasks/7/download?user_id=42', + ) +}) + +test('test_delete_brand_result_download_url', async (t) => { + setupWindow() + assert.equal( + getDeleteBrandResultDownloadUrl(9), + 'http://localhost/newApi/api/delete-brand/results/9/download?user_id=42', + ) +}) + +test('test_delete_brand_submit_result_body', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + const body: DeleteBrandSubmitResultRequest = { + submissionId: 'sub-1', + files: [{ sourceFilename: 'a.xlsx', countries: [] }], + } + await submitDeleteBrandResult(7, body) + assert.equal(captured.url, '/newApi/api/delete-brand/tasks/7/result') + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, body) +}) + +test('test_delete_brand_progress_batch', 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 getDeleteBrandTaskProgress([5, 3]) + assert.equal(captured.url, '/newApi/api/delete-brand/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_delete_brand_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromDeleteBrand = await import('../src/shared/api/types/modules/delete-brand.ts') + const names = [ + 'runDeleteBrand', 'getDeleteBrandHistory', 'deleteDeleteBrandHistory', + 'getDeleteBrandTaskDetail', 'getDeleteBrandTaskDetails', 'getDeleteBrandTaskDownloadUrl', + 'getDeleteBrandResultDownloadUrl', 'submitDeleteBrandResult', 'getDeleteBrandTaskProgress', + ] + for (const name of names) { + assert.equal(fromJavaModules[name], fromDeleteBrand[name], `${name} 应为同一引用`) + } + const item = { id: 1, taskNo: 'T1', status: 'RUNNING' } + assert.equal(item.taskNo, 'T1') + const status: 'MATCHED' | 'PENDING' = 'MATCHED' + assert.equal(status, 'MATCHED') +})