diff --git a/admin-frontend-vue/src/pages/tasks/image-video-api.ts b/admin-frontend-vue/src/pages/tasks/image-video-api.ts index 5a77aa6a..3914fdc7 100644 --- a/admin-frontend-vue/src/pages/tasks/image-video-api.ts +++ b/admin-frontend-vue/src/pages/tasks/image-video-api.ts @@ -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) return { blob: data, fileCount: counts.fileCount, errorCount: counts.errorCount } } + +/** 加载视频任务数据范围授权用户列表:GET /api/admin/image-video-task-permissions。 */ +export async function fetchImageVideoPermissionUsers(): Promise { + const { data } = await http.get(IMAGE_VIDEO_TASK_PERMISSIONS_ENDPOINT) + return parseTaskPermissionPayload(data) +} + +/** 保存视频任务数据范围授权:PUT /api/admin/image-video-task-permissions { user_ids }。 */ +export async function saveImageVideoPermissions(userIds: Iterable): Promise { + await http.put(IMAGE_VIDEO_TASK_PERMISSIONS_ENDPOINT, toPermissionUserIdsPayload(userIds)) +} diff --git a/admin-frontend-vue/src/pages/tasks/task-permission.ts b/admin-frontend-vue/src/pages/tasks/task-permission.ts new file mode 100644 index 00000000..d20c3d91 --- /dev/null +++ b/admin-frontend-vue/src/pages/tasks/task-permission.ts @@ -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(payload) + const list = Array.isArray(data) ? data : data && typeof data === 'object' ? (data as Record).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 + 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): { user_ids: number[] } { + return { user_ids: Array.from(userIds) } +} + +/** 初始授权集与当前选择是否不一致(需提示保存)。 */ +export function permissionSelectionChanged(initial: ReadonlySet, current: ReadonlySet): boolean { + if (initial.size !== current.size) return true + for (const id of initial) { + if (!current.has(id)) return true + } + return false +} diff --git a/admin-frontend-vue/tests/task-108.test.ts b/admin-frontend-vue/tests/task-108.test.ts new file mode 100644 index 00000000..a7155e5a --- /dev/null +++ b/admin-frontend-vue/tests/task-108.test.ts @@ -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', () => { + // 依赖失败/加载走 adapter:GET|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, '权限模型保持纯逻辑') +})