diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs index 850e5df..1033f5c 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/RolesController.cs @@ -30,9 +30,9 @@ public sealed class RolesController(IMediator mediator) : BaseApiController [HttpGet("templates")] [PermissionAuthorize("identity:role:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> ListTemplates(CancellationToken cancellationToken) + public async Task>> 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>.Ok(result); } @@ -54,6 +54,49 @@ public sealed class RolesController(IMediator mediator) : BaseApiController : ApiResponse.Ok(result); } + /// + /// 创建角色模板。 + /// + [HttpPost("templates")] + [PermissionAuthorize("role-template:create")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> CreateTemplate([FromBody, Required] CreateRoleTemplateCommand command, CancellationToken cancellationToken) + { + var result = await mediator.Send(command, cancellationToken); + return ApiResponse.Ok(result); + } + + /// + /// 更新角色模板。 + /// + [HttpPut("templates/{templateCode}")] + [PermissionAuthorize("role-template:update")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] + public async Task> 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.Error(StatusCodes.Status404NotFound, "角色模板不存在") + : ApiResponse.Ok(result); + } + + /// + /// 删除角色模板。 + /// + [HttpDelete("templates/{templateCode}")] + [PermissionAuthorize("role-template:delete")] + [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] + public async Task> DeleteTemplate(string templateCode, CancellationToken cancellationToken) + { + var result = await mediator.Send(new DeleteRoleTemplateCommand { TemplateCode = templateCode }, cancellationToken); + return ApiResponse.Ok(result); + } + /// /// 按模板复制角色并绑定权限。 /// diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json index 6f897a7..3092532 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Seed.Development.json @@ -39,6 +39,171 @@ }, "Identity": { "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": [ { "Account": "admin", @@ -47,17 +212,69 @@ "TenantId": 1000000000001, "Roles": [ "PlatformAdmin" ], "Permissions": [ - "merchant:*", - "merchant_category:*", + "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:*", - "product:*", - "order:*", - "payment:*", - "delivery:*" + "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" ] } ] diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRoleTemplateProvider.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRoleTemplateProvider.cs deleted file mode 100644 index 6f183ca..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IRoleTemplateProvider.cs +++ /dev/null @@ -1,23 +0,0 @@ -using System.Collections.Generic; -using TakeoutSaaS.Application.Identity.Templates; - -namespace TakeoutSaaS.Application.Identity.Abstractions; - -/// -/// 角色模板提供者,用于获取预置模板定义。 -/// -public interface IRoleTemplateProvider -{ - /// - /// 获取全部角色模板定义。 - /// - /// 模板定义集合。 - IReadOnlyList GetTemplates(); - - /// - /// 根据模板编码查找模板。 - /// - /// 模板编码。 - /// 模板定义;不存在时返回 null。 - RoleTemplateDefinition? FindByCode(string templateCode); -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleTemplateCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleTemplateCommand.cs new file mode 100644 index 0000000..7ca69d1 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreateRoleTemplateCommand.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 创建角色模板命令。 +/// +public sealed record CreateRoleTemplateCommand : IRequest +{ + /// + /// 模板编码。 + /// + public string TemplateCode { get; init; } = string.Empty; + + /// + /// 模板名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 模板描述。 + /// + public string? Description { get; init; } + + /// + /// 是否启用。 + /// + public bool IsActive { get; init; } = true; + + /// + /// 权限编码集合。 + /// + public IReadOnlyCollection PermissionCodes { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleTemplateCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleTemplateCommand.cs new file mode 100644 index 0000000..7f29db6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/DeleteRoleTemplateCommand.cs @@ -0,0 +1,14 @@ +using MediatR; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 删除角色模板命令。 +/// +public sealed record DeleteRoleTemplateCommand : IRequest +{ + /// + /// 模板编码。 + /// + public string TemplateCode { get; init; } = string.Empty; +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleTemplateCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleTemplateCommand.cs new file mode 100644 index 0000000..d88b3ba --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdateRoleTemplateCommand.cs @@ -0,0 +1,36 @@ +using System.Collections.Generic; +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Commands; + +/// +/// 更新角色模板命令。 +/// +public sealed record UpdateRoleTemplateCommand : IRequest +{ + /// + /// 模板编码(路径参数)。 + /// + public string TemplateCode { get; init; } = string.Empty; + + /// + /// 模板名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 模板描述。 + /// + public string? Description { get; init; } + + /// + /// 是否启用。 + /// + public bool IsActive { get; init; } = true; + + /// + /// 权限编码集合。 + /// + public IReadOnlyCollection PermissionCodes { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleTemplateDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleTemplateDto.cs index fc43f5b..4690fa2 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleTemplateDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleTemplateDto.cs @@ -22,6 +22,11 @@ public sealed record RoleTemplateDto /// public string? Description { get; init; } + /// + /// 是否启用。 + /// + public bool IsActive { get; init; } + /// /// 包含的权限定义。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs index f680ef7..c5df667 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Extensions/IdentityServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using Microsoft.Extensions.DependencyInjection; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Services; -using TakeoutSaaS.Application.Identity.Templates; namespace TakeoutSaaS.Application.Identity.Extensions; @@ -18,7 +17,6 @@ public static class IdentityServiceCollectionExtensions public static IServiceCollection AddIdentityApplication(this IServiceCollection services, bool enableMiniSupport = false) { services.AddScoped(); - services.AddSingleton(); if (enableMiniSupport) { diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs index d5da9be..54b658b 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using MediatR; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Entities; @@ -17,7 +16,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 角色模板复制处理器。 /// public sealed class CopyRoleTemplateCommandHandler( - IRoleTemplateProvider roleTemplateProvider, + IRoleTemplateRepository roleTemplateRepository, IRoleRepository roleRepository, IPermissionRepository permissionRepository, IRolePermissionRepository rolePermissionRepository, @@ -27,9 +26,16 @@ public sealed class CopyRoleTemplateCommandHandler( /// public async Task 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} 不存在"); + 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 roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim(); var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim(); @@ -64,18 +70,12 @@ public sealed class CopyRoleTemplateCommandHandler( } // 2. 确保模板权限全部存在,不存在则按模板定义创建。 - var targetPermissionCodes = template.Permissions - .Select(permission => permission.Code) - .Where(code => !string.IsNullOrWhiteSpace(code)) - .Distinct(StringComparer.OrdinalIgnoreCase) - .ToArray(); - - var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, targetPermissionCodes, cancellationToken); + var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, permissionCodes, cancellationToken); 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; } @@ -83,13 +83,13 @@ public sealed class CopyRoleTemplateCommandHandler( var permission = new Permission { TenantId = tenantId, - Name = permissionDefinition.Name, - Code = permissionDefinition.Code, - Description = permissionDefinition.Description + Name = code, + Code = code, + Description = code }; await permissionRepository.AddAsync(permission, cancellationToken); - permissionMap[permissionDefinition.Code] = permission; + permissionMap[code] = permission; } await roleRepository.SaveChangesAsync(cancellationToken); @@ -100,7 +100,7 @@ public sealed class CopyRoleTemplateCommandHandler( .Select(x => x.PermissionId) .ToHashSet(); - var targetPermissionIds = targetPermissionCodes + var targetPermissionIds = permissionCodes .Select(code => permissionMap[code].Id) .ToHashSet(); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleTemplateCommandHandler.cs new file mode 100644 index 0000000..64460ef --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleTemplateCommandHandler.cs @@ -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; + +/// +/// 创建角色模板处理器。 +/// +public sealed class CreateRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository) + : IRequestHandler +{ + /// + public async Task 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); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleTemplateCommandHandler.cs new file mode 100644 index 0000000..9947217 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleTemplateCommandHandler.cs @@ -0,0 +1,25 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Repositories; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 删除角色模板处理器。 +/// +public sealed class DeleteRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository) + : IRequestHandler +{ + public async Task 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; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetRoleTemplateQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetRoleTemplateQueryHandler.cs index 70206e7..b8b7bbe 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetRoleTemplateQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetRoleTemplateQueryHandler.cs @@ -1,20 +1,28 @@ +using System.Linq; using MediatR; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Application.Identity.Handlers; /// /// 角色模板详情查询处理器。 /// -public sealed class GetRoleTemplateQueryHandler(IRoleTemplateProvider roleTemplateProvider) +public sealed class GetRoleTemplateQueryHandler(IRoleTemplateRepository roleTemplateRepository) : IRequestHandler { /// - public Task Handle(GetRoleTemplateQuery request, CancellationToken cancellationToken) + public async Task Handle(GetRoleTemplateQuery request, CancellationToken cancellationToken) { - var template = roleTemplateProvider.FindByCode(request.TemplateCode); - return Task.FromResult(template is null ? null : TemplateMapper.ToDto(template)); + var template = await roleTemplateRepository.FindByCodeAsync(request.TemplateCode, cancellationToken); + 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); } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs index 765819d..483b336 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/InitializeRoleTemplatesCommandHandler.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Linq; using MediatR; -using TakeoutSaaS.Application.Identity.Abstractions; 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; @@ -14,39 +14,47 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 租户角色模板批量初始化处理器。 /// public sealed class InitializeRoleTemplatesCommandHandler( - IRoleTemplateProvider roleTemplateProvider, + IRoleTemplateRepository roleTemplateRepository, IMediator mediator) : IRequestHandler> { /// public async Task> Handle(InitializeRoleTemplatesCommand request, CancellationToken cancellationToken) { - // 1. 解析需要初始化的模板编码,默认取全部预置模板。 + // 1. 解析需要初始化的模板编码,默认取全部模板。 var requestedCodes = request.TemplateCodes? .Where(code => !string.IsNullOrWhiteSpace(code)) .Select(code => code.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); + var availableTemplates = await roleTemplateRepository.GetAllAsync(true, cancellationToken); + var availableCodes = availableTemplates.Select(t => t.TemplateCode).ToHashSet(StringComparer.OrdinalIgnoreCase); + var targetCodes = requestedCodes?.Length > 0 ? requestedCodes - : roleTemplateProvider.GetTemplates().Select(template => template.TemplateCode).ToArray(); + : availableTemplates.Select(template => template.TemplateCode).ToArray(); if (targetCodes.Length == 0) { return Array.Empty(); } + foreach (var code in targetCodes) + { + if (!availableCodes.Contains(code)) + { + throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {code} 不存在或未启用"); + } + } + // 2. 逐个复制模板,幂等写入角色与权限。 var roles = new List(targetCodes.Length); foreach (var templateCode in targetCodes) { - var template = roleTemplateProvider.FindByCode(templateCode) - ?? throw new BusinessException(ErrorCodes.NotFound, $"角色模板 {templateCode} 不存在"); - var role = await mediator.Send(new CopyRoleTemplateCommand { - TemplateCode = template.TemplateCode + TemplateCode = templateCode }, cancellationToken); roles.Add(role); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListRoleTemplatesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListRoleTemplatesQueryHandler.cs index 011029a..2b533cd 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListRoleTemplatesQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListRoleTemplatesQueryHandler.cs @@ -1,26 +1,35 @@ -using System; +using System.Collections.Generic; using System.Linq; using MediatR; -using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Application.Identity.Handlers; /// /// 角色模板列表查询处理器。 /// -public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateProvider roleTemplateProvider) +public sealed class ListRoleTemplatesQueryHandler(IRoleTemplateRepository roleTemplateRepository) : IRequestHandler> { /// - public Task> Handle(ListRoleTemplatesQuery request, CancellationToken cancellationToken) + public async Task> 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) - .Select(TemplateMapper.ToDto) + .Select(template => + { + var codes = permissionsMap.TryGetValue(template.Id, out var perms) + ? (IReadOnlyCollection)perms.Select(p => p.PermissionCode).ToArray() + : Array.Empty(); + return TemplateMapper.ToDto(template, codes); + }) .ToArray(); - return Task.FromResult>(templates); + return dtos; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/TemplateMapper.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/TemplateMapper.cs index 47b9670..73e0509 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/TemplateMapper.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/TemplateMapper.cs @@ -1,6 +1,7 @@ +using System.Collections.Generic; using System.Linq; using TakeoutSaaS.Application.Identity.Contracts; -using TakeoutSaaS.Application.Identity.Templates; +using TakeoutSaaS.Domain.Identity.Entities; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -10,28 +11,27 @@ namespace TakeoutSaaS.Application.Identity.Handlers; internal static class TemplateMapper { /// - /// 将角色模板定义映射为 DTO。 + /// 将角色模板与权限编码集合映射为 DTO。 /// - /// 角色模板定义。 + /// 角色模板实体。 + /// 权限编码集合。 /// 模板 DTO。 - public static RoleTemplateDto ToDto(RoleTemplateDefinition definition) + public static RoleTemplateDto ToDto(RoleTemplate template, IReadOnlyCollection permissionCodes) { return new RoleTemplateDto { - TemplateCode = definition.TemplateCode, - Name = definition.Name, - Description = definition.Description, - Permissions = definition.Permissions.Select(ToDto).ToArray() - }; - } - - private static PermissionTemplateDto ToDto(PermissionTemplateDefinition definition) - { - return new PermissionTemplateDto - { - Code = definition.Code, - Name = definition.Name, - Description = definition.Description + TemplateCode = template.TemplateCode, + Name = template.Name, + Description = template.Description, + IsActive = template.IsActive, + Permissions = permissionCodes + .Select(code => new PermissionTemplateDto + { + Code = code, + Name = code, + Description = null + }) + .ToArray() }; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleTemplateCommandHandler.cs new file mode 100644 index 0000000..1f07395 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleTemplateCommandHandler.cs @@ -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; + +/// +/// 更新角色模板处理器。 +/// +public sealed class UpdateRoleTemplateCommandHandler(IRoleTemplateRepository roleTemplateRepository) + : IRequestHandler +{ + /// + public async Task 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); + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/ListRoleTemplatesQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/ListRoleTemplatesQuery.cs index cf25f1e..7da3def 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Queries/ListRoleTemplatesQuery.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/ListRoleTemplatesQuery.cs @@ -7,4 +7,10 @@ namespace TakeoutSaaS.Application.Identity.Queries; /// /// 查询角色模板列表。 /// -public sealed record ListRoleTemplatesQuery : IRequest>; +public sealed record ListRoleTemplatesQuery : IRequest> +{ + /// + /// 是否仅返回启用模板。 + /// + public bool? IsActive { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Templates/PermissionTemplateDefinition.cs b/src/Application/TakeoutSaaS.Application/Identity/Templates/PermissionTemplateDefinition.cs deleted file mode 100644 index 2bb4a46..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Templates/PermissionTemplateDefinition.cs +++ /dev/null @@ -1,22 +0,0 @@ -namespace TakeoutSaaS.Application.Identity.Templates; - -/// -/// 权限模板定义。 -/// -public sealed record PermissionTemplateDefinition -{ - /// - /// 权限编码(唯一键)。 - /// - public required string Code { get; init; } - - /// - /// 权限名称。 - /// - public required string Name { get; init; } - - /// - /// 权限描述。 - /// - public string? Description { get; init; } -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Templates/RoleTemplateDefinition.cs b/src/Application/TakeoutSaaS.Application/Identity/Templates/RoleTemplateDefinition.cs deleted file mode 100644 index 6c4f3cc..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Templates/RoleTemplateDefinition.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System.Collections.Generic; - -namespace TakeoutSaaS.Application.Identity.Templates; - -/// -/// 角色模板定义。 -/// -public sealed record RoleTemplateDefinition -{ - /// - /// 模板编码(唯一键)。 - /// - public required string TemplateCode { get; init; } - - /// - /// 角色名称。 - /// - public required string Name { get; init; } - - /// - /// 角色描述。 - /// - public string? Description { get; init; } - - /// - /// 模板绑定的权限集合。 - /// - public IReadOnlyList Permissions { get; init; } = []; -} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Templates/RoleTemplateProvider.cs b/src/Application/TakeoutSaaS.Application/Identity/Templates/RoleTemplateProvider.cs deleted file mode 100644 index 1d1482e..0000000 --- a/src/Application/TakeoutSaaS.Application/Identity/Templates/RoleTemplateProvider.cs +++ /dev/null @@ -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; - -/// -/// 预置角色模板提供者。 -/// -public sealed class RoleTemplateProvider : IRoleTemplateProvider -{ - private static readonly FrozenDictionary PermissionDefinitions = - new Dictionary(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 Templates = - new Dictionary(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); - - /// - public IReadOnlyList GetTemplates() - => Templates.Values.ToArray(); - - /// - public RoleTemplateDefinition? FindByCode(string templateCode) - { - if (string.IsNullOrWhiteSpace(templateCode)) - { - return null; - } - - return Templates.GetValueOrDefault(templateCode); - } - - private static IReadOnlyList BuildPermissions(IEnumerable 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(); - } -} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RoleTemplate.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RoleTemplate.cs new file mode 100644 index 0000000..2570321 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RoleTemplate.cs @@ -0,0 +1,29 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 角色模板定义(平台级)。 +/// +public sealed class RoleTemplate : AuditableEntityBase +{ + /// + /// 模板编码(唯一)。 + /// + public string TemplateCode { get; set; } = string.Empty; + + /// + /// 模板名称。 + /// + public string Name { get; set; } = string.Empty; + + /// + /// 模板描述。 + /// + public string? Description { get; set; } + + /// + /// 是否启用。 + /// + public bool IsActive { get; set; } = true; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RoleTemplatePermission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RoleTemplatePermission.cs new file mode 100644 index 0000000..f3f9896 --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RoleTemplatePermission.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Domain.Identity.Entities; + +/// +/// 角色模板-权限关系(平台级)。 +/// +public sealed class RoleTemplatePermission : AuditableEntityBase +{ + /// + /// 模板 ID。 + /// + public long RoleTemplateId { get; set; } + + /// + /// 权限编码。 + /// + public string PermissionCode { get; set; } = string.Empty; +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleTemplateRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleTemplateRepository.cs new file mode 100644 index 0000000..963754a --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleTemplateRepository.cs @@ -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; + +/// +/// 角色模板仓储。 +/// +public interface IRoleTemplateRepository +{ + Task> GetAllAsync(bool? isActive, CancellationToken cancellationToken = default); + + Task FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default); + + Task> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default); + + Task>> GetPermissionsAsync(IEnumerable roleTemplateIds, CancellationToken cancellationToken = default); + + Task AddAsync(RoleTemplate template, IEnumerable permissionCodes, CancellationToken cancellationToken = default); + + Task UpdateAsync(RoleTemplate template, IEnumerable permissionCodes, CancellationToken cancellationToken = default); + + Task DeleteAsync(long roleTemplateId, CancellationToken cancellationToken = default); + + Task SaveChangesAsync(CancellationToken cancellationToken = default); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs index 0c8dc63..d98587f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -55,6 +55,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped(); + services.AddScoped(); services.AddScoped(); services.AddScoped(); services.AddScoped(); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs index 5a2f813..31ee1cf 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Options/AdminSeedOptions.cs @@ -11,6 +11,11 @@ public sealed class AdminSeedOptions /// 初始用户列表。 /// public List Users { get; set; } = new(); + + /// + /// 角色模板种子列表。 + /// + public List RoleTemplates { get; set; } = new(); } /// @@ -56,3 +61,36 @@ public sealed class SeedUserOptions /// public string[] Permissions { get; set; } = Array.Empty(); } + +/// +/// 角色模板种子配置。 +/// +public sealed class RoleTemplateSeedOptions +{ + /// + /// 模板编码。 + /// + [Required] + public string TemplateCode { get; set; } = string.Empty; + + /// + /// 模板名称。 + /// + [Required] + public string Name { get; set; } = string.Empty; + + /// + /// 模板描述。 + /// + public string? Description { get; set; } + + /// + /// 是否启用。 + /// + public bool IsActive { get; set; } = true; + + /// + /// 权限编码集合。 + /// + public string[] Permissions { get; set; } = Array.Empty(); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs new file mode 100644 index 0000000..e8c071b --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs @@ -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; + +/// +/// 角色模板仓储实现。 +/// +public sealed class EfRoleTemplateRepository(IdentityDbContext dbContext) : IRoleTemplateRepository +{ + public Task> 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)t.Result, cancellationToken); + } + + public Task FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default) + { + var normalized = templateCode.Trim(); + return dbContext.RoleTemplates.AsNoTracking().FirstOrDefaultAsync(x => x.TemplateCode == normalized, cancellationToken); + } + + public Task> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default) + { + return dbContext.RoleTemplatePermissions.AsNoTracking() + .Where(x => x.RoleTemplateId == roleTemplateId) + .ToListAsync(cancellationToken) + .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + } + + public async Task>> GetPermissionsAsync(IEnumerable roleTemplateIds, CancellationToken cancellationToken = default) + { + var ids = roleTemplateIds.Distinct().ToArray(); + if (ids.Length == 0) + { + return new Dictionary>(); + } + + 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)g.ToList()); + } + + public async Task AddAsync(RoleTemplate template, IEnumerable 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 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 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); + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index a9246dd..f4ec0a0 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -12,6 +12,8 @@ using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; using DomainPermission = TakeoutSaaS.Domain.Identity.Entities.Permission; using DomainRole = TakeoutSaaS.Domain.Identity.Entities.Role; 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; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -37,6 +39,8 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger return; } + await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken); + foreach (var userOptions in options.Users) { 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; + private static async Task SeedRoleTemplatesAsync( + IdentityDbContext context, + IList 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) => values == null ? [] diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index dfe4d4f..854b265 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -34,6 +34,16 @@ public sealed class IdentityDbContext( /// public DbSet Roles => Set(); + /// + /// 角色模板集合(平台级)。 + /// + public DbSet RoleTemplates => Set(); + + /// + /// 角色模板权限集合。 + /// + public DbSet RoleTemplatePermissions => Set(); + /// /// 权限集合。 /// @@ -59,6 +69,8 @@ public sealed class IdentityDbContext( ConfigureIdentityUser(modelBuilder.Entity()); ConfigureMiniUser(modelBuilder.Entity()); ConfigureRole(modelBuilder.Entity()); + ConfigureRoleTemplate(modelBuilder.Entity()); + ConfigureRoleTemplatePermission(modelBuilder.Entity()); ConfigurePermission(modelBuilder.Entity()); ConfigureUserRole(modelBuilder.Entity()); ConfigureRolePermission(modelBuilder.Entity()); @@ -133,6 +145,28 @@ public sealed class IdentityDbContext( builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); } + private static void ConfigureRoleTemplate(EntityTypeBuilder 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 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 builder) { builder.ToTable("user_roles");