docs: add xml comments and update ignore rules

This commit is contained in:
2025-12-12 10:39:51 +08:00
parent d38127d6b2
commit 715cbb3d36
24 changed files with 865 additions and 95 deletions

3
.gitignore vendored
View File

@@ -6,3 +6,6 @@ obj/
.claude/
*.log
/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj.user
# 保留根目录 scripts 目录提交
!scripts/

View File

@@ -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

View File

@@ -36,107 +36,331 @@ public sealed class TakeoutAppDbContext(
IIdGenerator? idGenerator = null)
: TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator)
{
/// <summary>
/// 租户聚合根。
/// </summary>
public DbSet<Tenant> Tenants => Set<Tenant>();
/// <summary>
/// 租户套餐。
/// </summary>
public DbSet<TenantPackage> TenantPackages => Set<TenantPackage>();
/// <summary>
/// 租户订阅。
/// </summary>
public DbSet<TenantSubscription> TenantSubscriptions => Set<TenantSubscription>();
/// <summary>
/// 租户订阅历史。
/// </summary>
public DbSet<TenantSubscriptionHistory> TenantSubscriptionHistories => Set<TenantSubscriptionHistory>();
/// <summary>
/// 租户配额使用记录。
/// </summary>
public DbSet<TenantQuotaUsage> TenantQuotaUsages => Set<TenantQuotaUsage>();
/// <summary>
/// 租户账单。
/// </summary>
public DbSet<TenantBillingStatement> TenantBillingStatements => Set<TenantBillingStatement>();
/// <summary>
/// 租户通知。
/// </summary>
public DbSet<TenantNotification> TenantNotifications => Set<TenantNotification>();
/// <summary>
/// 租户公告。
/// </summary>
public DbSet<TenantAnnouncement> TenantAnnouncements => Set<TenantAnnouncement>();
/// <summary>
/// 租户公告已读记录。
/// </summary>
public DbSet<TenantAnnouncementRead> TenantAnnouncementReads => Set<TenantAnnouncementRead>();
/// <summary>
/// 租户认证资料。
/// </summary>
public DbSet<TenantVerificationProfile> TenantVerificationProfiles => Set<TenantVerificationProfile>();
/// <summary>
/// 租户审计日志。
/// </summary>
public DbSet<TenantAuditLog> TenantAuditLogs => Set<TenantAuditLog>();
/// <summary>
/// 商户实体。
/// </summary>
public DbSet<Merchant> Merchants => Set<Merchant>();
/// <summary>
/// 商户资质文件。
/// </summary>
public DbSet<MerchantDocument> MerchantDocuments => Set<MerchantDocument>();
/// <summary>
/// 商户合同。
/// </summary>
public DbSet<MerchantContract> MerchantContracts => Set<MerchantContract>();
/// <summary>
/// 商户员工。
/// </summary>
public DbSet<MerchantStaff> MerchantStaff => Set<MerchantStaff>();
/// <summary>
/// 商户审计日志。
/// </summary>
public DbSet<MerchantAuditLog> MerchantAuditLogs => Set<MerchantAuditLog>();
/// <summary>
/// 商户分类。
/// </summary>
public DbSet<MerchantCategory> MerchantCategories => Set<MerchantCategory>();
/// <summary>
/// 门店实体。
/// </summary>
public DbSet<Store> Stores => Set<Store>();
/// <summary>
/// 门店营业时间。
/// </summary>
public DbSet<StoreBusinessHour> StoreBusinessHours => Set<StoreBusinessHour>();
/// <summary>
/// 门店节假日。
/// </summary>
public DbSet<StoreHoliday> StoreHolidays => Set<StoreHoliday>();
/// <summary>
/// 门店配送区域。
/// </summary>
public DbSet<StoreDeliveryZone> StoreDeliveryZones => Set<StoreDeliveryZone>();
/// <summary>
/// 门店桌台区域。
/// </summary>
public DbSet<StoreTableArea> StoreTableAreas => Set<StoreTableArea>();
/// <summary>
/// 门店桌台。
/// </summary>
public DbSet<StoreTable> StoreTables => Set<StoreTable>();
/// <summary>
/// 门店员工班次。
/// </summary>
public DbSet<StoreEmployeeShift> StoreEmployeeShifts => Set<StoreEmployeeShift>();
/// <summary>
/// 自提配置。
/// </summary>
public DbSet<StorePickupSetting> StorePickupSettings => Set<StorePickupSetting>();
/// <summary>
/// 自提时间段。
/// </summary>
public DbSet<StorePickupSlot> StorePickupSlots => Set<StorePickupSlot>();
/// <summary>
/// 商品分类。
/// </summary>
public DbSet<ProductCategory> ProductCategories => Set<ProductCategory>();
/// <summary>
/// 商品。
/// </summary>
public DbSet<Product> Products => Set<Product>();
/// <summary>
/// 商品属性组。
/// </summary>
public DbSet<ProductAttributeGroup> ProductAttributeGroups => Set<ProductAttributeGroup>();
/// <summary>
/// 商品属性项。
/// </summary>
public DbSet<ProductAttributeOption> ProductAttributeOptions => Set<ProductAttributeOption>();
/// <summary>
/// SKU 实体。
/// </summary>
public DbSet<ProductSku> ProductSkus => Set<ProductSku>();
/// <summary>
/// 加料分组。
/// </summary>
public DbSet<ProductAddonGroup> ProductAddonGroups => Set<ProductAddonGroup>();
/// <summary>
/// 加料选项。
/// </summary>
public DbSet<ProductAddonOption> ProductAddonOptions => Set<ProductAddonOption>();
/// <summary>
/// 定价规则。
/// </summary>
public DbSet<ProductPricingRule> ProductPricingRules => Set<ProductPricingRule>();
/// <summary>
/// 商品媒体资源。
/// </summary>
public DbSet<ProductMediaAsset> ProductMediaAssets => Set<ProductMediaAsset>();
/// <summary>
/// 库存项目。
/// </summary>
public DbSet<InventoryItem> InventoryItems => Set<InventoryItem>();
/// <summary>
/// 库存调整记录。
/// </summary>
public DbSet<InventoryAdjustment> InventoryAdjustments => Set<InventoryAdjustment>();
/// <summary>
/// 库存批次。
/// </summary>
public DbSet<InventoryBatch> InventoryBatches => Set<InventoryBatch>();
/// <summary>
/// 库存锁定记录。
/// </summary>
public DbSet<InventoryLockRecord> InventoryLockRecords => Set<InventoryLockRecord>();
/// <summary>
/// 购物车。
/// </summary>
public DbSet<ShoppingCart> ShoppingCarts => Set<ShoppingCart>();
/// <summary>
/// 购物车明细。
/// </summary>
public DbSet<CartItem> CartItems => Set<CartItem>();
/// <summary>
/// 购物车加料。
/// </summary>
public DbSet<CartItemAddon> CartItemAddons => Set<CartItemAddon>();
/// <summary>
/// 结账会话。
/// </summary>
public DbSet<CheckoutSession> CheckoutSessions => Set<CheckoutSession>();
/// <summary>
/// 订单聚合。
/// </summary>
public DbSet<Order> Orders => Set<Order>();
/// <summary>
/// 订单明细。
/// </summary>
public DbSet<OrderItem> OrderItems => Set<OrderItem>();
/// <summary>
/// 订单状态流转。
/// </summary>
public DbSet<OrderStatusHistory> OrderStatusHistories => Set<OrderStatusHistory>();
/// <summary>
/// 退款申请。
/// </summary>
public DbSet<RefundRequest> RefundRequests => Set<RefundRequest>();
/// <summary>
/// 支付记录。
/// </summary>
public DbSet<PaymentRecord> PaymentRecords => Set<PaymentRecord>();
/// <summary>
/// 支付退款记录。
/// </summary>
public DbSet<PaymentRefundRecord> PaymentRefundRecords => Set<PaymentRefundRecord>();
/// <summary>
/// 预订记录。
/// </summary>
public DbSet<Reservation> Reservations => Set<Reservation>();
/// <summary>
/// 排号记录。
/// </summary>
public DbSet<QueueTicket> QueueTickets => Set<QueueTicket>();
/// <summary>
/// 配送订单。
/// </summary>
public DbSet<DeliveryOrder> DeliveryOrders => Set<DeliveryOrder>();
/// <summary>
/// 配送事件。
/// </summary>
public DbSet<DeliveryEvent> DeliveryEvents => Set<DeliveryEvent>();
/// <summary>
/// 团购订单。
/// </summary>
public DbSet<GroupOrder> GroupOrders => Set<GroupOrder>();
/// <summary>
/// 团购参与者。
/// </summary>
public DbSet<GroupParticipant> GroupParticipants => Set<GroupParticipant>();
/// <summary>
/// 优惠券模板。
/// </summary>
public DbSet<CouponTemplate> CouponTemplates => Set<CouponTemplate>();
/// <summary>
/// 优惠券实例。
/// </summary>
public DbSet<Coupon> Coupons => Set<Coupon>();
/// <summary>
/// 营销活动。
/// </summary>
public DbSet<PromotionCampaign> PromotionCampaigns => Set<PromotionCampaign>();
/// <summary>
/// 会员档案。
/// </summary>
public DbSet<MemberProfile> MemberProfiles => Set<MemberProfile>();
/// <summary>
/// 会员等级。
/// </summary>
public DbSet<MemberTier> MemberTiers => Set<MemberTier>();
/// <summary>
/// 积分流水。
/// </summary>
public DbSet<MemberPointLedger> MemberPointLedgers => Set<MemberPointLedger>();
/// <summary>
/// 成长值日志。
/// </summary>
public DbSet<MemberGrowthLog> MemberGrowthLogs => Set<MemberGrowthLog>();
/// <summary>
/// 会话记录。
/// </summary>
public DbSet<ChatSession> ChatSessions => Set<ChatSession>();
/// <summary>
/// 会话消息。
/// </summary>
public DbSet<ChatMessage> ChatMessages => Set<ChatMessage>();
/// <summary>
/// 工单记录。
/// </summary>
public DbSet<SupportTicket> SupportTickets => Set<SupportTicket>();
/// <summary>
/// 工单评论。
/// </summary>
public DbSet<TicketComment> TicketComments => Set<TicketComment>();
/// <summary>
/// 分销合作伙伴。
/// </summary>
public DbSet<AffiliatePartner> AffiliatePartners => Set<AffiliatePartner>();
/// <summary>
/// 分销订单。
/// </summary>
public DbSet<AffiliateOrder> AffiliateOrders => Set<AffiliateOrder>();
/// <summary>
/// 分销结算。
/// </summary>
public DbSet<AffiliatePayout> AffiliatePayouts => Set<AffiliatePayout>();
/// <summary>
/// 打卡活动。
/// </summary>
public DbSet<CheckInCampaign> CheckInCampaigns => Set<CheckInCampaign>();
/// <summary>
/// 打卡记录。
/// </summary>
public DbSet<CheckInRecord> CheckInRecords => Set<CheckInRecord>();
/// <summary>
/// 社区帖子。
/// </summary>
public DbSet<CommunityPost> CommunityPosts => Set<CommunityPost>();
/// <summary>
/// 社区评论。
/// </summary>
public DbSet<CommunityComment> CommunityComments => Set<CommunityComment>();
/// <summary>
/// 社区互动。
/// </summary>
public DbSet<CommunityReaction> CommunityReactions => Set<CommunityReaction>();
/// <summary>
/// 地图位置。
/// </summary>
public DbSet<MapLocation> MapLocations => Set<MapLocation>();
/// <summary>
/// 导航请求。
/// </summary>
public DbSet<NavigationRequest> NavigationRequests => Set<NavigationRequest>();
/// <summary>
/// 指标定义。
/// </summary>
public DbSet<MetricDefinition> MetricDefinitions => Set<MetricDefinition>();
/// <summary>
/// 指标快照。
/// </summary>
public DbSet<MetricSnapshot> MetricSnapshots => Set<MetricSnapshot>();
/// <summary>
/// 告警规则。
/// </summary>
public DbSet<MetricAlertRule> MetricAlertRules => Set<MetricAlertRule>();
/// <summary>
/// 配置实体映射关系。
/// </summary>
/// <param name="modelBuilder">模型构建器。</param>
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// 1. 调用基类配置
base.OnModelCreating(modelBuilder);
// 2. 配置全部实体映射
ConfigureTenant(modelBuilder.Entity<Tenant>());
ConfigureMerchant(modelBuilder.Entity<Merchant>());
ConfigureStore(modelBuilder.Entity<Store>());

View File

@@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence;
internal sealed class TakeoutAppDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<TakeoutAppDbContext>
{
/// <summary>
/// 初始化业务库设计时上下文工厂。
/// </summary>
public TakeoutAppDesignTimeDbContextFactory()
: base(DatabaseConstants.AppDataSource, "TAKEOUTSAAS_APP_CONNECTION")
{
}
// 创建设计时上下文
/// <summary>
/// 创建设计时的业务库 DbContext。
/// </summary>
/// <param name="options">上下文选项。</param>
/// <param name="tenantProvider">租户提供器。</param>
/// <param name="currentUserAccessor">当前用户访问器。</param>
/// <returns>业务库上下文实例。</returns>
protected override TakeoutAppDbContext CreateContext(
DbContextOptions<TakeoutAppDbContext> options,
ITenantProvider tenantProvider,

View File

@@ -16,6 +16,11 @@ internal abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDb
private readonly string _dataSourceName;
private readonly string? _connectionStringEnvVar;
/// <summary>
/// 初始化设计时工厂基类。
/// </summary>
/// <param name="dataSourceName">数据源名称。</param>
/// <param name="connectionStringEnvVar">连接串环境变量名。</param>
protected DesignTimeDbContextFactoryBase(string dataSourceName, string? connectionStringEnvVar = null)
{
if (string.IsNullOrWhiteSpace(dataSourceName))
@@ -27,8 +32,14 @@ internal abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDb
_connectionStringEnvVar = connectionStringEnvVar;
}
/// <summary>
/// 创建设计时 DbContext。
/// </summary>
/// <param name="args">命令行参数。</param>
/// <returns>DbContext 实例。</returns>
public TContext CreateDbContext(string[] args)
{
// 1. 构建 DbContextOptions
var optionsBuilder = new DbContextOptionsBuilder<TContext>();
optionsBuilder.UseNpgsql(
ResolveConnectionString(),
@@ -38,12 +49,20 @@ internal abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDb
npgsql.EnableRetryOnFailure();
});
// 2. 创建上下文
return CreateContext(
optionsBuilder.Options,
new DesignTimeTenantProvider(),
new DesignTimeCurrentUserAccessor());
}
/// <summary>
/// 由子类实现的上下文工厂方法。
/// </summary>
/// <param name="options">上下文选项。</param>
/// <param name="tenantProvider">租户提供器。</param>
/// <param name="currentUserAccessor">当前用户访问器。</param>
/// <returns>DbContext 实例。</returns>
protected abstract TContext CreateContext(
DbContextOptions<TContext> options,
ITenantProvider tenantProvider,
@@ -138,12 +157,22 @@ internal abstract class DesignTimeDbContextFactoryBase<TContext> : IDesignTimeDb
private sealed class DesignTimeTenantProvider : ITenantProvider
{
/// <summary>
/// 设计时返回默认租户 ID。
/// </summary>
/// <returns>默认租户 ID。</returns>
public long GetCurrentTenantId() => 0;
}
private sealed class DesignTimeCurrentUserAccessor : ICurrentUserAccessor
{
/// <summary>
/// 设计时用户标识。
/// </summary>
public long UserId => 0;
/// <summary>
/// 设计时用户鉴权标识。
/// </summary>
public bool IsAuthenticated => false;
}
}

View File

@@ -11,6 +11,10 @@ namespace TakeoutSaaS.Infrastructure.Common.Persistence;
/// </summary>
internal static class ModelBuilderCommentExtensions
{
/// <summary>
/// 将 XML 注释应用到实体与属性的 Comment。
/// </summary>
/// <param name="modelBuilder">模型构建器。</param>
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<Assembly, IReadOnlyDictionary<string, string>> Cache = new();
/// <summary>
/// 尝试获取成员的摘要注释。
/// </summary>
/// <param name="member">反射成员。</param>
/// <param name="summary">输出的摘要文本。</param>
/// <returns>存在摘要则返回 true。</returns>
public static bool TryGetSummary(MemberInfo member, out string? summary)
{
summary = null;

View File

@@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence;
internal sealed class DictionaryDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<DictionaryDbContext>
{
/// <summary>
/// 初始化字典库设计时上下文工厂。
/// </summary>
public DictionaryDesignTimeDbContextFactory()
: base(DatabaseConstants.DictionaryDataSource, "TAKEOUTSAAS_DICTIONARY_CONNECTION")
{
}
// 创建设计时上下文
/// <summary>
/// 创建设计时的 DictionaryDbContext。
/// </summary>
/// <param name="options">上下文配置。</param>
/// <param name="tenantProvider">租户提供器。</param>
/// <param name="currentUserAccessor">当前用户访问器。</param>
/// <returns>DictionaryDbContext 实例。</returns>
protected override DictionaryDbContext CreateContext(
DbContextOptions<DictionaryDbContext> options,
ITenantProvider tenantProvider,

View File

@@ -11,42 +11,92 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
/// </summary>
public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDictionaryRepository
{
/// <summary>
/// 根据分组 ID 查询分组。
/// </summary>
/// <param name="id">分组 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配分组或 null。</returns>
public Task<DictionaryGroup?> FindGroupByIdAsync(long id, CancellationToken cancellationToken = default)
=> context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken);
/// <summary>
/// 根据分组编码查询分组。
/// </summary>
/// <param name="code">分组编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配分组或 null。</returns>
public Task<DictionaryGroup?> FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default)
=> context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken);
/// <summary>
/// 搜索分组列表。
/// </summary>
/// <param name="scope">字典作用域。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>分组列表。</returns>
public async Task<IReadOnlyList<DictionaryGroup>> 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);
}
/// <summary>
/// 新增分组。
/// </summary>
/// <param name="group">分组实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
{
// 1. 添加分组
context.DictionaryGroups.Add(group);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 删除分组。
/// </summary>
/// <param name="group">分组实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
{
// 1. 移除分组
context.DictionaryGroups.Remove(group);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 根据条目 ID 查询字典项。
/// </summary>
/// <param name="id">条目 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配条目或 null。</returns>
public Task<DictionaryItem?> FindItemByIdAsync(long id, CancellationToken cancellationToken = default)
=> context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken);
/// <summary>
/// 获取指定分组下的条目列表。
/// </summary>
/// <param name="groupId">分组 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>条目列表。</returns>
public async Task<IReadOnlyList<DictionaryItem>> 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);
}
/// <summary>
/// 新增字典项。
/// </summary>
/// <param name="item">字典项。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
{
// 1. 添加条目
context.DictionaryItems.Add(item);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 删除字典项。
/// </summary>
/// <param name="item">字典项。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
{
// 1. 移除条目
context.DictionaryItems.Remove(item);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 持久化变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> context.SaveChangesAsync(cancellationToken);
/// <summary>
/// 根据编码集合获取条目列表,可包含系统级条目。
/// </summary>
/// <param name="codes">分组编码集合。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="includeSystem">是否包含系统级。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>条目列表。</returns>
public async Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> 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<DictionaryItem>();
}
// 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);

View File

@@ -15,8 +15,16 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions
private readonly DictionaryCacheOptions _options = options.Value;
private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web);
/// <summary>
/// 读取指定租户与编码的字典缓存。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="code">字典编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>字典项集合或 null。</returns>
public async Task<IReadOnlyList<DictionaryItemDto>?> 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<List<DictionaryItemDto>>(payload, _serializerOptions);
}
/// <summary>
/// 设置指定租户与编码的字典缓存。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="code">字典编码。</param>
/// <param name="items">字典项集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task SetAsync(long tenantId, string code, IReadOnlyList<DictionaryItemDto> 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);
}
/// <summary>
/// 移除指定租户与编码的缓存。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="code">字典编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default)
{
// 1. 删除缓存键
var cacheKey = BuildKey(tenantId, code);
return cache.RemoveAsync(cacheKey, cancellationToken);
}

