feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验)
This commit is contained in:
parent
24155039b6
commit
74fa306d7a
@ -8,12 +8,16 @@
|
||||
*/
|
||||
|
||||
import { Elysia } from 'elysia';
|
||||
import { CreatePermissionSchema } from './permission.schema';
|
||||
import { CreatePermissionSchema, GetPermissionTreeByPidParamsSchema } from './permission.schema';
|
||||
import { CreatePermissionResponsesSchema } from './permission.response';
|
||||
import { DeletePermissionParamsSchema } from './permission.schema';
|
||||
import { DeletePermissionResponsesSchema } from './permission.response';
|
||||
import { UpdatePermissionParamsSchema, UpdatePermissionBodySchema } from './permission.schema';
|
||||
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';
|
||||
|
||||
export const permissionController = new Elysia()
|
||||
/**
|
||||
@ -22,7 +26,7 @@ export const permissionController = new Elysia()
|
||||
* @description 创建新的权限项,支持树形结构
|
||||
*/
|
||||
.post(
|
||||
'/api/permission',
|
||||
'/',
|
||||
async ({ body }) => permissionService.createPermission(body),
|
||||
{
|
||||
body: CreatePermissionSchema,
|
||||
@ -41,7 +45,7 @@ export const permissionController = new Elysia()
|
||||
* @description 删除指定权限,需检查子权限和引用关系
|
||||
*/
|
||||
.delete(
|
||||
'/api/permission/:id',
|
||||
'/:id',
|
||||
async ({ params }) => permissionService.deletePermission(params.id),
|
||||
{
|
||||
params: DeletePermissionParamsSchema,
|
||||
@ -53,4 +57,44 @@ export const permissionController = new Elysia()
|
||||
},
|
||||
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,
|
||||
}
|
||||
);
|
@ -402,4 +402,94 @@ CREATE TABLE sys_permission (
|
||||
- 预期:返回400错误
|
||||
6. **非法移动**
|
||||
- 输入:pid为自身或子节点
|
||||
- 预期:返回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错误
|
@ -195,4 +195,42 @@ 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: '权限ID(bigint字符串)', 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: '父权限ID(bigint字符串)', 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]>;
|
@ -287,4 +287,57 @@ export const PermissionType = {
|
||||
export const PermissionStatus = {
|
||||
ENABLED: 1, // 启用
|
||||
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: '父权限ID,bigint字符串,必须为正整数',
|
||||
}),
|
||||
], {
|
||||
description: '父权限ID,0表示获取根权限,其他值表示获取指定父权限下的子权限树',
|
||||
examples: ['0', '1', '100', '123456789'],
|
||||
}),
|
||||
});
|
||||
|
||||
/** 查看指定父权限下的子权限树参数类型 */
|
||||
export type GetPermissionTreeByPidParams = Static<typeof GetPermissionTreeByPidParamsSchema>;
|
@ -10,7 +10,8 @@
|
||||
import { Logger } from '@/plugins/logger/logger.service';
|
||||
import { db } from '@/plugins/drizzle/drizzle.service';
|
||||
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 { nextId } from '@/utils/snowflake';
|
||||
import { DistributedLockService } from '@/utils/distributedLock';
|
||||
@ -280,6 +281,141 @@ export class PermissionService {
|
||||
await lock.release();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取权限完整树
|
||||
* @param query 查询参数(module、status、type)
|
||||
* @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();
|
@ -80,21 +80,21 @@ CREATE TABLE sys_permission (
|
||||
|
||||
### 3.4 查看权限完整树接口
|
||||
|
||||
- [ ] 4.0 创建查看权限完整树接口 (GET /api/permission/tree)
|
||||
- [ ] 4.1 更新 `permission.docs.md` - 添加查看权限完整树业务逻辑文档
|
||||
- [ ] 4.2 更新 `permission.schema.ts` - 定义查看权限完整树Schema
|
||||
- [ ] 4.3 更新 `permission.response.ts` - 定义查看权限完整树响应格式
|
||||
- [ ] 4.4 更新 `permission.service.ts` - 实现查看权限完整树业务逻辑
|
||||
- [ ] 4.5 更新 `permission.controller.ts` - 实现查看权限完整树路由
|
||||
- [x] 4.0 创建查看权限完整树接口 (GET /api/permission/tree)
|
||||
- [x] 4.1 更新 `permission.docs.md` - 添加查看权限完整树业务逻辑文档
|
||||
- [x] 4.2 更新 `permission.schema.ts` - 定义查看权限完整树Schema
|
||||
- [x] 4.3 更新 `permission.response.ts` - 定义查看权限完整树响应格式
|
||||
- [x] 4.4 更新 `permission.service.ts` - 实现查看权限完整树业务逻辑
|
||||
- [x] 4.5 更新 `permission.controller.ts` - 实现查看权限完整树路由
|
||||
|
||||
### 3.5 查看指定权限树接口
|
||||
|
||||
- [ ] 5.0 创建查看指定权限树接口 (GET /api/permission/:id/tree)
|
||||
- [ ] 5.1 更新 `permission.docs.md` - 添加查看指定权限树业务逻辑文档
|
||||
- [ ] 5.2 更新 `permission.schema.ts` - 定义查看指定权限树Schema
|
||||
- [ ] 5.3 更新 `permission.response.ts` - 定义查看指定权限树响应格式
|
||||
- [ ] 5.4 更新 `permission.service.ts` - 实现查看指定权限树业务逻辑
|
||||
- [ ] 5.5 更新 `permission.controller.ts` - 实现查看指定权限树路由
|
||||
- ~~[ ] 5.0 创建查看指定权限树接口 (GET /api/permission/:id/tree)~~
|
||||
- ~~[ ] 5.1 更新 `permission.docs.md` - 添加查看指定权限树业务逻辑文档~~
|
||||
- ~~[ ] 5.2 更新 `permission.schema.ts` - 定义查看指定权限树Schema~~
|
||||
- ~~[ ] 5.3 更新 `permission.response.ts` - 定义查看指定权限树响应格式~~
|
||||
- ~~[ ] 5.4 更新 `permission.service.ts` - 实现查看指定权限树业务逻辑~~
|
||||
- ~~[ ] 5.5 更新 `permission.controller.ts` - 实现查看指定权限树路由~~
|
||||
|
||||
### 3.6 权限排序接口
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user