From 74fa306d7aa2acb29dcf72268c1d9766d075746b Mon Sep 17 00:00:00 2001 From: "HeXiaoLong:Suanier" Date: Tue, 8 Jul 2025 20:24:25 +0800 Subject: [PATCH] =?UTF-8?q?feat(permission):=20=E5=AE=8C=E6=88=90=E6=9D=83?= =?UTF-8?q?=E9=99=90=E6=A8=A1=E5=9D=97=E4=BF=AE=E6=94=B9=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E5=85=A8=E9=93=BE=E8=B7=AF=E5=AE=9E=E7=8E=B0=EF=BC=88=E5=90=AB?= =?UTF-8?q?=E5=88=86=E5=B8=83=E5=BC=8F=E9=94=81=E3=80=81=E5=90=8C=E7=BA=A7?= =?UTF-8?q?=E9=87=8D=E5=90=8D=E3=80=81=E7=88=B6=E5=AD=90module=E4=B8=80?= =?UTF-8?q?=E8=87=B4=E3=80=81=E9=9D=9E=E6=B3=95=E7=A7=BB=E5=8A=A8=E3=80=81?= =?UTF-8?q?=E7=8A=B6=E6=80=81=E5=8F=AF=E5=8F=98=E6=9B=B4=E7=AD=89=E6=A0=A1?= =?UTF-8?q?=E9=AA=8C=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../permission/permission.controller.ts | 50 ++++++- src/modules/permission/permission.docs.md | 90 ++++++++++++ src/modules/permission/permission.response.ts | 40 ++++- src/modules/permission/permission.schema.ts | 55 ++++++- src/modules/permission/permission.service.ts | 138 +++++++++++++++++- tasks/权限模块开发计划.md | 24 +-- 6 files changed, 379 insertions(+), 18 deletions(-) diff --git a/src/modules/permission/permission.controller.ts b/src/modules/permission/permission.controller.ts index 6530d9c..7d992d4 100644 --- a/src/modules/permission/permission.controller.ts +++ b/src/modules/permission/permission.controller.ts @@ -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, + } ); \ No newline at end of file diff --git a/src/modules/permission/permission.docs.md b/src/modules/permission/permission.docs.md index 3151ca6..55ce085 100644 --- a/src/modules/permission/permission.docs.md +++ b/src/modules/permission/permission.docs.md @@ -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错误 \ No newline at end of file diff --git a/src/modules/permission/permission.response.ts b/src/modules/permission/permission.response.ts index ffe7a44..84769a7 100644 --- a/src/modules/permission/permission.response.ts +++ b/src/modules/permission/permission.response.ts @@ -195,4 +195,42 @@ export const UpdatePermissionResponsesSchema = { }; /** 修改权限成功响应类型 */ -export type UpdatePermissionSuccessType = Static<(typeof UpdatePermissionResponsesSchema)[200]>; \ No newline at end of file +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; \ No newline at end of file diff --git a/src/modules/permission/permission.schema.ts b/src/modules/permission/permission.schema.ts index 0519db8..f16f197 100644 --- a/src/modules/permission/permission.schema.ts +++ b/src/modules/permission/permission.schema.ts @@ -287,4 +287,57 @@ export const PermissionType = { export const PermissionStatus = { ENABLED: 1, // 启用 DISABLED: 0, // 禁用 -} as const; \ No newline at end of file +} 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; + +/** + * 查看指定父权限下的子权限树接口路径参数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; \ No newline at end of file diff --git a/src/modules/permission/permission.service.ts b/src/modules/permission/permission.service.ts index 23559fc..861a98f 100644 --- a/src/modules/permission/permission.service.ts +++ b/src/modules/permission/permission.service.ts @@ -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 + */ + public async getPermissionTree(query: import('./permission.schema').GetPermissionTreeQuery, pid: string): Promise { + // 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 = {}; + 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(); \ No newline at end of file diff --git a/tasks/权限模块开发计划.md b/tasks/权限模块开发计划.md index fe5d176..c7726fe 100644 --- a/tasks/权限模块开发计划.md +++ b/tasks/权限模块开发计划.md @@ -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 权限排序接口