task-108(任务与重复分析中心): 实现视频任务数据范围权限配置

新增 task-permission.ts(授权用户列表解析/已授权 id/保存负载/变更判定),
image-video-api.ts 增加 fetch/saveImageVideoPermissions(GET|PUT /image-video-
task-permissions)。

TDD: task-108.test.ts 8 用例先 RED 后 GREEN。
This commit is contained in:
2026-09-05 17:14:38 +08:00
parent a1e466030b
commit 77d049990c
3 changed files with 140 additions and 0 deletions
@@ -3,8 +3,10 @@ import { http } from '@/api/http'
import { parseImageVideoDetail, parseImageVideoPage, type ImageVideoDetail, type ImageVideoPageResult } from './image-video-model.ts'
import { toImageVideoQuery, type ImageVideoFilter } from './image-video-filter.ts'
import { downloadZipHeaderCounts, toVideoZipRequest } from './image-video-download.ts'
import { parseTaskPermissionPayload, toPermissionUserIdsPayload, type TaskPermissionItem } from './task-permission.ts'
export const IMAGE_VIDEO_TASKS_ENDPOINT = '/api/admin/image-video-tasks'
export const IMAGE_VIDEO_TASK_PERMISSIONS_ENDPOINT = '/api/admin/image-video-task-permissions'
export async function fetchImageVideoTasks(
filter: ImageVideoFilter,
@@ -37,3 +39,14 @@ export async function requestVideoZipDownload(keys: readonly string[]): Promise<
const counts = downloadZipHeaderCounts(headers as Record<string, string>)
return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount }
}
/** 加载视频任务数据范围授权用户列表:GET /api/admin/image-video-task-permissions。 */
export async function fetchImageVideoPermissionUsers(): Promise<TaskPermissionItem[]> {
const { data } = await http.get<unknown>(IMAGE_VIDEO_TASK_PERMISSIONS_ENDPOINT)
return parseTaskPermissionPayload(data)
}
/** 保存视频任务数据范围授权:PUT /api/admin/image-video-task-permissions { user_ids }。 */
export async function saveImageVideoPermissions(userIds: Iterable<number>): Promise<void> {
await http.put<unknown>(IMAGE_VIDEO_TASK_PERMISSIONS_ENDPOINT, toPermissionUserIdsPayload(userIds))
}
@@ -0,0 +1,55 @@
/** 任务数据范围权限配置模型(任务 108,复用店铺数据任务 114):解析授权用户列表与授权集变更;纯逻辑。 */
import { unwrap } from '../../api/envelope.ts'
/** 数据范围授权用户行(Java ImageVideoDataPermissionUserVo / ShopDataCrawlDataPermissionUserVo)。 */
export interface TaskPermissionItem {
id: number
username: string
granted: boolean
}
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
}
/** 解析权限负载(data 数组或 {items})为授权用户行;缺 id 过滤。 */
export function parseTaskPermissionPayload(payload: unknown): TaskPermissionItem[] {
const data = unwrap<unknown>(payload)
const list = Array.isArray(data) ? data : data && typeof data === 'object' ? (data as Record<string, unknown>).items : null
if (!Array.isArray(list)) return []
return list
.map((raw): TaskPermissionItem | null => {
if (!raw || typeof raw !== 'object') return null
const record = raw as Record<string, unknown>
const id = numberOrNull(record.id)
if (id === null || id < 1) return null
return { id, username: text(record.username), granted: record.granted === true }
})
.filter((item): item is TaskPermissionItem => item !== null)
}
/** 已授权用户 id(升序)。 */
export function grantedUserIds(items: readonly TaskPermissionItem[]): number[] {
return items
.filter((item) => item.granted)
.map((item) => item.id)
.sort((a, b) => a - b)
}
/** 保存负载:PUT { user_ids }(后端按数值 id 重算授权)。 */
export function toPermissionUserIdsPayload(userIds: Iterable<number>): { user_ids: number[] } {
return { user_ids: Array.from(userIds) }
}
/** 初始授权集与当前选择是否不一致(需提示保存)。 */
export function permissionSelectionChanged(initial: ReadonlySet<number>, current: ReadonlySet<number>): boolean {
if (initial.size !== current.size) return true
for (const id of initial) {
if (!current.has(id)) return true
}
return false
}
+72
View File
@@ -0,0 +1,72 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import { readSource } from './helpers.ts'
import {
grantedUserIds,
parseTaskPermissionPayload,
permissionSelectionChanged,
toPermissionUserIdsPayload,
} from '../src/pages/tasks/task-permission.ts'
test('test_task_108_task_permission_normal_primary_path', () => {
// 正常主路径:权限用户负载(data 数组)解析并取已授权 id。
const items = parseTaskPermissionPayload({
success: true,
data: [
{ id: 1, username: '张伟', granted: true },
{ id: 2, username: '李四', granted: false },
],
})
assert.equal(items.length, 2)
assert.equal(items[1].username, '李四')
assert.deepEqual(grantedUserIds(items), [1])
})
test('test_task_108_task_permission_normal_variant_input', () => {
// 正常变体:data.items 包裹也可解析。
const items = parseTaskPermissionPayload({ data: { items: [{ id: 5, username: 'A', granted: true }] } })
assert.equal(items.length, 1)
assert.deepEqual(grantedUserIds(items), [5])
})
test('test_task_108_task_permission_repeated_is_idempotent', () => {
// 正常重复:解析稳定、不改输入。
const payload = { data: [{ id: 3, username: 'A', granted: true }] }
assert.deepEqual(parseTaskPermissionPayload(payload), parseTaskPermissionPayload(payload))
})
test('test_task_108_task_permission_boundary_empty_input', () => {
// 边界空值:空授权列表与无用户负载。
assert.deepEqual(parseTaskPermissionPayload({ data: [] }), [])
assert.deepEqual(parseTaskPermissionPayload({}), [])
})
test('test_task_108_task_permission_boundary_single_item', () => {
// 边界单元素:单用户授权 id 与保存负载。
const items = parseTaskPermissionPayload({ data: [{ id: 7, username: 'Z', granted: true }] })
assert.deepEqual(grantedUserIds(items), [7])
assert.deepEqual(toPermissionUserIdsPayload(new Set([7, 3])), { user_ids: [7, 3] })
})
test('test_task_108_task_permission_boundary_limit_or_missing_field', () => {
// 边界上限/缺字段:缺 granted 按未授权处理。
const items = parseTaskPermissionPayload({ data: [{ id: 4, username: 'U' }] })
assert.equal(items[0].granted, false)
assert.deepEqual(grantedUserIds(items), [])
})
test('test_task_108_task_permission_invalid_input_rejected', () => {
// 异常输入:无 id 用户被过滤;success=false 抛后端 message。
assert.equal(parseTaskPermissionPayload({ data: [{ username: 'no-id' }, { id: 2, username: 'x' }] }).length, 1)
assert.throws(() => parseTaskPermissionPayload({ success: false, message: '仅超级管理员可配置' }), /仅超级管理员/)
})
test('test_task_108_task_permission_dependency_failure_returns_actionable_message', () => {
// 依赖失败/加载走 adapterGET|PUT /api/admin/image-video-task-permissions + 解析。
const api = readSource('src/pages/tasks/image-video-api.ts')
assert.match(api, /\/api\/admin\/image-video-task-permissions/)
assert.match(api, /saveImageVideoPermissions/)
assert.match(api, /http\.put/)
const model = readSource('src/pages/tasks/task-permission.ts')
assert.equal(/axios|http\.|vue/.test(model), false, '权限模型保持纯逻辑')
})