feat: 实现权限排序接口 (PUT /api/permission/sort)

- 新增权限排序业务逻辑文档到 permission.docs.md
- 定义权限排序请求参数 Schema (SortPermissionSchema)
- 定义权限排序响应格式 Schema (SortPermissionResponsesSchema)
- 实现权限排序业务逻辑 (sortPermissions 方法)
- 添加权限排序路由控制器

功能特性:
- 支持批量权限排序更新
- 验证权限同级别约束
- 验证排序值唯一性
- 使用分布式锁防止并发冲突
- 完整的错误处理和业务校验

接口路径: PUT /api/permission/sort
This commit is contained in:
HeXiaoLong:Suanier 2025-07-08 22:22:12 +08:00
parent 74fa306d7a
commit 64cf3415ab
5 changed files with 508 additions and 10 deletions

View File

@ -16,8 +16,8 @@ import { UpdatePermissionParamsSchema, UpdatePermissionBodySchema } from './perm
import { UpdatePermissionResponsesSchema } from './permission.response'; import { UpdatePermissionResponsesSchema } from './permission.response';
import { permissionService } from './permission.service'; import { permissionService } from './permission.service';
import { tags } from '@/constants/swaggerTags'; import { tags } from '@/constants/swaggerTags';
import { GetPermissionTreeQuerySchema } from './permission.schema'; import { GetPermissionTreeQuerySchema, SortPermissionSchema } from './permission.schema';
import { GetPermissionTreeResponsesSchema } from './permission.response'; import { GetPermissionTreeResponsesSchema, SortPermissionResponsesSchema } from './permission.response';
export const permissionController = new Elysia() export const permissionController = new Elysia()
/** /**
@ -84,11 +84,10 @@ export const permissionController = new Elysia()
* @description * @description
*/ */
.get( .get(
'/tree/:pid', '/tree',
async ({ query, params }) => permissionService.getPermissionTree(query, params.pid), async ({ query }) => permissionService.getPermissionTree(query),
{ {
query: GetPermissionTreeQuerySchema, query: GetPermissionTreeQuerySchema,
params: GetPermissionTreeByPidParamsSchema,
detail: { detail: {
summary: '查看权限完整树', summary: '查看权限完整树',
description: '查询权限完整树支持module、status、type多条件筛选', description: '查询权限完整树支持module、status、type多条件筛选',
@ -97,4 +96,23 @@ export const permissionController = new Elysia()
}, },
response: GetPermissionTreeResponsesSchema, 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,
}
); );

View File

@ -492,4 +492,208 @@ CREATE TABLE sys_permission (
- 预期:只返回菜单类型权限 - 预期:只返回菜单类型权限
5. **参数错误** 5. **参数错误**
- 输入非法status/type/module - 输入非法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状态
- 失败时回滚前端显示状态
- 成功后实时更新权限树显示

View File

@ -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: '权限IDbigint字符串',
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]>;

View File

@ -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: '权限IDbigint字符串必须为正整数',
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>;

View File

@ -10,7 +10,7 @@
import { Logger } from '@/plugins/logger/logger.service'; import { Logger } from '@/plugins/logger/logger.service';
import { db } from '@/plugins/drizzle/drizzle.service'; import { db } from '@/plugins/drizzle/drizzle.service';
import { sysPermission, sysRolePermissions } from '@/eneities'; 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 { alias, bigint, int, mysqlTable } from 'drizzle-orm/mysql-core';
import { successResponse, BusinessError } from '@/utils/responseFormate'; import { successResponse, BusinessError } from '@/utils/responseFormate';
import { nextId } from '@/utils/snowflake'; import { nextId } from '@/utils/snowflake';
@ -285,9 +285,10 @@ export class PermissionService {
/** /**
* *
* @param query modulestatustype * @param query modulestatustype
* @param pid ID'0'
* @returns Promise<GetPermissionTreeSuccessType> * @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. 定义表别名 // 1. 定义表别名
const ctetTableName = 'ctet' const ctetTableName = 'ctet'
// 锚点表 // 锚点表
@ -416,6 +417,133 @@ export class PermissionService {
} }
return successResponse(roots, '查询权限树成功'); 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(); export const permissionService = new PermissionService();