diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs index 441c8b2..9a0385c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantAnnouncementCommandHandler.cs @@ -58,8 +58,7 @@ public sealed class CreateTenantAnnouncementCommandHandler( PublisherUserId = publisherUserId, Status = AnnouncementStatus.Draft, TargetType = request.TargetType.Trim(), - TargetParameters = request.TargetParameters, - IsActive = false + TargetParameters = request.TargetParameters }; // 3. 持久化并返回 DTO diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs index de9f41b..124af7d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/MarkAnnouncementAsReadCommandHandler.cs @@ -42,7 +42,7 @@ public sealed class MarkAnnouncementAsReadCommandHandler( // 2. 仅允许已发布且在有效期内的公告标记已读 var now = DateTime.UtcNow; - if (announcement.Status != AnnouncementStatus.Published || !announcement.IsActive) + if (announcement.Status != AnnouncementStatus.Published) { return null; } diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs index abc308a..e3b75f3 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/PublishAnnouncementCommandHandler.cs @@ -56,7 +56,6 @@ public sealed class PublishAnnouncementCommandHandler( announcement.Status = AnnouncementStatus.Published; announcement.PublishedAt = DateTime.UtcNow; announcement.RevokedAt = null; - announcement.IsActive = true; announcement.RowVersion = request.RowVersion; await announcementRepository.UpdateAsync(announcement, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs index 648a755..1e4d3bf 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/RevokeAnnouncementCommandHandler.cs @@ -50,7 +50,6 @@ public sealed class RevokeAnnouncementCommandHandler( // 3. 撤销公告 announcement.Status = AnnouncementStatus.Revoked; announcement.RevokedAt = DateTime.UtcNow; - announcement.IsActive = false; announcement.RowVersion = request.RowVersion; await announcementRepository.UpdateAsync(announcement, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs index 4b0a989..0a661a0 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/UpdateTenantAnnouncementCommandHandler.cs @@ -49,7 +49,6 @@ public sealed class UpdateTenantAnnouncementCommandHandler(ITenantAnnouncementRe announcement.Content = request.Content; announcement.TargetType = string.IsNullOrWhiteSpace(request.TargetType) ? announcement.TargetType : request.TargetType.Trim(); announcement.TargetParameters = request.TargetParameters; - announcement.IsActive = false; announcement.RowVersion = request.RowVersion; // 4. 持久化 diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs index a5b05f0..125f991 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/TenantMapping.cs @@ -1,5 +1,6 @@ using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Domain.Tenants.Enums; namespace TakeoutSaaS.Application.App.Tenants; @@ -193,7 +194,7 @@ internal static class TenantMapping TargetType = announcement.TargetType, TargetParameters = announcement.TargetParameters, RowVersion = announcement.RowVersion, - IsActive = announcement.IsActive, + IsActive = announcement.Status == AnnouncementStatus.Published, IsRead = isRead, ReadAt = readAt }; diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 9d21e02..40a3e0d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -774,14 +774,14 @@ public sealed class TakeoutAppDbContext( builder.Property(x => x.TargetType).HasMaxLength(64).IsRequired(); builder.Property(x => x.TargetParameters).HasColumnType("text"); builder.Property(x => x.Priority).IsRequired(); - builder.Property(x => x.IsActive).IsRequired(); + builder.Property("IsActive").IsRequired(); builder.Property(x => x.RowVersion) .IsRowVersion() .IsConcurrencyToken() .HasColumnType("bytea"); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => new { x.TenantId, x.AnnouncementType, x.IsActive }); + builder.HasIndex("TenantId", "AnnouncementType", "IsActive"); builder.HasIndex(x => new { x.TenantId, x.EffectiveFrom, x.EffectiveTo }); builder.HasIndex(x => new { x.TenantId, x.Status, x.EffectiveFrom }); builder.HasIndex(x => new { x.Status, x.EffectiveFrom }) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs index 5d64847..9249fd1 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfTenantAnnouncementRepository.cs @@ -50,7 +50,9 @@ public sealed class EfTenantAnnouncementRepository(TakeoutAppDbContext context) if (isActive.HasValue) { - query = query.Where(x => x.IsActive == isActive.Value); + query = isActive.Value + ? query.Where(x => x.Status == AnnouncementStatus.Published) + : query.Where(x => x.Status != AnnouncementStatus.Published); } if (effectiveFrom.HasValue) @@ -114,7 +116,9 @@ public sealed class EfTenantAnnouncementRepository(TakeoutAppDbContext context) if (isActive.HasValue) { - announcementQuery = announcementQuery.Where(x => x.IsActive == isActive.Value); + announcementQuery = isActive.Value + ? announcementQuery.Where(x => x.Status == AnnouncementStatus.Published) + : announcementQuery.Where(x => x.Status != AnnouncementStatus.Published); } if (effectiveAt.HasValue) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.Designer.cs new file mode 100644 index 0000000..8ab4246 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.Designer.cs @@ -0,0 +1,726 @@ +// +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("20251226231440_GrantIdentityUserPermissionsToSuperAdmin")] + partial class GrantIdentityUserPermissionsToSuperAdmin + { + /// + 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") + .HasColumnType("text") + .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("Email") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("邮箱(租户内唯一)。"); + + b.Property("FailedLoginCount") + .HasColumnType("integer") + .HasComment("登录失败次数。"); + + b.Property("LastLoginAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近登录时间(UTC)。"); + + b.Property("LockedUntil") + .HasColumnType("timestamp with time zone") + .HasComment("锁定截止时间(UTC)。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("所属商户(平台管理员为空)。"); + + b.Property("MustChangePassword") + .HasColumnType("boolean") + .HasComment("是否强制修改密码。"); + + b.Property("PasswordHash") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("密码哈希。"); + + b.Property("Phone") + .HasMaxLength(32) + .HasColumnType("character varying(32)") + .HasComment("手机号(租户内唯一)。"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .IsRequired() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea") + .HasComment("并发控制字段。"); + + b.Property("Status") + .HasColumnType("integer") + .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.HasIndex("TenantId", "Email") + .IsUnique() + .HasFilter("\"Email\" IS NOT NULL"); + + b.HasIndex("TenantId", "Phone") + .IsUnique() + .HasFilter("\"Phone\" IS NOT NULL"); + + 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") + .HasColumnType("text") + .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/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.cs new file mode 100644 index 0000000..333e9aa --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.cs @@ -0,0 +1,135 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + /// + public partial class GrantIdentityUserPermissionsToSuperAdmin : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @"WITH target_roles AS ( + SELECT ""Id"" AS role_id, ""TenantId"" AS tenant_id + FROM ""roles"" + WHERE ""Code"" IN ('super-admin', 'SUPER_ADMIN', 'PlatformAdmin', 'platform-admin') + AND ""DeletedAt"" IS NULL +), + target_permissions AS ( + SELECT DISTINCT tr.tenant_id, pc.code + FROM target_roles tr + CROSS JOIN (VALUES + ('identity:user:read'), + ('identity:user:create'), + ('identity:user:update'), + ('identity:user:delete'), + ('identity:user:status'), + ('identity:user:reset-password'), + ('identity:user:batch') + ) AS pc(code) +) +INSERT INTO ""permissions"" ( + ""TenantId"", + ""Name"", + ""Code"", + ""Description"", + ""CreatedAt"", + ""CreatedBy"", + ""UpdatedAt"", + ""UpdatedBy"", + ""DeletedAt"", + ""DeletedBy"" +) +SELECT + tp.tenant_id, + tp.code, + tp.code, + CONCAT('Seed permission ', tp.code), + NOW(), + NULL, + NULL, + NULL, + NULL, + NULL +FROM target_permissions tp +ON CONFLICT (""TenantId"", ""Code"") DO NOTHING;" + ); + migrationBuilder.Sql( + @"WITH target_roles AS ( + SELECT ""Id"" AS role_id, ""TenantId"" AS tenant_id + FROM ""roles"" + WHERE ""Code"" IN ('super-admin', 'SUPER_ADMIN', 'PlatformAdmin', 'platform-admin') + AND ""DeletedAt"" IS NULL +) +INSERT INTO ""role_permissions"" ( + ""TenantId"", + ""RoleId"", + ""PermissionId"", + ""CreatedAt"", + ""CreatedBy"", + ""UpdatedAt"", + ""UpdatedBy"", + ""DeletedAt"", + ""DeletedBy"" +) +SELECT + tr.tenant_id, + tr.role_id, + p.""Id"", + NOW(), + NULL, + NULL, + NULL, + NULL, + NULL +FROM target_roles tr +JOIN ""permissions"" p + ON p.""TenantId"" = tr.tenant_id + AND p.""Code"" IN ( + 'identity:user:read', + 'identity:user:create', + 'identity:user:update', + 'identity:user:delete', + 'identity:user:status', + 'identity:user:reset-password', + 'identity:user:batch' + ) +WHERE p.""DeletedAt"" IS NULL +ON CONFLICT (""TenantId"", ""RoleId"", ""PermissionId"") DO NOTHING;" + ); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.Sql( + @"WITH target_roles AS ( + SELECT ""Id"" AS role_id, ""TenantId"" AS tenant_id + FROM ""roles"" + WHERE ""Code"" IN ('super-admin', 'SUPER_ADMIN', 'PlatformAdmin', 'platform-admin') + AND ""DeletedAt"" IS NULL +), + target_permissions AS ( + SELECT ""Id"" AS permission_id, ""TenantId"" AS tenant_id + FROM ""permissions"" + WHERE ""Code"" IN ( + 'identity:user:read', + 'identity:user:create', + 'identity:user:update', + 'identity:user:delete', + 'identity:user:status', + 'identity:user:reset-password', + 'identity:user:batch' + ) +) +DELETE FROM ""role_permissions"" rp +USING target_roles tr, target_permissions tp +WHERE rp.""TenantId"" = tr.tenant_id + AND rp.""RoleId"" = tr.role_id + AND rp.""PermissionId"" = tp.permission_id;" + ); + } + } +} diff --git a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs index 923c59d..81f60f7 100644 --- a/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs +++ b/tests/TakeoutSaaS.Application.Tests/App/Tenants/Handlers/GetUnreadAnnouncementsQueryHandlerTests.cs @@ -25,9 +25,9 @@ public sealed class GetUnreadAnnouncementsQueryHandlerTests var announcements = new List { - AnnouncementTestData.CreateAnnouncement(1, 55, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1), status: AnnouncementStatus.Published, isActive: true), - AnnouncementTestData.CreateAnnouncement(2, 55, priority: 3, effectiveFrom: DateTime.UtcNow.AddDays(-2), status: AnnouncementStatus.Published, isActive: true), - AnnouncementTestData.CreateAnnouncement(3, 55, priority: 2, effectiveFrom: DateTime.UtcNow, status: AnnouncementStatus.Published, isActive: true) + AnnouncementTestData.CreateAnnouncement(1, 55, priority: 1, effectiveFrom: DateTime.UtcNow.AddDays(-1), status: AnnouncementStatus.Published), + AnnouncementTestData.CreateAnnouncement(2, 55, priority: 3, effectiveFrom: DateTime.UtcNow.AddDays(-2), status: AnnouncementStatus.Published), + AnnouncementTestData.CreateAnnouncement(3, 55, priority: 2, effectiveFrom: DateTime.UtcNow, status: AnnouncementStatus.Published) }; var announcementRepository = new Mock(); diff --git a/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs b/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs index 86516ae..6ac7f18 100644 --- a/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs +++ b/tests/TakeoutSaaS.Application.Tests/TestUtilities/AnnouncementTestData.cs @@ -52,8 +52,7 @@ public static class AnnouncementTestData long tenantId, int priority, DateTime effectiveFrom, - AnnouncementStatus status = AnnouncementStatus.Draft, - bool isActive = false) + AnnouncementStatus status = AnnouncementStatus.Draft) => new() { Id = id, @@ -68,7 +67,6 @@ public static class AnnouncementTestData Status = status, TargetType = string.Empty, TargetParameters = null, - IsActive = isActive, RowVersion = new byte[] { 1, 1, 1 } }; } diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs index f8922cb..f370a4b 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementRegressionTests.cs @@ -11,15 +11,14 @@ namespace TakeoutSaaS.Integration.Tests.App.Tenants; public sealed class AnnouncementRegressionTests { [Fact] - public async Task GivenLegacyIsActiveAnnouncement_WhenSearchByIsActive_ThenReturns() + public async Task GivenPublishedAnnouncement_WhenSearchByIsActive_ThenReturns() { // Arrange using var database = new SqliteTestDatabase(); using var context = database.CreateContext(tenantId: 600, userId: 12); var legacy = CreateAnnouncement(tenantId: 600, id: 9100); - legacy.Status = AnnouncementStatus.Draft; - legacy.IsActive = true; + legacy.Status = AnnouncementStatus.Published; context.TenantAnnouncements.Add(legacy); await context.SaveChangesAsync(); @@ -52,7 +51,6 @@ public sealed class AnnouncementRegressionTests var announcement = CreateAnnouncement(tenantId: 700, id: 9101); announcement.Status = AnnouncementStatus.Draft; - announcement.IsActive = false; context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); @@ -95,7 +93,6 @@ public sealed class AnnouncementRegressionTests Status = AnnouncementStatus.Draft, TargetType = "ALL_TENANTS", TargetParameters = null, - IsActive = false, RowVersion = new byte[] { 1 } }; } diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs index 4561e5f..29c793a 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/AnnouncementWorkflowTests.cs @@ -47,7 +47,6 @@ public sealed class AnnouncementWorkflowTests using var verifyContext = database.CreateContext(tenantId: 100); var persisted = await verifyContext.TenantAnnouncements.FirstAsync(x => x.Id == announcement.Id); persisted.Status.Should().Be(AnnouncementStatus.Published); - persisted.IsActive.Should().BeTrue(); persisted.PublishedAt.Should().NotBeNull(); } @@ -60,7 +59,6 @@ public sealed class AnnouncementWorkflowTests var announcement = CreateDraftAnnouncement(tenantId: 200, id: 9002); announcement.Status = AnnouncementStatus.Published; - announcement.IsActive = true; context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); @@ -85,7 +83,6 @@ public sealed class AnnouncementWorkflowTests using var verifyContext = database.CreateContext(tenantId: 200); var persisted = await verifyContext.TenantAnnouncements.FirstAsync(x => x.Id == announcement.Id); persisted.Status.Should().Be(AnnouncementStatus.Revoked); - persisted.IsActive.Should().BeFalse(); persisted.RevokedAt.Should().NotBeNull(); } @@ -98,7 +95,6 @@ public sealed class AnnouncementWorkflowTests var announcement = CreateDraftAnnouncement(tenantId: 300, id: 9003); announcement.Status = AnnouncementStatus.Revoked; - announcement.IsActive = false; announcement.RevokedAt = DateTime.UtcNow.AddMinutes(-5); context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); @@ -124,7 +120,6 @@ public sealed class AnnouncementWorkflowTests using var verifyContext = database.CreateContext(tenantId: 300); var persisted = await verifyContext.TenantAnnouncements.FirstAsync(x => x.Id == announcement.Id); persisted.Status.Should().Be(AnnouncementStatus.Published); - persisted.IsActive.Should().BeTrue(); persisted.RevokedAt.Should().BeNull(); } @@ -137,7 +132,6 @@ public sealed class AnnouncementWorkflowTests var announcement = CreateDraftAnnouncement(tenantId: 400, id: 9004); announcement.Status = AnnouncementStatus.Published; - announcement.IsActive = true; context.TenantAnnouncements.Add(announcement); await context.SaveChangesAsync(); context.ChangeTracker.Clear(); @@ -216,7 +210,6 @@ public sealed class AnnouncementWorkflowTests Status = AnnouncementStatus.Draft, TargetType = "ALL_TENANTS", TargetParameters = null, - IsActive = false, RowVersion = new byte[] { 1 } }; } diff --git a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs index e5e71a8..cb7697f 100644 --- a/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/App/Tenants/TenantAnnouncementRepositoryScopeTests.cs @@ -53,7 +53,6 @@ public sealed class TenantAnnouncementRepositoryScopeTests PublisherScope = PublisherScope.Tenant, Status = AnnouncementStatus.Draft, TargetType = "ALL_TENANTS", - IsActive = false, RowVersion = new byte[] { 1 } }; } diff --git a/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs b/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs index 1bcf59c..bc742af 100644 --- a/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs +++ b/tests/TakeoutSaaS.Integration.Tests/Performance/AnnouncementQueryPerformanceTests.cs @@ -38,7 +38,6 @@ public sealed class AnnouncementQueryPerformanceTests Status = AnnouncementStatus.Published, TargetType = targetType, TargetParameters = targetParameters, - IsActive = true, RowVersion = new byte[] { 1 } }); }