From d7434e6e8b09a4ee46626162953c4d0448bddad4 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 27 Dec 2025 07:19:27 +0800 Subject: [PATCH 1/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=85=AC=E5=91=8A?= =?UTF-8?q?=E8=BF=87=E6=97=B6=E8=AD=A6=E5=91=8A=E5=B9=B6=E8=A1=A5=E9=BD=90?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E6=9D=83=E9=99=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreateTenantAnnouncementCommandHandler.cs | 3 +- .../MarkAnnouncementAsReadCommandHandler.cs | 2 +- .../PublishAnnouncementCommandHandler.cs | 1 - .../RevokeAnnouncementCommandHandler.cs | 1 - .../UpdateTenantAnnouncementCommandHandler.cs | 1 - .../App/Tenants/TenantMapping.cs | 3 +- .../App/Persistence/TakeoutAppDbContext.cs | 4 +- .../EfTenantAnnouncementRepository.cs | 8 +- ...ityUserPermissionsToSuperAdmin.Designer.cs | 726 ++++++++++++++++++ ...rantIdentityUserPermissionsToSuperAdmin.cs | 135 ++++ ...GetUnreadAnnouncementsQueryHandlerTests.cs | 6 +- .../TestUtilities/AnnouncementTestData.cs | 4 +- .../Tenants/AnnouncementRegressionTests.cs | 7 +- .../App/Tenants/AnnouncementWorkflowTests.cs | 7 - .../TenantAnnouncementRepositoryScopeTests.cs | 1 - .../AnnouncementQueryPerformanceTests.cs | 1 - 16 files changed, 879 insertions(+), 31 deletions(-) create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251226231440_GrantIdentityUserPermissionsToSuperAdmin.cs 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 } }); } From 0d1db9a11a0c6858d8381d91c24a7fbaf0d7931b Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 27 Dec 2025 09:33:16 +0800 Subject: [PATCH 2/6] =?UTF-8?q?=E8=BA=AB=E4=BB=BD=E6=93=8D=E4=BD=9C?= =?UTF-8?q?=E6=97=A5=E5=BF=97=E6=94=B9=E9=80=A0=E4=B8=BAOutbox=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E6=97=A5=E5=BF=97=E5=BA=93=E8=BF=9E=E6=8E=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Directory.Build.props | 5 + src/Api/TakeoutSaaS.AdminApi/Program.cs | 2 + .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 4 +- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 4 +- .../appsettings.Development.json | 4 +- .../appsettings.Production.json | 4 +- .../IIdentityOperationLogPublisher.cs | 17 + .../Events/IdentityUserOperationLogMessage.cs | 47 + ...atchIdentityUserOperationCommandHandler.cs | 15 +- .../ChangeIdentityUserStatusCommandHandler.cs | 17 +- .../CreateIdentityUserCommandHandler.cs | 35 +- .../DeleteIdentityUserCommandHandler.cs | 20 +- ...ResetIdentityUserPasswordCommandHandler.cs | 16 +- .../RestoreIdentityUserCommandHandler.cs | 22 +- .../UpdateIdentityUserCommandHandler.cs | 44 +- .../Extensions/ServiceCollectionExtensions.cs | 2 + .../Identity/Persistence/IdentityDbContext.cs | 3 + .../IdentityUserOperationLogConsumer.cs | 72 ++ ...ionLogOutboxServiceCollectionExtensions.cs | 56 ++ .../Persistence/OperationLogInboxMessage.cs | 19 + .../Logs/Persistence/TakeoutLogsDbContext.cs | 15 + .../IdentityOperationLogPublisher.cs | 15 + ...251227004313_AddIdentityOutbox.Designer.cs | 847 ++++++++++++++++++ .../20251227004313_AddIdentityOutbox.cs | 101 +++ .../IdentityDbContextModelSnapshot.cs | 121 +++ ...7_AddOperationLogInboxMessages.Designer.cs | 358 ++++++++ ...1227004337_AddOperationLogInboxMessages.cs | 43 + .../TakeoutLogsDbContextModelSnapshot.cs | 23 + 30 files changed, 1840 insertions(+), 99 deletions(-) create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Abstractions/IIdentityOperationLogPublisher.cs create mode 100644 src/Application/TakeoutSaaS.Application/Identity/Events/IdentityUserOperationLogMessage.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Consumers/IdentityUserOperationLogConsumer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Extensions/OperationLogOutboxServiceCollectionExtensions.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/OperationLogInboxMessage.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Publishers/IdentityOperationLogPublisher.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.Designer.cs create mode 100644 src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.cs diff --git a/Directory.Build.props b/Directory.Build.props index 3bba554..da983d4 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -12,4 +12,9 @@ + + + + + diff --git a/src/Api/TakeoutSaaS.AdminApi/Program.cs b/src/Api/TakeoutSaaS.AdminApi/Program.cs index 211c70d..5d8837b 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Program.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Program.cs @@ -13,6 +13,7 @@ using TakeoutSaaS.Application.Sms.Extensions; using TakeoutSaaS.Application.Storage.Extensions; using TakeoutSaaS.Infrastructure.App.Extensions; using TakeoutSaaS.Infrastructure.Identity.Extensions; +using TakeoutSaaS.Infrastructure.Logs.Extensions; using TakeoutSaaS.Module.Authorization.Extensions; using TakeoutSaaS.Module.Dictionary.Extensions; using TakeoutSaaS.Module.Messaging.Extensions; @@ -76,6 +77,7 @@ builder.Services.AddSmsModule(builder.Configuration); builder.Services.AddSmsApplication(builder.Configuration); builder.Services.AddMessagingModule(builder.Configuration); builder.Services.AddMessagingApplication(); +builder.Services.AddOperationLogOutbox(builder.Configuration); builder.Services.AddSchedulerModule(builder.Configuration); builder.Services.AddHealthChecks(); builder.Services.AddRateLimiter(options => diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json index ad8b9b8..247eaa4 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Development.json @@ -32,9 +32,9 @@ "MaxRetryDelaySeconds": 5 }, "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json index a9ccc27..e6b7183 100644 --- a/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.AdminApi/appsettings.Production.json @@ -32,9 +32,9 @@ "MaxRetryDelaySeconds": 5 }, "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json index 6fa3d7f..7fd933a 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Development.json @@ -29,9 +29,9 @@ "MaxRetryDelaySeconds": 5 }, "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json index 6fa3d7f..7fd933a 100644 --- a/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.MiniApi/appsettings.Production.json @@ -29,9 +29,9 @@ "MaxRetryDelaySeconds": 5 }, "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json index 8e4e77e..0fe565e 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Development.json @@ -29,9 +29,9 @@ "MaxRetryDelaySeconds": 5 }, "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json index 8e4e77e..0fe565e 100644 --- a/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json +++ b/src/Api/TakeoutSaaS.UserApi/appsettings.Production.json @@ -29,9 +29,9 @@ "MaxRetryDelaySeconds": 5 }, "LogsDatabase": { - "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", + "Write": "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50", "Reads": [ - "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=pg_roles;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" + "Host=120.53.222.17;Port=5432;Database=takeout_logs_db;Username=logs_user;Password=Logs112233;Pooling=true;Minimum Pool Size=5;Maximum Pool Size=50" ], "CommandTimeoutSeconds": 30, "MaxRetryCount": 3, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IIdentityOperationLogPublisher.cs b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IIdentityOperationLogPublisher.cs new file mode 100644 index 0000000..2b0add6 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Abstractions/IIdentityOperationLogPublisher.cs @@ -0,0 +1,17 @@ +using TakeoutSaaS.Application.Identity.Events; + +namespace TakeoutSaaS.Application.Identity.Abstractions; + +/// +/// 身份模块操作日志发布器。 +/// +public interface IIdentityOperationLogPublisher +{ + /// + /// 发布身份模块操作日志消息。 + /// + /// 操作日志消息。 + /// 取消标记。 + /// 异步任务。 + Task PublishAsync(IdentityUserOperationLogMessage message, CancellationToken cancellationToken = default); +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Events/IdentityUserOperationLogMessage.cs b/src/Application/TakeoutSaaS.Application/Identity/Events/IdentityUserOperationLogMessage.cs new file mode 100644 index 0000000..611a3b9 --- /dev/null +++ b/src/Application/TakeoutSaaS.Application/Identity/Events/IdentityUserOperationLogMessage.cs @@ -0,0 +1,47 @@ +namespace TakeoutSaaS.Application.Identity.Events; + +/// +/// 身份用户操作日志消息。 +/// +public sealed record IdentityUserOperationLogMessage +{ + /// + /// 操作类型。 + /// + public string OperationType { get; init; } = string.Empty; + + /// + /// 目标类型。 + /// + public string TargetType { get; init; } = string.Empty; + + /// + /// 目标 ID 列表(JSON)。 + /// + public string? TargetIds { get; init; } + + /// + /// 操作人 ID。 + /// + public string? OperatorId { get; init; } + + /// + /// 操作人名称。 + /// + public string? OperatorName { get; init; } + + /// + /// 操作参数(JSON)。 + /// + public string? Parameters { get; init; } + + /// + /// 操作结果(JSON)。 + /// + public string? Result { get; init; } + + /// + /// 是否成功。 + /// + public bool Success { get; init; } +} diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs index 11dae75..31f4ddc 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs @@ -3,12 +3,11 @@ using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Application.Identity.Models; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; @@ -26,7 +25,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository) + IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// @@ -208,7 +207,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( })); } - // 7. (空行后) 写入操作日志 + // 7. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -217,7 +216,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:batch", TargetType = "identity_user", @@ -228,8 +227,10 @@ public sealed class BatchIdentityUserOperationCommandHandler( Result = JsonSerializer.Serialize(new { successCount, failureCount = failures.Count }), Success = failures.Count == 0 }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 8. (空行后) 写入 Outbox 并保存变更 + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); return new BatchIdentityUserOperationResult { diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs index be0534d..11da3a9 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs @@ -2,10 +2,9 @@ using MediatR; using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; @@ -23,7 +22,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository) + IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// @@ -81,9 +80,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( throw new BusinessException(ErrorCodes.BadRequest, "无效的用户状态"); } - await identityUserRepository.SaveChangesAsync(cancellationToken); - - // 6. (空行后) 写入操作日志 + // 6. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -92,7 +89,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:status-change", TargetType = "identity_user", @@ -109,8 +106,10 @@ public sealed class ChangeIdentityUserStatusCommandHandler( Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 7. (空行后) 写入 Outbox 并保存变更 + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); return true; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs index 68a1064..edc5d3f 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs @@ -4,14 +4,14 @@ using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; +using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -28,7 +28,8 @@ public sealed class CreateIdentityUserCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository, + IIdentityOperationLogPublisher operationLogPublisher, + IIdGenerator idGenerator, IMediator mediator) : IRequestHandler { @@ -85,6 +86,7 @@ public sealed class CreateIdentityUserCommandHandler( // 6. (空行后) 创建用户实体 var user = new IdentityUser { + Id = idGenerator.NextId(), TenantId = tenantId, Account = account, DisplayName = displayName, @@ -100,17 +102,7 @@ public sealed class CreateIdentityUserCommandHandler( }; user.PasswordHash = passwordHasher.HashPassword(user, request.Password); - // 7. (空行后) 持久化用户 - await identityUserRepository.AddAsync(user, cancellationToken); - await identityUserRepository.SaveChangesAsync(cancellationToken); - - // 8. (空行后) 绑定角色 - if (roleIds.Length > 0) - { - await userRoleRepository.ReplaceUserRolesAsync(tenantId, user.Id, roleIds, cancellationToken); - } - - // 9. (空行后) 写入操作日志 + // 7. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -119,7 +111,7 @@ public sealed class CreateIdentityUserCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:create", TargetType = "identity_user", @@ -138,8 +130,17 @@ public sealed class CreateIdentityUserCommandHandler( Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 8. (空行后) 持久化用户并写入 Outbox + await identityUserRepository.AddAsync(user, cancellationToken); + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); + + // 9. (空行后) 绑定角色 + if (roleIds.Length > 0) + { + await userRoleRepository.ReplaceUserRolesAsync(tenantId, user.Id, roleIds, cancellationToken); + } // 10. (空行后) 返回用户详情 var detail = await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs index 728aab8..74d3eba 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs @@ -2,10 +2,9 @@ using MediatR; using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; @@ -23,7 +22,7 @@ public sealed class DeleteIdentityUserCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository) + IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// @@ -60,11 +59,7 @@ public sealed class DeleteIdentityUserCommandHandler( await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken); } - // 5. (空行后) 软删除用户 - await identityUserRepository.RemoveAsync(user, cancellationToken); - await identityUserRepository.SaveChangesAsync(cancellationToken); - - // 6. (空行后) 写入操作日志 + // 5. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -73,7 +68,7 @@ public sealed class DeleteIdentityUserCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:delete", TargetType = "identity_user", @@ -84,8 +79,11 @@ public sealed class DeleteIdentityUserCommandHandler( Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 6. (空行后) 软删除用户并写入 Outbox + await identityUserRepository.RemoveAsync(user, cancellationToken); + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); return true; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs index 1e010e2..2cb7ac6 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs @@ -3,10 +3,9 @@ using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; @@ -23,7 +22,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository) + IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// @@ -66,9 +65,8 @@ public sealed class ResetIdentityUserPasswordCommandHandler( { user.Status = IdentityUserStatus.Active; } - await identityUserRepository.SaveChangesAsync(cancellationToken); - // 6. (空行后) 写入操作日志 + // 6. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -77,7 +75,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:password-reset", TargetType = "identity_user", @@ -88,8 +86,10 @@ public sealed class ResetIdentityUserPasswordCommandHandler( Result = JsonSerializer.Serialize(new { userId = user.Id, expiresAt }), Success = true }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 7. (空行后) 写入 Outbox 并保存变更 + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); return new ResetIdentityUserPasswordResult { diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs index eeb8da2..67ebe55 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs @@ -2,9 +2,8 @@ using MediatR; using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; @@ -20,7 +19,7 @@ public sealed class RestoreIdentityUserCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository) + IIdentityOperationLogPublisher operationLogPublisher) : IRequestHandler { /// @@ -54,12 +53,7 @@ public sealed class RestoreIdentityUserCommandHandler( return false; } - // 4. (空行后) 恢复软删除状态 - user.DeletedAt = null; - user.DeletedBy = null; - await identityUserRepository.SaveChangesAsync(cancellationToken); - - // 5. (空行后) 写入操作日志 + // 4. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -68,7 +62,7 @@ public sealed class RestoreIdentityUserCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:restore", TargetType = "identity_user", @@ -79,8 +73,12 @@ public sealed class RestoreIdentityUserCommandHandler( Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 5. (空行后) 恢复软删除状态并写入 Outbox + user.DeletedAt = null; + user.DeletedBy = null; + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); return true; } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs index 30d2827..2f771e7 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs @@ -3,11 +3,10 @@ using System.Text.Json; using TakeoutSaaS.Application.Identity.Abstractions; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Entities; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; using TakeoutSaaS.Shared.Abstractions.Security; @@ -25,7 +24,7 @@ public sealed class UpdateIdentityUserCommandHandler( ITenantProvider tenantProvider, ICurrentUserAccessor currentUserAccessor, IAdminAuthService adminAuthService, - IOperationLogRepository operationLogRepository, + IIdentityOperationLogPublisher operationLogPublisher, IMediator mediator) : IRequestHandler { @@ -93,23 +92,7 @@ public sealed class UpdateIdentityUserCommandHandler( user.Avatar = string.IsNullOrWhiteSpace(request.Avatar) ? null : request.Avatar.Trim(); user.RowVersion = request.RowVersion; - // 6. (空行后) 持久化用户更新 - try - { - await identityUserRepository.SaveChangesAsync(cancellationToken); - } - catch (Exception ex) when (IsConcurrencyException(ex)) - { - throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试"); - } - - // 7. (空行后) 覆盖角色绑定(仅当显式传入时) - if (roleIds != null) - { - await userRoleRepository.ReplaceUserRolesAsync(user.TenantId, user.Id, roleIds, cancellationToken); - } - - // 8. (空行后) 写入操作日志 + // 6. (空行后) 构建操作日志消息 var operatorName = string.IsNullOrWhiteSpace(operatorProfile.DisplayName) ? operatorProfile.Account : operatorProfile.DisplayName; @@ -118,7 +101,7 @@ public sealed class UpdateIdentityUserCommandHandler( operatorName = $"user:{currentUserAccessor.UserId}"; } - var log = new OperationLog + var logMessage = new IdentityUserOperationLogMessage { OperationType = "identity-user:update", TargetType = "identity_user", @@ -136,8 +119,23 @@ public sealed class UpdateIdentityUserCommandHandler( Result = JsonSerializer.Serialize(new { userId = user.Id }), Success = true }; - await operationLogRepository.AddAsync(log, cancellationToken); - await operationLogRepository.SaveChangesAsync(cancellationToken); + + // 7. (空行后) 持久化用户更新并写入 Outbox + try + { + await operationLogPublisher.PublishAsync(logMessage, cancellationToken); + await identityUserRepository.SaveChangesAsync(cancellationToken); + } + catch (Exception ex) when (IsConcurrencyException(ex)) + { + throw new BusinessException(ErrorCodes.Conflict, "用户数据已被修改,请刷新后重试"); + } + + // 8. (空行后) 覆盖角色绑定(仅当显式传入时) + if (roleIds != null) + { + await userRoleRepository.ReplaceUserRolesAsync(user.TenantId, user.Id, roleIds, cancellationToken); + } // 9. (空行后) 返回用户详情 return await mediator.Send(new GetIdentityUserDetailQuery { UserId = user.Id }, cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs index 266b011..217f916 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Extensions/ServiceCollectionExtensions.cs @@ -8,6 +8,7 @@ using TakeoutSaaS.Infrastructure.Identity.Options; using TakeoutSaaS.Infrastructure.Identity.Persistence; using TakeoutSaaS.Infrastructure.Identity.Repositories; using TakeoutSaaS.Infrastructure.Identity.Services; +using TakeoutSaaS.Infrastructure.Logs.Publishers; using TakeoutSaaS.Shared.Abstractions.Constants; using DomainIdentityUser = TakeoutSaaS.Domain.Identity.Entities.IdentityUser; @@ -60,6 +61,7 @@ public static class ServiceCollectionExtensions services.AddScoped(); services.AddScoped(); services.AddScoped, PasswordHasher>(); + services.AddScoped(); services.AddOptions() .Bind(configuration.GetSection("Identity:Jwt")) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 792fc14..9d0467b 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -1,3 +1,4 @@ +using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Domain.Identity.Entities; @@ -79,6 +80,8 @@ public sealed class IdentityDbContext( ConfigureUserRole(modelBuilder.Entity()); ConfigureRolePermission(modelBuilder.Entity()); ConfigureMenuDefinition(modelBuilder.Entity()); + modelBuilder.AddOutboxMessageEntity(); + modelBuilder.AddOutboxStateEntity(); ApplyTenantQueryFilters(modelBuilder); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Consumers/IdentityUserOperationLogConsumer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Consumers/IdentityUserOperationLogConsumer.cs new file mode 100644 index 0000000..12dcdea --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Consumers/IdentityUserOperationLogConsumer.cs @@ -0,0 +1,72 @@ +using MassTransit; +using Microsoft.EntityFrameworkCore; +using Npgsql; +using TakeoutSaaS.Application.Identity.Events; +using TakeoutSaaS.Domain.Tenants.Entities; +using TakeoutSaaS.Infrastructure.Logs.Persistence; + +namespace TakeoutSaaS.Infrastructure.Logs.Consumers; + +/// +/// 身份用户操作日志消费者。 +/// +public sealed class IdentityUserOperationLogConsumer(TakeoutLogsDbContext logsContext) : IConsumer +{ + /// + public async Task Consume(ConsumeContext context) + { + // 1. 校验消息标识并进行幂等检查 + var messageId = context.MessageId; + if (!messageId.HasValue) + { + throw new InvalidOperationException("缺少 MessageId,无法进行日志幂等处理。"); + } + + var exists = await logsContext.OperationLogInboxMessages + .AsNoTracking() + .AnyAsync(x => x.MessageId == messageId.Value, context.CancellationToken); + if (exists) + { + return; + } + + // 2. (空行后) 构建日志实体与去重记录 + var message = context.Message; + var log = new OperationLog + { + OperationType = message.OperationType, + TargetType = message.TargetType, + TargetIds = message.TargetIds, + OperatorId = message.OperatorId, + OperatorName = message.OperatorName, + Parameters = message.Parameters, + Result = message.Result, + Success = message.Success + }; + logsContext.OperationLogInboxMessages.Add(new OperationLogInboxMessage + { + MessageId = messageId.Value, + ConsumedAt = DateTime.UtcNow + }); + logsContext.OperationLogs.Add(log); + + // 3. (空行后) 保存并处理并发去重冲突 + try + { + await logsContext.SaveChangesAsync(context.CancellationToken); + } + catch (DbUpdateException ex) when (IsDuplicateMessage(ex)) + { + return; + } + } + + private static bool IsDuplicateMessage(DbUpdateException exception) + { + if (exception.InnerException is PostgresException postgresException) + { + return postgresException.SqlState == PostgresErrorCodes.UniqueViolation; + } + return false; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Extensions/OperationLogOutboxServiceCollectionExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Extensions/OperationLogOutboxServiceCollectionExtensions.cs new file mode 100644 index 0000000..c4a0029 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Extensions/OperationLogOutboxServiceCollectionExtensions.cs @@ -0,0 +1,56 @@ +using MassTransit; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TakeoutSaaS.Infrastructure.Identity.Persistence; +using TakeoutSaaS.Infrastructure.Logs.Consumers; +using TakeoutSaaS.Module.Messaging.Options; + +namespace TakeoutSaaS.Infrastructure.Logs.Extensions; + +/// +/// 操作日志 Outbox 注册扩展。 +/// +public static class OperationLogOutboxServiceCollectionExtensions +{ + /// + /// 注册操作日志 Outbox 与消费者。 + /// + /// 服务集合。 + /// 配置源。 + /// 服务集合。 + public static IServiceCollection AddOperationLogOutbox(this IServiceCollection services, IConfiguration configuration) + { + // 1. 读取 RabbitMQ 配置 + var options = configuration.GetSection("RabbitMQ").Get(); + if (options == null) + { + throw new InvalidOperationException("缺少 RabbitMQ 配置。"); + } + + // 2. (空行后) 注册 MassTransit 与 Outbox + services.AddMassTransit(configurator => + { + configurator.AddConsumer(); + configurator.AddEntityFrameworkOutbox(outbox => + { + outbox.UsePostgres(); + outbox.UseBusOutbox(); + }); + configurator.UsingRabbitMq((context, cfg) => + { + var virtualHost = string.IsNullOrWhiteSpace(options.VirtualHost) ? "/" : options.VirtualHost.Trim(); + var virtualHostPath = virtualHost == "/" ? "/" : $"/{virtualHost.TrimStart('/')}"; + var hostUri = new Uri($"rabbitmq://{options.Host}:{options.Port}{virtualHostPath}"); + cfg.Host(hostUri, host => + { + host.Username(options.Username); + host.Password(options.Password); + }); + cfg.PrefetchCount = options.PrefetchCount; + cfg.ConfigureEndpoints(context); + }); + }); + // 3. (空行后) 返回服务集合 + return services; + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/OperationLogInboxMessage.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/OperationLogInboxMessage.cs new file mode 100644 index 0000000..8246ced --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/OperationLogInboxMessage.cs @@ -0,0 +1,19 @@ +using TakeoutSaaS.Shared.Abstractions.Entities; + +namespace TakeoutSaaS.Infrastructure.Logs.Persistence; + +/// +/// 操作日志消息消费去重记录。 +/// +public sealed class OperationLogInboxMessage : EntityBase +{ + /// + /// 消息唯一标识。 + /// + public Guid MessageId { get; set; } + + /// + /// 消费时间(UTC)。 + /// + public DateTime ConsumedAt { get; set; } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs index 1743a1e..655e566 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs @@ -35,6 +35,11 @@ public sealed class TakeoutLogsDbContext( /// public DbSet OperationLogs => Set(); + /// + /// 操作日志消息去重集合。 + /// + public DbSet OperationLogInboxMessages => Set(); + /// /// 成长值日志集合。 /// @@ -50,6 +55,7 @@ public sealed class TakeoutLogsDbContext( ConfigureTenantAuditLog(modelBuilder.Entity()); ConfigureMerchantAuditLog(modelBuilder.Entity()); ConfigureOperationLog(modelBuilder.Entity()); + ConfigureOperationLogInboxMessage(modelBuilder.Entity()); ConfigureMemberGrowthLog(modelBuilder.Entity()); } @@ -91,6 +97,15 @@ public sealed class TakeoutLogsDbContext( builder.HasIndex(x => x.CreatedAt); } + private static void ConfigureOperationLogInboxMessage(EntityTypeBuilder builder) + { + builder.ToTable("operation_log_inbox_messages"); + builder.HasKey(x => x.Id); + builder.Property(x => x.MessageId).IsRequired(); + builder.Property(x => x.ConsumedAt).IsRequired(); + builder.HasIndex(x => x.MessageId).IsUnique(); + } + private static void ConfigureMemberGrowthLog(EntityTypeBuilder builder) { builder.ToTable("member_growth_logs"); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Publishers/IdentityOperationLogPublisher.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Publishers/IdentityOperationLogPublisher.cs new file mode 100644 index 0000000..a88055d --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Publishers/IdentityOperationLogPublisher.cs @@ -0,0 +1,15 @@ +using MassTransit; +using TakeoutSaaS.Application.Identity.Abstractions; +using TakeoutSaaS.Application.Identity.Events; + +namespace TakeoutSaaS.Infrastructure.Logs.Publishers; + +/// +/// 身份模块操作日志发布器(基于 MassTransit Outbox)。 +/// +public sealed class IdentityOperationLogPublisher(IPublishEndpoint publishEndpoint) : IIdentityOperationLogPublisher +{ + /// + public Task PublishAsync(IdentityUserOperationLogMessage message, CancellationToken cancellationToken = default) + => publishEndpoint.Publish(message, cancellationToken); +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.Designer.cs new file mode 100644 index 0000000..9435bd5 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.Designer.cs @@ -0,0 +1,847 @@ +// +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("20251227004313_AddIdentityOutbox")] + partial class AddIdentityOutbox + { + /// + 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("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + + 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/20251227004313_AddIdentityOutbox.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.cs new file mode 100644 index 0000000..2e903cf --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/20251227004313_AddIdentityOutbox.cs @@ -0,0 +1,101 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb +{ + /// + public partial class AddIdentityOutbox : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "OutboxMessage", + columns: table => new + { + SequenceNumber = table.Column(type: "bigint", nullable: false) + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + EnqueueTime = table.Column(type: "timestamp with time zone", nullable: true), + SentTime = table.Column(type: "timestamp with time zone", nullable: false), + Headers = table.Column(type: "text", nullable: true), + Properties = table.Column(type: "text", nullable: true), + InboxMessageId = table.Column(type: "uuid", nullable: true), + InboxConsumerId = table.Column(type: "uuid", nullable: true), + OutboxId = table.Column(type: "uuid", nullable: true), + MessageId = table.Column(type: "uuid", nullable: false), + ContentType = table.Column(type: "character varying(256)", maxLength: 256, nullable: false), + MessageType = table.Column(type: "text", nullable: false), + Body = table.Column(type: "text", nullable: false), + ConversationId = table.Column(type: "uuid", nullable: true), + CorrelationId = table.Column(type: "uuid", nullable: true), + InitiatorId = table.Column(type: "uuid", nullable: true), + RequestId = table.Column(type: "uuid", nullable: true), + SourceAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + DestinationAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ResponseAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + FaultAddress = table.Column(type: "character varying(256)", maxLength: 256, nullable: true), + ExpirationTime = table.Column(type: "timestamp with time zone", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxMessage", x => x.SequenceNumber); + }); + + migrationBuilder.CreateTable( + name: "OutboxState", + columns: table => new + { + OutboxId = table.Column(type: "uuid", nullable: false), + LockId = table.Column(type: "uuid", nullable: false), + RowVersion = table.Column(type: "bytea", rowVersion: true, nullable: true), + Created = table.Column(type: "timestamp with time zone", nullable: false), + Delivered = table.Column(type: "timestamp with time zone", nullable: true), + LastSequenceNumber = table.Column(type: "bigint", nullable: true) + }, + constraints: table => + { + table.PrimaryKey("PK_OutboxState", x => x.OutboxId); + }); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_EnqueueTime", + table: "OutboxMessage", + column: "EnqueueTime"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_ExpirationTime", + table: "OutboxMessage", + column: "ExpirationTime"); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_InboxMessageId_InboxConsumerId_SequenceNumber", + table: "OutboxMessage", + columns: new[] { "InboxMessageId", "InboxConsumerId", "SequenceNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OutboxMessage_OutboxId_SequenceNumber", + table: "OutboxMessage", + columns: new[] { "OutboxId", "SequenceNumber" }, + unique: true); + + migrationBuilder.CreateIndex( + name: "IX_OutboxState_Created", + table: "OutboxState", + column: "Created"); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "OutboxMessage"); + + migrationBuilder.DropTable( + name: "OutboxState"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs index e74d9ce..07a3cad 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/IdentityDb/IdentityDbContextModelSnapshot.cs @@ -22,6 +22,127 @@ namespace TakeoutSaaS.Infrastructure.Migrations.IdentityDb NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxMessage", b => + { + b.Property("SequenceNumber") + .ValueGeneratedOnAdd() + .HasColumnType("bigint"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("SequenceNumber")); + + b.Property("Body") + .IsRequired() + .HasColumnType("text"); + + b.Property("ContentType") + .IsRequired() + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("ConversationId") + .HasColumnType("uuid"); + + b.Property("CorrelationId") + .HasColumnType("uuid"); + + b.Property("DestinationAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("EnqueueTime") + .HasColumnType("timestamp with time zone"); + + b.Property("ExpirationTime") + .HasColumnType("timestamp with time zone"); + + b.Property("FaultAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("Headers") + .HasColumnType("text"); + + b.Property("InboxConsumerId") + .HasColumnType("uuid"); + + b.Property("InboxMessageId") + .HasColumnType("uuid"); + + b.Property("InitiatorId") + .HasColumnType("uuid"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.Property("MessageType") + .IsRequired() + .HasColumnType("text"); + + b.Property("OutboxId") + .HasColumnType("uuid"); + + b.Property("Properties") + .HasColumnType("text"); + + b.Property("RequestId") + .HasColumnType("uuid"); + + b.Property("ResponseAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.Property("SentTime") + .HasColumnType("timestamp with time zone"); + + b.Property("SourceAddress") + .HasMaxLength(256) + .HasColumnType("character varying(256)"); + + b.HasKey("SequenceNumber"); + + b.HasIndex("EnqueueTime"); + + b.HasIndex("ExpirationTime"); + + b.HasIndex("OutboxId", "SequenceNumber") + .IsUnique(); + + b.HasIndex("InboxMessageId", "InboxConsumerId", "SequenceNumber") + .IsUnique(); + + b.ToTable("OutboxMessage"); + }); + + modelBuilder.Entity("MassTransit.EntityFrameworkCoreIntegration.OutboxState", b => + { + b.Property("OutboxId") + .ValueGeneratedOnAdd() + .HasColumnType("uuid"); + + b.Property("Created") + .HasColumnType("timestamp with time zone"); + + b.Property("Delivered") + .HasColumnType("timestamp with time zone"); + + b.Property("LastSequenceNumber") + .HasColumnType("bigint"); + + b.Property("LockId") + .HasColumnType("uuid"); + + b.Property("RowVersion") + .IsConcurrencyToken() + .ValueGeneratedOnAddOrUpdate() + .HasColumnType("bytea"); + + b.HasKey("OutboxId"); + + b.HasIndex("Created"); + + b.ToTable("OutboxState"); + }); + modelBuilder.Entity("TakeoutSaaS.Domain.Identity.Entities.IdentityUser", b => { b.Property("Id") diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.Designer.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.Designer.cs new file mode 100644 index 0000000..9e305a6 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.Designer.cs @@ -0,0 +1,358 @@ +// +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.Logs.Persistence; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb +{ + [DbContext(typeof(TakeoutLogsDbContext))] + [Migration("20251227004337_AddOperationLogInboxMessages")] + partial class AddOperationLogInboxMessages + { + /// + 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.Membership.Entities.MemberGrowthLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChangeValue") + .HasColumnType("integer") + .HasComment("变动数量。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentValue") + .HasColumnType("integer") + .HasComment("当前成长值。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("MemberId") + .HasColumnType("bigint") + .HasComment("会员标识。"); + + b.Property("Notes") + .HasMaxLength(256) + .HasColumnType("character varying(256)") + .HasComment("备注。"); + + b.Property("OccurredAt") + .HasColumnType("timestamp with time zone") + .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", "MemberId", "OccurredAt"); + + b.ToTable("member_growth_logs", null, t => + { + t.HasComment("成长值变动日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Merchants.Entities.MerchantAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .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(1024) + .HasColumnType("character varying(1024)") + .HasComment("详情描述。"); + + b.Property("MerchantId") + .HasColumnType("bigint") + .HasComment("商户标识。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人 ID。"); + + b.Property("OperatorName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .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", "MerchantId"); + + b.ToTable("merchant_audit_logs", null, t => + { + t.HasComment("商户入驻审核日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.OperationLog", 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("OperationType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作类型:BatchExtend, BatchRemind, StatusChange 等。"); + + b.Property("OperatorId") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作人ID。"); + + b.Property("OperatorName") + .HasMaxLength(128) + .HasColumnType("character varying(128)") + .HasComment("操作人名称。"); + + b.Property("Parameters") + .HasColumnType("text") + .HasComment("操作参数(JSON)。"); + + b.Property("Result") + .HasColumnType("text") + .HasComment("操作结果(JSON)。"); + + b.Property("Success") + .HasColumnType("boolean") + .HasComment("是否成功。"); + + b.Property("TargetIds") + .HasColumnType("text") + .HasComment("目标ID列表(JSON)。"); + + b.Property("TargetType") + .IsRequired() + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("目标类型:Subscription, Bill 等。"); + + b.Property("UpdatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("最近一次更新时间(UTC),从未更新时为 null。"); + + b.Property("UpdatedBy") + .HasColumnType("bigint") + .HasComment("最后更新人用户标识,匿名或系统操作时为 null。"); + + b.HasKey("Id"); + + b.HasIndex("CreatedAt"); + + b.HasIndex("OperationType", "CreatedAt"); + + b.ToTable("operation_logs", null, t => + { + t.HasComment("运营操作日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Domain.Tenants.Entities.TenantAuditLog", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasComment("操作类型。"); + + b.Property("CreatedAt") + .HasColumnType("timestamp with time zone") + .HasComment("创建时间(UTC)。"); + + b.Property("CreatedBy") + .HasColumnType("bigint") + .HasComment("创建人用户标识,匿名或系统操作时为 null。"); + + b.Property("CurrentStatus") + .HasColumnType("integer") + .HasComment("新状态。"); + + b.Property("DeletedAt") + .HasColumnType("timestamp with time zone") + .HasComment("软删除时间(UTC),未删除时为 null。"); + + b.Property("DeletedBy") + .HasColumnType("bigint") + .HasComment("删除人用户标识(软删除),未删除时为 null。"); + + b.Property("Description") + .HasMaxLength(1024) + .HasColumnType("character varying(1024)") + .HasComment("详细描述。"); + + b.Property("OperatorId") + .HasColumnType("bigint") + .HasComment("操作人 ID。"); + + b.Property("OperatorName") + .HasMaxLength(64) + .HasColumnType("character varying(64)") + .HasComment("操作人名称。"); + + b.Property("PreviousStatus") + .HasColumnType("integer") + .HasComment("原状态。"); + + b.Property("TenantId") + .HasColumnType("bigint") + .HasComment("关联的租户标识。"); + + 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"); + + b.ToTable("tenant_audit_logs", null, t => + { + t.HasComment("租户运营审核日志。"); + }); + }); + + modelBuilder.Entity("TakeoutSaaS.Infrastructure.Logs.Persistence.OperationLogInboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.ToTable("operation_log_inbox_messages", (string)null); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.cs new file mode 100644 index 0000000..f6e1232 --- /dev/null +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/20251227004337_AddOperationLogInboxMessages.cs @@ -0,0 +1,43 @@ +using System; +using Microsoft.EntityFrameworkCore.Migrations; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb +{ + /// + public partial class AddOperationLogInboxMessages : Migration + { + /// + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "operation_log_inbox_messages", + columns: table => new + { + Id = table.Column(type: "bigint", nullable: false, comment: "实体唯一标识。") + .Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn), + MessageId = table.Column(type: "uuid", nullable: false), + ConsumedAt = table.Column(type: "timestamp with time zone", nullable: false) + }, + constraints: table => + { + table.PrimaryKey("PK_operation_log_inbox_messages", x => x.Id); + }); + + migrationBuilder.CreateIndex( + name: "IX_operation_log_inbox_messages_MessageId", + table: "operation_log_inbox_messages", + column: "MessageId", + unique: true); + } + + /// + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "operation_log_inbox_messages"); + } + } +} diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/TakeoutLogsDbContextModelSnapshot.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/TakeoutLogsDbContextModelSnapshot.cs index d35a5c9..7cfe69a 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/TakeoutLogsDbContextModelSnapshot.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Migrations/LogsDb/TakeoutLogsDbContextModelSnapshot.cs @@ -326,6 +326,29 @@ namespace TakeoutSaaS.Infrastructure.Migrations.LogsDb t.HasComment("租户运营审核日志。"); }); }); + + modelBuilder.Entity("TakeoutSaaS.Infrastructure.Logs.Persistence.OperationLogInboxMessage", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasComment("实体唯一标识。"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ConsumedAt") + .HasColumnType("timestamp with time zone"); + + b.Property("MessageId") + .HasColumnType("uuid"); + + b.HasKey("Id"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.ToTable("operation_log_inbox_messages", (string)null); + }); #pragma warning restore 612, 618 } } From c9980ef237cbf293ee193e2ccfb4c7fa8c687b99 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 27 Dec 2025 10:08:09 +0800 Subject: [PATCH 3/6] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=9B=BF=E6=8D=A2=E7=9A=84=E8=BD=AF=E5=88=A0?= =?UTF-8?q?=E9=99=A4=E5=86=B2=E7=AA=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Persistence/EfUserRoleRepository.cs | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs index c82e62e..3bb62af 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs @@ -61,19 +61,40 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol .Where(x => x.TenantId == tenantId && x.UserId == userId) .ToListAsync(cancellationToken); - // 3. 清空并保存 - dbContext.UserRoles.RemoveRange(existing); - await dbContext.SaveChangesAsync(cancellationToken); + // 3. 去重并构建目标集合 + var targetRoleIds = roleIds.Distinct().ToArray(); + var targetRoleSet = targetRoleIds.ToHashSet(); + var existingRoleMap = existing.ToDictionary(x => x.RoleId); - // 4. 构建新映射 - var toAdd = roleIds.Distinct().Select(roleId => new UserRole + // 4. 同步现有映射状态(软删除或恢复) + foreach (var mapping in existing) { - TenantId = tenantId, - UserId = userId, - RoleId = roleId - }); + if (targetRoleSet.Contains(mapping.RoleId)) + { + if (mapping.DeletedAt.HasValue) + { + mapping.DeletedAt = null; + mapping.DeletedBy = null; + } + continue; + } + + if (!mapping.DeletedAt.HasValue) + { + dbContext.UserRoles.Remove(mapping); + } + } + + // 5. 补齐新增角色映射 + var toAdd = targetRoleIds + .Where(roleId => !existingRoleMap.ContainsKey(roleId)) + .Select(roleId => new UserRole + { + TenantId = tenantId, + UserId = userId, + RoleId = roleId + }); - // 5. 批量新增并保存 await dbContext.UserRoles.AddRangeAsync(toAdd, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); From 7aeef0a24df65a8d7bc1073b943c9ef3cd98cb5c Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 27 Dec 2025 10:25:18 +0800 Subject: [PATCH 4/6] =?UTF-8?q?=E8=B0=83=E6=95=B4RabbitMQ=E5=AE=A2?= =?UTF-8?q?=E6=88=B7=E7=AB=AF=E7=89=88=E6=9C=AC=E5=B9=B6=E5=85=BC=E5=AE=B9?= =?UTF-8?q?=E5=90=8C=E6=AD=A5=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Services/RabbitMqConnectionFactory.cs | 3 ++- .../Services/RabbitMqMessagePublisher.cs | 18 +++++++-------- .../Services/RabbitMqMessageSubscriber.cs | 22 +++++++++---------- .../TakeoutSaaS.Module.Messaging.csproj | 2 +- 4 files changed, 22 insertions(+), 23 deletions(-) diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs index e015c72..1ffa9dd 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs @@ -14,6 +14,7 @@ public sealed class RabbitMqConnectionFactory(IOptionsMonitor o /// public Task CreateConnectionAsync(CancellationToken cancellationToken = default) { + cancellationToken.ThrowIfCancellationRequested(); var options = optionsMonitor.CurrentValue; var factory = new ConnectionFactory { @@ -24,6 +25,6 @@ public sealed class RabbitMqConnectionFactory(IOptionsMonitor o VirtualHost = options.VirtualHost }; - return factory.CreateConnectionAsync(cancellationToken); + return Task.FromResult(factory.CreateConnection()); } } diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs index 4a5b215..2319a23 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs @@ -14,7 +14,7 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio : IMessagePublisher, IAsyncDisposable { private IConnection? _connection; - private IChannel? _channel; + private IModel? _channel; private bool _disposed; /// @@ -26,18 +26,16 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio var channel = _channel ?? throw new InvalidOperationException("RabbitMQ channel is not available."); // 2. 声明交换机 - await channel.ExchangeDeclareAsync(options.Exchange, options.ExchangeType, durable: true, autoDelete: false, cancellationToken: cancellationToken); + channel.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); // 3. 序列化消息并设置属性 var body = serializer.Serialize(message); - var props = new BasicProperties - { - ContentType = "application/json", - DeliveryMode = DeliveryModes.Persistent, - MessageId = Guid.NewGuid().ToString("N") - }; + var props = channel.CreateBasicProperties(); + props.ContentType = "application/json"; + props.DeliveryMode = 2; + props.MessageId = Guid.NewGuid().ToString("N"); // 4. 发布消息 - await channel.BasicPublishAsync(options.Exchange, routingKey, mandatory: false, props, body, cancellationToken); + channel.BasicPublish(options.Exchange, routingKey, mandatory: false, basicProperties: props, body: body); logger.LogDebug("发布消息到交换机 {Exchange} RoutingKey {RoutingKey}", options.Exchange, routingKey); } @@ -49,7 +47,7 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio } _connection ??= await connectionFactory.CreateConnectionAsync(cancellationToken); - _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + _channel = _connection.CreateModel(); } /// diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs index 6287c77..608ee5a 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs @@ -15,7 +15,7 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti : IMessageSubscriber { private IConnection? _connection; - private IChannel? _channel; + private IModel? _channel; private bool _disposed; /// @@ -28,19 +28,19 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti var channel = _channel ?? throw new InvalidOperationException("RabbitMQ channel is not available."); // 2. 声明交换机、队列及绑定 - await channel.ExchangeDeclareAsync(options.Exchange, options.ExchangeType, durable: true, autoDelete: false, cancellationToken: cancellationToken); - await channel.QueueDeclareAsync(queue, durable: true, exclusive: false, autoDelete: false, cancellationToken: cancellationToken); - await channel.QueueBindAsync(queue, options.Exchange, routingKey, cancellationToken: cancellationToken); - await channel.BasicQosAsync(0, options.PrefetchCount, global: false, cancellationToken: cancellationToken); + channel.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); + channel.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false, arguments: null); + channel.QueueBind(queue, options.Exchange, routingKey); + channel.BasicQos(0, options.PrefetchCount, global: false); // 3. 设置消费者回调 var consumer = new AsyncEventingBasicConsumer(channel); - consumer.ReceivedAsync += async (_, ea) => + consumer.Received += async (_, ea) => { var message = serializer.Deserialize(ea.Body.ToArray()); if (message == null) { - await channel.BasicAckAsync(ea.DeliveryTag, multiple: false, cancellationToken); + channel.BasicAck(ea.DeliveryTag, multiple: false); return; } @@ -56,16 +56,16 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti if (success) { - await channel.BasicAckAsync(ea.DeliveryTag, multiple: false, cancellationToken); + channel.BasicAck(ea.DeliveryTag, multiple: false); } else { - await channel.BasicNackAsync(ea.DeliveryTag, multiple: false, requeue: false, cancellationToken); + channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); } }; // 4. 开始消费 - await channel.BasicConsumeAsync(queue, autoAck: false, consumer, cancellationToken); + channel.BasicConsume(queue, autoAck: false, consumer); } private async Task EnsureChannelAsync(CancellationToken cancellationToken) @@ -76,7 +76,7 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti } _connection ??= await connectionFactory.CreateConnectionAsync(cancellationToken); - _channel = await _connection.CreateChannelAsync(cancellationToken: cancellationToken); + _channel = _connection.CreateModel(); } /// diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj index f5650aa..6ac643f 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj +++ b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj @@ -11,7 +11,7 @@ - + From 7e130c7ae0bc772b055259b22c3c137fc723bbd6 Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 27 Dec 2025 10:36:13 +0800 Subject: [PATCH 5/6] =?UTF-8?q?=E7=A7=BB=E9=99=A4=20AutoMapper=20=E4=BE=9D?= =?UTF-8?q?=E8=B5=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../App/Billings/Mappings/BillingProfile.cs | 66 ------------------- ...pApplicationServiceCollectionExtensions.cs | 3 - .../TakeoutSaaS.Application.csproj | 2 - 3 files changed, 71 deletions(-) delete mode 100644 src/Application/TakeoutSaaS.Application/App/Billings/Mappings/BillingProfile.cs diff --git a/src/Application/TakeoutSaaS.Application/App/Billings/Mappings/BillingProfile.cs b/src/Application/TakeoutSaaS.Application/App/Billings/Mappings/BillingProfile.cs deleted file mode 100644 index 3b3a25b..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Billings/Mappings/BillingProfile.cs +++ /dev/null @@ -1,66 +0,0 @@ -using AutoMapper; -using System.Text.Json; -using TakeoutSaaS.Application.App.Billings.Dto; -using TakeoutSaaS.Domain.Tenants.Entities; -using TakeoutSaaS.Domain.Tenants.Enums; - -namespace TakeoutSaaS.Application.App.Billings.Mappings; - -/// -/// 账单模块 AutoMapper Profile。 -/// -public sealed class BillingProfile : Profile -{ - /// - /// 初始化映射配置。 - /// - public BillingProfile() - { - // 1. 账单实体 -> 列表 DTO - CreateMap() - .ForMember(x => x.TenantName, opt => opt.Ignore()) - .ForMember(x => x.TotalAmount, opt => opt.MapFrom(src => src.CalculateTotalAmount())) - .ForMember(x => x.IsOverdue, opt => opt.MapFrom(src => - src.Status == TenantBillingStatus.Overdue - || (src.Status == TenantBillingStatus.Pending && src.DueDate < DateTime.UtcNow))) - .ForMember(x => x.OverdueDays, opt => opt.MapFrom(src => - src.DueDate < DateTime.UtcNow ? (int)(DateTime.UtcNow - src.DueDate).TotalDays : 0)); - - // 2. (空行后) 账单实体 -> 详情 DTO - CreateMap() - .ForMember(x => x.TenantName, opt => opt.Ignore()) - .ForMember(x => x.TotalAmount, opt => opt.MapFrom(src => src.CalculateTotalAmount())) - .ForMember(x => x.LineItemsJson, opt => opt.MapFrom(src => src.LineItemsJson)) - .ForMember(x => x.LineItems, opt => opt.MapFrom(src => DeserializeLineItems(src.LineItemsJson))) - .ForMember(x => x.Payments, opt => opt.Ignore()); - - // 3. (空行后) 账单实体 -> 导出 DTO - CreateMap() - .ForMember(x => x.TenantName, opt => opt.Ignore()) - .ForMember(x => x.TotalAmount, opt => opt.MapFrom(src => src.CalculateTotalAmount())) - .ForMember(x => x.LineItems, opt => opt.MapFrom(src => DeserializeLineItems(src.LineItemsJson))); - - // 4. (空行后) 支付实体 -> 支付记录 DTO - CreateMap() - .ForMember(x => x.BillingId, opt => opt.MapFrom(src => src.BillingStatementId)) - .ForMember(x => x.IsVerified, opt => opt.MapFrom(src => src.VerifiedAt.HasValue)); - } - - private static IReadOnlyList DeserializeLineItems(string? json) - { - if (string.IsNullOrWhiteSpace(json)) - { - return []; - } - - try - { - return JsonSerializer.Deserialize>(json) ?? []; - } - catch - { - return []; - } - } -} - diff --git a/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs b/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs index 1fd8435..8f2944e 100644 --- a/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs +++ b/src/Application/TakeoutSaaS.Application/App/Extensions/AppApplicationServiceCollectionExtensions.cs @@ -20,9 +20,6 @@ public static class AppApplicationServiceCollectionExtensions { services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(Assembly.GetExecutingAssembly())); services.AddValidatorsFromAssembly(Assembly.GetExecutingAssembly()); - - // (空行后) 注册 AutoMapper Profile - services.AddAutoMapper(Assembly.GetExecutingAssembly()); services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)); return services; diff --git a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj index 8d09840..f8b5ffd 100644 --- a/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj +++ b/src/Application/TakeoutSaaS.Application/TakeoutSaaS.Application.csproj @@ -9,8 +9,6 @@ - - From b31a4ea90964d65397f1ef0f809dd4cb55aca38a Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Sat, 27 Dec 2025 11:04:11 +0800 Subject: [PATCH 6/6] =?UTF-8?q?=E5=8D=87=E7=BA=A7=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=E5=B9=B6=E9=80=82=E9=85=8D=E6=B6=88=E6=81=AF=E6=A8=A1=E5=9D=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TakeoutSaaS.AdminApi.csproj | 6 +-- .../TakeoutSaaS.MiniApi.csproj | Bin 2293 -> 4588 bytes .../TakeoutSaaS.UserApi.csproj | Bin 3434 -> 3434 bytes .../TakeoutSaaS.Application.csproj | Bin 1359 -> 2924 bytes .../TakeoutSaaS.Shared.Abstractions.csproj | 3 ++ .../TakeoutSaaS.Shared.Kernel.csproj | 3 ++ .../TakeoutSaaS.Shared.Web.csproj | 14 ++++--- .../TakeoutSaaS.Domain.csproj | 3 ++ .../TakeoutSaaS.ApiGateway.csproj | Bin 1924 -> 2130 bytes .../TakeoutSaaS.Infrastructure.csproj | Bin 3974 -> 4616 bytes .../TakeoutSaaS.Module.Authorization.csproj | Bin 1254 -> 1460 bytes .../TakeoutSaaS.Module.Delivery.csproj | 3 ++ .../TakeoutSaaS.Module.Dictionary.csproj | 3 ++ .../Services/RabbitMqConnectionFactory.cs | 4 +- .../Services/RabbitMqMessagePublisher.cs | 31 ++++++++++----- .../Services/RabbitMqMessageSubscriber.cs | 36 ++++++++++-------- .../TakeoutSaaS.Module.Messaging.csproj | 17 +++++---- .../TakeoutSaaS.Module.Scheduler.csproj | 15 +++++--- .../TakeoutSaaS.Module.Sms.csproj | 17 +++++---- .../TakeoutSaaS.Module.Storage.csproj | Bin 2130 -> 2336 bytes .../TakeoutSaaS.Module.Tenancy.csproj | 3 ++ .../TakeoutSaaS.Application.Tests.csproj | 13 +++++-- .../TakeoutSaaS.Integration.Tests.csproj | 17 ++++++--- 23 files changed, 124 insertions(+), 64 deletions(-) diff --git a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj index a92defc..59d35a0 100644 --- a/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj +++ b/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj @@ -9,9 +9,9 @@ ../../.. - + - + @@ -20,7 +20,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj b/src/Api/TakeoutSaaS.MiniApi/TakeoutSaaS.MiniApi.csproj index 1fd145f97ccb6bebd8f7f74bdd4b92d2a8cc3425..22d02129478fcd3c292f68dda33b37c4bd976ee6 100644 GIT binary patch literal 4588 zcmdUyUsKvp5XJY|nSKYR@5btM`b2e{7XM5~>jq9#suxn~>+;7>%U-XDQ1Ay@xTyc^_0wV-|H@qY4D7s)fQ zNiPy7L)f(Qm-l(!^n1%HJ>@xUI)`N*pSV_?tf*$ku0yM*Oo&-AO26AANNY+NR2qf& zqWYGf)sDB1s_&y$_?qJNs@-Rn`+3jLxv!a_d=^-AR+ybV7n>CVP__B$npJb#&4f3g z9)UYyo(TCgk!dnhXj+We3$tl;Iq3kFY6|h57VqTGL$c2sviUOK%Zoi-kNnGEJ0UAw z@}dfg$(rW(dAXXE3+%*6$H?~S=GyaD*Q_$mCR4-WQSs8%#-q-zvy9Gki{`9J)#h4N z#;1u@r^lh2s~5$0U+ZP?X)^7(Ol0$^PWY+ohQ)rbU+Z6^&31#?{h%1%{T<;G*_;q3 ZokH3rY%SJ$3SIeLnV-J1)0xgze*no=|49G< literal 2293 zcmcIlO>f#j5WPprf3SSdf}^S@Llh;DSQRviX?o$Z*aNt%cdgyo2L63#apJ_5p%l3i zlBJp9y_q*38{TYl;r#|}G9?Fo!1}%iGEr$JR|DVT@+$c1-wZD=FTy9SenEm>oNfmG zBulh1YKd$*nlUo5pRn*v!KkD zn(o?uu<-8$I0FGWpx&_$F!rbnQtLj0vfzsw!c#J@!$#6jAFIY55T-NuN5ta4mcHMNQg>Dlm|0>90%S>V=F`Ve7omt Gc<~34`~6G+ diff --git a/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj b/src/Api/TakeoutSaaS.UserApi/TakeoutSaaS.UserApi.csproj index 2a318505dc9dbe42de413defa0a61adf1ca5073f..ed59abd3b0db16a1be14c9f166242c2eace42caa 100644 GIT binary patch delta 18 ZcmaDQ^-5}k5Hq9UWI+!9%}LB0TmU=x1tkCg delta 22 dcmaDQ^-5}k5HqU*gC2w7vRzrq2>L3U2Q$U|+m2oO6Z|zR*75Xn=?wcV^wDXJVr{EqWXcafV}D@ae3!IKxZ+UeGUim}_iN z75ITu^PXZnXXk7$5OcIo+X5$iiDkFeOR@ zdR_CLa!z65zck~b#h-Ju7}c2GLgJS3TpHTKGq0F$xpg_W)^@l*hE)?Su9`B6Wmujw ztAwbp1HTIXAL4*W=6qMR z#1_Z9-1zpHUh`ahO^;oxp7^+J9B@qWZ231)AG05XQD(JKC*)kUWtnCcrMMVAWTa>o zqxDP+$+`a#@1bFOLrlf4b+N9Besio-Q;(P!c4}(ez#FTuR~=P(({)dbr>bYcI>pv~ zT|PbELL9N$>YVJp#tG{yhIJ8&o^ftf z=|ffD`_ZbgQeAv^&FcDUnh-LUD_**{Ep{nr%6_^s^}XZ!h#FE*|6~3lTF%;OUl^`! z9a2lAJ1$<2#Pi6fZQ19{wSV84t0J-J`JD5NSX1r1ZUZtLL#`dx6e*CaHvapPQDn=84F^sE z0Rm^q+2L@=?Bcaj(I2!<8a<3t7{n23ZiLi}VcgLCYx*O;$g_KEej}%-6w6^emfRX= z<^+@L8F(np&(6L?QJ7VurPZx9P2FN1oiV#Wy0NUnzs4?ejg$^xkR`i?Hxj>K(kR8A z73QeJFH6=yH>0R(CAp*r=XJXEydAg|mF!WK6@=Bc%6rC_Y=KjpqxBA8RA{amf&J}X zx_W{%G8W;EsD{gDN0xE&-nlq>>|8vDZ^6eT52gfLK0tl_N~psjI4A^alC*-*s7D)2 z`wZ`h@fUB`A=MlDqpDFzM$^8u@13+`Yj0HJclXGY6fF1#S6HL=JLaoGhlqJ><3+#4 zIi!#VSC6&$H60m&w!apBw#NCTS+Y6%Fx-r~aJ^@JPZ9v0{5LkDQRtp#Y>B3!lCct6 z49e;d4BXZJ4!xzKi$!~(DuqyOzJJ-gU~L0KR`Y*S$JPUaKn4|54}^tHUIzs diff --git a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj index 2de50de..43219c2 100644 --- a/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj +++ b/src/Core/TakeoutSaaS.Shared.Abstractions/TakeoutSaaS.Shared.Abstractions.csproj @@ -6,5 +6,8 @@ true 1591 + + + diff --git a/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj b/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj index c52f050..db63687 100644 --- a/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj +++ b/src/Core/TakeoutSaaS.Shared.Kernel/TakeoutSaaS.Shared.Kernel.csproj @@ -7,5 +7,8 @@ + + + diff --git a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj index ab9c720..f7a4f0e 100644 --- a/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj +++ b/src/Core/TakeoutSaaS.Shared.Web/TakeoutSaaS.Shared.Web.csproj @@ -11,14 +11,18 @@ - - - - - + + + + + + + + + diff --git a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj index 3f9021d..fc99d63 100644 --- a/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj +++ b/src/Domain/TakeoutSaaS.Domain/TakeoutSaaS.Domain.csproj @@ -9,5 +9,8 @@ + + + diff --git a/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj b/src/Gateway/TakeoutSaaS.ApiGateway/TakeoutSaaS.ApiGateway.csproj index 11ed7b88080d007d17631dfd7ed72a63a28ec0fc..207e70312c191ac92f77e62c7f21fdb09c6e57e8 100644 GIT binary patch delta 77 zcmZqSza+3BgMIP~MlbzPh608ZhD3%EhExVy1|^1IhDwHFFfW%ukHM3{A4moO<&zn* Wfn@q*M`n3W1E3fP8&0-n_XGgS#Sj(% delta 11 Scmca4(89kVgMG3YhYSE2N&|ub diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj b/src/Infrastructure/TakeoutSaaS.Infrastructure/TakeoutSaaS.Infrastructure.csproj index 06ca75d906ea812ad41ade7ca5aed0e6d11ff50b..931f2ccfa3441a5b23fff5f0dd880731e4dfd0e2 100644 GIT binary patch delta 385 zcmZpZ?@-y0!^CJhc_Op_K9e6X7cd%b4rIw>WHw|_ znk>jBy7>}o6%$x)28Rx-DOg7~Q{ZMPPCiDc7LmyvTxn3Y*yI527)Ha%j{LflA24c7 zKEPwQ*@;Jo8OZTc4P_``NMT51C}Butuw_tU@CCAqfiQ%j2*}CLpsQu$s1U?zyab40PagydH?_b delta 109 zcmeBBX_McO!^CJbc_Op_nk>jB zy7>}o6%$Bw@(d0gc4G!T215p;$+=Aao25AU7=aQAlRLQ5fOOL20PYwdEwp(b_Xg(4 H9|R=;c)cDD diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj b/src/Modules/TakeoutSaaS.Module.Authorization/TakeoutSaaS.Module.Authorization.csproj index e1c880a90369295d51fb67a5619bba20cea9fee0..16cf38c9c9bddd2580c43083ca561e2c4bc1a170 100644 GIT binary patch delta 95 zcmaFHxrKYf0wzYo$qSjTf=CwK$qyK{7N28g1kzrnp$r8KDGZ4WB@C$ywhT%P!3>oQ l#b918gC2t?gFlcA0LmvbWCO``hD?S$hV;qxOp0K$TmdYs7=Hi& delta 27 hcmdnO{fu+N0wzX-$qSjTf=HI|#pjqAC(mJ(005C13BCXT diff --git a/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj b/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj index ff77596..8001b38 100644 --- a/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj +++ b/src/Modules/TakeoutSaaS.Module.Delivery/TakeoutSaaS.Module.Delivery.csproj @@ -10,5 +10,8 @@ + + + diff --git a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj index b03dcec..8cb6277 100644 --- a/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj +++ b/src/Modules/TakeoutSaaS.Module.Dictionary/TakeoutSaaS.Module.Dictionary.csproj @@ -9,4 +9,7 @@ + + + diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs index 1ffa9dd..62935ef 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqConnectionFactory.cs @@ -12,7 +12,7 @@ public sealed class RabbitMqConnectionFactory(IOptionsMonitor o /// /// 创建连接。 /// - public Task CreateConnectionAsync(CancellationToken cancellationToken = default) + public async Task CreateConnectionAsync(CancellationToken cancellationToken = default) { cancellationToken.ThrowIfCancellationRequested(); var options = optionsMonitor.CurrentValue; @@ -25,6 +25,6 @@ public sealed class RabbitMqConnectionFactory(IOptionsMonitor o VirtualHost = options.VirtualHost }; - return Task.FromResult(factory.CreateConnection()); + return await factory.CreateConnectionAsync(cancellationToken).ConfigureAwait(false); } } diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs index 2319a23..3354ddc 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessagePublisher.cs @@ -14,7 +14,7 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio : IMessagePublisher, IAsyncDisposable { private IConnection? _connection; - private IModel? _channel; + private IChannel? _channel; private bool _disposed; /// @@ -26,16 +26,16 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio var channel = _channel ?? throw new InvalidOperationException("RabbitMQ channel is not available."); // 2. 声明交换机 - channel.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); + await channel.ExchangeDeclareAsync(options.Exchange, options.ExchangeType, durable: true, autoDelete: false, arguments: null, noWait: false, cancellationToken).ConfigureAwait(false); // 3. 序列化消息并设置属性 var body = serializer.Serialize(message); - var props = channel.CreateBasicProperties(); + var props = new BasicProperties(); props.ContentType = "application/json"; - props.DeliveryMode = 2; + props.DeliveryMode = DeliveryModes.Persistent; props.MessageId = Guid.NewGuid().ToString("N"); // 4. 发布消息 - channel.BasicPublish(options.Exchange, routingKey, mandatory: false, basicProperties: props, body: body); + await channel.BasicPublishAsync(options.Exchange, routingKey, mandatory: false, basicProperties: props, body: body, cancellationToken).ConfigureAwait(false); logger.LogDebug("发布消息到交换机 {Exchange} RoutingKey {RoutingKey}", options.Exchange, routingKey); } @@ -46,8 +46,8 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio return; } - _connection ??= await connectionFactory.CreateConnectionAsync(cancellationToken); - _channel = _connection.CreateModel(); + _connection ??= await connectionFactory.CreateConnectionAsync(cancellationToken).ConfigureAwait(false); + _channel = await _connection.CreateChannelAsync(new CreateChannelOptions(false, false, null, null), cancellationToken).ConfigureAwait(false); } /// @@ -61,8 +61,19 @@ public sealed class RabbitMqMessagePublisher(RabbitMqConnectionFactory connectio } _disposed = true; - _channel?.Dispose(); - _connection?.Dispose(); - return ValueTask.CompletedTask; + return CloseAsync(); + } + + private async ValueTask CloseAsync() + { + if (_channel != null) + { + await _channel.CloseAsync(CancellationToken.None).ConfigureAwait(false); + } + + if (_connection != null) + { + await _connection.CloseAsync(CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs index 608ee5a..f4a4c61 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs +++ b/src/Modules/TakeoutSaaS.Module.Messaging/Services/RabbitMqMessageSubscriber.cs @@ -15,7 +15,7 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti : IMessageSubscriber { private IConnection? _connection; - private IModel? _channel; + private IChannel? _channel; private bool _disposed; /// @@ -28,19 +28,19 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti var channel = _channel ?? throw new InvalidOperationException("RabbitMQ channel is not available."); // 2. 声明交换机、队列及绑定 - channel.ExchangeDeclare(options.Exchange, options.ExchangeType, durable: true, autoDelete: false); - channel.QueueDeclare(queue, durable: true, exclusive: false, autoDelete: false, arguments: null); - channel.QueueBind(queue, options.Exchange, routingKey); - channel.BasicQos(0, options.PrefetchCount, global: false); + await channel.ExchangeDeclareAsync(options.Exchange, options.ExchangeType, durable: true, autoDelete: false, arguments: null, noWait: false, cancellationToken).ConfigureAwait(false); + await channel.QueueDeclareAsync(queue, durable: true, exclusive: false, autoDelete: false, arguments: null, noWait: false, cancellationToken).ConfigureAwait(false); + await channel.QueueBindAsync(queue, options.Exchange, routingKey, arguments: null, noWait: false, cancellationToken).ConfigureAwait(false); + await channel.BasicQosAsync(0, options.PrefetchCount, global: false, cancellationToken).ConfigureAwait(false); // 3. 设置消费者回调 var consumer = new AsyncEventingBasicConsumer(channel); - consumer.Received += async (_, ea) => + consumer.ReceivedAsync += async (_, ea) => { var message = serializer.Deserialize(ea.Body.ToArray()); if (message == null) { - channel.BasicAck(ea.DeliveryTag, multiple: false); + await channel.BasicAckAsync(ea.DeliveryTag, multiple: false, cancellationToken).ConfigureAwait(false); return; } @@ -56,16 +56,16 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti if (success) { - channel.BasicAck(ea.DeliveryTag, multiple: false); + await channel.BasicAckAsync(ea.DeliveryTag, multiple: false, cancellationToken).ConfigureAwait(false); } else { - channel.BasicNack(ea.DeliveryTag, multiple: false, requeue: false); + await channel.BasicNackAsync(ea.DeliveryTag, multiple: false, requeue: false, cancellationToken).ConfigureAwait(false); } }; // 4. 开始消费 - channel.BasicConsume(queue, autoAck: false, consumer); + await channel.BasicConsumeAsync(queue, autoAck: false, consumer, cancellationToken).ConfigureAwait(false); } private async Task EnsureChannelAsync(CancellationToken cancellationToken) @@ -75,8 +75,8 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti return; } - _connection ??= await connectionFactory.CreateConnectionAsync(cancellationToken); - _channel = _connection.CreateModel(); + _connection ??= await connectionFactory.CreateConnectionAsync(cancellationToken).ConfigureAwait(false); + _channel = await _connection.CreateChannelAsync(new CreateChannelOptions(false, false, null, null), cancellationToken).ConfigureAwait(false); } /// @@ -88,10 +88,14 @@ public sealed class RabbitMqMessageSubscriber(RabbitMqConnectionFactory connecti } _disposed = true; - await Task.Run(() => + if (_channel != null) { - _channel?.Dispose(); - _connection?.Dispose(); - }).ConfigureAwait(false); + await _channel.CloseAsync(CancellationToken.None).ConfigureAwait(false); + } + + if (_connection != null) + { + await _connection.CloseAsync(CancellationToken.None).ConfigureAwait(false); + } } } diff --git a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj index 6ac643f..1350a29 100644 --- a/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj +++ b/src/Modules/TakeoutSaaS.Module.Messaging/TakeoutSaaS.Module.Messaging.csproj @@ -5,15 +5,18 @@ enable - - - - - - - + + + + + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj index b8a7787..ad73a3f 100644 --- a/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj +++ b/src/Modules/TakeoutSaaS.Module.Scheduler/TakeoutSaaS.Module.Scheduler.csproj @@ -6,16 +6,19 @@ - + - - - - - + + + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj index 7353ff4..7025f49 100644 --- a/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj +++ b/src/Modules/TakeoutSaaS.Module.Sms/TakeoutSaaS.Module.Sms.csproj @@ -5,15 +5,18 @@ enable - - - - - - - + + + + + + + + + + diff --git a/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj b/src/Modules/TakeoutSaaS.Module.Storage/TakeoutSaaS.Module.Storage.csproj index 733f714205aac15c59b71d6e4073f47d610f9b0a..e5ec475bb63bb015a8531f83251a2c8d13746b1b 100644 GIT binary patch delta 141 zcmca4us~=-9uuS4Er{fe3P|U72v!$I7?vi5;i*^kC#Jt zvjDpm++i3}wSsSLIZN({jal?=sTUM_Er{fe3P|U72v!$I7?vi5;i*^kC!8S Mu@yVxWDZUV0Pg!3XaE2J diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj index 2617cb0..db28803 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TakeoutSaaS.Module.Tenancy.csproj @@ -10,4 +10,7 @@ + + + diff --git a/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj b/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj index 8c1ec99..0e333a1 100644 --- a/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj +++ b/tests/TakeoutSaaS.Application.Tests/TakeoutSaaS.Application.Tests.csproj @@ -9,12 +9,15 @@ - + - + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -25,4 +28,8 @@ + + + + diff --git a/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj b/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj index f4bacfc..24ff938 100644 --- a/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj +++ b/tests/TakeoutSaaS.Integration.Tests/TakeoutSaaS.Integration.Tests.csproj @@ -9,13 +9,16 @@ - - - - + + + + - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + @@ -29,4 +32,8 @@ + + + +