View File

@@ -9,17 +9,41 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUserRepository
{
/// <summary>
/// 根据 OpenId 获取小程序用户。
/// </summary>
/// <param name="openId">微信 OpenId。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配的小程序用户或 null。</returns>
public Task<MiniUser?> FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default)
=> dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken);
/// <summary>
/// 根据用户 ID 获取小程序用户。
/// </summary>
/// <param name="id">用户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>匹配的小程序用户或 null。</returns>
public Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default)
=> dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken);
/// <summary>
/// 创建或更新小程序用户信息。
/// </summary>
/// <param name="openId">微信 OpenId。</param>
/// <param name="unionId">微信 UnionId。</param>
/// <param name="nickname">昵称。</param>
/// <param name="avatar">头像地址。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>创建或更新后的小程序用户。</returns>
public async Task<MiniUser> 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;
}

View File

@@ -9,66 +9,136 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository
{
/// <summary>
/// 根据权限 ID 获取权限。
/// </summary>
/// <param name="permissionId">权限 ID。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>权限实体或 null。</returns>
public Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
=> dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken);
/// <summary>
/// 根据权限编码获取权限。
/// </summary>
/// <param name="code">权限编码。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>权限实体或 null。</returns>
public Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
=> dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken);
/// <summary>
/// 根据权限编码集合批量获取权限。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="codes">权限编码集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>权限列表。</returns>
public Task<IReadOnlyList<Permission>> GetByCodesAsync(long tenantId, IEnumerable<string> 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<Permission>)t.Result, cancellationToken);
}
/// <summary>
/// 根据权限 ID 集合批量获取权限。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="permissionIds">权限 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>权限列表。</returns>
public Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
=> dbContext.Permissions.AsNoTracking()
.Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id))
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, cancellationToken);
/// <summary>
/// 按关键字搜索权限。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="keyword">搜索关键字。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>权限列表。</returns>
public Task<IReadOnlyList<Permission>> 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<Permission>)t.Result, cancellationToken);
}
/// <summary>
/// 新增权限。
/// </summary>
/// <param name="permission">权限实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddAsync(Permission permission, CancellationToken cancellationToken = default)
{
// 1. 添加实体
dbContext.Permissions.Add(permission);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 更新权限。
/// </summary>
/// <param name="permission">权限实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default)
{
// 1. 标记实体更新
dbContext.Permissions.Update(permission);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 删除指定权限。
/// </summary>
/// <param name="permissionId">权限 ID。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
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);
}
}
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken);
}

