feat: 增加角色/权限管理 API 与应用层命令

This commit is contained in:
2025-12-02 16:43:46 +08:00
parent b459c7edbe
commit 35b12fb054
25 changed files with 743 additions and 1 deletions

View File

@@ -0,0 +1,12 @@
using MediatR;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 为用户分配角色(覆盖式)。
/// </summary>
public sealed record AssignUserRolesCommand : IRequest<bool>
{
public long UserId { get; init; }
public long[] RoleIds { get; init; } = Array.Empty<long>();
}

View File

@@ -0,0 +1,12 @@
using MediatR;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 绑定角色权限(覆盖式)。
/// </summary>
public sealed record BindRolePermissionsCommand : IRequest<bool>
{
public long RoleId { get; init; }
public long[] PermissionIds { get; init; } = Array.Empty<long>();
}

View File

@@ -0,0 +1,14 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 创建权限。
/// </summary>
public sealed record CreatePermissionCommand : IRequest<PermissionDto>
{
public string Name { get; init; } = string.Empty;
public string Code { get; init; } = string.Empty;
public string? Description { get; init; }
}

View File

@@ -0,0 +1,14 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 创建角色。
/// </summary>
public sealed record CreateRoleCommand : IRequest<RoleDto>
{
public string Name { get; init; } = string.Empty;
public string Code { get; init; } = string.Empty;
public string? Description { get; init; }
}

View File

@@ -0,0 +1,11 @@
using MediatR;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 删除权限。
/// </summary>
public sealed record DeletePermissionCommand : IRequest<bool>
{
public long PermissionId { get; init; }
}

View File

@@ -0,0 +1,11 @@
using MediatR;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 删除角色。
/// </summary>
public sealed record DeleteRoleCommand : IRequest<bool>
{
public long RoleId { get; init; }
}

View File

@@ -0,0 +1,14 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 更新权限。
/// </summary>
public sealed record UpdatePermissionCommand : IRequest<PermissionDto?>
{
public long PermissionId { get; init; }
public string Name { get; init; } = string.Empty;
public string? Description { get; init; }
}

View File

