feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验)

This commit is contained in:
HeXiaoLong:Suanier 2025-07-08 20:24:25 +08:00
parent 24155039b6
commit 74fa306d7a
6 changed files with 379 additions and 18 deletions

View File

@ -8,12 +8,16 @@
*/ */
import { Elysia } from 'elysia'; import { Elysia } from 'elysia';
import { CreatePermissionSchema } from './permission.schema'; import { CreatePermissionSchema, GetPermissionTreeByPidParamsSchema } from './permission.schema';
import { CreatePermissionResponsesSchema } from './permission.response'; import { CreatePermissionResponsesSchema } from './permission.response';
import { DeletePermissionParamsSchema } from './permission.schema'; import { DeletePermissionParamsSchema } from './permission.schema';
import { DeletePermissionResponsesSchema } from './permission.response'; import { DeletePermissionResponsesSchema } from './permission.response';
import { UpdatePermissionParamsSchema, UpdatePermissionBodySchema } from './permission.schema';
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 { GetPermissionTreeResponsesSchema } from './permission.response';
export const permissionController = new Elysia() export const permissionController = new Elysia()
/** /**
@ -22,7 +26,7 @@ export const permissionController = new Elysia()
* @description * @description
*/ */
.post( .post(
'/api/permission', '/',
async ({ body }) => permissionService.createPermission(body), async ({ body }) => permissionService.createPermission(body),
{ {
body: CreatePermissionSchema, body: CreatePermissionSchema,
@ -41,7 +45,7 @@ export const permissionController = new Elysia()
* @description * @description
*/ */
.delete( .delete(
'/api/permission/:id', '/:id',
async ({ params }) => permissionService.deletePermission(params.id), async ({ params }) => permissionService.deletePermission(params.id),
{ {
params: DeletePermissionParamsSchema, params: DeletePermissionParamsSchema,
@ -53,4 +57,44 @@ export const permissionController = new Elysia()
}, },
response: DeletePermissionResponsesSchema, response: DeletePermissionResponsesSchema,
} }
)
/**
*
* @route PUT /api/permission/:id
* @description module一致
*/
.put(
'/:id',
async ({ params, body }) => permissionService.updatePermission(params.id, body),
{
params: UpdatePermissionParamsSchema,
body: UpdatePermissionBodySchema,
detail: {
summary: '修改权限',
description: '修改指定权限支持字段变更、同级重名、父子module一致、非法移动、状态可变更等校验',
tags: [tags.permission],
operationId: 'updatePermission',
},
response: UpdatePermissionResponsesSchema,
}
)
/**
*
* @route GET /api/permission/tree
* @description
*/
.get(
'/tree/:pid',
async ({ query, params }) => permissionService.getPermissionTree(query, params.pid),
{
query: GetPermissionTreeQuerySchema,
params: GetPermissionTreeByPidParamsSchema,
detail: {
summary: '查看权限完整树',
description: '查询权限完整树支持module、status、type多条件筛选',
tags: [tags.permission],
operationId: 'getPermissionTree',
},
response: GetPermissionTreeResponsesSchema,
}
); );

View File

@ -403,3 +403,93 @@ CREATE TABLE sys_permission (
6. **非法移动** 6. **非法移动**
- 输入pid为自身或子节点 - 输入pid为自身或子节点
- 预期返回400错误 - 预期返回400错误
---
### 4. 查看权限完整树接口 (GET /api/permission/tree)
#### 业务逻辑
1. **参数校验**
- 支持可选参数module模块、status状态、type权限类型
- module字符串筛选所属模块
- status数字或字符串1=启用0=禁用,默认全部
- type数字或字符串1=菜单2=按钮3=接口4=数据,默认全部
2. **业务规则**
- 查询所有符合条件的权限数据
- 按level和sort排序
- 构建树形结构(递归或循环)
- 只返回未被禁用或根据status参数权限
- 支持多模块、多类型、多状态筛选
3. **错误处理**
- 参数格式错误400 Bad Request
- 查询异常500 Internal Server Error
#### 性能与安全考虑
- 查询前加索引优化module、status、type、pid、sort
- 构建树结构时避免N+1查询建议一次性查全量后内存组装
- 支持缓存如Redis提升高频查询性能
- 需管理员或有权限用户访问
#### 缓存策略
- 支持按module、status、type等条件缓存完整树结构
- 查询后写入缓存,更新/删除/新增权限时清理相关缓存
#### 响应示例
**成功响应 (200)**
```json
{
"code": 200,
"message": "查询成功",
"data": [
{
"id": "1",
"permission_key": "system",
"name": "系统管理",
"type": 1,
"module": "system",
"pid": "0",
"level": 1,
"sort": 1,
"status": 1,
"children": [
{
"id": "2",
"permission_key": "user",
"name": "用户管理",
"type": 1,
"module": "system",
"pid": "1",
"level": 2,
"sort": 1,
"status": 1,
"children": []
}
]
}
]
}
```
#### 测试用例
1. **正常查询**
- 输入:无参数
- 预期:返回完整权限树
2. **按模块筛选**
- 输入module=system
- 预期只返回system模块的权限树
3. **按状态筛选**
- 输入status=1
- 预期:只返回启用权限
4. **按类型筛选**
- 输入type=1
- 预期:只返回菜单类型权限
5. **参数错误**
- 输入非法status/type/module
- 预期返回400错误

View File

@ -196,3 +196,41 @@ export const UpdatePermissionResponsesSchema = {
/** 修改权限成功响应类型 */ /** 修改权限成功响应类型 */
export type UpdatePermissionSuccessType = Static<(typeof UpdatePermissionResponsesSchema)[200]>; export type UpdatePermissionSuccessType = Static<(typeof UpdatePermissionResponsesSchema)[200]>;
/**
* Schema
* @description children为子权限数组
*/
export const PermissionTreeItemSchema = t.Recursive((self) =>
t.Object({
id: t.String({ description: '权限IDbigint字符串', examples: ['1', '2', '100'] }),
permissionKey: t.String({ description: '权限标识', examples: ['user:create'] }),
name: t.String({ description: '权限名称', examples: ['创建用户'] }),
description: t.Optional(t.String({ description: '权限描述', examples: ['创建新用户的权限'] })),
type: t.Number({ description: '权限类型1=菜单2=按钮3=接口4=数据', examples: [1, 2, 3, 4] }),
apiPathKey: t.Optional(t.String({ description: '接口路径标识', examples: ['/api/user'] })),
pagePathKey: t.Optional(t.String({ description: '前端路由标识', examples: ['/user'] })),
module: t.String({ description: '所属模块', examples: ['user'] }),
pid: t.String({ description: '父权限IDbigint字符串', examples: ['0', '1'] }),
level: t.Number({ description: '权限层级', examples: [1, 2, 3] }),
sort: t.String({ description: '排序值', examples: ['0', '1', '10'] }),
icon: t.Optional(t.String({ description: '图标标识', examples: ['icon-user'] })),
status: t.Number({ description: '权限状态1=启用0=禁用', examples: [1, 0] }),
createdAt: t.String({ description: '创建时间', examples: ['2024-12-19T10:30:00Z'] }),
updatedAt: t.String({ description: '更新时间', examples: ['2024-12-19T10:30:00Z'] }),
children: t.Optional(t.Array(self)),
})
);
/**
*
* @description Controller中定义所有可能的响应格式
*/
export const GetPermissionTreeResponsesSchema = {
200: responseWrapperSchema(t.Array(PermissionTreeItemSchema)),
400: responseWrapperSchema(t.Object({ error: t.String({ description: '请求错误', examples: ['参数错误'] }) })),
500: responseWrapperSchema(t.Object({ error: t.String({ description: '服务器错误', examples: ['内部服务器错误'] }) })),
};
/** 查看权限完整树成功响应类型 */
export type GetPermissionTreeSuccessType = Static<typeof GetPermissionTreeResponsesSchema[200]>;

View File

@ -288,3 +288,56 @@ export const PermissionStatus = {
ENABLED: 1, // 启用 ENABLED: 1, // 启用
DISABLED: 0, // 禁用 DISABLED: 0, // 禁用
} as const; } as const;
/**
* Schema
*/
export const GetPermissionTreeQuerySchema = t.Object({
module: t.Optional(
t.String({
minLength: 1,
maxLength: 30,
description: '所属模块,筛选权限所属模块',
examples: ['system', 'user'],
})
),
status: t.Optional(
t.Union([
t.Literal(1), t.Literal(0), t.Literal('1'), t.Literal('0'), t.Literal('all')
], {
description: '权限状态1=启用0=禁用all=全部',
examples: [1, 0, 'all'],
})
),
type: t.Optional(
t.Union([
t.Literal(1), t.Literal(2), t.Literal(3), t.Literal(4),
t.Literal('1'), t.Literal('2'), t.Literal('3'), t.Literal('4'), t.Literal('all')
], {
description: '权限类型1=菜单2=按钮3=接口4=数据all=全部',
examples: [1, 2, 3, 4, 'all'],
})
),
});
export type GetPermissionTreeQuery = Static<typeof GetPermissionTreeQuerySchema>;
/**
* Schema
* @description GET /api/permission/tree/:pid
*/
export const GetPermissionTreeByPidParamsSchema = t.Object({
pid: t.Union([
t.Literal('0'),
t.String({
pattern: '^[1-9]\\d*$',
description: '父权限IDbigint字符串必须为正整数',
}),
], {
description: '父权限ID0表示获取根权限其他值表示获取指定父权限下的子权限树',
examples: ['0', '1', '100', '123456789'],
}),
});
/** 查看指定父权限下的子权限树参数类型 */
export type GetPermissionTreeByPidParams = Static<typeof GetPermissionTreeByPidParamsSchema>;

View File

@ -10,7 +10,8 @@
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 } from 'drizzle-orm'; import { eq, and, max, ne, sql, asc } from 'drizzle-orm';
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';
import { DistributedLockService } from '@/utils/distributedLock'; import { DistributedLockService } from '@/utils/distributedLock';
@ -280,6 +281,141 @@ export class PermissionService {
await lock.release(); await lock.release();
} }
} }
/**
*
* @param query modulestatustype
* @returns Promise<GetPermissionTreeSuccessType>
*/
public async getPermissionTree(query: import('./permission.schema').GetPermissionTreeQuery, pid: string): Promise<import('./permission.response').GetPermissionTreeSuccessType> {
// 1. 定义表别名
const ctetTableName = 'ctet'
// 锚点表
const pointTable = alias(sysPermission, 'pt');
// CTE表
const cteTable = alias(sysPermission, ctetTableName);
const cteAlias = mysqlTable(ctetTableName, { id: int() })
// 递归表
const recursiveTable = alias(sysPermission, 'rt');
// 2. 构建基础查询条件
const baseConditions: any[] = [];
if (query.module) baseConditions.push(eq(sysPermission.module, query.module));
if (query.status && query.status !== 'all') baseConditions.push(eq(sysPermission.status, Number(query.status)));
if (query.type && query.type !== 'all') baseConditions.push(eq(sysPermission.type, Number(query.type)));
// 找根节点
if(pid !== '0'){
baseConditions.push(eq(pointTable.id, pid));
}else{
baseConditions.push(eq(pointTable.pid, pid));
}
// 3. 定义主查询
const mainQuery = db().select({
id: pointTable.id,
permissionKey: pointTable.permissionKey,
name: pointTable.name,
description: pointTable.description,
type: pointTable.type,
apiPathKey: pointTable.apiPathKey,
pagePathKey: pointTable.pagePathKey,
module: pointTable.module,
pid: pointTable.pid,
level: pointTable.level,
sort: pointTable.sort,
icon: pointTable.icon,
status: pointTable.status,
createdAt: pointTable.createdAt,
updatedAt: pointTable.updatedAt,
})
.from(pointTable)
.where(and(...baseConditions))
.union(
db().select({
id: recursiveTable.id,
permissionKey: recursiveTable.permissionKey,
name: recursiveTable.name,
description: recursiveTable.description,
type: recursiveTable.type,
apiPathKey: recursiveTable.apiPathKey,
pagePathKey: recursiveTable.pagePathKey,
module: recursiveTable.module,
pid: recursiveTable.pid,
level: recursiveTable.level,
sort: recursiveTable.sort,
icon: recursiveTable.icon,
status: recursiveTable.status,
createdAt: recursiveTable.createdAt,
updatedAt: recursiveTable.updatedAt,
})
.from(recursiveTable)
.innerJoin(cteAlias, eq(recursiveTable.pid, cteTable.id))
)
// 4. 构建最终查询
const finalQuery = db()
.select({
id: cteTable.id,
permissionKey: cteTable.permissionKey,
name: cteTable.name,
description: cteTable.description,
type: cteTable.type,
apiPathKey: cteTable.apiPathKey,
pagePathKey: cteTable.pagePathKey,
module: cteTable.module,
pid: cteTable.pid,
level: cteTable.level,
sort: cteTable.sort,
icon: cteTable.icon,
status: cteTable.status,
createdAt: cteTable.createdAt,
updatedAt: cteTable.updatedAt,
})
.from(cteAlias)
.orderBy(asc(cteTable.level), asc(cteTable.sort));
// 5. 将union查询放入WITH RECURSIVE中并构建最终查询
const recursiveSql = sql` WITH RECURSIVE ${cteTable} AS ( ${mainQuery} ) ${finalQuery} `;
// 6. 执行递归查询
const [rows] = await db().execute(recursiveSql);
const list = Array.isArray(rows) ? rows : (rows as any)[0];
// 7. 构建树结构
const nodeMap: Record<string, any> = {};
const roots: any[] = [];
for (const item of list as any[]) {
const node = {
id: String(item.id),
permissionKey: item.permissionKey || item.permission_key,
name: item.name,
description: item.description,
type: item.type,
apiPathKey: item.apiPathKey || item.api_path_key,
pagePathKey: item.pagePathKey || item.page_path_key,
module: item.module,
pid: String(item.pid),
level: item.level,
sort: String(item.sort || '0'),
icon: item.icon,
status: item.status,
createdAt: item.createdAt || item.created_at,
updatedAt: item.updatedAt || item.updated_at,
children: [],
};
nodeMap[node.id] = node;
}
// 8. 组装父子关系
for (const node of Object.values(nodeMap)) {
if (node.pid && node.pid !== '0' && nodeMap[node.pid]) {
nodeMap[node.pid].children.push(node);
} else {
roots.push(node);
}
}
return successResponse(roots, '查询权限树成功');
}
} }
export const permissionService = new PermissionService(); export const permissionService = new PermissionService();

