task-117(任务与重复分析中心): 实现重复检查明细列表
新增 duplicate-filter.ts(明细筛选 snake 查询) 与 parseDuplicateItemsPage/ fetchDuplicateItems(GET /duplicate-check-items)。 TDD: task-117.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
/** 店铺数据重复检查适配(任务 115-119):/api/admin/shop-data-crawl/duplicate-check-* 系列端点。 */
|
||||
import { http } from '@/api/http'
|
||||
import { parseDuplicateOverview, type DuplicateOverview } from './duplicate-model.ts'
|
||||
import { parseDuplicateItemsPage, parseDuplicateOverview, type DuplicateOverview } from './duplicate-model.ts'
|
||||
import { toDuplicateItemsQuery, type DuplicateFilter } from './duplicate-filter.ts'
|
||||
|
||||
export const DUPLICATE_CHECK_ENDPOINT = '/api/admin/shop-data-crawl'
|
||||
|
||||
@@ -11,3 +12,15 @@ export async function fetchDuplicateOverview(force = false): Promise<DuplicateOv
|
||||
})
|
||||
return parseDuplicateOverview(data)
|
||||
}
|
||||
|
||||
/** 加载撞款明细分页列表:GET /duplicate-check-items。 */
|
||||
export async function fetchDuplicateItems(
|
||||
filter: DuplicateFilter,
|
||||
page: number,
|
||||
pageSize: number,
|
||||
): Promise<ReturnType<typeof parseDuplicateItemsPage>> {
|
||||
const { data } = await http.get<unknown>(`${DUPLICATE_CHECK_ENDPOINT}/duplicate-check-items`, {
|
||||
params: toDuplicateItemsQuery(filter, page, pageSize),
|
||||
})
|
||||
return parseDuplicateItemsPage(data)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
/** 重复检查明细筛选与分页(任务 117):与 /duplicate-check-items 的 snake 参数契约对齐;纯逻辑。 */
|
||||
|
||||
export const DUPLICATE_DEFAULT_PAGE_SIZE = 20
|
||||
export const DUPLICATE_MIN_PAGE_SIZE = 10
|
||||
export const DUPLICATE_MAX_PAGE_SIZE = 100
|
||||
export const DUPLICATE_PAGE_MIN = 1
|
||||
|
||||
export interface DuplicateFilter {
|
||||
view: string
|
||||
asin: string
|
||||
shopName: string
|
||||
country: string
|
||||
site: string
|
||||
dateFrom: string
|
||||
dateTo: string
|
||||
}
|
||||
|
||||
export interface DuplicateItemsQuery {
|
||||
page: number
|
||||
page_size: number
|
||||
view?: string
|
||||
asin?: string
|
||||
shop_name?: string
|
||||
country?: string
|
||||
site?: string
|
||||
date_from?: string
|
||||
date_to?: string
|
||||
}
|
||||
|
||||
function textOrUndefined(value: unknown): string | undefined {
|
||||
const text = typeof value === 'string' ? value.trim() : ''
|
||||
return text || undefined
|
||||
}
|
||||
|
||||
function finiteInt(value: unknown): number | null {
|
||||
return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null
|
||||
}
|
||||
|
||||
/** 前端筛选 + 页码 → Java 查询参数(snake,仅下发非空)。 */
|
||||
export function toDuplicateItemsQuery(filter: DuplicateFilter, page: unknown, pageSize: unknown): DuplicateItemsQuery {
|
||||
const safePage = Math.max(finiteInt(page) ?? DUPLICATE_PAGE_MIN, DUPLICATE_PAGE_MIN)
|
||||
const rawSize = finiteInt(pageSize)
|
||||
const pageSizeClamped =
|
||||
rawSize === null || rawSize < 1
|
||||
? DUPLICATE_DEFAULT_PAGE_SIZE
|
||||
: Math.min(Math.max(rawSize, DUPLICATE_MIN_PAGE_SIZE), DUPLICATE_MAX_PAGE_SIZE)
|
||||
const query: DuplicateItemsQuery = { page: safePage, page_size: pageSizeClamped }
|
||||
const view = textOrUndefined(filter.view)
|
||||
if (view) query.view = view
|
||||
const asin = textOrUndefined(filter.asin)
|
||||
if (asin) query.asin = asin
|
||||
const shopName = textOrUndefined(filter.shopName)
|
||||
if (shopName) query.shop_name = shopName
|
||||
const country = textOrUndefined(filter.country)
|
||||
if (country) query.country = country
|
||||
const site = textOrUndefined(filter.site)
|
||||
if (site) query.site = site
|
||||
const dateFrom = textOrUndefined(filter.dateFrom)
|
||||
if (dateFrom) query.date_from = dateFrom
|
||||
const dateTo = textOrUndefined(filter.dateTo)
|
||||
if (dateTo) query.date_to = dateTo
|
||||
return query
|
||||
}
|
||||
@@ -138,3 +138,31 @@ function parseDuplicateMetrics(raw: unknown): DuplicateMetrics | null {
|
||||
source: text(r.source),
|
||||
}
|
||||
}
|
||||
|
||||
/** 明细列表分页负载(data: {pending,items,total,page,page_size}) → 前端结果。 */
|
||||
export function parseDuplicateItemsPage(payload: unknown): {
|
||||
pending: boolean
|
||||
scannedAt: string
|
||||
items: DuplicateItem[]
|
||||
total: number
|
||||
page: number
|
||||
pageSize: number
|
||||
} {
|
||||
const core = unwrap<unknown>(payload)
|
||||
const record = core && typeof core === 'object' ? (core as Record<string, unknown>) : {}
|
||||
const items = Array.isArray(record.items)
|
||||
? record.items.map((raw) => parseDuplicateItem(raw)).filter((item): item is DuplicateItem => item !== null)
|
||||
: []
|
||||
const total = count(record.total)
|
||||
const pageNumber = count(record.page) >= 1 ? count(record.page) : 1
|
||||
const rawSize = record.page_size ?? record.pageSize
|
||||
const pageSize = count(rawSize) >= 1 ? count(rawSize) : 20
|
||||
return {
|
||||
pending: record.pending === true,
|
||||
scannedAt: text(record.scanned_at ?? record.scannedAt),
|
||||
items,
|
||||
total,
|
||||
page: pageNumber,
|
||||
pageSize,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
import test from 'node:test'
|
||||
import assert from 'node:assert/strict'
|
||||
import { readSource } from './helpers.ts'
|
||||
import { parseDuplicateItemsPage } from '../src/pages/tasks/duplicate-model.ts'
|
||||
import { toDuplicateItemsQuery } from '../src/pages/tasks/duplicate-filter.ts'
|
||||
|
||||
test('test_task_117_duplicate_items_normal_primary_path', () => {
|
||||
// 正常主路径:明细筛选序列化与分页负载解析。
|
||||
const query = toDuplicateItemsQuery(
|
||||
{ view: 'monitor', asin: 'B0X', shopName: '店', country: 'DE', site: '', dateFrom: '2026-01-01', dateTo: '2026-01-31' },
|
||||
2,
|
||||
20,
|
||||
)
|
||||
assert.equal(query.view, 'monitor')
|
||||
assert.equal(query.asin, 'B0X')
|
||||
assert.equal(query.shop_name, '店')
|
||||
assert.equal(query.country, 'DE')
|
||||
assert.equal(query.date_from, '2026-01-01')
|
||||
assert.equal(query.page, 2)
|
||||
assert.equal(query.page_size, 20)
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_normal_variant_input', () => {
|
||||
// 正常变体:明细项含跨店出现记录解析。
|
||||
const page = parseDuplicateItemsPage({
|
||||
data: {
|
||||
pending: false,
|
||||
items: [
|
||||
{ asin: 'B0ABC', shop_count: 2, record_count: 3, occurrences: [
|
||||
{ asin: 'B0ABC', shop_name: 'A店', group_name: '华东', country_codes: ['DE'], country: 'DE', date: '2026-01-01', price: '9.9', brand: 'X' },
|
||||
] },
|
||||
],
|
||||
total: 1,
|
||||
page: 1,
|
||||
page_size: 20,
|
||||
},
|
||||
})
|
||||
assert.equal(page.items.length, 1)
|
||||
const item = page.items[0]
|
||||
assert.equal(item.asin, 'B0ABC')
|
||||
assert.equal(item.shopCount, 2)
|
||||
assert.equal(item.occurrences[0].shopName, 'A店')
|
||||
assert.equal(item.occurrences[0].price, '9.9')
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_repeated_is_idempotent', () => {
|
||||
// 正常重复:查询序列化稳定。
|
||||
const filter = { view: '', asin: 'A', shopName: '', country: '', site: '', dateFrom: '', dateTo: '' }
|
||||
assert.deepEqual(toDuplicateItemsQuery(filter, 1, 20), toDuplicateItemsQuery(filter, 1, 20))
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_boundary_empty_input', () => {
|
||||
// 边界空值:空筛选回默认分页;pending 空态。
|
||||
const page = parseDuplicateItemsPage({ data: { pending: true, items: [], total: 0, page: 1, page_size: 20 } })
|
||||
assert.equal(page.pending, true)
|
||||
assert.deepEqual(page.items, [])
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_boundary_single_item', () => {
|
||||
// 边界单元素:单明细项。
|
||||
const page = parseDuplicateItemsPage({ data: { pending: false, items: [{ asin: 'S', shop_count: 1, record_count: 1, occurrences: [] }], total: 1, page: 1, page_size: 20 } })
|
||||
assert.equal(page.items[0].asin, 'S')
|
||||
assert.deepEqual(page.items[0].occurrences, [])
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_boundary_limit_or_missing_field', () => {
|
||||
// 边界上限/缺字段:页大小钳制到上限。
|
||||
const query = toDuplicateItemsQuery({ view: '', asin: '', shopName: '', country: '', site: '', dateFrom: '', dateTo: '' }, 1, 999)
|
||||
assert.equal(query.page_size, 100)
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_invalid_input_rejected', () => {
|
||||
// 异常输入:success=false 抛后端 message。
|
||||
assert.throws(() => parseDuplicateItemsPage({ success: false, message: '暂无权限查看' }), /暂无权限/)
|
||||
})
|
||||
|
||||
test('test_task_117_duplicate_items_dependency_failure_returns_actionable_message', () => {
|
||||
// 依赖失败/加载走 adapter:GET /duplicate-check-items + 解析。
|
||||
const api = readSource('src/pages/tasks/duplicate-api.ts')
|
||||
assert.match(api, /\/duplicate-check-items/)
|
||||
assert.match(api, /fetchDuplicateItems/)
|
||||
const model = readSource('src/pages/tasks/duplicate-model.ts')
|
||||
assert.equal(/axios|http\./.test(model), false, '明细解析保持纯逻辑')
|
||||
})
|
||||
Reference in New Issue
Block a user