From 77d049990cc8bbdf60bfd17a86dc25933d62f047 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:14:38 +0800 Subject: [PATCH] =?UTF-8?q?task-108(=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=E6=95=B0=E6=8D=AE?= =?UTF-8?q?=E8=8C=83=E5=9B=B4=E6=9D=83=E9=99=90=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 task-permission.ts(授权用户列表解析/已授权 id/保存负载/变更判定), image-video-api.ts 增加 fetch/saveImageVideoPermissions(GET|PUT /image-video- task-permissions)。 TDD: task-108.test.ts 8 用例先 RED 后 GREEN。 --- .../src/pages/tasks/image-video-api.ts | 13 ++++ .../src/pages/tasks/task-permission.ts | 55 ++++++++++++++ admin-frontend-vue/tests/task-108.test.ts | 72 +++++++++++++++++++ 3 files changed, 140 insertions(+) create mode 100644 admin-frontend-vue/src/pages/tasks/task-permission.ts create mode 100644 admin-frontend-vue/tests/task-108.test.ts 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, '权限模型保持纯逻辑') +})