task-24: java-modules.ts 改为纯 re-export 聚合器,upload helpers 移至 upload.ts
This commit is contained in:
@@ -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/dedupe.ts";
|
||||||
export * from "./types/modules/split.ts";
|
export * from "./types/modules/split.ts";
|
||||||
export * from "./types/modules/convert.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/brand.ts";
|
||||||
export * from "./types/modules/permission.ts";
|
export * from "./types/modules/permission.ts";
|
||||||
export * from "./types/modules/digital-human.ts";
|
export * from "./types/modules/digital-human.ts";
|
||||||
import type {
|
export * from "./upload.ts";
|
||||||
ProductRiskCandidateVo,
|
export * from "./download-url.ts";
|
||||||
ProductRiskDashboardVo,
|
export { JAVA_API_PREFIX } from "./url.ts";
|
||||||
ProductRiskMatchShopsVo,
|
export { http } from "./http.ts";
|
||||||
ProductRiskShopQueueItem,
|
export type { JavaApiResponse } from "./http.ts";
|
||||||
} from "./types/modules/shop-match.ts";
|
export type {
|
||||||
import type {
|
ApiResponse,
|
||||||
QueryAsinCandidateVo,
|
LegacyApiSuccess,
|
||||||
QueryAsinDashboardVo,
|
LegacyApiFailure,
|
||||||
} from "./types/modules/query-asin.ts";
|
} from "./http.ts";
|
||||||
|
|
||||||
const JAVA_API_PREFIX = "/newApi/api";
|
|
||||||
|
|
||||||
interface TaskProgressBatchOptions {
|
|
||||||
force?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** 进度批量接口的响应缓存与并发合并:TTL 过期、有界条目、in-flight 去重 */
|
|
||||||
const taskProgressResponseCache = createTaskProgressRequestCache<unknown>({
|
|
||||||
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<T>(
|
|
||||||
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<JavaApiResponse<T>, { 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<JavaApiResponse<UploadFileVo>>(
|
|
||||||
`${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 };
|
|
||||||
|
|
||||||
|
|||||||
@@ -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<JavaApiResponse<UploadFileVo>>(
|
||||||
|
`${JAVA_API_PREFIX}/files/upload`,
|
||||||
|
formData,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'multipart/form-data',
|
||||||
|
},
|
||||||
|
timeout: 120000,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return response.data
|
||||||
|
}
|
||||||
@@ -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<string, unknown>).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<string, unknown>).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)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user