task-102(任务与重复分析中心): 实现视频任务筛选状态
新增 src/pages/tasks/image-video-filter.ts:用户名/user/状态/executeId/ 提交时间区间筛选归一与 snake 查询序列化,页大小钳制 [10,100]。 TDD: task-102.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
@@ -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>): 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
|
||||||
|
}
|
||||||
@@ -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/)
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user