@@ -0,0 +1,14 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
namespace TakeoutSaaS.Application.Identity.Commands;
/// <summary>
/// 更新角色。
/// </summary>
public sealed record UpdateRoleCommand : IRequest<RoleDto?>
{
public long RoleId { get; init; }
public string Name { get; init; } = string.Empty;
public string? Description { get; init; }
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.Identity.Contracts;
/// <summary>
/// 权限 DTO。
/// </summary>
public sealed class PermissionDto
{
/// <summary>
/// 权限 ID雪花序列化为字符串
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 权限名称。
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// 权限编码(租户内唯一)。
/// </summary>
public string Code { get; init; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; init; }
}

View File

@@ -0,0 +1,37 @@
using System.Text.Json.Serialization;
using TakeoutSaaS.Shared.Abstractions.Serialization;
namespace TakeoutSaaS.Application.Identity.Contracts;
/// <summary>
/// 角色 DTO。
/// </summary>
public sealed class RoleDto
{
/// <summary>
/// 角色 ID雪花序列化为字符串
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long Id { get; init; }
/// <summary>
/// 租户 ID。
/// </summary>
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
public long TenantId { get; init; }
/// <summary>
/// 角色名称。
/// </summary>
public string Name { get; init; } = string.Empty;
/// <summary>
/// 角色编码(租户内唯一)。
/// </summary>
public string Code { get; init; } = string.Empty;
/// <summary>
/// 描述。
/// </summary>
public string? Description { get; init; }
}

View File

@@ -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;
/// <summary>
/// 用户角色分配处理器。
/// </summary>
public sealed class AssignUserRolesCommandHandler(
IUserRoleRepository userRoleRepository,
ITenantProvider tenantProvider)
: IRequestHandler<AssignUserRolesCommand, bool>
{
public async Task<bool> Handle(AssignUserRolesCommand request, CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
await userRoleRepository.ReplaceUserRolesAsync(tenantId, request.UserId, request.RoleIds, cancellationToken);
await userRoleRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -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;
/// <summary>
/// 绑定角色权限处理器。
/// </summary>
public sealed class BindRolePermissionsCommandHandler(
IRolePermissionRepository rolePermissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<BindRolePermissionsCommand, bool>
{
public async Task<bool> Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, request.PermissionIds, cancellationToken);
await rolePermissionRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -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;
/// <summary>
/// 创建权限处理器。
/// </summary>
public sealed class CreatePermissionCommandHandler(
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<CreatePermissionCommand, PermissionDto>
{
public async Task<PermissionDto> 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
};
}
}

View File

@@ -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;
/// <summary>
/// 创建角色处理器。
/// </summary>
public sealed class CreateRoleCommandHandler(
IRoleRepository roleRepository,
ITenantProvider tenantProvider)
: IRequestHandler<CreateRoleCommand, RoleDto>
{
public async Task<RoleDto> 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
};
}
}

View File

@@ -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;
/// <summary>
/// 删除权限处理器。
/// </summary>
public sealed class DeletePermissionCommandHandler(
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<DeletePermissionCommand, bool>
{
public async Task<bool> Handle(DeletePermissionCommand request, CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
await permissionRepository.DeleteAsync(request.PermissionId, tenantId, cancellationToken);
await permissionRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -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;
/// <summary>
/// 删除角色处理器。
/// </summary>
public sealed class DeleteRoleCommandHandler(
IRoleRepository roleRepository,
ITenantProvider tenantProvider)
: IRequestHandler<DeleteRoleCommand, bool>
{
public async Task<bool> Handle(DeleteRoleCommand request, CancellationToken cancellationToken)
{
var tenantId = tenantProvider.GetCurrentTenantId();
await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken);
await roleRepository.SaveChangesAsync(cancellationToken);
return true;
}
}

View File

@@ -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;
/// <summary>
/// 权限分页查询处理器。
/// </summary>
public sealed class SearchPermissionsQueryHandler(
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchPermissionsQuery, PagedResult<PermissionDto>>
{
public async Task<PagedResult<PermissionDto>> 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<PermissionDto>(items, request.Page, request.PageSize, permissions.Count);
}
}

View File

@@ -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;
/// <summary>
/// 角色分页查询处理器。
/// </summary>
public sealed class SearchRolesQueryHandler(
IRoleRepository roleRepository,
ITenantProvider tenantProvider)
: IRequestHandler<SearchRolesQuery, PagedResult<RoleDto>>
{
public async Task<PagedResult<RoleDto>> 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<RoleDto>(items, request.Page, request.PageSize, roles.Count);
}
}

View File

@@ -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;
/// <summary>
/// 更新权限处理器。
/// </summary>
public sealed class UpdatePermissionCommandHandler(
IPermissionRepository permissionRepository,
ITenantProvider tenantProvider)
: IRequestHandler<UpdatePermissionCommand, PermissionDto?>
{
public async Task<PermissionDto?> 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
};
}
}

View File

@@ -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;
/// <summary>
/// 更新角色处理器。
/// </summary>
public sealed class UpdateRoleCommandHandler(
IRoleRepository roleRepository,
ITenantProvider tenantProvider)
: IRequestHandler<UpdateRoleCommand, RoleDto?>
{
public async Task<RoleDto?> 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
};
}
}

View File

@@ -0,0 +1,17 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.Identity.Queries;
/// <summary>
/// 分页查询权限。
/// </summary>
public sealed class SearchPermissionsQuery : IRequest<PagedResult<PermissionDto>>
{
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;
}

View File

@@ -0,0 +1,17 @@
using MediatR;
using TakeoutSaaS.Application.Identity.Contracts;
using TakeoutSaaS.Shared.Abstractions.Results;
namespace TakeoutSaaS.Application.Identity.Queries;
/// <summary>
/// 分页查询角色。
/// </summary>
public sealed class SearchRolesQuery : IRequest<PagedResult<RoleDto>>
{
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;
}