feat: 角色模板改为数据库管理支持前端自定义
This commit is contained in:
@@ -30,9 +30,9 @@ public sealed class RolesController(IMediator mediator) : BaseApiController
|
|||||||
[HttpGet("templates")]
|
[HttpGet("templates")]
|
||||||
[PermissionAuthorize("identity:role:read")]
|
[PermissionAuthorize("identity:role:read")]
|
||||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleTemplateDto>>), StatusCodes.Status200OK)]
|
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<RoleTemplateDto>>), StatusCodes.Status200OK)]
|
||||||
public async Task<ApiResponse<IReadOnlyList<RoleTemplateDto>>> ListTemplates(CancellationToken cancellationToken)
|
public async Task<ApiResponse<IReadOnlyList<RoleTemplateDto>>> ListTemplates([FromQuery] bool? isActive, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var result = await mediator.Send(new ListRoleTemplatesQuery(), cancellationToken);
|
var result = await mediator.Send(new ListRoleTemplatesQuery { IsActive = isActive }, cancellationToken);
|
||||||
return ApiResponse<IReadOnlyList<RoleTemplateDto>>.Ok(result);
|
return ApiResponse<IReadOnlyList<RoleTemplateDto>>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,6 +54,49 @@ public sealed class RolesController(IMediator mediator) : BaseApiController
|
|||||||
: ApiResponse<RoleTemplateDto>.Ok(result);
|
: ApiResponse<RoleTemplateDto>.Ok(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建角色模板。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPost("templates")]
|
||||||
|
[PermissionAuthorize("role-template:create")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<RoleTemplateDto>> CreateTemplate([FromBody, Required] CreateRoleTemplateCommand command, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return ApiResponse<RoleTemplateDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新角色模板。
|
||||||
|
/// </summary>
|
||||||
|
[HttpPut("templates/{templateCode}")]
|
||||||
|
[PermissionAuthorize("role-template:update")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status200OK)]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<RoleTemplateDto>), StatusCodes.Status404NotFound)]
|
||||||
|
public async Task<ApiResponse<RoleTemplateDto>> UpdateTemplate(
|
||||||
|
string templateCode,
|
||||||
|
[FromBody, Required] UpdateRoleTemplateCommand command,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
command = command with { TemplateCode = templateCode };
|
||||||
|
var result = await mediator.Send(command, cancellationToken);
|
||||||
|
return result is null
|
||||||
|
? ApiResponse<RoleTemplateDto>.Error(StatusCodes.Status404NotFound, "角色模板不存在")
|
||||||
|
: ApiResponse<RoleTemplateDto>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除角色模板。
|
||||||
|
/// </summary>
|
||||||
|
[HttpDelete("templates/{templateCode}")]
|
||||||
|
[PermissionAuthorize("role-template:delete")]
|
||||||
|
[ProducesResponseType(typeof(ApiResponse<bool>), StatusCodes.Status200OK)]
|
||||||
|
public async Task<ApiResponse<bool>> DeleteTemplate(string templateCode, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var result = await mediator.Send(new DeleteRoleTemplateCommand { TemplateCode = templateCode }, cancellationToken);
|
||||||
|
return ApiResponse<bool>.Ok(result);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 按模板复制角色并绑定权限。
|
/// 按模板复制角色并绑定权限。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -39,6 +39,171 @@
|
|||||||
},
|
},
|
||||||
"Identity": {
|
"Identity": {
|
||||||
"AdminSeed": {
|
"AdminSeed": {
|
||||||
|
"RoleTemplates": [
|
||||||
|
{
|
||||||
|
"TemplateCode": "platform-admin",
|
||||||
|
"Name": "平台管理员",
|
||||||
|
"Description": "平台全量权限",
|
||||||
|
"IsActive": true,
|
||||||
|
"Permissions": [
|
||||||
|
"identity:profile:read",
|
||||||
|
"identity:role:read",
|
||||||
|
"identity:role:create",
|
||||||
|
"identity:role:update",
|
||||||
|
"identity:role:delete",
|
||||||
|
"identity:role:bind-permission",
|
||||||
|
"identity:permission:read",
|
||||||
|
"identity:permission:create",
|
||||||
|
"identity:permission:update",
|
||||||
|
"identity:permission:delete",
|
||||||
|
"role-template:read",
|
||||||
|
"role-template:create",
|
||||||
|
"role-template:update",
|
||||||
|
"role-template:delete",
|
||||||
|
"tenant:create",
|
||||||
|
"tenant:read",
|
||||||
|
"tenant:review",
|
||||||
|
"tenant:subscription",
|
||||||
|
"tenant:quota:check",
|
||||||
|
"tenant-package:read",
|
||||||
|
"tenant-package:create",
|
||||||
|
"tenant-package:update",
|
||||||
|
"tenant-package:delete",
|
||||||
|
"merchant:create",
|
||||||
|
"merchant:read",
|
||||||
|
"merchant:update",
|
||||||
|
"merchant:delete",
|
||||||
|
"merchant:review",
|
||||||
|
"merchant_category:read",
|
||||||
|
"merchant_category:create",
|
||||||
|
"merchant_category:update",
|
||||||
|
"merchant_category:delete",
|
||||||
|
"store:create",
|
||||||
|
"store:read",
|
||||||
|
"store:update",
|
||||||
|
"store:delete",
|
||||||
|
"product:create",
|
||||||
|
"product:read",
|
||||||
|
"product:update",
|
||||||
|
"product:delete",
|
||||||
|
"order:create",
|
||||||
|
"order:read",
|
||||||
|
"order:update",
|
||||||
|
"order:delete",
|
||||||
|
"payment:create",
|
||||||
|
"payment:read",
|
||||||
|
"payment:update",
|
||||||
|
"payment:delete",
|
||||||
|
"delivery:create",
|
||||||
|
"delivery:read",
|
||||||
|
"delivery:update",
|
||||||
|
"delivery:delete",
|
||||||
|
"dictionary:group:read",
|
||||||
|
"dictionary:group:create",
|
||||||
|
"dictionary:group:update",
|
||||||
|
"dictionary:group:delete",
|
||||||
|
"dictionary:item:create",
|
||||||
|
"dictionary:item:update",
|
||||||
|
"dictionary:item:delete",
|
||||||
|
"system-parameter:create",
|
||||||
|
"system-parameter:read",
|
||||||
|
"system-parameter:update",
|
||||||
|
"system-parameter:delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TemplateCode": "tenant-admin",
|
||||||
|
"Name": "租户管理员",
|
||||||
|
"Description": "管理本租户的门店、商品、订单与权限",
|
||||||
|
"IsActive": true,
|
||||||
|
"Permissions": [
|
||||||
|
"identity:profile:read",
|
||||||
|
"identity:role:read",
|
||||||
|
"identity:role:create",
|
||||||
|
"identity:role:update",
|
||||||
|
"identity:role:delete",
|
||||||
|
"identity:role:bind-permission",
|
||||||
|
"identity:permission:read",
|
||||||
|
"identity:permission:create",
|
||||||
|
"identity:permission:update",
|
||||||
|
"identity:permission:delete",
|
||||||
|
"tenant:read",
|
||||||
|
"tenant:subscription",
|
||||||
|
"tenant:quota:check",
|
||||||
|
"merchant:read",
|
||||||
|
"merchant:update",
|
||||||
|
"merchant_category:read",
|
||||||
|
"merchant_category:create",
|
||||||
|
"merchant_category:update",
|
||||||
|
"merchant_category:delete",
|
||||||
|
"store:create",
|
||||||
|
"store:read",
|
||||||
|
"store:update",
|
||||||
|
"store:delete",
|
||||||
|
"product:create",
|
||||||
|
"product:read",
|
||||||
|
"product:update",
|
||||||
|
"product:delete",
|
||||||
|
"order:create",
|
||||||
|
"order:read",
|
||||||
|
"order:update",
|
||||||
|
"delivery:create",
|
||||||
|
"delivery:read",
|
||||||
|
"delivery:update",
|
||||||
|
"payment:create",
|
||||||
|
"payment:read",
|
||||||
|
"payment:update",
|
||||||
|
"dictionary:group:read",
|
||||||
|
"dictionary:group:create",
|
||||||
|
"dictionary:group:update",
|
||||||
|
"dictionary:group:delete",
|
||||||
|
"dictionary:item:create",
|
||||||
|
"dictionary:item:update",
|
||||||
|
"dictionary:item:delete",
|
||||||
|
"system-parameter:read"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TemplateCode": "store-manager",
|
||||||
|
"Name": "店长",
|
||||||
|
"Description": "负责门店运营与商品、订单管理",
|
||||||
|
"IsActive": true,
|
||||||
|
"Permissions": [
|
||||||
|
"identity:profile:read",
|
||||||
|
"store:read",
|
||||||
|
"store:update",
|
||||||
|
"product:create",
|
||||||
|
"product:read",
|
||||||
|
"product:update",
|
||||||
|
"order:create",
|
||||||
|
"order:read",
|
||||||
|
"order:update",
|
||||||
|
"delivery:read",
|
||||||
|
"delivery:update",
|
||||||
|
"payment:read",
|
||||||
|
"payment:update",
|
||||||
|
"dictionary:group:read",
|
||||||
|
"dictionary:item:create",
|
||||||
|
"dictionary:item:update",
|
||||||
|
"dictionary:item:delete"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"TemplateCode": "store-staff",
|
||||||
|
"Name": "店员",
|
||||||
|
"Description": "处理订单履约与收款查询",
|
||||||
|
"IsActive": true,
|
||||||
|
"Permissions": [
|
||||||
|
"identity:profile:read",
|
||||||
|
"store:read",
|
||||||
|
"product:read",
|
||||||
|
"order:read",
|
||||||
|
"order:update",
|
||||||
|
"delivery:read",
|
||||||
|
"payment:read"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
"Users": [
|
"Users": [
|
||||||
{
|
{
|
||||||
"Account": "admin",
|
"Account": "admin",
|
||||||
@@ -47,17 +212,69 @@
|
|||||||
"TenantId": 1000000000001,
|
"TenantId": 1000000000001,
|
||||||
"Roles": [ "PlatformAdmin" ],
|
"Roles": [ "PlatformAdmin" ],
|
||||||
"Permissions": [
|
"Permissions": [
|
||||||
"merchant:*",
|
"identity:profile:read",
|
||||||
"merchant_category:*",
|
"identity:role:read",
|
||||||
|
"identity:role:create",
|
||||||
|
"identity:role:update",
|
||||||
|
"identity:role:delete",
|
||||||
|
"identity:role:bind-permission",
|
||||||
|
"identity:permission:read",
|
||||||
|
"identity:permission:create",
|
||||||
|
"identity:permission:update",
|
||||||
|
"identity:permission:delete",
|
||||||
|
"role-template:read",
|
||||||
|
"role-template:create",
|
||||||
|
"role-template:update",
|
||||||
|
"role-template:delete",
|
||||||
|
"tenant:create",
|
||||||
|
"tenant:read",
|
||||||
|
"tenant:review",
|
||||||
|
"tenant:subscription",
|
||||||
|
"tenant:quota:check",
|
||||||
|
"tenant-package:read",
|
||||||
|
"tenant-package:create",
|
||||||
|
"tenant-package:update",
|
||||||
|
"tenant-package:delete",
|
||||||
|
"merchant:create",
|
||||||
|
"merchant:read",
|
||||||
|
"merchant:update",
|
||||||
|
"merchant:delete",
|
||||||
|
"merchant:review",
|
||||||
"merchant_category:read",
|
"merchant_category:read",
|
||||||
"merchant_category:create",
|
"merchant_category:create",
|
||||||
"merchant_category:update",
|
"merchant_category:update",
|
||||||
"merchant_category:delete",
|
"merchant_category:delete",
|
||||||
"store:*",
|
"store:create",
|
||||||
"product:*",
|
"store:read",
|
||||||
"order:*",
|
"store:update",
|
||||||
"payment:*",
|
"store:delete",
|
||||||
"delivery:*"
|
"product:create",
|
||||||
|
"product:read",
|
||||||
|
"product:update",
|
||||||
|
"product:delete",
|
||||||
|
"order:create",
|
||||||
|
"order:read",
|
||||||
|
"order:update",
|
||||||
|
"order:delete",
|
||||||
|
"payment:create",
|
||||||
|
"payment:read",
|
||||||
|
"payment:update",
|
||||||
|
"payment:delete",
|
||||||
|
"delivery:create",
|
||||||
|
"delivery:read",
|
||||||
|
"delivery:update",
|
||||||
|
"delivery:delete",
|
||||||
|
"dictionary:group:read",
|
||||||
|
"dictionary:group:create",
|
||||||
|
"dictionary:group:update",
|
||||||
|
"dictionary:group:delete",
|
||||||
|
"dictionary:item:create",
|
||||||
|
"dictionary:item:update",
|
||||||
|
"dictionary:item:delete",
|
||||||
|
"system-parameter:create",
|
||||||
|
"system-parameter:read",
|
||||||
|
"system-parameter:update",
|
||||||
|
"system-parameter:delete"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -1,23 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
using TakeoutSaaS.Application.Identity.Templates;
|
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Abstractions;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 角色模板提供者,用于获取预置模板定义。
|
|
||||||
/// </summary>
|
|
||||||
public interface IRoleTemplateProvider
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 获取全部角色模板定义。
|
|
||||||
/// </summary>
|
|
||||||
/// <returns>模板定义集合。</returns>
|
|
||||||
IReadOnlyList<RoleTemplateDefinition> GetTemplates();
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 根据模板编码查找模板。
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="templateCode">模板编码。</param>
|
|
||||||
/// <returns>模板定义;不存在时返回 null。</returns>
|
|
||||||
RoleTemplateDefinition? FindByCode(string templateCode);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建角色模板命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record CreateRoleTemplateCommand : IRequest<RoleTemplateDto>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板编码。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateCode { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 权限编码集合。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<string> PermissionCodes { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
using MediatR;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除角色模板命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record DeleteRoleTemplateCommand : IRequest<bool>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板编码。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateCode { get; init; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Commands;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新角色模板命令。
|
||||||
|
/// </summary>
|
||||||
|
public sealed record UpdateRoleTemplateCommand : IRequest<RoleTemplateDto?>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板编码(路径参数)。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateCode { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; init; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; init; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 权限编码集合。
|
||||||
|
/// </summary>
|
||||||
|
public IReadOnlyCollection<string> PermissionCodes { get; init; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
@@ -22,6 +22,11 @@ public sealed record RoleTemplateDto
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string? Description { get; init; }
|
public string? Description { get; init; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; init; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 包含的权限定义。
|
/// 包含的权限定义。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
using TakeoutSaaS.Application.Identity.Abstractions;
|
||||||
using TakeoutSaaS.Application.Identity.Services;
|
using TakeoutSaaS.Application.Identity.Services;
|
||||||
using TakeoutSaaS.Application.Identity.Templates;
|
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Extensions;
|
namespace TakeoutSaaS.Application.Identity.Extensions;
|
||||||
|
|
||||||
@@ -18,7 +17,6 @@ public static class IdentityServiceCollectionExtensions
|
|||||||
public static IServiceCollection AddIdentityApplication(this IServiceCollection services, bool enableMiniSupport = false)
|
public static IServiceCollection AddIdentityApplication(this IServiceCollection services, bool enableMiniSupport = false)
|
||||||
{
|
{
|
||||||
services.AddScoped<IAdminAuthService, AdminAuthService>();
|
services.AddScoped<IAdminAuthService, AdminAuthService>();
|
||||||
services.AddSingleton<IRoleTemplateProvider, RoleTemplateProvider>();
|
|
||||||
|
|
||||||
if (enableMiniSupport)
|
if (enableMiniSupport)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
|
||||||
using TakeoutSaaS.Application.Identity.Commands;
|
using TakeoutSaaS.Application.Identity.Commands;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
using TakeoutSaaS.Domain.Identity.Entities;
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
@@ -17,7 +16,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
|||||||
/// 角色模板复制处理器。
|
/// 角色模板复制处理器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class CopyRoleTemplateCommandHandler(
|
public sealed class CopyRoleTemplateCommandHandler(
|
||||||
IRoleTemplateProvider roleTemplateProvider,
|
IRoleTemplateRepository roleTemplateRepository,
|
||||||
IRoleRepository roleRepository,
|
IRoleRepository roleRepository,
|
||||||
IPermissionRepository permissionRepository,
|
IPermissionRepository permissionRepository,
|
||||||
IRolePermissionRepository rolePermissionRepository,
|
IRolePermissionRepository rolePermissionRepository,
|
||||||
@@ -27,9 +26,16 @@ public sealed class CopyRoleTemplateCommandHandler(
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<RoleDto> Handle(CopyRoleTemplateCommand request, CancellationToken cancellationToken)
|
public async Task<RoleDto> Handle(CopyRoleTemplateCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var template = roleTemplateProvider.FindByCode(request.TemplateCode)
|
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken)
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在");
|
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {request.TemplateCode} 不存在");
|
||||||
|
|
||||||
|
var templatePermissions = await roleTemplateRepository.GetPermissionsAsync(template.Id, cancellationToken);
|
||||||
|
var permissionCodes = templatePermissions
|
||||||
|
.Select(x => x.PermissionCode)
|
||||||
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
var tenantId = tenantProvider.GetCurrentTenantId();
|
var tenantId = tenantProvider.GetCurrentTenantId();
|
||||||
var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim();
|
var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim();
|
||||||
var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim();
|
var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim();
|
||||||
@@ -64,18 +70,12 @@ public sealed class CopyRoleTemplateCommandHandler(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 2. 确保模板权限全部存在,不存在则按模板定义创建。
|
// 2. 确保模板权限全部存在,不存在则按模板定义创建。
|
||||||
var targetPermissionCodes = template.Permissions
|
var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, permissionCodes, cancellationToken);
|
||||||
.Select(permission => permission.Code)
|
|
||||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToArray();
|
|
||||||
|
|
||||||
var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, targetPermissionCodes, cancellationToken);
|
|
||||||
var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
foreach (var permissionDefinition in template.Permissions)
|
foreach (var code in permissionCodes)
|
||||||
{
|
{
|
||||||
if (permissionMap.ContainsKey(permissionDefinition.Code))
|
if (permissionMap.ContainsKey(code))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -83,13 +83,13 @@ public sealed class CopyRoleTemplateCommandHandler(
|
|||||||
var permission = new Permission
|
var permission = new Permission
|
||||||
{
|
{
|
||||||
TenantId = tenantId,
|
TenantId = tenantId,
|
||||||
Name = permissionDefinition.Name,
|
Name = code,
|
||||||
Code = permissionDefinition.Code,
|
Code = code,
|
||||||
Description = permissionDefinition.Description
|
Description = code
|
||||||
};
|
};
|
||||||
|
|
||||||
await permissionRepository.AddAsync(permission, cancellationToken);
|
await permissionRepository.AddAsync(permission, cancellationToken);
|
||||||
permissionMap[permissionDefinition.Code] = permission;
|
permissionMap[code] = permission;
|
||||||
}
|
}
|
||||||
|
|
||||||
await roleRepository.SaveChangesAsync(cancellationToken);
|
await roleRepository.SaveChangesAsync(cancellationToken);
|
||||||
@@ -100,7 +100,7 @@ public sealed class CopyRoleTemplateCommandHandler(
|
|||||||
.Select(x => x.PermissionId)
|
.Select(x => x.PermissionId)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
var targetPermissionIds = targetPermissionCodes
|
var targetPermissionIds = permissionCodes
|
||||||
.Select(code => permissionMap[code].Id)
|
.Select(code => permissionMap[code].Id)
|
||||||
.ToHashSet();
|
.ToHashSet();
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
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.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 创建角色模板处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class CreateRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository)
|
||||||
|
: IRequestHandler<CreateRoleTemplateCommand, RoleTemplateDto>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<RoleTemplateDto> Handle(CreateRoleTemplateCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.TemplateCode) || string.IsNullOrWhiteSpace(request.Name))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "模板编码与名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
|
||||||
|
if (existing != null)
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.Conflict, $"模板编码 {request.TemplateCode} 已存在");
|
||||||
|
}
|
||||||
|
|
||||||
|
var template = new RoleTemplate
|
||||||
|
{
|
||||||
|
TemplateCode = request.TemplateCode.Trim(),
|
||||||
|
Name = request.Name.Trim(),
|
||||||
|
Description = request.Description,
|
||||||
|
IsActive = request.IsActive
|
||||||
|
};
|
||||||
|
|
||||||
|
var permissions = request.PermissionCodes
|
||||||
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
|
.Select(code => code.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await roleTemplateRepository.AddAsync(template, permissions, cancellationToken);
|
||||||
|
await roleTemplateRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(template, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Commands;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 删除角色模板处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class DeleteRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository)
|
||||||
|
: IRequestHandler<DeleteRoleTemplateCommand, bool>
|
||||||
|
{
|
||||||
|
public async Task<bool> Handle(DeleteRoleTemplateCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
|
||||||
|
if (template == null)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await roleTemplateRepository.DeleteAsync(template.Id, cancellationToken);
|
||||||
|
await roleTemplateRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,20 +1,28 @@
|
|||||||
|
using System.Linq;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
using TakeoutSaaS.Application.Identity.Queries;
|
using TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 角色模板详情查询处理器。
|
/// 角色模板详情查询处理器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class GetRoleTemplateQueryHandler(IRoleTemplateProvider roleTemplateProvider)
|
public sealed class GetRoleTemplateQueryHandler(IRoleTemplateRepository roleTemplateRepository)
|
||||||
: IRequestHandler<GetRoleTemplateQuery, RoleTemplateDto?>
|
: IRequestHandler<GetRoleTemplateQuery, RoleTemplateDto?>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<RoleTemplateDto?> Handle(GetRoleTemplateQuery request, CancellationToken cancellationToken)
|
public async Task<RoleTemplateDto?> Handle(GetRoleTemplateQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var template = roleTemplateProvider.FindByCode(request.TemplateCode);
|
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
|
||||||
return Task.FromResult(template is null ? null : TemplateMapper.ToDto(template));
|
if (template == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissions = await roleTemplateRepository.GetPermissionsAsync(template.Id, cancellationToken);
|
||||||
|
var codes = permissions.Select(x => x.PermissionCode).ToArray();
|
||||||
|
return TemplateMapper.ToDto(template, codes);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
|
||||||
using TakeoutSaaS.Application.Identity.Commands;
|
using TakeoutSaaS.Application.Identity.Commands;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Constants;
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
@@ -14,39 +14,47 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
|||||||
/// 租户角色模板批量初始化处理器。
|
/// 租户角色模板批量初始化处理器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class InitializeRoleTemplatesCommandHandler(
|
public sealed class InitializeRoleTemplatesCommandHandler(
|
||||||
IRoleTemplateProvider roleTemplateProvider,
|
IRoleTemplateRepository roleTemplateRepository,
|
||||||
IMediator mediator)
|
IMediator mediator)
|
||||||
: IRequestHandler<InitializeRoleTemplatesCommand, IReadOnlyList<RoleDto>>
|
: IRequestHandler<InitializeRoleTemplatesCommand, IReadOnlyList<RoleDto>>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<IReadOnlyList<RoleDto>> Handle(InitializeRoleTemplatesCommand request, CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<RoleDto>> Handle(InitializeRoleTemplatesCommand request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
// 1. 解析需要初始化的模板编码,默认取全部预置模板。
|
// 1. 解析需要初始化的模板编码,默认取全部模板。
|
||||||
var requestedCodes = request.TemplateCodes?
|
var requestedCodes = request.TemplateCodes?
|
||||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
.Select(code => code.Trim())
|
.Select(code => code.Trim())
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
|
var availableTemplates = await roleTemplateRepository.GetAllAsync(true, cancellationToken);
|
||||||
|
var availableCodes = availableTemplates.Select(t => t.TemplateCode).ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
var targetCodes = requestedCodes?.Length > 0
|
var targetCodes = requestedCodes?.Length > 0
|
||||||
? requestedCodes
|
? requestedCodes
|
||||||
: roleTemplateProvider.GetTemplates().Select(template => template.TemplateCode).ToArray();
|
: availableTemplates.Select(template => template.TemplateCode).ToArray();
|
||||||
|
|
||||||
if (targetCodes.Length == 0)
|
if (targetCodes.Length == 0)
|
||||||
{
|
{
|
||||||
return Array.Empty<RoleDto>();
|
return Array.Empty<RoleDto>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var code in targetCodes)
|
||||||
|
{
|
||||||
|
if (!availableCodes.Contains(code))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {code} 不存在或未启用");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// 2. 逐个复制模板,幂等写入角色与权限。
|
// 2. 逐个复制模板,幂等写入角色与权限。
|
||||||
var roles = new List<RoleDto>(targetCodes.Length);
|
var roles = new List<RoleDto>(targetCodes.Length);
|
||||||
foreach (var templateCode in targetCodes)
|
foreach (var templateCode in targetCodes)
|
||||||
{
|
{
|
||||||
var template = roleTemplateProvider.FindByCode(templateCode)
|
|
||||||
?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {templateCode} 不存在");
|
|
||||||
|
|
||||||
var role = await mediator.Send(new CopyRoleTemplateCommand
|
var role = await mediator.Send(new CopyRoleTemplateCommand
|
||||||
{
|
{
|
||||||
TemplateCode = template.TemplateCode
|
TemplateCode = templateCode
|
||||||
}, cancellationToken);
|
}, cancellationToken);
|
||||||
|
|
||||||
roles.Add(role);
|
roles.Add(role);
|
||||||
|
|||||||
@@ -1,26 +1,35 @@
|
|||||||
using System;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using MediatR;
|
using MediatR;
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
using TakeoutSaaS.Application.Identity.Queries;
|
using TakeoutSaaS.Application.Identity.Queries;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 角色模板列表查询处理器。
|
/// 角色模板列表查询处理器。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateProvider roleTemplateProvider)
|
public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateRepository roleTemplateRepository)
|
||||||
: IRequestHandler<ListRoleTemplatesQuery, IReadOnlyList<RoleTemplateDto>>
|
: IRequestHandler<ListRoleTemplatesQuery, IReadOnlyList<RoleTemplateDto>>
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public Task<IReadOnlyList<RoleTemplateDto>> Handle(ListRoleTemplatesQuery request, CancellationToken cancellationToken)
|
public async Task<IReadOnlyList<RoleTemplateDto>> Handle(ListRoleTemplatesQuery request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var templates = roleTemplateProvider.GetTemplates()
|
var templates = await roleTemplateRepository.GetAllAsync(request.IsActive, cancellationToken);
|
||||||
|
var permissionsMap = await roleTemplateRepository.GetPermissionsAsync(templates.Select(t => t.Id), cancellationToken);
|
||||||
|
|
||||||
|
var dtos = templates
|
||||||
.OrderBy(template => template.TemplateCode, StringComparer.OrdinalIgnoreCase)
|
.OrderBy(template => template.TemplateCode, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(TemplateMapper.ToDto)
|
.Select(template =>
|
||||||
|
{
|
||||||
|
var codes = permissionsMap.TryGetValue(template.Id, out var perms)
|
||||||
|
? (IReadOnlyCollection<string>)perms.Select(p => p.PermissionCode).ToArray()
|
||||||
|
: Array.Empty<string>();
|
||||||
|
return TemplateMapper.ToDto(template, codes);
|
||||||
|
})
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
return Task.FromResult<IReadOnlyList<RoleTemplateDto>>(templates);
|
return dtos;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using TakeoutSaaS.Application.Identity.Contracts;
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
using TakeoutSaaS.Application.Identity.Templates;
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Handlers;
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
@@ -10,28 +11,27 @@ namespace TakeoutSaaS.Application.Identity.Handlers;
|
|||||||
internal static class TemplateMapper
|
internal static class TemplateMapper
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 将角色模板定义映射为 DTO。
|
/// 将角色模板与权限编码集合映射为 DTO。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="definition">角色模板定义。</param>
|
/// <param name="template">角色模板实体。</param>
|
||||||
|
/// <param name="permissionCodes">权限编码集合。</param>
|
||||||
/// <returns>模板 DTO。</returns>
|
/// <returns>模板 DTO。</returns>
|
||||||
public static RoleTemplateDto ToDto(RoleTemplateDefinition definition)
|
public static RoleTemplateDto ToDto(RoleTemplate template, IReadOnlyCollection<string> permissionCodes)
|
||||||
{
|
{
|
||||||
return new RoleTemplateDto
|
return new RoleTemplateDto
|
||||||
{
|
{
|
||||||
TemplateCode = definition.TemplateCode,
|
TemplateCode = template.TemplateCode,
|
||||||
Name = definition.Name,
|
Name = template.Name,
|
||||||
Description = definition.Description,
|
Description = template.Description,
|
||||||
Permissions = definition.Permissions.Select(ToDto).ToArray()
|
IsActive = template.IsActive,
|
||||||
};
|
Permissions = permissionCodes
|
||||||
}
|
.Select(code => new PermissionTemplateDto
|
||||||
|
{
|
||||||
private static PermissionTemplateDto ToDto(PermissionTemplateDefinition definition)
|
Code = code,
|
||||||
{
|
Name = code,
|
||||||
return new PermissionTemplateDto
|
Description = null
|
||||||
{
|
})
|
||||||
Code = definition.Code,
|
.ToArray()
|
||||||
Name = definition.Name,
|
|
||||||
Description = definition.Description
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using MediatR;
|
||||||
|
using TakeoutSaaS.Application.Identity.Commands;
|
||||||
|
using TakeoutSaaS.Application.Identity.Contracts;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
||||||
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Application.Identity.Handlers;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 更新角色模板处理器。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class UpdateRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository)
|
||||||
|
: IRequestHandler<UpdateRoleTemplateCommand, RoleTemplateDto?>
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<RoleTemplateDto?> Handle(UpdateRoleTemplateCommand request, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(request.TemplateCode) || string.IsNullOrWhiteSpace(request.Name))
|
||||||
|
{
|
||||||
|
throw new BusinessException(ErrorCodes.BadRequest, "模板编码与名称不能为空");
|
||||||
|
}
|
||||||
|
|
||||||
|
var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken);
|
||||||
|
if (template == null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
template.Name = request.Name.Trim();
|
||||||
|
template.Description = request.Description;
|
||||||
|
template.IsActive = request.IsActive;
|
||||||
|
|
||||||
|
var permissions = request.PermissionCodes
|
||||||
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
|
.Select(code => code.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
await roleTemplateRepository.UpdateAsync(template, permissions, cancellationToken);
|
||||||
|
await roleTemplateRepository.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
return TemplateMapper.ToDto(template, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,4 +7,10 @@ namespace TakeoutSaaS.Application.Identity.Queries;
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// 查询角色模板列表。
|
/// 查询角色模板列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public sealed record ListRoleTemplatesQuery : IRequest<IReadOnlyList<RoleTemplateDto>>;
|
public sealed record ListRoleTemplatesQuery : IRequest<IReadOnlyList<RoleTemplateDto>>
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 是否仅返回启用模板。
|
||||||
|
/// </summary>
|
||||||
|
public bool? IsActive { get; init; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
namespace TakeoutSaaS.Application.Identity.Templates;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 权限模板定义。
|
|
||||||
/// </summary>
|
|
||||||
public sealed record PermissionTemplateDefinition
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 权限编码(唯一键)。
|
|
||||||
/// </summary>
|
|
||||||
public required string Code { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 权限名称。
|
|
||||||
/// </summary>
|
|
||||||
public required string Name { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 权限描述。
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; init; }
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
using System.Collections.Generic;
|
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Templates;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 角色模板定义。
|
|
||||||
/// </summary>
|
|
||||||
public sealed record RoleTemplateDefinition
|
|
||||||
{
|
|
||||||
/// <summary>
|
|
||||||
/// 模板编码(唯一键)。
|
|
||||||
/// </summary>
|
|
||||||
public required string TemplateCode { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 角色名称。
|
|
||||||
/// </summary>
|
|
||||||
public required string Name { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 角色描述。
|
|
||||||
/// </summary>
|
|
||||||
public string? Description { get; init; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 模板绑定的权限集合。
|
|
||||||
/// </summary>
|
|
||||||
public IReadOnlyList<PermissionTemplateDefinition> Permissions { get; init; } = [];
|
|
||||||
}
|
|
||||||
@@ -1,511 +0,0 @@
|
|||||||
using System;
|
|
||||||
using System.Collections.Frozen;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using TakeoutSaaS.Application.Identity.Abstractions;
|
|
||||||
|
|
||||||
namespace TakeoutSaaS.Application.Identity.Templates;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 预置角色模板提供者。
|
|
||||||
/// </summary>
|
|
||||||
public sealed class RoleTemplateProvider : IRoleTemplateProvider
|
|
||||||
{
|
|
||||||
private static readonly FrozenDictionary<string, PermissionTemplateDefinition> PermissionDefinitions =
|
|
||||||
new Dictionary<string, PermissionTemplateDefinition>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
["identity:role:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:role:read",
|
|
||||||
Name = "角色查询",
|
|
||||||
Description = "查看当前租户的角色列表与详情。"
|
|
||||||
},
|
|
||||||
["identity:role:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:role:create",
|
|
||||||
Name = "角色创建",
|
|
||||||
Description = "在当前租户内创建新角色。"
|
|
||||||
},
|
|
||||||
["identity:role:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:role:update",
|
|
||||||
Name = "角色更新",
|
|
||||||
Description = "修改角色名称与描述。"
|
|
||||||
},
|
|
||||||
["identity:role:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:role:delete",
|
|
||||||
Name = "角色删除",
|
|
||||||
Description = "删除租户内的角色记录。"
|
|
||||||
},
|
|
||||||
["identity:role:bind-permission"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:role:bind-permission",
|
|
||||||
Name = "角色权限绑定",
|
|
||||||
Description = "为角色绑定或调整权限集合。"
|
|
||||||
},
|
|
||||||
["identity:permission:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:permission:read",
|
|
||||||
Name = "权限查询",
|
|
||||||
Description = "查看权限列表与详情。"
|
|
||||||
},
|
|
||||||
["identity:permission:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:permission:create",
|
|
||||||
Name = "权限创建",
|
|
||||||
Description = "创建新的权限定义。"
|
|
||||||
},
|
|
||||||
["identity:permission:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:permission:update",
|
|
||||||
Name = "权限更新",
|
|
||||||
Description = "修改权限名称或描述。"
|
|
||||||
},
|
|
||||||
["identity:permission:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:permission:delete",
|
|
||||||
Name = "权限删除",
|
|
||||||
Description = "删除租户内的权限记录。"
|
|
||||||
},
|
|
||||||
["identity:profile:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "identity:profile:read",
|
|
||||||
Name = "个人信息查看",
|
|
||||||
Description = "查看当前登录人的账号与权限信息。"
|
|
||||||
},
|
|
||||||
["tenant:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant:create",
|
|
||||||
Name = "租户创建",
|
|
||||||
Description = "创建新的租户记录。"
|
|
||||||
},
|
|
||||||
["tenant:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant:read",
|
|
||||||
Name = "租户查询",
|
|
||||||
Description = "查看租户列表与详情。"
|
|
||||||
},
|
|
||||||
["tenant:review"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant:review",
|
|
||||||
Name = "租户审核",
|
|
||||||
Description = "审核租户实名与资质信息。"
|
|
||||||
},
|
|
||||||
["tenant:subscription"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant:subscription",
|
|
||||||
Name = "租户订阅管理",
|
|
||||||
Description = "创建、续费或调整租户套餐订阅。"
|
|
||||||
},
|
|
||||||
["tenant:quota:check"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant:quota:check",
|
|
||||||
Name = "租户配额校验",
|
|
||||||
Description = "校验并占用门店、账号、短信、配送等配额。"
|
|
||||||
},
|
|
||||||
["tenant-package:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant-package:read",
|
|
||||||
Name = "套餐查询",
|
|
||||||
Description = "查看平台套餐定义列表与详情。"
|
|
||||||
},
|
|
||||||
["tenant-package:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant-package:create",
|
|
||||||
Name = "套餐创建",
|
|
||||||
Description = "创建平台套餐定义。"
|
|
||||||
},
|
|
||||||
["tenant-package:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant-package:update",
|
|
||||||
Name = "套餐更新",
|
|
||||||
Description = "更新平台套餐定义。"
|
|
||||||
},
|
|
||||||
["tenant-package:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "tenant-package:delete",
|
|
||||||
Name = "套餐删除",
|
|
||||||
Description = "删除平台套餐定义。"
|
|
||||||
},
|
|
||||||
["merchant:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant:create",
|
|
||||||
Name = "商户创建",
|
|
||||||
Description = "创建或提交商户入驻信息。"
|
|
||||||
},
|
|
||||||
["merchant:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant:read",
|
|
||||||
Name = "商户查看",
|
|
||||||
Description = "查看商户资料与审核状态。"
|
|
||||||
},
|
|
||||||
["merchant:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant:update",
|
|
||||||
Name = "商户更新",
|
|
||||||
Description = "更新商户资料或合同等信息。"
|
|
||||||
},
|
|
||||||
["merchant:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant:delete",
|
|
||||||
Name = "商户删除",
|
|
||||||
Description = "删除或作废商户记录。"
|
|
||||||
},
|
|
||||||
["merchant:review"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant:review",
|
|
||||||
Name = "商户审核",
|
|
||||||
Description = "审核商户入驻、合同与证照。"
|
|
||||||
},
|
|
||||||
["merchant_category:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant_category:read",
|
|
||||||
Name = "类目查询",
|
|
||||||
Description = "查看经营类目列表。"
|
|
||||||
},
|
|
||||||
["merchant_category:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant_category:create",
|
|
||||||
Name = "类目创建",
|
|
||||||
Description = "新增经营类目。"
|
|
||||||
},
|
|
||||||
["merchant_category:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant_category:update",
|
|
||||||
Name = "类目更新",
|
|
||||||
Description = "调整经营类目名称或顺序。"
|
|
||||||
},
|
|
||||||
["merchant_category:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "merchant_category:delete",
|
|
||||||
Name = "类目删除",
|
|
||||||
Description = "删除经营类目。"
|
|
||||||
},
|
|
||||||
["store:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "store:create",
|
|
||||||
Name = "门店创建",
|
|
||||||
Description = "创建新的门店记录。"
|
|
||||||
},
|
|
||||||
["store:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "store:read",
|
|
||||||
Name = "门店查看",
|
|
||||||
Description = "查看门店列表与详情。"
|
|
||||||
},
|
|
||||||
["store:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "store:update",
|
|
||||||
Name = "门店更新",
|
|
||||||
Description = "更新门店资料与配置。"
|
|
||||||
},
|
|
||||||
["store:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "store:delete",
|
|
||||||
Name = "门店删除",
|
|
||||||
Description = "删除或停用门店。"
|
|
||||||
},
|
|
||||||
["product:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "product:create",
|
|
||||||
Name = "商品创建",
|
|
||||||
Description = "创建商品、菜品或规格。"
|
|
||||||
},
|
|
||||||
["product:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "product:read",
|
|
||||||
Name = "商品查看",
|
|
||||||
Description = "查看商品/菜品列表与详情。"
|
|
||||||
},
|
|
||||||
["product:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "product:update",
|
|
||||||
Name = "商品更新",
|
|
||||||
Description = "更新商品、菜品或上下架状态。"
|
|
||||||
},
|
|
||||||
["product:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "product:delete",
|
|
||||||
Name = "商品删除",
|
|
||||||
Description = "删除商品或菜品。"
|
|
||||||
},
|
|
||||||
["order:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "order:create",
|
|
||||||
Name = "订单创建",
|
|
||||||
Description = "创建订单或手工录单。"
|
|
||||||
},
|
|
||||||
["order:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "order:read",
|
|
||||||
Name = "订单查看",
|
|
||||||
Description = "查看订单列表与详情。"
|
|
||||||
},
|
|
||||||
["order:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "order:update",
|
|
||||||
Name = "订单更新",
|
|
||||||
Description = "修改订单状态或履约信息。"
|
|
||||||
},
|
|
||||||
["order:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "order:delete",
|
|
||||||
Name = "订单删除",
|
|
||||||
Description = "删除或作废订单。"
|
|
||||||
},
|
|
||||||
["delivery:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "delivery:create",
|
|
||||||
Name = "配送创建",
|
|
||||||
Description = "创建或发起配送单。"
|
|
||||||
},
|
|
||||||
["delivery:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "delivery:read",
|
|
||||||
Name = "配送查看",
|
|
||||||
Description = "查看配送订单与轨迹。"
|
|
||||||
},
|
|
||||||
["delivery:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "delivery:update",
|
|
||||||
Name = "配送更新",
|
|
||||||
Description = "更新配送状态或骑手信息。"
|
|
||||||
},
|
|
||||||
["delivery:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "delivery:delete",
|
|
||||||
Name = "配送删除",
|
|
||||||
Description = "取消或删除配送单。"
|
|
||||||
},
|
|
||||||
["payment:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "payment:create",
|
|
||||||
Name = "支付创建",
|
|
||||||
Description = "创建收款或支付记录。"
|
|
||||||
},
|
|
||||||
["payment:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "payment:read",
|
|
||||||
Name = "支付查看",
|
|
||||||
Description = "查看支付、退款记录。"
|
|
||||||
},
|
|
||||||
["payment:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "payment:update",
|
|
||||||
Name = "支付更新",
|
|
||||||
Description = "更新支付状态或补充信息。"
|
|
||||||
},
|
|
||||||
["payment:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "payment:delete",
|
|
||||||
Name = "支付删除",
|
|
||||||
Description = "删除或作废支付记录。"
|
|
||||||
},
|
|
||||||
["dictionary:group:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:group:read",
|
|
||||||
Name = "字典分组查询",
|
|
||||||
Description = "查看字典分组与明细。"
|
|
||||||
},
|
|
||||||
["dictionary:group:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:group:create",
|
|
||||||
Name = "字典分组创建",
|
|
||||||
Description = "新增字典分组。"
|
|
||||||
},
|
|
||||||
["dictionary:group:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:group:update",
|
|
||||||
Name = "字典分组更新",
|
|
||||||
Description = "修改字典分组信息。"
|
|
||||||
},
|
|
||||||
["dictionary:group:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:group:delete",
|
|
||||||
Name = "字典分组删除",
|
|
||||||
Description = "删除字典分组。"
|
|
||||||
},
|
|
||||||
["dictionary:item:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:item:create",
|
|
||||||
Name = "字典项创建",
|
|
||||||
Description = "新增字典项。"
|
|
||||||
},
|
|
||||||
["dictionary:item:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:item:update",
|
|
||||||
Name = "字典项更新",
|
|
||||||
Description = "调整字典项内容。"
|
|
||||||
},
|
|
||||||
["dictionary:item:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "dictionary:item:delete",
|
|
||||||
Name = "字典项删除",
|
|
||||||
Description = "删除字典项。"
|
|
||||||
},
|
|
||||||
["system-parameter:create"] = new()
|
|
||||||
{
|
|
||||||
Code = "system-parameter:create",
|
|
||||||
Name = "系统参数创建",
|
|
||||||
Description = "新增系统参数配置。"
|
|
||||||
},
|
|
||||||
["system-parameter:read"] = new()
|
|
||||||
{
|
|
||||||
Code = "system-parameter:read",
|
|
||||||
Name = "系统参数查询",
|
|
||||||
Description = "查看系统参数列表与详情。"
|
|
||||||
},
|
|
||||||
["system-parameter:update"] = new()
|
|
||||||
{
|
|
||||||
Code = "system-parameter:update",
|
|
||||||
Name = "系统参数更新",
|
|
||||||
Description = "更新系统参数配置。"
|
|
||||||
},
|
|
||||||
["system-parameter:delete"] = new()
|
|
||||||
{
|
|
||||||
Code = "system-parameter:delete",
|
|
||||||
Name = "系统参数删除",
|
|
||||||
Description = "删除系统参数配置。"
|
|
||||||
}
|
|
||||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
private static readonly FrozenDictionary<string, RoleTemplateDefinition> Templates =
|
|
||||||
new Dictionary<string, RoleTemplateDefinition>(StringComparer.OrdinalIgnoreCase)
|
|
||||||
{
|
|
||||||
["platform-admin"] = new()
|
|
||||||
{
|
|
||||||
TemplateCode = "platform-admin",
|
|
||||||
Name = "平台管理员",
|
|
||||||
Description = "平台全量权限,负责租户、商户、配置与运维。",
|
|
||||||
Permissions = BuildPermissions(PermissionDefinitions.Keys)
|
|
||||||
},
|
|
||||||
["tenant-admin"] = new()
|
|
||||||
{
|
|
||||||
TemplateCode = "tenant-admin",
|
|
||||||
Name = "租户管理员",
|
|
||||||
Description = "管理本租户的门店、商品、订单与团队权限。",
|
|
||||||
Permissions = BuildPermissions(new[]
|
|
||||||
{
|
|
||||||
"identity:profile:read",
|
|
||||||
"identity:role:read",
|
|
||||||
"identity:role:create",
|
|
||||||
"identity:role:update",
|
|
||||||
"identity:role:delete",
|
|
||||||
"identity:role:bind-permission",
|
|
||||||
"identity:permission:read",
|
|
||||||
"identity:permission:create",
|
|
||||||
"identity:permission:update",
|
|
||||||
"identity:permission:delete",
|
|
||||||
"tenant:read",
|
|
||||||
"tenant:subscription",
|
|
||||||
"tenant:quota:check",
|
|
||||||
"merchant:read",
|
|
||||||
"merchant:update",
|
|
||||||
"merchant_category:read",
|
|
||||||
"merchant_category:create",
|
|
||||||
"merchant_category:update",
|
|
||||||
"merchant_category:delete",
|
|
||||||
"store:create",
|
|
||||||
"store:read",
|
|
||||||
"store:update",
|
|
||||||
"store:delete",
|
|
||||||
"product:create",
|
|
||||||
"product:read",
|
|
||||||
"product:update",
|
|
||||||
"product:delete",
|
|
||||||
"order:create",
|
|
||||||
"order:read",
|
|
||||||
"order:update",
|
|
||||||
"delivery:create",
|
|
||||||
"delivery:read",
|
|
||||||
"delivery:update",
|
|
||||||
"payment:create",
|
|
||||||
"payment:read",
|
|
||||||
"payment:update",
|
|
||||||
"dictionary:group:read",
|
|
||||||
"dictionary:group:create",
|
|
||||||
"dictionary:group:update",
|
|
||||||
"dictionary:group:delete",
|
|
||||||
"dictionary:item:create",
|
|
||||||
"dictionary:item:update",
|
|
||||||
"dictionary:item:delete",
|
|
||||||
"system-parameter:read"
|
|
||||||
})
|
|
||||||
},
|
|
||||||
["store-manager"] = new()
|
|
||||||
{
|
|
||||||
TemplateCode = "store-manager",
|
|
||||||
Name = "店长",
|
|
||||||
Description = "负责门店日常运营、商品与订单管理。",
|
|
||||||
Permissions = BuildPermissions(new[]
|
|
||||||
{
|
|
||||||
"identity:profile:read",
|
|
||||||
"store:read",
|
|
||||||
"store:update",
|
|
||||||
"product:create",
|
|
||||||
"product:read",
|
|
||||||
"product:update",
|
|
||||||
"order:create",
|
|
||||||
"order:read",
|
|
||||||
"order:update",
|
|
||||||
"delivery:read",
|
|
||||||
"delivery:update",
|
|
||||||
"payment:read",
|
|
||||||
"payment:update",
|
|
||||||
"dictionary:group:read",
|
|
||||||
"dictionary:item:create",
|
|
||||||
"dictionary:item:update",
|
|
||||||
"dictionary:item:delete"
|
|
||||||
})
|
|
||||||
},
|
|
||||||
["store-staff"] = new()
|
|
||||||
{
|
|
||||||
TemplateCode = "store-staff",
|
|
||||||
Name = "店员",
|
|
||||||
Description = "处理订单履约、配送跟踪与收款查询。",
|
|
||||||
Permissions = BuildPermissions(new[]
|
|
||||||
{
|
|
||||||
"identity:profile:read",
|
|
||||||
"store:read",
|
|
||||||
"product:read",
|
|
||||||
"order:read",
|
|
||||||
"order:update",
|
|
||||||
"delivery:read",
|
|
||||||
"payment:read"
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}.ToFrozenDictionary(StringComparer.OrdinalIgnoreCase);
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public IReadOnlyList<RoleTemplateDefinition> GetTemplates()
|
|
||||||
=> Templates.Values.ToArray();
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public RoleTemplateDefinition? FindByCode(string templateCode)
|
|
||||||
{
|
|
||||||
if (string.IsNullOrWhiteSpace(templateCode))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Templates.GetValueOrDefault(templateCode);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static IReadOnlyList<PermissionTemplateDefinition> BuildPermissions(IEnumerable<string> codes)
|
|
||||||
{
|
|
||||||
return codes
|
|
||||||
.Where(code => !string.IsNullOrWhiteSpace(code))
|
|
||||||
.Select(code => code.Trim())
|
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
|
||||||
.Select(code => PermissionDefinitions.TryGetValue(code, out var definition)
|
|
||||||
? definition
|
|
||||||
: new PermissionTemplateDefinition
|
|
||||||
{
|
|
||||||
Code = code,
|
|
||||||
Name = code,
|
|
||||||
Description = "未在预置表中定义的权限,请补充描述。"
|
|
||||||
})
|
|
||||||
.ToArray();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板定义(平台级)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RoleTemplate : AuditableEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板编码(唯一)。
|
||||||
|
/// </summary>
|
||||||
|
public string TemplateCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板名称。
|
||||||
|
/// </summary>
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
}
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板-权限关系(平台级)。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RoleTemplatePermission : AuditableEntityBase
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板 ID。
|
||||||
|
/// </summary>
|
||||||
|
public long RoleTemplateId { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 权限编码。
|
||||||
|
/// </summary>
|
||||||
|
public string PermissionCode { get; set; } = string.Empty;
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板仓储。
|
||||||
|
/// </summary>
|
||||||
|
public interface IRoleTemplateRepository
|
||||||
|
{
|
||||||
|
Task<IReadOnlyList<RoleTemplate>> GetAllAsync(bool? isActive, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<RoleTemplate?> FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<IReadOnlyList<RoleTemplatePermission>> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task<IDictionary<long, IReadOnlyList<RoleTemplatePermission>>> GetPermissionsAsync(IEnumerable<long> roleTemplateIds, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task AddAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task UpdateAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task DeleteAsync(long roleTemplateId, CancellationToken cancellationToken = default);
|
||||||
|
|
||||||
|
Task SaveChangesAsync(CancellationToken cancellationToken = default);
|
||||||
|
}
|
||||||
@@ -55,6 +55,7 @@ public static class ServiceCollectionExtensions
|
|||||||
services.AddScoped<IPermissionRepository, EfPermissionRepository>();
|
services.AddScoped<IPermissionRepository, EfPermissionRepository>();
|
||||||
services.AddScoped<IUserRoleRepository, EfUserRoleRepository>();
|
services.AddScoped<IUserRoleRepository, EfUserRoleRepository>();
|
||||||
services.AddScoped<IRolePermissionRepository, EfRolePermissionRepository>();
|
services.AddScoped<IRolePermissionRepository, EfRolePermissionRepository>();
|
||||||
|
services.AddScoped<IRoleTemplateRepository, EfRoleTemplateRepository>();
|
||||||
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
services.AddScoped<IJwtTokenService, JwtTokenService>();
|
||||||
services.AddScoped<IRefreshTokenStore, RedisRefreshTokenStore>();
|
services.AddScoped<IRefreshTokenStore, RedisRefreshTokenStore>();
|
||||||
services.AddScoped<ILoginRateLimiter, RedisLoginRateLimiter>();
|
services.AddScoped<ILoginRateLimiter, RedisLoginRateLimiter>();
|
||||||
|
|||||||
@@ -11,6 +11,11 @@ public sealed class AdminSeedOptions
|
|||||||
/// 初始用户列表。
|
/// 初始用户列表。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public List<SeedUserOptions> Users { get; set; } = new();
|
public List<SeedUserOptions> Users { get; set; } = new();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板种子列表。
|
||||||
|
/// </summary>
|
||||||
|
public List<RoleTemplateSeedOptions> RoleTemplates { get; set; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -56,3 +61,36 @@ public sealed class SeedUserOptions
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public string[] Permissions { get; set; } = Array.Empty<string>();
|
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板种子配置。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class RoleTemplateSeedOptions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// 模板编码。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string TemplateCode { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板名称。
|
||||||
|
/// </summary>
|
||||||
|
[Required]
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 模板描述。
|
||||||
|
/// </summary>
|
||||||
|
public string? Description { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 是否启用。
|
||||||
|
/// </summary>
|
||||||
|
public bool IsActive { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 权限编码集合。
|
||||||
|
/// </summary>
|
||||||
|
public string[] Permissions { get; set; } = Array.Empty<string>();
|
||||||
|
}
|
||||||
|
|||||||
@@ -0,0 +1,117 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Entities;
|
||||||
|
using TakeoutSaaS.Domain.Identity.Repositories;
|
||||||
|
|
||||||
|
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板仓储实现。
|
||||||
|
/// </summary>
|
||||||
|
public sealed class EfRoleTemplateRepository(IdentityDbContext dbContext) : IRoleTemplateRepository
|
||||||
|
{
|
||||||
|
public Task<IReadOnlyList<RoleTemplate>> GetAllAsync(bool? isActive, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var query = dbContext.RoleTemplates.AsNoTracking();
|
||||||
|
if (isActive.HasValue)
|
||||||
|
{
|
||||||
|
query = query.Where(x => x.IsActive == isActive.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return query
|
||||||
|
.OrderBy(x => x.TemplateCode)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.ContinueWith(t => (IReadOnlyList<RoleTemplate>)t.Result, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<RoleTemplate?> FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var normalized = templateCode.Trim();
|
||||||
|
return dbContext.RoleTemplates.AsNoTracking().FirstOrDefaultAsync(x => x.TemplateCode == normalized, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<IReadOnlyList<RoleTemplatePermission>> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
return dbContext.RoleTemplatePermissions.AsNoTracking()
|
||||||
|
.Where(x => x.RoleTemplateId == roleTemplateId)
|
||||||
|
.ToListAsync(cancellationToken)
|
||||||
|
.ContinueWith(t => (IReadOnlyList<RoleTemplatePermission>)t.Result, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<IDictionary<long, IReadOnlyList<RoleTemplatePermission>>> GetPermissionsAsync(IEnumerable<long> roleTemplateIds, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var ids = roleTemplateIds.Distinct().ToArray();
|
||||||
|
if (ids.Length == 0)
|
||||||
|
{
|
||||||
|
return new Dictionary<long, IReadOnlyList<RoleTemplatePermission>>();
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissions = await dbContext.RoleTemplatePermissions.AsNoTracking()
|
||||||
|
.Where(x => ids.Contains(x.RoleTemplateId))
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
return permissions
|
||||||
|
.GroupBy(x => x.RoleTemplateId)
|
||||||
|
.ToDictionary(g => g.Key, g => (IReadOnlyList<RoleTemplatePermission>)g.ToList());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task AddAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
template.TemplateCode = template.TemplateCode.Trim();
|
||||||
|
template.Name = template.Name.Trim();
|
||||||
|
await dbContext.RoleTemplates.AddAsync(template, cancellationToken);
|
||||||
|
await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
template.TemplateCode = template.TemplateCode.Trim();
|
||||||
|
template.Name = template.Name.Trim();
|
||||||
|
dbContext.RoleTemplates.Update(template);
|
||||||
|
await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteAsync(long roleTemplateId, CancellationToken cancellationToken = default)
|
||||||
|
{
|
||||||
|
var entity = await dbContext.RoleTemplates.FirstOrDefaultAsync(x => x.Id == roleTemplateId, cancellationToken);
|
||||||
|
if (entity != null)
|
||||||
|
{
|
||||||
|
var permissions = dbContext.RoleTemplatePermissions.Where(x => x.RoleTemplateId == roleTemplateId);
|
||||||
|
dbContext.RoleTemplatePermissions.RemoveRange(permissions);
|
||||||
|
dbContext.RoleTemplates.Remove(entity);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
|
private async Task ReplacePermissionsInternalAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// 确保模板已持久化,便于 FK 正确填充
|
||||||
|
if (!dbContext.Entry(template).IsKeySet || template.Id == 0)
|
||||||
|
{
|
||||||
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var normalized = permissionCodes
|
||||||
|
.Where(code => !string.IsNullOrWhiteSpace(code))
|
||||||
|
.Select(code => code.Trim())
|
||||||
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToArray();
|
||||||
|
|
||||||
|
var existing = await dbContext.RoleTemplatePermissions
|
||||||
|
.Where(x => x.RoleTemplateId == template.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
dbContext.RoleTemplatePermissions.RemoveRange(existing);
|
||||||
|
|
||||||
|
var toAdd = normalized.Select(code => new RoleTemplatePermission
|
||||||
|
{
|
||||||
|
RoleTemplateId = template.Id,
|
||||||
|
PermissionCode = code
|
||||||
|
});
|
||||||
|
|
||||||
|
await dbContext.RoleTemplatePermissions.AddRangeAsync(toAdd, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,6 +12,8 @@ using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser;
|
|||||||
using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission;
|
using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission;
|
||||||
using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role;
|
using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role;
|
||||||
using DomainRolePermission = TakeoutSaaS.Domain.Identity.Entities.RolePermission;
|
using DomainRolePermission = TakeoutSaaS.Domain.Identity.Entities.RolePermission;
|
||||||
|
using DomainRoleTemplate = TakeoutSaaS.Domain.Identity.Entities.RoleTemplate;
|
||||||
|
using DomainRoleTemplatePermission = TakeoutSaaS.Domain.Identity.Entities.RoleTemplatePermission;
|
||||||
using DomainUserRole = TakeoutSaaS.Domain.Identity.Entities.UserRole;
|
using DomainUserRole = TakeoutSaaS.Domain.Identity.Entities.UserRole;
|
||||||
|
|
||||||
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
|
||||||
@@ -37,6 +39,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken);
|
||||||
|
|
||||||
foreach (var userOptions in options.Users)
|
foreach (var userOptions in options.Users)
|
||||||
{
|
{
|
||||||
using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId);
|
using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId);
|
||||||
@@ -159,6 +163,66 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
|
|||||||
|
|
||||||
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
|
||||||
|
|
||||||
|
private static async Task SeedRoleTemplatesAsync(
|
||||||
|
IdentityDbContext context,
|
||||||
|
IList<RoleTemplateSeedOptions> templates,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (templates is null || templates.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var templateOptions in templates)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(templateOptions.TemplateCode) || string.IsNullOrWhiteSpace(templateOptions.Name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var code = templateOptions.TemplateCode.Trim();
|
||||||
|
var existing = await context.RoleTemplates.FirstOrDefaultAsync(x => x.TemplateCode == code, cancellationToken);
|
||||||
|
|
||||||
|
if (existing == null)
|
||||||
|
{
|
||||||
|
existing = new DomainRoleTemplate
|
||||||
|
{
|
||||||
|
TemplateCode = code,
|
||||||
|
Name = templateOptions.Name.Trim(),
|
||||||
|
Description = templateOptions.Description,
|
||||||
|
IsActive = templateOptions.IsActive
|
||||||
|
};
|
||||||
|
|
||||||
|
await context.RoleTemplates.AddAsync(existing, cancellationToken);
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
existing.Name = templateOptions.Name.Trim();
|
||||||
|
existing.Description = templateOptions.Description;
|
||||||
|
existing.IsActive = templateOptions.IsActive;
|
||||||
|
context.RoleTemplates.Update(existing);
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
var permissionCodes = NormalizeValues(templateOptions.Permissions);
|
||||||
|
var existingPermissions = await context.RoleTemplatePermissions
|
||||||
|
.Where(x => x.RoleTemplateId == existing.Id)
|
||||||
|
.ToListAsync(cancellationToken);
|
||||||
|
|
||||||
|
context.RoleTemplatePermissions.RemoveRange(existingPermissions);
|
||||||
|
|
||||||
|
var toAdd = permissionCodes.Select(code => new DomainRoleTemplatePermission
|
||||||
|
{
|
||||||
|
RoleTemplateId = existing.Id,
|
||||||
|
PermissionCode = code
|
||||||
|
});
|
||||||
|
|
||||||
|
await context.RoleTemplatePermissions.AddRangeAsync(toAdd, cancellationToken);
|
||||||
|
await context.SaveChangesAsync(cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static string[] NormalizeValues(string[]? values)
|
private static string[] NormalizeValues(string[]? values)
|
||||||
=> values == null
|
=> values == null
|
||||||
? []
|
? []
|
||||||
|
|||||||
@@ -34,6 +34,16 @@ public sealed class IdentityDbContext(
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public DbSet<Role> Roles => Set<Role>();
|
public DbSet<Role> Roles => Set<Role>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板集合(平台级)。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<RoleTemplate> RoleTemplates => Set<RoleTemplate>();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 角色模板权限集合。
|
||||||
|
/// </summary>
|
||||||
|
public DbSet<RoleTemplatePermission> RoleTemplatePermissions => Set<RoleTemplatePermission>();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// 权限集合。
|
/// 权限集合。
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -59,6 +69,8 @@ public sealed class IdentityDbContext(
|
|||||||
ConfigureIdentityUser(modelBuilder.Entity<IdentityUser>());
|
ConfigureIdentityUser(modelBuilder.Entity<IdentityUser>());
|
||||||
ConfigureMiniUser(modelBuilder.Entity<MiniUser>());
|
ConfigureMiniUser(modelBuilder.Entity<MiniUser>());
|
||||||
ConfigureRole(modelBuilder.Entity<Role>());
|
ConfigureRole(modelBuilder.Entity<Role>());
|
||||||
|
ConfigureRoleTemplate(modelBuilder.Entity<RoleTemplate>());
|
||||||
|
ConfigureRoleTemplatePermission(modelBuilder.Entity<RoleTemplatePermission>());
|
||||||
ConfigurePermission(modelBuilder.Entity<Permission>());
|
ConfigurePermission(modelBuilder.Entity<Permission>());
|
||||||
ConfigureUserRole(modelBuilder.Entity<UserRole>());
|
ConfigureUserRole(modelBuilder.Entity<UserRole>());
|
||||||
ConfigureRolePermission(modelBuilder.Entity<RolePermission>());
|
ConfigureRolePermission(modelBuilder.Entity<RolePermission>());
|
||||||
@@ -133,6 +145,28 @@ public sealed class IdentityDbContext(
|
|||||||
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ConfigureRoleTemplate(EntityTypeBuilder<RoleTemplate> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("role_templates");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.TemplateCode).HasMaxLength(64).IsRequired();
|
||||||
|
builder.Property(x => x.Name).HasMaxLength(128).IsRequired();
|
||||||
|
builder.Property(x => x.Description).HasMaxLength(256);
|
||||||
|
builder.Property(x => x.IsActive).IsRequired();
|
||||||
|
ConfigureAuditableEntity(builder);
|
||||||
|
builder.HasIndex(x => x.TemplateCode).IsUnique();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureRoleTemplatePermission(EntityTypeBuilder<RoleTemplatePermission> builder)
|
||||||
|
{
|
||||||
|
builder.ToTable("role_template_permissions");
|
||||||
|
builder.HasKey(x => x.Id);
|
||||||
|
builder.Property(x => x.RoleTemplateId).IsRequired();
|
||||||
|
builder.Property(x => x.PermissionCode).HasMaxLength(128).IsRequired();
|
||||||
|
ConfigureAuditableEntity(builder);
|
||||||
|
builder.HasIndex(x => new { x.RoleTemplateId, x.PermissionCode }).IsUnique();
|
||||||
|
}
|
||||||
|
|
||||||
private static void ConfigureUserRole(EntityTypeBuilder<UserRole> builder)
|
private static void ConfigureUserRole(EntityTypeBuilder<UserRole> builder)
|
||||||
{
|
{
|
||||||
builder.ToTable("user_roles");
|
builder.ToTable("user_roles");
|
||||||
|
|||||||
Reference in New Issue
Block a user