From 53b4cb52f3818b07f5419a26006dc80024d3bc1d 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:26:42 +0800 Subject: [PATCH] =?UTF-8?q?task-61(ASIN=20=E6=95=B0=E6=8D=AE=E4=B8=AD?= =?UTF-8?q?=E5=BF=83):=20=E5=AE=9A=E4=B9=89=E5=85=B1=E4=BA=AB=E5=88=86?= =?UTF-8?q?=E9=A1=B5=E5=92=8C=E7=AD=9B=E9=80=89=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 src/pages/asin/asin-filter.ts 纯共享层:AsinListParams 分页+keyword/username/日期区间/ groupId/国家筛选,normalizeAsinPageParams 钳制页码页大小并校验 ISO 日期,toAsinPageQuery 序列化 snake_case(仅下发非空),镜像 DedupeTotalDataController.page 参数契约。 8 用例全过,443 单测 + build 绿。 --- .../src/pages/asin/asin-filter.ts | 81 +++++++++++++++++ admin-frontend-vue/tests/task-61.test.ts | 87 +++++++++++++++++++ 2 files changed, 168 insertions(+) create mode 100644 admin-frontend-vue/src/pages/asin/asin-filter.ts create mode 100644 admin-frontend-vue/tests/task-61.test.ts diff --git a/admin-frontend-vue/src/pages/asin/asin-filter.ts b/admin-frontend-vue/src/pages/asin/asin-filter.ts new file mode 100644 index 00000000..ae26f30e --- /dev/null +++ b/admin-frontend-vue/src/pages/asin/asin-filter.ts @@ -0,0 +1,81 @@ +/** ASIN 数据中心共享分页/筛选类型(任务 61):与 Java 各列表接口(snake)对齐,纯逻辑。 */ + +export const ASIN_PAGE_DEFAULT_SIZE = 15 +export const ASIN_PAGE_MIN_PAGE = 1 +export const ASIN_PAGE_MAX_SIZE = 200 + +/** ISO 日期(yyyy-MM-dd)或空。 */ +export function validIsoDateOrUndefined(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const text = value.trim() + return /^\d{4}-\d{2}-\d{2}$/.test(text) ? text : undefined +} + +/** ASIN 中心列表页共享筛选/分页状态。 */ +export interface AsinListParams { + page: number + pageSize: number + /** 值/商品模糊关键字。 */ + keyword?: string + /** 上传用户名模糊搜索。 */ + username?: string + startDate?: string + endDate?: string + groupId?: number | null + /** 国家代码(如 DE、UK)。 */ + country?: string +} + +/** 序列化到 Java 分页接口的查询参数(snake_case)。 */ +export interface AsinPageQuery { + page: number + page_size: number + keyword?: string + username?: string + start_date?: string + end_date?: string + group_id?: number + country?: string +} + +function finiteInt(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 归一化共享分页/筛选:页码下限 1、页大小 1..上限,空条件视为无筛选。 */ +export function normalizeAsinPageParams(raw: Partial): AsinListParams { + const page = Math.max(finiteInt(raw.page) ?? ASIN_PAGE_MIN_PAGE, ASIN_PAGE_MIN_PAGE) + const rawSize = finiteInt(raw.pageSize) + const pageSize = + rawSize === null || rawSize < ASIN_PAGE_MIN_PAGE + ? ASIN_PAGE_DEFAULT_SIZE + : Math.min(rawSize, ASIN_PAGE_MAX_SIZE) + const textOrUndefined = (value: unknown) => + typeof value === 'string' && value.trim() ? value.trim() : undefined + const keyword = textOrUndefined(raw.keyword) + const username = textOrUndefined(raw.username) + const country = textOrUndefined(raw.country) + const groupId = finiteInt(raw.groupId) + return { + page, + pageSize, + keyword, + username, + startDate: validIsoDateOrUndefined(raw.startDate), + endDate: validIsoDateOrUndefined(raw.endDate), + groupId: groupId === null || groupId < 1 ? null : groupId, + country, + } +} + +/** 前端状态 → Java 查询参数;仅下发非空字段。 */ +export function toAsinPageQuery(params: AsinListParams): AsinPageQuery { + const query: AsinPageQuery = { page: params.page, page_size: params.pageSize } + if (params.keyword) query.keyword = params.keyword + if (params.username) query.username = params.username + if (params.startDate) query.start_date = params.startDate + if (params.endDate) query.end_date = params.endDate + if (params.groupId != null) query.group_id = params.groupId + if (params.country) query.country = params.country + return query +} diff --git a/admin-frontend-vue/tests/task-61.test.ts b/admin-frontend-vue/tests/task-61.test.ts new file mode 100644 index 00000000..b23e5fb3 --- /dev/null +++ b/admin-frontend-vue/tests/task-61.test.ts @@ -0,0 +1,87 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + ASIN_PAGE_DEFAULT_SIZE, + normalizeAsinPageParams, + toAsinPageQuery, + validIsoDateOrUndefined, +} from '../src/pages/asin/asin-filter.ts' + +test('test_task_061_asin_shared_filter_normal_primary_path', () => { + // 正常主路径:完整筛选条件归一并序列化为 Java snake_case 查询参数。 + const params = normalizeAsinPageParams({ + page: 2, + pageSize: 20, + keyword: 'B0ABC', + startDate: '2026-01-01', + endDate: '2026-01-31', + groupId: 3, + country: 'DE', + }) + assert.equal(params.page, 2) + const query = toAsinPageQuery(params) + assert.equal(query.page, 2) + assert.equal(query.page_size, 20) + assert.equal(query.keyword, 'B0ABC') + assert.equal(query.start_date, '2026-01-01') + assert.equal(query.end_date, '2026-01-31') + assert.equal(query.group_id, 3) + assert.equal(query.country, 'DE') +}) + +test('test_task_061_asin_shared_filter_normal_variant_input', () => { + // 正常变体:username 维度可选;空条件不下发字段。 + const query = toAsinPageQuery(normalizeAsinPageParams({ page: 1, pageSize: 15, username: ' 张伟恒 ' })) + assert.equal(query.username, '张伟恒') + assert.equal('keyword' in query, false) +}) + +test('test_task_061_asin_shared_filter_normal_repeated_operation_is_idempotent', () => { + // 正常重复:归一不改输入、结果稳定。 + const input = { page: 3, pageSize: 30, keyword: 'K' } + assert.deepEqual(normalizeAsinPageParams(input), normalizeAsinPageParams(input)) + assert.equal((input as { keyword: string }).keyword, 'K') +}) + +test('test_task_061_asin_shared_filter_boundary_empty_input', () => { + // 边界空值:空入参回默认分页(第 1 页/15 条),无其它筛选。 + const params = normalizeAsinPageParams({}) + assert.equal(params.page, 1) + assert.equal(params.pageSize, ASIN_PAGE_DEFAULT_SIZE) + const query = toAsinPageQuery(params) + assert.deepEqual(Object.keys(query).sort(), ['page', 'page_size']) +}) + +test('test_task_061_asin_shared_filter_boundary_single_item', () => { + // 边界单元素:country 单筛选保留。 + const query = toAsinPageQuery(normalizeAsinPageParams({ page: 1, country: 'UK' })) + assert.equal(query.country, 'UK') +}) + +test('test_task_061_asin_shared_filter_boundary_limit_or_missing_field', () => { + // 边界上限:页码/页大小钳制;非法 ISO 日期按无日期处理;负 groupId 忽略。 + const params = normalizeAsinPageParams({ page: 0, pageSize: 999, startDate: 'not-a-date', groupId: -1 }) + assert.equal(params.page, 1) + assert.equal(params.pageSize, 200) + assert.equal(params.startDate, undefined) + assert.equal(params.groupId, null) + assert.equal(validIsoDateOrUndefined('2026-02-01'), '2026-02-01') +}) + +test('test_task_061_asin_shared_filter_invalid_input_rejected', () => { + // 异常输入:非数字/NaN 回默认;空字符串条件不下发。 + const query = toAsinPageQuery(normalizeAsinPageParams({ page: 'x' as never, pageSize: NaN, keyword: ' ' })) + assert.equal(query.page, 1) + assert.equal(query.page_size, ASIN_PAGE_DEFAULT_SIZE) + assert.equal('keyword' in query, false) +}) + +test('test_task_061_asin_shared_filter_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:共享筛选为纯逻辑、无框架/http,日期与国家可空。 + const mod = readSource('src/pages/asin/asin-filter.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '共享筛选保持纯逻辑') + assert.match(mod, /start_date/) + assert.match(mod, /end_date/) + assert.match(mod, /country/) +})