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

View File

@@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.App.Persistence;
internal sealed class TakeoutAppDesignTimeDbContextFactory internal sealed class TakeoutAppDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<TakeoutAppDbContext> : DesignTimeDbContextFactoryBase<TakeoutAppDbContext>
{ {
/// <summary>
/// 初始化业务库设计时上下文工厂。
/// </summary>
public TakeoutAppDesignTimeDbContextFactory() public TakeoutAppDesignTimeDbContextFactory()
: base(DatabaseConstants.AppDataSource, "TAKEOUTSAAS_APP_CONNECTION") : 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( protected override TakeoutAppDbContext CreateContext(
DbContextOptions<TakeoutAppDbContext> options, DbContextOptions<TakeoutAppDbContext> options,
ITenantProvider tenantProvider, ITenantProvider tenantProvider,

View File

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

View File

@@ -11,6 +11,10 @@ namespace TakeoutSaaS.Infrastructure.Common.Persistence;
/// </summary> /// </summary>
internal static class ModelBuilderCommentExtensions internal static class ModelBuilderCommentExtensions
{ {
/// <summary>
/// 将 XML 注释应用到实体与属性的 Comment。
/// </summary>
/// <param name="modelBuilder">模型构建器。</param>
public static void ApplyXmlComments(this ModelBuilder modelBuilder) public static void ApplyXmlComments(this ModelBuilder modelBuilder)
{ {
foreach (var entityType in modelBuilder.Model.GetEntityTypes()) 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(); 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) public static bool TryGetSummary(MemberInfo member, out string? summary)
{ {
summary = null; summary = null;

View File

@@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence;
internal sealed class DictionaryDesignTimeDbContextFactory internal sealed class DictionaryDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<DictionaryDbContext> : DesignTimeDbContextFactoryBase<DictionaryDbContext>
{ {
/// <summary>
/// 初始化字典库设计时上下文工厂。
/// </summary>
public DictionaryDesignTimeDbContextFactory() public DictionaryDesignTimeDbContextFactory()
: base(DatabaseConstants.DictionaryDataSource, "TAKEOUTSAAS_DICTIONARY_CONNECTION") : 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( protected override DictionaryDbContext CreateContext(
DbContextOptions<DictionaryDbContext> options, DbContextOptions<DictionaryDbContext> options,
ITenantProvider tenantProvider, ITenantProvider tenantProvider,

View File

@@ -11,42 +11,92 @@ namespace TakeoutSaaS.Infrastructure.Dictionary.Repositories;
/// </summary> /// </summary>
public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDictionaryRepository 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) public Task<DictionaryGroup?> FindGroupByIdAsync(long id, CancellationToken cancellationToken = default)
=> context.DictionaryGroups.FirstOrDefaultAsync(group => group.Id == id, cancellationToken); => 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) public Task<DictionaryGroup?> FindGroupByCodeAsync(string code, CancellationToken cancellationToken = default)
=> context.DictionaryGroups.FirstOrDefaultAsync(group => group.Code == code, cancellationToken); => 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) public async Task<IReadOnlyList<DictionaryGroup>> SearchGroupsAsync(DictionaryScope? scope, CancellationToken cancellationToken = default)
{ {
// 1. 构建分组查询
var query = context.DictionaryGroups.AsNoTracking(); var query = context.DictionaryGroups.AsNoTracking();
if (scope.HasValue) if (scope.HasValue)
{ {
// 2. 按作用域过滤
query = query.Where(group => group.Scope == scope.Value); query = query.Where(group => group.Scope == scope.Value);
} }
// 3. 排序返回
return await query return await query
.OrderBy(group => group.Code) .OrderBy(group => group.Code)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
/// <summary>
/// 新增分组。
/// </summary>
/// <param name="group">分组实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) public Task AddGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
{ {
// 1. 添加分组
context.DictionaryGroups.Add(group); context.DictionaryGroups.Add(group);
// 2. 返回完成任务
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// 删除分组。
/// </summary>
/// <param name="group">分组实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default) public Task RemoveGroupAsync(DictionaryGroup group, CancellationToken cancellationToken = default)
{ {
// 1. 移除分组
context.DictionaryGroups.Remove(group); context.DictionaryGroups.Remove(group);
// 2. 返回完成任务
return Task.CompletedTask; 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) public Task<DictionaryItem?> FindItemByIdAsync(long id, CancellationToken cancellationToken = default)
=> context.DictionaryItems.FirstOrDefaultAsync(item => item.Id == id, cancellationToken); => 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) public async Task<IReadOnlyList<DictionaryItem>> GetItemsByGroupIdAsync(long groupId, CancellationToken cancellationToken = default)
{ {
// 1. 过滤分组
return await context.DictionaryItems return await context.DictionaryItems
.AsNoTracking() .AsNoTracking()
.Where(item => item.GroupId == groupId) .Where(item => item.GroupId == groupId)
@@ -54,23 +104,53 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
} }
/// <summary>
/// 新增字典项。
/// </summary>
/// <param name="item">字典项。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) public Task AddItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
{ {
// 1. 添加条目
context.DictionaryItems.Add(item); context.DictionaryItems.Add(item);
// 2. 返回完成任务
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// 删除字典项。
/// </summary>
/// <param name="item">字典项。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default) public Task RemoveItemAsync(DictionaryItem item, CancellationToken cancellationToken = default)
{ {
// 1. 移除条目
context.DictionaryItems.Remove(item); context.DictionaryItems.Remove(item);
// 2. 返回完成任务
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// 持久化变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> context.SaveChangesAsync(cancellationToken); => 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) public async Task<IReadOnlyList<DictionaryItem>> GetItemsByCodesAsync(IEnumerable<string> codes, long tenantId, bool includeSystem, CancellationToken cancellationToken = default)
{ {
// 1. 规范化编码
var normalizedCodes = codes var normalizedCodes = codes
.Where(code => !string.IsNullOrWhiteSpace(code)) .Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code.Trim().ToLowerInvariant()) .Select(code => code.Trim().ToLowerInvariant())
@@ -82,14 +162,17 @@ public sealed class EfDictionaryRepository(DictionaryDbContext context) : IDicti
return Array.Empty<DictionaryItem>(); return Array.Empty<DictionaryItem>();
} }
// 2. 构建查询并忽略 QueryFilter
var query = context.DictionaryItems var query = context.DictionaryItems
.AsNoTracking() .AsNoTracking()
.IgnoreQueryFilters() .IgnoreQueryFilters()
.Include(item => item.Group) .Include(item => item.Group)
.Where(item => normalizedCodes.Contains(item.Group!.Code)); .Where(item => normalizedCodes.Contains(item.Group!.Code));
// 3. 按租户或系统级过滤
query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == 0)); query = query.Where(item => item.TenantId == tenantId || (includeSystem && item.TenantId == 0));
// 4. 排序返回
return await query return await query
.OrderBy(item => item.SortOrder) .OrderBy(item => item.SortOrder)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);

View File

@@ -15,8 +15,16 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions
private readonly DictionaryCacheOptions _options = options.Value; private readonly DictionaryCacheOptions _options = options.Value;
private readonly JsonSerializerOptions _serializerOptions = new(JsonSerializerDefaults.Web); 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) public async Task<IReadOnlyList<DictionaryItemDto>?> GetAsync(long tenantId, string code, CancellationToken cancellationToken = default)
{ {
// 1. 拼装缓存键
var cacheKey = BuildKey(tenantId, code); var cacheKey = BuildKey(tenantId, code);
var payload = await cache.GetAsync(cacheKey, cancellationToken); var payload = await cache.GetAsync(cacheKey, cancellationToken);
if (payload == null || payload.Length == 0) if (payload == null || payload.Length == 0)
@@ -24,11 +32,21 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions
return null; return null;
} }
// 2. 反序列化
return JsonSerializer.Deserialize<List<DictionaryItemDto>>(payload, _serializerOptions); 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) public Task SetAsync(long tenantId, string code, IReadOnlyList<DictionaryItemDto> items, CancellationToken cancellationToken = default)
{ {
// 1. 序列化并写入缓存
var cacheKey = BuildKey(tenantId, code); var cacheKey = BuildKey(tenantId, code);
var payload = JsonSerializer.SerializeToUtf8Bytes(items, _serializerOptions); var payload = JsonSerializer.SerializeToUtf8Bytes(items, _serializerOptions);
var options = new DistributedCacheEntryOptions var options = new DistributedCacheEntryOptions
@@ -38,8 +56,16 @@ public sealed class DistributedDictionaryCache(IDistributedCache cache, IOptions
return cache.SetAsync(cacheKey, payload, options, cancellationToken); 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) public Task RemoveAsync(long tenantId, string code, CancellationToken cancellationToken = default)
{ {
// 1. 删除缓存键
var cacheKey = BuildKey(tenantId, code); var cacheKey = BuildKey(tenantId, code);
return cache.RemoveAsync(cacheKey, cancellationToken); return cache.RemoveAsync(cacheKey, cancellationToken);
} }

View File

@@ -9,17 +9,41 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUserRepository 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) public Task<MiniUser?> FindByOpenIdAsync(string openId, CancellationToken cancellationToken = default)
=> dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken); => 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) public Task<MiniUser?> FindByIdAsync(long id, CancellationToken cancellationToken = default)
=> dbContext.MiniUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id, cancellationToken); => 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) 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); var user = await dbContext.MiniUsers.FirstOrDefaultAsync(x => x.OpenId == openId, cancellationToken);
if (user == null) if (user == null)
{ {
// 2. 未找到则创建
user = new MiniUser user = new MiniUser
{ {
Id = 0, Id = 0,
@@ -33,11 +57,13 @@ public sealed class EfMiniUserRepository(IdentityDbContext dbContext) : IMiniUse
} }
else else
{ {
// 3. 已存在则更新可变字段
user.UnionId = unionId ?? user.UnionId; user.UnionId = unionId ?? user.UnionId;
user.Nickname = nickname ?? user.Nickname; user.Nickname = nickname ?? user.Nickname;
user.Avatar = avatar ?? user.Avatar; user.Avatar = avatar ?? user.Avatar;
} }
// 4. 保存更改
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
return user; return user;
} }

View File

@@ -9,66 +9,136 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermissionRepository 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) public Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default)
=> dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken); => 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) public Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default)
=> dbContext.Permissions.AsNoTracking().FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId, cancellationToken); => 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) public Task<IReadOnlyList<Permission>> GetByCodesAsync(long tenantId, IEnumerable<string> codes, CancellationToken cancellationToken = default)
{ {
// 1. 规范化编码集合
var normalizedCodes = codes var normalizedCodes = codes
.Where(code => !string.IsNullOrWhiteSpace(code)) .Where(code => !string.IsNullOrWhiteSpace(code))
.Select(code => code.Trim()) .Select(code => code.Trim())
.Distinct() .Distinct()
.ToArray(); .ToArray();
// 2. 按租户筛选权限
return dbContext.Permissions.AsNoTracking() return dbContext.Permissions.AsNoTracking()
.Where(x => x.TenantId == tenantId && normalizedCodes.Contains(x.Code)) .Where(x => x.TenantId == tenantId && normalizedCodes.Contains(x.Code))
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, 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) public Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
=> dbContext.Permissions.AsNoTracking() => dbContext.Permissions.AsNoTracking()
.Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id)) .Where(x => x.TenantId == tenantId && permissionIds.Contains(x.Id))
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, 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) public Task<IReadOnlyList<Permission>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default)
{ {
// 1. 构建基础查询
var query = dbContext.Permissions.AsNoTracking().Where(x => x.TenantId == tenantId); var query = dbContext.Permissions.AsNoTracking().Where(x => x.TenantId == tenantId);
if (!string.IsNullOrWhiteSpace(keyword)) if (!string.IsNullOrWhiteSpace(keyword))
{ {
// 2. 追加关键字过滤
var normalized = keyword.Trim(); var normalized = keyword.Trim();
query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized));
} }
// 3. 返回列表
return query.ToListAsync(cancellationToken) return query.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Permission>)t.Result, 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) public Task AddAsync(Permission permission, CancellationToken cancellationToken = default)
{ {
// 1. 添加实体
dbContext.Permissions.Add(permission); dbContext.Permissions.Add(permission);
// 2. 返回完成任务
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// 更新权限。
/// </summary>
/// <param name="permission">权限实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default) public Task UpdateAsync(Permission permission, CancellationToken cancellationToken = default)
{ {
// 1. 标记实体更新
dbContext.Permissions.Update(permission); dbContext.Permissions.Update(permission);
// 2. 返回完成任务
return Task.CompletedTask; 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) 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); var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId && x.TenantId == tenantId, cancellationToken);
if (entity != null) if (entity != null)
{ {
// 2. 删除实体
dbContext.Permissions.Remove(entity); dbContext.Permissions.Remove(entity);
} }
} }
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken); => dbContext.SaveChangesAsync(cancellationToken);
} }

