- 左侧负责录入店铺并加入备选区,点击“匹配店铺”只会生成匹配结果;确认后需手动点击“推送到 Python 队列”才会创建并执行任务。
+ 左侧负责录入店铺并加入备选区,点击“匹配店铺”只会生成匹配结果;确认后需手动点击“推送到 Python 队列”才会创建并执行任务。任务只会跑下方勾选的国家。
+
+
@@ -363,7 +369,7 @@
+
+
diff --git a/frontend-vue/src/shared/country-options.ts b/frontend-vue/src/shared/country-options.ts
new file mode 100644
index 00000000..cf3295c5
--- /dev/null
+++ b/frontend-vue/src/shared/country-options.ts
@@ -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]
+}
diff --git a/frontend-vue/tests/modules-dedupe.test.ts b/frontend-vue/tests/modules-dedupe.test.ts
index 761ebf34..f9ba58f6 100644
--- a/frontend-vue/tests/modules-dedupe.test.ts
+++ b/frontend-vue/tests/modules-dedupe.test.ts
@@ -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 } = {}
@@ -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({ 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({ files: [{ fileKey: 'k' }], selectedColumns: [], keepIntegerIds: false, keepUnderscoreIds: false, keepIntegerMainIdsWhenNoSubIds: false })
+ assert.deepEqual(progress, runningProgress('run-1'), '应返回解包后的 data')
})