feat: 接入文档权限批量接口
This commit is contained in:
@@ -161,11 +161,15 @@ API 模块与页面按业务域对应。工作台页面使用 `api/workbench`
|
|||||||
- `POST /api/v1/users/{id}/password/reset`、`POST /api/v1/users/import/preview`、`POST /api/v1/users/import/commit`、`GET /api/v1/users/import/jobs/{jobId}`:管理员重置密码和用户导入流程;
|
- `POST /api/v1/users/{id}/password/reset`、`POST /api/v1/users/import/preview`、`POST /api/v1/users/import/commit`、`GET /api/v1/users/import/jobs/{jobId}`:管理员重置密码和用户导入流程;
|
||||||
- `GET /api/v1/roles/summary`、`GET/PUT /api/v1/roles/{roleId}/permissions`:角色人数统计和角色 API 权限分配;
|
- `GET /api/v1/roles/summary`、`GET/PUT /api/v1/roles/{roleId}/permissions`:角色人数统计和角色 API 权限分配;
|
||||||
- `GET/POST/PUT/DELETE /api/v1/permissions`:API 权限分页查询、详情、新增、修改和停用;
|
- `GET/POST/PUT/DELETE /api/v1/permissions`:API 权限分页查询、详情、新增、修改和停用;
|
||||||
- `GET/POST/DELETE /api/v1/templates/{id}/permissions`:按模板查询和维护文档权限;
|
- `GET /api/v1/templates/permission-config`:按权限配置范围分页查询模板;
|
||||||
|
- `GET /api/v1/templates/permissions/matrix`、`POST /api/v1/templates/permissions/batch`:跨模板权限矩阵查询和批量新增/撤销;
|
||||||
|
- `GET/POST/DELETE /api/v1/templates/{id}/permissions`:单模板权限兼容接口,其中前端使用 GET 补充用户继承预览;
|
||||||
- `GET /api/v1/audit-logs`:全局审计日志查询 API。
|
- `GET /api/v1/audit-logs`:全局审计日志查询 API。
|
||||||
|
|
||||||
用户与权限页面的接口核验结果、当前缺口和后端接口建议见 [`docs/users-permissions-api.md`](./docs/users-permissions-api.md)。
|
用户与权限页面的接口核验结果、当前缺口和后端接口建议见 [`docs/users-permissions-api.md`](./docs/users-permissions-api.md)。
|
||||||
|
|
||||||
|
文档权限页面的新接口契约、继承规则、批量保存行为和联调验收标准见 [`docs/document-permissions-api.md`](./docs/document-permissions-api.md)。
|
||||||
|
|
||||||
报表页真实模式已接入签署报表的 options、overview、tasks 和异步 export-jobs 接口,支持权限范围内的动态筛选、服务端分页、趋势/科室聚合和完整结果导出;Mock 模式仍可用于演示。接口矩阵、时间口径和联调验收见 [reports-api.md](./docs/reports-api.md),后端联调要求见 [reports-backend-requirements.md](./docs/reports-backend-requirements.md)。首页真实模式已接入 workbench overview。
|
报表页真实模式已接入签署报表的 options、overview、tasks 和异步 export-jobs 接口,支持权限范围内的动态筛选、服务端分页、趋势/科室聚合和完整结果导出;Mock 模式仍可用于演示。接口矩阵、时间口径和联调验收见 [reports-api.md](./docs/reports-api.md),后端联调要求见 [reports-backend-requirements.md](./docs/reports-backend-requirements.md)。首页真实模式已接入 workbench overview。
|
||||||
|
|
||||||
首页真实模式已接入 GET /api/v1/workbench/overview;接口口径、字段映射和验收标准见 [docs/workbench-home-api.md](./docs/workbench-home-api.md)。
|
首页真实模式已接入 GET /api/v1/workbench/overview;接口口径、字段映射和验收标准见 [docs/workbench-home-api.md](./docs/workbench-home-api.md)。
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# 文档权限页面接口接入说明
|
# 文档权限页面接口接入说明
|
||||||
|
|
||||||
依据本机后端 `http://127.0.0.1:18080/v3/api-docs`,文档权限页面当前按“文档模板”维度接入权限。页面保留原型的三部分,并补充单用户直接授权:
|
核验时间:2026-09-03。依据后端提交 `b2f9a48` 部署后的 `http://127.0.0.1:18080/v3/api-docs`,文档权限页面已按“文档模板”维度接入新权限配置目录、跨模板矩阵和批量保存接口。页面保留原型的三部分,并补充单用户直接授权:
|
||||||
|
|
||||||
1. **文档库权限矩阵**:说明可见、可用、可维护和继承规则。
|
1. **文档库权限矩阵**:说明可见、可用、可维护和继承规则。
|
||||||
2. **按角色授权**:选择一个文档模板,调整角色的 VIEW、USE、MAINTAIN。
|
2. **按角色授权**:选择一个文档模板,调整角色的 VIEW、USE、MAINTAIN。
|
||||||
@@ -9,17 +9,19 @@
|
|||||||
|
|
||||||
## 已接入接口
|
## 已接入接口
|
||||||
|
|
||||||
| 用途 | 方法 | 路径 |
|
| 用途 | 方法 | 路径 |
|
||||||
| ---------------------- | ------ | --------------------------------------------------------- |
|
| ------------------ | ------ | --------------------------------------------------------- |
|
||||||
| 查询可配置模板 | GET | /api/v1/templates |
|
| 查询权限配置模板 | GET | /api/v1/templates/permission-config |
|
||||||
| 查询角色及人数 | GET | /api/v1/roles/summary |
|
| 查询角色及人数 | GET | /api/v1/roles/summary |
|
||||||
| 查询科室 | GET | /api/v1/departments |
|
| 查询科室 | GET | /api/v1/departments |
|
||||||
| 查询用户 | GET | /api/v1/users |
|
| 查询用户 | GET | /api/v1/users |
|
||||||
| 查询模板权限及继承结果 | GET | /api/v1/templates/{templateId}/permissions |
|
| 查询跨模板权限矩阵 | GET | /api/v1/templates/permissions/matrix |
|
||||||
| 绑定模板权限 | POST | /api/v1/templates/{templateId}/permissions |
|
| 批量新增或撤销权限 | POST | /api/v1/templates/permissions/batch |
|
||||||
| 撤销模板权限 | DELETE | /api/v1/templates/{templateId}/permissions/{permissionId} |
|
| 查询用户继承结果 | GET | /api/v1/templates/{templateId}/permissions |
|
||||||
|
| 旧版单模板新增权限 | POST | /api/v1/templates/{templateId}/permissions |
|
||||||
|
| 旧版单模板撤销权限 | DELETE | /api/v1/templates/{templateId}/permissions/{permissionId} |
|
||||||
|
|
||||||
权限绑定请求使用 OpenAPI 中的 PermissionBindingRequest:
|
单条权限绑定请求使用 OpenAPI 中的 PermissionBindingRequest:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
@@ -30,32 +32,60 @@
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
批量保存请求使用 `TemplatePermissionBatchRequest`:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"operations": [
|
||||||
|
{
|
||||||
|
"templateId": "<template-uuid>",
|
||||||
|
"action": "ADD",
|
||||||
|
"subjectType": "ROLE",
|
||||||
|
"subjectId": "<role-uuid>",
|
||||||
|
"permissionLevel": "USE",
|
||||||
|
"effect": "ALLOW"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"templateId": "<template-uuid>",
|
||||||
|
"action": "REVOKE",
|
||||||
|
"permissionId": "<direct-permission-uuid>"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
成功响应的 `data.results` 与 `operations` 下标对应,ADD 的 `result` 为 `CREATED` 或 `EXISTING`,REVOKE 的 `result` 为 `REVOKED`。
|
||||||
|
|
||||||
权限等级映射:
|
权限等级映射:
|
||||||
|
|
||||||
- VIEW:可浏览模板
|
- VIEW:可浏览模板
|
||||||
- USE:可发起签署
|
- USE:可发起签署
|
||||||
- MAINTAIN:可维护模板
|
- MAINTAIN:可维护模板
|
||||||
|
|
||||||
模板、科室和用户接口都是分页接口,页面按后端返回的 `total` 自动补齐后续页,不会因为接口单页上限 200 而漏掉授权对象。角色优先使用 `/roles/summary` 获取人数;该接口失败时回退 `/roles`,人数按已加载用户的角色关系估算。
|
权限配置目录、权限矩阵、科室和用户接口都是分页接口,页面按后端返回的 `total` 自动补齐后续页,不会因为接口单页上限 200 而漏掉配置模板或授权对象。权限配置目录遵守操作者数据范围;管理员可以查询范围内全部模板,但不能绕过院区、科室和数据范围。角色优先使用 `/roles/summary` 获取人数;该接口失败时回退 `/roles`,人数按已加载用户的角色关系估算。
|
||||||
|
|
||||||
查询模板权限时不传 `userId` 表示查看当前登录用户的继承结果;页面也支持选择用户,将其 UUID 作为 `userId` 重新查询,用于预览该用户的科室签署员默认 `USE` 继承。接口返回的直接绑定和继承结果统一展示,但只有直接绑定带有可删除的 `id`。
|
权限矩阵按当前选择的模板调用 `templateId` 查询,并自动读取所有矩阵分页。矩阵支持 `templateId/templateIds`(最多 100 个)、`roleId`、`subjectDepartmentId`、`userId`、`subjectType`、`campusId` 和 `templateDepartmentId` 筛选;数组参数按重复 query 参数发送。页面使用旧的单模板 GET 接口补充指定用户的有效继承预览,因为矩阵接口中的 `userId` 是授权主体筛选条件,不是继承预览参数。接口返回的直接绑定和继承结果统一展示;继承项的 `permissionId`、`subjectId`、`createdAt` 和 `createdBy` 可为空,页面将其标记为系统默认继承规则且不可删除。
|
||||||
|
|
||||||
## 页面保存行为
|
## 页面保存行为
|
||||||
|
|
||||||
当前契约没有更新权限绑定的 PUT 接口,因此开关按以下规则保存:
|
页面开关和明细表单统一使用 `/templates/permissions/batch` 保存,后端保证整批预校验、事务回滚和幂等:
|
||||||
|
|
||||||
- 开启:删除同主体、同等级的直接 DENY;没有有效允许权限时 POST ALLOW。
|
- 开启:批量 REVOKE 同主体、同等级的直接 DENY;没有有效允许权限时 ADD ALLOW。
|
||||||
- 关闭:撤销直接 ALLOW;如果仍有继承的 ALLOW,POST 直接 DENY 覆盖继承。
|
- 关闭:批量 REVOKE 直接 ALLOW;如果仍有继承的 ALLOW,ADD 直接 DENY 覆盖继承。
|
||||||
|
- 明细新增发送单项 ADD,明细删除发送单项 REVOKE;REVOKE 只携带直接权限绑定 ID。
|
||||||
|
- ADD 返回 `CREATED` 或 `EXISTING` 均视为成功;继承权限不参与撤销。
|
||||||
- 继承 DENY 没有持久化绑定 ID,且契约约定拒绝优先,页面将其显示为不可直接覆盖。
|
- 继承 DENY 没有持久化绑定 ID,且契约约定拒绝优先,页面将其显示为不可直接覆盖。
|
||||||
|
|
||||||
页面不会把继承结果当成可删除的直接绑定,也不会伪造全局权限矩阵数据。权限查询失败时会显示错误态并禁用新增、删除和矩阵调整,避免把空响应误当成“无权限”。真实环境接口没有数据时会显示空态。
|
页面不会把继承结果当成可删除的直接绑定,也不会伪造权限数据。权限矩阵查询失败时会显示错误态并禁用新增、删除和矩阵调整,避免把空响应误当成“无权限”。真实环境接口没有数据时会显示空态。
|
||||||
|
|
||||||
## 当前 OpenAPI 未提供的能力
|
## 当前核验结论
|
||||||
|
|
||||||
如果产品要求“跨全部文档模板”的全局角色/科室权限矩阵,后端还需要提供以下能力之一:
|
本轮新增接口已覆盖当前文档权限页面的后端缺口,暂无阻断前端接入的缺失接口。跨模板矩阵查询最多支持 100 个模板,批量保存最多支持 200 项;页面当前采用“模板选择 + 当前模板矩阵”的交互,没有臆造跨模板对比表。
|
||||||
|
|
||||||
- 全局角色权限查询与批量保存接口;
|
联调验收至少包括:
|
||||||
- 全局科室权限查询与批量保存接口;
|
|
||||||
- 或提供带模板维度的批量权限查询/保存接口,避免前端逐个模板请求。
|
|
||||||
|
|
||||||
另外,当前 GET /api/v1/templates 的语义是查询当前用户有权限查看的模板。若权限管理员需要配置自己当前不可见的模板,请后端提供管理员可见的模板列表语义或专用接口。用户直接授权已使用 `subjectType=USER`,不需要新增专用用户权限接口。
|
- 权限管理员能看到数据范围内、但普通模板 ACL 不可见的配置模板;越过院区、科室或数据范围时返回 403。
|
||||||
|
- Network 中模板目录使用 `/api/v1/templates/permission-config`,矩阵使用 `/api/v1/templates/permissions/matrix` 并正确发送 `page/size/templateId`。
|
||||||
|
- 页面能跨矩阵分页读取直接权限和继承权限;继承空字段显示为默认规则且不提供删除。
|
||||||
|
- 开关操作使用一次批量请求;同时撤销旧绑定并新增覆盖规则时整批成功或整批回滚。
|
||||||
|
- 重复新增返回 `EXISTING`,撤销继承权限被拒绝,审计日志记录批量操作。
|
||||||
|
|||||||
@@ -1,13 +1,22 @@
|
|||||||
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
import { unwrapApiResponse, unwrapNullableApiResponse } from '@/utils/api-response'
|
||||||
import { request } from '@/utils/request'
|
import { request } from '@/utils/request'
|
||||||
import type { ApiResponse } from '@/types/common'
|
import type { ApiResponse, PageResult } from '@/types/common'
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
BackendCollection,
|
BackendCollection,
|
||||||
|
BackendPage,
|
||||||
CreateTemplatePermissionRequest,
|
CreateTemplatePermissionRequest,
|
||||||
PermissionListResponse,
|
PermissionListResponse,
|
||||||
PermissionQuery,
|
PermissionQuery,
|
||||||
PermissionRecord,
|
PermissionRecord,
|
||||||
|
TemplatePermissionBatchRequest,
|
||||||
|
TemplatePermissionBatchResponse,
|
||||||
|
TemplatePermissionBatchResponseDto,
|
||||||
|
TemplatePermissionBatchResult,
|
||||||
|
TemplatePermissionBatchResultDto,
|
||||||
|
TemplatePermissionMatrixItemDto,
|
||||||
|
TemplatePermissionMatrixQuery,
|
||||||
|
TemplatePermissionMatrixResponse,
|
||||||
TemplatePermissionRecord,
|
TemplatePermissionRecord,
|
||||||
TemplatePermissionResponseDto,
|
TemplatePermissionResponseDto,
|
||||||
} from './types'
|
} from './types'
|
||||||
@@ -85,17 +94,96 @@ function formatDateTime(value: string | null | undefined) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function mapTemplatePermission(dto: TemplatePermissionResponseDto): TemplatePermissionRecord {
|
function mapTemplatePermission(dto: TemplatePermissionResponseDto): TemplatePermissionRecord {
|
||||||
|
const subjectId = dto.subjectId ?? null
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id: dto.id,
|
id: dto.id ?? undefined,
|
||||||
templateId: dto.templateId,
|
templateId: dto.templateId,
|
||||||
subjectType: dto.subjectType,
|
subjectType: dto.subjectType,
|
||||||
subjectId: dto.subjectId,
|
subjectId,
|
||||||
subjectName: dto.subjectId,
|
subjectName: subjectId ?? '系统默认继承规则',
|
||||||
permissionLevel: dto.permissionLevel,
|
permissionLevel: dto.permissionLevel,
|
||||||
effect: dto.effect,
|
effect: dto.effect,
|
||||||
inherited: dto.inherited,
|
inherited: dto.inherited,
|
||||||
createdAt: formatDateTime(dto.createdAt),
|
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)
|
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(
|
export async function createTemplatePermission(
|
||||||
templateId: string,
|
templateId: string,
|
||||||
payload: CreateTemplatePermissionRequest,
|
payload: CreateTemplatePermissionRequest,
|
||||||
|
|||||||
@@ -138,23 +138,35 @@ function toBackendQuery(query: DocumentQuery) {
|
|||||||
params.status = statusMap[query.status]
|
params.status = statusMap[query.status]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (query.campusId) {
|
||||||
|
params.campusId = query.campusId
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.departmentId) {
|
||||||
|
params.departmentId = query.departmentId
|
||||||
|
}
|
||||||
|
|
||||||
return params
|
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> {
|
export function getDocuments(query: DocumentQuery): Promise<DocumentListResponse> {
|
||||||
if (!useMockData) {
|
if (!useMockData) {
|
||||||
return request
|
return getRemoteDocuments('/v1/templates', query)
|
||||||
.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),
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const keyword = query.keyword?.trim().toLowerCase()
|
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> {
|
export async function getDocumentDetail(id: string): Promise<DocumentRecord | null> {
|
||||||
if (useMockData) {
|
if (useMockData) {
|
||||||
const record = mockRecords.find((item) => item.id === id)
|
const record = mockRecords.find((item) => item.id === id)
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ export type DocumentStatus = 'draft' | 'published' | 'archived' | 'review'
|
|||||||
export interface DocumentQuery extends PageQuery {
|
export interface DocumentQuery extends PageQuery {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
status?: DocumentStatus | 'all'
|
status?: DocumentStatus | 'all'
|
||||||
|
campusId?: string
|
||||||
|
departmentId?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DocumentRecord {
|
export interface DocumentRecord {
|
||||||
@@ -625,22 +627,22 @@ export type ApiPermissionLevel = 'VIEW' | 'USE' | 'MAINTAIN'
|
|||||||
export type ApiPermissionEffect = 'ALLOW' | 'DENY'
|
export type ApiPermissionEffect = 'ALLOW' | 'DENY'
|
||||||
|
|
||||||
export interface TemplatePermissionResponseDto {
|
export interface TemplatePermissionResponseDto {
|
||||||
id?: string
|
id?: string | null
|
||||||
templateId: string
|
templateId: string
|
||||||
subjectType: ApiPermissionSubjectType
|
subjectType: ApiPermissionSubjectType
|
||||||
subjectId: string
|
subjectId: string | null
|
||||||
permissionLevel: ApiPermissionLevel
|
permissionLevel: ApiPermissionLevel
|
||||||
effect: ApiPermissionEffect
|
effect: ApiPermissionEffect
|
||||||
inherited: boolean
|
inherited: boolean
|
||||||
createdAt: string
|
createdAt?: string | null
|
||||||
createdBy?: string
|
createdBy?: string | null
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TemplatePermissionRecord {
|
export interface TemplatePermissionRecord {
|
||||||
id?: string
|
id?: string
|
||||||
templateId: string
|
templateId: string
|
||||||
subjectType: ApiPermissionSubjectType
|
subjectType: ApiPermissionSubjectType
|
||||||
subjectId: string
|
subjectId: string | null
|
||||||
subjectName: string
|
subjectName: string
|
||||||
permissionLevel: ApiPermissionLevel
|
permissionLevel: ApiPermissionLevel
|
||||||
effect: ApiPermissionEffect
|
effect: ApiPermissionEffect
|
||||||
@@ -656,6 +658,74 @@ export interface CreateTemplatePermissionRequest {
|
|||||||
effect: ApiPermissionEffect
|
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 {
|
export interface AuditLogQuery extends PageQuery {
|
||||||
keyword?: string
|
keyword?: string
|
||||||
}
|
}
|
||||||
|
|||||||
+5
-1
@@ -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)
|
const subject = props.subjects.find((item) => item.type === subjectType && item.id === subjectId)
|
||||||
return subject?.label ?? subjectId
|
return subject?.label ?? subjectId
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,11 @@ import { computed, onMounted, ref } from 'vue'
|
|||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
|
|
||||||
import { getAllDepartments } from '@/api/management/organization'
|
import { getAllDepartments } from '@/api/management/organization'
|
||||||
import { getDocuments } from '@/api/management/documents'
|
import { getTemplatePermissionConfig } from '@/api/management/documents'
|
||||||
import {
|
import {
|
||||||
createTemplatePermission,
|
batchSaveTemplatePermissions,
|
||||||
deleteTemplatePermission,
|
|
||||||
getTemplatePermissions,
|
getTemplatePermissions,
|
||||||
|
getTemplatePermissionMatrix,
|
||||||
isPermissionMockEnabled,
|
isPermissionMockEnabled,
|
||||||
} from '@/api/management/document-permissions'
|
} from '@/api/management/document-permissions'
|
||||||
import { getUsers } from '@/api/management/users'
|
import { getUsers } from '@/api/management/users'
|
||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
DepartmentRecord,
|
DepartmentRecord,
|
||||||
DocumentRecord,
|
DocumentRecord,
|
||||||
RoleRecord,
|
RoleRecord,
|
||||||
|
TemplatePermissionBatchItemRequest,
|
||||||
TemplatePermissionRecord,
|
TemplatePermissionRecord,
|
||||||
UserRecord,
|
UserRecord,
|
||||||
} from '@/api/management/types'
|
} from '@/api/management/types'
|
||||||
@@ -146,7 +147,7 @@ function syncPermissionRows() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function getAllTemplatesForPermission() {
|
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)
|
const pageCount = Math.ceil(firstPage.total / firstPage.pageSize)
|
||||||
|
|
||||||
if (pageCount <= 1) {
|
if (pageCount <= 1) {
|
||||||
@@ -155,13 +156,69 @@ async function getAllTemplatesForPermission() {
|
|||||||
|
|
||||||
const remainingPages = await Promise.all(
|
const remainingPages = await Promise.all(
|
||||||
Array.from({ length: pageCount - 1 }, (_, index) =>
|
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)
|
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<{
|
async function getRolesForPermission(): Promise<{
|
||||||
records: RoleRecord[]
|
records: RoleRecord[]
|
||||||
usedFallback: boolean
|
usedFallback: boolean
|
||||||
@@ -281,17 +338,28 @@ async function loadTemplatePermissions() {
|
|||||||
permissionError.value = false
|
permissionError.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const records = await getTemplatePermissions(
|
const [matrixResult, effectiveResult] = await Promise.allSettled([
|
||||||
templateId,
|
getAllTemplatePermissionMatrix(templateId),
|
||||||
permissionPreviewUserId.value || undefined,
|
getTemplatePermissions(templateId, permissionPreviewUserId.value || undefined),
|
||||||
)
|
])
|
||||||
|
|
||||||
|
if (matrixResult.status === 'rejected') {
|
||||||
|
throw matrixResult.reason
|
||||||
|
}
|
||||||
|
|
||||||
if (requestId !== permissionRequestId || templateId !== selectedTemplateId.value) {
|
if (requestId !== permissionRequestId || templateId !== selectedTemplateId.value) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
templatePermissions.value = records
|
templatePermissions.value = mergeTemplatePermissions(
|
||||||
|
matrixResult.value,
|
||||||
|
effectiveResult.status === 'fulfilled' ? effectiveResult.value : [],
|
||||||
|
)
|
||||||
syncPermissionRows()
|
syncPermissionRows()
|
||||||
|
|
||||||
|
if (effectiveResult.status === 'rejected') {
|
||||||
|
ElMessage.warning('用户继承权限预览加载失败,当前仅展示矩阵权限数据')
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
if (requestId !== permissionRequestId) {
|
if (requestId !== permissionRequestId) {
|
||||||
return
|
return
|
||||||
@@ -341,6 +409,7 @@ async function applyTemplatePermission(
|
|||||||
permission: PermissionKey,
|
permission: PermissionKey,
|
||||||
enabled: boolean,
|
enabled: boolean,
|
||||||
) {
|
) {
|
||||||
|
const operations: TemplatePermissionBatchItemRequest[] = []
|
||||||
const matchingRecords = templatePermissions.value.filter(
|
const matchingRecords = templatePermissions.value.filter(
|
||||||
(record) =>
|
(record) =>
|
||||||
record.subjectType === subjectType &&
|
record.subjectType === subjectType &&
|
||||||
@@ -362,34 +431,49 @@ async function applyTemplatePermission(
|
|||||||
if (!binding.id) {
|
if (!binding.id) {
|
||||||
throw new Error('该直接拒绝权限缺少绑定 ID,无法撤销')
|
throw new Error('该直接拒绝权限缺少绑定 ID,无法撤销')
|
||||||
}
|
}
|
||||||
await deleteTemplatePermission(templateId, binding.id)
|
operations.push({
|
||||||
|
templateId,
|
||||||
|
action: 'REVOKE',
|
||||||
|
permissionId: binding.id,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!directAllows.length && !inheritedAllows) {
|
if (!directAllows.length && !inheritedAllows) {
|
||||||
await createTemplatePermission(templateId, {
|
operations.push({
|
||||||
|
templateId,
|
||||||
|
action: 'ADD',
|
||||||
subjectType,
|
subjectType,
|
||||||
subjectId,
|
subjectId,
|
||||||
permissionLevel: permission,
|
permissionLevel: permission,
|
||||||
effect: 'ALLOW',
|
effect: 'ALLOW',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return
|
} else {
|
||||||
}
|
for (const binding of directAllows) {
|
||||||
|
if (!binding.id) {
|
||||||
for (const binding of directAllows) {
|
throw new Error('该直接允许权限缺少绑定 ID,无法撤销')
|
||||||
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) {
|
if (operations.length) {
|
||||||
await createTemplatePermission(templateId, {
|
await batchSaveTemplatePermissions({ operations })
|
||||||
subjectType,
|
|
||||||
subjectId,
|
|
||||||
permissionLevel: permission,
|
|
||||||
effect: 'DENY',
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -437,7 +521,15 @@ async function addTemplatePermission(form: TemplatePermissionForm) {
|
|||||||
permissionSaving.value = true
|
permissionSaving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await createTemplatePermission(selectedTemplateId.value, form)
|
await batchSaveTemplatePermissions({
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
templateId: selectedTemplateId.value,
|
||||||
|
action: 'ADD',
|
||||||
|
...form,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
await loadTemplatePermissions()
|
await loadTemplatePermissions()
|
||||||
ElMessage.success('模板权限已添加')
|
ElMessage.success('模板权限已添加')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -455,7 +547,15 @@ async function removeTemplatePermission(permissionId: string) {
|
|||||||
permissionSaving.value = true
|
permissionSaving.value = true
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await deleteTemplatePermission(selectedTemplateId.value, permissionId)
|
await batchSaveTemplatePermissions({
|
||||||
|
operations: [
|
||||||
|
{
|
||||||
|
templateId: selectedTemplateId.value,
|
||||||
|
action: 'REVOKE',
|
||||||
|
permissionId,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
await loadTemplatePermissions()
|
await loadTemplatePermissions()
|
||||||
ElMessage.success('模板权限已删除')
|
ElMessage.success('模板权限已删除')
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
Reference in New Issue
Block a user