View File

@ -80,21 +80,21 @@ CREATE TABLE sys_permission (
### 3.4 查看权限完整树接口 ### 3.4 查看权限完整树接口
- [ ] 4.0 创建查看权限完整树接口 (GET /api/permission/tree) - [x] 4.0 创建查看权限完整树接口 (GET /api/permission/tree)
- [ ] 4.1 更新 `permission.docs.md` - 添加查看权限完整树业务逻辑文档 - [x] 4.1 更新 `permission.docs.md` - 添加查看权限完整树业务逻辑文档
- [ ] 4.2 更新 `permission.schema.ts` - 定义查看权限完整树Schema - [x] 4.2 更新 `permission.schema.ts` - 定义查看权限完整树Schema
- [ ] 4.3 更新 `permission.response.ts` - 定义查看权限完整树响应格式 - [x] 4.3 更新 `permission.response.ts` - 定义查看权限完整树响应格式
- [ ] 4.4 更新 `permission.service.ts` - 实现查看权限完整树业务逻辑 - [x] 4.4 更新 `permission.service.ts` - 实现查看权限完整树业务逻辑
- [ ] 4.5 更新 `permission.controller.ts` - 实现查看权限完整树路由 - [x] 4.5 更新 `permission.controller.ts` - 实现查看权限完整树路由
### 3.5 查看指定权限树接口 ### 3.5 查看指定权限树接口
- [ ] 5.0 创建查看指定权限树接口 (GET /api/permission/:id/tree) - ~~[ ] 5.0 创建查看指定权限树接口 (GET /api/permission/:id/tree)~~
- [ ] 5.1 更新 `permission.docs.md` - 添加查看指定权限树业务逻辑文档 - ~~[ ] 5.1 更新 `permission.docs.md` - 添加查看指定权限树业务逻辑文档~~
- [ ] 5.2 更新 `permission.schema.ts` - 定义查看指定权限树Schema - ~~[ ] 5.2 更新 `permission.schema.ts` - 定义查看指定权限树Schema~~
- [ ] 5.3 更新 `permission.response.ts` - 定义查看指定权限树响应格式 - ~~[ ] 5.3 更新 `permission.response.ts` - 定义查看指定权限树响应格式~~
- [ ] 5.4 更新 `permission.service.ts` - 实现查看指定权限树业务逻辑 - ~~[ ] 5.4 更新 `permission.service.ts` - 实现查看指定权限树业务逻辑~~
- [ ] 5.5 更新 `permission.controller.ts` - 实现查看指定权限树路由 - ~~[ ] 5.5 更新 `permission.controller.ts` - 实现查看指定权限树路由~~
### 3.6 权限排序接口 ### 3.6 权限排序接口