task-119(任务与重复分析中心): 实现重复检查导入/导出状态
新增 duplicate-import-export.ts:导入结果摘要/导入参数归一与导出查询构建。 TDD: task-119.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -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<unknown>(payload)
|
||||||
|
const record = data && typeof data === 'object' ? (data as Record<string, unknown>) : {}
|
||||||
|
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<string, string> {
|
||||||
|
const query: Record<string, string> = {}
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -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/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user