From a4ca9d5a19cf91f2f78cc09481841050fd7eb148 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:27:39 +0800 Subject: [PATCH] =?UTF-8?q?task-11:=20=E6=8B=86=E5=88=86=20dedupe=20?= =?UTF-8?q?=E6=A8=A1=E5=9D=97=20API=20=E8=87=B3=20types/modules/dedupe.ts?= =?UTF-8?q?=EF=BC=88callJava+buildJavaUrl=20=E5=8C=96=EF=BC=89=EF=BC=8Cjav?= =?UTF-8?q?a-modules.ts=20=E6=94=B9=20re-export=20=E4=BF=9D=E6=8C=81?= =?UTF-8?q?=E6=97=A7=20import=20=E5=85=BC=E5=AE=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend-vue/src/shared/api/download-url.ts | 19 +++ frontend-vue/src/shared/api/java-modules.ts | 73 +---------- .../src/shared/api/types/modules/dedupe.ts | 78 +++++++++++ frontend-vue/src/shared/api/types/upload.ts | 5 + frontend-vue/src/shared/api/user.ts | 11 ++ frontend-vue/tests/modules-dedupe.test.ts | 122 ++++++++++++++++++ 6 files changed, 240 insertions(+), 68 deletions(-) create mode 100644 frontend-vue/src/shared/api/download-url.ts create mode 100644 frontend-vue/src/shared/api/types/modules/dedupe.ts create mode 100644 frontend-vue/src/shared/api/types/upload.ts create mode 100644 frontend-vue/src/shared/api/user.ts create mode 100644 frontend-vue/tests/modules-dedupe.test.ts diff --git a/frontend-vue/src/shared/api/download-url.ts b/frontend-vue/src/shared/api/download-url.ts new file mode 100644 index 00000000..89a6da87 --- /dev/null +++ b/frontend-vue/src/shared/api/download-url.ts @@ -0,0 +1,19 @@ +import { buildJavaUrl } from './url.ts' +import { getCurrentUserId } from './user.ts' + +export function getJavaDownloadUrl(path: string) { + let raw = + path.startsWith("http://") || path.startsWith("https://") + ? path + : buildJavaUrl(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()))}`; +} diff --git a/frontend-vue/src/shared/api/java-modules.ts b/frontend-vue/src/shared/api/java-modules.ts index 332569d4..0ddc47b8 100644 --- a/frontend-vue/src/shared/api/java-modules.ts +++ b/frontend-vue/src/shared/api/java-modules.ts @@ -6,9 +6,11 @@ put, type JavaApiResponse, unwrapJavaResponse, -} from "@/shared/api/http"; -import { getTaskProgressCacheTtlMs, getTaskProgressTimeoutMs } from "@/shared/task-progress-config"; -import { createTaskProgressRequestCache } from "@/shared/task-progress-request-cache"; +} 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"; const JAVA_API_PREFIX = "/newApi/api"; @@ -142,35 +144,6 @@ export async function uploadTempFileToJava( return response.data } -export interface DedupeResultItem { - resultId?: number; - sourceFilename: string; - outputFilename?: string; - success: boolean; - error?: string; - downloadUrl?: string; -} - -export interface DedupeRunVo { - total: number; - successCount: number; - failedCount: number; - items: DedupeResultItem[]; -} - -export interface DedupeHistoryVo { - items: DedupeResultItem[]; -} - -export interface DedupeRunRequest { - files: UploadedFileRef[]; - selectedColumns: string[]; - keepIntegerIds: boolean; - keepUnderscoreIds: boolean; - keepIntegerMainIdsWhenNoSubIds: boolean; - archiveName?: string; - user_id: number; -} export interface SplitArchiveEntry { filename: string; @@ -416,42 +389,6 @@ export function getExcelInfo(fileKey: string) { ); } -export function runDedupe( - request: Omit | DedupeRunRequest, -) { - return unwrapJavaResponse( - post, DedupeRunRequest>( - `${JAVA_API_PREFIX}/dedupe/run`, - { - ...request, - user_id: getCurrentUserId(), - }, - ), - ); -} - -export function getDedupeHistory() { - return unwrapJavaResponse( - get>(`${JAVA_API_PREFIX}/dedupe/history`, { - params: { user_id: getCurrentUserId() }, - }), - ); -} - -export function deleteDedupeHistory(resultId: number) { - return unwrapJavaResponse( - del>( - `${JAVA_API_PREFIX}/dedupe/history/${resultId}`, - { - params: { user_id: getCurrentUserId() }, - }, - ), - ); -} - -export function getDedupeResultDownloadUrl(resultId: number) { - return getJavaDownloadUrl(`/dedupe/results/${resultId}/download`); -} export function runSplit( request: Omit | SplitRunRequest, diff --git a/frontend-vue/src/shared/api/types/modules/dedupe.ts b/frontend-vue/src/shared/api/types/modules/dedupe.ts new file mode 100644 index 00000000..7b392a75 --- /dev/null +++ b/frontend-vue/src/shared/api/types/modules/dedupe.ts @@ -0,0 +1,78 @@ +import { get, post, del, type JavaApiResponse, unwrapJavaResponse } from '../../http.ts' +import { buildJavaUrl } from '../../url.ts' +import { getCurrentUserId } from '../../user.ts' +import { getJavaDownloadUrl } from '../../download-url.ts' +import type { UploadedFileRef } from '../upload.ts' +import { API_ENDPOINTS } from '../../endpoints.ts' + +export interface DedupeResultItem { + resultId?: number; + sourceFilename: string; + outputFilename?: string; + success: boolean; + error?: string; + downloadUrl?: string; +} + +export interface DedupeRunVo { + total: number; + successCount: number; + failedCount: number; + items: DedupeResultItem[]; +} + +export interface DedupeHistoryVo { + items: DedupeResultItem[]; +} + +export interface DedupeRunRequest { + files: UploadedFileRef[]; + selectedColumns: string[]; + keepIntegerIds: boolean; + keepUnderscoreIds: boolean; + keepIntegerMainIdsWhenNoSubIds: boolean; + archiveName?: string; + user_id: number; +} + +export function runDedupe( + request: Omit | DedupeRunRequest, +) { + return unwrapJavaResponse( + post, DedupeRunRequest>( + buildJavaUrl(API_ENDPOINTS.dedupe.run), + { + ...request, + user_id: getCurrentUserId(), + }, + ), + ); +} + +export function getDedupeHistory() { + return unwrapJavaResponse( + get>( + buildJavaUrl(API_ENDPOINTS.dedupe.history), + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function deleteDedupeHistory(resultId: number) { + return unwrapJavaResponse( + del>( + buildJavaUrl(API_ENDPOINTS.dedupe.historyDelete.replace('{resultId}', String(resultId))), + { + params: { user_id: getCurrentUserId() }, + }, + ), + ); +} + +export function getDedupeResultDownloadUrl(resultId: number) { + return getJavaDownloadUrl( + API_ENDPOINTS.dedupe.resultDownload.replace('{resultId}', String(resultId)), + ); +} diff --git a/frontend-vue/src/shared/api/types/upload.ts b/frontend-vue/src/shared/api/types/upload.ts new file mode 100644 index 00000000..11b0bc27 --- /dev/null +++ b/frontend-vue/src/shared/api/types/upload.ts @@ -0,0 +1,5 @@ +export interface UploadedFileRef { + fileKey: string; + originalFilename?: string; + relativePath?: string; +} diff --git a/frontend-vue/src/shared/api/user.ts b/frontend-vue/src/shared/api/user.ts new file mode 100644 index 00000000..0c745c99 --- /dev/null +++ b/frontend-vue/src/shared/api/user.ts @@ -0,0 +1,11 @@ +export 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; +} diff --git a/frontend-vue/tests/modules-dedupe.test.ts b/frontend-vue/tests/modules-dedupe.test.ts new file mode 100644 index 00000000..761ebf34 --- /dev/null +++ b/frontend-vue/tests/modules-dedupe.test.ts @@ -0,0 +1,122 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' +import { http } from '../src/shared/api/http.ts' +import { + runDedupe, + getDedupeHistory, + deleteDedupeHistory, + getDedupeResultDownloadUrl, + type DedupeRunRequest, + type DedupeRunVo, +} from '../src/shared/api/types/modules/dedupe.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 }) => Promise, +) { + t.mock.method(http, 'request', impl as never) +} + +const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } }) + +test('test_dedupe_run_url', async (t) => { + setupWindow() + let captured: { url?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ total: 1, successCount: 1, failedCount: 0, items: [] }) + }) + await runDedupe({ files: [{ fileKey: 'k1' }], selectedColumns: ['a'], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: true }) + assert.equal(captured.url, '/newApi/api/dedupe/run') +}) + +test('test_dedupe_run_method_payload', async (t) => { + setupWindow() + let captured: { method?: string; data?: unknown } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ total: 0, successCount: 0, failedCount: 0, items: [] }) + }) + await runDedupe({ files: [{ fileKey: 'k1' }], selectedColumns: ['a'], keepIntegerIds: true, keepUnderscoreIds: true, keepIntegerMainIdsWhenNoSubIds: false }) + assert.equal(captured.method, 'POST') + assert.deepEqual(captured.data, { + files: [{ fileKey: 'k1' }], + selectedColumns: ['a'], + keepIntegerIds: true, + keepUnderscoreIds: true, + keepIntegerMainIdsWhenNoSubIds: false, + user_id: 42, + }) +}) + +test('test_dedupe_history_url', async (t) => { + setupWindow() + let captured: { url?: string; method?: string; params?: Record } = {} + mockRequest(t, (config) => { + captured = config + return okResponse({ items: [] }) + }) + await getDedupeHistory() + assert.equal(captured.url, '/newApi/api/dedupe/history') + assert.equal(captured.method, 'GET') + assert.deepEqual(captured.params, { user_id: 42 }) +}) + +test('test_dedupe_delete_history', async (t) => { + setupWindow() + let captured: { url?: string; method?: string } = {} + mockRequest(t, (config) => { + captured = config + return okResponse(null) + }) + await deleteDedupeHistory(5) + assert.equal(captured.url, '/newApi/api/dedupe/history/5') + assert.equal(captured.method, 'DELETE') +}) + +test('test_dedupe_download_url', async (t) => { + setupWindow() + const url = getDedupeResultDownloadUrl(5) + assert.equal(url, 'http://localhost/newApi/api/dedupe/results/5/download?user_id=42') +}) + +test('test_dedupe_signature_unchanged', async (t) => { + setupWindow() + assert.equal(runDedupe.length, 1, 'runDedupe 应保持单参数') + assert.equal(getDedupeHistory.length, 0, 'getDedupeHistory 应保持无参') + assert.equal(deleteDedupeHistory.length, 1, 'deleteDedupeHistory 应保持单参数') + assert.equal(getDedupeResultDownloadUrl.length, 1, 'getDedupeResultDownloadUrl 应保持单参数') + const request: DedupeRunRequest = { + files: [{ fileKey: 'k' }], + selectedColumns: [], + keepIntegerIds: false, + keepUnderscoreIds: false, + keepIntegerMainIdsWhenNoSubIds: false, + user_id: 1, + } + assert.ok(request.user_id === 1) +}) + +test('test_dedupe_export_compat', async (t) => { + setupWindow() + const fromJavaModules = await import('../src/shared/api/java-modules.ts') + const fromDedupe = await import('../src/shared/api/types/modules/dedupe.ts') + assert.equal(fromJavaModules.runDedupe, fromDedupe.runDedupe, '旧路径 import 应与新路径同引用') + assert.equal(fromJavaModules.getDedupeHistory, fromDedupe.getDedupeHistory) + assert.equal(fromJavaModules.deleteDedupeHistory, fromDedupe.deleteDedupeHistory) + assert.equal(fromJavaModules.getDedupeResultDownloadUrl, fromDedupe.getDedupeResultDownloadUrl) +}) + +test('test_dedupe_unwrap', async (t) => { + setupWindow() + mockRequest(t, () => okResponse({ total: 2, successCount: 1, failedCount: 1, items: [{ success: true }] })) + const result = await runDedupe({ files: [{ fileKey: 'k' }], selectedColumns: [], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: false }) + assert.deepEqual(result, { total: 2, successCount: 1, failedCount: 1, items: [{ success: true }] }, '应返回解包后的 data') +})