View File

@@ -9,25 +9,49 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IRolePermissionRepository
{
/// <summary>
/// 根据角色 ID 集合获取角色权限映射。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="roleIds">角色 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色权限映射列表。</returns>
public Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
=> dbContext.RolePermissions.AsNoTracking()
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId))
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RolePermission>)t.Result, cancellationToken);
/// <summary>
/// 批量新增角色权限。
/// </summary>
/// <param name="rolePermissions">角色权限集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task AddRangeAsync(IEnumerable<RolePermission> rolePermissions, CancellationToken cancellationToken = default)
{
// 1. 转为数组便于计数
var toAdd = rolePermissions as RolePermission[] ?? rolePermissions.ToArray();
if (toAdd.Length == 0)
{
return;
}
// 2. 批量插入
await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken);
}
/// <summary>
/// 替换指定角色的权限集合。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="roleId">角色 ID。</param>
/// <param name="permissionIds">权限 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable<long> 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
});
}
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken);
}

View File

@@ -9,53 +9,114 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleRepository
{
/// <summary>
/// 根据角色 ID 获取角色。
/// </summary>
/// <param name="roleId">角色 ID。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色实体或 null。</returns>
public Task<Role?> FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default)
=> dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
/// <summary>
/// 根据角色编码获取角色。
/// </summary>
/// <param name="code">角色编码。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色实体或 null。</returns>
public Task<Role?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
=> dbContext.Roles.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken);
/// <summary>
/// 根据角色 ID 集合获取角色列表。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="roleIds">角色 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色列表。</returns>
public Task<IReadOnlyList<Role>> GetByIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
=> dbContext.Roles.AsNoTracking()
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null)
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Role>)t.Result, cancellationToken);
/// <summary>
/// 按关键字搜索角色。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="keyword">搜索关键字。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色列表。</returns>
public Task<IReadOnlyList<Role>> 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<Role>)t.Result, cancellationToken);
}
/// <summary>
/// 新增角色。
/// </summary>
/// <param name="role">角色实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddAsync(Role role, CancellationToken cancellationToken = default)
{
// 1. 添加实体
dbContext.Roles.Add(role);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 更新角色。
/// </summary>
/// <param name="role">角色实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task UpdateAsync(Role role, CancellationToken cancellationToken = default)
{
// 1. 标记更新
dbContext.Roles.Update(role);
// 2. 返回完成任务
return Task.CompletedTask;
}
/// <summary>
/// 软删除角色。
/// </summary>
/// <param name="roleId">角色 ID。</param>
/// <param name="tenantId">租户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
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);
}
}
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken);
}

