feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验)
This commit is contained in:
parent
384213714c
commit
a78824ab31
@ -308,3 +308,98 @@ CREATE TABLE sys_permission (
|
||||
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错误
|
@ -161,3 +161,38 @@ export const DeletePermissionResponsesSchema = {
|
||||
|
||||
/** 删除权限成功响应类型 */
|
||||
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]>;
|
@ -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: '权限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<typeof UpdatePermissionParamsSchema>;
|
||||
export type UpdatePermissionBody = Static<typeof UpdatePermissionBodySchema>;
|
||||
|
||||
/**
|
||||
* 权限类型枚举
|
||||
*/
|
||||
|
@ -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();
|
@ -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` - 权限模块业务逻辑文档
|
||||
|
Loading…
Reference in New Issue
Block a user