docs: add xml comments and update ignore rules
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -6,3 +6,6 @@ obj/
|
||||
.claude/
|
||||
*.log
|
||||
/src/Api/TakeoutSaaS.AdminApi/TakeoutSaaS.AdminApi.csproj.user
|
||||
|
||||
# 保留根目录 scripts 目录提交
|
||||
!scripts/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 () =>
|
||||
{
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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; }
|
||||
|
||||
Reference in New Issue
Block a user