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:
2026-09-05 17:25:41 +08:00
parent b5a1a51403
commit 479cc449d6
4 changed files with 189 additions and 1 deletions
@@ -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,
}
}