后台店铺数据记录页新增 Tab 切换与重复 ASIN 分析;task-169/170 HTTP 客户端配置接入;dedupe/brand 相关改造
Build Backend JAR / build (push) Has been cancelled

- 管理后台:店铺数据记录改为列表分页展示(去任务号/文件列/累计标题,'最新'改'更新时间'),新增'重复 ASIN' Tab 按跨店聚合展示 ASIN/店铺/分组/国家/日期/价格;修复失败/进行中任务被过滤不显示的问题
- task-169: BrandCheckHttpConfigResolver 从 aiimage.http-client.* 读取品牌检查超时/重试(默认同现状、钳制),附 8 条测试
- task-170: surefire 内存调整为 1536m
- dedupe: DedupeRunService 重构(事务边界/进度)、新增 DedupeRunProgressVo、Controller 与结果 VO 调整,PermissionMenuService 相应适配
- brand/dedupe 前端:BrandDedupeTab/BrandPatrolDeleteTab 改造、新增 CountrySelector 组件与 country-options、类型与接口端对齐、测试更新
- 移除无引用文件:backend/static/logo.jpg、prompts/
This commit is contained in:
2026-09-03 01:57:41 +08:00
parent 152ea6eec0
commit 417a2bf831
20 changed files with 1883 additions and 475 deletions
@@ -79,7 +79,7 @@
{{ cleanRunning ? '清洗中...' : '开始清洗' }}
</button>
<span class="loading-msg">
{{ cleanRunning ? '正在处理文件并上传结果,请稍候…' : '选择文件、列和 ID 规则后即可执行清洗,结果会显示在右侧供下载' }}
{{ cleanRunning ? `正在处理文件并上传结果,已处理 ${cleanProgressProcessed}/${cleanSummary.total || 0} 个文件…` : '选择文件、列和 ID 规则后即可执行清洗,结果会显示在右侧供下载' }}
</span>
</div>
</aside>
@@ -148,7 +148,7 @@ import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import BrandTopBar from './BrandTopBar.vue'
import { expandBrandFolderRecursive, type BrandExpandFolderItem } from '@/shared/api/brand'
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunVo } from '@/shared/api/java-modules'
import { deleteDedupeHistory, getDedupeHistory, getDedupeResultDownloadUrl, getDedupeRunProgress, getExcelInfo, runDedupe, type DedupeResultItem, type DedupeRunProgressVo, type DedupeRunVo } from '@/shared/api/java-modules'
import { getPywebviewApi, type UploadedJavaFile } from '@/shared/bridges/pywebview'
import { saveUrlWithProgress } from '@/shared/utils/download-progress'
import { EXCEL_EXTENSIONS, checkSelectedFiles, guardBlocked } from '@/shared/dispatch-guard'
@@ -163,6 +163,7 @@ const cleanKeepIntegerIds = ref(false)
const cleanKeepUnderscoreIds = ref(true)
const cleanKeepIntegerMainIdsWhenNoSubIds = ref(true)
const cleanRunning = ref(false)
const cleanProgressProcessed = ref(0)
const cleanResultItems = ref<DedupeResultItem[]>([])
const cleanSummary = ref<DedupeRunVo>({ total: 0, successCount: 0, failedCount: 0, items: [] })
const cleanDisplayPaths = computed(() => cleanSelectedPaths.value.slice(0, 8))
@@ -290,7 +291,7 @@ async function submitCleanRun() {
try {
cleanRunning.value = true
const result = await runDedupe({
const progress = await runDedupe({
files: cleanUploadedFiles.value.map((item) => ({ fileKey: item.fileKey, originalFilename: item.originalFilename, relativePath: item.relativePath })),
selectedColumns: cleanSelectedColumns.value,
keepIntegerIds: cleanKeepIntegerIds.value,
@@ -301,6 +302,15 @@ async function submitCleanRun() {
? cleanArchiveName.value
: undefined,
})
const result = await pollDedupeRunProgress(progress.runId, (latest) => {
cleanProgressProcessed.value = latest.processedCount
cleanSummary.value = {
total: latest.total,
successCount: latest.successCount,
failedCount: latest.failedCount,
items: cleanResultItems.value,
}
})
cleanSummary.value = result
cleanResultItems.value = result.items || []
await loadCleanHistory()
@@ -325,6 +335,36 @@ async function submitCleanRun() {
}
}
// 去重任务进度轮询:2 秒一次,10 分钟超时(超时任务由后端继续执行,结果可在历史列表中查看)
const DEDUPE_POLL_INTERVAL_MS = 2000
const DEDUPE_POLL_TIMEOUT_MS = 10 * 60 * 1000
async function pollDedupeRunProgress(runId: string, onProgress?: (progress: DedupeRunProgressVo) => void): Promise<DedupeRunVo> {
const deadline = Date.now() + DEDUPE_POLL_TIMEOUT_MS
while (true) {
const progress = await getDedupeRunProgress(runId)
if (progress.status === 'not_found') {
throw new Error('去重任务不存在或已过期')
}
if (progress.status !== 'running') {
if (progress.status === 'failed') {
throw new Error(progress.error || '去重任务执行失败')
}
if (!progress.result) {
throw new Error(progress.error || '去重任务未返回结果')
}
return progress.result
}
onProgress?.(progress)
if (Date.now() > deadline) {
throw new Error(
`去重任务仍在处理中(已处理 ${progress.processedCount}/${progress.total} 个文件),请稍后在历史列表中查看结果`,
)
}
await new Promise((resolve) => setTimeout(resolve, DEDUPE_POLL_INTERVAL_MS))
}
}
async function loadCleanHistory() {
try {
const response = await getDedupeHistory()
@@ -7,7 +7,7 @@
<div class="section-title">店铺输入</div>
<div class="input-zone">
<div class="hint">
左侧负责录入店铺并加入备选区点击匹配店铺只会生成匹配结果确认后需手动点击推送到 Python 队列才会创建并执行任务
左侧负责录入店铺并加入备选区点击匹配店铺只会生成匹配结果确认后需手动点击推送到 Python 队列才会创建并执行任务任务只会跑下方勾选的国家
</div>
<div class="input-row">
<el-input
@@ -113,6 +113,12 @@
</ul>
</div>
<CountrySelector
v-model="selectedCountryCodes"
title="巡店国家与顺序"
min-selected-warning="至少保留 1 个国家否则任务没有可巡查的站点"
/>
<ZiniaoVersionSetting v-model="ziniaoVersion" />
<div class="run-row">
@@ -363,7 +369,7 @@
</template>
<script setup lang="ts">
import { computed, onMounted, onUnmounted, ref } from "vue";
import { computed, onMounted, onUnmounted, ref, watch } from "vue";
import { ElMessage } from "element-plus";
import BrandTopBar from "@/pages/brand/components/BrandTopBar.vue";
import {
@@ -399,8 +405,13 @@ import { checkQueuePayload } from "@/shared/dispatch-guard";
import { passGuard } from "@/shared/dispatch-guard-ui";
import ZiniaoVersionSetting from "@/shared/components/ZiniaoVersionSetting.vue";
import { useZiniaoVersion } from "@/shared/utils/ziniao-version";
import CountrySelector from "@/shared/components/CountrySelector.vue";
import {
EU_COUNTRY_CODES,
countryLabel,
sanitizeCountryCodes,
} from "@/shared/country-options";
const COUNTRY_TEMPLATE = ["德国", "英国", "法国", "意大利", "西班牙"] as const;
const MAX_TRANSIENT_ERRORS = 30;
const ziniaoVersion = useZiniaoVersion();
@@ -410,6 +421,7 @@ const candidates = ref<PatrolDeleteCandidateVo[]>([]);
const selectedCandidates = ref<PatrolDeleteCandidateVo[]>([]);
const deleteConditions = ref<PatrolDeleteConditionVo[]>([]);
const selectedConditionIds = ref<number[]>([]);
const selectedCountryCodes = ref<string[]>([...EU_COUNTRY_CODES]);
const matchedItems = ref<PatrolDeleteShopQueueItem[]>([]);
const historyItems = ref<PatrolDeleteHistoryItem[]>([]);
const dashboard = ref<PatrolDeleteDashboardVo>({
@@ -432,6 +444,10 @@ const timers = createCategorizedTimers("patrol-delete");
const matchedRunnableItems = computed(() =>
matchedItems.value.filter((item) => item.matched),
);
// 模板结构以中文国家名为 keyJava 的 Excel 列、Python 的站点切换都按中文名匹配)
const selectedCountryNames = computed(() =>
selectedCountryCodes.value.map((code) => countryLabel(code)),
);
const taskRecordItems = computed(() => groupHistoryItemsByTask(historyItems.value));
const currentSectionItems = computed(() =>
taskRecordItems.value.filter((item) => !isTaskTerminal(item.taskStatus)),
@@ -459,6 +475,10 @@ function queueStateStorageKey() {
return `patrol-delete:queue-state:${uidForStorage()}`;
}
function countryCodesStorageKey() {
return `patrol-delete:country-codes:${uidForStorage()}`;
}
function rowKeyForMatch(row: PatrolDeleteShopQueueItem) {
return `${(row.shopName || "").trim()}::${row.shopId || ""}`;
}
@@ -559,7 +579,7 @@ function formatMatchRemark(row: PatrolDeleteShopQueueItem) {
}
function buildTemplateCountrySections(): PatrolDeleteCountrySection[] {
return COUNTRY_TEMPLATE.map((country) => ({
return selectedCountryNames.value.map((country) => ({
country,
rows: [
{
@@ -573,7 +593,7 @@ function buildTemplateCountrySections(): PatrolDeleteCountrySection[] {
}
function buildTemplateCartRatios(): PatrolDeleteCartRatio[] {
return COUNTRY_TEMPLATE.map((country) => ({
return selectedCountryNames.value.map((country) => ({
country,
ratio: "",
}));
@@ -745,6 +765,29 @@ function loadQueueState() {
}
}
function saveCountryCodes() {
if (typeof window === "undefined") return;
window.localStorage.setItem(
countryCodesStorageKey(),
JSON.stringify(selectedCountryCodes.value),
);
}
function loadCountryCodes() {
try {
const raw =
typeof window !== "undefined"
? window.localStorage.getItem(countryCodesStorageKey())
: null;
// 没存过(首次进入)时保持默认的五国全选,与改动前的行为一致
selectedCountryCodes.value = raw
? sanitizeCountryCodes(JSON.parse(raw))
: [...EU_COUNTRY_CODES];
} catch {
selectedCountryCodes.value = [...EU_COUNTRY_CODES];
}
}
function clearActiveQueueTask() {
activeTaskId.value = null;
saveQueueState();
@@ -1011,6 +1054,8 @@ function buildQueuePayload(taskId: number, items: PatrolDeleteHistoryItem[]) {
source: "frontend-vue-patrol-delete",
delete_conditions: deleteConditionsForTask,
deleteConditions: deleteConditionsForTask,
// 与店铺数据抓取 / 跟价 / 商品风险等页面统一:国家一律用 country_codes 传代码
country_codes: [...selectedCountryCodes.value],
items: items.map((item) => ({
shopName: item.shopName,
shopId: item.shopId,
@@ -1122,10 +1167,11 @@ async function processQueue() {
queuePayloadText.value = JSON.stringify(payload, null, 2);
// 删除条件为空时 Python 端没有可执行的判定规则,任务会一直停在执行中
// country_codes 为空时 Python 端没有可遍历的站点,同样会空转
const guard = checkQueuePayload(payload, {
expectedType: "patrol-delete-run",
requiredDataKeys: ["taskId"],
nonEmptyArrayKeys: ["items", "delete_conditions"],
nonEmptyArrayKeys: ["items", "delete_conditions", "country_codes"],
});
if (!(await passGuard(guard))) {
await submitPatrolDeleteTaskResult(created.taskId, {
@@ -1171,7 +1217,7 @@ async function processQueue() {
}
for (const item of runnable) removeMatchedRowLocally(item);
queuePushResult.value = `任务 ${created.taskId} 已入队,共 ${createdItems.length} 个店铺,等待执行完成`;
queuePushResult.value = `任务 ${created.taskId} 已入队,共 ${createdItems.length} 个店铺${selectedCountryNames.value.length} 个国家,等待执行完成`;
const finalStatus = await waitForTaskTerminal(created.taskId);
queuePushResult.value = `任务 ${created.taskId} ${finalStatus === "SUCCESS" ? "已完成" : finalStatus === "DELETED" ? "已删除" : "执行失败"}`;
@@ -1198,7 +1244,11 @@ async function pushToPythonQueue() {
ElMessage.warning("请先匹配可用店铺");
return;
}
queuePushResult.value = `开始创建任务,共 ${runnable.length} 个店铺`;
if (!selectedCountryCodes.value.length) {
ElMessage.warning("请至少勾选 1 个巡店国家");
return;
}
queuePushResult.value = `开始创建任务,共 ${runnable.length} 个店铺、${selectedCountryNames.value.length} 个国家(${selectedCountryNames.value.join("、")}`;
await processQueue();
}
@@ -1250,6 +1300,8 @@ async function deleteTaskRecord(item: PatrolDeleteHistoryItem) {
onMounted(async () => {
loadMatchedItems();
loadQueueState();
loadCountryCodes();
watch(selectedCountryCodes, saveCountryCodes, { deep: true });
await Promise.all([loadCandidates(), loadConditions(), loadDashboard(), loadHistory()]);
if (activeTaskId.value) {
+1
View File
@@ -7,6 +7,7 @@ export const API_ENDPOINTS = {
},
dedupe: {
run: '/api/dedupe/run',
runProgress: '/api/dedupe/run/{runId}/progress',
history: '/api/dedupe/history',
historyDelete: '/api/dedupe/history/{resultId}',
resultDownload: '/api/dedupe/results/{resultId}/download',
@@ -35,11 +35,25 @@ export interface DedupeRunRequest {
user_id: number;
}
export interface DedupeRunProgressVo {
runId: string;
/** running / success / failed / not_found */
status: string;
total: number;
processedCount: number;
successCount: number;
failedCount: number;
finished: boolean;
error?: string;
/** 任务完成后才有 */
result?: DedupeRunVo | null;
}
export function runDedupe(
request: Omit<DedupeRunRequest, "user_id"> | DedupeRunRequest,
) {
return unwrapJavaResponse(
post<JavaApiResponse<DedupeRunVo>, DedupeRunRequest>(
post<JavaApiResponse<DedupeRunProgressVo>, DedupeRunRequest>(
buildJavaUrl(API_ENDPOINTS.dedupe.run),
{
...request,
@@ -49,6 +63,14 @@ export function runDedupe(
);
}
export function getDedupeRunProgress(runId: string) {
return unwrapJavaResponse(
get<JavaApiResponse<DedupeRunProgressVo>>(
buildJavaUrl(API_ENDPOINTS.dedupe.runProgress.replace('{runId}', encodeURIComponent(runId))),
),
);
}
export function getDedupeHistory() {
return unwrapJavaResponse(
get<JavaApiResponse<DedupeHistoryVo>>(
@@ -0,0 +1,132 @@
<template>
<section class="country-selector">
<div v-if="title" class="selector-title">{{ title }}</div>
<div class="country-pref-checks">
<label v-for="row in checkboxRows" :key="row.code" class="country-check-row">
<input
type="checkbox"
class="country-check-input"
:checked="isSelected(row.code)"
:disabled="disabled || isLastSelected(row.code)"
@change="onNativeChange(row.code, $event)"
/>
<span>{{ row.text }}</span>
</label>
</div>
<div class="country-order-panel">
<div class="country-order-caption">{{ orderCaption }}</div>
<div v-if="!modelValue.length" class="country-order-empty">尚未选择国家</div>
<div v-else class="country-order-list">
<div
v-for="(code, index) in modelValue"
:key="code"
class="country-drag-row"
:class="{ dragging: dragIndex === index }"
:draggable="!disabled"
@dragstart="dragIndex = index"
@dragend="dragIndex = null"
@dragover.prevent
@drop.prevent="onDrop(index)"
>
<span class="drag-handle" title="拖动排序"></span>
<span>{{ textOf(code) }}</span>
</div>
</div>
</div>
</section>
</template>
<script setup lang="ts">
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { EU_COUNTRY_OPTIONS, type CountryOption } from '@/shared/country-options'
const props = withDefaults(
defineProps<{
/** 已选国家代码,数组顺序即执行顺序 */
modelValue: string[]
options?: readonly CountryOption[]
title?: string
orderCaption?: string
minSelectedWarning?: string
disabled?: boolean
}>(),
{
options: () => EU_COUNTRY_OPTIONS,
title: '国家与顺序',
orderCaption: '已选顺序(拖动可调整执行先后)',
minSelectedWarning: '至少保留 1 个国家',
disabled: false,
},
)
const emit = defineEmits<{ 'update:modelValue': [value: string[]] }>()
const dragIndex = ref<number | null>(null)
/** 国家代码和展示名相同时(例如直接用中文名作 code)不再重复追加括号 */
function textOf(code: string) {
const label = props.options.find((row) => row.code === code)?.label || code
return label === code ? label : `${label}${code}`
}
// 已勾选的按当前顺序排在前面,未勾选的按选项原始顺序补在后面
const checkboxRows = computed(() => {
const selected = new Set(props.modelValue)
return [
...props.modelValue.map((code) => ({ code, text: textOf(code) })),
...props.options
.filter((row) => !selected.has(row.code))
.map((row) => ({ code: row.code, text: textOf(row.code) })),
]
})
function isSelected(code: string) {
return props.modelValue.includes(code)
}
function isLastSelected(code: string) {
return props.modelValue.length === 1 && props.modelValue[0] === code
}
function onNativeChange(code: string, event: Event) {
const input = event.target as HTMLInputElement
if (!input.checked && isLastSelected(code)) {
input.checked = true
ElMessage.warning(props.minSelectedWarning)
return
}
emit(
'update:modelValue',
input.checked
? [...props.modelValue, code]
: props.modelValue.filter((item) => item !== code),
)
}
function onDrop(toIndex: number) {
const from = dragIndex.value
dragIndex.value = null
if (props.disabled || from == null || from === toIndex) return
const next = [...props.modelValue]
const [moved] = next.splice(from, 1)
next.splice(toIndex, 0, moved)
emit('update:modelValue', next)
}
</script>
<style scoped>
.country-selector { margin: 16px 0; }
.selector-title { margin-bottom: 10px; color: #bbb; font-size: 13px; }
.country-pref-checks { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 8px; margin-bottom: 10px; }
.country-check-row { display: flex; align-items: center; gap: 7px; min-height: 32px; color: #ccc; font-size: 13px; }
.country-check-input { width: 15px; height: 15px; }
.country-check-input:disabled { cursor: not-allowed; }
.country-order-panel { padding: 10px; border: 1px solid #333; border-radius: 6px; background: #242424; }
.country-order-caption { margin-bottom: 8px; color: #888; font-size: 12px; }
.country-order-empty { color: #666; font-size: 12px; }
.country-order-list { display: flex; flex-direction: column; gap: 6px; }
.country-drag-row { display: flex; align-items: center; gap: 8px; min-height: 30px; padding: 0 9px; border: 1px solid #383838; border-radius: 4px; color: #ccc; font-size: 13px; cursor: grab; }
.country-drag-row.dragging { opacity: .5; }
.drag-handle { color: #777; }
</style>
@@ -0,0 +1,43 @@
/**
* 各功能页共用的欧洲五国选项。
*
* code 用于接口参数与本地存储(和店铺数据抓取 / 跟价 / 商品风险等页面的
* country_codes 保持一致),label 用于界面展示;巡店删除的模板结构直接以
* 中文国家名作为 key,取 label 即可。
*/
export interface CountryOption {
code: string
label: string
}
export const EU_COUNTRY_OPTIONS: readonly CountryOption[] = [
{ code: 'DE', label: '德国' },
{ code: 'UK', label: '英国' },
{ code: 'FR', label: '法国' },
{ code: 'IT', label: '意大利' },
{ code: 'ES', label: '西班牙' },
]
export const EU_COUNTRY_CODES: readonly string[] = EU_COUNTRY_OPTIONS.map((row) => row.code)
export function countryLabel(code: string) {
return EU_COUNTRY_OPTIONS.find((row) => row.code === code)?.label || code
}
/**
* 过滤出合法国家代码并去重,用于校验本地缓存或接口返回的顺序。
* 结果为空时回落到 fallback,避免出现「一个国家都没有」导致任务空转。
*/
export function sanitizeCountryCodes(
raw: unknown,
fallback: readonly string[] = EU_COUNTRY_CODES,
): string[] {
if (!Array.isArray(raw)) return [...fallback]
const valid = new Set(EU_COUNTRY_CODES)
const result: string[] = []
for (const item of raw) {
const code = String(item ?? '').trim().toUpperCase()
if (valid.has(code) && !result.includes(code)) result.push(code)
}
return result.length ? result : [...fallback]
}
+43 -7
View File
@@ -3,11 +3,12 @@ import assert from 'node:assert/strict'
import { http } from '../src/shared/api/http.ts'
import {
runDedupe,
getDedupeRunProgress,
getDedupeHistory,
deleteDedupeHistory,
getDedupeResultDownloadUrl,
type DedupeRunRequest,
type DedupeRunVo,
type DedupeRunProgressVo,
} from '../src/shared/api/types/modules/dedupe.ts'
function setupWindow() {
@@ -26,12 +27,22 @@ function mockRequest(
const okResponse = (data: unknown) => Promise.resolve({ data: { success: true, message: 'ok', data } })
const runningProgress = (runId: string): DedupeRunProgressVo => ({
runId,
status: 'running',
total: 2,
processedCount: 1,
successCount: 1,
failedCount: 0,
finished: false,
})
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: [] })
return okResponse(runningProgress('run-1'))
})
await runDedupe({ files: [{ fileKey: 'k1' }], selectedColumns: ['a'], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: true })
assert.equal(captured.url, '/newApi/api/dedupe/run')
@@ -42,7 +53,7 @@ test('test_dedupe_run_method_payload', async (t) => {
let captured: { method?: string; data?: unknown } = {}
mockRequest(t, (config) => {
captured = config
return okResponse({ total: 0, successCount: 0, failedCount: 0, items: [] })
return okResponse(runningProgress('run-1'))
})
await runDedupe({ files: [{ fileKey: 'k1' }], selectedColumns: ['a'], keepIntegerIds: true, keepUnderscoreIds: true, keepIntegerMainIdsWhenNoSubIds: false })
assert.equal(captured.method, 'POST')
@@ -56,6 +67,30 @@ test('test_dedupe_run_method_payload', async (t) => {
})
})
test('test_dedupe_run_returns_progress', async (t) => {
setupWindow()
mockRequest(t, () => okResponse(runningProgress('run-1')))
const progress = await runDedupe({ files: [{ fileKey: 'k1' }], selectedColumns: ['a'], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: true })
assert.equal(progress.runId, 'run-1', 'run 应立即返回 runId')
assert.equal(progress.status, 'running')
assert.equal(progress.finished, false)
assert.equal(progress.result, undefined, '运行中不应携带最终结果')
})
test('test_dedupe_progress_url', async (t) => {
setupWindow()
let captured: { url?: string; method?: string } = {}
mockRequest(t, (config) => {
captured = config
return okResponse({ runId: 'run-1', status: 'success', total: 2, processedCount: 2, successCount: 2, failedCount: 0, finished: true, result: { total: 2, successCount: 2, failedCount: 0, items: [] } })
})
const progress = await getDedupeRunProgress('run-1')
assert.equal(captured.url, '/newApi/api/dedupe/run/run-1/progress')
assert.equal(captured.method, 'GET')
assert.equal(progress.status, 'success')
assert.ok(progress.result, '完成后应返回结果')
})
test('test_dedupe_history_url', async (t) => {
setupWindow()
let captured: { url?: string; method?: string; params?: Record<string, unknown> } = {}
@@ -90,6 +125,7 @@ test('test_dedupe_download_url', async (t) => {
test('test_dedupe_signature_unchanged', async (t) => {
setupWindow()
assert.equal(runDedupe.length, 1, 'runDedupe 应保持单参数')
assert.equal(getDedupeRunProgress.length, 1, 'getDedupeRunProgress 应保持单参数')
assert.equal(getDedupeHistory.length, 0, 'getDedupeHistory 应保持无参')
assert.equal(deleteDedupeHistory.length, 1, 'deleteDedupeHistory 应保持单参数')
assert.equal(getDedupeResultDownloadUrl.length, 1, 'getDedupeResultDownloadUrl 应保持单参数')
@@ -114,9 +150,9 @@ test('test_dedupe_export_compat', async (t) => {
assert.equal(fromJavaModules.getDedupeResultDownloadUrl, fromDedupe.getDedupeResultDownloadUrl)
})
test('test_dedupe_unwrap', async (t) => {
test('test_dedupe_unwrap_progress', async (t) => {
setupWindow()
mockRequest(t, () => okResponse({ total: 2, successCount: 1, failedCount: 1, items: [{ success: true }] }))
const result = await runDedupe<DedupeRunVo>({ files: [{ fileKey: 'k' }], selectedColumns: [], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: false })
assert.deepEqual(result, { total: 2, successCount: 1, failedCount: 1, items: [{ success: true }] }, '应返回解包后的 data')
mockRequest(t, () => okResponse(runningProgress('run-1')))
const progress = await runDedupe<DedupeRunProgressVo>({ files: [{ fileKey: 'k' }], selectedColumns: [], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: false })
assert.deepEqual(progress, runningProgress('run-1'), '应返回解包后的 data')
})