From 1acb93ff1cc77c6e57ce0ccff2914f7a68cb883a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=84=E8=87=AA=E8=BE=BE?= <980324341@qq.com> Date: Sat, 5 Sep 2026 17:27:50 +0800 Subject: [PATCH] =?UTF-8?q?task-119(=E4=BB=BB=E5=8A=A1=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=86=E6=9E=90=E4=B8=AD=E5=BF=83):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E9=87=8D=E5=A4=8D=E6=A3=80=E6=9F=A5=E5=AF=BC=E5=85=A5?= =?UTF-8?q?/=E5=AF=BC=E5=87=BA=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 duplicate-import-export.ts:导入结果摘要/导入参数归一与导出查询构建。 TDD: task-119.test.ts 8 用例先 RED 后 GREEN。 --- .../pages/tasks/duplicate-import-export.ts | 71 +++++++++++++++++++ admin-frontend-vue/tests/task-119.test.ts | 70 ++++++++++++++++++ 2 files changed, 141 insertions(+) create mode 100644 admin-frontend-vue/src/pages/tasks/duplicate-import-export.ts create mode 100644 admin-frontend-vue/tests/task-119.test.ts diff --git a/admin-frontend-vue/src/pages/tasks/duplicate-import-export.ts b/admin-frontend-vue/src/pages/tasks/duplicate-import-export.ts new file mode 100644 index 00000000..a7f7a59d --- /dev/null +++ b/admin-frontend-vue/src/pages/tasks/duplicate-import-export.ts @@ -0,0 +1,71 @@ +/** 重复检查导入/导出(任务 119):导入结果摘要、导入参数与导出查询构建;纯逻辑。 + * 导出走 GET /duplicate-check-export 直接下载 CSV,导入 POST /duplicate-check-import(multipart)。 */ +import { unwrap } from '../../api/envelope.ts' +import type { DuplicateFilter } from './duplicate-filter.ts' + +/** 导出端点路径(供下载 URL 构造)。 */ +export const DUPLICATE_EXPORT_PATH = '/duplicate-check-export' + +function count(value: unknown): number { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : 0 +} + +export type DuplicateImportMode = 'add' | 'delete' + +/** 导入模式归一:delete/del/remove → delete,其余按 add。 */ +export function normalizeImportMode(mode: unknown): DuplicateImportMode { + const text = typeof mode === 'string' ? mode.trim().toLowerCase() : '' + return text === 'delete' || text === 'del' || text === 'remove' ? 'delete' : 'add' +} + +export interface DuplicateImportResult { + totalRows: number + asinCount: number + mode: DuplicateImportMode +} + +/** 导入结果摘要解析(data: {total_rows, asin_count, ...})。 */ +export function parseDuplicateImportResult(payload: unknown): DuplicateImportResult { + const data = unwrap(payload) + const record = data && typeof data === 'object' ? (data as Record) : {} + return { + totalRows: count(record.total_rows ?? record.totalRows), + asinCount: count(record.asin_count ?? record.asinCount), + mode: normalizeImportMode(record.mode), + } +} + +/** 导入查询参数(multipart 外的固定参数)。 */ +export function toDuplicateImportQuery(input: { + mode?: unknown + shopName?: string + countryCodes?: readonly string[] +}): { mode: DuplicateImportMode; shop_name?: string; country_codes?: string } { + const query: { mode: DuplicateImportMode; shop_name?: string; country_codes?: string } = { + mode: normalizeImportMode(input.mode), + } + const shopName = typeof input.shopName === 'string' ? input.shopName.trim() : '' + if (shopName) query.shop_name = shopName + const countryCodes = Array.isArray(input.countryCodes) + ? input.countryCodes.map((code) => String(code).trim()).filter(Boolean) + : [] + if (countryCodes.length) query.country_codes = countryCodes.join(',') + return query +} + +/** 导出查询参数(不含分页,直接下载 CSV)。 */ +export function toDuplicateExportQuery(filter: DuplicateFilter): Record { + const query: Record = {} + const put = (key: string, value: string) => { + const text = value.trim() + if (text) query[key] = text + } + put('view', filter.view) + put('asin', filter.asin) + put('shop_name', filter.shopName) + put('country', filter.country) + put('site', filter.site) + put('date_from', filter.dateFrom) + put('date_to', filter.dateTo) + return query +} diff --git a/admin-frontend-vue/tests/task-119.test.ts b/admin-frontend-vue/tests/task-119.test.ts new file mode 100644 index 00000000..139403fb --- /dev/null +++ b/admin-frontend-vue/tests/task-119.test.ts @@ -0,0 +1,70 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + normalizeImportMode, + parseDuplicateImportResult, + toDuplicateExportQuery, + toDuplicateImportQuery, +} from '../src/pages/tasks/duplicate-import-export.ts' + +test('test_task_119_duplicate_import_export_normal_primary_path', () => { + // 正常主路径:导入结果摘要解析。 + const summary = parseDuplicateImportResult({ + success: true, + data: { total_rows: 120, asin_count: 45, mode: 'add' }, + }) + assert.equal(summary.totalRows, 120) + assert.equal(summary.asinCount, 45) + assert.equal(summary.mode, 'add') +}) + +test('test_task_119_duplicate_import_export_normal_variant_input', () => { + // 正常变体:导入参数归一(delete)。 + assert.equal(normalizeImportMode('DEL'), 'delete') + assert.equal(normalizeImportMode('add'), 'add') + assert.deepEqual(toDuplicateImportQuery({ mode: 'add', shopName: '店', countryCodes: ['DE', 'UK'] }), { + mode: 'add', + shop_name: '店', + country_codes: 'DE,UK', + }) +}) + +test('test_task_119_duplicate_import_export_repeated_is_idempotent', () => { + // 正常重复:导出查询构建稳定、不改输入。 + const filter = { view: 'monitor', asin: 'A', shopName: '', country: 'DE', site: '', dateFrom: '', dateTo: '' } + assert.deepEqual(toDuplicateExportQuery(filter), toDuplicateExportQuery(filter)) +}) + +test('test_task_119_duplicate_import_export_boundary_empty_input', () => { + // 边界空值:无模式按 add;空 country 不下发。 + assert.equal(normalizeImportMode(''), 'add') + const q = toDuplicateImportQuery({ mode: 'x', shopName: '', countryCodes: [] }) + assert.equal(q.mode, 'add') + assert.equal('country_codes' in q, false) +}) + +test('test_task_119_duplicate_import_export_boundary_single_item', () => { + // 边界单元素:单国家导出。 + const query = toDuplicateExportQuery({ view: '', asin: '', shopName: '', country: 'DE', site: '', dateFrom: '', dateTo: '' }) + assert.equal(query.country, 'DE') +}) + +test('test_task_119_duplicate_import_export_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:导入结果缺省字段回 0。 + const summary = parseDuplicateImportResult({ data: {} }) + assert.equal(summary.totalRows, 0) + assert.equal(summary.asinCount, 0) +}) + +test('test_task_119_duplicate_import_export_invalid_input_rejected', () => { + // 异常输入:success=false 抛后端 message。 + assert.throws(() => parseDuplicateImportResult({ success: false, message: '请上传 Excel 文件' }), /请上传 Excel 文件/) +}) + +test('test_task_119_duplicate_import_export_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:导入导出纯逻辑;导出走 GET /duplicate-check-export。 + const mod = readSource('src/pages/tasks/duplicate-import-export.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '导入导出保持纯逻辑') + assert.match(mod, /duplicate-check-export/) +})