diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs index 5eda408..c0f37eb 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs @@ -37,6 +37,27 @@ public sealed class PermissionsController(IMediator mediator) : BaseApiControlle return ApiResponse>.Ok(result); } + /// + /// 获取权限树。 + /// + /// 关键字(可选)。 + /// 取消标记。 + /// 权限树列表。 + [HttpGet("tree")] + [PermissionAuthorize("identity:permission:read")] + [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] + public async Task>> Tree([FromQuery] string? keyword, CancellationToken cancellationToken) + { + // 1. 构造查询对象 + var query = new PermissionTreeQuery { Keyword = keyword }; + + // 2. 查询权限树 + var result = await mediator.Send(query, cancellationToken); + + // 3. 返回结果 + return ApiResponse>.Ok(result); + } + /// /// 创建权限。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs index d554152..e8a5989 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/CreatePermissionCommand.cs @@ -8,6 +8,9 @@ namespace TakeoutSaaS.Application.Identity.Commands; /// public sealed record CreatePermissionCommand : IRequest { + public long ParentId { get; init; } = 0; + public int SortOrder { get; init; } = 0; + public string Type { get; init; } = "leaf"; public string Name { get; init; } = string.Empty; public string Code { get; init; } = string.Empty; public string? Description { get; init; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs index edcb482..a33558a 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Commands/UpdatePermissionCommand.cs @@ -9,6 +9,9 @@ namespace TakeoutSaaS.Application.Identity.Commands; public sealed record UpdatePermissionCommand : IRequest { public long PermissionId { get; init; } + public long ParentId { get; init; } + public int SortOrder { get; init; } + public string Type { get; init; } = "leaf"; public string Name { get; init; } = string.Empty; public string? Description { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs index e9623bd..eb28004 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs @@ -20,6 +20,22 @@ public sealed class PermissionDto [JsonConverter(typeof(SnowflakeIdJsonConverter))] public long TenantId { get; init; } + /// + /// 父级权限 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long ParentId { get; init; } + + /// + /// 排序值,值越小越靠前。 + /// + public int SortOrder { get; init; } + + /// + /// 权限类型(group/leaf)。 + /// + public string Type { get; init; } = string.Empty; + /// /// 权限名称。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs new file mode 100644 index 0000000..a4dac6c --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs @@ -0,0 +1,58 @@ +using System.Text.Json.Serialization; +using TakeoutSaaS.Shared.Abstractions.Serialization; + +namespace TakeoutSaaS.Application.Identity.Contracts; + +/// +/// 权限树节点 DTO。 +/// +public sealed record PermissionTreeDto +{ + /// + /// 权限 ID(雪花,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long Id { get; init; } + + /// + /// 租户 ID(雪花,序列化为字符串)。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long TenantId { get; init; } + + /// + /// 父级权限 ID。 + /// + [JsonConverter(typeof(SnowflakeIdJsonConverter))] + public long ParentId { get; init; } + + /// + /// 排序值,值越小越靠前。 + /// + public int SortOrder { get; init; } + + /// + /// 权限类型(group/leaf)。 + /// + public string Type { get; init; } = string.Empty; + + /// + /// 权限名称。 + /// + public string Name { get; init; } = string.Empty; + + /// + /// 权限编码(租户内唯一)。 + /// + public string Code { get; init; } = string.Empty; + + /// + /// 描述。 + /// + public string? Description { get; init; } + + /// + /// 子节点集合。 + /// + public IReadOnlyList Children { get; init; } = Array.Empty(); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs index 1e5cfc0..3ad074e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs @@ -21,9 +21,18 @@ public sealed class CreatePermissionCommandHandler( var tenantId = tenantProvider.GetCurrentTenantId(); // 2. 构建权限实体 + var normalizedType = string.IsNullOrWhiteSpace(request.Type) + ? "leaf" + : request.Type.Trim().ToLowerInvariant(); + normalizedType = normalizedType is "group" or "leaf" ? normalizedType : "leaf"; + var parentId = request.ParentId > 0 ? request.ParentId : 0; + var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder; var permission = new Permission { TenantId = tenantId, + ParentId = parentId, + SortOrder = sortOrder, + Type = normalizedType, Name = request.Name, Code = request.Code, Description = request.Description @@ -38,6 +47,9 @@ public sealed class CreatePermissionCommandHandler( { Id = permission.Id, TenantId = permission.TenantId, + ParentId = permission.ParentId, + SortOrder = permission.SortOrder, + Type = permission.Type, Name = permission.Name, Code = permission.Code, Description = permission.Description diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs new file mode 100644 index 0000000..70a9fd3 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs @@ -0,0 +1,69 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Repositories; +using TakeoutSaaS.Shared.Abstractions.Tenancy; + +namespace TakeoutSaaS.Application.Identity.Handlers; + +/// +/// 权限树查询处理器。 +/// +public sealed class PermissionTreeQueryHandler( + IPermissionRepository permissionRepository, + ITenantProvider tenantProvider) + : IRequestHandler> +{ + public async Task> Handle(PermissionTreeQuery request, CancellationToken cancellationToken) + { + // 1. 获取租户上下文并查询权限 + var tenantId = tenantProvider.GetCurrentTenantId(); + var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + + // 2. 构建节点映射与父子分组 + var nodeMap = permissions.ToDictionary( + x => x.Id, + x => new PermissionTreeDto + { + Id = x.Id, + TenantId = x.TenantId, + ParentId = x.ParentId, + SortOrder = x.SortOrder, + Type = x.Type, + Name = x.Name, + Code = x.Code, + Description = x.Description, + Children = Array.Empty() + }); + var childrenLookup = permissions + .GroupBy(x => x.ParentId) + .ToDictionary(g => g.Key, g => g.OrderBy(c => c.SortOrder).ThenBy(c => c.Id).Select(c => c.Id).ToList()); + + // 3. 递归组装树,确保子节点引用最新 + List Build(long parentId) + { + if (!childrenLookup.TryGetValue(parentId, out var childIds)) + { + return []; + } + + var result = new List(childIds.Count); + foreach (var childId in childIds) + { + if (!nodeMap.TryGetValue(childId, out var child)) + { + continue; + } + + var withChildren = child with { Children = Build(child.Id) }; + result.Add(withChildren); + } + + return result; + } + + // 4. 返回根节点集合 + var roots = Build(0); + return roots; + } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs index 3615164..c2e0730 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs @@ -41,6 +41,10 @@ public sealed class RoleDetailQueryHandler( .Select(x => new PermissionDto { Id = x.Id, + TenantId = x.TenantId, + ParentId = x.ParentId, + SortOrder = x.SortOrder, + Type = x.Type, Code = x.Code, Name = x.Name, Description = x.Description diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs index df6245b..3821489 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs @@ -30,9 +30,18 @@ public sealed class SearchPermissionsQueryHandler( "code" => request.SortDescending ? permissions.OrderByDescending(x => x.Code) : permissions.OrderBy(x => x.Code), + "parentid" => request.SortDescending + ? permissions.OrderByDescending(x => x.ParentId).ThenByDescending(x => x.SortOrder) + : permissions.OrderBy(x => x.ParentId).ThenBy(x => x.SortOrder), + "type" => request.SortDescending + ? permissions.OrderByDescending(x => x.Type).ThenByDescending(x => x.SortOrder) + : permissions.OrderBy(x => x.Type).ThenBy(x => x.SortOrder), + "sortorder" => request.SortDescending + ? permissions.OrderByDescending(x => x.SortOrder).ThenByDescending(x => x.CreatedAt) + : permissions.OrderBy(x => x.SortOrder).ThenBy(x => x.CreatedAt), _ => request.SortDescending - ? permissions.OrderByDescending(x => x.CreatedAt) - : permissions.OrderBy(x => x.CreatedAt) + ? permissions.OrderByDescending(x => x.SortOrder).ThenByDescending(x => x.CreatedAt) + : permissions.OrderBy(x => x.SortOrder).ThenBy(x => x.CreatedAt) }; // 3. 分页 @@ -46,6 +55,9 @@ public sealed class SearchPermissionsQueryHandler( { Id = permission.Id, TenantId = permission.TenantId, + ParentId = permission.ParentId, + SortOrder = permission.SortOrder, + Type = permission.Type, Name = permission.Name, Code = permission.Code, Description = permission.Description diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs index cde1cdd..d45dac8 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs @@ -25,6 +25,17 @@ public sealed class UpdatePermissionCommandHandler( } // 2. 更新字段 + var normalizedType = string.IsNullOrWhiteSpace(request.Type) + ? "leaf" + : request.Type.Trim().ToLowerInvariant(); + normalizedType = normalizedType is "group" or "leaf" ? normalizedType : "leaf"; + var parentId = request.ParentId > 0 && request.ParentId != permission.Id + ? request.ParentId + : 0; + var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder; + permission.ParentId = parentId; + permission.SortOrder = sortOrder; + permission.Type = normalizedType; permission.Name = request.Name; permission.Description = request.Description; @@ -37,6 +48,9 @@ public sealed class UpdatePermissionCommandHandler( { Id = permission.Id, TenantId = permission.TenantId, + ParentId = permission.ParentId, + SortOrder = permission.SortOrder, + Type = permission.Type, Name = permission.Name, Code = permission.Code, Description = permission.Description diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs new file mode 100644 index 0000000..5847f02 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs @@ -0,0 +1,15 @@ +using MediatR; +using TakeoutSaaS.Application.Identity.Contracts; + +namespace TakeoutSaaS.Application.Identity.Queries; + +/// +/// 权限树查询。 +/// +public sealed class PermissionTreeQuery : IRequest> +{ + /// + /// 关键字(可选)。 + /// + public string? Keyword { get; init; } +} diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs index 53a4d0b..aa8f1f5 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs @@ -7,6 +7,21 @@ namespace TakeoutSaaS.Domain.Identity.Entities; /// public sealed class Permission : MultiTenantEntityBase { + /// + /// 父级权限 ID,根节点为 0。 + /// + public long ParentId { get; set; } + + /// + /// 排序值,值越小越靠前。 + /// + public int SortOrder { get; set; } + + /// + /// 权限类型(group/leaf)。 + /// + public string Type { get; set; } = "leaf"; + /// /// 权限名称。 /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index a553f2f..b2b4804 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -141,12 +141,16 @@ public sealed class IdentityDbContext( builder.ToTable("permissions"); builder.HasKey(x => x.Id); builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.ParentId).IsRequired(); + builder.Property(x => x.SortOrder).IsRequired(); + builder.Property(x => x.Type).HasMaxLength(16).IsRequired(); builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); builder.Property(x => x.Code).HasMaxLength(128).IsRequired(); builder.Property(x => x.Description).HasMaxLength(256); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); builder.HasIndex(x => x.TenantId); + builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder }); builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251206021946_AddPermissionHierarchyFields.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251206021946_AddPermissionHierarchyFields.Designer.cs new file mode 100644 index 0000000..f806b14 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251206021946_AddPermissionHierarchyFields.Designer.cs @@ -0,0 +1,683 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; +using TakeoutSaaS.Infrastructure.Identity.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + [DbContext(typeof(IdentityDbContext))] + [Migration("20251206021946_AddPermissionHierarchyFields")] + partial class AddPermissionHierarchyFields + { + /// + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "10.0.0") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Account") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("登录账号。"); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("DisplayName") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("展示名称。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户(平台管理员为空)。"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("密码哈希。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Account") + .IsUnique(); + + b.ToTable("identity_users", null, t => + { + t.HasComment("管理后台账户实体(平台管理员、租户管理员或商户员工)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MenuDefinition", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthListJson") + .HasColumnType("text") + .HasComment("按钮权限列表 JSON(存储 MenuAuthItemDto 数组)。"); + + b.Property("Component") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("组件路径(不含 .vue)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Icon") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("图标标识。"); + + b.Property("IsIframe") + .HasColumnType("boolean") + .HasComment("是否 iframe。"); + + b.Property("KeepAlive") + .HasColumnType("boolean") + .HasComment("是否缓存。"); + + b.Property("Link") + .HasMaxLength(512) + .HasColumnType("character varying(512)") + .HasComment("外链或 iframe 地址。"); + + b.Property("MetaPermissions") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Meta.permissions(逗号分隔)。"); + + b.Property("MetaRoles") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("Meta.roles(逗号分隔)。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("菜单名称(前端路由 name)。"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级菜单 ID,根节点为 0。"); + + b.Property("Path") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("路由路径。"); + + b.Property("RequiredPermissions") + .IsRequired() + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("访问该菜单所需的权限集合(逗号分隔)。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Title") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("标题。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId", "ParentId", "SortOrder"); + + b.ToTable("menu_definitions", null, t => + { + t.HasComment("管理端菜单定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.MiniUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Avatar") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("头像地址。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Nickname") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("昵称。"); + + b.Property("OpenId") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 OpenId。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UnionId") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("微信 UnionId,可能为空。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "OpenId") + .IsUnique(); + + b.ToTable("mini_users", null, t => + { + t.HasComment("小程序用户实体。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Permission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("权限编码(租户内唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("权限名称。"); + + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级权限 ID,根节点为 0。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值,值越小越靠前。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("Type") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("权限类型(group/leaf)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.HasIndex("TenantId", "ParentId", "SortOrder"); + + b.ToTable("permissions", null, t => + { + t.HasComment("权限定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.Role", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Code") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("角色编码(租户内唯一)。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("描述。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("角色名称。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "Code") + .IsUnique(); + + b.ToTable("roles", null, t => + { + t.HasComment("角色定义。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RolePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PermissionId") + .HasColumnType("bigint") + .HasComment("权限 ID。"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasComment("角色 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "RoleId", "PermissionId") + .IsUnique(); + + b.ToTable("role_permissions", null, t => + { + t.HasComment("角色-权限关系。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RoleTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("模板描述。"); + + b.Property("IsActive") + .HasColumnType("boolean") + .HasComment("是否启用。"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("模板名称。"); + + b.Property("TemplateCode") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("模板编码(唯一)。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("TemplateCode") + .IsUnique(); + + b.ToTable("role_templates", null, t => + { + t.HasComment("角色模板定义(平台级)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.RoleTemplatePermission", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("PermissionCode") + .IsRequired() + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("权限编码。"); + + b.Property("RoleTemplateId") + .HasColumnType("bigint") + .HasComment("模板 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("RoleTemplateId", "PermissionCode") + .IsUnique(); + + b.ToTable("role_template_permissions", null, t => + { + t.HasComment("角色模板-权限关系(平台级)。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.UserRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("RoleId") + .HasColumnType("bigint") + .HasComment("角色 ID。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("所属租户 ID。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.Property("UserId") + .HasColumnType("bigint") + .HasComment("用户 ID。"); + + b.HasKey("Id"); + + b.HasIndex("TenantId"); + + b.HasIndex("TenantId", "UserId", "RoleId") + .IsUnique(); + + b.ToTable("user_roles", null, t => + { + t.HasComment("用户-角色关系。"); + }); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251206021946_AddPermissionHierarchyFields.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251206021946_AddPermissionHierarchyFields.cs new file mode 100644 index 0000000..65de3d4 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251206021946_AddPermissionHierarchyFields.cs @@ -0,0 +1,67 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + /// + public partial class AddPermissionHierarchyFields : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.AddColumn( + name: "ParentId", + table: "permissions", + type: "bigint", + nullable: false, + defaultValue: 0L, + comment: "父级权限 ID,根节点为 0。"); + + migrationBuilder.AddColumn( + name: "SortOrder", + table: "permissions", + type: "integer", + nullable: false, + defaultValue: 0, + comment: "排序值,值越小越靠前。"); + + migrationBuilder.AddColumn( + name: "Type", + table: "permissions", + type: "character varying(16)", + maxLength: 16, + nullable: false, + defaultValue: "leaf", + comment: "权限类型(group/leaf)。"); + + migrationBuilder.CreateIndex( + name: "IX_permissions_TenantId_ParentId_SortOrder", + table: "permissions", + columns: new[] { "TenantId", "ParentId", "SortOrder" }); + + migrationBuilder.Sql( + "UPDATE \"permissions\" SET \"Type\" = 'leaf' WHERE \"Type\" IS NULL OR \"Type\" = '';"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropIndex( + name: "IX_permissions_TenantId_ParentId_SortOrder", + table: "permissions"); + + migrationBuilder.DropColumn( + name: "ParentId", + table: "permissions"); + + migrationBuilder.DropColumn( + name: "SortOrder", + table: "permissions"); + + migrationBuilder.DropColumn( + name: "Type", + table: "permissions"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs index 2a8b361..21b8b40 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs @@ -332,10 +332,24 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb .HasColumnType("character varying(64)") .HasComment("权限名称。"); + b.Property("ParentId") + .HasColumnType("bigint") + .HasComment("父级权限 ID,根节点为 0。"); + + b.Property("SortOrder") + .HasColumnType("integer") + .HasComment("排序值,值越小越靠前。"); + b.Property("TenantId") .HasColumnType("bigint") .HasComment("所属租户 ID。"); + b.Property("Type") + .IsRequired() + .HasMaxLength(16) + .HasColumnType("character varying(16)") + .HasComment("权限类型(group/leaf)。"); + b.Property("UpdatedAt") .HasColumnType("timestamp with time zone") .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); @@ -351,6 +365,8 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb b.HasIndex("TenantId", "Code") .IsUnique(); + b.HasIndex("TenantId", "ParentId", "SortOrder"); + b.ToTable("permissions", null, t => { t.HasComment("权限定义。");