View File

@@ -9,83 +9,151 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class EfRoleTemplateRepository(IdentityDbContext dbContext) : IRoleTemplateRepository
{
/// <summary>
/// 获取全部角色模板,可选按启用状态过滤。
/// </summary>
/// <param name="isActive">是否启用过滤。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色模板列表。</returns>
public Task<IReadOnlyList<RoleTemplate>> 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<RoleTemplate>)t.Result, cancellationToken);
}
/// <summary>
/// 根据模板编码获取角色模板。
/// </summary>
/// <param name="templateCode">模板编码。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>角色模板或 null。</returns>
public Task<RoleTemplate?> FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default)
{
// 1. 规范化编码
var normalized = templateCode.Trim();
// 2. 查询模板
return dbContext.RoleTemplates.AsNoTracking().FirstOrDefaultAsync(x => x.TemplateCode == normalized, cancellationToken);
}
/// <summary>
/// 获取指定模板的权限集合。
/// </summary>
/// <param name="roleTemplateId">模板 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>模板权限列表。</returns>
public Task<IReadOnlyList<RoleTemplatePermission>> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default)
{
// 1. 查询模板权限
return dbContext.RoleTemplatePermissions.AsNoTracking()
.Where(x => x.RoleTemplateId == roleTemplateId)
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RoleTemplatePermission>)t.Result, cancellationToken);
}
/// <summary>
/// 获取多个模板的权限集合。
/// </summary>
/// <param name="roleTemplateIds">模板 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>模板到权限的字典。</returns>
public async Task<IDictionary<long, IReadOnlyList<RoleTemplatePermission>>> GetPermissionsAsync(IEnumerable<long> roleTemplateIds, CancellationToken cancellationToken = default)
{
// 1. 去重 ID
var ids = roleTemplateIds.Distinct().ToArray();
if (ids.Length == 0)
{
return new Dictionary<long, IReadOnlyList<RoleTemplatePermission>>();
}
// 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<RoleTemplatePermission>)g.ToList());
}
/// <summary>
/// 新增角色模板并配置权限。
/// </summary>
/// <param name="template">角色模板实体。</param>
/// <param name="permissionCodes">权限编码集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task AddAsync(RoleTemplate template, IEnumerable<string> 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);
}
/// <summary>
/// 更新角色模板并重置权限。
/// </summary>
/// <param name="template">角色模板实体。</param>
/// <param name="permissionCodes">权限编码集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task UpdateAsync(RoleTemplate template, IEnumerable<string> 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);
}
/// <summary>
/// 删除角色模板及其权限。
/// </summary>
/// <param name="roleTemplateId">模板 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
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);
}
}
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken);
private async Task ReplacePermissionsInternalAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken)
{
// 1. 使用执行策略保证一致性
var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () =>
{

View File

@@ -9,32 +9,58 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRoleRepository
{
/// <summary>
/// 根据用户 ID 集合获取用户角色映射。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="userIds">用户 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>用户角色映射列表。</returns>
public Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
=> dbContext.UserRoles.AsNoTracking()
.Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId))
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, cancellationToken);
/// <summary>
/// 获取指定用户的角色集合。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="userId">用户 ID。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>用户角色列表。</returns>
public Task<IReadOnlyList<UserRole>> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default)
=> dbContext.UserRoles.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.UserId == userId)
.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, cancellationToken);
/// <summary>
/// 替换指定用户的角色集合。
/// </summary>
/// <param name="tenantId">租户 ID。</param>
/// <param name="userId">用户 ID。</param>
/// <param name="roleIds">角色 ID 集合。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable<long> 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
});
}
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken);
}

