From 602c2118d3ab247a933125b8b3d8dc2de0ac9b55 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:02:25 +0800 Subject: [PATCH] =?UTF-8?q?task-102(=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=E8=A7=86=E9=A2=91=E4=BB=BB=E5=8A=A1=E7=AD=9B=E9=80=89?= =?UTF-8?q?=E7=8A=B6=E6=80=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 src/pages/tasks/image-video-filter.ts:用户名/user/状态/executeId/ 提交时间区间筛选归一与 snake 查询序列化,页大小钳制 [10,100]。 TDD: task-102.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/tasks/image-video-filter.ts | 106 ++++++++++++++++++ admin-frontend-vue/tests/task-102.test.ts | 80 +++++++++++++ 2 files changed, 186 insertions(+) create mode 100644 admin-frontend-vue/src/pages/tasks/image-video-filter.ts create mode 100644 admin-frontend-vue/tests/task-102.test.ts diff --git a/admin-frontend-vue/src/pages/tasks/image-video-filter.ts b/admin-frontend-vue/src/pages/tasks/image-video-filter.ts new file mode 100644 index 00000000..3af64d86 --- /dev/null +++ b/admin-frontend-vue/src/pages/tasks/image-video-filter.ts @@ -0,0 +1,106 @@ +/** 视频任务筛选与分页(任务 102):图生视频任务列表筛选/分页归一,与 Java AdminImageVideoTaskController + * snake_case 查询契约对齐;纯逻辑。 */ + +export const IMAGE_VIDEO_DEFAULT_PAGE_SIZE = 20 +export const IMAGE_VIDEO_MIN_PAGE_SIZE = 10 +export const IMAGE_VIDEO_MAX_PAGE_SIZE = 100 +export const IMAGE_VIDEO_PAGE_MIN = 1 + +const ALLOWED_STATUS: readonly string[] = ['PENDING', 'RUNNING', 'SUCCESS', 'FAILED', 'CANCELLED'] +const LOCAL_DATETIME = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}(:\d{2})?$/ + +export interface ImageVideoFilter { + username: string + userId: number | null + status: string + executeId: string + submittedFrom: string + submittedTo: string +} + +export interface ImageVideoQuery { + page: number + page_size: number + user_id?: number + username?: string + status?: string + coze_execute_id?: string + submitted_from?: string + submitted_to?: string +} + +export function createImageVideoFilter(): ImageVideoFilter { + return { username: '', userId: null, status: '', executeId: '', submittedFrom: '', submittedTo: '' } +} + +function textOrUndefined(value: unknown): string | undefined { + const text = typeof value === 'string' ? value.trim() : '' + return text || undefined +} + +function positiveIdOrNull(value: unknown): number | null { + if (typeof value === 'number') return Number.isFinite(value) && value >= 1 ? Math.floor(value) : null + if (typeof value === 'string' && /^\d+$/.test(value.trim())) { + const parsed = Number(value.trim()) + return parsed >= 1 ? Math.floor(parsed) : null + } + return null +} + +function statusOrUndefined(value: unknown): string | undefined { + const text = typeof value === 'string' ? value.trim().toUpperCase() : '' + return ALLOWED_STATUS.includes(text) ? 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 +} + +/** 归一筛选状态:去空白、非法状态/日期/负数 id 置空。 */ +export function normalizeImageVideoFilter(raw: Partial): ImageVideoFilter { + return { + username: typeof raw.username === 'string' ? raw.username.trim() : '', + userId: positiveIdOrNull(raw.userId), + status: statusOrUndefined(raw.status) ?? '', + executeId: typeof raw.executeId === 'string' ? raw.executeId.trim() : '', + submittedFrom: datetimeOrUndefined(raw.submittedFrom) ?? '', + submittedTo: datetimeOrUndefined(raw.submittedTo) ?? '', + } +} + +export function imageVideoFilterActive(filter: ImageVideoFilter): boolean { + return Boolean( + filter.username.trim() || + filter.userId !== null || + filter.status || + filter.executeId.trim() || + filter.submittedFrom || + filter.submittedTo, + ) +} + +function finiteInt(value: unknown): number | null { + return typeof value === 'number' && Number.isFinite(value) ? Math.floor(value) : null +} + +/** 前端状态 → Java 查询参数(snake_case,仅下发非空)。 */ +export function toImageVideoQuery(filter: ImageVideoFilter, page: unknown, pageSize: unknown): ImageVideoQuery { + const safePage = Math.max(finiteInt(page) ?? IMAGE_VIDEO_PAGE_MIN, IMAGE_VIDEO_PAGE_MIN) + const rawSize = finiteInt(pageSize) + const pageSizeClamped = + rawSize === null || rawSize < 1 + ? IMAGE_VIDEO_DEFAULT_PAGE_SIZE + : Math.min(Math.max(rawSize, IMAGE_VIDEO_MIN_PAGE_SIZE), IMAGE_VIDEO_MAX_PAGE_SIZE) + const query: ImageVideoQuery = { page: safePage, page_size: pageSizeClamped } + const username = filter.username.trim() + if (username) query.username = username + if (filter.userId !== null) query.user_id = filter.userId + if (filter.status) query.status = filter.status + const executeId = filter.executeId.trim() + if (executeId) query.coze_execute_id = executeId + if (filter.submittedFrom) query.submitted_from = filter.submittedFrom + if (filter.submittedTo) query.submitted_to = filter.submittedTo + return query +} diff --git a/admin-frontend-vue/tests/task-102.test.ts b/admin-frontend-vue/tests/task-102.test.ts new file mode 100644 index 00000000..004d1c13 --- /dev/null +++ b/admin-frontend-vue/tests/task-102.test.ts @@ -0,0 +1,80 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' +import { + IMAGE_VIDEO_DEFAULT_PAGE_SIZE, + IMAGE_VIDEO_MAX_PAGE_SIZE, + createImageVideoFilter, + imageVideoFilterActive, + normalizeImageVideoFilter, + toImageVideoQuery, +} from '../src/pages/tasks/image-video-filter.ts' + +test('test_task_102_video_filter_normal_primary_path', () => { + // 正常主路径:完整筛选归一并序列化为 snake 查询参数。 + const filter = normalizeImageVideoFilter({ username: ' 张伟 ', userId: 3, status: ' success ', executeId: 'exec-1', submittedFrom: '2026-01-01T10:00', submittedTo: '2026-01-02T18:30' }) + const query = toImageVideoQuery(filter, 2, 20) + assert.equal(query.page, 2) + assert.equal(query.page_size, 20) + assert.equal(query.username, '张伟') + assert.equal(query.user_id, 3) + assert.equal(query.status, 'SUCCESS') + assert.equal(query.coze_execute_id, 'exec-1') + assert.equal(query.submitted_from, '2026-01-01T10:00') + assert.equal(query.submitted_to, '2026-01-02T18:30') +}) + +test('test_task_102_video_filter_normal_variant_input', () => { + // 正常变体:仅状态筛选下发;空用户名/无 user 条件不下发。 + const query = toImageVideoQuery(normalizeImageVideoFilter({ status: 'running' }), 1, 20) + assert.equal(query.status, 'RUNNING') + assert.equal('username' in query, false) + assert.equal('user_id' in query, false) +}) + +test('test_task_102_video_filter_repeated_is_idempotent', () => { + // 正常重复:归一不改输入、结果稳定。 + const input = { username: ' admin ', status: 'FAILED' } + assert.deepEqual(normalizeImageVideoFilter(input), normalizeImageVideoFilter(input)) + assert.deepEqual(input, { username: ' admin ', status: 'FAILED' }) +}) + +test('test_task_102_video_filter_boundary_empty_input', () => { + // 边界空值:空筛选回默认分页、无可选条件。 + const query = toImageVideoQuery(createImageVideoFilter(), 0, 0) + assert.equal(query.page, 1) + assert.equal(query.page_size, IMAGE_VIDEO_DEFAULT_PAGE_SIZE) + assert.deepEqual(Object.keys(query).sort(), ['page', 'page_size']) +}) + +test('test_task_102_video_filter_boundary_single_item', () => { + // 边界单元素:仅 coze_execute_id 筛选。 + const query = toImageVideoQuery(normalizeImageVideoFilter({ executeId: 'abc' }), 1, 20) + assert.equal(query.coze_execute_id, 'abc') +}) + +test('test_task_102_video_filter_boundary_limit_or_missing_field', () => { + // 边界上限/缺字段:页大小钳制到 [10,100];非法日期/状态过滤。 + const query = toImageVideoQuery(normalizeImageVideoFilter({ status: 'weird', submittedFrom: 'not-a-date' }), 1, 999) + assert.equal(query.page_size, IMAGE_VIDEO_MAX_PAGE_SIZE) + assert.equal('status' in query, false, '非法状态不下发') + assert.equal('submitted_from' in query, false, '非法日期不下发') +}) + +test('test_task_102_video_filter_invalid_input_rejected', () => { + // 异常输入:数字/NaN 页码按缺省处理;负数 user id 忽略。 + const filter = normalizeImageVideoFilter({ userId: -1, status: '' }) + const query = toImageVideoQuery(filter, Number.NaN, Number.NaN) + assert.equal(query.page, 1) + assert.equal(query.page_size, IMAGE_VIDEO_DEFAULT_PAGE_SIZE) + assert.equal('user_id' in query, false) + assert.equal('status' in query, false) +}) + +test('test_task_102_video_filter_dependency_failure_returns_actionable_message', () => { + // 依赖失败/可操作:筛选纯逻辑、无框架/http。 + const mod = readSource('src/pages/tasks/image-video-filter.ts') + assert.equal(/axios|http\.|vue/.test(mod), false, '筛选模块保持纯逻辑') + assert.match(mod, /submitted_from/) + assert.match(mod, /coze_execute_id/) +})