feat: 接入文档权限批量接口

This commit is contained in:
xy
2026-09-03 14:15:38 +08:00
parent 925d8c9202
commit 6d56bc0075
7 changed files with 437 additions and 75 deletions
@@ -1,13 +1,22 @@
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
import { request } from '@/utils/request'
import type { ApiResponse } from '@/types/common'
import type { ApiResponse, PageResult } from '@/types/common'
import type {
BackendCollection,
BackendPage,
CreateTemplatePermissionRequest,
PermissionListResponse,
PermissionQuery,
PermissionRecord,
TemplatePermissionBatchRequest,
TemplatePermissionBatchResponse,
TemplatePermissionBatchResponseDto,
TemplatePermissionBatchResult,
TemplatePermissionBatchResultDto,
TemplatePermissionMatrixItemDto,
TemplatePermissionMatrixQuery,
TemplatePermissionMatrixResponse,
TemplatePermissionRecord,
TemplatePermissionResponseDto,
} from './types'
@@ -85,17 +94,96 @@ function formatDateTime(value: string | null | undefined) {
}
function mapTemplatePermission(dto: TemplatePermissionResponseDto): TemplatePermissionRecord {
const subjectId = dto.subjectId ?? null
return {
id: dto.id,
id: dto.id ?? undefined,
templateId: dto.templateId,
subjectType: dto.subjectType,
subjectId: dto.subjectId,
subjectName: dto.subjectId,
subjectId,
subjectName: subjectId ?? '系统默认继承规则',
permissionLevel: dto.permissionLevel,
effect: dto.effect,
inherited: dto.inherited,
createdAt: formatDateTime(dto.createdAt),
createdBy: dto.createdBy,
createdBy: dto.createdBy ?? undefined,
}
}
function mapMatrixPermission(dto: TemplatePermissionMatrixItemDto): TemplatePermissionRecord {
const subjectId = dto.subjectId ?? null
return {
id: dto.permissionId ?? undefined,
templateId: dto.templateId,
subjectType: dto.subjectType,
subjectId,
subjectName: subjectId ?? '系统默认继承规则',
permissionLevel: dto.permissionLevel,
effect: dto.effect,
inherited: dto.inherited,
createdAt: formatDateTime(dto.createdAt),
createdBy: dto.createdBy ?? undefined,
}
}
function normalizePage<T>(
data: BackendPage<T> | T[],
fallbackPage: number,
fallbackPageSize: number,
): PageResult<T> {
const records = Array.isArray(data) ? data : (data.records ?? data.items ?? data.content ?? [])
return {
records,
total: Array.isArray(data) ? records.length : (data.total ?? records.length),
page: Array.isArray(data) ? fallbackPage : (data.page ?? fallbackPage),
pageSize: Array.isArray(data) ? fallbackPageSize : (data.size ?? fallbackPageSize),
}
}
function toMatrixQuery(query: TemplatePermissionMatrixQuery) {
if (query.templateIds && query.templateIds.length > 100) {
throw new Error('跨模板权限矩阵最多同时查询 100 个模板')
}
const params: Record<string, string | number | string[]> = {
page: Math.max(query.page, 1),
size: Math.min(Math.max(query.pageSize, 1), 200),
}
const optionalParams: Array<[string, string | undefined]> = [
['keyword', query.keyword?.trim() || undefined],
['templateId', query.templateId],
['roleId', query.roleId],
['subjectDepartmentId', query.subjectDepartmentId],
['userId', query.userId],
['subjectType', query.subjectType],
['campusId', query.campusId],
['templateDepartmentId', query.templateDepartmentId],
]
for (const [key, value] of optionalParams) {
if (value) {
params[key] = value
}
}
if (query.templateIds?.length) {
params.templateIds = query.templateIds
}
return params
}
function mapBatchResult(dto: TemplatePermissionBatchResultDto): TemplatePermissionBatchResult {
return {
index: dto.index,
templateId: dto.templateId,
action: dto.action,
result: dto.result,
permissionId: dto.permissionId ?? undefined,
permission: dto.permission ? mapMatrixPermission(dto.permission) : undefined,
}
}
@@ -116,6 +204,52 @@ export async function getTemplatePermissions(
return records.map(mapTemplatePermission)
}
export async function getTemplatePermissionMatrix(
query: TemplatePermissionMatrixQuery,
): Promise<TemplatePermissionMatrixResponse> {
const page = Math.max(query.page, 1)
const pageSize = Math.min(Math.max(query.pageSize, 1), 200)
if (useMockData) {
return { records: [], total: 0, page, pageSize }
}
const response = await request.get<
ApiResponse<BackendCollection<TemplatePermissionMatrixItemDto>>
>('/v1/templates/permissions/matrix', {
params: toMatrixQuery(query),
paramsSerializer: { indexes: null },
})
const data = normalizePage(unwrapApiResponse(response), page, pageSize)
return {
...data,
records: data.records.map(mapMatrixPermission),
}
}
export async function batchSaveTemplatePermissions(
payload: TemplatePermissionBatchRequest,
): Promise<TemplatePermissionBatchResponse> {
if (useMockData) {
throw new Error('Mock 模式不执行权限批量写操作')
}
if (payload.operations.length > 200) {
throw new Error('模板权限批量保存最多支持 200 项操作')
}
const response = await request.post<ApiResponse<TemplatePermissionBatchResponseDto>>(
'/v1/templates/permissions/batch',
payload,
)
const data = unwrapApiResponse(response)
return {
results: (data.results ?? []).map(mapBatchResult),
}
}
export async function createTemplatePermission(
templateId: string,
payload: CreateTemplatePermissionRequest,
+32 -12
View File
@@ -138,23 +138,35 @@ function toBackendQuery(query: DocumentQuery) {
params.status = statusMap[query.status]
}
if (query.campusId) {
params.campusId = query.campusId
}
if (query.departmentId) {
params.departmentId = query.departmentId
}
return params
}
function getRemoteDocuments(path: string, query: DocumentQuery): Promise<DocumentListResponse> {
return request
.get<ApiResponse<BackendCollection<TemplateResponseDto>>>(path, {
params: toBackendQuery(query),
})
.then((response) => {
const page = normalizePage(unwrapApiResponse(response), query.page, query.pageSize)
return {
...page,
records: page.records.map(mapDocument),
}
})
}
export function getDocuments(query: DocumentQuery): Promise<DocumentListResponse> {
if (!useMockData) {
return request
.get<ApiResponse<BackendCollection<TemplateResponseDto>>>('/v1/templates', {
params: toBackendQuery(query),
})
.then((response) => {
const page = normalizePage(unwrapApiResponse(response), query.page, query.pageSize)
return {
...page,
records: page.records.map(mapDocument),
}
})
return getRemoteDocuments('/v1/templates', query)
}
const keyword = query.keyword?.trim().toLowerCase()
@@ -176,6 +188,14 @@ export function getDocuments(query: DocumentQuery): Promise<DocumentListResponse
})
}
export function getTemplatePermissionConfig(query: DocumentQuery): Promise<DocumentListResponse> {
if (useMockData) {
return getDocuments(query)
}
return getRemoteDocuments('/v1/templates/permission-config', query)
}
export async function getDocumentDetail(id: string): Promise<DocumentRecord | null> {
if (useMockData) {
const record = mockRecords.find((item) => item.id === id)
+75 -5
View File
@@ -5,6 +5,8 @@ export type DocumentStatus = 'draft' | 'published' | 'archived' | 'review'
export interface DocumentQuery extends PageQuery {
keyword?: string
status?: DocumentStatus | 'all'
campusId?: string
departmentId?: string
}
export interface DocumentRecord {
@@ -625,22 +627,22 @@ export type ApiPermissionLevel = 'VIEW' | 'USE' | 'MAINTAIN'
export type ApiPermissionEffect = 'ALLOW' | 'DENY'
export interface TemplatePermissionResponseDto {
id?: string
id?: string | null
templateId: string
subjectType: ApiPermissionSubjectType
subjectId: string
subjectId: string | null
permissionLevel: ApiPermissionLevel
effect: ApiPermissionEffect
inherited: boolean
createdAt: string
createdBy?: string
createdAt?: string | null
createdBy?: string | null
}
export interface TemplatePermissionRecord {
id?: string
templateId: string
subjectType: ApiPermissionSubjectType
subjectId: string
subjectId: string | null
subjectName: string
permissionLevel: ApiPermissionLevel
effect: ApiPermissionEffect
@@ -656,6 +658,74 @@ export interface CreateTemplatePermissionRequest {
effect: ApiPermissionEffect
}
export interface TemplatePermissionMatrixQuery extends PageQuery {
keyword?: string
templateId?: string
templateIds?: string[]
roleId?: string
subjectDepartmentId?: string
userId?: string
subjectType?: ApiPermissionSubjectType
campusId?: string
templateDepartmentId?: string
}
export interface TemplatePermissionMatrixItemDto {
templateId: string
permissionId?: string | null
subjectType: ApiPermissionSubjectType
subjectId?: string | null
permissionLevel: ApiPermissionLevel
effect: ApiPermissionEffect
inherited: boolean
createdAt?: string | null
createdBy?: string | null
}
export type TemplatePermissionBatchAction = 'ADD' | 'REVOKE'
export interface TemplatePermissionBatchItemRequest {
templateId: string
action: TemplatePermissionBatchAction
permissionId?: string
subjectType?: ApiPermissionSubjectType
subjectId?: string
permissionLevel?: ApiPermissionLevel
effect?: ApiPermissionEffect
}
export interface TemplatePermissionBatchRequest {
operations: TemplatePermissionBatchItemRequest[]
}
export interface TemplatePermissionBatchResultDto {
index: number
templateId: string
action: TemplatePermissionBatchAction
result: string
permissionId?: string | null
permission?: TemplatePermissionMatrixItemDto | null
}
export interface TemplatePermissionBatchResponseDto {
results?: TemplatePermissionBatchResultDto[] | null
}
export interface TemplatePermissionBatchResult {
index: number
templateId: string
action: TemplatePermissionBatchAction
result: string
permissionId?: string
permission?: TemplatePermissionRecord
}
export interface TemplatePermissionBatchResponse {
results: TemplatePermissionBatchResult[]
}
export type TemplatePermissionMatrixResponse = PageResult<TemplatePermissionRecord>
export interface AuditLogQuery extends PageQuery {
keyword?: string
}
@@ -84,7 +84,11 @@ function deletePermission(permissionId: string | undefined) {
}
}
function formatSubject(subjectType: PermissionSubjectOption['type'], subjectId: string) {
function formatSubject(subjectType: PermissionSubjectOption['type'], subjectId: string | null) {
if (!subjectId) {
return '系统默认继承规则'
}
const subject = props.subjects.find((item) => item.type === subjectType && item.id === subjectId)
return subject?.label ?? subjectId
}
@@ -3,11 +3,11 @@ import { computed, onMounted, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getAllDepartments } from '@/api/management/organization'
import { getDocuments } from '@/api/management/documents'
import { getTemplatePermissionConfig } from '@/api/management/documents'
import {
createTemplatePermission,
deleteTemplatePermission,
batchSaveTemplatePermissions,
getTemplatePermissions,
getTemplatePermissionMatrix,
isPermissionMockEnabled,
} from '@/api/management/document-permissions'
import { getUsers } from '@/api/management/users'
@@ -16,6 +16,7 @@ import type {
DepartmentRecord,
DocumentRecord,
RoleRecord,
TemplatePermissionBatchItemRequest,
TemplatePermissionRecord,
UserRecord,
} from '@/api/management/types'
@@ -146,7 +147,7 @@ function syncPermissionRows() {
}
async function getAllTemplatesForPermission() {
const firstPage = await getDocuments({ page: 1, pageSize: 200, status: 'all' })
const firstPage = await getTemplatePermissionConfig({ page: 1, pageSize: 200, status: 'all' })
const pageCount = Math.ceil(firstPage.total / firstPage.pageSize)
if (pageCount <= 1) {
@@ -155,13 +156,69 @@ async function getAllTemplatesForPermission() {
const remainingPages = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
getDocuments({ page: index + 2, pageSize: firstPage.pageSize, status: 'all' }),
getTemplatePermissionConfig({
page: index + 2,
pageSize: firstPage.pageSize,
status: 'all',
}),
),
)
return [firstPage, ...remainingPages].flatMap((page) => page.records)
}
async function getAllTemplatePermissionMatrix(templateId: string) {
const firstPage = await getTemplatePermissionMatrix({
page: 1,
pageSize: 200,
templateId,
})
const pageCount = Math.ceil(firstPage.total / firstPage.pageSize)
if (pageCount <= 1) {
return firstPage.records
}
const remainingPages = await Promise.all(
Array.from({ length: pageCount - 1 }, (_, index) =>
getTemplatePermissionMatrix({
page: index + 2,
pageSize: firstPage.pageSize,
templateId,
}),
),
)
return [firstPage, ...remainingPages].flatMap((page) => page.records)
}
function mergeTemplatePermissions(
matrixRecords: TemplatePermissionRecord[],
effectiveRecords: TemplatePermissionRecord[],
) {
const seen = new Set<string>()
return [...matrixRecords, ...effectiveRecords.filter((record) => record.inherited)].filter(
(record) => {
const key = [
record.templateId,
record.subjectType,
record.subjectId ?? '',
record.permissionLevel,
record.effect,
record.inherited,
].join('|')
if (seen.has(key)) {
return false
}
seen.add(key)
return true
},
)
}
async function getRolesForPermission(): Promise<{
records: RoleRecord[]
usedFallback: boolean
@@ -281,17 +338,28 @@ async function loadTemplatePermissions() {
permissionError.value = false
try {
const records = await getTemplatePermissions(
templateId,
permissionPreviewUserId.value || undefined,
)
const [matrixResult, effectiveResult] = await Promise.allSettled([
getAllTemplatePermissionMatrix(templateId),
getTemplatePermissions(templateId, permissionPreviewUserId.value || undefined),
])
if (matrixResult.status === 'rejected') {
throw matrixResult.reason
}
if (requestId !== permissionRequestId || templateId !== selectedTemplateId.value) {
return
}
templatePermissions.value = records
templatePermissions.value = mergeTemplatePermissions(
matrixResult.value,
effectiveResult.status === 'fulfilled' ? effectiveResult.value : [],
)
syncPermissionRows()
if (effectiveResult.status === 'rejected') {
ElMessage.warning('用户继承权限预览加载失败,当前仅展示矩阵权限数据')
}
} catch {
if (requestId !== permissionRequestId) {
return
@@ -341,6 +409,7 @@ async function applyTemplatePermission(
permission: PermissionKey,
enabled: boolean,
) {
const operations: TemplatePermissionBatchItemRequest[] = []
const matchingRecords = templatePermissions.value.filter(
(record) =>
record.subjectType === subjectType &&
@@ -362,34 +431,49 @@ async function applyTemplatePermission(
if (!binding.id) {
throw new Error('该直接拒绝权限缺少绑定 ID,无法撤销')
}
await deleteTemplatePermission(templateId, binding.id)
operations.push({
templateId,
action: 'REVOKE',
permissionId: binding.id,
})
}
if (!directAllows.length && !inheritedAllows) {
await createTemplatePermission(templateId, {
operations.push({
templateId,
action: 'ADD',
subjectType,
subjectId,
permissionLevel: permission,
effect: 'ALLOW',
})
}
return
}
for (const binding of directAllows) {
if (!binding.id) {
throw new Error('该直接允许权限缺少绑定 ID,无法撤销')
} else {
for (const binding of directAllows) {
if (!binding.id) {
throw new Error('该直接允许权限缺少绑定 ID,无法撤销')
}
operations.push({
templateId,
action: 'REVOKE',
permissionId: binding.id,
})
}
if (!directDenies.length && inheritedAllows) {
operations.push({
templateId,
action: 'ADD',
subjectType,
subjectId,
permissionLevel: permission,
effect: 'DENY',
})
}
await deleteTemplatePermission(templateId, binding.id)
}
if (!directDenies.length && inheritedAllows) {
await createTemplatePermission(templateId, {
subjectType,
subjectId,
permissionLevel: permission,
effect: 'DENY',
})
if (operations.length) {
await batchSaveTemplatePermissions({ operations })
}
}
@@ -437,7 +521,15 @@ async function addTemplatePermission(form: TemplatePermissionForm) {
permissionSaving.value = true
try {
await createTemplatePermission(selectedTemplateId.value, form)
await batchSaveTemplatePermissions({
operations: [
{
templateId: selectedTemplateId.value,
action: 'ADD',
...form,
},
],
})
await loadTemplatePermissions()
ElMessage.success('模板权限已添加')
} catch (error) {
@@ -455,7 +547,15 @@ async function removeTemplatePermission(permissionId: string) {
permissionSaving.value = true
try {
await deleteTemplatePermission(selectedTemplateId.value, permissionId)
await batchSaveTemplatePermissions({
operations: [
{
templateId: selectedTemplateId.value,
action: 'REVOKE',
permissionId,
},
],
})
await loadTemplatePermissions()
ElMessage.success('模板权限已删除')
} catch (error) {