View File

@@ -22,38 +22,52 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary>
public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger) : IHostedService
{
/// <summary>
/// 执行后台账号与权限种子。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task StartAsync(CancellationToken cancellationToken)
{
// 1. 创建作用域并解析依赖
using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<AdminSeedOptions>>().Value;
var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher<DomainIdentityUser>>();
var tenantContextAccessor = scope.ServiceProvider.GetRequiredService<ITenantContextAccessor>();
// 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);
}
/// <summary>
/// 停止生命周期时的清理(此处无需处理)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>已完成任务。</returns>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static async Task SeedRoleTemplatesAsync(
@@ -219,23 +242,28 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
IList<RoleTemplateSeedOptions> 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)
{

View File

@@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
internal sealed class IdentityDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<IdentityDbContext>
{
/// <summary>
/// 初始化 Identity 设计时上下文工厂。
/// </summary>
public IdentityDesignTimeDbContextFactory()
: base(DatabaseConstants.IdentityDataSource, "TAKEOUTSAAS_IDENTITY_CONNECTION")
{
}
// 创建设计时上下文实例
/// <summary>
/// 创建设计时的 IdentityDbContext。
/// </summary>
/// <param name="options">DbContext 配置。</param>
/// <param name="tenantProvider">租户提供器。</param>
/// <param name="currentUserAccessor">当前用户访问器。</param>
/// <returns>IdentityDbContext 实例。</returns>
protected override IdentityDbContext CreateContext(
DbContextOptions<IdentityDbContext> options,
ITenantProvider tenantProvider,

View File

@@ -14,8 +14,15 @@ public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions<Logi
{
private readonly LoginRateLimitOptions _options = options.Value;
/// <summary>
/// 校验指定键的登录尝试次数,超限将抛出业务异常。
/// </summary>
/// <param name="key">限流键(如账号或 IP。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
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<Logi
throw new BusinessException(ErrorCodes.Forbidden, "尝试次数过多,请稍后再试");
}
// 2. 累加计数并回写缓存
count++;
await cache.SetStringAsync(
cacheKey,
@@ -35,6 +43,12 @@ public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions<Logi
cancellationToken);
}
/// <summary>
/// 重置指定键的登录计数。
/// </summary>
/// <param name="key">限流键(如账号或 IP。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task ResetAsync(string key, CancellationToken cancellationToken = default)
=> cache.RemoveAsync(BuildKey(key), cancellationToken);

View File

@@ -16,11 +16,20 @@ public sealed class RedisRefreshTokenStore(IDistributedCache cache, IOptions<Ref
private static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly RefreshTokenStoreOptions _options = options.Value;
/// <summary>
/// 签发刷新令牌并写入缓存。
/// </summary>
/// <param name="userId">用户 ID。</param>
/// <param name="expiresAt">过期时间。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>刷新令牌描述。</returns>
public async Task<RefreshTokenDescriptor> 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<Ref
return descriptor;
}
/// <summary>
/// 获取刷新令牌描述。
/// </summary>
/// <param name="refreshToken">刷新令牌值。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>刷新令牌描述或 null。</returns>
public async Task<RefreshTokenDescriptor?> GetAsync(string refreshToken, CancellationToken cancellationToken = default)
{
// 1. 读取缓存
var json = await cache.GetStringAsync(BuildKey(refreshToken), cancellationToken);
return string.IsNullOrWhiteSpace(json)
? null
: JsonSerializer.Deserialize<RefreshTokenDescriptor>(json, JsonOptions);
}
/// <summary>
/// 吊销刷新令牌。
/// </summary>
/// <param name="refreshToken">刷新令牌值。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
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);

View File

@@ -15,18 +15,27 @@ public sealed class WeChatAuthService(HttpClient httpClient, IOptions<WeChatMini
{
private readonly WeChatMiniOptions _options = options.Value;
/// <summary>
/// 调用微信接口完成 code2Session。
/// </summary>
/// <param name="code">临时登录凭证 code。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>微信会话信息。</returns>
public async Task<WeChatSessionInfo> 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<WeChatSessionResponse>(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<WeChatMini
throw new BusinessException(ErrorCodes.Unauthorized, message);
}
// 4. 校验必要字段
if (string.IsNullOrWhiteSpace(payload.OpenId) || string.IsNullOrWhiteSpace(payload.SessionKey))
{
throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:返回数据无效");
}
// 5. 组装会话信息
return new WeChatSessionInfo
{
OpenId = payload.OpenId,

View File

@@ -1,34 +1,40 @@
using Microsoft.AspNetCore.Authorization;
using TakeoutSaaS.Module.Authorization.Policies;
namespace TakeoutSaaS.Module.Authorization.Attributes;
/// <summary>
/// 权限校验特性
/// 权限校验特性
/// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class PermissionAuthorizeAttribute : AuthorizeAttribute
{
/// <summary>
/// 初始化权限校验特性并构建对应策略。
/// </summary>
/// <param name="permissions">所需的权限标识集合。</param>
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);
}
/// <summary>
/// 所需权限集合
/// 所需权限集合
/// </summary>
public IReadOnlyCollection<string> Permissions { get; }
}

View File

@@ -1,38 +1,45 @@
using Microsoft.AspNetCore.Authorization;
namespace TakeoutSaaS.Module.Authorization.Policies;
/// <summary>
/// 权限校验处理器
/// 权限校验处理器
/// </summary>
public sealed class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{
/// <summary>
/// 用户声明中权限的声明类型键。
/// </summary>
public const string PermissionClaimType = "permission";
/// <summary>
/// 校验当前用户是否具备要求的权限集合。
/// </summary>
/// <param name="context">授权上下文。</param>
/// <param name="requirement">权限需求描述。</param>
/// <returns>异步完成任务。</returns>
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;
}
}

View File

@@ -1,55 +1,62 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options;
namespace TakeoutSaaS.Module.Authorization.Policies;
/// <summary>
/// 权限策略提供者(按需动态构建策略)
/// 权限策略提供者(按需动态构建策略)
/// </summary>
public sealed class PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : DefaultAuthorizationPolicyProvider(options)
{
/// <summary>
/// 权限策略名称前缀。
/// </summary>
public const string PolicyPrefix = "PERMISSION:";
private readonly AuthorizationOptions _options = options.Value;
/// <summary>
/// 获取或构建指定名称的权限策略。
/// </summary>
/// <param name="policyName">策略名称。</param>
/// <returns>匹配的授权策略。</returns>
public override Task<AuthorizationPolicy?> 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<AuthorizationPolicy?>(existingPolicy);
}
var permissions = ParsePermissions(policyName);
if (permissions.Length == 0)
{
return Task.FromResult<AuthorizationPolicy?>(null);
}
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new PermissionRequirement(permissions))
.Build();
_options.AddPolicy(policyName, policy);
return Task.FromResult<AuthorizationPolicy?>(policy);
return base.GetPolicyAsync(policyName);
}
return base.GetPolicyAsync(policyName);
// 2. 复用已存在的策略
var existingPolicy = _options.GetPolicy(policyName);
if (existingPolicy != null)
{
return Task.FromResult<AuthorizationPolicy?>(existingPolicy);
}
// 3. 解析策略携带的权限列表
var permissions = ParsePermissions(policyName);
if (permissions.Length == 0)
{
return Task.FromResult<AuthorizationPolicy?>(null);
}
// 4. 动态构建策略并缓存
var policy = new AuthorizationPolicyBuilder()
.AddRequirements(new PermissionRequirement(permissions))
.Build();
_options.AddPolicy(policyName, policy);
// 5. 返回构建好的策略
return Task.FromResult<AuthorizationPolicy?>(policy);
}
/// <summary>
/// 根据权限集合构建策略名称
/// 根据权限集合构建策略名称
/// </summary>
/// <param name="permissions">权限标识集合。</param>
/// <returns>策略名称。</returns>
public static string BuildPolicyName(IEnumerable<string> 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<string> permissions)
=> [.. permissions
.Where(p => !string.IsNullOrWhiteSpace(p))

View File

@@ -1,7 +1,5 @@
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Module.Tenancy;
/// <summary>
/// 基于 <see cref="AsyncLocal{T}"/> 的租户上下文访问器,实现请求级别隔离。
/// </summary>
@@ -9,7 +7,10 @@ public sealed class TenantContextAccessor : ITenantContextAccessor
{
private static readonly AsyncLocal<TenantContextHolder?> Holder = new();
/// <inheritdoc />
// 当前请求的租户上下文访问入口
/// <summary>
/// 获取或设置当前请求的租户上下文。
/// </summary>
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; }