diff --git a/.gitignore b/.gitignore index de1ce4f..8c8e5b2 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,6 @@ obj/ .claude/ *.log /src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj.user + +# 保留根目录 scripts 目录提交 +!scripts/ diff --git a/MISSING_XML_DOCS.md b/MISSING_XML_DOCS.md index 5927194..5b962d5 100644 --- a/MISSING_XML_DOCS.md +++ b/MISSING_XML_DOCS.md @@ -76,26 +76,3 @@ - src/Core/TakeoutSaaS.Shared.Web/Middleware/ExceptionHandlingMiddleware.cs:34 - src/Core/TakeoutSaaS.Shared.Web/Middleware/RequestLoggingMiddleware.cs:13 - src/Core/TakeoutSaaS.Shared.Web/Middleware/SecurityHeadersMiddleware.cs:10 -- src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs:39 -- src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs:15 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs:30 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs:14 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs:15 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs:14 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs:18 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs:12 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs:25 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs:15 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs:17 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs:19 -- src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs:18 -- src/Modules/TakeoutSaaS.Module.Authorization/Attributes/PermissionAuthorizeAttribute.cs:12 -- src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs:10 -- src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs:11 -- src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs:31 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 19ecffe..7320052 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -36,107 +36,331 @@ public sealed class TakeoutAppDbContext( IIdGenerator? idGenerator = null) : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { + /// + /// 租户聚合根。 + /// public DbSet Tenants => Set(); + /// + /// 租户套餐。 + /// public DbSet TenantPackages => Set(); + /// + /// 租户订阅。 + /// public DbSet TenantSubscriptions => Set(); + /// + /// 租户订阅历史。 + /// public DbSet TenantSubscriptionHistories => Set(); + /// + /// 租户配额使用记录。 + /// public DbSet TenantQuotaUsages => Set(); + /// + /// 租户账单。 + /// public DbSet TenantBillingStatements => Set(); + /// + /// 租户通知。 + /// public DbSet TenantNotifications => Set(); + /// + /// 租户公告。 + /// public DbSet TenantAnnouncements => Set(); + /// + /// 租户公告已读记录。 + /// public DbSet TenantAnnouncementReads => Set(); + /// + /// 租户认证资料。 + /// public DbSet TenantVerificationProfiles => Set(); + /// + /// 租户审计日志。 + /// public DbSet TenantAuditLogs => Set(); - + /// + /// 商户实体。 + /// public DbSet Merchants => Set(); + /// + /// 商户资质文件。 + /// public DbSet MerchantDocuments => Set(); + /// + /// 商户合同。 + /// public DbSet MerchantContracts => Set(); + /// + /// 商户员工。 + /// public DbSet MerchantStaff => Set(); + /// + /// 商户审计日志。 + /// public DbSet MerchantAuditLogs => Set(); + /// + /// 商户分类。 + /// public DbSet MerchantCategories => Set(); - + /// + /// 门店实体。 + /// public DbSet Stores => Set(); + /// + /// 门店营业时间。 + /// public DbSet StoreBusinessHours => Set(); + /// + /// 门店节假日。 + /// public DbSet StoreHolidays => Set(); + /// + /// 门店配送区域。 + /// public DbSet StoreDeliveryZones => Set(); + /// + /// 门店桌台区域。 + /// public DbSet StoreTableAreas => Set(); + /// + /// 门店桌台。 + /// public DbSet StoreTables => Set(); + /// + /// 门店员工班次。 + /// public DbSet StoreEmployeeShifts => Set(); + /// + /// 自提配置。 + /// public DbSet StorePickupSettings => Set(); + /// + /// 自提时间段。 + /// public DbSet StorePickupSlots => Set(); - + /// + /// 商品分类。 + /// public DbSet ProductCategories => Set(); + /// + /// 商品。 + /// public DbSet Products => Set(); + /// + /// 商品属性组。 + /// public DbSet ProductAttributeGroups => Set(); + /// + /// 商品属性项。 + /// public DbSet ProductAttributeOptions => Set(); + /// + /// SKU 实体。 + /// public DbSet ProductSkus => Set(); + /// + /// 加料分组。 + /// public DbSet ProductAddonGroups => Set(); + /// + /// 加料选项。 + /// public DbSet ProductAddonOptions => Set(); + /// + /// 定价规则。 + /// public DbSet ProductPricingRules => Set(); + /// + /// 商品媒体资源。 + /// public DbSet ProductMediaAssets => Set(); - + /// + /// 库存项目。 + /// public DbSet InventoryItems => Set(); + /// + /// 库存调整记录。 + /// public DbSet InventoryAdjustments => Set(); + /// + /// 库存批次。 + /// public DbSet InventoryBatches => Set(); + /// + /// 库存锁定记录。 + /// public DbSet InventoryLockRecords => Set(); - + /// + /// 购物车。 + /// public DbSet ShoppingCarts => Set(); + /// + /// 购物车明细。 + /// public DbSet CartItems => Set(); + /// + /// 购物车加料。 + /// public DbSet CartItemAddons => Set(); + /// + /// 结账会话。 + /// public DbSet CheckoutSessions => Set(); - + /// + /// 订单聚合。 + /// public DbSet Orders => Set(); + /// + /// 订单明细。 + /// public DbSet OrderItems => Set(); + /// + /// 订单状态流转。 + /// public DbSet OrderStatusHistories => Set(); + /// + /// 退款申请。 + /// public DbSet RefundRequests => Set(); - + /// + /// 支付记录。 + /// public DbSet PaymentRecords => Set(); + /// + /// 支付退款记录。 + /// public DbSet PaymentRefundRecords => Set(); - + /// + /// 预订记录。 + /// public DbSet Reservations => Set(); + /// + /// 排号记录。 + /// public DbSet QueueTickets => Set(); - + /// + /// 配送订单。 + /// public DbSet DeliveryOrders => Set(); + /// + /// 配送事件。 + /// public DbSet DeliveryEvents => Set(); - + /// + /// 团购订单。 + /// public DbSet GroupOrders => Set(); + /// + /// 团购参与者。 + /// public DbSet GroupParticipants => Set(); - + /// + /// 优惠券模板。 + /// public DbSet CouponTemplates => Set(); + /// + /// 优惠券实例。 + /// public DbSet Coupons => Set(); + /// + /// 营销活动。 + /// public DbSet PromotionCampaigns => Set(); - + /// + /// 会员档案。 + /// public DbSet MemberProfiles => Set(); + /// + /// 会员等级。 + /// public DbSet MemberTiers => Set(); + /// + /// 积分流水。 + /// public DbSet MemberPointLedgers => Set(); + /// + /// 成长值日志。 + /// public DbSet MemberGrowthLogs => Set(); - + /// + /// 会话记录。 + /// public DbSet ChatSessions => Set(); + /// + /// 会话消息。 + /// public DbSet ChatMessages => Set(); + /// + /// 工单记录。 + /// public DbSet SupportTickets => Set(); + /// + /// 工单评论。 + /// public DbSet TicketComments => Set(); - + /// + /// 分销合作伙伴。 + /// public DbSet AffiliatePartners => Set(); + /// + /// 分销订单。 + /// public DbSet AffiliateOrders => Set(); + /// + /// 分销结算。 + /// public DbSet AffiliatePayouts => Set(); - + /// + /// 打卡活动。 + /// public DbSet CheckInCampaigns => Set(); + /// + /// 打卡记录。 + /// public DbSet CheckInRecords => Set(); + /// + /// 社区帖子。 + /// public DbSet CommunityPosts => Set(); + /// + /// 社区评论。 + /// public DbSet CommunityComments => Set(); + /// + /// 社区互动。 + /// public DbSet CommunityReactions => Set(); - + /// + /// 地图位置。 + /// public DbSet MapLocations => Set(); + /// + /// 导航请求。 + /// public DbSet NavigationRequests => Set(); - + /// + /// 指标定义。 + /// public DbSet MetricDefinitions => Set(); + /// + /// 指标快照。 + /// public DbSet MetricSnapshots => Set(); + /// + /// 告警规则。 + /// public DbSet MetricAlertRules => Set(); - + /// + /// 配置实体映射关系。 + /// + /// 模型构建器。 protected override void OnModelCreating(ModelBuilder modelBuilder) { + // 1. 调用基类配置 base.OnModelCreating(modelBuilder); - + // 2. 配置全部实体映射 ConfigureTenant(modelBuilder.Entity()); ConfigureMerchant(modelBuilder.Entity()); ConfigureStore(modelBuilder.Entity()); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs index e29572b..9146c16 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDesignTimeDbContextFactory.cs @@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence; internal sealed class TakeoutAppDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase { + /// + /// 初始化业务库设计时上下文工厂。 + /// public TakeoutAppDesignTimeDbContextFactory() : base(DatabaseConstants.AppDataSource, "TAKEOUTSAAS_APP_CONNECTION") { } - + // 创建设计时上下文 + /// + /// 创建设计时的业务库 DbContext。 + /// + /// 上下文选项。 + /// 租户提供器。 + /// 当前用户访问器。 + /// 业务库上下文实例。 protected override TakeoutAppDbContext CreateContext( DbContextOptions options, ITenantProvider tenantProvider, diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs index a2dcba9..177a576 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/DesignTime/DesignTimeDbContextFactoryBase.cs @@ -16,6 +16,11 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb private readonly string _dataSourceName; private readonly string? _connectionStringEnvVar; + /// + /// 初始化设计时工厂基类。 + /// + /// 数据源名称。 + /// 连接串环境变量名。 protected DesignTimeDbContextFactoryBase(string dataSourceName, string? connectionStringEnvVar = null) { if (string.IsNullOrWhiteSpace(dataSourceName)) @@ -27,8 +32,14 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb _connectionStringEnvVar = connectionStringEnvVar; } + /// + /// 创建设计时 DbContext。 + /// + /// 命令行参数。 + /// DbContext 实例。 public TContext CreateDbContext(string[] args) { + // 1. 构建 DbContextOptions var optionsBuilder = new DbContextOptionsBuilder(); optionsBuilder.UseNpgsql( ResolveConnectionString(), @@ -38,12 +49,20 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb npgsql.EnableRetryOnFailure(); }); + // 2. 创建上下文 return CreateContext( optionsBuilder.Options, new DesignTimeTenantProvider(), new DesignTimeCurrentUserAccessor()); } + /// + /// 由子类实现的上下文工厂方法。 + /// + /// 上下文选项。 + /// 租户提供器。 + /// 当前用户访问器。 + /// DbContext 实例。 protected abstract TContext CreateContext( DbContextOptions options, ITenantProvider tenantProvider, @@ -138,12 +157,22 @@ internal abstract class DesignTimeDbContextFactoryBase : IDesignTimeDb private sealed class DesignTimeTenantProvider : ITenantProvider { + /// + /// 设计时返回默认租户 ID。 + /// + /// 默认租户 ID。 public long GetCurrentTenantId() => 0; } private sealed class DesignTimeCurrentUserAccessor : ICurrentUserAccessor { + /// + /// 设计时用户标识。 + /// public long UserId => 0; + /// + /// 设计时用户鉴权标识。 + /// public bool IsAuthenticated => false; } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs index 193409c..810759e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/ModelBuilderCommentExtensions.cs @@ -11,6 +11,10 @@ namespace TakeoutSaaS.Infrastructure.Common.Persistence; /// internal static class ModelBuilderCommentExtensions { + /// + /// 将 XML 注释应用到实体与属性的 Comment。 + /// + /// 模型构建器。 public static void ApplyXmlComments(this ModelBuilder modelBuilder) { foreach (var entityType in modelBuilder.Model.GetEntityTypes()) @@ -51,6 +55,12 @@ internal static class ModelBuilderCommentExtensions { private static readonly ConcurrentDictionary> Cache = new(); + /// + /// 尝试获取成员的摘要注释。 + /// + /// 反射成员。 + /// 输出的摘要文本。 + /// 存在摘要则返回 true。 public static bool TryGetSummary(MemberInfo member, out string? summary) { summary = null; diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs index 45d82e2..774c649 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDesignTimeDbContextFactory.cs @@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; internal sealed class DictionaryDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase { + /// + /// 初始化字典库设计时上下文工厂。 + /// public DictionaryDesignTimeDbContextFactory() : base(DatabaseConstants.DictionaryDataSource, "TAKEOUTSAAS_DICTIONARY_CONNECTION") { } - + // 创建设计时上下文 + /// + /// 创建设计时的 DictionaryDbContext。 + /// + /// 上下文配置。 + /// 租户提供器。 + /// 当前用户访问器。 + /// DictionaryDbContext 实例。 protected override DictionaryDbContext CreateContext( DbContextOptions options, ITenantProvider tenantProvider, diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs index 4ce7005..18e7bbe 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Repositories/EfDictionaryRepository.cs @@ -11,42 +11,92 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories; /// public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDictionaryRepository { + /// + /// 根据分组 ID 查询分组。 + /// + /// 分组 ID。 + /// 取消标记。 + /// 匹配分组或 null。 public Task FindGroupByIdAsync(long id, CancellationToken cancellationToken = default) => context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); + /// + /// 根据分组编码查询分组。 + /// + /// 分组编码。 + /// 取消标记。 + /// 匹配分组或 null。 public Task FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default) => context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken); + /// + /// 搜索分组列表。 + /// + /// 字典作用域。 + /// 取消标记。 + /// 分组列表。 public async Task> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default) { + // 1. 构建分组查询 var query = context.DictionaryGroups.AsNoTracking(); if (scope.HasValue) { + // 2. 按作用域过滤 query = query.Where(group => group.Scope == scope.Value); } + // 3. 排序返回 return await query .OrderBy(group => group.Code) .ToListAsync(cancellationToken); } + /// + /// 新增分组。 + /// + /// 分组实体。 + /// 取消标记。 + /// 异步任务。 public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) { + // 1. 添加分组 context.DictionaryGroups.Add(group); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 删除分组。 + /// + /// 分组实体。 + /// 取消标记。 + /// 异步任务。 public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) { + // 1. 移除分组 context.DictionaryGroups.Remove(group); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 根据条目 ID 查询字典项。 + /// + /// 条目 ID。 + /// 取消标记。 + /// 匹配条目或 null。 public Task FindItemByIdAsync(long id, CancellationToken cancellationToken = default) => context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); + /// + /// 获取指定分组下的条目列表。 + /// + /// 分组 ID。 + /// 取消标记。 + /// 条目列表。 public async Task> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default) { + // 1. 过滤分组 return await context.DictionaryItems .AsNoTracking() .Where(item => item.GroupId == groupId) @@ -54,23 +104,53 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti .ToListAsync(cancellationToken); } + /// + /// 新增字典项。 + /// + /// 字典项。 + /// 取消标记。 + /// 异步任务。 public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) { + // 1. 添加条目 context.DictionaryItems.Add(item); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 删除字典项。 + /// + /// 字典项。 + /// 取消标记。 + /// 异步任务。 public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) { + // 1. 移除条目 context.DictionaryItems.Remove(item); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 持久化变更。 + /// + /// 取消标记。 + /// 保存任务。 public Task SaveChangesAsync(CancellationToken cancellationToken = default) => context.SaveChangesAsync(cancellationToken); + /// + /// 根据编码集合获取条目列表,可包含系统级条目。 + /// + /// 分组编码集合。 + /// 租户 ID。 + /// 是否包含系统级。 + /// 取消标记。 + /// 条目列表。 public async Task> GetItemsByCodesAsync(IEnumerable codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default) { + // 1. 规范化编码 var normalizedCodes = codes .Where(code => !string.IsNullOrWhiteSpace(code)) .Select(code => code.Trim().ToLowerInvariant()) @@ -82,14 +162,17 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti return Array.Empty(); } + // 2. 构建查询并忽略 QueryFilter var query = context.DictionaryItems .AsNoTracking() .IgnoreQueryFilters() .Include(item => item.Group) .Where(item => normalizedCodes.Contains(item.Group!.Code)); + // 3. 按租户或系统级过滤 query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == 0)); + // 4. 排序返回 return await query .OrderBy(item => item.SortOrder) .ToListAsync(cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs index 4897f82..800af5e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Services/DistributedDictionaryCache.cs @@ -15,8 +15,16 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions private readonly DictionaryCacheOptions _options = options.Value; private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); + /// + /// 读取指定租户与编码的字典缓存。 + /// + /// 租户 ID。 + /// 字典编码。 + /// 取消标记。 + /// 字典项集合或 null。 public async Task?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default) { + // 1. 拼装缓存键 var cacheKey = BuildKey(tenantId, code); var payload = await cache.GetAsync(cacheKey, cancellationToken); if (payload == null || payload.Length == 0) @@ -24,11 +32,21 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions return null; } + // 2. 反序列化 return JsonSerializer.Deserialize>(payload, _serializerOptions); } + /// + /// 设置指定租户与编码的字典缓存。 + /// + /// 租户 ID。 + /// 字典编码。 + /// 字典项集合。 + /// 取消标记。 + /// 异步任务。 public Task SetAsync(long tenantId, string code, IReadOnlyList items, CancellationToken cancellationToken = default) { + // 1. 序列化并写入缓存 var cacheKey = BuildKey(tenantId, code); var payload = JsonSerializer.SerializeToUtf8Bytes(items, _serializerOptions); var options = new DistributedCacheEntryOptions @@ -38,8 +56,16 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions return cache.SetAsync(cacheKey, payload, options, cancellationToken); } + /// + /// 移除指定租户与编码的缓存。 + /// + /// 租户 ID。 + /// 字典编码。 + /// 取消标记。 + /// 异步任务。 public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default) { + // 1. 删除缓存键 var cacheKey = BuildKey(tenantId, code); return cache.RemoveAsync(cacheKey, cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs index 78c3d52..efb45a4 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfMiniUserRepository.cs @@ -9,17 +9,41 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUserRepository { + /// + /// 根据 OpenId 获取小程序用户。 + /// + /// 微信 OpenId。 + /// 取消标记。 + /// 匹配的小程序用户或 null。 public Task FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default) => dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); + /// + /// 根据用户 ID 获取小程序用户。 + /// + /// 用户 ID。 + /// 取消标记。 + /// 匹配的小程序用户或 null。 public Task FindByIdAsync(long id, CancellationToken cancellationToken = default) => dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); + /// + /// 创建或更新小程序用户信息。 + /// + /// 微信 OpenId。 + /// 微信 UnionId。 + /// 昵称。 + /// 头像地址。 + /// 租户 ID。 + /// 取消标记。 + /// 创建或更新后的小程序用户。 public async Task CreateOrUpdateAsync(string openId, string? unionId, string? nickname, string? avatar, long tenantId, CancellationToken cancellationToken = default) { + // 1. 查询现有用户 var user = await dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); if (user == null) { + // 2. 未找到则创建 user = new MiniUser { Id = 0, @@ -33,11 +57,13 @@ public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUse } else { + // 3. 已存在则更新可变字段 user.UnionId = unionId ?? user.UnionId; user.Nickname = nickname ?? user.Nickname; user.Avatar = avatar ?? user.Avatar; } + // 4. 保存更改 await dbContext.SaveChangesAsync(cancellationToken); return user; } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs index b8dbafc..e8b0683 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs @@ -9,66 +9,136 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository { + /// + /// 根据权限 ID 获取权限。 + /// + /// 权限 ID。 + /// 租户 ID。 + /// 取消标记。 + /// 权限实体或 null。 public Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) => dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); + /// + /// 根据权限编码获取权限。 + /// + /// 权限编码。 + /// 租户 ID。 + /// 取消标记。 + /// 权限实体或 null。 public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) => dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken); + /// + /// 根据权限编码集合批量获取权限。 + /// + /// 租户 ID。 + /// 权限编码集合。 + /// 取消标记。 + /// 权限列表。 public Task> GetByCodesAsync(long tenantId, IEnumerable codes, CancellationToken cancellationToken = default) { + // 1. 规范化编码集合 var normalizedCodes = codes .Where(code => !string.IsNullOrWhiteSpace(code)) .Select(code => code.Trim()) .Distinct() .ToArray(); + // 2. 按租户筛选权限 return dbContext.Permissions.AsNoTracking() .Where(x => x.TenantId == tenantId && normalizedCodes.Contains(x.Code)) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } + /// + /// 根据权限 ID 集合批量获取权限。 + /// + /// 租户 ID。 + /// 权限 ID 集合。 + /// 取消标记。 + /// 权限列表。 public Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default) => dbContext.Permissions.AsNoTracking() .Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id)) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + /// + /// 按关键字搜索权限。 + /// + /// 租户 ID。 + /// 搜索关键字。 + /// 取消标记。 + /// 权限列表。 public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) { + // 1. 构建基础查询 var query = dbContext.Permissions.AsNoTracking().Where(x => x.TenantId == tenantId); if (!string.IsNullOrWhiteSpace(keyword)) { + // 2. 追加关键字过滤 var normalized = keyword.Trim(); query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); } + // 3. 返回列表 return query.ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } + /// + /// 新增权限。 + /// + /// 权限实体。 + /// 取消标记。 + /// 异步任务。 public Task AddAsync(Permission permission, CancellationToken cancellationToken = default) { + // 1. 添加实体 dbContext.Permissions.Add(permission); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 更新权限。 + /// + /// 权限实体。 + /// 取消标记。 + /// 异步任务。 public Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default) { + // 1. 标记实体更新 dbContext.Permissions.Update(permission); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 删除指定权限。 + /// + /// 权限 ID。 + /// 租户 ID。 + /// 取消标记。 + /// 异步任务。 public async Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) { + // 1. 查询目标权限 var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); if (entity != null) { + // 2. 删除实体 dbContext.Permissions.Remove(entity); } } + /// + /// 保存仓储变更。 + /// + /// 取消标记。 + /// 保存任务。 public Task SaveChangesAsync(CancellationToken cancellationToken = default) => dbContext.SaveChangesAsync(cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs index 83c5101..ae13a45 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs @@ -9,25 +9,49 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IRolePermissionRepository { + /// + /// 根据角色 ID 集合获取角色权限映射。 + /// + /// 租户 ID。 + /// 角色 ID 集合。 + /// 取消标记。 + /// 角色权限映射列表。 public Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) => dbContext.RolePermissions.AsNoTracking() .Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId)) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + /// + /// 批量新增角色权限。 + /// + /// 角色权限集合。 + /// 取消标记。 + /// 异步任务。 public async Task AddRangeAsync(IEnumerable rolePermissions, CancellationToken cancellationToken = default) { + // 1. 转为数组便于计数 var toAdd = rolePermissions as RolePermission[] ?? rolePermissions.ToArray(); if (toAdd.Length == 0) { return; } + // 2. 批量插入 await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken); } + /// + /// 替换指定角色的权限集合。 + /// + /// 租户 ID。 + /// 角色 ID。 + /// 权限 ID 集合。 + /// 取消标记。 + /// 异步任务。 public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default) { + // 1. 使用执行策略保证可靠性 var strategy = dbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { @@ -52,6 +76,11 @@ public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IR }); } + /// + /// 保存仓储变更。 + /// + /// 取消标记。 + /// 保存任务。 public Task SaveChangesAsync(CancellationToken cancellationToken = default) => dbContext.SaveChangesAsync(cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs index 10f17d8..aa7241f 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs @@ -9,53 +9,114 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleRepository { + /// + /// 根据角色 ID 获取角色。 + /// + /// 角色 ID。 + /// 租户 ID。 + /// 取消标记。 + /// 角色实体或 null。 public Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) => dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken); + /// + /// 根据角色编码获取角色。 + /// + /// 角色编码。 + /// 租户 ID。 + /// 取消标记。 + /// 角色实体或 null。 public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) => dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken); + /// + /// 根据角色 ID 集合获取角色列表。 + /// + /// 租户 ID。 + /// 角色 ID 集合。 + /// 取消标记。 + /// 角色列表。 public Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) => dbContext.Roles.AsNoTracking() .Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + /// + /// 按关键字搜索角色。 + /// + /// 租户 ID。 + /// 搜索关键字。 + /// 取消标记。 + /// 角色列表。 public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) { + // 1. 构建基础查询 var query = dbContext.Roles.AsNoTracking().Where(x => x.TenantId == tenantId && x.DeletedAt == null); if (!string.IsNullOrWhiteSpace(keyword)) { + // 2. 追加关键字过滤 var normalized = keyword.Trim(); query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); } + // 3. 返回列表 return query.ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } + /// + /// 新增角色。 + /// + /// 角色实体。 + /// 取消标记。 + /// 异步任务。 public Task AddAsync(Role role, CancellationToken cancellationToken = default) { + // 1. 添加实体 dbContext.Roles.Add(role); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 更新角色。 + /// + /// 角色实体。 + /// 取消标记。 + /// 异步任务。 public Task UpdateAsync(Role role, CancellationToken cancellationToken = default) { + // 1. 标记更新 dbContext.Roles.Update(role); + // 2. 返回完成任务 return Task.CompletedTask; } + /// + /// 软删除角色。 + /// + /// 角色 ID。 + /// 租户 ID。 + /// 取消标记。 + /// 异步任务。 public async Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) { + // 1. 查询目标角色 var entity = await dbContext.Roles.FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken); if (entity != null) { + // 2. 标记删除时间 entity.DeletedAt = DateTime.UtcNow; dbContext.Roles.Update(entity); } } + /// + /// 保存仓储变更。 + /// + /// 取消标记。 + /// 保存任务。 public Task SaveChangesAsync(CancellationToken cancellationToken = default) => dbContext.SaveChangesAsync(cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs index 625c0df..e84131d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleTemplateRepository.cs @@ -9,83 +9,151 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class EfRoleTemplateRepository(IdentityDbContext dbContext) : IRoleTemplateRepository { + /// + /// 获取全部角色模板,可选按启用状态过滤。 + /// + /// 是否启用过滤。 + /// 取消标记。 + /// 角色模板列表。 public Task> GetAllAsync(bool? isActive, CancellationToken cancellationToken = default) { + // 1. 构建基础查询 var query = dbContext.RoleTemplates.AsNoTracking(); if (isActive.HasValue) { + // 2. 按启用状态过滤 query = query.Where(x => x.IsActive == isActive.Value); } + // 3. 排序并返回 return query .OrderBy(x => x.TemplateCode) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } + /// + /// 根据模板编码获取角色模板。 + /// + /// 模板编码。 + /// 取消标记。 + /// 角色模板或 null。 public Task FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default) { + // 1. 规范化编码 var normalized = templateCode.Trim(); + // 2. 查询模板 return dbContext.RoleTemplates.AsNoTracking().FirstOrDefaultAsync(x => x.TemplateCode == normalized, cancellationToken); } + /// + /// 获取指定模板的权限集合。 + /// + /// 模板 ID。 + /// 取消标记。 + /// 模板权限列表。 public Task> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default) { + // 1. 查询模板权限 return dbContext.RoleTemplatePermissions.AsNoTracking() .Where(x => x.RoleTemplateId == roleTemplateId) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); } + /// + /// 获取多个模板的权限集合。 + /// + /// 模板 ID 集合。 + /// 取消标记。 + /// 模板到权限的字典。 public async Task>> GetPermissionsAsync(IEnumerable roleTemplateIds, CancellationToken cancellationToken = default) { + // 1. 去重 ID var ids = roleTemplateIds.Distinct().ToArray(); if (ids.Length == 0) { return new Dictionary>(); } + // 2. 批量查询权限 var permissions = await dbContext.RoleTemplatePermissions.AsNoTracking() .Where(x => ids.Contains(x.RoleTemplateId)) .ToListAsync(cancellationToken); + // 3. 组装字典 return permissions .GroupBy(x => x.RoleTemplateId) .ToDictionary(g => g.Key, g => (IReadOnlyList)g.ToList()); } + /// + /// 新增角色模板并配置权限。 + /// + /// 角色模板实体。 + /// 权限编码集合。 + /// 取消标记。 + /// 异步任务。 public async Task AddAsync(RoleTemplate template, IEnumerable permissionCodes, CancellationToken cancellationToken = default) { + // 1. 规范化模板字段 template.TemplateCode = template.TemplateCode.Trim(); template.Name = template.Name.Trim(); + // 2. 保存模板 await dbContext.RoleTemplates.AddAsync(template, cancellationToken); + // 3. 替换权限 await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken); } + /// + /// 更新角色模板并重置权限。 + /// + /// 角色模板实体。 + /// 权限编码集合。 + /// 取消标记。 + /// 异步任务。 public async Task UpdateAsync(RoleTemplate template, IEnumerable permissionCodes, CancellationToken cancellationToken = default) { + // 1. 规范化模板字段 template.TemplateCode = template.TemplateCode.Trim(); template.Name = template.Name.Trim(); + // 2. 更新模板 dbContext.RoleTemplates.Update(template); + // 3. 重置权限 await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken); } + /// + /// 删除角色模板及其权限。 + /// + /// 模板 ID。 + /// 取消标记。 + /// 异步任务。 public async Task DeleteAsync(long roleTemplateId, CancellationToken cancellationToken = default) { + // 1. 查询模板 var entity = await dbContext.RoleTemplates.FirstOrDefaultAsync(x => x.Id == roleTemplateId, cancellationToken); if (entity != null) { + // 2. 删除关联权限 var permissions = dbContext.RoleTemplatePermissions.Where(x => x.RoleTemplateId == roleTemplateId); dbContext.RoleTemplatePermissions.RemoveRange(permissions); + // 3. 删除模板 dbContext.RoleTemplates.Remove(entity); } } + /// + /// 保存仓储变更。 + /// + /// 取消标记。 + /// 保存任务。 public Task SaveChangesAsync(CancellationToken cancellationToken = default) => dbContext.SaveChangesAsync(cancellationToken); private async Task ReplacePermissionsInternalAsync(RoleTemplate template, IEnumerable permissionCodes, CancellationToken cancellationToken) { + // 1. 使用执行策略保证一致性 var strategy = dbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs index 8b26acb..95c120b 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs @@ -9,32 +9,58 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRoleRepository { + /// + /// 根据用户 ID 集合获取用户角色映射。 + /// + /// 租户 ID。 + /// 用户 ID 集合。 + /// 取消标记。 + /// 用户角色映射列表。 public Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) => dbContext.UserRoles.AsNoTracking() .Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId)) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + /// + /// 获取指定用户的角色集合。 + /// + /// 租户 ID。 + /// 用户 ID。 + /// 取消标记。 + /// 用户角色列表。 public Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default) => dbContext.UserRoles.AsNoTracking() .Where(x => x.TenantId == tenantId && x.UserId == userId) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + /// + /// 替换指定用户的角色集合。 + /// + /// 租户 ID。 + /// 用户 ID。 + /// 角色 ID 集合。 + /// 取消标记。 + /// 异步任务。 public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default) { + // 1. 使用执行策略保障一致性 var strategy = dbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { await using var trx = await dbContext.Database.BeginTransactionAsync(cancellationToken); + // 2. 读取当前角色映射 var existing = await dbContext.UserRoles .Where(x => x.TenantId == tenantId && x.UserId == userId) .ToListAsync(cancellationToken); + // 3. 清空并保存 dbContext.UserRoles.RemoveRange(existing); await dbContext.SaveChangesAsync(cancellationToken); + // 4. 构建新映射 var toAdd = roleIds.Distinct().Select(roleId => new UserRole { TenantId = tenantId, @@ -42,6 +68,7 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol RoleId = roleId }); + // 5. 批量新增并保存 await dbContext.UserRoles.AddRangeAsync(toAdd, cancellationToken); await dbContext.SaveChangesAsync(cancellationToken); @@ -49,6 +76,11 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol }); } + /// + /// 保存仓储变更。 + /// + /// 取消标记。 + /// 保存任务。 public Task SaveChangesAsync(CancellationToken cancellationToken = default) => dbContext.SaveChangesAsync(cancellationToken); } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs index 17727ae..768e992 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDataSeeder.cs @@ -22,38 +22,52 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; /// public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger logger) : IHostedService { + /// + /// 执行后台账号与权限种子。 + /// + /// 取消标记。 + /// 异步任务。 public async Task StartAsync(CancellationToken cancellationToken) { + // 1. 创建作用域并解析依赖 using var scope = serviceProvider.CreateScope(); var context = scope.ServiceProvider.GetRequiredService(); var options = scope.ServiceProvider.GetRequiredService>().Value; var passwordHasher = scope.ServiceProvider.GetRequiredService>(); var tenantContextAccessor = scope.ServiceProvider.GetRequiredService(); + // 2. 校验功能开关 if (!options.Enabled) { logger.LogInformation("AdminSeed 已禁用,跳过后台账号初始化"); return; } + // 3. 确保数据库已迁移 await context.Database.MigrateAsync(cancellationToken); + // 4. 校验账号配置 if (options.Users is null or { Count: 0 }) { logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化"); return; } + // 5. 写入角色模板 await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken); + // 6. 逐个账号处理 foreach (var userOptions in options.Users) { + // 6.1 进入租户作用域 using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId); + // 6.2 查询账号并收集配置 var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken); var roles = NormalizeValues(userOptions.Roles); var permissions = NormalizeValues(userOptions.Permissions); if (user == null) { + // 6.3 创建新账号 user = new DomainIdentityUser { Id = 0, @@ -69,6 +83,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger } else { + // 6.4 更新既有账号 user.DisplayName = userOptions.DisplayName; user.TenantId = userOptions.TenantId; user.MerchantId = userOptions.MerchantId; @@ -76,7 +91,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger logger.LogInformation("已更新后台账号 {Account}", user.Account); } - // 确保角色存在 + // 6.5 确保角色存在 var existingRoles = await context.Roles .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) .ToListAsync(cancellationToken); @@ -97,7 +112,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger }); } - // 确保权限存在 + // 6.6 确保权限存在 var existingPermissions = await context.Permissions .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) .ToListAsync(cancellationToken); @@ -118,9 +133,10 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger }); } + // 6.7 保存基础角色/权限 await context.SaveChangesAsync(cancellationToken); - // 重新加载角色/权限以获取 Id + // 6.8 重新加载角色/权限以获取 Id var roleEntities = await context.Roles .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) .ToListAsync(cancellationToken); @@ -128,7 +144,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) .ToListAsync(cancellationToken); - // 重置用户角色 + // 6.9 重置用户角色 var existingUserRoles = await context.UserRoles .Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id) .ToListAsync(cancellationToken); @@ -191,6 +207,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger continue; } + // 6.10 绑定角色与权限 await context.RolePermissions.AddAsync(new DomainRolePermission { TenantId = userOptions.TenantId, @@ -209,9 +226,15 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger } } + // 7. 最终保存 await context.SaveChangesAsync(cancellationToken); } + /// + /// 停止生命周期时的清理(此处无需处理)。 + /// + /// 取消标记。 + /// 已完成任务。 public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; private static async Task SeedRoleTemplatesAsync( @@ -219,23 +242,28 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger IList templates, CancellationToken cancellationToken) { + // 1. 空集合直接返回 if (templates is null || templates.Count == 0) { return; } + // 2. 逐个处理模板 foreach (var templateOptions in templates) { + // 2.1 校验必填字段 if (string.IsNullOrWhiteSpace(templateOptions.TemplateCode) || string.IsNullOrWhiteSpace(templateOptions.Name)) { continue; } + // 2.2 查询现有模板 var code = templateOptions.TemplateCode.Trim(); var existing = await context.RoleTemplates.FirstOrDefaultAsync(x => x.TemplateCode == code, cancellationToken); if (existing == null) { + // 2.3 新增模板 existing = new DomainRoleTemplate { TemplateCode = code, @@ -249,6 +277,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger } else { + // 2.4 更新模板 existing.Name = templateOptions.Name.Trim(); existing.Description = templateOptions.Description; existing.IsActive = templateOptions.IsActive; @@ -256,13 +285,15 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger await context.SaveChangesAsync(cancellationToken); } + // 2.5 重置模板权限 var permissionCodes = NormalizeValues(templateOptions.Permissions); var existingPermissions = await context.RoleTemplatePermissions .Where(x => x.RoleTemplateId == existing.Id) .ToListAsync(cancellationToken); - + // 2.6 清空旧权限并保存 context.RoleTemplatePermissions.RemoveRange(existingPermissions); await context.SaveChangesAsync(cancellationToken); + // 2.7 去重后的权限编码 var distinctPermissionCodes = permissionCodes.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); foreach (var permissionCode in distinctPermissionCodes) { diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs index c18d179..926cba6 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDesignTimeDbContextFactory.cs @@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence; internal sealed class IdentityDesignTimeDbContextFactory : DesignTimeDbContextFactoryBase { + /// + /// 初始化 Identity 设计时上下文工厂。 + /// public IdentityDesignTimeDbContextFactory() : base(DatabaseConstants.IdentityDataSource, "TAKEOUTSAAS_IDENTITY_CONNECTION") { } - + // 创建设计时上下文实例 + /// + /// 创建设计时的 IdentityDbContext。 + /// + /// DbContext 配置。 + /// 租户提供器。 + /// 当前用户访问器。 + /// IdentityDbContext 实例。 protected override IdentityDbContext CreateContext( DbContextOptions options, ITenantProvider tenantProvider, diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs index 3516290..94491e0 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisLoginRateLimiter.cs @@ -14,8 +14,15 @@ public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions + /// 校验指定键的登录尝试次数,超限将抛出业务异常。 + /// + /// 限流键(如账号或 IP)。 + /// 取消标记。 + /// 异步任务。 public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default) { + // 1. 读取当前计数 var cacheKey = BuildKey(key); var current = await cache.GetStringAsync(cacheKey, cancellationToken); var count = string.IsNullOrWhiteSpace(current) ? 0 : int.Parse(current); @@ -24,6 +31,7 @@ public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions + /// 重置指定键的登录计数。 + /// + /// 限流键(如账号或 IP)。 + /// 取消标记。 + /// 异步任务。 public Task ResetAsync(string key, CancellationToken cancellationToken = default) => cache.RemoveAsync(BuildKey(key), cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs index 6c5651e..a1970c6 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/RedisRefreshTokenStore.cs @@ -16,11 +16,20 @@ public sealed class RedisRefreshTokenStore(IDistributedCache cache, IOptions + /// 签发刷新令牌并写入缓存。 + /// + /// 用户 ID。 + /// 过期时间。 + /// 取消标记。 + /// 刷新令牌描述。 public async Task IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default) { + // 1. 生成随机令牌 var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48)); var descriptor = new RefreshTokenDescriptor(token, userId, expiresAt, false); + // 2. 写入缓存 var key = BuildKey(token); var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt }; await cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken); @@ -28,22 +37,37 @@ public sealed class RedisRefreshTokenStore(IDistributedCache cache, IOptions + /// 获取刷新令牌描述。 + /// + /// 刷新令牌值。 + /// 取消标记。 + /// 刷新令牌描述或 null。 public async Task GetAsync(string refreshToken, CancellationToken cancellationToken = default) { + // 1. 读取缓存 var json = await cache.GetStringAsync(BuildKey(refreshToken), cancellationToken); return string.IsNullOrWhiteSpace(json) ? null : JsonSerializer.Deserialize(json, JsonOptions); } + /// + /// 吊销刷新令牌。 + /// + /// 刷新令牌值。 + /// 取消标记。 + /// 异步任务。 public async Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default) { + // 1. 读取令牌 var descriptor = await GetAsync(refreshToken, cancellationToken); if (descriptor == null) { return; } + // 2. 标记吊销并回写缓存 var updated = descriptor with { Revoked = true }; var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt }; await cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs index bbc6328..272ffc9 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/WeChatAuthService.cs @@ -15,18 +15,27 @@ public sealed class WeChatAuthService(HttpClient httpClient, IOptions + /// 调用微信接口完成 code2Session。 + /// + /// 临时登录凭证 code。 + /// 取消标记。 + /// 微信会话信息。 public async Task Code2SessionAsync(string code, CancellationToken cancellationToken = default) { + // 1. 拼装请求地址 var requestUri = $"sns/jscode2session?appid={Uri.EscapeDataString(_options.AppId)}&secret={Uri.EscapeDataString(_options.Secret)}&js_code={Uri.EscapeDataString(code)}&grant_type=authorization_code"; using var response = await httpClient.GetAsync(requestUri, cancellationToken); response.EnsureSuccessStatusCode(); + // 2. 读取响应 var payload = await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken); if (payload == null) { throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:响应为空"); } + // 3. 校验错误码 if (payload.ErrorCode.HasValue && payload.ErrorCode.Value != 0) { var message = string.IsNullOrWhiteSpace(payload.ErrorMessage) @@ -35,11 +44,13 @@ public sealed class WeChatAuthService(HttpClient httpClient, IOptions -/// 权限校验特性 +/// 权限校验特性。 /// [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] public sealed class PermissionAuthorizeAttribute : AuthorizeAttribute { + /// + /// 初始化权限校验特性并构建对应策略。 + /// + /// 所需的权限标识集合。 public PermissionAuthorizeAttribute(params string[] permissions) { + // 1. 校验权限参数不为空 ArgumentNullException.ThrowIfNull(permissions); + + // 2. 规范化权限标识 var normalized = permissions .Where(p => !string.IsNullOrWhiteSpace(p)) .Select(p => p.Trim()) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); + // 3. 确保至少提供一个有效权限 if (normalized.Length == 0) { throw new ArgumentException("至少需要一个权限标识", nameof(permissions)); } + // 4. 绑定权限集合并生成策略名称 Permissions = normalized; Policy = PermissionAuthorizationPolicyProvider.BuildPolicyName(normalized); } - /// - /// 所需权限集合 + /// 所需权限集合。 /// public IReadOnlyCollection Permissions { get; } } diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs index 780e58d..138d96b 100644 --- a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationHandler.cs @@ -1,38 +1,45 @@ using Microsoft.AspNetCore.Authorization; - namespace TakeoutSaaS.Module.Authorization.Policies; - /// -/// 权限校验处理器 +/// 权限校验处理器。 /// public sealed class PermissionAuthorizationHandler : AuthorizationHandler { + /// + /// 用户声明中权限的声明类型键。 + /// public const string PermissionClaimType = "permission"; - + /// + /// 校验当前用户是否具备要求的权限集合。 + /// + /// 授权上下文。 + /// 权限需求描述。 + /// 异步完成任务。 protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement) { + // 1. 校验用户已通过认证 if (context.User?.Identity?.IsAuthenticated != true) { return Task.CompletedTask; } - + // 2. 收集用户已授予的权限标识 var userPermissions = context.User .FindAll(PermissionClaimType) .Select(claim => claim.Value) .Where(value => !string.IsNullOrWhiteSpace(value)) .Select(value => value.Trim()) .ToHashSet(StringComparer.OrdinalIgnoreCase); - + // 3. 无权限直接结束 if (userPermissions.Count == 0) { return Task.CompletedTask; } - + // 4. 任一权限匹配即视为授权通过 if (requirement.Permissions.Any(userPermissions.Contains)) { context.Succeed(requirement); } - + // 5. 结束处理 return Task.CompletedTask; } } diff --git a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs index 4f2e610..4f1329c 100644 --- a/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs +++ b/src/Modules/TakeoutSaaS.Module.Authorization/Policies/PermissionAuthorizationPolicyProvider.cs @@ -1,55 +1,62 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Options; - namespace TakeoutSaaS.Module.Authorization.Policies; - /// -/// 权限策略提供者(按需动态构建策略) +/// 权限策略提供者(按需动态构建策略)。 /// public sealed class PermissionAuthorizationPolicyProvider(IOptions options) : DefaultAuthorizationPolicyProvider(options) { + /// + /// 权限策略名称前缀。 + /// public const string PolicyPrefix = "PERMISSION:"; private readonly AuthorizationOptions _options = options.Value; - + /// + /// 获取或构建指定名称的权限策略。 + /// + /// 策略名称。 + /// 匹配的授权策略。 public override Task GetPolicyAsync(string policyName) { - if (policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase)) + // 1. 非权限策略走基类逻辑 + if (!policyName.StartsWith(PolicyPrefix, StringComparison.OrdinalIgnoreCase)) { - var existingPolicy = _options.GetPolicy(policyName); - if (existingPolicy != null) - { - return Task.FromResult(existingPolicy); - } - - var permissions = ParsePermissions(policyName); - if (permissions.Length == 0) - { - return Task.FromResult(null); - } - - var policy = new AuthorizationPolicyBuilder() - .AddRequirements(new PermissionRequirement(permissions)) - .Build(); - - _options.AddPolicy(policyName, policy); - return Task.FromResult(policy); + return base.GetPolicyAsync(policyName); } - - return base.GetPolicyAsync(policyName); + // 2. 复用已存在的策略 + var existingPolicy = _options.GetPolicy(policyName); + if (existingPolicy != null) + { + return Task.FromResult(existingPolicy); + } + // 3. 解析策略携带的权限列表 + var permissions = ParsePermissions(policyName); + if (permissions.Length == 0) + { + return Task.FromResult(null); + } + // 4. 动态构建策略并缓存 + var policy = new AuthorizationPolicyBuilder() + .AddRequirements(new PermissionRequirement(permissions)) + .Build(); + _options.AddPolicy(policyName, policy); + // 5. 返回构建好的策略 + return Task.FromResult(policy); } - /// - /// 根据权限集合构建策略名称 + /// 根据权限集合构建策略名称。 /// + /// 权限标识集合。 + /// 策略名称。 public static string BuildPolicyName(IEnumerable permissions) => $"{PolicyPrefix}{string.Join('|', NormalizePermissions(permissions))}"; - private static string[] ParsePermissions(string policyName) { + // 1. 拆分策略名称得到原始权限列表 var raw = policyName[PolicyPrefix.Length..]; + // 2. 规范化并过滤权限 return NormalizePermissions(raw.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); } - private static string[] NormalizePermissions(IEnumerable permissions) => [.. permissions .Where(p => !string.IsNullOrWhiteSpace(p)) diff --git a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs index b3b0589..ed3d78b 100644 --- a/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs +++ b/src/Modules/TakeoutSaaS.Module.Tenancy/TenantContextAccessor.cs @@ -1,7 +1,5 @@ using TakeoutSaaS.Shared.Abstractions.Tenancy; - namespace TakeoutSaaS.Module.Tenancy; - /// /// 基于 的租户上下文访问器,实现请求级别隔离。 /// @@ -9,7 +7,10 @@ public sealed class TenantContextAccessor : ITenantContextAccessor { private static readonly AsyncLocal Holder = new(); - /// + // 当前请求的租户上下文访问入口 + /// + /// 获取或设置当前请求的租户上下文。 + /// public TenantContext? Current { get => Holder.Value?.Context; @@ -26,6 +27,7 @@ public sealed class TenantContextAccessor : ITenantContextAccessor } } + // 内部持有器用于绑定异步局部上下文 private sealed class TenantContextHolder { public TenantContext? Context { get; set; }