diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 848bde50..1f9a73e5 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -1,15 +1,3 @@ -import { - del, - get, - http, - post, - put, - type JavaApiResponse, - unwrapJavaResponse, -} from "./http.ts"; -import { getTaskProgressCacheTtlMs, getTaskProgressTimeoutMs } from "../task-progress-config.ts"; -import { createTaskProgressRequestCache } from "../task-progress-request-cache.ts"; - export * from "./types/modules/dedupe.ts"; export * from "./types/modules/split.ts"; export * from "./types/modules/convert.ts"; @@ -26,166 +14,13 @@ export * from "./types/modules/image-video.ts"; export * from "./types/modules/brand.ts"; export * from "./types/modules/permission.ts"; export * from "./types/modules/digital-human.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"; - -interface TaskProgressBatchOptions { - force?: boolean; -} - -/** 进度批量接口的响应缓存与并发合并:TTL 过期、有界条目、in-flight 去重 */ -const taskProgressResponseCache = createTaskProgressRequestCache({ - ttlMs: () => getTaskProgressCacheTtlMs(), - maxEntries: 100, - maxInflight: 16, -}); - -function getCurrentUserId() { - const raw = - typeof window === "undefined" - ? "" - : window.localStorage.getItem("uid") || ""; - const value = Number(raw); - if (!Number.isFinite(value) || value <= 0) { - throw new Error("未获取到用户ID"); - } - return value; -} - -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 interface UploadedFileRef { - fileKey: string; - originalFilename?: string; - relativePath?: string; -} - -export interface UploadFileVo { - fileKey: string; - originalFilename: string; - localPath: string; - size: number; - relativePath?: string; - objectKey?: string; - url?: string; - mediaType?: "image" | "video" | "audio" | "file" | string; -} - -export interface UploadTempFileOptions { - relativePath?: string; - uploadToOss?: boolean; - moduleType?: string; -} - -export async function uploadTempFileToJava( - file: File, - relativePathOrOptions?: string | UploadTempFileOptions, -) { - const options = - typeof relativePathOrOptions === 'string' - ? { relativePath: relativePathOrOptions } - : relativePathOrOptions || {} - const formData = new FormData() - formData.append('file', file) - if (options.relativePath) { - formData.append('relativePath', options.relativePath) - } - if (options.uploadToOss) { - formData.append('uploadToOss', 'true') - } - if (options.moduleType) { - formData.append('moduleType', options.moduleType) - } - const response = await http.post>( - `${JAVA_API_PREFIX}/files/upload`, - formData, - { - headers: { - 'Content-Type': 'multipart/form-data', - }, - timeout: 120000, - }, - ) - return response.data -} - - -export function getJavaDownloadUrl(path: string) { - let raw = - path.startsWith("http://") || path.startsWith("https://") - ? path - : `${JAVA_API_PREFIX}${path}`; - // pywebview 的 save_file_from_url_new 需要完整且带 schema 的 URL - if ( - !raw.startsWith("http://") && - !raw.startsWith("https://") && - typeof window !== "undefined" - ) { - raw = `${window.location.origin}${raw}`; - } - const separator = raw.includes("?") ? "&" : "?"; - return `${raw}${separator}user_id=${encodeURIComponent(String(getCurrentUserId()))}`; -} - -export { JAVA_API_PREFIX, http }; - +export * from "./upload.ts"; +export * from "./download-url.ts"; +export { JAVA_API_PREFIX } from "./url.ts"; +export { http } from "./http.ts"; +export type { JavaApiResponse } from "./http.ts"; +export type { + ApiResponse, + LegacyApiSuccess, + LegacyApiFailure, +} from "./http.ts"; diff --git a/frontend-vue/src/shared/api/upload.ts b/frontend-vue/src/shared/api/upload.ts new file mode 100644 index 00000000..be0adff6 --- /dev/null +++ b/frontend-vue/src/shared/api/upload.ts @@ -0,0 +1,57 @@ +import { http, type JavaApiResponse } from './http.ts' +import { JAVA_API_PREFIX } from './url.ts' + +export interface UploadedFileRef { + fileKey: string; + originalFilename?: string; + relativePath?: string; +} + +export interface UploadFileVo { + fileKey: string; + originalFilename: string; + localPath: string; + size: number; + relativePath?: string; + objectKey?: string; + url?: string; + mediaType?: "image" | "video" | "audio" | "file" | string; +} + +export interface UploadTempFileOptions { + relativePath?: string; + uploadToOss?: boolean; + moduleType?: string; +} + +export async function uploadTempFileToJava( + file: File, + relativePathOrOptions?: string | UploadTempFileOptions, +) { + const options = + typeof relativePathOrOptions === 'string' + ? { relativePath: relativePathOrOptions } + : relativePathOrOptions || {} + const formData = new FormData() + formData.append('file', file) + if (options.relativePath) { + formData.append('relativePath', options.relativePath) + } + if (options.uploadToOss) { + formData.append('uploadToOss', 'true') + } + if (options.moduleType) { + formData.append('moduleType', options.moduleType) + } + const response = await http.post>( + `${JAVA_API_PREFIX}/files/upload`, + formData, + { + headers: { + 'Content-Type': 'multipart/form-data', + }, + timeout: 120000, + }, + ) + return response.data +} diff --git a/frontend-vue/tests/java-modules-reexport.test.ts b/frontend-vue/tests/java-modules-reexport.test.ts new file mode 100644 index 00000000..85c3f126 --- /dev/null +++ b/frontend-vue/tests/java-modules-reexport.test.ts @@ -0,0 +1,126 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { fileURLToPath } from 'node:url' +import { resolve, dirname } from 'node:path' +import { http } from '../src/shared/api/http.ts' +import { http as httpAgain } from '../src/shared/api/http.ts' + +const moduleFiles = [ + 'dedupe', 'split', 'convert', 'publish', 'similar-asin', 'appearance-patent', + 'delete-brand', 'price-track', 'shop-match', 'query-asin', 'withdraw', + 'collect-data', 'image-video', 'brand', 'permission', 'digital-human', +] + +const functionNames = [ + 'runDedupe', 'getSplitHistory', 'runConvert', 'parsePublish', + 'parseSimilarAsin', 'parseAppearancePatent', + 'runDeleteBrand', 'listPriceTrackCandidates', 'listProductRiskCandidates', + 'listShopMatchCandidates', 'listQueryAsinCandidates', 'listWithdrawCandidates', + 'listShopDataCrawlCandidates', 'parseCollectData', 'getImageVideoSecretStatus', + 'expandBrandFolderRecursive', 'getCurrentUserAppColumnKeys', + 'getDigitalHumanVersions', 'uploadTempFileToJava', 'getJavaDownloadUrl', + 'JAVA_API_PREFIX', 'http', +] + +test('test_reexport_all_symbols', async () => { + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + for (const name of functionNames) { + assert.ok(name in fromJavaModules, `${name} 应从 java-modules 导出`) + } +}) + +test('test_reexport_same_reference', async () => { + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + for (const file of moduleFiles) { + const fromModule = await import(`../src/shared/api/types/modules/${file}.ts`) + const reexports = Object.keys(fromJavaModules).filter( + (k) => k in fromModule && typeof fromJavaModules[k] === 'function', + ) + for (const name of reexports) { + assert.equal(fromJavaModules[name], fromModule[name], `${file}:${name} 应为同一引用`) + } + } +}) + +test('test_reexport_upload_helpers', async (t) => { + ;(globalThis as Record).window = { + localStorage: { getItem: () => '42' }, + location: { origin: 'http://localhost' }, + } + const calls: { url?: string; method?: string; data?: unknown }[] = [] + t.mock.method(http, 'post', ((url: string, data?: unknown, config?: unknown) => { + calls.push({ url, method: 'POST', data }) + return Promise.resolve({ data: { success: true, message: 'ok', data: { fileKey: 'f1', originalFilename: 'a.xlsx', localPath: '/tmp/a.xlsx', size: 1 } } }) + }) as never) + + const { uploadTempFileToJava } = await import('../src/shared/api/java-modules.ts') + const { uploadTempFileToJava: fromUpload } = await import('../src/shared/api/upload.ts') + assert.equal(uploadTempFileToJava, fromUpload) + const file = new File(['x'], 'a.xlsx') + const result = await uploadTempFileToJava(file, 'sub/') + assert.equal(calls[0].url, '/newApi/api/files/upload') + assert.equal(calls[0].method, 'POST') + assert.ok(calls[0].data instanceof FormData) + assert.equal(result.success, true) + assert.equal(result.data.fileKey, 'f1') +}) +test('test_reexport_download_and_constants', async () => { + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const { getJavaDownloadUrl } = await import('../src/shared/api/download-url.ts') + const { JAVA_API_PREFIX } = await import('../src/shared/api/url.ts') + assert.equal(fromJavaModules.getJavaDownloadUrl, getJavaDownloadUrl) + assert.equal(fromJavaModules.JAVA_API_PREFIX, JAVA_API_PREFIX) + assert.equal(fromJavaModules.http, httpAgain) +}) + +test('test_reexport_no_dead_body', async () => { + const here = dirname(fileURLToPath(import.meta.url)) + const source = readFileSync(resolve(here, '../src/shared/api/java-modules.ts'), 'utf-8') + const deadMarkers = [ + 'function postTaskProgressBatch', + 'const taskProgressResponseCache', + 'function getCurrentUserId', + 'function normalizeTaskIds', + 'function buildTaskProgressRequestKey', + 'async function uploadTempFileToJava', + 'export function getJavaDownloadUrl', + 'JAVA_API_PREFIX = "/newApi/api"', + ] + for (const marker of deadMarkers) { + assert.ok(!source.includes(marker), `java-modules.ts 不应残留: ${marker}`) + } +}) + +test('test_reexport_no_duplicate_exports', async () => { + const here = dirname(fileURLToPath(import.meta.url)) + const source = readFileSync(resolve(here, '../src/shared/api/java-modules.ts'), 'utf-8') + const exportLines = source + .split('\n') + .filter((line) => line.includes('export * from')) + .map((line) => line.match(/export \* from "\.\/([^"]+)"/)?.[1]) + .filter(Boolean) + assert.ok(exportLines.length >= 13, `应有 ≥13 个模块 re-export,实际 ${exportLines.length}`) + assert.equal(new Set(exportLines).size, exportLines.length, '不应有重复的 re-export') +}) + +test('test_reexport_runtime_symbols', async (t) => { + ;(globalThis as Record).window = { + localStorage: { getItem: () => '42' }, + location: { origin: 'http://localhost' }, + } + t.mock.method(http, 'request', (() => { + return Promise.resolve({ data: { success: true, message: 'ok', data: { items: [] } } }) + }) as never) + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const result = await fromJavaModules.getWithdrawHistory() + assert.deepEqual(result.items, []) +}) + +test('test_reexport_upload_types', async () => { + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromUpload = await import('../src/shared/api/upload.ts') + assert.equal(fromJavaModules.UploadFileVo, fromUpload.UploadFileVo) + assert.equal(fromJavaModules.UploadedFileRef, fromUpload.UploadedFileRef) + assert.equal(fromJavaModules.UploadTempFileOptions, fromUpload.UploadTempFileOptions) +})