From 09b3d9f0132e702df3730093e5a8c5f898c7c237 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 15:35:44 +0800 Subject: [PATCH] =?UTF-8?q?task-62(ASIN=20=E6=95=B0=E6=8D=AE=E4=B8=AD?= =?UTF-8?q?=E5=BF=83):=20=E5=AE=9E=E7=8E=B0=E5=8E=BB=E9=87=8D=E6=B1=87?= =?UTF-8?q?=E6=80=BB=E5=88=97=E8=A1=A8=E6=9F=A5=E8=AF=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 dedupe-total-model.ts 纯解析 DedupeTotalDataPageVo(camel items/total/page/pageSize, 条目 dataValue/country/groupId/groupName/uploader/username/createdAt,缺 id 过滤); dedupe-total-api.ts fetchDedupeTotalList 复用共享筛选归一后 GET /api/admin/dedupe-total-data。 8 用例全过,451 单测 + build 绿。 --- .../src/pages/asin/dedupe-total-api.ts | 12 ++ .../src/pages/asin/dedupe-total-model.ts | 76 +++++++++++++ admin-frontend-vue/tests/task-62.test.ts | 106 ++++++++++++++++++ 3 files changed, 194 insertions(+) create mode 100644 admin-frontend-vue/src/pages/asin/dedupe-total-api.ts create mode 100644 admin-frontend-vue/src/pages/asin/dedupe-total-model.ts create mode 100644 admin-frontend-vue/tests/task-62.test.ts diff --git a/admin-frontend-vue/src/pages/asin/dedupe-total-api.ts b/admin-frontend-vue/src/pages/asin/dedupe-total-api.ts new file mode 100644 index 00000000..35428af0 --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/dedupe-total-api.ts @@ -0,0 +1,12 @@ +/** 去重汇总列表查询适配(任务 62):GET /api/admin/dedupe-total-data + 共享筛选归一。 */ +import { http } from '@/api/http' +import { normalizeAsinPageParams, toAsinPageQuery, type AsinListParams } from './asin-filter' +import { parseDedupeTotalPage, type DedupeTotalPageResult } from './dedupe-total-model' + +export const DEDUPE_TOTAL_ENDPOINT = '/api/admin/dedupe-total-data' + +export async function fetchDedupeTotalList(params: Partial = {}): Promise { + const normalized = normalizeAsinPageParams(params) + const { data } = await http.get(DEDUPE_TOTAL_ENDPOINT, { params: toAsinPageQuery(normalized) }) + return parseDedupeTotalPage(data) +} diff --git a/admin-frontend-vue/src/pages/asin/dedupe-total-model.ts b/admin-frontend-vue/src/pages/asin/dedupe-total-model.ts new file mode 100644 index 00000000..a445c614 --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/dedupe-total-model.ts @@ -0,0 +1,76 @@ +/** 去重汇总列表查询模型(任务 62):解析 DedupeTotalDataPageVo(camel),纯逻辑。 */ +import { unwrap } from '../../api/envelope.ts' +import { ASIN_PAGE_DEFAULT_SIZE, type AsinListParams } from './asin-filter.ts' + +export interface DedupeTotalItem { + id: number + dataValue: string + country: string + groupId: number | null + groupName: string + uploaderUserId: number | null + username: string + createdAt?: string +} + +export interface DedupeTotalPageResult { + items: DedupeTotalItem[] + total: number + page: number + pageSize: number +} + +export function emptyDedupeTotalPage(): DedupeTotalPageResult { + return { items: [], total: 0, page: 1, pageSize: ASIN_PAGE_DEFAULT_SIZE } +} + +function text(value: unknown): string { + return typeof value === 'string' ? value.trim() : '' +} + +function numberOrNull(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 解析单条去重总数据;缺 id 视为无效。 */ +export function toDedupeTotalItem(raw: unknown): DedupeTotalItem | null { + if (!raw || typeof raw !== 'object') return null + const record = raw as Record + const id = numberOrNull(record.id) + if (id === null) return null + const item: DedupeTotalItem = { + id, + dataValue: text(record.dataValue ?? record.data_value), + country: text(record.country), + groupId: numberOrNull(record.groupId ?? record.group_id), + groupName: text(record.groupName ?? record.group_name), + uploaderUserId: numberOrNull(record.uploaderUserId ?? record.uploader_user_id), + username: text(record.username), + } + const createdAt = text(record.createdAt ?? record.created_at) + if (createdAt) item.createdAt = createdAt + return item +} + +/** 归一化分页负载(信封或已解包 VO)为前端结果;缺省字段回默认。 */ +export function parseDedupeTotalPage(payload: unknown): DedupeTotalPageResult { + const out = emptyDedupeTotalPage() + const core = unwrap(payload) + if (!core || typeof core !== 'object') return out + const record = core as Record + if (Array.isArray(record.items)) { + out.items = record.items + .map((raw) => toDedupeTotalItem(raw)) + .filter((item): item is DedupeTotalItem => item !== null) + } + if (typeof record.total === 'number') out.total = Math.floor(record.total) + if (typeof record.page === 'number' && record.page >= 1) out.page = Math.floor(record.page) + const rawSize = record.pageSize ?? record.page_size + if (typeof rawSize === 'number' && rawSize >= 1) out.pageSize = Math.floor(rawSize) + return out +} + +/** 构造缺省分页查询参数(供初始加载使用)。 */ +export function emptyDedupePageParams(): Partial { + return { page: 1, pageSize: ASIN_PAGE_DEFAULT_SIZE } +} diff --git a/admin-frontend-vue/tests/task-62.test.ts b/admin-frontend-vue/tests/task-62.test.ts new file mode 100644 index 00000000..7c1a5af9 --- /dev/null +++ b/admin-frontend-vue/tests/task-62.test.ts @@ -0,0 +1,106 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { parseDedupeTotalPage, toDedupeTotalItem } from '../src/pages/asin/dedupe-total-model.ts' + +test('test_task_062_dedupe_list_normal_primary_path', () => { + // 正常主路径:Java DedupeTotalDataPageVo(camel) 解析为前端分页结果。 + const page = parseDedupeTotalPage({ + success: true, + data: { + items: [ + { + id: 1, + dataValue: 'B0ABC123', + country: 'DE', + groupId: 3, + groupName: '华东组', + uploaderUserId: 2, + username: '张伟恒', + createdAt: '2026-01-01T10:00:00', + }, + ], + total: 1, + page: 1, + pageSize: 15, + }, + }) + assert.equal(page.items.length, 1) + const item = page.items[0] + assert.equal(item.dataValue, 'B0ABC123') + assert.equal(item.country, 'DE') + assert.equal(item.groupName, '华东组') + assert.equal(item.username, '张伟恒') + assert.equal(item.id, 1) + assert.equal(page.total, 1) + assert.equal(page.pageSize, 15) +}) + +test('test_task_062_dedupe_list_normal_variant_input', () => { + // 正常变体:已解包 VO 直传同样归一;缺分组/上传者字段可空。 + const page = parseDedupeTotalPage({ + items: [{ id: 2, dataValue: 'X', country: 'UK' }], + total: 5, + page: 2, + pageSize: 20, + }) + assert.equal(page.items[0].country, 'UK') + assert.equal(page.items[0].groupId, null) + assert.equal(page.items[0].username, '') + assert.equal(page.page, 2) + assert.equal(page.pageSize, 20) +}) + +test('test_task_062_dedupe_list_normal_repeated_operation_is_idempotent', () => { + // 正常重复:解析不改输入、结果稳定。 + const payload = { data: { items: [{ id: 3, dataValue: 'A', country: 'DE' }], total: 1, page: 1, pageSize: 15 } } + assert.deepEqual(parseDedupeTotalPage(payload), parseDedupeTotalPage(payload)) +}) + +test('test_task_062_dedupe_list_boundary_empty_input', () => { + // 边界空值:空负载回默认分页结果(空列表/total 0/page1/页大小 15)。 + const page = parseDedupeTotalPage({}) + assert.deepEqual(page.items, []) + assert.equal(page.total, 0) + assert.equal(page.page, 1) + assert.equal(page.pageSize, 15) +}) + +test('test_task_062_dedupe_list_boundary_single_item', () => { + // 边界单元素:单条记录;缺 id 的行被过滤。 + const page = parseDedupeTotalPage({ + data: { + items: [{ id: 7, dataValue: 'Z' }, { dataValue: 'no-id' }], + total: 2, + page: 1, + pageSize: 15, + }, + }) + assert.equal(page.items.length, 1) + assert.equal(page.items[0].id, 7) +}) + +test('test_task_062_dedupe_list_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:缺 page/pageSize 用默认;空 dataValue 保留空串。 + const page = parseDedupeTotalPage({ items: [{ id: 9, dataValue: '' }], total: 0, page: 1 }) + assert.equal(page.items[0].dataValue, '') + assert.equal(page.pageSize, 15) + assert.equal(toDedupeTotalItem({ id: 10, dataValue: 'v' })?.dataValue, 'v') +}) + +test('test_task_062_dedupe_list_invalid_input_rejected', () => { + // 异常输入:success=false 抛后端 message;缺 id 行不入列表。 + assert.throws(() => parseDedupeTotalPage({ success: false, message: '无权访问数据去重总数据' }), /无权访问/) + assert.equal(toDedupeTotalItem('garbage'), null) +}) + +test('test_task_062_dedupe_list_dependency_failure_returns_actionable_message', () => { + // 依赖失败/加载走 adapter:GET /api/admin/dedupe-total-data + 共享筛选归一,页面不内联。 + const api = readSource('src/pages/asin/dedupe-total-api.ts') + assert.match(api, /dedupe-total-data/) + assert.match(api, /http\.get/) + assert.match(api, /parseDedupeTotalPage/) + assert.match(api, /normalizeAsinPageParams|toAsinPageQuery/) + const model = readSource('src/pages/asin/dedupe-total-model.ts') + assert.equal(/axios|http\./.test(model), false, '去重列表解析保持纯逻辑') +})