feat(plop): 生成的模块自动应用权限控制

后端 Controller:
- 添加 PermissionGuard 守卫
- 每个接口添加 @RequirePermission 装饰器
- 权限格式: {module}:create/read/update/delete

前端 Table:
- 使用 PermissionGuard 组件进行权限控制
- 新建按钮根据 create 权限显示/隐藏
- 编辑/删除/恢复按钮根据对应权限显示/隐藏
- 整个操作菜单根据 update/delete 权限显示/隐藏

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
charilezhou
2026-01-19 19:54:02 +08:00
parent ed901250bd
commit c37ee29071
2 changed files with 64 additions and 38 deletions

View File

@@ -42,15 +42,18 @@ import { JwtAuthGuard } from '@/auth/guards/jwt-auth.guard';
{{#unless hasQueryDto}}
import { PaginationQueryDto } from '@/common/crud/dto/pagination.dto';
{{/unless}}
import { RequirePermission } from '@/permission/decorators/require-permission.decorator';
import { PermissionGuard } from '@/permission/guards/permission.guard';
@ApiTags('{{chineseName}}')
@Controller('{{kebabCase pluralName}}')
@UseGuards(JwtAuthGuard)
@UseGuards(JwtAuthGuard, PermissionGuard)
@ApiBearerAuth()
export class {{pascalCase name}}Controller {
constructor(private readonly {{camelCase name}}Service: {{pascalCase name}}Service) {}
@Post()
@RequirePermission('{{kebabCase name}}:create')
@ApiOperation({ summary: '创建{{chineseName}}' })
@ApiCreatedResponse({ type: {{pascalCase name}}ResponseDto, description: '创建成功' })
create(@Body() dto: Create{{pascalCase name}}Dto) {
@@ -58,6 +61,7 @@ export class {{pascalCase name}}Controller {
}
@Get()
@RequirePermission('{{kebabCase name}}:read')
@ApiOperation({ summary: '获取所有{{chineseName}}(分页)' })
@ApiOkResponse({ type: Paginated{{pascalCase name}}ResponseDto, description: '{{chineseName}}列表' })
{{#if hasQueryDto}}
@@ -91,6 +95,7 @@ export class {{pascalCase name}}Controller {
{{#if softDelete}}
@Get('deleted')
@RequirePermission('{{kebabCase name}}:read')
@ApiOperation({ summary: '获取已删除的{{chineseName}}列表(分页)' })
@ApiOkResponse({ type: Paginated{{pascalCase name}}ResponseDto, description: '已删除{{chineseName}}列表' })
{{#if hasQueryDto}}
@@ -116,6 +121,7 @@ export class {{pascalCase name}}Controller {
{{/if}}
@Get(':id')
@RequirePermission('{{kebabCase name}}:read')
@ApiOperation({ summary: '根据 ID 获取{{chineseName}}' })
{{#if needsDetailDto}}
@ApiOkResponse({ type: {{pascalCase name}}DetailResponseDto, description: '{{chineseName}}详情' })
@@ -135,6 +141,7 @@ export class {{pascalCase name}}Controller {
{{/if}}
@{{#if (eq serviceType 'CrudService')}}Patch{{else}}Put{{/if}}(':id')
@RequirePermission('{{kebabCase name}}:update')
@ApiOperation({ summary: '更新{{chineseName}}信息' })
@ApiOkResponse({ type: {{pascalCase name}}ResponseDto, description: '更新后的{{chineseName}}信息' })
update(@Param('id') id: string, @Body() dto: Update{{pascalCase name}}Dto) {
@@ -142,6 +149,7 @@ export class {{pascalCase name}}Controller {
}
@Delete(':id')
@RequirePermission('{{kebabCase name}}:delete')
@ApiOperation({ summary: '删除{{chineseName}}' })
@ApiOkResponse({ description: '删除成功' })
delete(@Param('id') id: string) {
@@ -150,6 +158,7 @@ export class {{pascalCase name}}Controller {
{{#if softDelete}}
@{{#if (eq serviceType 'CrudService')}}Patch{{else}}Put{{/if}}(':id/restore')
@RequirePermission('{{kebabCase name}}:update')
@ApiOperation({ summary: '恢复已删除的{{chineseName}}' })
@ApiOkResponse({ type: {{pascalCase name}}ResponseDto, description: '恢复后的{{chineseName}}信息' })
restore(@Param('id') id: string) {
@@ -160,6 +169,7 @@ export class {{pascalCase name}}Controller {
{{#each manyToMany}}
@Get(':id/{{name}}')
@RequirePermission('{{kebabCase ../name}}:read')
@ApiOperation({ summary: '获取{{../chineseName}}{{name}}列表' })
@ApiOkResponse({ type: [{{targetModel}}BriefDto], description: '{{name}}列表' })
get{{pascalCase name}}(@Param('id') id: string) {
@@ -167,6 +177,7 @@ export class {{pascalCase name}}Controller {
}
@Put(':id/{{name}}')
@RequirePermission('{{kebabCase ../name}}:update')
@ApiOperation({ summary: '分配{{../chineseName}}{{name}}' })
@ApiOkResponse({ type: {{pascalCase ../name}}DetailResponseDto, description: '分配成功' })
assign{{pascalCase name}}(@Param('id') id: string, @Body() dto: Assign{{pascalCase name}}Dto) {

View File

@@ -10,6 +10,7 @@ import { toast } from 'sonner';
import { {{pascalCase name}}CreateDialog } from './{{pascalCase name}}CreateDialog';
import { {{pascalCase name}}EditDialog } from './{{pascalCase name}}EditDialog';
import { PermissionGuard } from '@/components/permission/PermissionGuard';
import {
DataTable,
DataTableColumnHeader,
@@ -70,28 +71,52 @@ function {{pascalCase name}}Actions({
onEdit,
}: {{pascalCase name}}ActionsProps) {
return (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">打开菜单</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>操作</DropdownMenuLabel>
<DropdownMenuSeparator />
<PermissionGuard permission={['{{kebabCase name}}:update', '{{kebabCase name}}:delete']}>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost" className="h-8 w-8 p-0">
<span className="sr-only">打开菜单</span>
<MoreHorizontal className="h-4 w-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>操作</DropdownMenuLabel>
<DropdownMenuSeparator />
{{#if softDelete}}
{isDeleted ? (
<DropdownMenuItem onClick={() => onRestore({{camelCase name}}.id)}>
<RotateCcw className="mr-2 h-4 w-4" />
恢复
</DropdownMenuItem>
) : (
<>
{isDeleted ? (
<PermissionGuard permission="{{kebabCase name}}:update">
<DropdownMenuItem onClick={() => onRestore({{camelCase name}}.id)}>
<RotateCcw className="mr-2 h-4 w-4" />
恢复
</DropdownMenuItem>
</PermissionGuard>
) : (
<>
<PermissionGuard permission="{{kebabCase name}}:update">
<DropdownMenuItem onClick={() => onEdit({{camelCase name}})}>
<Pencil className="mr-2 h-4 w-4" />
编辑
</DropdownMenuItem>
</PermissionGuard>
<PermissionGuard permission="{{kebabCase name}}:delete">
<DropdownMenuItem
onClick={() => onDelete({{camelCase name}}.id)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
删除
</DropdownMenuItem>
</PermissionGuard>
</>
)}
{{else}}
<PermissionGuard permission="{{kebabCase name}}:update">
<DropdownMenuItem onClick={() => onEdit({{camelCase name}})}>
<Pencil className="mr-2 h-4 w-4" />
编辑
</DropdownMenuItem>
</PermissionGuard>
<PermissionGuard permission="{{kebabCase name}}:delete">
<DropdownMenuItem
onClick={() => onDelete({{camelCase name}}.id)}
className="text-destructive"
@@ -99,23 +124,11 @@ function {{pascalCase name}}Actions({
<Trash2 className="mr-2 h-4 w-4" />
删除
</DropdownMenuItem>
</>
)}
{{else}}
<DropdownMenuItem onClick={() => onEdit({{camelCase name}})}>
<Pencil className="mr-2 h-4 w-4" />
编辑
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => onDelete({{camelCase name}}.id)}
className="text-destructive"
>
<Trash2 className="mr-2 h-4 w-4" />
删除
</DropdownMenuItem>
</PermissionGuard>
{{/if}}
</DropdownMenuContent>
</DropdownMenu>
</DropdownMenuContent>
</DropdownMenu>
</PermissionGuard>
);
}
@@ -352,10 +365,12 @@ export function {{pascalCase pluralName}}Table() {
>
刷新
</Button>
<Button size="sm" onClick={() => setCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
新建{{chineseName}}
</Button>
<PermissionGuard permission="{{kebabCase name}}:create">
<Button size="sm" onClick={() => setCreateDialogOpen(true)}>
<Plus className="mr-2 h-4 w-4" />
新建{{chineseName}}
</Button>
</PermissionGuard>
</div>
</div>