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

This commit is contained in:
HeXiaoLong:Suanier 2025-07-08 17:02:49 +08:00
parent 384213714c
commit a78824ab31
5 changed files with 346 additions and 9 deletions

View File

@ -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错误

View File

@ -160,4 +160,39 @@ export const DeletePermissionResponsesSchema = {
};
/** 删除权限成功响应类型 */
export type DeletePermissionSuccessType = Static<(typeof DeletePermissionResponsesSchema)[200]>;
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]>;

View File

@ -169,6 +169,108 @@ export const DeletePermissionParamsSchema = t.Object({
/** 删除权限参数类型 */
export type DeletePermissionParams = Static<typeof DeletePermissionParamsSchema>;
/**
* Schema
*/
export const UpdatePermissionParamsSchema = t.Object({
id: t.String({
pattern: '^[1-9]\\d*$',
description: '权限IDbigint字符串必须为正整数',
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: '父权限IDBigint字符串',
}),
], {
description: '父权限ID0表示顶级权限',
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<typeof UpdatePermissionParamsSchema>;
export type UpdatePermissionBody = Static<typeof UpdatePermissionBodySchema>;
/**
*
*/

View File

@ -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<UpdatePermissionSuccessType>
* @throws BusinessError
*/
public async updatePermission(id: string, body: import('./permission.schema').UpdatePermissionBody): Promise<import('./permission.response').UpdatePermissionSuccessType> {
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();

View File

@ -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` - 权限模块业务逻辑文档