View File

@@ -9,25 +9,49 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IRolePermissionRepository 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) public Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
=> dbContext.RolePermissions.AsNoTracking() => dbContext.RolePermissions.AsNoTracking()
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId)) .Where(x => x.TenantId == tenantId && roleIds.Contains(x.RoleId))
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RolePermission>)t.Result, 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) public async Task AddRangeAsync(IEnumerable<RolePermission> rolePermissions, CancellationToken cancellationToken = default)
{ {
// 1. 转为数组便于计数
var toAdd = rolePermissions as RolePermission[] ?? rolePermissions.ToArray(); var toAdd = rolePermissions as RolePermission[] ?? rolePermissions.ToArray();
if (toAdd.Length == 0) if (toAdd.Length == 0)
{ {
return; return;
} }
// 2. 批量插入
await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken); 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) public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default)
{ {
// 1. 使用执行策略保证可靠性
var strategy = dbContext.Database.CreateExecutionStrategy(); var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () => 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) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken); => dbContext.SaveChangesAsync(cancellationToken);
} }

View File

@@ -9,53 +9,114 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleRepository 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) 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); => 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) 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); => 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) public Task<IReadOnlyList<Role>> GetByIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
=> dbContext.Roles.AsNoTracking() => dbContext.Roles.AsNoTracking()
.Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null) .Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Role>)t.Result, 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) 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); var query = dbContext.Roles.AsNoTracking().Where(x => x.TenantId == tenantId && x.DeletedAt == null);
if (!string.IsNullOrWhiteSpace(keyword)) if (!string.IsNullOrWhiteSpace(keyword))
{ {
// 2. 追加关键字过滤
var normalized = keyword.Trim(); var normalized = keyword.Trim();
query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized)); query = query.Where(x => x.Name.Contains(normalized) || x.Code.Contains(normalized));
} }
// 3. 返回列表
return query.ToListAsync(cancellationToken) return query.ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<Role>)t.Result, 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) public Task AddAsync(Role role, CancellationToken cancellationToken = default)
{ {
// 1. 添加实体
dbContext.Roles.Add(role); dbContext.Roles.Add(role);
// 2. 返回完成任务
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// 更新角色。
/// </summary>
/// <param name="role">角色实体。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task UpdateAsync(Role role, CancellationToken cancellationToken = default) public Task UpdateAsync(Role role, CancellationToken cancellationToken = default)
{ {
// 1. 标记更新
dbContext.Roles.Update(role); dbContext.Roles.Update(role);
// 2. 返回完成任务
return Task.CompletedTask; 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) 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); var entity = await dbContext.Roles.FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId, cancellationToken);
if (entity != null) if (entity != null)
{ {
// 2. 标记删除时间
entity.DeletedAt = DateTime.UtcNow; entity.DeletedAt = DateTime.UtcNow;
dbContext.Roles.Update(entity); dbContext.Roles.Update(entity);
} }
} }
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken); => dbContext.SaveChangesAsync(cancellationToken);
} }

