From 88d3bfb8b2dba6ce98c90c6eca19ecd6b128fc78 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 17:15:49 +0800 Subject: [PATCH] =?UTF-8?q?task-109(=E4=BB=BB=E5=8A=A1=E4=B8=8E=E9=87=8D?= =?UTF-8?q?=E5=A4=8D=E5=88=86=E6=9E=90=E4=B8=AD=E5=BF=83):=20=E5=AE=9E?= =?UTF-8?q?=E7=8E=B0=E5=BA=97=E9=93=BA=E6=95=B0=E6=8D=AE=E4=BB=BB=E5=8A=A1?= =?UTF-8?q?=E7=AD=9B=E9=80=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 shop-data-filter.ts:店铺名/分组/国家/创建时间区间筛选归一与 snake 查询序列化,页大小钳制 [10,100]。 TDD: task-109.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/tasks/shop-data-filter.ts | 77 +++++++++++++++++++ admin-frontend-vue/tests/task-109.test.ts | 75 ++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 admin-frontend-vue/src/pages/tasks/shop-data-filter.ts create mode 100644 admin-frontend-vue/tests/task-109.test.ts diff --git a/admin-frontend-vue/src/pages/tasks/shop-data-filter.ts b/admin-frontend-vue/src/pages/tasks/shop-data-filter.ts new file mode 100644 index 00000000..11150731 --- /dev/null +++ b/admin-frontend-vue/src/pages/tasks/shop-data-filter.ts @@ -0,0 +1,77 @@ +/** 店铺数据任务筛选与分页(任务 109):与 Java AdminShopDataCrawlTasksController snake 契约对齐;纯逻辑。 */ + +export const SHOP_DATA_DEFAULT_PAGE_SIZE = 20 +export const SHOP_DATA_MIN_PAGE_SIZE = 10 +export const SHOP_DATA_MAX_PAGE_SIZE = 100 +export const SHOP_DATA_PAGE_MIN = 1 + +const LOCAL_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/ + +export interface ShopDataFilter { + shopName: string + groupName: string + country: string + createdFrom: string + createdTo: string +} + +export interface ShopDataQuery { + page: number + page_size: number + shop_name?: string + group_name?: string + country?: string + created_from?: string + created_to?: string +} + +export function createShopDataFilter(): ShopDataFilter { + return { shopName: '', groupName: '', country: '', createdFrom: '', createdTo: '' } +} + +function textOrUndefined(value: unknown): string | undefined { + const text = typeof value === 'string' ? value.trim() : '' + return text || undefined +} + +function datetimeOrUndefined(value: unknown): string | undefined { + if (typeof value !== 'string') return undefined + const text = value.trim() + return LOCAL_DATETIME.test(text) ? text : undefined +} + +export function shopDataFilterActive(filter: ShopDataFilter): boolean { + return Boolean( + filter.shopName.trim() || + filter.groupName.trim() || + filter.country.trim() || + filter.createdFrom || + filter.createdTo, + ) +} + +function finiteInt(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 前端状态 → Java 查询参数(snake_case,仅下发非空)。 */ +export function toShopDataQuery(filter: ShopDataFilter, page: unknown, pageSize: unknown): ShopDataQuery { + const safePage = Math.max(finiteInt(page) ?? SHOP_DATA_PAGE_MIN, SHOP_DATA_PAGE_MIN) + const rawSize = finiteInt(pageSize) + const pageSizeClamped = + rawSize === null || rawSize < 1 + ? SHOP_DATA_DEFAULT_PAGE_SIZE + : Math.min(Math.max(rawSize, SHOP_DATA_MIN_PAGE_SIZE), SHOP_DATA_MAX_PAGE_SIZE) + const query: ShopDataQuery = { page: safePage, page_size: pageSizeClamped } + const shopName = textOrUndefined(filter.shopName) + if (shopName) query.shop_name = shopName + const groupName = textOrUndefined(filter.groupName) + if (groupName) query.group_name = groupName + const country = textOrUndefined(filter.country) + if (country) query.country = country + const createdFrom = datetimeOrUndefined(filter.createdFrom) + if (createdFrom) query.created_from = createdFrom + const createdTo = datetimeOrUndefined(filter.createdTo) + if (createdTo) query.created_to = createdTo + return query +} diff --git a/admin-frontend-vue/tests/task-109.test.ts b/admin-frontend-vue/tests/task-109.test.ts new file mode 100644 index 00000000..0d09af31 --- /dev/null +++ b/admin-frontend-vue/tests/task-109.test.ts @@ -0,0 +1,75 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + SHOP_DATA_DEFAULT_PAGE_SIZE, + SHOP_DATA_MAX_PAGE_SIZE, + createShopDataFilter, + shopDataFilterActive, + toShopDataQuery, +} from '../src/pages/tasks/shop-data-filter.ts' + +test('test_task_109_shop_data_filter_normal_primary_path', () => { + // 正常主路径:完整筛选归一并序列化为 snake 查询参数。 + const filter = { shopName: ' A店 ', groupName: '华东组', country: 'DE', createdFrom: '2026-01-01T10:00', createdTo: '2026-01-02T18:30' } + const query = toShopDataQuery(filter, 2, 20) + assert.equal(query.page, 2) + assert.equal(query.page_size, 20) + assert.equal(query.shop_name, 'A店') + assert.equal(query.group_name, '华东组') + assert.equal(query.country, 'DE') + assert.equal(query.created_from, '2026-01-01T10:00') + assert.equal(query.created_to, '2026-01-02T18:30') +}) + +test('test_task_109_shop_data_filter_normal_variant_input', () => { + // 正常变体:仅店铺名筛选下发;空国家不下发。 + const query = toShopDataQuery({ shopName: '蓝店', groupName: '', country: '', createdFrom: '', createdTo: '' }, 1, 20) + assert.equal(query.shop_name, '蓝店') + assert.equal('country' in query, false) + assert.equal('group_name' in query, false) +}) + +test('test_task_109_shop_data_filter_repeated_is_idempotent', () => { + // 正常重复:归一不改输入、结果稳定。 + const input = { shopName: ' X ', groupName: ' G ' } + assert.deepEqual(toShopDataQuery(input, 1, 20), toShopDataQuery(input, 1, 20)) + assert.equal((input as { shopName: string }).shopName, ' X ') +}) + +test('test_task_109_shop_data_filter_boundary_empty_input', () => { + // 边界空值:空筛选回默认分页、无可选条件。 + const query = toShopDataQuery(createShopDataFilter(), 0, 0) + assert.equal(query.page, 1) + assert.equal(query.page_size, SHOP_DATA_DEFAULT_PAGE_SIZE) + assert.deepEqual(Object.keys(query).sort(), ['page', 'page_size']) +}) + +test('test_task_109_shop_data_filter_boundary_single_item', () => { + // 边界单元素:仅分组筛选。 + const query = toShopDataQuery({ shopName: '', groupName: 'G', country: '', createdFrom: '', createdTo: '' }, 1, 20) + assert.equal(query.group_name, 'G') +}) + +test('test_task_109_shop_data_filter_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:页大小钳制到上限;非法日期不下发。 + const query = toShopDataQuery(createShopDataFilter(), 1, 999) + assert.equal(query.page_size, SHOP_DATA_MAX_PAGE_SIZE) + const q2 = toShopDataQuery({ shopName: '', groupName: '', country: '', createdFrom: 'bad', createdTo: '' }, 1, 20) + assert.equal('created_from' in q2, false) +}) + +test('test_task_109_shop_data_filter_invalid_input_rejected', () => { + // 异常输入:NaN 页码按缺省处理。 + const query = toShopDataQuery(createShopDataFilter(), Number.NaN, Number.NaN) + assert.equal(query.page, 1) + assert.equal(query.page_size, SHOP_DATA_DEFAULT_PAGE_SIZE) +}) + +test('test_task_109_shop_data_filter_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:筛选纯逻辑、无框架/http。 + const mod = readSource('src/pages/tasks/shop-data-filter.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '筛选模块保持纯逻辑') + assert.match(mod, /created_from/) + assert.match(mod, /group_name/) +})