feat(permission): 完成权限模块修改接口全链路实现(含分布式锁、同级重名、父子module一致、非法移动、状态可变更等校验)
This commit is contained in:
parent
384213714c
commit
a78824ab31
@ -308,3 +308,98 @@ CREATE TABLE sys_permission (
|
|||||||
5. **被引用**
|
5. **被引用**
|
||||||
- 输入:被角色/用户引用的权限id
|
- 输入:被角色/用户引用的权限id
|
||||||
- 预期:返回400错误
|
- 预期:返回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]>;
|
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>;
|
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 { Logger } from '@/plugins/logger/logger.service';
|
||||||
import { db } from '@/plugins/drizzle/drizzle.service';
|
import { db } from '@/plugins/drizzle/drizzle.service';
|
||||||
import { sysPermission, sysRolePermissions } from '@/eneities';
|
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 { successResponse, BusinessError } from '@/utils/responseFormate';
|
||||||
import { nextId } from '@/utils/snowflake';
|
import { nextId } from '@/utils/snowflake';
|
||||||
import { DistributedLockService } from '@/utils/distributedLock';
|
import { DistributedLockService } from '@/utils/distributedLock';
|
||||||
@ -176,6 +176,110 @@ export class PermissionService {
|
|||||||
await lock.release();
|
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();
|
export const permissionService = new PermissionService();
|
@ -53,7 +53,7 @@ CREATE TABLE sys_permission (
|
|||||||
|
|
||||||
### 3.1 新增权限接口
|
### 3.1 新增权限接口
|
||||||
|
|
||||||
- [ ] 1.0 创建新增权限接口 (POST /api/permission)
|
- [x] 1.0 创建新增权限接口 (POST /api/permission)
|
||||||
- [x] 1.1 生成当前接口业务逻辑文档,写入 `permission.docs.md`
|
- [x] 1.1 生成当前接口业务逻辑文档,写入 `permission.docs.md`
|
||||||
- [x] 1.2 创建 `permission.schema.ts` - 定义新增权限Schema
|
- [x] 1.2 创建 `permission.schema.ts` - 定义新增权限Schema
|
||||||
- [x] 1.3 创建 `permission.response.ts` - 定义新增权限响应格式
|
- [x] 1.3 创建 `permission.response.ts` - 定义新增权限响应格式
|
||||||
@ -62,12 +62,12 @@ CREATE TABLE sys_permission (
|
|||||||
|
|
||||||
### 3.2 删除权限接口
|
### 3.2 删除权限接口
|
||||||
|
|
||||||
- [ ] 2.0 创建删除权限接口 (DELETE /api/permission/:id)
|
- [x] 2.0 创建删除权限接口 (DELETE /api/permission/:id)
|
||||||
- [ ] 2.1 更新 `permission.docs.md` - 添加删除权限业务逻辑文档
|
- [x] 2.1 更新 `permission.docs.md` - 添加删除权限业务逻辑文档
|
||||||
- [ ] 2.2 更新 `permission.schema.ts` - 定义删除权限Schema
|
- [x] 2.2 更新 `permission.schema.ts` - 定义删除权限Schema
|
||||||
- [ ] 2.3 更新 `permission.response.ts` - 定义删除权限响应格式
|
- [x] 2.3 更新 `permission.response.ts` - 定义删除权限响应格式
|
||||||
- [ ] 2.4 更新 `permission.service.ts` - 实现删除权限业务逻辑
|
- [x] 2.4 更新 `permission.service.ts` - 实现删除权限业务逻辑
|
||||||
- [ ] 2.5 更新 `permission.controller.ts` - 实现删除权限路由
|
- [x] 2.5 更新 `permission.controller.ts` - 实现删除权限路由
|
||||||
|
|
||||||
### 3.3 修改权限接口
|
### 3.3 修改权限接口
|
||||||
|
|
||||||
@ -114,6 +114,7 @@ CREATE TABLE sys_permission (
|
|||||||
- [ ] 7.4 更新 `permission.service.ts` - 实现获取权限详情业务逻辑
|
- [ ] 7.4 更新 `permission.service.ts` - 实现获取权限详情业务逻辑
|
||||||
- [ ] 7.5 更新 `permission.controller.ts` - 实现获取权限详情路由
|
- [ ] 7.5 更新 `permission.controller.ts` - 实现获取权限详情路由
|
||||||
|
|
||||||
|
|
||||||
## 4. 相关文件
|
## 4. 相关文件
|
||||||
|
|
||||||
- `src/modules/permission/permission.docs.md` - 权限模块业务逻辑文档
|
- `src/modules/permission/permission.docs.md` - 权限模块业务逻辑文档
|
||||||
|
Loading…
Reference in New Issue
Block a user