From a78824ab313a7d6b16056bf2ff1170cff9b1704d Mon Sep 17 00:00:00 2001 From: "HeXiaoLong:Suanier" Date: Tue, 8 Jul 2025 17:02:49 +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 --- src/modules/permission/permission.docs.md | 95 ++++++++++++++++ src/modules/permission/permission.response.ts | 37 +++++- src/modules/permission/permission.schema.ts | 102 +++++++++++++++++ src/modules/permission/permission.service.ts | 106 +++++++++++++++++- tasks/权限模块开发计划.md | 15 +-- 5 files changed, 346 insertions(+), 9 deletions(-) diff --git a/src/modules/permission/permission.docs.md b/src/modules/permission/permission.docs.md index 7316b6d..3151ca6 100644 --- a/src/modules/permission/permission.docs.md +++ b/src/modules/permission/permission.docs.md @@ -307,4 +307,99 @@ CREATE TABLE sys_permission ( - 预期:返回400错误 5. **被引用** - 输入:被角色/用户引用的权限id + - 预期:返回400错误 + +--- + +### 3. 修改权限接口 (PUT /api/permission/:id) + +#### 业务逻辑 + +1. **参数验证** + - 验证id格式(bigint字符串) + - 验证id是否存在 + - 校验可修改字段(如name、description、type、apiPathKey、pagePathKey、module、sort、icon、status等) + - name同级唯一性校验(如有修改) + - type、module等字段格式校验 + +2. **业务规则** + - 只允许修改状态为启用(status=1)的权限 + - 存在父权限时,module必须与父权限一致 + - 同级允许重名,但permissionKey必须唯一 + - 不允许修改permissionKey(如需支持请说明) + - 不允许将权限移动到自身或其子节点下 + - 修改type、module等需考虑子权限影响 + - 记录修改操作日志 + +3. **错误处理** + - 权限不存在:404 Not Found + - 权限状态非启用:400 Bad Request + - name同级下已存在:409 Conflict + - 父权限module不一致:400 Bad Request + - 非法移动:400 Bad Request + +#### 性能与安全考虑 + +- name、module、type等字段加索引优化 +- 修改操作需加分布式锁防止并发冲突 +- 需管理员权限 +- 防止越权修改 + +#### 缓存策略 + +- 修改后清除相关权限缓存、权限树缓存、模块权限缓存 + +#### 响应示例 + +**成功响应 (200)** +```json +{ + "code": 200, + "message": "权限修改成功", + "data": { + "id": "123456789", + "permission_key": "user:create", + "name": "创建用户", + "description": "创建新用户的权限", + "type": 3, + "module": "user", + "pid": "0", + "level": 1, + "sort": 1, + "icon": null, + "status": 1, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:40:00Z" + } +} +``` + +**错误响应 (400/404/409)** +```json +{ + "code": 409, + "message": "同级下已存在该名称", + "data": null +} +``` + +#### 测试用例 + +1. **正常修改** + - 输入:有效的权限id和修改字段 + - 预期:修改成功,返回新数据 +2. **同级重名** + - 输入:同级下已存在的name + - 预期:返回409错误 +3. **权限不存在** + - 输入:不存在的id + - 预期:返回404错误 +4. **状态非启用** + - 输入:status=0的权限id + - 预期:返回400错误 +5. **父权限module不一致** + - 输入:module与父权限不一致 + - 预期:返回400错误 +6. **非法移动** + - 输入:pid为自身或子节点 - 预期:返回400错误 \ No newline at end of file diff --git a/src/modules/permission/permission.response.ts b/src/modules/permission/permission.response.ts index add3256..ffe7a44 100644 --- a/src/modules/permission/permission.response.ts +++ b/src/modules/permission/permission.response.ts @@ -160,4 +160,39 @@ export const DeletePermissionResponsesSchema = { }; /** 删除权限成功响应类型 */ -export type DeletePermissionSuccessType = Static<(typeof DeletePermissionResponsesSchema)[200]>; \ No newline at end of file +export type DeletePermissionSuccessType = Static<(typeof DeletePermissionResponsesSchema)[200]>; + +/** + * 修改权限接口响应组合 + * @description 用于Controller中定义所有可能的响应格式 + */ +export const UpdatePermissionResponsesSchema = { + 200: responseWrapperSchema(CreatePermissionSuccessSchema), + 400: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '请求错误', + examples: ['权限状态非启用', '父权限module不一致', '非法移动'], + }), + }), + ), + 404: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '资源不存在', + examples: ['权限不存在'], + }), + }), + ), + 409: responseWrapperSchema( + t.Object({ + error: t.String({ + description: '唯一性冲突', + examples: ['同级下已存在该名称'], + }), + }), + ), +}; + +/** 修改权限成功响应类型 */ +export type UpdatePermissionSuccessType = Static<(typeof UpdatePermissionResponsesSchema)[200]>; \ No newline at end of file diff --git a/src/modules/permission/permission.schema.ts b/src/modules/permission/permission.schema.ts index c47b2ac..0519db8 100644 --- a/src/modules/permission/permission.schema.ts +++ b/src/modules/permission/permission.schema.ts @@ -169,6 +169,108 @@ export const DeletePermissionParamsSchema = t.Object({ /** 删除权限参数类型 */ export type DeletePermissionParams = Static; +/** + * 修改权限接口参数Schema(路径参数) + */ +export const UpdatePermissionParamsSchema = t.Object({ + id: t.String({ + pattern: '^[1-9]\\d*$', + description: '权限ID,bigint字符串,必须为正整数', + examples: ['1', '100', '123456789'], + }), +}); + +/** + * 修改权限接口请求体Schema + */ +export const UpdatePermissionBodySchema = t.Object({ + name: t.Optional( + t.String({ + minLength: 1, + maxLength: 50, + description: '权限名称,同级下唯一', + examples: ['创建用户', '查看用户'], + }) + ), + description: t.Optional( + t.String({ + maxLength: 255, + description: '权限描述', + examples: ['创建新用户的权限'], + }) + ), + 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'), + ], { + description: '权限类型:1=菜单,2=按钮,3=接口,4=数据', + examples: [1, 2, 3, 4], + }) + ), + apiPathKey: t.Optional( + t.String({ + maxLength: 200, + description: '接口路径标识', + examples: ['/api/user'], + }) + ), + pagePathKey: t.Optional( + t.String({ + maxLength: 200, + description: '前端路由标识', + examples: ['/user'], + }) + ), + module: t.Optional( + t.String({ + minLength: 1, + maxLength: 30, + pattern: '^[a-zA-Z0-9_]+$', + description: '所属模块,只允许字母数字下划线', + examples: ['user', 'role'], + }) + ), + pid: t.Optional( + t.Union([ + t.Literal('0'), + t.String({ + pattern: '^[1-9]\\d*$', + description: '父权限ID,Bigint字符串', + }), + ], { + description: '父权限ID,0表示顶级权限', + examples: ['0', '1', '2'], + }) + ), + sort: t.Optional( + t.Number({ + minimum: 0, + maximum: 999999, + description: '排序值', + examples: [0, 1, 10], + }) + ), + icon: t.Optional( + t.String({ + maxLength: 100, + description: '图标标识', + examples: ['icon-user'], + }) + ), + status: t.Optional( + t.Union([ + t.Literal(1), t.Literal(0), t.Literal('1'), t.Literal('0') + ], { + description: '权限状态:1=启用,0=禁用', + examples: [1, 0], + }) + ), +}); + +export type UpdatePermissionParams = Static; +export type UpdatePermissionBody = Static; + /** * 权限类型枚举 */ diff --git a/src/modules/permission/permission.service.ts b/src/modules/permission/permission.service.ts index d59af78..23559fc 100644 --- a/src/modules/permission/permission.service.ts +++ b/src/modules/permission/permission.service.ts @@ -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 } from 'drizzle-orm'; +import { eq, and, max, ne } from 'drizzle-orm'; import { successResponse, BusinessError } from '@/utils/responseFormate'; import { nextId } from '@/utils/snowflake'; import { DistributedLockService } from '@/utils/distributedLock'; @@ -176,6 +176,110 @@ export class PermissionService { await lock.release(); } } + + /** + * 修改权限 + * @param id 权限ID + * @param body 修改字段 + * @returns Promise + * @throws BusinessError 业务逻辑错误 + */ + public async updatePermission(id: string, body: import('./permission.schema').UpdatePermissionBody): Promise { + const lockKey = `permission:update:id:${id}`; + const lock = await DistributedLockService.acquire({ key: lockKey, ttl: 10 }); + try { + // 1. 校验存在性 + const permArr = await db().select().from(sysPermission).where(eq(sysPermission.id, id)).limit(1); + if (!permArr || permArr.length === 0) { + throw new BusinessError('权限不存在', 404); + } + const perm = permArr[0]!; + if (perm.status !== 1) { + throw new BusinessError('权限状态非启用', 400); + } + // 2. 校验同级重名(如有修改name) + if (body.name && body.name !== perm.name) { + const existName = await db() + .select({ id: sysPermission.id }) + .from(sysPermission) + .where(and(eq(sysPermission.name, body.name), eq(sysPermission.pid, perm.pid as string), ne(sysPermission.id, id))) + .limit(1); + if (existName.length > 0) { + throw new BusinessError('同级下已存在该名称', 409); + } + } + // 3. 校验父权限module一致、非法移动 + let newPid = perm.pid; + if (body.pid && body.pid !== String(perm.pid)) { + if (body.pid === id) { + throw new BusinessError('不能将权限移动到自身下', 400); + } + // 检查是否移动到子节点下(递归查) + const allPerms = await db().select().from(sysPermission); + const findChildren = (parentId: string, list: any[]): string[] => { + const children = list.filter(item => String(item.pid) === parentId).map(item => String(item.id)); + return children.concat(children.flatMap(cid => findChildren(cid, list))); + }; + const childrenIds = findChildren(id, allPerms); + if (childrenIds.includes(body.pid)) { + throw new BusinessError('不能将权限移动到自身子节点下', 400); + } + // 检查父权限存在且module一致 + const parentArr = await db().select().from(sysPermission).where(eq(sysPermission.id, body.pid)).limit(1); + if (!parentArr || parentArr.length === 0) { + throw new BusinessError('父权限不存在', 400); + } + if (body.module && parentArr[0]!.module !== body.module) { + throw new BusinessError('父权限module不一致', 400); + } + newPid = body.pid; + } + // 4. 组装更新字段 + const updateData: any = {}; + if (body.name) updateData.name = body.name; + if (body.description !== undefined) updateData.description = body.description; + if (body.type !== undefined) updateData.type = Number(body.type); + if (body.apiPathKey !== undefined) updateData.apiPathKey = body.apiPathKey; + if (body.pagePathKey !== undefined) updateData.pagePathKey = body.pagePathKey; + if (body.module !== undefined) updateData.module = body.module; + if (body.icon !== undefined) updateData.icon = body.icon; + if (body.status !== undefined) updateData.status = Number(body.status); + if (body.sort !== undefined) updateData.sort = Number(body.sort); + if (body.pid !== undefined) updateData.pid = newPid; + updateData.updatedAt = new Date().toISOString(); + // 5. 执行更新 + await db().update(sysPermission).set(updateData).where(eq(sysPermission.id, id)); + // 6. 查询最新数据 + const updatedArr = await db().select().from(sysPermission).where(eq(sysPermission.id, id)).limit(1); + if (!updatedArr || updatedArr.length === 0) { + throw new BusinessError('权限修改失败', 500); + } + const updated = updatedArr[0]!; + // 7. 返回统一响应 + return successResponse( + { + id: String(updated.id), + permission_key: updated.permissionKey, + name: updated.name, + description: updated.description, + type: updated.type, + api_path_key: updated.apiPathKey, + page_path_key: updated.pagePathKey, + module: updated.module, + pid: String(updated.pid), + level: updated.level, + sort: updated.sort, + icon: updated.icon, + status: updated.status, + created_at: updated.createdAt, + updated_at: updated.updatedAt, + }, + '权限修改成功', + ); + } finally { + await lock.release(); + } + } } export const permissionService = new PermissionService(); \ No newline at end of file diff --git a/tasks/权限模块开发计划.md b/tasks/权限模块开发计划.md index 1c0e02f..d0d30bd 100644 --- a/tasks/权限模块开发计划.md +++ b/tasks/权限模块开发计划.md @@ -53,7 +53,7 @@ CREATE TABLE sys_permission ( ### 3.1 新增权限接口 -- [ ] 1.0 创建新增权限接口 (POST /api/permission) +- [x] 1.0 创建新增权限接口 (POST /api/permission) - [x] 1.1 生成当前接口业务逻辑文档,写入 `permission.docs.md` - [x] 1.2 创建 `permission.schema.ts` - 定义新增权限Schema - [x] 1.3 创建 `permission.response.ts` - 定义新增权限响应格式 @@ -62,12 +62,12 @@ CREATE TABLE sys_permission ( ### 3.2 删除权限接口 -- [ ] 2.0 创建删除权限接口 (DELETE /api/permission/:id) - - [ ] 2.1 更新 `permission.docs.md` - 添加删除权限业务逻辑文档 - - [ ] 2.2 更新 `permission.schema.ts` - 定义删除权限Schema - - [ ] 2.3 更新 `permission.response.ts` - 定义删除权限响应格式 - - [ ] 2.4 更新 `permission.service.ts` - 实现删除权限业务逻辑 - - [ ] 2.5 更新 `permission.controller.ts` - 实现删除权限路由 +- [x] 2.0 创建删除权限接口 (DELETE /api/permission/:id) + - [x] 2.1 更新 `permission.docs.md` - 添加删除权限业务逻辑文档 + - [x] 2.2 更新 `permission.schema.ts` - 定义删除权限Schema + - [x] 2.3 更新 `permission.response.ts` - 定义删除权限响应格式 + - [x] 2.4 更新 `permission.service.ts` - 实现删除权限业务逻辑 + - [x] 2.5 更新 `permission.controller.ts` - 实现删除权限路由 ### 3.3 修改权限接口 @@ -114,6 +114,7 @@ CREATE TABLE sys_permission ( - [ ] 7.4 更新 `permission.service.ts` - 实现获取权限详情业务逻辑 - [ ] 7.5 更新 `permission.controller.ts` - 实现获取权限详情路由 + ## 4. 相关文件 - `src/modules/permission/permission.docs.md` - 权限模块业务逻辑文档