View File

@@ -9,83 +9,151 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class EfRoleTemplateRepository(IdentityDbContext dbContext) : IRoleTemplateRepository 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) public Task<IReadOnlyList<RoleTemplate>> GetAllAsync(bool? isActive, CancellationToken cancellationToken = default)
{ {
// 1. 构建基础查询
var query = dbContext.RoleTemplates.AsNoTracking(); var query = dbContext.RoleTemplates.AsNoTracking();
if (isActive.HasValue) if (isActive.HasValue)
{ {
// 2. 按启用状态过滤
query = query.Where(x => x.IsActive == isActive.Value); query = query.Where(x => x.IsActive == isActive.Value);
} }
// 3. 排序并返回
return query return query
.OrderBy(x => x.TemplateCode) .OrderBy(x => x.TemplateCode)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RoleTemplate>)t.Result, 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) public Task<RoleTemplate?> FindByCodeAsync(string templateCode, CancellationToken cancellationToken = default)
{ {
// 1. 规范化编码
var normalized = templateCode.Trim(); var normalized = templateCode.Trim();
// 2. 查询模板
return dbContext.RoleTemplates.AsNoTracking().FirstOrDefaultAsync(x => x.TemplateCode == normalized, cancellationToken); 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) public Task<IReadOnlyList<RoleTemplatePermission>> GetPermissionsAsync(long roleTemplateId, CancellationToken cancellationToken = default)
{ {
// 1. 查询模板权限
return dbContext.RoleTemplatePermissions.AsNoTracking() return dbContext.RoleTemplatePermissions.AsNoTracking()
.Where(x => x.RoleTemplateId == roleTemplateId) .Where(x => x.RoleTemplateId == roleTemplateId)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<RoleTemplatePermission>)t.Result, 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) public async Task<IDictionary<long, IReadOnlyList<RoleTemplatePermission>>> GetPermissionsAsync(IEnumerable<long> roleTemplateIds, CancellationToken cancellationToken = default)
{ {
// 1. 去重 ID
var ids = roleTemplateIds.Distinct().ToArray(); var ids = roleTemplateIds.Distinct().ToArray();
if (ids.Length == 0) if (ids.Length == 0)
{ {
return new Dictionary<long, IReadOnlyList<RoleTemplatePermission>>(); return new Dictionary<long, IReadOnlyList<RoleTemplatePermission>>();
} }
// 2. 批量查询权限
var permissions = await dbContext.RoleTemplatePermissions.AsNoTracking() var permissions = await dbContext.RoleTemplatePermissions.AsNoTracking()
.Where(x => ids.Contains(x.RoleTemplateId)) .Where(x => ids.Contains(x.RoleTemplateId))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// 3. 组装字典
return permissions return permissions
.GroupBy(x => x.RoleTemplateId) .GroupBy(x => x.RoleTemplateId)
.ToDictionary(g => g.Key, g => (IReadOnlyList<RoleTemplatePermission>)g.ToList()); .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) public async Task AddAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default)
{ {
// 1. 规范化模板字段
template.TemplateCode = template.TemplateCode.Trim(); template.TemplateCode = template.TemplateCode.Trim();
template.Name = template.Name.Trim(); template.Name = template.Name.Trim();
// 2. 保存模板
await dbContext.RoleTemplates.AddAsync(template, cancellationToken); await dbContext.RoleTemplates.AddAsync(template, cancellationToken);
// 3. 替换权限
await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken); 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) public async Task UpdateAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken = default)
{ {
// 1. 规范化模板字段
template.TemplateCode = template.TemplateCode.Trim(); template.TemplateCode = template.TemplateCode.Trim();
template.Name = template.Name.Trim(); template.Name = template.Name.Trim();
// 2. 更新模板
dbContext.RoleTemplates.Update(template); dbContext.RoleTemplates.Update(template);
// 3. 重置权限
await ReplacePermissionsInternalAsync(template, permissionCodes, cancellationToken); 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) public async Task DeleteAsync(long roleTemplateId, CancellationToken cancellationToken = default)
{ {
// 1. 查询模板
var entity = await dbContext.RoleTemplates.FirstOrDefaultAsync(x => x.Id == roleTemplateId, cancellationToken); var entity = await dbContext.RoleTemplates.FirstOrDefaultAsync(x => x.Id == roleTemplateId, cancellationToken);
if (entity != null) if (entity != null)
{ {
// 2. 删除关联权限
var permissions = dbContext.RoleTemplatePermissions.Where(x => x.RoleTemplateId == roleTemplateId); var permissions = dbContext.RoleTemplatePermissions.Where(x => x.RoleTemplateId == roleTemplateId);
dbContext.RoleTemplatePermissions.RemoveRange(permissions); dbContext.RoleTemplatePermissions.RemoveRange(permissions);
// 3. 删除模板
dbContext.RoleTemplates.Remove(entity); dbContext.RoleTemplates.Remove(entity);
} }
} }
/// <summary>
/// 保存仓储变更。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>保存任务。</returns>
public Task SaveChangesAsync(CancellationToken cancellationToken = default) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken); => dbContext.SaveChangesAsync(cancellationToken);
private async Task ReplacePermissionsInternalAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken) private async Task ReplacePermissionsInternalAsync(RoleTemplate template, IEnumerable<string> permissionCodes, CancellationToken cancellationToken)
{ {
// 1. 使用执行策略保证一致性
var strategy = dbContext.Database.CreateExecutionStrategy(); var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () => await strategy.ExecuteAsync(async () =>
{ {

View File

@@ -9,32 +9,58 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRoleRepository 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) public Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default)
=> dbContext.UserRoles.AsNoTracking() => dbContext.UserRoles.AsNoTracking()
.Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId)) .Where(x => x.TenantId == tenantId && userIds.Contains(x.UserId))
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, 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) public Task<IReadOnlyList<UserRole>> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default)
=> dbContext.UserRoles.AsNoTracking() => dbContext.UserRoles.AsNoTracking()
.Where(x => x.TenantId == tenantId && x.UserId == userId) .Where(x => x.TenantId == tenantId && x.UserId == userId)
.ToListAsync(cancellationToken) .ToListAsync(cancellationToken)
.ContinueWith(t => (IReadOnlyList<UserRole>)t.Result, 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) public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default)
{ {
// 1. 使用执行策略保障一致性
var strategy = dbContext.Database.CreateExecutionStrategy(); var strategy = dbContext.Database.CreateExecutionStrategy();
await strategy.ExecuteAsync(async () => await strategy.ExecuteAsync(async () =>
{ {
await using var trx = await dbContext.Database.BeginTransactionAsync(cancellationToken); await using var trx = await dbContext.Database.BeginTransactionAsync(cancellationToken);
// 2. 读取当前角色映射
var existing = await dbContext.UserRoles var existing = await dbContext.UserRoles
.Where(x => x.TenantId == tenantId && x.UserId == userId) .Where(x => x.TenantId == tenantId && x.UserId == userId)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// 3. 清空并保存
dbContext.UserRoles.RemoveRange(existing); dbContext.UserRoles.RemoveRange(existing);
await dbContext.SaveChangesAsync(cancellationToken); await dbContext.SaveChangesAsync(cancellationToken);
// 4. 构建新映射
var toAdd = roleIds.Distinct().Select(roleId => new UserRole var toAdd = roleIds.Distinct().Select(roleId => new UserRole
{ {
TenantId = tenantId, TenantId = tenantId,
@@ -42,6 +68,7 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol
RoleId = roleId RoleId = roleId
}); });
// 5. 批量新增并保存
await dbContext.UserRoles.AddRangeAsync(toAdd, cancellationToken); await dbContext.UserRoles.AddRangeAsync(toAdd, cancellationToken);
await dbContext.SaveChangesAsync(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) public Task SaveChangesAsync(CancellationToken cancellationToken = default)
=> dbContext.SaveChangesAsync(cancellationToken); => dbContext.SaveChangesAsync(cancellationToken);
} }

View File

@@ -22,38 +22,52 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
/// </summary> /// </summary>
public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger) : IHostedService public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger<IdentityDataSeeder> logger) : IHostedService
{ {
/// <summary>
/// 执行后台账号与权限种子。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public async Task StartAsync(CancellationToken cancellationToken) public async Task StartAsync(CancellationToken cancellationToken)
{ {
// 1. 创建作用域并解析依赖
using var scope = serviceProvider.CreateScope(); using var scope = serviceProvider.CreateScope();
var context = scope.ServiceProvider.GetRequiredService<IdentityDbContext>(); var context = scope.ServiceProvider.GetRequiredService<IdentityDbContext>();
var options = scope.ServiceProvider.GetRequiredService<IOptions<AdminSeedOptions>>().Value; var options = scope.ServiceProvider.GetRequiredService<IOptions<AdminSeedOptions>>().Value;
var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher<DomainIdentityUser>>(); var passwordHasher = scope.ServiceProvider.GetRequiredService<IPasswordHasher<DomainIdentityUser>>();
var tenantContextAccessor = scope.ServiceProvider.GetRequiredService<ITenantContextAccessor>(); var tenantContextAccessor = scope.ServiceProvider.GetRequiredService<ITenantContextAccessor>();
// 2. 校验功能开关
if (!options.Enabled) if (!options.Enabled)
{ {
logger.LogInformation("AdminSeed 已禁用,跳过后台账号初始化"); logger.LogInformation("AdminSeed 已禁用,跳过后台账号初始化");
return; return;
} }
// 3. 确保数据库已迁移
await context.Database.MigrateAsync(cancellationToken); await context.Database.MigrateAsync(cancellationToken);
// 4. 校验账号配置
if (options.Users is null or { Count: 0 }) if (options.Users is null or { Count: 0 })
{ {
logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化"); logger.LogInformation("AdminSeed 未配置账号,跳过后台账号初始化");
return; return;
} }
// 5. 写入角色模板
await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken); await SeedRoleTemplatesAsync(context, options.RoleTemplates, cancellationToken);
// 6. 逐个账号处理
foreach (var userOptions in options.Users) foreach (var userOptions in options.Users)
{ {
// 6.1 进入租户作用域
using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId); using var tenantScope = EnterTenantScope(tenantContextAccessor, userOptions.TenantId);
// 6.2 查询账号并收集配置
var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken); var user = await context.IdentityUsers.FirstOrDefaultAsync(x => x.Account == userOptions.Account, cancellationToken);
var roles = NormalizeValues(userOptions.Roles); var roles = NormalizeValues(userOptions.Roles);
var permissions = NormalizeValues(userOptions.Permissions); var permissions = NormalizeValues(userOptions.Permissions);
if (user == null) if (user == null)
{ {
// 6.3 创建新账号
user = new DomainIdentityUser user = new DomainIdentityUser
{ {
Id = 0, Id = 0,
@@ -69,6 +83,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
} }
else else
{ {
// 6.4 更新既有账号
user.DisplayName = userOptions.DisplayName; user.DisplayName = userOptions.DisplayName;
user.TenantId = userOptions.TenantId; user.TenantId = userOptions.TenantId;
user.MerchantId = userOptions.MerchantId; user.MerchantId = userOptions.MerchantId;
@@ -76,7 +91,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
logger.LogInformation("已更新后台账号 {Account}", user.Account); logger.LogInformation("已更新后台账号 {Account}", user.Account);
} }
// 确保角色存在 // 6.5 确保角色存在
var existingRoles = await context.Roles var existingRoles = await context.Roles
.Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -97,7 +112,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
}); });
} }
// 确保权限存在 // 6.6 确保权限存在
var existingPermissions = await context.Permissions var existingPermissions = await context.Permissions
.Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -118,9 +133,10 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
}); });
} }
// 6.7 保存基础角色/权限
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
// 重新加载角色/权限以获取 Id // 6.8 重新加载角色/权限以获取 Id
var roleEntities = await context.Roles var roleEntities = await context.Roles
.Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code)) .Where(r => r.TenantId == userOptions.TenantId && roles.Contains(r.Code))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -128,7 +144,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
.Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code)) .Where(p => p.TenantId == userOptions.TenantId && permissions.Contains(p.Code))
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// 重置用户角色 // 6.9 重置用户角色
var existingUserRoles = await context.UserRoles var existingUserRoles = await context.UserRoles
.Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id) .Where(ur => ur.TenantId == userOptions.TenantId && ur.UserId == user.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
@@ -191,6 +207,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
continue; continue;
} }
// 6.10 绑定角色与权限
await context.RolePermissions.AddAsync(new DomainRolePermission await context.RolePermissions.AddAsync(new DomainRolePermission
{ {
TenantId = userOptions.TenantId, TenantId = userOptions.TenantId,
@@ -209,9 +226,15 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
} }
} }
// 7. 最终保存
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
} }
/// <summary>
/// 停止生命周期时的清理(此处无需处理)。
/// </summary>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>已完成任务。</returns>
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
private static async Task SeedRoleTemplatesAsync( private static async Task SeedRoleTemplatesAsync(
@@ -219,23 +242,28 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
IList<RoleTemplateSeedOptions> templates, IList<RoleTemplateSeedOptions> templates,
CancellationToken cancellationToken) CancellationToken cancellationToken)
{ {
// 1. 空集合直接返回
if (templates is null || templates.Count == 0) if (templates is null || templates.Count == 0)
{ {
return; return;
} }
// 2. 逐个处理模板
foreach (var templateOptions in templates) foreach (var templateOptions in templates)
{ {
// 2.1 校验必填字段
if (string.IsNullOrWhiteSpace(templateOptions.TemplateCode) || string.IsNullOrWhiteSpace(templateOptions.Name)) if (string.IsNullOrWhiteSpace(templateOptions.TemplateCode) || string.IsNullOrWhiteSpace(templateOptions.Name))
{ {
continue; continue;
} }
// 2.2 查询现有模板
var code = templateOptions.TemplateCode.Trim(); var code = templateOptions.TemplateCode.Trim();
var existing = await context.RoleTemplates.FirstOrDefaultAsync(x => x.TemplateCode == code, cancellationToken); var existing = await context.RoleTemplates.FirstOrDefaultAsync(x => x.TemplateCode == code, cancellationToken);
if (existing == null) if (existing == null)
{ {
// 2.3 新增模板
existing = new DomainRoleTemplate existing = new DomainRoleTemplate
{ {
TemplateCode = code, TemplateCode = code,
@@ -249,6 +277,7 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
} }
else else
{ {
// 2.4 更新模板
existing.Name = templateOptions.Name.Trim(); existing.Name = templateOptions.Name.Trim();
existing.Description = templateOptions.Description; existing.Description = templateOptions.Description;
existing.IsActive = templateOptions.IsActive; existing.IsActive = templateOptions.IsActive;
@@ -256,13 +285,15 @@ public sealed class IdentityDataSeeder(IServiceProvider serviceProvider, ILogger
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
} }
// 2.5 重置模板权限
var permissionCodes = NormalizeValues(templateOptions.Permissions); var permissionCodes = NormalizeValues(templateOptions.Permissions);
var existingPermissions = await context.RoleTemplatePermissions var existingPermissions = await context.RoleTemplatePermissions
.Where(x => x.RoleTemplateId == existing.Id) .Where(x => x.RoleTemplateId == existing.Id)
.ToListAsync(cancellationToken); .ToListAsync(cancellationToken);
// 2.6 清空旧权限并保存
context.RoleTemplatePermissions.RemoveRange(existingPermissions); context.RoleTemplatePermissions.RemoveRange(existingPermissions);
await context.SaveChangesAsync(cancellationToken); await context.SaveChangesAsync(cancellationToken);
// 2.7 去重后的权限编码
var distinctPermissionCodes = permissionCodes.Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); var distinctPermissionCodes = permissionCodes.Distinct(StringComparer.OrdinalIgnoreCase).ToArray();
foreach (var permissionCode in distinctPermissionCodes) foreach (var permissionCode in distinctPermissionCodes)
{ {

View File

@@ -12,11 +12,21 @@ namespace TakeoutSaaS.Infrastructure.Identity.Persistence;
internal sealed class IdentityDesignTimeDbContextFactory internal sealed class IdentityDesignTimeDbContextFactory
: DesignTimeDbContextFactoryBase<IdentityDbContext> : DesignTimeDbContextFactoryBase<IdentityDbContext>
{ {
/// <summary>
/// 初始化 Identity 设计时上下文工厂。
/// </summary>
public IdentityDesignTimeDbContextFactory() public IdentityDesignTimeDbContextFactory()
: base(DatabaseConstants.IdentityDataSource, "TAKEOUTSAAS_IDENTITY_CONNECTION") : 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( protected override IdentityDbContext CreateContext(
DbContextOptions<IdentityDbContext> options, DbContextOptions<IdentityDbContext> options,
ITenantProvider tenantProvider, ITenantProvider tenantProvider,

View File

@@ -14,8 +14,15 @@ public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions<Logi
{ {
private readonly LoginRateLimitOptions _options = options.Value; 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) public async Task EnsureAllowedAsync(string key, CancellationToken cancellationToken = default)
{ {
// 1. 读取当前计数
var cacheKey = BuildKey(key); var cacheKey = BuildKey(key);
var current = await cache.GetStringAsync(cacheKey, cancellationToken); var current = await cache.GetStringAsync(cacheKey, cancellationToken);
var count = string.IsNullOrWhiteSpace(current) ? 0 : int.Parse(current); 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, "尝试次数过多,请稍后再试"); throw new BusinessException(ErrorCodes.Forbidden, "尝试次数过多,请稍后再试");
} }
// 2. 累加计数并回写缓存
count++; count++;
await cache.SetStringAsync( await cache.SetStringAsync(
cacheKey, cacheKey,
@@ -35,6 +43,12 @@ public sealed class RedisLoginRateLimiter(IDistributedCache cache, IOptions<Logi
cancellationToken); cancellationToken);
} }
/// <summary>
/// 重置指定键的登录计数。
/// </summary>
/// <param name="key">限流键(如账号或 IP。</param>
/// <param name="cancellationToken">取消标记。</param>
/// <returns>异步任务。</returns>
public Task ResetAsync(string key, CancellationToken cancellationToken = default) public Task ResetAsync(string key, CancellationToken cancellationToken = default)
=> cache.RemoveAsync(BuildKey(key), cancellationToken); => 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 static readonly JsonSerializerOptions JsonOptions = new(JsonSerializerDefaults.Web);
private readonly RefreshTokenStoreOptions _options = options.Value; 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) public async Task<RefreshTokenDescriptor> IssueAsync(long userId, DateTime expiresAt, CancellationToken cancellationToken = default)
{ {
// 1. 生成随机令牌
var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48)); var token = Convert.ToBase64String(RandomNumberGenerator.GetBytes(48));
var descriptor = new RefreshTokenDescriptor(token, userId, expiresAt, false); var descriptor = new RefreshTokenDescriptor(token, userId, expiresAt, false);
// 2. 写入缓存
var key = BuildKey(token); var key = BuildKey(token);
var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt }; var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = expiresAt };
await cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken); await cache.SetStringAsync(key, JsonSerializer.Serialize(descriptor, JsonOptions), entryOptions, cancellationToken);
@@ -28,22 +37,37 @@ public sealed class RedisRefreshTokenStore(IDistributedCache cache, IOptions<Ref
return descriptor; 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) public async Task<RefreshTokenDescriptor?> GetAsync(string refreshToken, CancellationToken cancellationToken = default)
{ {
// 1. 读取缓存
var json = await cache.GetStringAsync(BuildKey(refreshToken), cancellationToken); var json = await cache.GetStringAsync(BuildKey(refreshToken), cancellationToken);
return string.IsNullOrWhiteSpace(json) return string.IsNullOrWhiteSpace(json)
? null ? null
: JsonSerializer.Deserialize<RefreshTokenDescriptor>(json, JsonOptions); : 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) public async Task RevokeAsync(string refreshToken, CancellationToken cancellationToken = default)
{ {
// 1. 读取令牌
var descriptor = await GetAsync(refreshToken, cancellationToken); var descriptor = await GetAsync(refreshToken, cancellationToken);
if (descriptor == null) if (descriptor == null)
{ {
return; return;
} }
// 2. 标记吊销并回写缓存
var updated = descriptor with { Revoked = true }; var updated = descriptor with { Revoked = true };
var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt }; var entryOptions = new DistributedCacheEntryOptions { AbsoluteExpiration = updated.ExpiresAt };
await cache.SetStringAsync(BuildKey(refreshToken), JsonSerializer.Serialize(updated, JsonOptions), entryOptions, cancellationToken); 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; 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) 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"; 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); using var response = await httpClient.GetAsync(requestUri, cancellationToken);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
// 2. 读取响应
var payload = await response.Content.ReadFromJsonAsync<WeChatSessionResponse>(cancellationToken: cancellationToken); var payload = await response.Content.ReadFromJsonAsync<WeChatSessionResponse>(cancellationToken: cancellationToken);
if (payload == null) if (payload == null)
{ {
throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:响应为空"); throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:响应为空");
} }
// 3. 校验错误码
if (payload.ErrorCode.HasValue && payload.ErrorCode.Value != 0) if (payload.ErrorCode.HasValue && payload.ErrorCode.Value != 0)
{ {
var message = string.IsNullOrWhiteSpace(payload.ErrorMessage) var message = string.IsNullOrWhiteSpace(payload.ErrorMessage)
@@ -35,11 +44,13 @@ public sealed class WeChatAuthService(HttpClient httpClient, IOptions<WeChatMini
throw new BusinessException(ErrorCodes.Unauthorized, message); throw new BusinessException(ErrorCodes.Unauthorized, message);
} }
// 4. 校验必要字段
if (string.IsNullOrWhiteSpace(payload.OpenId) || string.IsNullOrWhiteSpace(payload.SessionKey)) if (string.IsNullOrWhiteSpace(payload.OpenId) || string.IsNullOrWhiteSpace(payload.SessionKey))
{ {
throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:返回数据无效"); throw new BusinessException(ErrorCodes.Unauthorized, "微信登录失败:返回数据无效");
} }
// 5. 组装会话信息
return new WeChatSessionInfo return new WeChatSessionInfo
{ {
OpenId = payload.OpenId, OpenId = payload.OpenId,

View File

@@ -1,34 +1,40 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using TakeoutSaaS.Module.Authorization.Policies; using TakeoutSaaS.Module.Authorization.Policies;
namespace TakeoutSaaS.Module.Authorization.Attributes; namespace TakeoutSaaS.Module.Authorization.Attributes;
/// <summary> /// <summary>
/// 权限校验特性 /// 权限校验特性
/// </summary> /// </summary>
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = true)]
public sealed class PermissionAuthorizeAttribute : AuthorizeAttribute public sealed class PermissionAuthorizeAttribute : AuthorizeAttribute
{ {
/// <summary>
/// 初始化权限校验特性并构建对应策略。
/// </summary>
/// <param name="permissions">所需的权限标识集合。</param>
public PermissionAuthorizeAttribute(params string[] permissions) public PermissionAuthorizeAttribute(params string[] permissions)
{ {
// 1. 校验权限参数不为空
ArgumentNullException.ThrowIfNull(permissions); ArgumentNullException.ThrowIfNull(permissions);
// 2. 规范化权限标识
var normalized = permissions var normalized = permissions
.Where(p => !string.IsNullOrWhiteSpace(p)) .Where(p => !string.IsNullOrWhiteSpace(p))
.Select(p => p.Trim()) .Select(p => p.Trim())
.Distinct(StringComparer.OrdinalIgnoreCase) .Distinct(StringComparer.OrdinalIgnoreCase)
.ToArray(); .ToArray();
// 3. 确保至少提供一个有效权限
if (normalized.Length == 0) if (normalized.Length == 0)
{ {
throw new ArgumentException("至少需要一个权限标识", nameof(permissions)); throw new ArgumentException("至少需要一个权限标识", nameof(permissions));
} }
// 4. 绑定权限集合并生成策略名称
Permissions = normalized; Permissions = normalized;
Policy = PermissionAuthorizationPolicyProvider.BuildPolicyName(normalized); Policy = PermissionAuthorizationPolicyProvider.BuildPolicyName(normalized);
} }
/// <summary> /// <summary>
/// 所需权限集合 /// 所需权限集合
/// </summary> /// </summary>
public IReadOnlyCollection<string> Permissions { get; } public IReadOnlyCollection<string> Permissions { get; }
} }

View File

@@ -1,38 +1,45 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
namespace TakeoutSaaS.Module.Authorization.Policies; namespace TakeoutSaaS.Module.Authorization.Policies;
/// <summary> /// <summary>
/// 权限校验处理器 /// 权限校验处理器
/// </summary> /// </summary>
public sealed class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement> public sealed class PermissionAuthorizationHandler : AuthorizationHandler<PermissionRequirement>
{ {
/// <summary>
/// 用户声明中权限的声明类型键。
/// </summary>
public const string PermissionClaimType = "permission"; 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) protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{ {
// 1. 校验用户已通过认证
if (context.User?.Identity?.IsAuthenticated != true) if (context.User?.Identity?.IsAuthenticated != true)
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
// 2. 收集用户已授予的权限标识
var userPermissions = context.User var userPermissions = context.User
.FindAll(PermissionClaimType) .FindAll(PermissionClaimType)
.Select(claim => claim.Value) .Select(claim => claim.Value)
.Where(value => !string.IsNullOrWhiteSpace(value)) .Where(value => !string.IsNullOrWhiteSpace(value))
.Select(value => value.Trim()) .Select(value => value.Trim())
.ToHashSet(StringComparer.OrdinalIgnoreCase); .ToHashSet(StringComparer.OrdinalIgnoreCase);
// 3. 无权限直接结束
if (userPermissions.Count == 0) if (userPermissions.Count == 0)
{ {
return Task.CompletedTask; return Task.CompletedTask;
} }
// 4. 任一权限匹配即视为授权通过
if (requirement.Permissions.Any(userPermissions.Contains)) if (requirement.Permissions.Any(userPermissions.Contains))
{ {
context.Succeed(requirement); context.Succeed(requirement);
} }
// 5. 结束处理
return Task.CompletedTask; return Task.CompletedTask;
} }
} }

View File

@@ -1,55 +1,62 @@
using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
namespace TakeoutSaaS.Module.Authorization.Policies; namespace TakeoutSaaS.Module.Authorization.Policies;
/// <summary> /// <summary>
/// 权限策略提供者(按需动态构建策略) /// 权限策略提供者(按需动态构建策略)
/// </summary> /// </summary>
public sealed class PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : DefaultAuthorizationPolicyProvider(options) public sealed class PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options) : DefaultAuthorizationPolicyProvider(options)
{ {
/// <summary>
/// 权限策略名称前缀。
/// </summary>
public const string PolicyPrefix = "PERMISSION:"; public const string PolicyPrefix = "PERMISSION:";
private readonly AuthorizationOptions _options = options.Value; private readonly AuthorizationOptions _options = options.Value;
/// <summary>
/// 获取或构建指定名称的权限策略。
/// </summary>
/// <param name="policyName">策略名称。</param>
/// <returns>匹配的授权策略。</returns>
public override Task<AuthorizationPolicy?> GetPolicyAsync(string policyName) 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); return base.GetPolicyAsync(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);
} }
// 2. 复用已存在的策略
return base.GetPolicyAsync(policyName); 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>
/// 根据权限集合构建策略名称 /// 根据权限集合构建策略名称
/// </summary> /// </summary>
/// <param name="permissions">权限标识集合。</param>
/// <returns>策略名称。</returns>
public static string BuildPolicyName(IEnumerable<string> permissions) public static string BuildPolicyName(IEnumerable<string> permissions)
=> $"{PolicyPrefix}{string.Join('|', NormalizePermissions(permissions))}"; => $"{PolicyPrefix}{string.Join('|', NormalizePermissions(permissions))}";
private static string[] ParsePermissions(string policyName) private static string[] ParsePermissions(string policyName)
{ {
// 1. 拆分策略名称得到原始权限列表
var raw = policyName[PolicyPrefix.Length..]; var raw = policyName[PolicyPrefix.Length..];
// 2. 规范化并过滤权限
return NormalizePermissions(raw.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)); return NormalizePermissions(raw.Split('|', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries));
} }
private static string[] NormalizePermissions(IEnumerable<string> permissions) private static string[] NormalizePermissions(IEnumerable<string> permissions)
=> [.. permissions => [.. permissions
.Where(p => !string.IsNullOrWhiteSpace(p)) .Where(p => !string.IsNullOrWhiteSpace(p))

View File

@@ -1,7 +1,5 @@
using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Module.Tenancy; namespace TakeoutSaaS.Module.Tenancy;
/// <summary> /// <summary>
/// 基于 <see cref="AsyncLocal{T}"/> 的租户上下文访问器,实现请求级别隔离。 /// 基于 <see cref="AsyncLocal{T}"/> 的租户上下文访问器,实现请求级别隔离。
/// </summary> /// </summary>
@@ -9,7 +7,10 @@ public sealed class TenantContextAccessor : ITenantContextAccessor
{ {
private static readonly AsyncLocal<TenantContextHolder?> Holder = new(); private static readonly AsyncLocal<TenantContextHolder?> Holder = new();
/// <inheritdoc /> // 当前请求的租户上下文访问入口
/// <summary>
/// 获取或设置当前请求的租户上下文。
/// </summary>
public TenantContext? Current public TenantContext? Current
{ {
get => Holder.Value?.Context; get => Holder.Value?.Context;
@@ -26,6 +27,7 @@ public sealed class TenantContextAccessor : ITenantContextAccessor
} }
} }
// 内部持有器用于绑定异步局部上下文
private sealed class TenantContextHolder private sealed class TenantContextHolder
{ {
public TenantContext? Context { get; set; } public TenantContext? Context { get; set; }