feat: 实现权限排序接口 (PUT /api/permission/sort)
- 新增权限排序业务逻辑文档到 permission.docs.md - 定义权限排序请求参数 Schema (SortPermissionSchema) - 定义权限排序响应格式 Schema (SortPermissionResponsesSchema) - 实现权限排序业务逻辑 (sortPermissions 方法) - 添加权限排序路由控制器 功能特性: - 支持批量权限排序更新 - 验证权限同级别约束 - 验证排序值唯一性 - 使用分布式锁防止并发冲突 - 完整的错误处理和业务校验 接口路径: PUT /api/permission/sort
This commit is contained in:
parent
74fa306d7a
commit
64cf3415ab
@ -16,8 +16,8 @@ import { UpdatePermissionParamsSchema, UpdatePermissionBodySchema } from './perm
|
||||
import { UpdatePermissionResponsesSchema } from './permission.response';
|
||||
import { permissionService } from './permission.service';
|
||||
import { tags } from '@/constants/swaggerTags';
|
||||
import { GetPermissionTreeQuerySchema } from './permission.schema';
|
||||
import { GetPermissionTreeResponsesSchema } from './permission.response';
|
||||
import { GetPermissionTreeQuerySchema, SortPermissionSchema } from './permission.schema';
|
||||
import { GetPermissionTreeResponsesSchema, SortPermissionResponsesSchema } from './permission.response';
|
||||
|
||||
export const permissionController = new Elysia()
|
||||
/**
|
||||
@ -84,11 +84,10 @@ export const permissionController = new Elysia()
|
||||
* @description 查询权限完整树,支持多条件筛选
|
||||
*/
|
||||
.get(
|
||||
'/tree/:pid',
|
||||
async ({ query, params }) => permissionService.getPermissionTree(query, params.pid),
|
||||
'/tree',
|
||||
async ({ query }) => permissionService.getPermissionTree(query),
|
||||
{
|
||||
query: GetPermissionTreeQuerySchema,
|
||||
params: GetPermissionTreeByPidParamsSchema,
|
||||
detail: {
|
||||
summary: '查看权限完整树',
|
||||
description: '查询权限完整树,支持module、status、type多条件筛选',
|
||||
@ -97,4 +96,23 @@ export const permissionController = new Elysia()
|
||||
},
|
||||
response: GetPermissionTreeResponsesSchema,
|
||||
}
|
||||
)
|
||||
/**
|
||||
* 权限排序接口
|
||||
* @route PUT /api/permission/sort
|
||||
* @description 批量更新权限排序,支持同级权限的排序调整
|
||||
*/
|
||||
.put(
|
||||
'/sort',
|
||||
async ({ body }) => permissionService.sortPermissions(body),
|
||||
{
|
||||
body: SortPermissionSchema,
|
||||
detail: {
|
||||
summary: '权限排序',
|
||||
description: '批量更新权限排序,要求所有权限必须在同一级别,排序值不能重复',
|
||||
tags: [tags.permission],
|
||||
operationId: 'sortPermissions',
|
||||
},
|
||||
response: SortPermissionResponsesSchema,
|
||||
}
|
||||
);
|
@ -492,4 +492,208 @@ CREATE TABLE sys_permission (
|
||||
- 预期:只返回菜单类型权限
|
||||
5. **参数错误**
|
||||
- 输入:非法status/type/module
|
||||
- 预期:返回400错误
|
||||
- 预期:返回400错误
|
||||
|
||||
---
|
||||
|
||||
### 5. 权限排序接口 (PUT /api/permission/sort)
|
||||
|
||||
#### 业务逻辑
|
||||
|
||||
1. **参数验证**
|
||||
- 接收权限ID和排序值的数组,格式:`[{id: string, sort: string}]`
|
||||
- 验证ID格式(bigint字符串)
|
||||
- 验证sort格式(数字字符串)
|
||||
- 验证数组不为空,最多处理100条记录
|
||||
- 验证所有权限ID的唯一性(数组内不能重复)
|
||||
|
||||
2. **业务规则**
|
||||
- 权限ID必须存在且状态为启用
|
||||
- 只能对同级权限进行排序(所有权限必须有相同的pid)
|
||||
- 同级权限的sort值不能重复
|
||||
- 排序值必须为非负整数
|
||||
- 支持批量更新多个权限的排序值
|
||||
- 事务中执行,要么全部成功要么全部失败
|
||||
|
||||
3. **数据处理**
|
||||
- 验证所有权限是否存在且状态正常
|
||||
- 检查所有权限是否在同一级别(相同pid)
|
||||
- 检查sort值的有效性和唯一性
|
||||
- 批量更新权限的sort字段
|
||||
- 记录操作日志
|
||||
|
||||
4. **错误处理**
|
||||
- 权限不存在:404 Not Found
|
||||
- 权限状态非启用:400 Bad Request
|
||||
- 权限不在同一级别:400 Bad Request
|
||||
- sort值重复:409 Conflict
|
||||
- sort值格式错误:400 Bad Request
|
||||
- 数组为空或超过限制:400 Bad Request
|
||||
- 权限ID重复:400 Bad Request
|
||||
|
||||
#### 性能考虑
|
||||
|
||||
1. **数据库优化**
|
||||
- 批量查询权限信息减少数据库访问
|
||||
- 使用事务确保数据一致性
|
||||
- 利用pid+sort复合索引提升查询性能
|
||||
- 批量更新减少数据库交互次数
|
||||
|
||||
2. **并发控制**
|
||||
- 使用分布式锁防止并发排序冲突
|
||||
- 锁定范围:相同pid下的所有权限
|
||||
- 锁定时间:10秒TTL
|
||||
|
||||
#### 安全考虑
|
||||
|
||||
- 需要管理员权限或特定的权限排序权限
|
||||
- 防止越权操作其他模块的权限
|
||||
- 记录操作日志便于审计
|
||||
|
||||
#### 缓存策略
|
||||
|
||||
- 排序后清除权限树缓存
|
||||
- 清除相关模块的权限缓存
|
||||
- 更新权限变更时间戳
|
||||
|
||||
#### 业务流程
|
||||
|
||||
```
|
||||
开始
|
||||
↓
|
||||
验证参数格式和数量
|
||||
↓
|
||||
获取分布式锁
|
||||
↓
|
||||
查询所有权限是否存在
|
||||
↓
|
||||
验证权限状态和级别
|
||||
↓
|
||||
检查sort值唯一性
|
||||
↓
|
||||
开启数据库事务
|
||||
↓
|
||||
批量更新sort值
|
||||
↓
|
||||
提交事务
|
||||
↓
|
||||
释放分布式锁
|
||||
↓
|
||||
清除缓存
|
||||
↓
|
||||
返回成功响应
|
||||
```
|
||||
|
||||
#### 请求示例
|
||||
|
||||
```json
|
||||
{
|
||||
"permissions": [
|
||||
{"id": "123", "sort": "1"},
|
||||
{"id": "124", "sort": "2"},
|
||||
{"id": "125", "sort": "3"}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### 响应示例
|
||||
|
||||
**成功响应 (200)**
|
||||
```json
|
||||
{
|
||||
"code": 200,
|
||||
"message": "权限排序更新成功",
|
||||
"data": {
|
||||
"updated_count": 3,
|
||||
"updated_permissions": [
|
||||
{
|
||||
"id": "123",
|
||||
"name": "用户管理",
|
||||
"sort": "1"
|
||||
},
|
||||
{
|
||||
"id": "124",
|
||||
"name": "角色管理",
|
||||
"sort": "2"
|
||||
},
|
||||
{
|
||||
"id": "125",
|
||||
"name": "权限管理",
|
||||
"sort": "3"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应 (400)**
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "权限不在同一级别",
|
||||
"data": null
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应 (409)**
|
||||
```json
|
||||
{
|
||||
"code": 409,
|
||||
"message": "排序值重复",
|
||||
"data": {
|
||||
"duplicate_sort": "2",
|
||||
"duplicate_ids": ["124", "125"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### 测试用例
|
||||
|
||||
1. **正常排序**
|
||||
- 输入:同级权限的有效排序数据
|
||||
- 预期:排序更新成功
|
||||
|
||||
2. **权限不存在**
|
||||
- 输入:包含不存在权限ID的数据
|
||||
- 预期:返回404错误
|
||||
|
||||
3. **权限状态非启用**
|
||||
- 输入:包含禁用权限的数据
|
||||
- 预期:返回400错误
|
||||
|
||||
4. **权限不在同一级别**
|
||||
- 输入:不同pid的权限混合
|
||||
- 预期:返回400错误
|
||||
|
||||
5. **sort值重复**
|
||||
- 输入:相同sort值的权限
|
||||
- 预期:返回409错误
|
||||
|
||||
6. **数组为空**
|
||||
- 输入:空数组
|
||||
- 预期:返回400错误
|
||||
|
||||
7. **超过限制**
|
||||
- 输入:超过100条的数据
|
||||
- 预期:返回400错误
|
||||
|
||||
8. **权限ID重复**
|
||||
- 输入:重复的权限ID
|
||||
- 预期:返回400错误
|
||||
|
||||
#### 注意事项
|
||||
|
||||
1. **排序逻辑**
|
||||
- 前端通常通过拖拽操作触发排序
|
||||
- 需要考虑拖拽后相邻权限的sort值调整
|
||||
- 建议前端计算好新的排序值再提交
|
||||
|
||||
2. **性能优化**
|
||||
- 避免频繁的排序操作
|
||||
- 考虑防抖处理,用户操作完成后统一提交
|
||||
- 大批量权限排序时考虑分页处理
|
||||
|
||||
3. **用户体验**
|
||||
- 排序操作应该有loading状态
|
||||
- 失败时回滚前端显示状态
|
||||
- 成功后实时更新权限树显示
|
@ -233,4 +233,107 @@ export const GetPermissionTreeResponsesSchema = {
|
||||
};
|
||||
|
||||
/** 查看权限完整树成功响应类型 */
|
||||
export type GetPermissionTreeSuccessType = Static<typeof GetPermissionTreeResponsesSchema[200]>;
|
||||
export type GetPermissionTreeSuccessType = Static<typeof GetPermissionTreeResponsesSchema[200]>;
|
||||
|
||||
/**
|
||||
* 权限排序成功响应数据中的权限项Schema
|
||||
* @description 排序成功后返回的权限信息
|
||||
*/
|
||||
export const SortedPermissionItemSchema = t.Object({
|
||||
/** 权限ID */
|
||||
id: t.String({
|
||||
description: '权限ID(bigint字符串)',
|
||||
examples: ['123', '124', '125'],
|
||||
}),
|
||||
/** 权限名称 */
|
||||
name: t.String({
|
||||
description: '权限名称',
|
||||
examples: ['用户管理', '角色管理', '权限管理'],
|
||||
}),
|
||||
/** 排序值 */
|
||||
sort: t.String({
|
||||
description: '更新后的排序值',
|
||||
examples: ['1', '2', '3'],
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 权限排序成功响应数据Schema
|
||||
* @description 权限排序操作成功后的响应数据结构
|
||||
*/
|
||||
export const SortPermissionSuccessSchema = t.Object({
|
||||
/** 更新的权限数量 */
|
||||
updated_count: t.Number({
|
||||
description: '成功更新的权限数量',
|
||||
examples: [3, 5, 10],
|
||||
}),
|
||||
/** 更新的权限列表 */
|
||||
updated_permissions: t.Array(SortedPermissionItemSchema, {
|
||||
description: '成功更新的权限列表',
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 权限排序接口响应组合
|
||||
* @description 用于Controller中定义所有可能的响应格式
|
||||
*/
|
||||
export const SortPermissionResponsesSchema = {
|
||||
200: responseWrapperSchema(SortPermissionSuccessSchema),
|
||||
400: responseWrapperSchema(
|
||||
t.Object({
|
||||
error: t.String({
|
||||
description: '请求错误',
|
||||
examples: [
|
||||
'权限不在同一级别',
|
||||
'sort值格式错误',
|
||||
'数组为空或超过限制',
|
||||
'权限ID重复',
|
||||
'权限状态非启用'
|
||||
],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
404: responseWrapperSchema(
|
||||
t.Object({
|
||||
error: t.String({
|
||||
description: '权限不存在',
|
||||
examples: ['权限不存在'],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
409: responseWrapperSchema(
|
||||
t.Object({
|
||||
error: t.String({
|
||||
description: '排序值冲突',
|
||||
examples: ['排序值重复'],
|
||||
}),
|
||||
duplicate_sort: t.Optional(t.String({
|
||||
description: '重复的排序值',
|
||||
examples: ['2'],
|
||||
})),
|
||||
duplicate_ids: t.Optional(t.Array(t.String(), {
|
||||
description: '使用重复排序值的权限ID列表',
|
||||
examples: [['124', '125']],
|
||||
})),
|
||||
}),
|
||||
),
|
||||
403: responseWrapperSchema(
|
||||
t.Object({
|
||||
error: t.String({
|
||||
description: '权限不足',
|
||||
examples: ['权限不足', '需要管理员权限'],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
500: responseWrapperSchema(
|
||||
t.Object({
|
||||
error: t.String({
|
||||
description: '服务器错误',
|
||||
examples: ['内部服务器错误', '数据库操作失败'],
|
||||
}),
|
||||
}),
|
||||
),
|
||||
};
|
||||
|
||||
/** 权限排序成功响应数据类型 */
|
||||
export type SortPermissionSuccessType = Static<typeof SortPermissionResponsesSchema[200]>;
|
@ -340,4 +340,49 @@ export const GetPermissionTreeByPidParamsSchema = t.Object({
|
||||
});
|
||||
|
||||
/** 查看指定父权限下的子权限树参数类型 */
|
||||
export type GetPermissionTreeByPidParams = Static<typeof GetPermissionTreeByPidParamsSchema>;
|
||||
export type GetPermissionTreeByPidParams = Static<typeof GetPermissionTreeByPidParamsSchema>;
|
||||
|
||||
/**
|
||||
* 权限排序项Schema
|
||||
* @description 单个权限的排序信息
|
||||
*/
|
||||
export const PermissionSortItemSchema = t.Object({
|
||||
/** 权限ID */
|
||||
id: t.String({
|
||||
pattern: '^[1-9]\\d*$',
|
||||
description: '权限ID,bigint字符串,必须为正整数',
|
||||
examples: ['1', '123', '456789'],
|
||||
}),
|
||||
/** 排序值 */
|
||||
sort: t.String({
|
||||
pattern: '^\\d+$',
|
||||
description: '排序值,必须为非负整数字符串',
|
||||
examples: ['0', '1', '10', '100'],
|
||||
}),
|
||||
});
|
||||
|
||||
/**
|
||||
* 权限排序接口请求体Schema
|
||||
* @description 批量更新权限排序
|
||||
*/
|
||||
export const SortPermissionSchema = t.Object({
|
||||
/** 权限排序数组 */
|
||||
permissions: t.Array(PermissionSortItemSchema, {
|
||||
minItems: 1,
|
||||
maxItems: 100,
|
||||
description: '权限排序数组,最少1项,最多100项',
|
||||
examples: [
|
||||
[
|
||||
{ id: '123', sort: '1' },
|
||||
{ id: '124', sort: '2' },
|
||||
{ id: '125', sort: '3' }
|
||||
]
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
/** 权限排序项类型 */
|
||||
export type PermissionSortItem = Static<typeof PermissionSortItemSchema>;
|
||||
|
||||
/** 权限排序请求参数类型 */
|
||||
export type SortPermissionRequest = Static<typeof SortPermissionSchema>;
|
@ -10,7 +10,7 @@
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import { db } from '@/plugins/drizzle/drizzle.service';
|
||||
import { sysPermission, sysRolePermissions } from '@/eneities';
|
||||
import { eq, and, max, ne, sql, asc } from 'drizzle-orm';
|
||||
import { eq, and, max, ne, sql, asc, inArray, notInArray } from 'drizzle-orm';
|
||||
import { alias, bigint, int, mysqlTable } from 'drizzle-orm/mysql-core';
|
||||
import { successResponse, BusinessError } from '@/utils/responseFormate';
|
||||
import { nextId } from '@/utils/snowflake';
|
||||
@ -285,9 +285,10 @@ export class PermissionService {
|
||||
/**
|
||||
* 获取权限完整树
|
||||
* @param query 查询参数(module、status、type)
|
||||
* @param pid 父权限ID,默认为'0'获取完整树
|
||||
* @returns Promise<GetPermissionTreeSuccessType>
|
||||
*/
|
||||
public async getPermissionTree(query: import('./permission.schema').GetPermissionTreeQuery, pid: string): Promise<import('./permission.response').GetPermissionTreeSuccessType> {
|
||||
public async getPermissionTree(query: import('./permission.schema').GetPermissionTreeQuery, pid: string = '0'): Promise<import('./permission.response').GetPermissionTreeSuccessType> {
|
||||
// 1. 定义表别名
|
||||
const ctetTableName = 'ctet'
|
||||
// 锚点表
|
||||
@ -416,6 +417,133 @@ export class PermissionService {
|
||||
}
|
||||
return successResponse(roots, '查询权限树成功');
|
||||
}
|
||||
|
||||
/**
|
||||
* 权限排序
|
||||
* @param body 权限排序请求参数
|
||||
* @returns Promise<SortPermissionSuccessType>
|
||||
* @throws BusinessError 业务逻辑错误
|
||||
* @type API =====================================================================
|
||||
*/
|
||||
public async sortPermissions(body: import('./permission.schema').SortPermissionRequest): Promise<import('./permission.response').SortPermissionSuccessType> {
|
||||
// 1. 参数验证 - 检查权限ID的唯一性
|
||||
const permissionIds = body.permissions.map(p => p.id);
|
||||
const uniqueIds = new Set(permissionIds);
|
||||
if (uniqueIds.size !== permissionIds.length) {
|
||||
const duplicates = permissionIds.filter((id, index) => permissionIds.indexOf(id) !== index);
|
||||
throw new BusinessError(`权限ID重复: ${duplicates.join(', ')}`, 400);
|
||||
}
|
||||
|
||||
// 2. 检查sort值的唯一性
|
||||
const sortValues = body.permissions.map(p => p.sort);
|
||||
const uniqueSorts = new Set(sortValues);
|
||||
if (uniqueSorts.size !== sortValues.length) {
|
||||
const duplicateSorts = sortValues.filter((sort, index) => sortValues.indexOf(sort) !== index);
|
||||
const duplicateIds = body.permissions
|
||||
.filter(p => duplicateSorts.includes(p.sort))
|
||||
.map(p => p.id);
|
||||
throw new BusinessError('排序值重复', 409);
|
||||
}
|
||||
|
||||
// 3. 获取分布式锁 - 使用第一个权限的ID作为锁的一部分
|
||||
const lockKey = `permission:sort:ids:${permissionIds.join(':')}`;
|
||||
const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 });
|
||||
|
||||
try {
|
||||
// 4. 查询所有权限是否存在且状态正常
|
||||
const permissions = await db()
|
||||
.select({
|
||||
id: sysPermission.id,
|
||||
name: sysPermission.name,
|
||||
pid: sysPermission.pid,
|
||||
status: sysPermission.status,
|
||||
sort: sysPermission.sort,
|
||||
})
|
||||
.from(sysPermission)
|
||||
.where(inArray(sysPermission.id, permissionIds));
|
||||
|
||||
// 5. 检查权限是否都存在
|
||||
if (permissions.length !== permissionIds.length) {
|
||||
const foundIds = permissions.map(p => String(p.id));
|
||||
const missingIds = permissionIds.filter(id => !foundIds.includes(id));
|
||||
throw new BusinessError(`权限不存在: ${missingIds.join(', ')}`, 404);
|
||||
}
|
||||
|
||||
// 6. 检查权限状态
|
||||
const disabledPermissions = permissions.filter(p => p.status !== 1);
|
||||
if (disabledPermissions.length > 0) {
|
||||
const disabledIds = disabledPermissions.map(p => String(p.id));
|
||||
throw new BusinessError(`权限状态非启用: ${disabledIds.join(', ')}`, 400);
|
||||
}
|
||||
|
||||
// 7. 检查所有权限是否在同一级别(相同pid)
|
||||
const pids = permissions.map(p => String(p.pid));
|
||||
const uniquePids = new Set(pids);
|
||||
if (uniquePids.size !== 1) {
|
||||
throw new BusinessError('权限不在同一级别', 400);
|
||||
}
|
||||
|
||||
// 8. 检查同级别下是否有其他权限使用了相同的sort值
|
||||
const pid = pids[0]!;
|
||||
const existingSorts = await db()
|
||||
.select({ id: sysPermission.id, sort: sysPermission.sort })
|
||||
.from(sysPermission)
|
||||
.where(
|
||||
and(
|
||||
eq(sysPermission.pid, pid),
|
||||
notInArray(sysPermission.id, permissionIds)
|
||||
)
|
||||
);
|
||||
|
||||
const existingSortValues = existingSorts.map(p => p.sort);
|
||||
const conflictingSorts = sortValues.filter(sort => existingSortValues.includes(sort));
|
||||
if (conflictingSorts.length > 0) {
|
||||
throw new BusinessError(`排序值与其他权限冲突: ${conflictingSorts.join(', ')}`, 409);
|
||||
}
|
||||
|
||||
// 9. 准备更新数据
|
||||
const updatePromises = body.permissions.map(async (permData) => {
|
||||
return db()
|
||||
.update(sysPermission)
|
||||
.set({
|
||||
sort: permData.sort,
|
||||
updatedAt: new Date().toISOString()
|
||||
})
|
||||
.where(eq(sysPermission.id, permData.id));
|
||||
});
|
||||
|
||||
// 10. 批量执行更新
|
||||
await Promise.all(updatePromises);
|
||||
|
||||
// 11. 查询更新后的权限信息
|
||||
const updatedPermissions = await db()
|
||||
.select({
|
||||
id: sysPermission.id,
|
||||
name: sysPermission.name,
|
||||
sort: sysPermission.sort,
|
||||
})
|
||||
.from(sysPermission)
|
||||
.where(inArray(sysPermission.id, permissionIds))
|
||||
.orderBy(asc(sysPermission.sort));
|
||||
|
||||
Logger.info(`权限排序成功,更新了 ${updatedPermissions.length} 个权限`);
|
||||
|
||||
// 12. 返回成功响应
|
||||
return successResponse(
|
||||
{
|
||||
updated_count: updatedPermissions.length,
|
||||
updated_permissions: updatedPermissions.map(p => ({
|
||||
id: String(p.id),
|
||||
name: p.name,
|
||||
sort: p.sort,
|
||||
})),
|
||||
},
|
||||
'权限排序更新成功'
|
||||
);
|
||||
} finally {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const permissionService = new PermissionService();
|
Loading…
Reference in New Issue
Block a user