From 35b12fb0543437a164c4a85a8480010d3db4a273 Mon Sep 17 00:00:00 2001
From: MSuMshk <2039814060@qq.com>
Date: Tue, 2 Dec 2025 16:43:46 +0800
Subject: [PATCH] =?UTF-8?q?feat:=20=E5=A2=9E=E5=8A=A0=E8=A7=92=E8=89=B2/?=
=?UTF-8?q?=E6=9D=83=E9=99=90=E7=AE=A1=E7=90=86=20API=20=E4=B8=8E=E5=BA=94?=
=?UTF-8?q?=E7=94=A8=E5=B1=82=E5=91=BD=E4=BB=A4?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
Document/11_SystemTodo.md | 2 +-
.../Controllers/PermissionsController.cs | 78 ++++++++++++++++
.../Controllers/RolesController.cs | 93 +++++++++++++++++++
.../Commands/AssignUserRolesCommand.cs | 12 +++
.../Commands/BindRolePermissionsCommand.cs | 12 +++
.../Commands/CreatePermissionCommand.cs | 14 +++
.../Identity/Commands/CreateRoleCommand.cs | 14 +++
.../Commands/DeletePermissionCommand.cs | 11 +++
.../Identity/Commands/DeleteRoleCommand.cs | 11 +++
.../Commands/UpdatePermissionCommand.cs | 14 +++
.../Identity/Commands/UpdateRoleCommand.cs | 14 +++
.../Identity/Contracts/PermissionDto.cs | 37 ++++++++
.../Identity/Contracts/RoleDto.cs | 37 ++++++++
.../Handlers/AssignUserRolesCommandHandler.cs | 23 +++++
.../BindRolePermissionsCommandHandler.cs | 23 +++++
.../CreatePermissionCommandHandler.cs | 41 ++++++++
.../Handlers/CreateRoleCommandHandler.cs | 41 ++++++++
.../DeletePermissionCommandHandler.cs | 23 +++++
.../Handlers/DeleteRoleCommandHandler.cs | 23 +++++
.../Handlers/SearchPermissionsQueryHandler.cs | 54 +++++++++++
.../Handlers/SearchRolesQueryHandler.cs | 51 ++++++++++
.../UpdatePermissionCommandHandler.cs | 41 ++++++++
.../Handlers/UpdateRoleCommandHandler.cs | 41 ++++++++
.../Queries/SearchPermissionsQuery.cs | 17 ++++
.../Identity/Queries/SearchRolesQuery.cs | 17 ++++
25 files changed, 743 insertions(+), 1 deletion(-)
create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs
create mode 100644 src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs
create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs
diff --git a/Document/11_SystemTodo.md b/Document/11_SystemTodo.md
index a18b103..02510ca 100644
--- a/Document/11_SystemTodo.md
+++ b/Document/11_SystemTodo.md
@@ -31,7 +31,7 @@
- [ ] 现状梳理:租户解析/过滤已具备(TenantResolutionMiddleware、TenantAwareDbContext),JWT 已写入 roles/permissions/tenant_id(JwtTokenService),PermissionAuthorize 已在 Admin API 使用,CurrentUserProfile 含角色/权限/租户;但仅有内嵌 string[] 权限存储,无角色/权限表与洞察查询,Swagger 缺少示例与多租户示例。
- [x] 差距与步骤:
- [x] 增加权限/租户洞察查询(按用户、按租户分页)并确保带 tenant 过滤(TenantAwareDbContext 或 Dapper 参数化)。
- - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。
+ - [ ] 输出可读的角色/权限列表(基于现有种子/配置的只读查询)。【进行中:RBAC1 已落地,待补角色/权限管理 API 与 Swagger 示例】
- [x] 为洞察接口和 /auth/profile 增加 Swagger 示例,包含 tenant_id、roles、permissions,展示 Bearer 示例与租户 Header 示例。
- [ ] 若用 Dapper 读侧,SQL 必须参数化并显式过滤 tenant_id。
- [x] 计划顺序:Step A 设计应用层洞察 DTO/Query;Step B Admin API 只读端点(Authorize/PermissionAuthorize);Step C Swagger 示例扩展;Step D 校验租户过滤与忽略路径配置。
diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs
new file mode 100644
index 0000000..26b8518
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs
@@ -0,0 +1,78 @@
+using System.ComponentModel.DataAnnotations;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Application.Identity.Queries;
+using TakeoutSaaS.Module.Authorization.Attributes;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+
+namespace TakeoutSaaS.AdminApi.Controllers;
+
+///
+/// 权限管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/admin/v{version:apiVersion}/permissions")]
+public sealed class PermissionsController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 分页查询权限。
+ ///
+ ///
+ /// 示例:GET /api/admin/v1/permissions?keyword=order&page=1&pageSize=20
+ ///
+ [HttpGet]
+ [PermissionAuthorize("identity:permission:read")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> Search([FromQuery] SearchPermissionsQuery query, CancellationToken cancellationToken)
+ {
+ var result = await mediator.Send(query, cancellationToken);
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 创建权限。
+ ///
+ [HttpPost]
+ [PermissionAuthorize("identity:permission:create")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Create([FromBody, Required] CreatePermissionCommand command, CancellationToken cancellationToken)
+ {
+ var result = await mediator.Send(command, cancellationToken);
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 更新权限。
+ ///
+ [HttpPut("{permissionId:long}")]
+ [PermissionAuthorize("identity:permission:update")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> Update(long permissionId, [FromBody, Required] UpdatePermissionCommand command, CancellationToken cancellationToken)
+ {
+ command = command with { PermissionId = permissionId };
+ var result = await mediator.Send(command, cancellationToken);
+ return result is null
+ ? ApiResponse.Error(StatusCodes.Status404NotFound, "权限不存在")
+ : ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 删除权限。
+ ///
+ [HttpDelete("{permissionId:long}")]
+ [PermissionAuthorize("identity:permission:delete")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Delete(long permissionId, CancellationToken cancellationToken)
+ {
+ var command = new DeletePermissionCommand { PermissionId = permissionId };
+ var result = await mediator.Send(command, cancellationToken);
+ return ApiResponse.Ok(result);
+ }
+}
diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs
new file mode 100644
index 0000000..a9f7148
--- /dev/null
+++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs
@@ -0,0 +1,93 @@
+using System.ComponentModel.DataAnnotations;
+using MediatR;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Http;
+using Microsoft.AspNetCore.Mvc;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Application.Identity.Queries;
+using TakeoutSaaS.Module.Authorization.Attributes;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Web.Api;
+
+namespace TakeoutSaaS.AdminApi.Controllers;
+
+///
+/// 角色管理。
+///
+[ApiVersion("1.0")]
+[Authorize]
+[Route("api/admin/v{version:apiVersion}/roles")]
+public sealed class RolesController(IMediator mediator) : BaseApiController
+{
+ ///
+ /// 分页查询角色。
+ ///
+ ///
+ /// 示例:
+ /// GET /api/admin/v1/roles?keyword=ops&page=1&pageSize=20
+ /// Header: Authorization: Bearer <JWT> + X-Tenant-Id
+ ///
+ [HttpGet]
+ [PermissionAuthorize("identity:role:read")]
+ [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)]
+ public async Task>> Search([FromQuery] SearchRolesQuery query, CancellationToken cancellationToken)
+ {
+ var result = await mediator.Send(query, cancellationToken);
+ return ApiResponse>.Ok(result);
+ }
+
+ ///
+ /// 创建角色。
+ ///
+ [HttpPost]
+ [PermissionAuthorize("identity:role:create")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Create([FromBody, Required] CreateRoleCommand command, CancellationToken cancellationToken)
+ {
+ var result = await mediator.Send(command, cancellationToken);
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 更新角色。
+ ///
+ [HttpPut("{roleId:long}")]
+ [PermissionAuthorize("identity:role:update")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)]
+ public async Task> Update(long roleId, [FromBody, Required] UpdateRoleCommand command, CancellationToken cancellationToken)
+ {
+ command = command with { RoleId = roleId };
+ var result = await mediator.Send(command, cancellationToken);
+ return result is null
+ ? ApiResponse.Error(StatusCodes.Status404NotFound, "角色不存在")
+ : ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 删除角色。
+ ///
+ [HttpDelete("{roleId:long}")]
+ [PermissionAuthorize("identity:role:delete")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> Delete(long roleId, CancellationToken cancellationToken)
+ {
+ var command = new DeleteRoleCommand { RoleId = roleId };
+ var result = await mediator.Send(command, cancellationToken);
+ return ApiResponse.Ok(result);
+ }
+
+ ///
+ /// 绑定角色权限(覆盖式)。
+ ///
+ [HttpPut("{roleId:long}/permissions")]
+ [PermissionAuthorize("identity:role:bind-permission")]
+ [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)]
+ public async Task> BindPermissions(long roleId, [FromBody, Required] BindRolePermissionsCommand command, CancellationToken cancellationToken)
+ {
+ command = command with { RoleId = roleId };
+ var result = await mediator.Send(command, cancellationToken);
+ return ApiResponse.Ok(result);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs
new file mode 100644
index 0000000..14672a3
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/AssignUserRolesCommand.cs
@@ -0,0 +1,12 @@
+using MediatR;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 为用户分配角色(覆盖式)。
+///
+public sealed record AssignUserRolesCommand : IRequest
+{
+ public long UserId { get; init; }
+ public long[] RoleIds { get; init; } = Array.Empty();
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs
new file mode 100644
index 0000000..aec3397
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/BindRolePermissionsCommand.cs
@@ -0,0 +1,12 @@
+using MediatR;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 绑定角色权限(覆盖式)。
+///
+public sealed record BindRolePermissionsCommand : IRequest
+{
+ public long RoleId { get; init; }
+ public long[] PermissionIds { get; init; } = Array.Empty();
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs
new file mode 100644
index 0000000..d554152
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 创建权限。
+///
+public sealed record CreatePermissionCommand : IRequest
+{
+ public string Name { get; init; } = string.Empty;
+ public string Code { get; init; } = string.Empty;
+ public string? Description { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs
new file mode 100644
index 0000000..dadc2a3
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 创建角色。
+///
+public sealed record CreateRoleCommand : IRequest
+{
+ public string Name { get; init; } = string.Empty;
+ public string Code { get; init; } = string.Empty;
+ public string? Description { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs
new file mode 100644
index 0000000..ea91997
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeletePermissionCommand.cs
@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 删除权限。
+///
+public sealed record DeletePermissionCommand : IRequest
+{
+ public long PermissionId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs
new file mode 100644
index 0000000..09085c4
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleCommand.cs
@@ -0,0 +1,11 @@
+using MediatR;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 删除角色。
+///
+public sealed record DeleteRoleCommand : IRequest
+{
+ public long RoleId { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs
new file mode 100644
index 0000000..edcb482
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 更新权限。
+///
+public sealed record UpdatePermissionCommand : IRequest
+{
+ public long PermissionId { get; init; }
+ public string Name { get; init; } = string.Empty;
+ public string? Description { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs
new file mode 100644
index 0000000..b4d58a2
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleCommand.cs
@@ -0,0 +1,14 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+
+namespace TakeoutSaaS.Application.Identity.Commands;
+
+///
+/// 更新角色。
+///
+public sealed record UpdateRoleCommand : IRequest
+{
+ public long RoleId { get; init; }
+ public string Name { get; init; } = string.Empty;
+ public string? Description { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs
new file mode 100644
index 0000000..e9623bd
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs
@@ -0,0 +1,37 @@
+using System.Text.Json.Serialization;
+using TakeoutSaaS.Shared.Abstractions.Serialization;
+
+namespace TakeoutSaaS.Application.Identity.Contracts;
+
+///
+/// 权限 DTO。
+///
+public sealed class PermissionDto
+{
+ ///
+ /// 权限 ID(雪花,序列化为字符串)。
+ ///
+ [JsonConverter(typeof(SnowflakeIdJsonConverter))]
+ public long Id { get; init; }
+
+ ///
+ /// 租户 ID。
+ ///
+ [JsonConverter(typeof(SnowflakeIdJsonConverter))]
+ public long TenantId { get; init; }
+
+ ///
+ /// 权限名称。
+ ///
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ /// 权限编码(租户内唯一)。
+ ///
+ public string Code { get; init; } = string.Empty;
+
+ ///
+ /// 描述。
+ ///
+ public string? Description { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs
new file mode 100644
index 0000000..31119d3
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs
@@ -0,0 +1,37 @@
+using System.Text.Json.Serialization;
+using TakeoutSaaS.Shared.Abstractions.Serialization;
+
+namespace TakeoutSaaS.Application.Identity.Contracts;
+
+///
+/// 角色 DTO。
+///
+public sealed class RoleDto
+{
+ ///
+ /// 角色 ID(雪花,序列化为字符串)。
+ ///
+ [JsonConverter(typeof(SnowflakeIdJsonConverter))]
+ public long Id { get; init; }
+
+ ///
+ /// 租户 ID。
+ ///
+ [JsonConverter(typeof(SnowflakeIdJsonConverter))]
+ public long TenantId { get; init; }
+
+ ///
+ /// 角色名称。
+ ///
+ public string Name { get; init; } = string.Empty;
+
+ ///
+ /// 角色编码(租户内唯一)。
+ ///
+ public string Code { get; init; } = string.Empty;
+
+ ///
+ /// 描述。
+ ///
+ public string? Description { get; init; }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs
new file mode 100644
index 0000000..8105f69
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs
@@ -0,0 +1,23 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 用户角色分配处理器。
+///
+public sealed class AssignUserRolesCommandHandler(
+ IUserRoleRepository userRoleRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(AssignUserRolesCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ await userRoleRepository.ReplaceUserRolesAsync(tenantId, request.UserId, request.RoleIds, cancellationToken);
+ await userRoleRepository.SaveChangesAsync(cancellationToken);
+ return true;
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs
new file mode 100644
index 0000000..eee1e9e
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs
@@ -0,0 +1,23 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 绑定角色权限处理器。
+///
+public sealed class BindRolePermissionsCommandHandler(
+ IRolePermissionRepository rolePermissionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, request.PermissionIds, cancellationToken);
+ await rolePermissionRepository.SaveChangesAsync(cancellationToken);
+ return true;
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs
new file mode 100644
index 0000000..275946e
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs
@@ -0,0 +1,41 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Domain.Identity.Entities;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 创建权限处理器。
+///
+public sealed class CreatePermissionCommandHandler(
+ IPermissionRepository permissionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(CreatePermissionCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var permission = new Permission
+ {
+ TenantId = tenantId,
+ Name = request.Name,
+ Code = request.Code,
+ Description = request.Description
+ };
+
+ await permissionRepository.AddAsync(permission, cancellationToken);
+ await permissionRepository.SaveChangesAsync(cancellationToken);
+
+ return new PermissionDto
+ {
+ Id = permission.Id,
+ TenantId = permission.TenantId,
+ Name = permission.Name,
+ Code = permission.Code,
+ Description = permission.Description
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs
new file mode 100644
index 0000000..717393a
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs
@@ -0,0 +1,41 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Domain.Identity.Entities;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 创建角色处理器。
+///
+public sealed class CreateRoleCommandHandler(
+ IRoleRepository roleRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(CreateRoleCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var role = new Role
+ {
+ TenantId = tenantId,
+ Name = request.Name,
+ Code = request.Code,
+ Description = request.Description
+ };
+
+ await roleRepository.AddAsync(role, cancellationToken);
+ await roleRepository.SaveChangesAsync(cancellationToken);
+
+ return new RoleDto
+ {
+ Id = role.Id,
+ TenantId = role.TenantId,
+ Name = role.Name,
+ Code = role.Code,
+ Description = role.Description
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs
new file mode 100644
index 0000000..9dc2ce8
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs
@@ -0,0 +1,23 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 删除权限处理器。
+///
+public sealed class DeletePermissionCommandHandler(
+ IPermissionRepository permissionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(DeletePermissionCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ await permissionRepository.DeleteAsync(request.PermissionId, tenantId, cancellationToken);
+ await permissionRepository.SaveChangesAsync(cancellationToken);
+ return true;
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs
new file mode 100644
index 0000000..c45241a
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs
@@ -0,0 +1,23 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 删除角色处理器。
+///
+public sealed class DeleteRoleCommandHandler(
+ IRoleRepository roleRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(DeleteRoleCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken);
+ await roleRepository.SaveChangesAsync(cancellationToken);
+ return true;
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs
new file mode 100644
index 0000000..97bdd1b
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs
@@ -0,0 +1,54 @@
+using System;
+using System.Linq;
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Application.Identity.Queries;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 权限分页查询处理器。
+///
+public sealed class SearchPermissionsQueryHandler(
+ IPermissionRepository permissionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler>
+{
+ public async Task> Handle(SearchPermissionsQuery request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
+
+ var sorted = request.SortBy?.ToLowerInvariant() switch
+ {
+ "name" => request.SortDescending
+ ? permissions.OrderByDescending(x => x.Name)
+ : permissions.OrderBy(x => x.Name),
+ "code" => request.SortDescending
+ ? permissions.OrderByDescending(x => x.Code)
+ : permissions.OrderBy(x => x.Code),
+ _ => request.SortDescending
+ ? permissions.OrderByDescending(x => x.CreatedAt)
+ : permissions.OrderBy(x => x.CreatedAt)
+ };
+
+ var paged = sorted
+ .Skip((request.Page - 1) * request.PageSize)
+ .Take(request.PageSize)
+ .ToList();
+
+ var items = paged.Select(permission => new PermissionDto
+ {
+ Id = permission.Id,
+ TenantId = permission.TenantId,
+ Name = permission.Name,
+ Code = permission.Code,
+ Description = permission.Description
+ }).ToList();
+
+ return new PagedResult(items, request.Page, request.PageSize, permissions.Count);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs
new file mode 100644
index 0000000..bd11a5d
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Linq;
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Application.Identity.Queries;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Results;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 角色分页查询处理器。
+///
+public sealed class SearchRolesQueryHandler(
+ IRoleRepository roleRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler>
+{
+ public async Task> Handle(SearchRolesQuery request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var roles = await roleRepository.SearchAsync(tenantId, request.Keyword, cancellationToken);
+
+ var sorted = request.SortBy?.ToLowerInvariant() switch
+ {
+ "name" => request.SortDescending
+ ? roles.OrderByDescending(x => x.Name)
+ : roles.OrderBy(x => x.Name),
+ _ => request.SortDescending
+ ? roles.OrderByDescending(x => x.CreatedAt)
+ : roles.OrderBy(x => x.CreatedAt)
+ };
+
+ var paged = sorted
+ .Skip((request.Page - 1) * request.PageSize)
+ .Take(request.PageSize)
+ .ToList();
+
+ var items = paged.Select(role => new RoleDto
+ {
+ Id = role.Id,
+ TenantId = role.TenantId,
+ Name = role.Name,
+ Code = role.Code,
+ Description = role.Description
+ }).ToList();
+
+ return new PagedResult(items, request.Page, request.PageSize, roles.Count);
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs
new file mode 100644
index 0000000..b123164
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs
@@ -0,0 +1,41 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 更新权限处理器。
+///
+public sealed class UpdatePermissionCommandHandler(
+ IPermissionRepository permissionRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(UpdatePermissionCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var permission = await permissionRepository.FindByIdAsync(request.PermissionId, tenantId, cancellationToken);
+ if (permission == null)
+ {
+ return null;
+ }
+
+ permission.Name = request.Name;
+ permission.Description = request.Description;
+
+ await permissionRepository.UpdateAsync(permission, cancellationToken);
+ await permissionRepository.SaveChangesAsync(cancellationToken);
+
+ return new PermissionDto
+ {
+ Id = permission.Id,
+ TenantId = permission.TenantId,
+ Name = permission.Name,
+ Code = permission.Code,
+ Description = permission.Description
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs
new file mode 100644
index 0000000..c9b6a2d
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs
@@ -0,0 +1,41 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Commands;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Domain.Identity.Repositories;
+using TakeoutSaaS.Shared.Abstractions.Tenancy;
+
+namespace TakeoutSaaS.Application.Identity.Handlers;
+
+///
+/// 更新角色处理器。
+///
+public sealed class UpdateRoleCommandHandler(
+ IRoleRepository roleRepository,
+ ITenantProvider tenantProvider)
+ : IRequestHandler
+{
+ public async Task Handle(UpdateRoleCommand request, CancellationToken cancellationToken)
+ {
+ var tenantId = tenantProvider.GetCurrentTenantId();
+ var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken);
+ if (role == null)
+ {
+ return null;
+ }
+
+ role.Name = request.Name;
+ role.Description = request.Description;
+
+ await roleRepository.UpdateAsync(role, cancellationToken);
+ await roleRepository.SaveChangesAsync(cancellationToken);
+
+ return new RoleDto
+ {
+ Id = role.Id,
+ TenantId = role.TenantId,
+ Name = role.Name,
+ Code = role.Code,
+ Description = role.Description
+ };
+ }
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs
new file mode 100644
index 0000000..8547d6e
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs
@@ -0,0 +1,17 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Shared.Abstractions.Results;
+
+namespace TakeoutSaaS.Application.Identity.Queries;
+
+///
+/// 分页查询权限。
+///
+public sealed class SearchPermissionsQuery : IRequest>
+{
+ public string? Keyword { get; init; }
+ public int Page { get; init; } = 1;
+ public int PageSize { get; init; } = 20;
+ public string? SortBy { get; init; }
+ public bool SortDescending { get; init; } = true;
+}
diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs
new file mode 100644
index 0000000..c4160a2
--- /dev/null
+++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchRolesQuery.cs
@@ -0,0 +1,17 @@
+using MediatR;
+using TakeoutSaaS.Application.Identity.Contracts;
+using TakeoutSaaS.Shared.Abstractions.Results;
+
+namespace TakeoutSaaS.Application.Identity.Queries;
+
+///
+/// 分页查询角色。
+///
+public sealed class SearchRolesQuery : IRequest>
+{
+ public string? Keyword { get; init; }
+ public int Page { get; init; } = 1;
+ public int PageSize { get; init; } = 20;
+ public string? SortBy { get; init; }
+ public bool SortDescending { get; init; } = true;
+}