From b3639ff34bab68aaee7645057790367f4fc45d9c Mon Sep 17 00:00:00 2001 From: MSuMshk <2039814060@qq.com> Date: Thu, 29 Jan 2026 10:46:49 +0000 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=AE=A1=E7=90=86=E7=AB=AF?= =?UTF-8?q?=E5=8E=BB=E7=A7=9F=E6=88=B7=E8=BF=87=E6=BB=A4=E5=B9=B6Portal?= =?UTF-8?q?=E5=8C=96RBAC=E8=8F=9C=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TakeoutSaaS.Docs | 2 +- .../Controllers/PermissionsController.cs | 20 +- .../Controllers/StoresController.cs | 12 +- .../Controllers/SubscriptionsController.cs | 8 +- .../BatchUpdateBusinessHoursCommandHandler.cs | 8 +- .../Handlers/CalculateStoreFeeQueryHandler.cs | 10 +- .../CheckStoreDeliveryZoneQueryHandler.cs | 11 +- .../CheckStoreQualificationsQueryHandler.cs | 12 +- .../Handlers/CreateStoreCommandHandler.cs | 11 +- .../CreateStoreDeliveryZoneCommandHandler.cs | 8 +- .../CreateStoreHolidayCommandHandler.cs | 19 +- .../DeleteStoreDeliveryZoneCommandHandler.cs | 20 +- .../DeleteStoreHolidayCommandHandler.cs | 20 +- .../Handlers/GetStoreByIdQueryHandler.cs | 11 +- .../Handlers/GetStoreFeeQueryHandler.cs | 13 +- .../ListStoreBusinessHoursQueryHandler.cs | 17 +- .../ListStoreDeliveryZonesQueryHandler.cs | 17 +- .../Handlers/ListStoreHolidaysQueryHandler.cs | 16 +- .../ListStoreQualificationsQueryHandler.cs | 13 +- .../Handlers/SearchStoresQueryHandler.cs | 15 +- .../Handlers/UpdateStoreCommandHandler.cs | 10 +- .../UpdateStoreDeliveryZoneCommandHandler.cs | 8 +- .../Handlers/UpdateStoreFeeCommandHandler.cs | 10 +- .../UpdateStoreHolidayCommandHandler.cs | 20 +- .../App/Stores/Queries/GetStoreByIdQuery.cs | 5 + .../App/Stores/Queries/SearchStoresQuery.cs | 10 + .../App/Stores/StoreTenantAccess.cs | 45 ---- .../BatchExtendSubscriptionsCommandHandler.cs | 7 +- .../BatchSendReminderCommandHandler.cs | 7 +- .../ChangeSubscriptionPlanCommandHandler.cs | 7 +- .../ExtendSubscriptionCommandHandler.cs | 7 +- .../GetSubscriptionDetailQueryHandler.cs | 12 +- .../GetSubscriptionListQueryHandler.cs | 8 +- .../ProcessAutoRenewalCommandHandler.cs | 7 +- .../ProcessRenewalRemindersCommandHandler.cs | 7 +- ...ProcessSubscriptionExpiryCommandHandler.cs | 10 +- .../UpdateSubscriptionCommandHandler.cs | 7 +- .../UpdateSubscriptionStatusCommandHandler.cs | 7 +- .../Queries/GetSubscriptionDetailQuery.cs | 5 + .../Queries/GetSubscriptionListQuery.cs | 5 + .../Subscriptions/SubscriptionTenantAccess.cs | 39 --- .../CreateTenantManuallyCommandHandler.cs | 4 +- .../SelfRegisterTenantCommandHandler.cs | 4 +- .../Identity/Contracts/CurrentUserProfile.cs | 9 +- .../Identity/Contracts/PermissionDto.cs | 12 +- .../Identity/Contracts/PermissionTreeDto.cs | 14 +- .../Identity/Contracts/RoleDetailDto.cs | 11 +- .../Identity/Contracts/RoleDto.cs | 12 +- .../Identity/Contracts/UserDetailDto.cs | 11 +- .../Identity/Contracts/UserListItemDto.cs | 11 +- .../Identity/Contracts/UserPermissionDto.cs | 6 +- .../Handlers/AssignUserRolesCommandHandler.cs | 12 +- ...atchIdentityUserOperationCommandHandler.cs | 18 +- .../BindRolePermissionsCommandHandler.cs | 12 +- .../ChangeIdentityUserStatusCommandHandler.cs | 21 +- .../CopyRoleTemplateCommandHandler.cs | 20 +- .../CreateIdentityUserCommandHandler.cs | 8 +- .../Handlers/CreateMenuCommandHandler.cs | 11 +- .../CreatePermissionCommandHandler.cs | 18 +- .../Handlers/CreateRoleCommandHandler.cs | 18 +- .../DeleteIdentityUserCommandHandler.cs | 18 +- .../Handlers/DeleteMenuCommandHandler.cs | 11 +- .../DeletePermissionCommandHandler.cs | 13 +- .../Handlers/DeleteRoleCommandHandler.cs | 12 +- .../GetIdentityUserDetailQueryHandler.cs | 29 +-- .../GetUserPermissionsQueryHandler.cs | 20 +- .../Handlers/ListMenusQueryHandler.cs | 11 +- .../Handlers/MenuDetailQueryHandler.cs | 11 +- .../Identity/Handlers/MenuMapper.cs | 12 +- .../Handlers/PermissionTreeQueryHandler.cs | 11 +- ...ResetAdminPasswordByTokenCommandHandler.cs | 4 +- ...ResetIdentityUserPasswordCommandHandler.cs | 4 +- .../RestoreIdentityUserCommandHandler.cs | 2 +- .../Handlers/RoleDetailQueryHandler.cs | 21 +- .../SearchIdentityUsersQueryHandler.cs | 14 +- .../Handlers/SearchPermissionsQueryHandler.cs | 11 +- .../Handlers/SearchRolesQueryHandler.cs | 19 +- .../SearchUserPermissionsQueryHandler.cs | 13 +- .../UpdateIdentityUserCommandHandler.cs | 26 +- .../Handlers/UpdateMenuCommandHandler.cs | 11 +- .../UpdatePermissionCommandHandler.cs | 11 +- .../Handlers/UpdateRoleCommandHandler.cs | 15 +- .../Identity/Queries/PermissionTreeQuery.cs | 6 + .../Queries/SearchPermissionsQuery.cs | 6 + .../Identity/Services/AdminAuthService.cs | 40 ++-- .../Identity/Entities/IdentityUser.cs | 14 +- .../Identity/Entities/MenuDefinition.cs | 10 +- .../Identity/Entities/Permission.cs | 8 +- .../Identity/Entities/Role.cs | 13 +- .../Identity/Entities/RolePermission.cs | 13 +- .../Identity/Entities/UserRole.cs | 13 +- .../Identity/Enums/PortalType.cs | 18 ++ .../Repositories/IIdentityUserRepository.cs | 29 +-- .../Identity/Repositories/IMenuRepository.cs | 17 +- .../Repositories/IPermissionRepository.cs | 24 +- .../Repositories/IRolePermissionRepository.cs | 20 +- .../Identity/Repositories/IRoleRepository.cs | 30 ++- .../Repositories/IUserRoleRepository.cs | 34 ++- .../Stores/Repositories/IStoreRepository.cs | 62 ++++- .../Repositories/ISubscriptionRepository.cs | 40 ++-- .../App/Persistence/TakeoutAdminDbContext.cs | 7 +- .../App/Persistence/TakeoutAppDbContext.cs | 6 +- .../App/Repositories/EfStoreRepository.cs | 223 ++++++++++-------- .../Repositories/EfSubscriptionRepository.cs | 40 ++-- .../Persistence/TenantAwareDbContext.cs | 33 +-- .../Persistence/DictionaryDbContext.cs | 6 +- .../Persistence/EfIdentityUserRepository.cs | 153 ++++-------- .../Persistence/EfPermissionRepository.cs | 24 +- .../Persistence/EfRolePermissionRepository.cs | 53 +++-- .../Identity/Persistence/EfRoleRepository.cs | 40 +++- .../Persistence/EfUserRoleRepository.cs | 35 ++- .../Identity/Persistence/IdentityDbContext.cs | 111 ++++++--- .../Identity/Repositories/EfMenuRepository.cs | 15 +- .../Identity/Services/JwtTokenService.cs | 12 +- .../Logs/Persistence/TakeoutLogsDbContext.cs | 6 +- 115 files changed, 1106 insertions(+), 1092 deletions(-) delete mode 100644 src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs delete mode 100644 src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs create mode 100644 src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs diff --git a/TakeoutSaaS.Docs b/TakeoutSaaS.Docs index 79fcefa..65fecf7 160000 --- a/TakeoutSaaS.Docs +++ b/TakeoutSaaS.Docs @@ -1 +1 @@ -Subproject commit 79fcefaebcf5982338ee9f5e69b2616975242ac3 +Subproject commit 65fecf74e83221e4921ef25eef04fe59a318d08d diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs index a32d48c..92090ea 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/PermissionsController.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Mvc; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Module.Authorization.Attributes; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Web.Api; @@ -37,19 +38,20 @@ public sealed class PermissionsController(IMediator mediator) : BaseApiControlle return ApiResponse>.Ok(result); } - /// - /// 获取权限树。 - /// - /// 关键字(可选)。 - /// 取消标记。 - /// 权限树列表。 - [HttpGet("tree")] + /// + /// 获取权限树。 + /// + /// Portal 类型(Admin/Tenant)。 + /// 关键字(可选)。 + /// 取消标记。 + /// 权限树列表。 + [HttpGet("tree")] [PermissionAuthorize("identity:permission:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] - public async Task>> Tree([FromQuery] string? keyword, CancellationToken cancellationToken) + public async Task>> Tree([FromQuery] PortalType portal, [FromQuery] string? keyword, CancellationToken cancellationToken) { // 1. 构造查询对象 - var query = new PermissionTreeQuery { Keyword = keyword }; + var query = new PermissionTreeQuery { Portal = portal, Keyword = keyword }; // 2. 查询权限树 var result = await mediator.Send(query, cancellationToken); diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs index 5bb3b1d..e2354f0 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/StoresController.cs @@ -45,12 +45,14 @@ public sealed class StoresController(IMediator mediator) : BaseApiController [PermissionAuthorize("store:read")] [ProducesResponseType(typeof(ApiResponse>), StatusCodes.Status200OK)] public async Task>> List( + [FromQuery] long? tenantId, [FromQuery] long? merchantId, [FromQuery] StoreStatus? status, [FromQuery] StoreAuditStatus? auditStatus, [FromQuery] StoreBusinessStatus? businessStatus, [FromQuery] StoreOwnershipType? ownershipType, [FromQuery] string? keyword, + [FromQuery] bool includeDeleted = false, [FromQuery] int page = 1, [FromQuery] int pageSize = 20, [FromQuery] string? sortBy = null, @@ -60,12 +62,14 @@ public sealed class StoresController(IMediator mediator) : BaseApiController // 1. 组装查询参数并执行查询 var result = await mediator.Send(new SearchStoresQuery { + TenantId = tenantId, MerchantId = merchantId, Status = status, AuditStatus = auditStatus, BusinessStatus = businessStatus, OwnershipType = ownershipType, Keyword = keyword, + IncludeDeleted = includeDeleted, Page = page, PageSize = pageSize, SortBy = sortBy, @@ -84,10 +88,14 @@ public sealed class StoresController(IMediator mediator) : BaseApiController [PermissionAuthorize("store:read")] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status200OK)] [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] - public async Task> Detail(long storeId, CancellationToken cancellationToken) + public async Task> Detail(long storeId, [FromQuery] bool includeDeleted, CancellationToken cancellationToken) { // 1. 查询门店详情 - var result = await mediator.Send(new GetStoreByIdQuery { StoreId = storeId }, cancellationToken); + var result = await mediator.Send(new GetStoreByIdQuery + { + StoreId = storeId, + IncludeDeleted = includeDeleted + }, cancellationToken); // 2. 返回详情或 404 return result == null diff --git a/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs index 75336d1..710a8a5 100644 --- a/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs +++ b/src/Api/TakeoutSaaS.AdminApi/Controllers/SubscriptionsController.cs @@ -43,6 +43,7 @@ public sealed class SubscriptionsController(IMediator mediator) : BaseApiControl /// 查看订阅详情(含套餐信息、配额使用、变更历史)。 /// /// 订阅 ID。 + /// 是否包含已软删除数据。 /// 取消标记。 /// 订阅详情或未找到。 [HttpGet("{subscriptionId:long}")] @@ -51,10 +52,15 @@ public sealed class SubscriptionsController(IMediator mediator) : BaseApiControl [ProducesResponseType(typeof(ApiResponse), StatusCodes.Status404NotFound)] public async Task> Detail( long subscriptionId, + [FromQuery] bool includeDeleted, CancellationToken cancellationToken) { // 1. 查询订阅详情 - var result = await mediator.Send(new GetSubscriptionDetailQuery { SubscriptionId = subscriptionId }, cancellationToken); + var result = await mediator.Send(new GetSubscriptionDetailQuery + { + SubscriptionId = subscriptionId, + IncludeDeleted = includeDeleted + }, cancellationToken); // 2. 返回查询结果或 404 return result is null diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/BatchUpdateBusinessHoursCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/BatchUpdateBusinessHoursCommandHandler.cs index 6934e85..c012a57 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/BatchUpdateBusinessHoursCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/BatchUpdateBusinessHoursCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; @@ -9,7 +8,6 @@ using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,8 +16,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class BatchUpdateBusinessHoursCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler> { @@ -27,9 +23,7 @@ public sealed class BatchUpdateBusinessHoursCommandHandler( public async Task> Handle(BatchUpdateBusinessHoursCommand request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CalculateStoreFeeQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CalculateStoreFeeQueryHandler.cs index cd72893..8025507 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CalculateStoreFeeQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CalculateStoreFeeQueryHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Application.App.Stores.Services; @@ -8,7 +7,6 @@ using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -17,8 +15,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class CalculateStoreFeeQueryHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, IStoreFeeCalculationService feeCalculationService) : IRequestHandler { @@ -26,16 +22,14 @@ public sealed class CalculateStoreFeeQueryHandler( public async Task Handle(CalculateStoreFeeQuery request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); } // 2. (空行后) 获取费用配置 - var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken) + var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, null, cancellationToken) ?? new StoreFee { StoreId = request.StoreId, diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreDeliveryZoneQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreDeliveryZoneQueryHandler.cs index 4c7c8c1..f1a9855 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreDeliveryZoneQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreDeliveryZoneQueryHandler.cs @@ -1,13 +1,10 @@ using MediatR; -using Microsoft.AspNetCore.Http; -using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Application.App.Stores.Services; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -16,8 +13,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class CheckStoreDeliveryZoneQueryHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, IDeliveryZoneService deliveryZoneService) : IRequestHandler { @@ -25,16 +20,14 @@ public sealed class CheckStoreDeliveryZoneQueryHandler( public async Task Handle(CheckStoreDeliveryZoneQuery request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); } // 2. (空行后) 执行配送范围判断 - var zones = await storeRepository.GetDeliveryZonesAsync(request.StoreId, tenantId, cancellationToken); + var zones = await storeRepository.GetDeliveryZonesAsync(request.StoreId, null, cancellationToken); var result = deliveryZoneService.CheckPointInZones(zones, request.Longitude, request.Latitude); // 3. (空行后) 计算距离 diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreQualificationsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreQualificationsQueryHandler.cs index 2257e38..40166c2 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreQualificationsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CheckStoreQualificationsQueryHandler.cs @@ -1,12 +1,10 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -14,18 +12,14 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 门店资质完整性检查处理器。 /// public sealed class CheckStoreQualificationsQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler { /// public async Task Handle(CheckStoreQualificationsQuery request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); @@ -42,7 +36,7 @@ public sealed class CheckStoreQualificationsQueryHandler( } // 3. (空行后) 读取资质列表并统计 - var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, tenantId, cancellationToken); + var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, null, cancellationToken); var grouped = qualifications .GroupBy(x => x.QualificationType) .ToDictionary(x => x.Key, x => x.ToList()); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs index 5acd666..f8145b8 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreCommandHandler.cs @@ -1,7 +1,5 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Domain.Merchants.Repositories; @@ -10,7 +8,6 @@ using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -20,8 +17,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; public sealed class CreateStoreCommandHandler( IStoreRepository storeRepository, IMerchantRepository merchantRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { @@ -29,11 +24,7 @@ public sealed class CreateStoreCommandHandler( public async Task Handle(CreateStoreCommand request, CancellationToken cancellationToken) { // 1. 校验商户存在并解析租户 - var currentTenantId = tenantProvider.GetCurrentTenantId(); - var allowCrossTenant = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var merchant = allowCrossTenant - ? await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken) - : await merchantRepository.FindByIdAsync(request.MerchantId, currentTenantId, cancellationToken); + var merchant = await merchantRepository.FindByIdAsync(request.MerchantId, cancellationToken); if (merchant == null) { throw new BusinessException(ErrorCodes.NotFound, "商户不存在"); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs index 548c7d0..a477879 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreDeliveryZoneCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; @@ -9,7 +8,6 @@ using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,8 +16,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class CreateStoreDeliveryZoneCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, IGeoJsonValidationService geoJsonValidationService, ILogger logger) : IRequestHandler @@ -28,9 +24,7 @@ public sealed class CreateStoreDeliveryZoneCommandHandler( public async Task Handle(CreateStoreDeliveryZoneCommand request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreHolidayCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreHolidayCommandHandler.cs index 3bc2b2f..2b714f7 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreHolidayCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/CreateStoreHolidayCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; @@ -9,7 +8,6 @@ using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,23 +16,14 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class CreateStoreHolidayCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - private readonly ILogger _logger = logger; - /// public async Task Handle(CreateStoreHolidayCommand request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var store = await _storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); @@ -57,9 +46,9 @@ public sealed class CreateStoreHolidayCommandHandler( }; // 3. 持久化 - await _storeRepository.AddHolidaysAsync(new[] { holiday }, cancellationToken); - await _storeRepository.SaveChangesAsync(cancellationToken); - _logger.LogInformation("创建节假日 {HolidayId} 对应门店 {StoreId}", holiday.Id, request.StoreId); + await storeRepository.AddHolidaysAsync(new[] { holiday }, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("创建节假日 {HolidayId} 对应门店 {StoreId}", holiday.Id, request.StoreId); // 4. 返回 DTO return StoreMapping.ToDto(holiday); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreDeliveryZoneCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreDeliveryZoneCommandHandler.cs index e6e3935..728cdfe 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreDeliveryZoneCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreDeliveryZoneCommandHandler.cs @@ -1,10 +1,7 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -13,23 +10,14 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class DeleteStoreDeliveryZoneCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - private readonly ILogger _logger = logger; - /// public async Task Handle(DeleteStoreDeliveryZoneCommand request, CancellationToken cancellationToken) { // 1. 读取区域 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var existing = await _storeRepository.FindDeliveryZoneByIdAsync(request.DeliveryZoneId, tenantId, cancellationToken); + var existing = await storeRepository.FindDeliveryZoneByIdAsync(request.DeliveryZoneId, null, cancellationToken); if (existing is null) { return false; @@ -42,9 +30,9 @@ public sealed class DeleteStoreDeliveryZoneCommandHandler( } // 3. 删除 - await _storeRepository.DeleteDeliveryZoneAsync(request.DeliveryZoneId, tenantId, cancellationToken); - await _storeRepository.SaveChangesAsync(cancellationToken); - _logger.LogInformation("删除配送区域 {DeliveryZoneId} 对应门店 {StoreId}", request.DeliveryZoneId, request.StoreId); + await storeRepository.DeleteDeliveryZoneAsync(request.DeliveryZoneId, null, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("删除配送区域 {DeliveryZoneId} 对应门店 {StoreId}", request.DeliveryZoneId, request.StoreId); return true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreHolidayCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreHolidayCommandHandler.cs index 7174630..8dbb9d3 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreHolidayCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/DeleteStoreHolidayCommandHandler.cs @@ -1,10 +1,7 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -13,23 +10,14 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class DeleteStoreHolidayCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - private readonly ILogger _logger = logger; - /// public async Task Handle(DeleteStoreHolidayCommand request, CancellationToken cancellationToken) { // 1. 读取配置 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var existing = await _storeRepository.FindHolidayByIdAsync(request.HolidayId, tenantId, cancellationToken); + var existing = await storeRepository.FindHolidayByIdAsync(request.HolidayId, null, cancellationToken); if (existing is null) { return false; @@ -42,9 +30,9 @@ public sealed class DeleteStoreHolidayCommandHandler( } // 3. 删除 - await _storeRepository.DeleteHolidayAsync(request.HolidayId, tenantId, cancellationToken); - await _storeRepository.SaveChangesAsync(cancellationToken); - _logger.LogInformation("删除节假日 {HolidayId} 对应门店 {StoreId}", request.HolidayId, request.StoreId); + await storeRepository.DeleteHolidayAsync(request.HolidayId, null, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("删除节假日 {HolidayId} 对应门店 {StoreId}", request.HolidayId, request.StoreId); return true; } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs index dda3076..1d07fff 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreByIdQueryHandler.cs @@ -1,11 +1,9 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -13,17 +11,14 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 门店详情查询处理器。 /// public sealed class GetStoreByIdQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler { /// public async Task Handle(GetStoreByIdQuery request, CancellationToken cancellationToken) { - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + // 1. 查询门店详情(默认跨租户,支持包含软删除) + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken, includeDeleted: request.IncludeDeleted); return store == null ? null : StoreMapping.ToDto(store); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs index 824eba2..485fb9a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/GetStoreFeeQueryHandler.cs @@ -1,12 +1,11 @@ using MediatR; -using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -14,25 +13,21 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 获取门店费用配置处理器。 /// public sealed class GetStoreFeeQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler { /// public async Task Handle(GetStoreFeeQuery request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); } // 2. (空行后) 查询费用配置 - var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken); + var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, null, cancellationToken); if (fee is null) { var fallback = new StoreFee diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreBusinessHoursQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreBusinessHoursQueryHandler.cs index 76b6815..5a5b708 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreBusinessHoursQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreBusinessHoursQueryHandler.cs @@ -1,10 +1,9 @@ using System.Linq; using MediatR; -using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -12,24 +11,16 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 营业时段列表查询处理器。 /// public sealed class ListStoreBusinessHoursQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler> { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - /// public async Task> Handle(ListStoreBusinessHoursQuery request, CancellationToken cancellationToken) { // 1. 查询时段列表 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var hours = await _storeRepository.GetBusinessHoursAsync(request.StoreId, tenantId, cancellationToken); + var hours = await storeRepository.GetBusinessHoursAsync(request.StoreId, null, cancellationToken); - // 2. 映射 DTO + // 2. (空行后) 映射 DTO return hours.Select(StoreMapping.ToDto).ToList(); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreDeliveryZonesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreDeliveryZonesQueryHandler.cs index 498b168..56f733d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreDeliveryZonesQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreDeliveryZonesQueryHandler.cs @@ -1,10 +1,9 @@ using System.Linq; using MediatR; -using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -12,24 +11,16 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 配送区域列表查询处理器。 /// public sealed class ListStoreDeliveryZonesQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler> { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - /// public async Task> Handle(ListStoreDeliveryZonesQuery request, CancellationToken cancellationToken) { // 1. 查询配送区域 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var zones = await _storeRepository.GetDeliveryZonesAsync(request.StoreId, tenantId, cancellationToken); + var zones = await storeRepository.GetDeliveryZonesAsync(request.StoreId, null, cancellationToken); - // 2. 映射 DTO + // 2. (空行后) 映射 DTO return zones.Select(StoreMapping.ToDto).ToList(); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreHolidaysQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreHolidaysQueryHandler.cs index 5acd01d..f88ba36 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreHolidaysQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreHolidaysQueryHandler.cs @@ -1,11 +1,9 @@ using System.Linq; using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -13,24 +11,16 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 门店节假日列表查询处理器。 /// public sealed class ListStoreHolidaysQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler> { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - /// public async Task> Handle(ListStoreHolidaysQuery request, CancellationToken cancellationToken) { // 1. 查询节假日 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var holidays = await _storeRepository.GetHolidaysAsync(request.StoreId, tenantId, cancellationToken); + var holidays = await storeRepository.GetHolidaysAsync(request.StoreId, null, cancellationToken); - // 2. 映射 DTO + // 2. (空行后) 映射 DTO return holidays.Select(StoreMapping.ToDto).ToList(); } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreQualificationsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreQualificationsQueryHandler.cs index 38da278..3908d34 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreQualificationsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/ListStoreQualificationsQueryHandler.cs @@ -1,11 +1,10 @@ using MediatR; -using Microsoft.AspNetCore.Http; +using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -13,25 +12,21 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 门店资质列表查询处理器。 /// public sealed class ListStoreQualificationsQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler> { /// public async Task> Handle(ListStoreQualificationsQuery request, CancellationToken cancellationToken) { // 1. 校验门店存在 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); } // 2. (空行后) 读取资质列表 - var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, tenantId, cancellationToken); + var qualifications = await storeRepository.GetQualificationsAsync(request.StoreId, null, cancellationToken); // 3. (空行后) 映射 DTO return qualifications.Select(StoreMapping.ToDto).ToList(); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs index 72a9883..5244c27 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/SearchStoresQueryHandler.cs @@ -1,11 +1,9 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Dto; using TakeoutSaaS.Application.App.Stores.Queries; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -13,33 +11,32 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// 门店列表查询处理器。 /// public sealed class SearchStoresQueryHandler( - IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor) + IStoreRepository storeRepository) : IRequestHandler> { /// public async Task> Handle(SearchStoresQuery request, CancellationToken cancellationToken) { - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); + // 1. 查询门店列表(可选租户过滤) var stores = await storeRepository.SearchAsync( - tenantId, + request.TenantId, request.MerchantId, request.Status, request.AuditStatus, request.BusinessStatus, request.OwnershipType, request.Keyword, - ignoreTenantFilter, + request.IncludeDeleted, cancellationToken); + // 2. (空行后) 排序与分页 var sorted = ApplySorting(stores, request.SortBy, request.SortDescending); var paged = sorted .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToList(); + // 3. (空行后) 映射 DTO var items = paged.Select(StoreMapping.ToDto).ToList(); return new PagedResult(items, request.Page, request.PageSize, stores.Count); } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs index f514e15..87cdd00 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreCommandHandler.cs @@ -1,15 +1,13 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; using TakeoutSaaS.Application.App.Stores.Dto; +using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,8 +16,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class UpdateStoreCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { @@ -27,9 +23,7 @@ public sealed class UpdateStoreCommandHandler( public async Task Handle(UpdateStoreCommand request, CancellationToken cancellationToken) { // 1. 读取门店 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var existing = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var existing = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (existing == null) { return null; diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs index 031e9e7..f697cf7 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreDeliveryZoneCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; @@ -9,7 +8,6 @@ using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,8 +16,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class UpdateStoreDeliveryZoneCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, IGeoJsonValidationService geoJsonValidationService, ILogger logger) : IRequestHandler @@ -28,9 +24,7 @@ public sealed class UpdateStoreDeliveryZoneCommandHandler( public async Task Handle(UpdateStoreDeliveryZoneCommand request, CancellationToken cancellationToken) { // 1. 读取区域 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var existing = await storeRepository.FindDeliveryZoneByIdAsync(request.DeliveryZoneId, tenantId, cancellationToken); + var existing = await storeRepository.FindDeliveryZoneByIdAsync(request.DeliveryZoneId, null, cancellationToken); if (existing is null) { return null; diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs index d734c71..12ec41a 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreFeeCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; @@ -9,7 +8,6 @@ using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,8 +16,6 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class UpdateStoreFeeCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { @@ -27,9 +23,7 @@ public sealed class UpdateStoreFeeCommandHandler( public async Task Handle(UpdateStoreFeeCommand request, CancellationToken cancellationToken) { // 1. 校验门店状态 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : tenantProvider.GetCurrentTenantId(); - var store = await storeRepository.FindByIdAsync(request.StoreId, tenantId, cancellationToken); + var store = await storeRepository.FindByIdAsync(request.StoreId, null, cancellationToken); if (store is null) { throw new BusinessException(ErrorCodes.NotFound, "门店不存在"); @@ -45,7 +39,7 @@ public sealed class UpdateStoreFeeCommandHandler( } // 2. (空行后) 获取或创建费用配置 - var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, tenantId, cancellationToken); + var fee = await storeRepository.GetStoreFeeAsync(request.StoreId, null, cancellationToken); var isNew = fee is null; fee ??= new StoreFee { diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreHolidayCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreHolidayCommandHandler.cs index ec05a3c..db6da16 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreHolidayCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Handlers/UpdateStoreHolidayCommandHandler.cs @@ -1,15 +1,12 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Stores; using TakeoutSaaS.Application.App.Stores.Commands; using TakeoutSaaS.Application.App.Stores.Dto; -using TakeoutSaaS.Domain.Stores.Entities; using TakeoutSaaS.Domain.Stores.Enums; using TakeoutSaaS.Domain.Stores.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Stores.Handlers; @@ -18,23 +15,14 @@ namespace TakeoutSaaS.Application.App.Stores.Handlers; /// public sealed class UpdateStoreHolidayCommandHandler( IStoreRepository storeRepository, - ITenantProvider tenantProvider, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { - private readonly IStoreRepository _storeRepository = storeRepository; - private readonly ITenantProvider _tenantProvider = tenantProvider; - private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor; - private readonly ILogger _logger = logger; - /// public async Task Handle(UpdateStoreHolidayCommand request, CancellationToken cancellationToken) { // 1. 读取配置 - var ignoreTenantFilter = StoreTenantAccess.ShouldIgnoreTenantFilter(_httpContextAccessor); - var tenantId = ignoreTenantFilter ? 0 : _tenantProvider.GetCurrentTenantId(); - var existing = await _storeRepository.FindHolidayByIdAsync(request.HolidayId, tenantId, cancellationToken); + var existing = await storeRepository.FindHolidayByIdAsync(request.HolidayId, null, cancellationToken); if (existing is null) { return null; @@ -57,9 +45,9 @@ public sealed class UpdateStoreHolidayCommandHandler( existing.Reason = request.Reason?.Trim(); // 4. 持久化 - await _storeRepository.UpdateHolidayAsync(existing, cancellationToken); - await _storeRepository.SaveChangesAsync(cancellationToken); - _logger.LogInformation("更新节假日 {HolidayId} 对应门店 {StoreId}", existing.Id, existing.StoreId); + await storeRepository.UpdateHolidayAsync(existing, cancellationToken); + await storeRepository.SaveChangesAsync(cancellationToken); + logger.LogInformation("更新节假日 {HolidayId} 对应门店 {StoreId}", existing.Id, existing.StoreId); // 5. 返回 DTO return StoreMapping.ToDto(existing); diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs index 7f0699e..f5c0d6c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/GetStoreByIdQuery.cs @@ -12,4 +12,9 @@ public sealed class GetStoreByIdQuery : IRequest /// 门店 ID。 /// public long StoreId { get; init; } + + /// + /// 是否包含已软删除数据。 + /// + public bool IncludeDeleted { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs index 16532d4..33dcf49 100644 --- a/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Stores/Queries/SearchStoresQuery.cs @@ -10,6 +10,11 @@ namespace TakeoutSaaS.Application.App.Stores.Queries; /// public sealed class SearchStoresQuery : IRequest> { + /// + /// 租户 ID(为空则查询全部租户)。 + /// + public long? TenantId { get; init; } + /// /// 商户 ID(可选)。 /// @@ -40,6 +45,11 @@ public sealed class SearchStoresQuery : IRequest> /// public string? Keyword { get; init; } + /// + /// 是否包含已软删除数据。 + /// + public bool IncludeDeleted { get; init; } + /// /// 页码。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs b/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs deleted file mode 100644 index f63f77c..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Stores/StoreTenantAccess.cs +++ /dev/null @@ -1,45 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System; -using System.Linq; - -namespace TakeoutSaaS.Application.App.Stores; - -internal static class StoreTenantAccess -{ - private const string PermissionClaimType = "permission"; - private const string ViewAllStoresPermission = "store:read:all"; - private static readonly string[] PlatformRoleCodes = - { - "super-admin", - "SUPER_ADMIN", - "PlatformAdmin", - "platform-admin" - }; - - public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor) - { - var httpContext = httpContextAccessor.HttpContext; - if (httpContext == null) - { - return false; - } - - var user = httpContext.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - if (PlatformRoleCodes.Any(user.IsInRole)) - { - return true; - } - - var permissions = user.FindAll(PermissionClaimType) - .Select(c => c.Value?.Trim()) - .Where(value => !string.IsNullOrWhiteSpace(value)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - return permissions.Contains(ViewAllStoresPermission); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs index 2760e90..a617f3f 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchExtendSubscriptionsCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class BatchExtendSubscriptionsCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, ILogger logger) : IRequestHandler @@ -23,8 +21,6 @@ public sealed class BatchExtendSubscriptionsCommandHandler( /// public async Task Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var successCount = 0; var failures = new List(); @@ -41,8 +37,7 @@ public sealed class BatchExtendSubscriptionsCommandHandler( // 查询所有订阅 var subscriptions = await subscriptionRepository.FindByIdsAsync( request.SubscriptionIds, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); foreach (var subscriptionId in request.SubscriptionIds) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs index cea5313..95f36b1 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/BatchSendReminderCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class BatchSendReminderCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, ILogger logger) : IRequestHandler @@ -23,16 +21,13 @@ public sealed class BatchSendReminderCommandHandler( /// public async Task Handle(BatchSendReminderCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - var successCount = 0; var failures = new List(); // 查询所有订阅及租户信息 var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync( request.SubscriptionIds, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); foreach (var subscriptionId in request.SubscriptionIds) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs index 7d443c0..b744a34 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ChangeSubscriptionPlanCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ChangeSubscriptionPlanCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, IMediator mediator) : IRequestHandler @@ -23,13 +21,10 @@ public sealed class ChangeSubscriptionPlanCommandHandler( /// public async Task Handle(ChangeSubscriptionPlanCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs index fbfb825..2717c86 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ExtendSubscriptionCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ExtendSubscriptionCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IIdGenerator idGenerator, IMediator mediator) : IRequestHandler @@ -23,13 +21,10 @@ public sealed class ExtendSubscriptionCommandHandler( /// public async Task Handle(ExtendSubscriptionCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs index f77fda1..d4c3c0d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionDetailQueryHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Application.App.Tenants; @@ -12,20 +11,17 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// 订阅详情查询处理器。 /// public sealed class GetSubscriptionDetailQueryHandler( - ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor) + ISubscriptionRepository subscriptionRepository) : IRequestHandler { /// public async Task Handle(GetSubscriptionDetailQuery request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅基础信息 var detail = await subscriptionRepository.GetDetailAsync( request.SubscriptionId, cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + includeDeleted: request.IncludeDeleted); if (detail == null) { @@ -36,7 +32,7 @@ public sealed class GetSubscriptionDetailQueryHandler( var quotaUsages = await subscriptionRepository.GetQuotaUsagesAsync( detail.Subscription.TenantId, cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + includeDeleted: request.IncludeDeleted); var quotaUsageDtos = BuildQuotaUsageDtos(detail.Package, quotaUsages); @@ -118,7 +114,7 @@ public sealed class GetSubscriptionDetailQueryHandler( }); } - // Add any extra quota usages not covered by package fields (e.g. promotion slots). + // 补充套餐字段未覆盖的配额使用项(例如营销槽位等扩展配额) foreach (var usage in usageByType.Values) { if (baselineTypes.Any(x => x.Type == usage.QuotaType)) diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs index 58d8268..132ff51 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/GetSubscriptionListQueryHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; using TakeoutSaaS.Domain.Tenants.Repositories; @@ -11,15 +10,12 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// 订阅分页查询处理器。 /// public sealed class GetSubscriptionListQueryHandler( - ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor) + ISubscriptionRepository subscriptionRepository) : IRequestHandler> { /// public async Task> Handle(GetSubscriptionListQuery request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 构建查询过滤条件 var filter = new SubscriptionSearchFilter { @@ -37,7 +33,7 @@ public sealed class GetSubscriptionListQueryHandler( var (items, total) = await subscriptionRepository.SearchPagedAsync( filter, cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + includeDeleted: request.IncludeDeleted); // 3. 映射为 DTO var dtos = items.Select(x => new SubscriptionListDto diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs index 64548ea..960ed10 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessAutoRenewalCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -14,7 +13,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ProcessAutoRenewalCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, ITenantBillingRepository billingRepository, IIdGenerator idGenerator, ILogger logger) @@ -23,8 +21,6 @@ public sealed class ProcessAutoRenewalCommandHandler( /// public async Task Handle(ProcessAutoRenewalCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 计算续费阈值时间 var now = DateTime.UtcNow; var renewalThreshold = now.AddDays(request.RenewalDaysBeforeExpiry); @@ -33,8 +29,7 @@ public sealed class ProcessAutoRenewalCommandHandler( var candidates = await subscriptionRepository.FindAutoRenewalCandidatesAsync( now, renewalThreshold, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); var createdBillCount = 0; // 3. 遍历候选订阅,生成账单 diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs index 27be88d..f1c1aba 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessRenewalRemindersCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using System.Text.Json; using TakeoutSaaS.Application.App.Subscriptions.Commands; @@ -15,7 +14,6 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ProcessRenewalRemindersCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, ITenantNotificationRepository notificationRepository, IIdGenerator idGenerator, ILogger logger) @@ -26,8 +24,6 @@ public sealed class ProcessRenewalRemindersCommandHandler( /// public async Task Handle(ProcessRenewalRemindersCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 读取提醒配置 var now = DateTime.UtcNow; var candidateCount = 0; @@ -46,8 +42,7 @@ public sealed class ProcessRenewalRemindersCommandHandler( var candidates = await subscriptionRepository.FindRenewalReminderCandidatesAsync( startOfDay, endOfDay, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); candidateCount += candidates.Count; foreach (var item in candidates) diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs index 0d798bb..9b655e5 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/ProcessSubscriptionExpiryCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Domain.Tenants.Enums; @@ -12,26 +11,21 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class ProcessSubscriptionExpiryCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, ILogger logger) : IRequestHandler { /// public async Task Handle(ProcessSubscriptionExpiryCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询到期订阅 var now = DateTime.UtcNow; var expiredActive = await subscriptionRepository.FindExpiredActiveSubscriptionsAsync( now, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); var gracePeriodExpired = await subscriptionRepository.FindGracePeriodExpiredSubscriptionsAsync( now, request.GracePeriodDays, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); // 2. 更新订阅状态 foreach (var subscription in expiredActive) diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs index 225700f..70c852c 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class UpdateSubscriptionCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IMediator mediator) : IRequestHandler { /// public async Task Handle(UpdateSubscriptionCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs index 9f32016..58c2652 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Handlers/UpdateSubscriptionStatusCommandHandler.cs @@ -1,5 +1,4 @@ using MediatR; -using Microsoft.AspNetCore.Http; using TakeoutSaaS.Application.App.Subscriptions.Commands; using TakeoutSaaS.Application.App.Subscriptions.Dto; using TakeoutSaaS.Application.App.Subscriptions.Queries; @@ -12,20 +11,16 @@ namespace TakeoutSaaS.Application.App.Subscriptions.Handlers; /// public sealed class UpdateSubscriptionStatusCommandHandler( ISubscriptionRepository subscriptionRepository, - IHttpContextAccessor httpContextAccessor, IMediator mediator) : IRequestHandler { /// public async Task Handle(UpdateSubscriptionStatusCommand request, CancellationToken cancellationToken) { - var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor); - // 1. 查询订阅 var subscription = await subscriptionRepository.FindByIdAsync( request.SubscriptionId, - cancellationToken, - ignoreTenantFilter: ignoreTenantFilter); + cancellationToken); if (subscription == null) { diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs index c69c3e4..c602a31 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionDetailQuery.cs @@ -12,4 +12,9 @@ public sealed record GetSubscriptionDetailQuery : IRequest public long SubscriptionId { get; init; } + + /// + /// 是否包含已软删除数据。 + /// + public bool IncludeDeleted { get; init; } } diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs index e7a8cd4..3c83c3d 100644 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs +++ b/src/Application/TakeoutSaaS.Application/App/Subscriptions/Queries/GetSubscriptionListQuery.cs @@ -40,6 +40,11 @@ public sealed record GetSubscriptionListQuery : IRequest public bool? AutoRenew { get; init; } + /// + /// 是否包含已软删除数据。 + /// + public bool IncludeDeleted { get; init; } + /// /// 页码(从 1 开始)。 /// diff --git a/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs b/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs deleted file mode 100644 index 356ada2..0000000 --- a/src/Application/TakeoutSaaS.Application/App/Subscriptions/SubscriptionTenantAccess.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Http; -using System.Security.Claims; - -namespace TakeoutSaaS.Application.App.Subscriptions; - -internal static class SubscriptionTenantAccess -{ - private const string PermissionClaimType = "permission"; - private const string PlatformAdminRole = "PlatformAdmin"; - - public static bool ShouldIgnoreTenantFilter(IHttpContextAccessor httpContextAccessor) - { - var httpContext = httpContextAccessor.HttpContext; - if (httpContext == null) - { - // Background jobs / out-of-request execution should process across tenants. - return true; - } - - var user = httpContext.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - if (user.IsInRole(PlatformAdminRole)) - { - return true; - } - - var permissions = user.FindAll(PermissionClaimType) - .Select(c => c.Value?.Trim()) - .Where(v => !string.IsNullOrWhiteSpace(v)) - .ToHashSet(StringComparer.OrdinalIgnoreCase); - - // Platform-level tenant permissions imply cross-tenant visibility. - return permissions.Contains("tenant:read"); - } -} diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs index 8297b18..8db8ad0 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/CreateTenantManuallyCommandHandler.cs @@ -5,6 +5,7 @@ using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Enums; @@ -215,6 +216,7 @@ public sealed class CreateTenantManuallyCommandHandler( // 13. 创建租户管理员账号 var adminUser = new IdentityUser { + Portal = PortalType.Tenant, TenantId = tenant.Id, Account = normalizedAccount, DisplayName = request.AdminDisplayName.Trim(), @@ -234,7 +236,7 @@ public sealed class CreateTenantManuallyCommandHandler( TemplateCodes = new[] { "tenant-admin" } }, cancellationToken); - var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenant.Id, cancellationToken); + var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenant.Id, "tenant-admin", cancellationToken); if (tenantAdminRole != null) { await mediator.Send(new AssignUserRolesCommand diff --git a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs index 4d15ddf..6f573c9 100644 --- a/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/SelfRegisterTenantCommandHandler.cs @@ -4,6 +4,7 @@ using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Domain.Tenants.Entities; using TakeoutSaaS.Domain.Tenants.Enums; @@ -86,6 +87,7 @@ public sealed class SelfRegisterTenantCommandHandler( // 7. 使用用户自设密码创建管理员 var adminUser = new IdentityUser { + Portal = PortalType.Tenant, TenantId = tenant.Id, Account = normalizedAccount, DisplayName = string.IsNullOrWhiteSpace(request.AdminDisplayName) ? normalizedAccount : request.AdminDisplayName!.Trim(), @@ -109,7 +111,7 @@ public sealed class SelfRegisterTenantCommandHandler( }, cancellationToken); // 9. 绑定租户管理员角色 - var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenant.Id, cancellationToken); + var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenant.Id, "tenant-admin", cancellationToken); if (tenantAdminRole != null) { await mediator.Send(new AssignUserRolesCommand diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs index 922b7d3..2687d0c 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/CurrentUserProfile.cs @@ -1,3 +1,5 @@ +using TakeoutSaaS.Domain.Identity.Enums; + namespace TakeoutSaaS.Application.Identity.Contracts; /// @@ -5,6 +7,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed class CurrentUserProfile { + /// + /// 账号所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 用户 ID。 /// @@ -23,7 +30,7 @@ public sealed class CurrentUserProfile /// /// 所属租户 ID。 /// - public long TenantId { get; init; } + public long? TenantId { get; init; } /// /// 所属商户 ID(平台管理员为空)。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs index f25dc09..02b0b14 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionDto.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.Identity.Contracts; @@ -8,18 +9,17 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed class PermissionDto { + /// + /// 权限所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 权限 ID(雪花,序列化为字符串)。 /// [JsonConverter(typeof(SnowflakeIdJsonConverter))] public long Id { get; init; } - /// - /// 租户 ID(固定权限时为基准租户)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - /// /// 父级权限 ID。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs index a4dac6c..c96818f 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/PermissionTreeDto.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.Identity.Contracts; @@ -8,18 +9,17 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed record PermissionTreeDto { + /// + /// 权限所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 权限 ID(雪花,序列化为字符串)。 /// [JsonConverter(typeof(SnowflakeIdJsonConverter))] public long Id { get; init; } - /// - /// 租户 ID(雪花,序列化为字符串)。 - /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } - /// /// 父级权限 ID。 /// @@ -42,7 +42,7 @@ public sealed record PermissionTreeDto public string Name { get; init; } = string.Empty; /// - /// 权限编码(租户内唯一)。 + /// 权限编码(全局唯一)。 /// public string Code { get; init; } = string.Empty; diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDetailDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDetailDto.cs index e6983c1..cea3b8c 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDetailDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDetailDto.cs @@ -1,3 +1,5 @@ +using TakeoutSaaS.Domain.Identity.Enums; + namespace TakeoutSaaS.Application.Identity.Contracts; /// @@ -5,15 +7,20 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed record RoleDetailDto { + /// + /// 角色所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 角色 ID。 /// public long Id { get; init; } /// - /// 租户 ID。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。 /// - public long TenantId { get; init; } + public long? TenantId { get; init; } /// /// 角色名称。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs index 31119d3..812b08a 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/RoleDto.cs @@ -1,4 +1,5 @@ using System.Text.Json.Serialization; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Serialization; namespace TakeoutSaaS.Application.Identity.Contracts; @@ -8,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed class RoleDto { + /// + /// 角色所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 角色 ID(雪花,序列化为字符串)。 /// @@ -15,10 +21,10 @@ public sealed class RoleDto public long Id { get; init; } /// - /// 租户 ID。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。 /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } + [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] + public long? TenantId { get; init; } /// /// 角色名称。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserDetailDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserDetailDto.cs index 8fbe0ae..86a2c62 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserDetailDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserDetailDto.cs @@ -9,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed record UserDetailDto { + /// + /// 账号所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 用户 ID。 /// @@ -16,10 +21,10 @@ public sealed record UserDetailDto public long UserId { get; init; } /// - /// 租户 ID。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。 /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } + [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] + public long? TenantId { get; init; } /// /// 商户 ID。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserListItemDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserListItemDto.cs index c3f0abc..811a71d 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserListItemDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserListItemDto.cs @@ -9,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts; /// public sealed record UserListItemDto { + /// + /// 账号所属 Portal。 + /// + public PortalType Portal { get; init; } + /// /// 用户 ID。 /// @@ -16,10 +21,10 @@ public sealed record UserListItemDto public long UserId { get; init; } /// - /// 租户 ID。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。 /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } + [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] + public long? TenantId { get; init; } /// /// 登录账号。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs index 3506491..3055b59 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Contracts/UserPermissionDto.cs @@ -15,10 +15,10 @@ public sealed class UserPermissionDto public long UserId { get; init; } /// - /// 租户 ID(雪花,序列化为字符串)。 + /// 租户 ID(雪花,序列化为字符串;平台管理员为空)。 /// - [JsonConverter(typeof(SnowflakeIdJsonConverter))] - public long TenantId { get; init; } + [JsonConverter(typeof(NullableSnowflakeIdJsonConverter))] + public long? TenantId { get; init; } /// /// 商户 ID(雪花,序列化为字符串,可空)。 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs index df00a53..405d8e9 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/AssignUserRolesCommandHandler.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -21,14 +22,17 @@ public sealed class AssignUserRolesCommandHandler( /// 执行结果。 public async Task Handle(AssignUserRolesCommand request, CancellationToken cancellationToken) { - // 1. 获取租户上下文 + // 1. 固定为租户侧用户分配角色 + var portal = PortalType.Tenant; + + // 2. 获取租户上下文 var tenantId = tenantProvider.GetCurrentTenantId(); - // 2. 覆盖式绑定角色 - await userRoleRepository.ReplaceUserRolesAsync(tenantId, request.UserId, request.RoleIds, cancellationToken); + // 3. 覆盖式绑定角色 + await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, request.UserId, request.RoleIds, cancellationToken); await userRoleRepository.SaveChangesAsync(cancellationToken); - // 3. 返回执行结果 + // 4. 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs index 5a56dc0..f5c0a26 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BatchIdentityUserOperationCommandHandler.cs @@ -63,14 +63,14 @@ public sealed class BatchIdentityUserOperationCommandHandler( // 4. 查询目标用户集合 var includeDeleted = request.Operation == IdentityUserBatchOperation.Restore; - var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, isSuperAdmin, cancellationToken); + var users = await identityUserRepository.GetForUpdateByIdsAsync(tenantId, userIds, includeDeleted, cancellationToken); var usersById = users.ToDictionary(user => user.Id, user => user, EqualityComparer.Default); // 5. 预计算租户管理员约束 - var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); + var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenantId, "tenant-admin", cancellationToken); var tenantAdminUserIds = tenantAdminRole == null ? Array.Empty() - : (await userRoleRepository.GetByUserIdsAsync(tenantId, usersById.Keys, cancellationToken)) + : (await userRoleRepository.GetByUserIdsAsync(PortalType.Tenant, tenantId, usersById.Keys, cancellationToken)) .Where(x => x.RoleId == tenantAdminRole.Id) .Select(x => x.UserId) .Distinct() @@ -85,7 +85,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( IncludeDeleted = false, Page = 1, PageSize = 1 - }, isSuperAdmin, cancellationToken)).Total; + }, cancellationToken)).Total; var remainingActiveAdmins = activeAdminCount; // 6. 执行批量操作 @@ -193,6 +193,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( exportItems.AddRange(users.Select(user => new UserListItemDto { UserId = user.Id, + Portal = user.Portal, TenantId = user.TenantId, Account = user.Account, DisplayName = user.DisplayName, @@ -276,9 +277,10 @@ public sealed class BatchIdentityUserOperationCommandHandler( var result = new Dictionary(users.Count); // 2. 按租户分组,降低角色查询次数 - foreach (var group in users.GroupBy(user => user.TenantId)) + foreach (var group in users.GroupBy(user => new { user.Portal, user.TenantId })) { - var tenantId = group.Key; + var portal = group.Key.Portal; + var tenantId = group.Key.TenantId; var userIds = group.Select(user => user.Id).Distinct().ToArray(); if (userIds.Length == 0) { @@ -286,7 +288,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( } // 3. 查询用户角色映射 - var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var relations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken); if (relations.Count == 0) { continue; @@ -296,7 +298,7 @@ public sealed class BatchIdentityUserOperationCommandHandler( var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); var roles = roleIds.Length == 0 ? Array.Empty() - : await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + : await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer.Default); // 5. 组装用户角色编码列表 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs index 5200272..f2e5915 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/BindRolePermissionsCommandHandler.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -21,19 +22,22 @@ public sealed class BindRolePermissionsCommandHandler( /// 执行结果。 public async Task Handle(BindRolePermissionsCommand request, CancellationToken cancellationToken) { - // 1. 获取租户上下文 + // 1. 固定绑定租户侧角色权限 + var portal = PortalType.Tenant; + + // 2. 获取租户上下文 var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - // 2. 覆盖式绑定权限 + // 3. 覆盖式绑定权限 var distinctPermissionIds = request.PermissionIds .Where(id => id > 0) .Distinct() .ToArray(); - await rolePermissionRepository.ReplaceRolePermissionsAsync(tenantId, request.RoleId, distinctPermissionIds, cancellationToken); + await rolePermissionRepository.ReplaceRolePermissionsAsync(portal, tenantId, request.RoleId, distinctPermissionIds, cancellationToken); await rolePermissionRepository.SaveChangesAsync(cancellationToken); - // 3. 返回执行结果 + // 4. 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs index 0390a09..bf9163e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ChangeIdentityUserStatusCommandHandler.cs @@ -40,9 +40,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( } // 3. 查询用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; @@ -53,10 +51,13 @@ public sealed class ChangeIdentityUserStatusCommandHandler( return false; } - // 4. 校验租户管理员保留规则 - if (request.Status == IdentityUserStatus.Disabled && user.Status == IdentityUserStatus.Active) + // 4. 校验租户管理员保留规则(仅租户侧用户适用) + if (user.Portal == PortalType.Tenant + && request.Status == IdentityUserStatus.Disabled + && user.Status == IdentityUserStatus.Active + && user.TenantId.HasValue) { - await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken); + await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken); } // 5. 更新状态 @@ -114,17 +115,17 @@ public sealed class ChangeIdentityUserStatusCommandHandler( return true; } - private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken) + private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken) { // 1. 获取租户管理员角色 - var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); + var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenantId, "tenant-admin", cancellationToken); if (tenantAdminRole == null) { return; } // 2. 判断用户是否为租户管理员 - var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await userRoleRepository.GetByUserIdAsync(PortalType.Tenant, tenantId, userId, cancellationToken); if (!relations.Any(x => x.RoleId == tenantAdminRole.Id)) { return; @@ -140,7 +141,7 @@ public sealed class ChangeIdentityUserStatusCommandHandler( Page = 1, PageSize = 1 }; - var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken); + var result = await identityUserRepository.SearchPagedAsync(filter, cancellationToken); if (result.Total <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs index 807371a..91518ae 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CopyRoleTemplateCommandHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; @@ -36,16 +37,20 @@ public sealed class CopyRoleTemplateCommandHandler( // 2. 计算角色名称/编码与描述 var tenantId = tenantProvider.GetCurrentTenantId(); + + // 3. 固定复制为租户侧角色 + var portal = PortalType.Tenant; var roleCode = string.IsNullOrWhiteSpace(request.RoleCode) ? template.TemplateCode : request.RoleCode.Trim(); var roleName = string.IsNullOrWhiteSpace(request.RoleName) ? template.Name : request.RoleName.Trim(); var roleDescription = request.Description ?? template.Description; - // 1. 准备或更新角色主体(幂等创建)。 - var role = await roleRepository.FindByCodeAsync(roleCode, tenantId, cancellationToken); + // 4. 准备或更新角色主体(幂等创建)。 + var role = await roleRepository.FindByCodeAsync(portal, tenantId, roleCode, cancellationToken); if (role is null) { role = new Role { + Portal = portal, TenantId = tenantId, Name = roleName, Code = roleCode, @@ -68,8 +73,8 @@ public sealed class CopyRoleTemplateCommandHandler( await roleRepository.UpdateAsync(role, cancellationToken); } - // 3. 确保模板权限全部存在,不存在则按模板定义创建。 - var existingPermissions = await permissionRepository.GetByCodesAsync(tenantId, permissionCodes, cancellationToken); + // 5. 确保模板权限全部存在,不存在则按模板定义创建。 + var existingPermissions = await permissionRepository.GetByCodesAsync(permissionCodes, cancellationToken); var permissionMap = existingPermissions.ToDictionary(x => x.Code, StringComparer.OrdinalIgnoreCase); foreach (var code in permissionCodes) @@ -81,7 +86,6 @@ public sealed class CopyRoleTemplateCommandHandler( var permission = new Permission { - TenantId = tenantId, Name = code, Code = code, Description = code @@ -93,8 +97,8 @@ public sealed class CopyRoleTemplateCommandHandler( await roleRepository.SaveChangesAsync(cancellationToken); - // 4. 绑定缺失的权限,保留租户自定义的已有授权。 - var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken); + // 6. 绑定缺失的权限,保留租户自定义的已有授权。 + var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken); var existingPermissionIds = rolePermissions .Select(x => x.PermissionId) .ToHashSet(); @@ -108,6 +112,7 @@ public sealed class CopyRoleTemplateCommandHandler( { var relations = toAdd.Select(permissionId => new RolePermission { + Portal = portal, TenantId = tenantId, RoleId = role.Id, PermissionId = permissionId @@ -121,6 +126,7 @@ public sealed class CopyRoleTemplateCommandHandler( return new RoleDto { Id = role.Id, + Portal = role.Portal, TenantId = role.TenantId, Name = role.Name, Code = role.Code, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs index 00d38c9..839dc06 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateIdentityUserCommandHandler.cs @@ -76,7 +76,8 @@ public sealed class CreateIdentityUserCommandHandler( // 5. 校验角色合法性 if (roleIds.Length > 0) { - var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var portal = PortalType.Tenant; + var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); if (roles.Count != roleIds.Length) { throw new BusinessException(ErrorCodes.BadRequest, "角色列表包含无效项"); @@ -84,9 +85,11 @@ public sealed class CreateIdentityUserCommandHandler( } // 6. 创建用户实体 + var userPortal = PortalType.Tenant; var user = new IdentityUser { Id = idGenerator.NextId(), + Portal = userPortal, TenantId = tenantId, Account = account, DisplayName = displayName, @@ -139,7 +142,7 @@ public sealed class CreateIdentityUserCommandHandler( // 9. 绑定角色 if (roleIds.Length > 0) { - await userRoleRepository.ReplaceUserRolesAsync(tenantId, user.Id, roleIds, cancellationToken); + await userRoleRepository.ReplaceUserRolesAsync(userPortal, tenantId, user.Id, roleIds, cancellationToken); } // 10. 返回用户详情 @@ -147,6 +150,7 @@ public sealed class CreateIdentityUserCommandHandler( return detail ?? new UserDetailDto { UserId = user.Id, + Portal = user.Portal, TenantId = user.TenantId, MerchantId = user.MerchantId, Account = user.Account, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateMenuCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateMenuCommandHandler.cs index eeda175..3c78e2b 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateMenuCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateMenuCommandHandler.cs @@ -3,10 +3,10 @@ using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -14,8 +14,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 创建菜单处理器。 /// public sealed class CreateMenuCommandHandler( - IMenuRepository menuRepository, - ITenantProvider tenantProvider) + IMenuRepository menuRepository) : IRequestHandler { /// @@ -27,11 +26,11 @@ public sealed class CreateMenuCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止新增"); } - // 2. 构造实体 - var tenantId = tenantProvider.GetCurrentTenantId(); + // 2. 构造实体(仅允许维护管理端菜单) + var portal = PortalType.Admin; var entity = new MenuDefinition { - TenantId = tenantId, + Portal = portal, ParentId = request.ParentId, Name = request.Name.Trim(), Path = request.Path.Trim(), diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs index 8cf4ec2..2c88bad 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreatePermissionCommandHandler.cs @@ -2,10 +2,10 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -13,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 创建权限处理器。 /// public sealed class CreatePermissionCommandHandler( - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider) + IPermissionRepository permissionRepository) : IRequestHandler { /// @@ -31,10 +30,7 @@ public sealed class CreatePermissionCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "权限已固定,禁止新增"); } - // 2. 获取租户上下文 - var tenantId = tenantProvider.GetCurrentTenantId(); - - // 3. 构建权限实体 + // 2. 构建权限实体 var normalizedType = string.IsNullOrWhiteSpace(request.Type) ? "leaf" : request.Type.Trim().ToLowerInvariant(); @@ -43,7 +39,7 @@ public sealed class CreatePermissionCommandHandler( var sortOrder = request.SortOrder < 0 ? 0 : request.SortOrder; var permission = new Permission { - TenantId = tenantId, + Portal = PortalType.Admin, ParentId = parentId, SortOrder = sortOrder, Type = normalizedType, @@ -52,15 +48,15 @@ public sealed class CreatePermissionCommandHandler( Description = request.Description }; - // 4. 持久化 + // 3. 持久化 await permissionRepository.AddAsync(permission, cancellationToken); await permissionRepository.SaveChangesAsync(cancellationToken); - // 5. 返回 DTO + // 4. 返回 DTO return new PermissionDto { + Portal = permission.Portal, Id = permission.Id, - TenantId = permission.TenantId, ParentId = permission.ParentId, SortOrder = permission.SortOrder, Type = permission.Type, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs index 232b8ab..9126878 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/CreateRoleCommandHandler.cs @@ -2,6 +2,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; @@ -25,10 +26,13 @@ public sealed class CreateRoleCommandHandler( /// 创建后的角色 DTO。 public async Task Handle(CreateRoleCommand request, CancellationToken cancellationToken) { - // 1. 获取租户上下文 + // 1. 固定创建租户侧角色 + var portal = PortalType.Tenant; + + // 2. 获取租户上下文 var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - // 2. 归一化输入并校验唯一 + // 3. 归一化输入并校验唯一 var name = request.Name?.Trim() ?? string.Empty; var code = request.Code?.Trim() ?? string.Empty; if (string.IsNullOrWhiteSpace(name) || string.IsNullOrWhiteSpace(code)) @@ -36,29 +40,31 @@ public sealed class CreateRoleCommandHandler( throw new BusinessException(ErrorCodes.BadRequest, "角色名称与编码不能为空"); } - var exists = await roleRepository.FindByCodeAsync(code, tenantId, cancellationToken); + var exists = await roleRepository.FindByCodeAsync(portal, tenantId, code, cancellationToken); if (exists is not null) { throw new BusinessException(ErrorCodes.Conflict, "角色编码已存在"); } - // 3. 构建角色实体 + // 4. 构建角色实体 var role = new Role { + Portal = portal, TenantId = tenantId, Name = name, Code = code, Description = request.Description }; - // 4. 持久化 + // 5. 持久化 await roleRepository.AddAsync(role, cancellationToken); await roleRepository.SaveChangesAsync(cancellationToken); - // 5. 返回 DTO + // 6. 返回 DTO return new RoleDto { Id = role.Id, + Portal = role.Portal, TenantId = role.TenantId, Name = role.Name, Code = role.Code, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs index e452305..dd0cb86 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteIdentityUserCommandHandler.cs @@ -40,9 +40,7 @@ public sealed class DeleteIdentityUserCommandHandler( } // 3. 查询用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return false; @@ -53,10 +51,10 @@ public sealed class DeleteIdentityUserCommandHandler( return false; } - // 4. 校验租户管理员保留规则 - if (user.Status == IdentityUserStatus.Active) + // 4. 校验租户管理员保留规则(仅租户侧用户适用) + if (user.Portal == PortalType.Tenant && user.Status == IdentityUserStatus.Active && user.TenantId.HasValue) { - await EnsureNotLastActiveTenantAdminAsync(user.TenantId, user.Id, isSuperAdmin, cancellationToken); + await EnsureNotLastActiveTenantAdminAsync(user.TenantId.Value, user.Id, cancellationToken); } // 5. 构建操作日志消息 @@ -88,17 +86,17 @@ public sealed class DeleteIdentityUserCommandHandler( return true; } - private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, bool ignoreTenantFilter, CancellationToken cancellationToken) + private async Task EnsureNotLastActiveTenantAdminAsync(long tenantId, long userId, CancellationToken cancellationToken) { // 1. 获取租户管理员角色 - var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenantId, cancellationToken); + var tenantAdminRole = await roleRepository.FindByCodeAsync(PortalType.Tenant, tenantId, "tenant-admin", cancellationToken); if (tenantAdminRole == null) { return; } // 2. 判断用户是否为租户管理员 - var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await userRoleRepository.GetByUserIdAsync(PortalType.Tenant, tenantId, userId, cancellationToken); if (!relations.Any(x => x.RoleId == tenantAdminRole.Id)) { return; @@ -114,7 +112,7 @@ public sealed class DeleteIdentityUserCommandHandler( Page = 1, PageSize = 1 }; - var result = await identityUserRepository.SearchPagedAsync(filter, ignoreTenantFilter, cancellationToken); + var result = await identityUserRepository.SearchPagedAsync(filter, cancellationToken); if (result.Total <= 1) { throw new BusinessException(ErrorCodes.Conflict, "至少保留一个管理员"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteMenuCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteMenuCommandHandler.cs index 6e8da19..7772f92 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteMenuCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteMenuCommandHandler.cs @@ -1,10 +1,10 @@ using MediatR; using TakeoutSaaS.Application.Identity; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -12,8 +12,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 删除菜单处理器。 /// public sealed class DeleteMenuCommandHandler( - IMenuRepository menuRepository, - ITenantProvider tenantProvider) + IMenuRepository menuRepository) : IRequestHandler { /// @@ -25,9 +24,9 @@ public sealed class DeleteMenuCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止删除"); } - // 2. 删除目标及可能的孤儿由外层保证 - var tenantId = tenantProvider.GetCurrentTenantId(); - await menuRepository.DeleteAsync(request.Id, tenantId, cancellationToken); + // 2. 删除目标及可能的孤儿由外层保证(仅维护管理端菜单) + var portal = PortalType.Admin; + await menuRepository.DeleteAsync(request.Id, portal, cancellationToken); // 3. 持久化 await menuRepository.SaveChangesAsync(cancellationToken); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs index fb31f03..cf2b371 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeletePermissionCommandHandler.cs @@ -3,7 +3,6 @@ using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -11,8 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 删除权限处理器。 /// public sealed class DeletePermissionCommandHandler( - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider) + IPermissionRepository permissionRepository) : IRequestHandler { /// @@ -29,14 +27,11 @@ public sealed class DeletePermissionCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "权限已固定,禁止删除"); } - // 2. 获取租户上下文 - var tenantId = tenantProvider.GetCurrentTenantId(); - - // 3. 删除权限 - await permissionRepository.DeleteAsync(request.PermissionId, tenantId, cancellationToken); + // 2. 删除权限 + await permissionRepository.DeleteAsync(request.PermissionId, cancellationToken); await permissionRepository.SaveChangesAsync(cancellationToken); - // 4. 返回执行结果 + // 3. 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs index d1e0e4f..414f711 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/DeleteRoleCommandHandler.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -21,14 +22,17 @@ public sealed class DeleteRoleCommandHandler( /// 执行结果。 public async Task Handle(DeleteRoleCommand request, CancellationToken cancellationToken) { - // 1. 获取租户上下文 + // 1. 固定删除租户侧角色 + var portal = PortalType.Tenant; + + // 2. 获取租户上下文 var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - // 2. 删除角色 - await roleRepository.DeleteAsync(request.RoleId, tenantId, cancellationToken); + // 3. 删除角色 + await roleRepository.DeleteAsync(portal, tenantId, request.RoleId, cancellationToken); await roleRepository.SaveChangesAsync(cancellationToken); - // 3. 返回执行结果 + // 4. 返回执行结果 return true; } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs index 87d1aad..b23662a 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetIdentityUserDetailQueryHandler.cs @@ -33,17 +33,9 @@ public sealed class GetIdentityUserDetailQueryHandler( var isSuperAdmin = IdentityUserAccess.IsSuperAdmin(operatorProfile); // 2. 查询用户实体 - IdentityUser? user; - if (request.IncludeDeleted) - { - user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken); - } - else - { - user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); - } + var user = request.IncludeDeleted + ? await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken) + : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { @@ -56,34 +48,39 @@ public sealed class GetIdentityUserDetailQueryHandler( } // 3. 加载角色与权限 - var roleRelations = await userRoleRepository.GetByUserIdAsync(user.TenantId, user.Id, cancellationToken); + var portal = user.Portal; + var tenantId = user.TenantId; + + // 4. 查询用户角色关系 + var roleRelations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, user.Id, cancellationToken); var roleIds = roleRelations.Select(x => x.RoleId).Distinct().ToArray(); var roles = roleIds.Length == 0 ? Array.Empty() - : await roleRepository.GetByIdsAsync(user.TenantId, roleIds, cancellationToken); + : await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); var roleCodes = roles.Select(x => x.Code) .Where(code => !string.IsNullOrWhiteSpace(code)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); var permissionIds = roleIds.Length == 0 ? Array.Empty() - : (await rolePermissionRepository.GetByRoleIdsAsync(user.TenantId, roleIds, cancellationToken)) + : (await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken)) .Select(x => x.PermissionId) .Distinct() .ToArray(); var permissions = permissionIds.Length == 0 ? Array.Empty() - : (await permissionRepository.GetByIdsAsync(user.TenantId, permissionIds, cancellationToken)) + : (await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken)) .Select(x => x.Code) .Where(code => !string.IsNullOrWhiteSpace(code)) .Distinct(StringComparer.OrdinalIgnoreCase) .ToArray(); - // 4. 组装详情 DTO + // 5. 组装详情 DTO var now = DateTime.UtcNow; return new UserDetailDto { UserId = user.Id, + Portal = user.Portal, TenantId = user.TenantId, MerchantId = user.MerchantId, Account = user.Account, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs index 915f386..cefd895 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/GetUserPermissionsQueryHandler.cs @@ -1,6 +1,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -22,6 +23,7 @@ public sealed class GetUserPermissionsQueryHandler( public async Task Handle(GetUserPermissionsQuery request, CancellationToken cancellationToken) { // 1. 获取租户并查询用户 + var portal = PortalType.Tenant; var tenantId = tenantProvider.GetCurrentTenantId(); var user = await identityUserRepository.FindByIdAsync(request.UserId, cancellationToken); if (user == null || user.TenantId != tenantId) @@ -30,8 +32,8 @@ public sealed class GetUserPermissionsQueryHandler( } // 2. 解析角色与权限 - var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); - var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + var roleCodes = await ResolveUserRolesAsync(portal, tenantId, user.Id, cancellationToken); + var permissionCodes = await ResolveUserPermissionsAsync(portal, tenantId, user.Id, cancellationToken); // 3. 返回用户权限概览 return new UserPermissionDto @@ -47,10 +49,10 @@ public sealed class GetUserPermissionsQueryHandler( }; } - private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) + private async Task ResolveUserRolesAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken) { // 1. 查询用户角色关系 - var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken); var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { @@ -58,14 +60,14 @@ public sealed class GetUserPermissionsQueryHandler( } // 2. 查询角色编码 - var roles = await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } - private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) + private async Task ResolveUserPermissionsAsync(PortalType portal, long tenantId, long userId, CancellationToken cancellationToken) { // 1. 查询用户角色关系 - var relations = await userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken); var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { @@ -73,7 +75,7 @@ public sealed class GetUserPermissionsQueryHandler( } // 2. 查询角色-权限关系 - var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var rolePermissions = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); if (permissionIds.Length == 0) { @@ -81,7 +83,7 @@ public sealed class GetUserPermissionsQueryHandler( } // 3. 查询权限编码 - var permissions = await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissions = await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListMenusQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListMenusQueryHandler.cs index 08b14b6..43d4aad 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListMenusQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ListMenusQueryHandler.cs @@ -1,8 +1,8 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -10,18 +10,17 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 菜单列表查询处理器。 /// public sealed class ListMenusQueryHandler( - IMenuRepository menuRepository, - ITenantProvider tenantProvider) + IMenuRepository menuRepository) : IRequestHandler> { /// public async Task> Handle(ListMenusQuery request, CancellationToken cancellationToken) { - // 1. 获取租户 - var tenantId = tenantProvider.GetCurrentTenantId(); + // 1. 固定读取管理端菜单 + var portal = PortalType.Admin; // 2. 查询列表 - var entities = await menuRepository.GetByTenantAsync(tenantId, cancellationToken); + var entities = await menuRepository.GetByPortalAsync(portal, cancellationToken); // 3. 映射 DTO var items = entities.Select(MenuMapper.ToDto).ToList(); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuDetailQueryHandler.cs index e681bc3..3f07718 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuDetailQueryHandler.cs @@ -1,8 +1,8 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -10,18 +10,17 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 菜单详情查询处理器。 /// public sealed class MenuDetailQueryHandler( - IMenuRepository menuRepository, - ITenantProvider tenantProvider) + IMenuRepository menuRepository) : IRequestHandler { /// public async Task Handle(MenuDetailQuery request, CancellationToken cancellationToken) { - // 1. 获取租户 - var tenantId = tenantProvider.GetCurrentTenantId(); + // 1. 固定读取管理端菜单 + var portal = PortalType.Admin; // 2. 查询实体 - var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken); + var entity = await menuRepository.FindByIdAsync(request.Id, portal, cancellationToken); if (entity is null) { return null; diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuMapper.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuMapper.cs index 9d68488..dfb9922 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuMapper.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/MenuMapper.cs @@ -1,6 +1,7 @@ using System.Text.Json; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -79,23 +80,22 @@ internal static class MenuMapper /// 构建或更新菜单实体并返回 DTO。 /// /// 已存在的菜单实体。 - /// 租户 ID。 + /// Portal 类型。 /// 菜单名称。 /// 菜单 DTO 载荷。 /// 菜单定义 DTO。 - public static MenuDefinitionDto FromCommand(MenuDefinition? existing, long tenantId, string name, MenuDefinitionDto payload) + public static MenuDefinitionDto FromCommand(MenuDefinition? existing, PortalType portal, string name, MenuDefinitionDto payload) { // 1. 构造实体 var entity = existing ?? new MenuDefinition { - TenantId = tenantId, - CreatedAt = DateTime.UtcNow + Portal = portal }; - // // 填充字段 + // 2. 填充字段 FillEntity(entity, payload); - // 2. 返回 DTO 映射 + // 3. 返回 DTO 映射 return ToDto(entity); } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs index e134b54..7043df1 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/PermissionTreeQueryHandler.cs @@ -2,7 +2,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Repositories; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -10,8 +9,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 权限树查询处理器。 /// public sealed class PermissionTreeQueryHandler( - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider) + IPermissionRepository permissionRepository) : IRequestHandler> { /// @@ -22,17 +20,16 @@ public sealed class PermissionTreeQueryHandler( /// 权限树列表。 public async Task> Handle(PermissionTreeQuery request, CancellationToken cancellationToken) { - // 1. 获取租户上下文并查询权限 - var tenantId = tenantProvider.GetCurrentTenantId(); - var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + // 1. 查询权限(按 Portal) + var permissions = await permissionRepository.SearchAsync(request.Portal, request.Keyword, cancellationToken); // 2. 构建节点映射与父子分组 var nodeMap = permissions.ToDictionary( x => x.Id, x => new PermissionTreeDto { + Portal = x.Portal, Id = x.Id, - TenantId = x.TenantId, ParentId = x.ParentId, SortOrder = x.SortOrder, Type = x.Type, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetAdminPasswordByTokenCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetAdminPasswordByTokenCommandHandler.cs index ad9bc80..3a8b413 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetAdminPasswordByTokenCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetAdminPasswordByTokenCommandHandler.cs @@ -47,8 +47,8 @@ public sealed class ResetAdminPasswordByTokenCommandHandler( throw new BusinessException(ErrorCodes.BadRequest, "重置链接无效或已过期"); } - // 3. 获取用户(可更新,忽略租户过滤器)并写入新密码哈希 - var user = await userRepository.GetForUpdateIgnoringTenantAsync(userId.Value, cancellationToken) + // 3. 获取用户(可更新)并写入新密码哈希 + var user = await userRepository.GetForUpdateAsync(userId.Value, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); user.PasswordHash = passwordHasher.HashPassword(user, password); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs index 6f57f76..6289d70 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/ResetIdentityUserPasswordCommandHandler.cs @@ -40,9 +40,7 @@ public sealed class ResetIdentityUserPasswordCommandHandler( } // 3. 查询用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { throw new BusinessException(ErrorCodes.NotFound, "用户不存在"); diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs index 525341a..b00725f 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RestoreIdentityUserCommandHandler.cs @@ -37,7 +37,7 @@ public sealed class RestoreIdentityUserCommandHandler( } // 3. 查询用户实体(包含已删除) - var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(currentTenantId, request.UserId, isSuperAdmin, cancellationToken); + var user = await identityUserRepository.GetForUpdateIncludingDeletedAsync(request.UserId, cancellationToken); if (user == null) { return false; diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs index c2e0730..104c7d6 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/RoleDetailQueryHandler.cs @@ -1,6 +1,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -19,29 +20,32 @@ public sealed class RoleDetailQueryHandler( /// public async Task Handle(RoleDetailQuery request, CancellationToken cancellationToken) { - // 1. 获取租户上下文并查询角色 + // 1. 固定查询租户侧角色详情 + var portal = PortalType.Tenant; + + // 2. 获取租户上下文并查询角色 var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken); + var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken); if (role is null) { return null; } - // 2. 查询角色权限关系 - var relations = await rolePermissionRepository.GetByRoleIdsAsync(tenantId, new[] { role.Id }, cancellationToken); + // 3. 查询角色权限关系 + var relations = await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, new[] { role.Id }, cancellationToken); var permissionIds = relations.Select(x => x.PermissionId).ToArray(); - // 3. 拉取权限实体 + // 4. 拉取权限实体 var permissions = permissionIds.Length == 0 ? Array.Empty() - : await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + : await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); - // 4. 映射 DTO + // 5. 映射 DTO var permissionDtos = permissions .Select(x => new PermissionDto { + Portal = x.Portal, Id = x.Id, - TenantId = x.TenantId, ParentId = x.ParentId, SortOrder = x.SortOrder, Type = x.Type, @@ -54,6 +58,7 @@ public sealed class RoleDetailQueryHandler( return new RoleDetailDto { Id = role.Id, + Portal = role.Portal, TenantId = role.TenantId, Name = role.Name, Code = role.Code, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs index 887136e..5043b29 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchIdentityUsersQueryHandler.cs @@ -58,7 +58,7 @@ public sealed class SearchIdentityUsersQueryHandler( }; // 4. 执行分页查询 - var (items, total) = await identityUserRepository.SearchPagedAsync(filter, isSuperAdmin, cancellationToken); + var (items, total) = await identityUserRepository.SearchPagedAsync(filter, cancellationToken); if (items.Count == 0) { return new PagedResult(Array.Empty(), request.Page, request.PageSize, total); @@ -72,6 +72,7 @@ public sealed class SearchIdentityUsersQueryHandler( var dtos = items.Select(user => new UserListItemDto { UserId = user.Id, + Portal = user.Portal, TenantId = user.TenantId, Account = user.Account, DisplayName = user.DisplayName, @@ -103,10 +104,11 @@ public sealed class SearchIdentityUsersQueryHandler( // 1. 预分配字典容量 var result = new Dictionary(users.Count); - // 2. 按租户分组,降低角色查询次数 - foreach (var group in users.GroupBy(user => user.TenantId)) + // 2. 按 Portal + TenantId 分组,降低角色查询次数 + foreach (var group in users.GroupBy(user => new { user.Portal, user.TenantId })) { - var tenantId = group.Key; + var portal = group.Key.Portal; + var tenantId = group.Key.TenantId; var userIds = group.Select(user => user.Id).Distinct().ToArray(); if (userIds.Length == 0) { @@ -114,7 +116,7 @@ public sealed class SearchIdentityUsersQueryHandler( } // 3. 查询用户角色映射 - var relations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var relations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken); if (relations.Count == 0) { continue; @@ -124,7 +126,7 @@ public sealed class SearchIdentityUsersQueryHandler( var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); var roles = roleIds.Length == 0 ? Array.Empty() - : await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + : await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer.Default); // 5. 组装用户角色编码列表 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs index e5e061c..5bf0762 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchPermissionsQueryHandler.cs @@ -3,7 +3,6 @@ using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -11,8 +10,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 权限分页查询处理器。 /// public sealed class SearchPermissionsQueryHandler( - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider) + IPermissionRepository permissionRepository) : IRequestHandler> { /// @@ -23,9 +21,8 @@ public sealed class SearchPermissionsQueryHandler( /// 分页结果。 public async Task> Handle(SearchPermissionsQuery request, CancellationToken cancellationToken) { - // 1. 获取租户上下文并查询权限 - var tenantId = tenantProvider.GetCurrentTenantId(); - var permissions = await permissionRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + // 1. 查询权限(按 Portal) + var permissions = await permissionRepository.SearchAsync(request.Portal, request.Keyword, cancellationToken); // 2. 排序 var sorted = request.SortBy?.ToLowerInvariant() switch @@ -59,8 +56,8 @@ public sealed class SearchPermissionsQueryHandler( // 4. 映射 DTO var items = paged.Select(permission => new PermissionDto { + Portal = permission.Portal, Id = permission.Id, - TenantId = permission.TenantId, ParentId = permission.ParentId, SortOrder = permission.SortOrder, Type = permission.Type, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs index 2c247bb..8868ae8 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchRolesQueryHandler.cs @@ -1,6 +1,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -23,11 +24,14 @@ public sealed class SearchRolesQueryHandler( /// 分页结果。 public async Task> Handle(SearchRolesQuery request, CancellationToken cancellationToken) { - // 1. 获取租户上下文并查询角色 - var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - var roles = await roleRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); + // 1. 固定查询租户侧角色 + var portal = PortalType.Tenant; - // 2. 排序 + // 2. 获取租户上下文并查询角色 + var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); + var roles = await roleRepository.SearchAsync(portal, tenantId, request.Keyword, cancellationToken); + + // 3. 排序 var sorted = request.SortBy?.ToLowerInvariant() switch { "name" => request.SortDescending @@ -38,23 +42,24 @@ public sealed class SearchRolesQueryHandler( : roles.OrderBy(x => x.CreatedAt) }; - // 3. 分页 + // 4. 分页 var paged = sorted .Skip((request.Page - 1) * request.PageSize) .Take(request.PageSize) .ToList(); - // 4. 映射 DTO + // 5. 映射 DTO var items = paged.Select(role => new RoleDto { Id = role.Id, + Portal = role.Portal, TenantId = role.TenantId, Name = role.Name, Code = role.Code, Description = role.Description }).ToList(); - // 5. 返回分页结果 + // 6. 返回分页结果 return new PagedResult(items, request.Page, request.PageSize, roles.Count); } } diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs index 4184be8..c99912e 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/SearchUserPermissionsQueryHandler.cs @@ -1,6 +1,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Queries; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Results; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -23,6 +24,7 @@ public sealed class SearchUserPermissionsQueryHandler( public async Task> Handle(SearchUserPermissionsQuery request, CancellationToken cancellationToken) { // 1. 获取租户并查询用户 + var portal = PortalType.Tenant; var tenantId = tenantProvider.GetCurrentTenantId(); var users = await identityUserRepository.SearchAsync(tenantId, request.Keyword, cancellationToken); @@ -34,7 +36,7 @@ public sealed class SearchUserPermissionsQueryHandler( .ToList(); // 3. 解析角色与权限 - var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken); + var resolved = await ResolveRolesAndPermissionsAsync(portal, tenantId, paged, cancellationToken); var items = paged.Select(user => new UserPermissionDto { UserId = user.Id, @@ -70,31 +72,32 @@ public sealed class SearchUserPermissionsQueryHandler( } private async Task> ResolveRolesAndPermissionsAsync( + PortalType portal, long tenantId, IReadOnlyCollection users, CancellationToken cancellationToken) { // 1. 查询用户角色关系 var userIds = users.Select(x => x.Id).ToArray(); - var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var userRoleRelations = await userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken); var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); // 2. 查询角色信息 var roles = roleIds.Length == 0 ? Array.Empty() - : await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + : await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); // 3. 查询角色-权限关系 var rolePermissions = roleIds.Length == 0 ? Array.Empty() - : await rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + : await rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); // 4. 查询权限详情 var permissions = permissionIds.Length == 0 ? Array.Empty() - : await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + : await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); var rolePermissionsLookup = rolePermissions diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs index fefa002..1ee0f1c 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateIdentityUserCommandHandler.cs @@ -6,6 +6,7 @@ using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Application.Identity.Events; using TakeoutSaaS.Application.Identity.Queries; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; @@ -43,9 +44,7 @@ public sealed class UpdateIdentityUserCommandHandler( } // 3. 获取用户实体 - var user = isSuperAdmin - ? await identityUserRepository.GetForUpdateIgnoringTenantAsync(request.UserId, cancellationToken) - : await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); + var user = await identityUserRepository.GetForUpdateAsync(request.UserId, cancellationToken); if (user == null) { return null; @@ -57,28 +56,37 @@ public sealed class UpdateIdentityUserCommandHandler( } // 4. 规范化输入并校验唯一性 + var portal = user.Portal; + var tenantId = user.TenantId; + if (portal == PortalType.Tenant && (!tenantId.HasValue || tenantId.Value == 0)) + { + throw new BusinessException(ErrorCodes.InternalServerError, "用户缺少有效的租户标识"); + } + var displayName = request.DisplayName.Trim(); var phone = string.IsNullOrWhiteSpace(request.Phone) ? null : request.Phone.Trim(); var email = string.IsNullOrWhiteSpace(request.Email) ? null : request.Email.Trim(); var roleIds = request.RoleIds == null ? null : ParseIds(request.RoleIds, "角色"); - if (!string.IsNullOrWhiteSpace(phone) + if (portal == PortalType.Tenant + && !string.IsNullOrWhiteSpace(phone) && !string.Equals(phone, user.Phone, StringComparison.OrdinalIgnoreCase) - && await identityUserRepository.ExistsByPhoneAsync(user.TenantId, phone, user.Id, cancellationToken)) + && await identityUserRepository.ExistsByPhoneAsync(tenantId!.Value, phone, user.Id, cancellationToken)) { throw new BusinessException(ErrorCodes.Conflict, "手机号已存在"); } - if (!string.IsNullOrWhiteSpace(email) + if (portal == PortalType.Tenant + && !string.IsNullOrWhiteSpace(email) && !string.Equals(email, user.Email, StringComparison.OrdinalIgnoreCase) - && await identityUserRepository.ExistsByEmailAsync(user.TenantId, email, user.Id, cancellationToken)) + && await identityUserRepository.ExistsByEmailAsync(tenantId!.Value, email, user.Id, cancellationToken)) { throw new BusinessException(ErrorCodes.Conflict, "邮箱已存在"); } if (roleIds is { Length: > 0 }) { - var roles = await roleRepository.GetByIdsAsync(user.TenantId, roleIds, cancellationToken); + var roles = await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); if (roles.Count != roleIds.Length) { throw new BusinessException(ErrorCodes.BadRequest, "角色列表包含无效项"); @@ -134,7 +142,7 @@ public sealed class UpdateIdentityUserCommandHandler( // 8. 覆盖角色绑定(仅当显式传入时) if (roleIds != null) { - await userRoleRepository.ReplaceUserRolesAsync(user.TenantId, user.Id, roleIds, cancellationToken); + await userRoleRepository.ReplaceUserRolesAsync(portal, tenantId, user.Id, roleIds, cancellationToken); } // 9. 返回用户详情 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateMenuCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateMenuCommandHandler.cs index ad18b6d..6fc94ab 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateMenuCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateMenuCommandHandler.cs @@ -2,10 +2,10 @@ using MediatR; using TakeoutSaaS.Application.Identity; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -13,8 +13,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 更新菜单处理器。 /// public sealed class UpdateMenuCommandHandler( - IMenuRepository menuRepository, - ITenantProvider tenantProvider) + IMenuRepository menuRepository) : IRequestHandler { /// @@ -26,9 +25,9 @@ public sealed class UpdateMenuCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "菜单已固定,禁止修改"); } - // 2. 校验存在 - var tenantId = tenantProvider.GetCurrentTenantId(); - var entity = await menuRepository.FindByIdAsync(request.Id, tenantId, cancellationToken) + // 2. 校验存在(仅维护管理端菜单) + var portal = PortalType.Admin; + var entity = await menuRepository.FindByIdAsync(request.Id, portal, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "菜单不存在"); // 3. 更新字段 diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs index 294045d..55a6ba3 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdatePermissionCommandHandler.cs @@ -4,7 +4,6 @@ using TakeoutSaaS.Application.Identity.Contracts; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Constants; using TakeoutSaaS.Shared.Abstractions.Exceptions; -using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.Identity.Handlers; @@ -12,8 +11,7 @@ namespace TakeoutSaaS.Application.Identity.Handlers; /// 更新权限处理器。 /// public sealed class UpdatePermissionCommandHandler( - IPermissionRepository permissionRepository, - ITenantProvider tenantProvider) + IPermissionRepository permissionRepository) : IRequestHandler { /// @@ -30,9 +28,8 @@ public sealed class UpdatePermissionCommandHandler( throw new BusinessException(ErrorCodes.Forbidden, "权限已固定,禁止修改"); } - // 2. 获取租户上下文并查询权限 - var tenantId = tenantProvider.GetCurrentTenantId(); - var permission = await permissionRepository.FindByIdAsync(request.PermissionId, tenantId, cancellationToken); + // 2. 查询权限 + var permission = await permissionRepository.FindByIdAsync(request.PermissionId, cancellationToken); if (permission == null) { return null; @@ -60,8 +57,8 @@ public sealed class UpdatePermissionCommandHandler( // 5. 返回 DTO return new PermissionDto { + Portal = permission.Portal, Id = permission.Id, - TenantId = permission.TenantId, ParentId = permission.ParentId, SortOrder = permission.SortOrder, Type = permission.Type, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs index e8a6f49..53a95c5 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Handlers/UpdateRoleCommandHandler.cs @@ -1,6 +1,7 @@ using MediatR; using TakeoutSaaS.Application.Identity.Commands; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Shared.Abstractions.Tenancy; @@ -22,26 +23,30 @@ public sealed class UpdateRoleCommandHandler( /// 更新后的角色 DTO 或 null。 public async Task Handle(UpdateRoleCommand request, CancellationToken cancellationToken) { - // 1. 获取租户上下文并查询角色 + // 1. 固定更新租户侧角色 + var portal = PortalType.Tenant; + + // 2. 获取租户上下文并查询角色 var tenantId = request.TenantId ?? tenantProvider.GetCurrentTenantId(); - var role = await roleRepository.FindByIdAsync(request.RoleId, tenantId, cancellationToken); + var role = await roleRepository.FindByIdAsync(portal, tenantId, request.RoleId, cancellationToken); if (role == null) { return null; } - // 2. 更新字段 + // 3. 更新字段 role.Name = request.Name; role.Description = request.Description; - // 3. 持久化 + // 4. 持久化 await roleRepository.UpdateAsync(role, cancellationToken); await roleRepository.SaveChangesAsync(cancellationToken); - // 4. 返回 DTO + // 5. 返回 DTO return new RoleDto { Id = role.Id, + Portal = role.Portal, TenantId = role.TenantId, Name = role.Name, Code = role.Code, diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs index 5847f02..b8485f8 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/PermissionTreeQuery.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Application.Identity.Queries; @@ -8,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Queries; /// public sealed class PermissionTreeQuery : IRequest> { + /// + /// Portal 类型。 + /// + public PortalType Portal { get; init; } = PortalType.Admin; + /// /// 关键字(可选)。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs index 1987a5a..fba63d2 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Queries/SearchPermissionsQuery.cs @@ -1,5 +1,6 @@ using MediatR; using TakeoutSaaS.Application.Identity.Contracts; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Results; namespace TakeoutSaaS.Application.Identity.Queries; @@ -9,6 +10,11 @@ namespace TakeoutSaaS.Application.Identity.Queries; /// public sealed class SearchPermissionsQuery : IRequest> { + /// + /// Portal 类型。 + /// + public PortalType Portal { get; init; } = PortalType.Admin; + /// /// 搜索关键字。 /// diff --git a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs index 9691306..13008c3 100644 --- a/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs +++ b/src/Application/TakeoutSaaS.Application/Identity/Services/AdminAuthService.cs @@ -197,8 +197,7 @@ public sealed class AdminAuthService( // 1. 读取档案以获取权限 var profile = await GetProfileAsync(userId, cancellationToken); // 2. 读取菜单定义 - var tenantId = _tenantProvider.GetCurrentTenantId(); - var definitions = await _menuRepository.GetByTenantAsync(tenantId, cancellationToken); + var definitions = await _menuRepository.GetByPortalAsync(profile.Portal, cancellationToken); // 3. 生成菜单树 var menu = BuildMenuTree(definitions, profile.Permissions); @@ -217,8 +216,8 @@ public sealed class AdminAuthService( return null; } - var roleCodes = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); - var permissionCodes = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + var roleCodes = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken); + var permissionCodes = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken); return new UserPermissionDto { @@ -265,7 +264,7 @@ public sealed class AdminAuthService( .Take(pageSize) .ToList(); - var resolved = await ResolveRolesAndPermissionsAsync(tenantId, paged, cancellationToken); + var resolved = await ResolveRolesAndPermissionsAsync(PortalType.Tenant, tenantId, paged, cancellationToken); var items = paged.Select(user => new UserPermissionDto { UserId = user.Id, @@ -283,12 +282,12 @@ public sealed class AdminAuthService( private async Task BuildProfileAsync(IdentityUser user, CancellationToken cancellationToken) { - var tenantId = user.TenantId; - var roles = await ResolveUserRolesAsync(tenantId, user.Id, cancellationToken); - var permissions = await ResolveUserPermissionsAsync(tenantId, user.Id, cancellationToken); + var roles = await ResolveUserRolesAsync(user.Portal, user.TenantId, user.Id, cancellationToken); + var permissions = await ResolveUserPermissionsAsync(user.Portal, user.TenantId, user.Id, cancellationToken); return new CurrentUserProfile { + Portal = user.Portal, UserId = user.Id, Account = user.Account, DisplayName = user.DisplayName, @@ -493,61 +492,62 @@ public sealed class AdminAuthService( .ToArray(); } - private async Task ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken) + private async Task ResolveUserRolesAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken) { - var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await _userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken); var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { return Array.Empty(); } - var roles = await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + var roles = await _roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); return roles.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } - private async Task ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken) + private async Task ResolveUserPermissionsAsync(PortalType portal, long? tenantId, long userId, CancellationToken cancellationToken) { - var relations = await _userRoleRepository.GetByUserIdAsync(tenantId, userId, cancellationToken); + var relations = await _userRoleRepository.GetByUserIdAsync(portal, tenantId, userId, cancellationToken); var roleIds = relations.Select(x => x.RoleId).Distinct().ToArray(); if (roleIds.Length == 0) { return Array.Empty(); } - var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + var rolePermissions = await _rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); if (permissionIds.Length == 0) { return Array.Empty(); } - var permissions = await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + var permissions = await _permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); return permissions.Select(x => x.Code).Distinct(StringComparer.OrdinalIgnoreCase).ToArray(); } private async Task> ResolveRolesAndPermissionsAsync( - long tenantId, + PortalType portal, + long? tenantId, IReadOnlyCollection users, CancellationToken cancellationToken) { var userIds = users.Select(x => x.Id).ToArray(); - var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(tenantId, userIds, cancellationToken); + var userRoleRelations = await _userRoleRepository.GetByUserIdsAsync(portal, tenantId, userIds, cancellationToken); var roleIds = userRoleRelations.Select(x => x.RoleId).Distinct().ToArray(); var roles = roleIds.Length == 0 ? Array.Empty() - : await _roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken); + : await _roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken); var roleCodeMap = roles.ToDictionary(r => r.Id, r => r.Code, comparer: EqualityComparer.Default); var rolePermissions = roleIds.Length == 0 ? Array.Empty() - : await _rolePermissionRepository.GetByRoleIdsAsync(tenantId, roleIds, cancellationToken); + : await _rolePermissionRepository.GetByRoleIdsAsync(portal, tenantId, roleIds, cancellationToken); var permissionIds = rolePermissions.Select(x => x.PermissionId).Distinct().ToArray(); var permissions = permissionIds.Length == 0 ? Array.Empty() - : await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken); + : await _permissionRepository.GetByIdsAsync(permissionIds, cancellationToken); var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer.Default); var rolePermissionsLookup = rolePermissions diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs index 70a46ed..400e9eb 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/IdentityUser.cs @@ -4,10 +4,20 @@ using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; /// -/// 管理后台账户实体(平台管理员、租户管理员或商户员工)。 +/// 后台账户实体(按 Portal 区分平台管理员与租户后台账号)。 /// -public sealed class IdentityUser : MultiTenantEntityBase +public sealed class IdentityUser : AuditableEntityBase { + /// + /// 账号所属 Portal。 + /// + public PortalType Portal { get; set; } + + /// + /// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 + /// + public long? TenantId { get; set; } + /// /// 登录账号。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MenuDefinition.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MenuDefinition.cs index 065b659..9308404 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MenuDefinition.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/MenuDefinition.cs @@ -1,12 +1,18 @@ +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; /// -/// 管理端菜单定义。 +/// 后台菜单定义(按 Portal 区分 Admin/Tenant 两套菜单树)。 /// -public sealed class MenuDefinition : MultiTenantEntityBase +public sealed class MenuDefinition : AuditableEntityBase { + /// + /// 菜单所属 Portal。 + /// + public PortalType Portal { get; set; } + /// /// 父级菜单 ID,根节点为 0。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs index d638ffd..999c2b8 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Permission.cs @@ -1,3 +1,4 @@ +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; @@ -5,8 +6,13 @@ namespace TakeoutSaaS.Domain.Identity.Entities; /// /// 权限定义。 /// -public sealed class Permission : MultiTenantEntityBase +public sealed class Permission : AuditableEntityBase { + /// + /// 权限所属 Portal。 + /// + public PortalType Portal { get; set; } + /// /// 父级权限 ID,根节点为 0。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs index 356d8bc..e933aa1 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/Role.cs @@ -1,3 +1,4 @@ +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; @@ -5,8 +6,18 @@ namespace TakeoutSaaS.Domain.Identity.Entities; /// /// 角色定义。 /// -public sealed class Role : MultiTenantEntityBase +public sealed class Role : AuditableEntityBase { + /// + /// 角色所属 Portal。 + /// + public PortalType Portal { get; set; } + + /// + /// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 + /// + public long? TenantId { get; set; } + /// /// 角色名称。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs index 55ac3e0..186e2e9 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/RolePermission.cs @@ -1,3 +1,4 @@ +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; @@ -5,8 +6,18 @@ namespace TakeoutSaaS.Domain.Identity.Entities; /// /// 角色-权限关系。 /// -public sealed class RolePermission : MultiTenantEntityBase +public sealed class RolePermission : AuditableEntityBase { + /// + /// 关系所属 Portal。 + /// + public PortalType Portal { get; set; } + + /// + /// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 + /// + public long? TenantId { get; set; } + /// /// 角色 ID。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs index eeb147c..096565c 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Entities/UserRole.cs @@ -1,3 +1,4 @@ +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Shared.Abstractions.Entities; namespace TakeoutSaaS.Domain.Identity.Entities; @@ -5,8 +6,18 @@ namespace TakeoutSaaS.Domain.Identity.Entities; /// /// 用户-角色关系。 /// -public sealed class UserRole : MultiTenantEntityBase +public sealed class UserRole : AuditableEntityBase { + /// + /// 关系所属 Portal。 + /// + public PortalType Portal { get; set; } + + /// + /// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。 + /// + public long? TenantId { get; set; } + /// /// 用户 ID。 /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs new file mode 100644 index 0000000..97ede6e --- /dev/null +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs @@ -0,0 +1,18 @@ +namespace TakeoutSaaS.Domain.Identity.Enums; + +/// +/// 后台端类型(用于区分平台管理端与租户管理端)。 +/// +public enum PortalType +{ + /// + /// 平台管理端(Admin)。 + /// + Admin = 0, + + /// + /// 租户管理端(Tenant)。 + /// + Tenant = 1 +} + diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs index ecac706..7276268 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IIdentityUserRepository.cs @@ -74,14 +74,6 @@ public interface IIdentityUserRepository /// 后台用户或 null。 Task FindByIdAsync(long userId, CancellationToken cancellationToken = default); - /// - /// 根据 ID 获取后台用户(忽略租户过滤器,仅用于只读查询)。 - /// - /// 用户 ID。 - /// 取消标记。 - /// 后台用户或 null。 - Task FindByIdIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default); - /// /// 根据 ID 获取后台用户(用于更新,返回可跟踪实体)。 /// @@ -90,28 +82,13 @@ public interface IIdentityUserRepository /// 后台用户或 null。 Task GetForUpdateAsync(long userId, CancellationToken cancellationToken = default); - /// - /// 根据 ID 获取后台用户(用于更新,忽略租户过滤器)。 - /// - /// 用于跨租户场景(如平台生成的重置密码链接)。 - /// 用户 ID。 - /// 取消标记。 - /// 后台用户或 null。 - Task GetForUpdateIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default); - /// /// 根据 ID 获取后台用户(用于更新,包含已删除数据)。 /// - /// 租户 ID。 /// 用户 ID。 - /// 是否忽略租户过滤。 /// 取消标记。 /// 后台用户或 null。 - Task GetForUpdateIncludingDeletedAsync( - long tenantId, - long userId, - bool ignoreTenantFilter = false, - CancellationToken cancellationToken = default); + Task GetForUpdateIncludingDeletedAsync(long userId, CancellationToken cancellationToken = default); /// /// 按租户与关键字查询后台用户列表(仅读)。 @@ -126,12 +103,10 @@ public interface IIdentityUserRepository /// 分页查询后台用户列表。 /// /// 查询过滤条件。 - /// 是否忽略租户过滤器。 /// 取消标记。 /// 分页结果。 Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( IdentityUserSearchFilter filter, - bool ignoreTenantFilter = false, CancellationToken cancellationToken = default); /// @@ -149,14 +124,12 @@ public interface IIdentityUserRepository /// 租户 ID。 /// 用户 ID 集合。 /// 是否包含已删除数据。 - /// 是否忽略租户过滤器。 /// 取消标记。 /// 后台用户列表。 Task> GetForUpdateByIdsAsync( long tenantId, IEnumerable userIds, bool includeDeleted, - bool ignoreTenantFilter = false, CancellationToken cancellationToken = default); /// diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMenuRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMenuRepository.cs index 093c0ce..cc7d21f 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMenuRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IMenuRepository.cs @@ -1,4 +1,5 @@ using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Domain.Identity.Repositories; @@ -8,14 +9,19 @@ namespace TakeoutSaaS.Domain.Identity.Repositories; public interface IMenuRepository { /// - /// 按租户获取菜单列表。 + /// 按 Portal 获取菜单列表。 /// - Task> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default); + /// Portal 类型。 + /// 取消标记。 + Task> GetByPortalAsync(PortalType portal, CancellationToken cancellationToken = default); /// /// 根据 ID 查询菜单。 /// - Task FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default); + /// 菜单 ID。 + /// Portal 类型。 + /// 取消标记。 + Task FindByIdAsync(long id, PortalType portal, CancellationToken cancellationToken = default); /// /// 新增菜单。 @@ -30,7 +36,10 @@ public interface IMenuRepository /// /// 删除菜单。 /// - Task DeleteAsync(long id, long tenantId, CancellationToken cancellationToken = default); + /// 菜单 ID。 + /// Portal 类型。 + /// 取消标记。 + Task DeleteAsync(long id, PortalType portal, CancellationToken cancellationToken = default); /// /// 持久化变更。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs index d643394..6a7668d 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IPermissionRepository.cs @@ -1,9 +1,10 @@ +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Entities; namespace TakeoutSaaS.Domain.Identity.Repositories; /// -/// 权限仓储。 +/// 权限仓储(全局权限定义,按 Portal 区分)。 /// public interface IPermissionRepository { @@ -11,46 +12,42 @@ public interface IPermissionRepository /// 根据 ID 查询权限。 /// /// 权限 ID。 - /// 租户 ID。 /// 取消标记。 /// 权限实体或 null。 - Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default); + Task FindByIdAsync(long permissionId, CancellationToken cancellationToken = default); /// /// 根据编码查询权限。 /// /// 权限编码。 - /// 租户 ID。 /// 取消标记。 /// 权限实体或 null。 - Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default); + Task FindByCodeAsync(string code, CancellationToken cancellationToken = default); /// /// 根据编码集合查询权限列表。 /// - /// 租户 ID。 /// 权限编码集合。 /// 取消标记。 /// 权限集合。 - Task> GetByCodesAsync(long tenantId, IEnumerable codes, CancellationToken cancellationToken = default); + Task> GetByCodesAsync(IEnumerable codes, CancellationToken cancellationToken = default); /// /// 根据 ID 集合查询权限列表。 /// - /// 租户 ID。 /// 权限 ID 集合。 /// 取消标记。 /// 权限集合。 - Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default); + Task> GetByIdsAsync(IEnumerable permissionIds, CancellationToken cancellationToken = default); /// - /// 按关键字搜索权限。 + /// 按 Portal 与关键字搜索权限。 /// - /// 租户 ID。 + /// Portal 类型。 /// 关键字。 /// 取消标记。 /// 权限集合。 - Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + Task> SearchAsync(PortalType portal, string? keyword, CancellationToken cancellationToken = default); /// /// 新增权限。 @@ -72,10 +69,9 @@ public interface IPermissionRepository /// 删除权限。 /// /// 权限 ID。 - /// 租户 ID。 /// 取消标记。 /// 异步操作任务。 - Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default); + Task DeleteAsync(long permissionId, CancellationToken cancellationToken = default); /// /// 保存仓储变更。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs index 4502d1e..881f75a 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRolePermissionRepository.cs @@ -1,4 +1,5 @@ using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Domain.Identity.Repositories; @@ -10,11 +11,16 @@ public interface IRolePermissionRepository /// /// 根据角色 ID 集合获取角色权限关系。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 角色 ID 集合。 /// 取消标记。 /// 角色权限关系列表。 - Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task> GetByRoleIdsAsync( + PortalType portal, + long? tenantId, + IEnumerable roleIds, + CancellationToken cancellationToken = default); /// /// 批量新增角色权限关系。 @@ -27,12 +33,18 @@ public interface IRolePermissionRepository /// /// 替换角色的权限集合。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 角色 ID。 /// 权限 ID 集合。 /// 取消标记。 /// 异步操作任务。 - Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default); + Task ReplaceRolePermissionsAsync( + PortalType portal, + long? tenantId, + long roleId, + IEnumerable permissionIds, + CancellationToken cancellationToken = default); /// /// 提交持久化变更。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs index 726a513..b445244 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IRoleRepository.cs @@ -1,4 +1,5 @@ using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Domain.Identity.Repositories; @@ -11,37 +12,45 @@ public interface IRoleRepository /// 根据 ID 查询角色。 /// /// 角色 ID。 - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 取消标记。 /// 角色实体或 null。 - Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default); + Task FindByIdAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default); /// /// 根据编码查询角色。 /// /// 角色编码。 - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 取消标记。 /// 角色实体或 null。 - Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default); + Task FindByCodeAsync(PortalType portal, long? tenantId, string code, CancellationToken cancellationToken = default); /// /// 批量获取角色列表。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 角色 ID 集合。 /// 取消标记。 /// 角色集合。 - Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task> GetByIdsAsync( + PortalType portal, + long? tenantId, + IEnumerable roleIds, + CancellationToken cancellationToken = default); /// /// 按关键字搜索角色。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 关键字。 /// 取消标记。 /// 角色集合。 - Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default); + Task> SearchAsync(PortalType portal, long? tenantId, string? keyword, CancellationToken cancellationToken = default); /// /// 新增角色。 @@ -63,10 +72,11 @@ public interface IRoleRepository /// 删除角色。 /// /// 角色 ID。 - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 取消标记。 /// 异步操作任务。 - Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default); + Task DeleteAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default); /// /// 保存仓储变更。 diff --git a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs index 8345e2b..0b91589 100644 --- a/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Identity/Repositories/IUserRoleRepository.cs @@ -1,4 +1,5 @@ using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; namespace TakeoutSaaS.Domain.Identity.Repositories; @@ -10,39 +11,56 @@ public interface IUserRoleRepository /// /// 批量获取指定用户的角色关系。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 用户 ID 集合。 /// 取消标记。 /// 用户角色关系集合。 - Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default); + Task> GetByUserIdsAsync( + PortalType portal, + long? tenantId, + IEnumerable userIds, + CancellationToken cancellationToken = default); /// /// 获取单个用户的角色关系。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 用户 ID。 /// 取消标记。 /// 指定用户的角色关系列表。 - Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default); + Task> GetByUserIdAsync( + PortalType portal, + long? tenantId, + long userId, + CancellationToken cancellationToken = default); /// /// 替换用户的角色列表。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 用户 ID。 /// 角色 ID 集合。 /// 取消标记。 /// 异步操作任务。 - Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default); + Task ReplaceUserRolesAsync( + PortalType portal, + long? tenantId, + long userId, + IEnumerable roleIds, + CancellationToken cancellationToken = default); /// /// 统计指定角色下的用户数量。 /// - /// 租户 ID。 + /// Portal 类型。 + /// 租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。 /// 角色 ID。 /// 取消标记。 /// 用户数量。 - Task CountUsersByRoleAsync(long tenantId, long roleId, CancellationToken cancellationToken = default); + Task CountUsersByRoleAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default); /// /// 提交持久化变更。 diff --git a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs index 9440775..cf11052 100644 --- a/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Stores/Repositories/IStoreRepository.cs @@ -11,7 +11,11 @@ public interface IStoreRepository /// /// 依据标识获取门店。 /// - Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// 门店 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task FindByIdAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 获取指定商户的门店列表。 @@ -22,14 +26,14 @@ public interface IStoreRepository /// 按租户筛选门店列表。 /// Task> SearchAsync( - long tenantId, + long? tenantId, long? merchantId, StoreStatus? status, StoreAuditStatus? auditStatus, StoreBusinessStatus? businessStatus, StoreOwnershipType? ownershipType, string? keyword, - bool ignoreTenantFilter = false, + bool includeDeleted = false, CancellationToken cancellationToken = default); /// @@ -51,12 +55,20 @@ public interface IStoreRepository /// /// 获取门店营业时段。 /// - Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// 门店 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task> GetBusinessHoursAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 获取门店费用配置。 /// - Task GetStoreFeeAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// 门店 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task GetStoreFeeAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 新增门店费用配置。 @@ -71,7 +83,11 @@ public interface IStoreRepository /// /// 获取门店资质列表。 /// - Task> GetQualificationsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// 门店 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task> GetQualificationsAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 依据标识获取门店资质。 @@ -111,22 +127,38 @@ public interface IStoreRepository /// /// 获取门店配送区域配置。 /// - Task> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// 门店 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task> GetDeliveryZonesAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 依据标识获取配送区域。 /// - Task FindDeliveryZoneByIdAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default); + /// 配送区域 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task FindDeliveryZoneByIdAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 获取门店节假日配置。 /// - Task> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default); + /// 门店 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task> GetHolidaysAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 依据标识获取节假日配置。 /// - Task FindHolidayByIdAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default); + /// 节假日配置 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + /// 是否包含已删除数据。 + Task FindHolidayByIdAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false); /// /// 获取门店桌台区域。 @@ -276,12 +308,18 @@ public interface IStoreRepository /// /// 删除配送区域。 /// - Task DeleteDeliveryZoneAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default); + /// 配送区域 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + Task DeleteDeliveryZoneAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default); /// /// 删除节假日。 /// - Task DeleteHolidayAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default); + /// 节假日配置 ID。 + /// 租户 ID(为空则不做租户过滤)。 + /// 取消标记。 + Task DeleteHolidayAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default); /// /// 删除桌台区域。 diff --git a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs index 95d267e..68e550d 100644 --- a/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs +++ b/src/Domain/TakeoutSaaS.Domain/Tenants/Repositories/ISubscriptionRepository.cs @@ -15,60 +15,60 @@ public interface ISubscriptionRepository /// /// 订阅 ID。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 订阅实体,未找到返回 null。 Task FindByIdAsync( long subscriptionId, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 按 ID 列表批量查询订阅。 /// /// 订阅 ID 列表。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 订阅实体列表。 Task> FindByIdsAsync( IEnumerable subscriptionIds, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 分页查询订阅列表(含关联信息)。 /// /// 查询过滤条件。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 分页结果。 Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( SubscriptionSearchFilter filter, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 获取订阅详情(含关联信息)。 /// /// 订阅 ID。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 订阅详情信息。 Task GetDetailAsync( long subscriptionId, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 按 ID 列表批量查询订阅(含租户信息)。 /// /// 订阅 ID 列表。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 订阅与租户信息列表。 Task> FindByIdsWithTenantAsync( IEnumerable subscriptionIds, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。 @@ -76,13 +76,13 @@ public interface ISubscriptionRepository /// 当前时间(UTC)。 /// 续费阈值时间(UTC),到期时间小于等于该时间视为候选。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 候选订阅集合(含套餐信息)。 Task> FindAutoRenewalCandidatesAsync( DateTime now, DateTime renewalThreshold, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。 @@ -90,25 +90,25 @@ public interface ISubscriptionRepository /// 筛选开始时间(UTC,含)。 /// 筛选结束时间(UTC,不含)。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 候选订阅集合(含租户与套餐信息)。 Task> FindRenewalReminderCandidatesAsync( DateTime startOfDay, DateTime endOfDay, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。 /// /// 当前时间(UTC)。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 到期订阅集合。 Task> FindExpiredActiveSubscriptionsAsync( DateTime now, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); /// /// 查询宽限期已结束的订阅(用于自动暂停)。 @@ -116,13 +116,13 @@ public interface ISubscriptionRepository /// 当前时间(UTC)。 /// 宽限期天数。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 宽限期到期订阅集合。 Task> FindGracePeriodExpiredSubscriptionsAsync( DateTime now, int gracePeriodDays, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); #endregion @@ -177,12 +177,12 @@ public interface ISubscriptionRepository /// /// 租户 ID。 /// 取消标记。 - /// 是否忽略租户过滤(用于平台级查询/任务)。 + /// 是否包含已删除数据。 /// 配额使用列表。 Task> GetQuotaUsagesAsync( long tenantId, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false); + bool includeDeleted = false); #endregion diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs index 4871575..7feeb24 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAdminDbContext.cs @@ -1,4 +1,3 @@ -using Microsoft.AspNetCore.Http; using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; @@ -13,9 +12,8 @@ public sealed class TakeoutAdminDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TakeoutAppDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TakeoutAppDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 配置实体映射关系(不启用租户过滤)。 @@ -27,4 +25,3 @@ public sealed class TakeoutAdminDbContext( OnModelCreatingCore(modelBuilder); } } - diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs index 37962a8..aee1ee0 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Persistence/TakeoutAppDbContext.cs @@ -26,7 +26,6 @@ using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; using TakeoutSaaS.Infrastructure.App.Persistence.Configurations; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.App.Persistence; @@ -37,9 +36,8 @@ public class TakeoutAppDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 租户聚合根。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs index 45306cc..d7350dc 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfStoreRepository.cs @@ -17,19 +17,23 @@ namespace TakeoutSaaS.Infrastructure.App.Repositories; public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRepository { /// - public Task FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.Stores.AsNoTracking(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 返回门店实体 return query .Where(x => x.Id == storeId) .FirstOrDefaultAsync(cancellationToken); @@ -47,58 +51,68 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep /// public async Task> SearchAsync( - long tenantId, + long? tenantId, long? merchantId, StoreStatus? status, StoreAuditStatus? auditStatus, StoreBusinessStatus? businessStatus, StoreOwnershipType? ownershipType, string? keyword, - bool ignoreTenantFilter = false, + bool includeDeleted = false, CancellationToken cancellationToken = default) { var query = context.Stores.AsNoTracking(); - if (ignoreTenantFilter) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 可选过滤:商户 if (merchantId.HasValue) { query = query.Where(x => x.MerchantId == merchantId.Value); } + // 4. (空行后) 可选过滤:状态 if (status.HasValue) { query = query.Where(x => x.Status == status.Value); } + // 5. (空行后) 可选过滤:审核状态 if (auditStatus.HasValue) { query = query.Where(x => x.AuditStatus == auditStatus.Value); } + // 6. (空行后) 可选过滤:经营状态 if (businessStatus.HasValue) { query = query.Where(x => x.BusinessStatus == businessStatus.Value); } + // 7. (空行后) 可选过滤:主体类型 if (ownershipType.HasValue) { query = query.Where(x => x.OwnershipType == ownershipType.Value); } + // 8. (空行后) 可选过滤:关键词 if (!string.IsNullOrWhiteSpace(keyword)) { var trimmed = keyword.Trim(); query = query.Where(x => x.Name.Contains(trimmed) || x.Code.Contains(trimmed)); } + // 9. (空行后) 查询并返回结果 var stores = await query .OrderBy(x => x.Name) .ToListAsync(cancellationToken); @@ -152,15 +166,14 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } var query = context.Stores.AsNoTracking(); - if (!tenantId.HasValue || tenantId.Value <= 0) - { - query = query.IgnoreQueryFilters(); - } - else + + // 1. 可选租户过滤 + if (tenantId.HasValue) { query = query.Where(x => x.TenantId == tenantId.Value); } + // 2. (空行后) 分组统计门店数量 return await query .Where(x => merchantIds.Contains(x.MerchantId)) .GroupBy(x => x.MerchantId) @@ -169,19 +182,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public async Task> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public async Task> GetBusinessHoursAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreBusinessHours.AsNoTracking(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 查询并返回营业时段 var hours = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.DayOfWeek) @@ -192,19 +209,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public Task GetStoreFeeAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public Task GetStoreFeeAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreFees.AsNoTracking(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 返回费用配置 return query .Where(x => x.StoreId == storeId) .FirstOrDefaultAsync(cancellationToken); @@ -224,19 +245,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public async Task> GetQualificationsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public async Task> GetQualificationsAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreQualifications.AsNoTracking(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 查询并返回资质列表 var qualifications = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.SortOrder) @@ -305,19 +330,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public async Task> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public async Task> GetDeliveryZonesAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreDeliveryZones.AsNoTracking(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 查询并返回配送区域 var zones = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.SortOrder) @@ -327,38 +356,46 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public Task FindDeliveryZoneByIdAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default) + public Task FindDeliveryZoneByIdAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreDeliveryZones.AsQueryable(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 返回配送区域实体 return query .Where(x => x.Id == deliveryZoneId) .FirstOrDefaultAsync(cancellationToken); } /// - public async Task> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default) + public async Task> GetHolidaysAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreHolidays.AsNoTracking(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 查询并返回节假日 var holidays = await query .Where(x => x.StoreId == storeId) .OrderBy(x => x.Date) @@ -368,19 +405,23 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public Task FindHolidayByIdAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default) + public Task FindHolidayByIdAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false) { var query = context.StoreHolidays.AsQueryable(); - if (tenantId <= 0) + + // 1. 包含软删除数据时忽略全局过滤 + if (includeDeleted) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.IgnoreQueryFilters(); } + // 2. (空行后) 可选租户过滤 + if (tenantId.HasValue) + { + query = query.Where(x => x.TenantId == tenantId.Value); + } + + // 3. (空行后) 返回节假日实体 return query .Where(x => x.Id == holidayId) .FirstOrDefaultAsync(cancellationToken); @@ -624,19 +665,16 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public async Task DeleteDeliveryZoneAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default) + public async Task DeleteDeliveryZoneAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default) { + // 1. 查询目标配送区域 var query = context.StoreDeliveryZones.AsQueryable(); - if (tenantId <= 0) + if (tenantId.HasValue) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.Where(x => x.TenantId == tenantId.Value); } + // 2. (空行后) 执行软删除 var existing = await query .Where(x => x.Id == deliveryZoneId) .FirstOrDefaultAsync(cancellationToken); @@ -648,19 +686,16 @@ public sealed class EfStoreRepository(TakeoutAdminDbContext context) : IStoreRep } /// - public async Task DeleteHolidayAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default) + public async Task DeleteHolidayAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default) { + // 1. 查询目标节假日 var query = context.StoreHolidays.AsQueryable(); - if (tenantId <= 0) + if (tenantId.HasValue) { - query = query.IgnoreQueryFilters() - .Where(x => x.DeletedAt == null); - } - else - { - query = query.Where(x => x.TenantId == tenantId); + query = query.Where(x => x.TenantId == tenantId.Value); } + // 2. (空行后) 执行软删除 var existing = await query .Where(x => x.Id == holidayId) .FirstOrDefaultAsync(cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs index 75584c8..c7c23eb 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/App/Repositories/EfSubscriptionRepository.cs @@ -18,9 +18,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task FindByIdAsync( long subscriptionId, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { - var query = ignoreTenantFilter + var query = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -32,10 +32,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task> FindByIdsAsync( IEnumerable subscriptionIds, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { var ids = subscriptionIds.ToList(); - var query = ignoreTenantFilter + var query = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -48,10 +48,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( SubscriptionSearchFilter filter, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { // 1. 构建基础查询 - var subscriptionQuery = ignoreTenantFilter + var subscriptionQuery = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -134,9 +134,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task GetDetailAsync( long subscriptionId, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { - var subscriptionQuery = ignoreTenantFilter + var subscriptionQuery = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -173,11 +173,11 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task> FindByIdsWithTenantAsync( IEnumerable subscriptionIds, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { var ids = subscriptionIds.ToList(); - var query = ignoreTenantFilter + var query = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -201,10 +201,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta DateTime now, DateTime renewalThreshold, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { // 1. 查询开启自动续费且即将到期的活跃订阅 - var subscriptionQuery = ignoreTenantFilter + var subscriptionQuery = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -232,10 +232,10 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta DateTime startOfDay, DateTime endOfDay, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { // 1. 查询到期落在指定区间的订阅(且未开启自动续费) - var subscriptionQuery = ignoreTenantFilter + var subscriptionQuery = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -268,9 +268,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task> FindExpiredActiveSubscriptionsAsync( DateTime now, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { - var query = ignoreTenantFilter + var query = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -285,9 +285,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta DateTime now, int gracePeriodDays, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { - var query = ignoreTenantFilter + var query = includeDeleted ? dbContext.TenantSubscriptions.IgnoreQueryFilters() : dbContext.TenantSubscriptions; @@ -364,9 +364,9 @@ public sealed class EfSubscriptionRepository(TakeoutAdminDbContext dbContext, Ta public async Task> GetQuotaUsagesAsync( long tenantId, CancellationToken cancellationToken = default, - bool ignoreTenantFilter = false) + bool includeDeleted = false) { - var query = ignoreTenantFilter + var query = includeDeleted ? dbContext.TenantQuotaUsages.IgnoreQueryFilters() : dbContext.TenantQuotaUsages; diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs index 3d10e96..920292e 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Common/Persistence/TenantAwareDbContext.cs @@ -1,11 +1,9 @@ using Microsoft.EntityFrameworkCore; using System.Reflection; -using System.Linq; using TakeoutSaaS.Shared.Abstractions.Entities; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Common.Persistence; @@ -16,23 +14,12 @@ public abstract class TenantAwareDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) : AppDbContext(options, currentUserAccessor, idGenerator) + IIdGenerator? idGenerator = null) : AppDbContext(options, currentUserAccessor, idGenerator) { - private readonly ITenantProvider _tenantProvider = tenantProvider ?? throw new ArgumentNullException(nameof(tenantProvider)); - private readonly IHttpContextAccessor? _httpContextAccessor = httpContextAccessor; - private static readonly string[] PlatformRoleCodes = - { - "super-admin", - "SUPER_ADMIN", - "PlatformAdmin", - "platform-admin" - }; - /// /// 当前请求租户 ID。 /// - protected long CurrentTenantId => _tenantProvider.GetCurrentTenantId(); + protected long CurrentTenantId => tenantProvider.GetCurrentTenantId(); /// /// 保存前填充租户元数据并执行基础处理。 @@ -86,22 +73,8 @@ public abstract class TenantAwareDbContext( { if (entry.State == EntityState.Added && entry.Entity.TenantId == 0 && tenantId != 0) { - if (!IsPlatformAdmin()) - { - entry.Entity.TenantId = tenantId; - } + entry.Entity.TenantId = tenantId; } } } - - private bool IsPlatformAdmin() - { - var user = _httpContextAccessor?.HttpContext?.User; - if (user?.Identity?.IsAuthenticated != true) - { - return false; - } - - return PlatformRoleCodes.Any(user.IsInRole); - } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs index 0ff9fff..0339ac7 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Dictionary/Persistence/DictionaryDbContext.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Dictionary.Persistence; @@ -18,9 +17,8 @@ public sealed class DictionaryDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 字典分组集合。 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs index a1a9aa5..ad0c58c 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfIdentityUserRepository.cs @@ -125,19 +125,6 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde public Task FindByIdAsync(long userId, CancellationToken cancellationToken = default) => dbContext.IdentityUsers.AsNoTracking().FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - /// - /// 根据 ID 获取后台用户(忽略租户过滤器,仅用于只读查询)。 - /// - /// 用户 ID。 - /// 取消标记。 - /// 后台用户或 null。 - public Task FindByIdIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default) - => dbContext.IdentityUsers - .IgnoreQueryFilters() - .AsNoTracking() - .Where(x => x.DeletedAt == null) - .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - /// /// 根据 ID 获取后台用户(用于更新,返回可跟踪实体)。 /// @@ -147,43 +134,16 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde public Task GetForUpdateAsync(long userId, CancellationToken cancellationToken = default) => dbContext.IdentityUsers.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - /// - /// 根据 ID 获取后台用户(用于更新,忽略租户过滤器)。 - /// - /// 用于跨租户场景(如平台生成的重置密码链接)。 - /// 用户 ID。 - /// 取消标记。 - /// 后台用户或 null。 - public Task GetForUpdateIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default) - => dbContext.IdentityUsers - .IgnoreQueryFilters() - .Where(x => x.DeletedAt == null) - .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - /// /// 根据 ID 获取后台用户(用于更新,包含已删除数据)。 /// - /// 租户 ID。 /// 用户 ID。 - /// 是否忽略租户过滤。 /// 取消标记。 /// 后台用户或 null。 - public Task GetForUpdateIncludingDeletedAsync( - long tenantId, - long userId, - bool ignoreTenantFilter = false, - CancellationToken cancellationToken = default) - { - // 1. 构建查询(包含已删除数据) - var query = dbContext.IdentityUsers.IgnoreQueryFilters(); - if (!ignoreTenantFilter) - { - query = query.Where(x => x.TenantId == tenantId); - } - - // 2. 返回实体 - return query.FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - } + public Task GetForUpdateIncludingDeletedAsync(long userId, CancellationToken cancellationToken = default) + => dbContext.IdentityUsers + .IgnoreQueryFilters() + .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); /// /// 按租户与关键字搜索后台用户(只读)。 @@ -214,40 +174,28 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde /// 分页查询后台用户列表。 /// /// 查询过滤条件。 - /// 是否忽略租户过滤器。 /// 取消标记。 /// 分页结果。 public async Task<(IReadOnlyList Items, int Total)> SearchPagedAsync( IdentityUserSearchFilter filter, - bool ignoreTenantFilter = false, CancellationToken cancellationToken = default) { // 1. 构建基础查询 var query = dbContext.IdentityUsers.AsNoTracking(); - if (ignoreTenantFilter || filter.IncludeDeleted) + + // 2. (空行后) 包含软删除数据时忽略全局过滤 + if (filter.IncludeDeleted) { query = query.IgnoreQueryFilters(); } - // 2. 租户过滤 - if (!ignoreTenantFilter) - { - if (filter.TenantId.HasValue && filter.TenantId.Value != 0) - { - query = query.Where(x => x.TenantId == filter.TenantId.Value); - } - } - else if (filter.TenantId.HasValue && filter.TenantId.Value != 0) + // 3. (空行后) 可选租户过滤 + if (filter.TenantId.HasValue) { query = query.Where(x => x.TenantId == filter.TenantId.Value); } - if (!filter.IncludeDeleted) - { - query = query.Where(x => x.DeletedAt == null); - } - - // 3. 关键字筛选 + // 4. (空行后) 关键字筛选 if (!string.IsNullOrWhiteSpace(filter.Keyword)) { var normalized = filter.Keyword.Trim(); @@ -259,43 +207,35 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde || (x.Email != null && EF.Functions.ILike(x.Email, likeValue))); } - // 4. 状态过滤 + // 5. (空行后) 状态过滤 if (filter.Status.HasValue) { query = query.Where(x => x.Status == filter.Status.Value); } - // 5. 角色过滤 + // 6. (空行后) 角色过滤 if (filter.RoleId.HasValue) { var roleId = filter.RoleId.Value; var userRoles = dbContext.UserRoles.AsNoTracking(); - if (ignoreTenantFilter || filter.IncludeDeleted) + + // 6.1 包含软删除数据时忽略全局过滤 + if (filter.IncludeDeleted) { userRoles = userRoles.IgnoreQueryFilters(); } - if (!ignoreTenantFilter) - { - if (filter.TenantId.HasValue && filter.TenantId.Value != 0) - { - userRoles = userRoles.Where(x => x.TenantId == filter.TenantId.Value); - } - } - else if (filter.TenantId.HasValue && filter.TenantId.Value != 0) + // 6.2 (空行后) 可选租户过滤 + if (filter.TenantId.HasValue) { userRoles = userRoles.Where(x => x.TenantId == filter.TenantId.Value); } - if (!filter.IncludeDeleted) - { - userRoles = userRoles.Where(x => x.DeletedAt == null); - } - + // 6.3 (空行后) 用户角色关联过滤 query = query.Where(user => userRoles.Any(role => role.UserId == user.Id && role.RoleId == roleId)); } - // 6. 时间范围过滤 + // 7. (空行后) 时间范围过滤 if (filter.CreatedAtFrom.HasValue) { query = query.Where(x => x.CreatedAt >= filter.CreatedAtFrom.Value); @@ -316,7 +256,7 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde query = query.Where(x => x.LastLoginAt <= filter.LastLoginTo.Value); } - // 7. 排序 + // 8. (空行后) 排序 var sorted = filter.SortBy?.ToLowerInvariant() switch { "account" => filter.SortDescending @@ -336,7 +276,7 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde : query.OrderBy(x => x.CreatedAt) }; - // 8. 分页 + // 9. (空行后) 分页 var page = filter.Page <= 0 ? 1 : filter.Page; var pageSize = filter.PageSize <= 0 ? 20 : filter.PageSize; var total = await sorted.CountAsync(cancellationToken); @@ -355,11 +295,21 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde /// 用户 ID 集合。 /// 取消标记。 /// 后台用户列表。 - public Task> GetByIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) - => dbContext.IdentityUsers.AsNoTracking() - .Where(x => x.TenantId == tenantId && userIds.Contains(x.Id)) - .ToListAsync(cancellationToken) - .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + public async Task> GetByIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) + { + // 1. 去重并快速返回空集合 + var ids = userIds.Distinct().ToArray(); + if (ids.Length == 0) + { + return Array.Empty(); + } + + // 2. (空行后) 查询并返回列表 + return await dbContext.IdentityUsers + .AsNoTracking() + .Where(x => x.TenantId == tenantId && ids.Contains(x.Id)) + .ToListAsync(cancellationToken); + } /// /// 批量获取后台用户(可用于更新,支持包含已删除数据)。 @@ -367,42 +317,33 @@ public sealed class EfIdentityUserRepository(IdentityDbContext dbContext) : IIde /// 租户 ID。 /// 用户 ID 集合。 /// 是否包含已删除数据。 - /// 是否忽略租户过滤器。 /// 取消标记。 /// 后台用户列表。 - public Task> GetForUpdateByIdsAsync( + public async Task> GetForUpdateByIdsAsync( long tenantId, IEnumerable userIds, bool includeDeleted, - bool ignoreTenantFilter = false, CancellationToken cancellationToken = default) { - // 1. 构建基础查询 + // 1. 去重并快速返回空集合 var ids = userIds.Distinct().ToArray(); if (ids.Length == 0) { - return Task.FromResult>(Array.Empty()); + return Array.Empty(); } - var query = dbContext.IdentityUsers.Where(x => ids.Contains(x.Id)); - if (ignoreTenantFilter || includeDeleted) + // 2. (空行后) 构建查询 + var query = dbContext.IdentityUsers + .Where(x => x.TenantId == tenantId && ids.Contains(x.Id)); + + // 3. (空行后) 包含软删除数据时忽略全局过滤 + if (includeDeleted) { query = query.IgnoreQueryFilters(); } - if (!ignoreTenantFilter) - { - query = query.Where(x => x.TenantId == tenantId); - } - - if (!includeDeleted) - { - query = query.Where(x => x.DeletedAt == null); - } - - // 2. 返回列表 - return query.ToListAsync(cancellationToken) - .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); + // 4. (空行后) 返回列表 + return await query.ToListAsync(cancellationToken); } /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs index e5e95bd..be5fc86 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfPermissionRepository.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -13,10 +14,9 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi /// 根据权限 ID 获取权限。 /// /// 权限 ID。 - /// 租户 ID。 /// 取消标记。 /// 权限实体或 null。 - public Task FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(long permissionId, CancellationToken cancellationToken = default) => dbContext.Permissions .IgnoreQueryFilters() .AsNoTracking() @@ -26,10 +26,9 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi /// 根据权限编码获取权限。 /// /// 权限编码。 - /// 租户 ID。 /// 取消标记。 /// 权限实体或 null。 - public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) + public Task FindByCodeAsync(string code, CancellationToken cancellationToken = default) => dbContext.Permissions .IgnoreQueryFilters() .AsNoTracking() @@ -38,11 +37,10 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi /// /// 根据权限编码集合批量获取权限。 /// - /// 租户 ID。 /// 权限编码集合。 /// 取消标记。 /// 权限列表。 - public Task> GetByCodesAsync(long tenantId, IEnumerable codes, CancellationToken cancellationToken = default) + public Task> GetByCodesAsync(IEnumerable codes, CancellationToken cancellationToken = default) { // 1. 规范化编码集合 var normalizedCodes = codes @@ -63,11 +61,10 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi /// /// 根据权限 ID 集合批量获取权限。 /// - /// 租户 ID。 /// 权限 ID 集合。 /// 取消标记。 /// 权限列表。 - public Task> GetByIdsAsync(long tenantId, IEnumerable permissionIds, CancellationToken cancellationToken = default) + public Task> GetByIdsAsync(IEnumerable permissionIds, CancellationToken cancellationToken = default) => dbContext.Permissions .IgnoreQueryFilters() .AsNoTracking() @@ -76,19 +73,19 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); /// - /// 按关键字搜索权限。 + /// 按 Portal 与关键字搜索权限。 /// - /// 租户 ID。 + /// Portal 类型。 /// 搜索关键字。 /// 取消标记。 /// 权限列表。 - public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + public Task> SearchAsync(PortalType portal, string? keyword, CancellationToken cancellationToken = default) { // 1. 构建基础查询 var query = dbContext.Permissions .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.DeletedAt == null); + .Where(x => x.DeletedAt == null && x.Portal == portal); if (!string.IsNullOrWhiteSpace(keyword)) { // 2. 追加关键字过滤 @@ -133,10 +130,9 @@ public sealed class EfPermissionRepository(IdentityDbContext dbContext) : IPermi /// 删除指定权限。 /// /// 权限 ID。 - /// 租户 ID。 /// 取消标记。 /// 异步任务。 - public async Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default) + public async Task DeleteAsync(long permissionId, CancellationToken cancellationToken = default) { // 1. 查询目标权限 var entity = await dbContext.Permissions.FirstOrDefaultAsync(x => x.Id == permissionId, cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs index 8edaa7b..1aa136d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRolePermissionRepository.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -12,15 +13,20 @@ public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IR /// /// 根据角色 ID 集合获取角色权限映射。 /// + /// Portal 类型。 /// 租户 ID。 /// 角色 ID 集合。 /// 取消标记。 /// 角色权限映射列表。 - public Task> GetByRoleIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) + public Task> GetByRoleIdsAsync( + PortalType portal, + long? tenantId, + IEnumerable roleIds, + CancellationToken cancellationToken = default) => dbContext.RolePermissions .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.DeletedAt == null && roleIds.Contains(x.RoleId)) + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && roleIds.Contains(x.RoleId)) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); @@ -46,32 +52,51 @@ public sealed class EfRolePermissionRepository(IdentityDbContext dbContext) : IR /// /// 替换指定角色的权限集合。 /// + /// Portal 类型。 /// 租户 ID。 /// 角色 ID。 /// 权限 ID 集合。 /// 取消标记。 /// 异步任务。 - public async Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable permissionIds, CancellationToken cancellationToken = default) + public async Task ReplaceRolePermissionsAsync( + PortalType portal, + long? tenantId, + long roleId, + IEnumerable permissionIds, + CancellationToken cancellationToken = default) { + // 0. 防御性参数检查 + if (portal == PortalType.Tenant && (!tenantId.HasValue || tenantId.Value == 0)) + { + throw new ArgumentException("Portal=Tenant 时必须提供有效的 TenantId", nameof(tenantId)); + } + // 1. 使用执行策略保证可靠性 var strategy = dbContext.Database.CreateExecutionStrategy(); await strategy.ExecuteAsync(async () => { await using var trx = await dbContext.Database.BeginTransactionAsync(cancellationToken); - // 1. 删除旧记录(原生 SQL,避免跟踪干扰) - await dbContext.Database.ExecuteSqlRawAsync( - "DELETE FROM \"role_permissions\" WHERE \"TenantId\" = {0} AND \"RoleId\" = {1};", - parameters: new object[] { tenantId, roleId }, - cancellationToken: cancellationToken); + // 1. 删除旧记录 + await dbContext.RolePermissions + .IgnoreQueryFilters() + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.RoleId == roleId) + .ExecuteDeleteAsync(cancellationToken); - // 2. 插入新记录(防重复) - foreach (var permissionId in permissionIds.Distinct()) + // 2. 插入新记录(权限去重) + var distinctPermissionIds = permissionIds.Distinct().ToArray(); + if (distinctPermissionIds.Length > 0) { - await dbContext.Database.ExecuteSqlRawAsync( - "INSERT INTO \"role_permissions\" (\"TenantId\",\"RoleId\",\"PermissionId\",\"CreatedAt\",\"CreatedBy\",\"UpdatedAt\",\"UpdatedBy\",\"DeletedAt\",\"DeletedBy\") VALUES ({0},{1},{2},NOW(),NULL,NULL,NULL,NULL,NULL) ON CONFLICT DO NOTHING;", - parameters: new object[] { tenantId, roleId, permissionId }, - cancellationToken: cancellationToken); + var toAdd = distinctPermissionIds.Select(permissionId => new RolePermission + { + Portal = portal, + TenantId = tenantId, + RoleId = roleId, + PermissionId = permissionId + }); + + await dbContext.RolePermissions.AddRangeAsync(toAdd, cancellationToken); + await dbContext.SaveChangesAsync(cancellationToken); } await trx.CommitAsync(cancellationToken); diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs index 0916a90..5e2af23 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfRoleRepository.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -12,58 +13,70 @@ public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleReposit /// /// 根据角色 ID 获取角色。 /// - /// 角色 ID。 + /// Portal 类型。 /// 租户 ID。 + /// 角色 ID。 /// 取消标记。 /// 角色实体或 null。 - public Task FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default) => dbContext.Roles .IgnoreQueryFilters() .AsNoTracking() - .FirstOrDefaultAsync(x => x.Id == roleId && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken); + .FirstOrDefaultAsync( + x => x.Id == roleId && x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null, + cancellationToken); /// /// 根据角色编码获取角色。 /// - /// 角色编码。 + /// Portal 类型。 /// 租户 ID。 + /// 角色编码。 /// 取消标记。 /// 角色实体或 null。 - public Task FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default) + public Task FindByCodeAsync(PortalType portal, long? tenantId, string code, CancellationToken cancellationToken = default) => dbContext.Roles .IgnoreQueryFilters() .AsNoTracking() - .FirstOrDefaultAsync(x => x.Code == code && x.TenantId == tenantId && x.DeletedAt == null, cancellationToken); + .FirstOrDefaultAsync( + x => x.Code == code && x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null, + cancellationToken); /// /// 根据角色 ID 集合获取角色列表。 /// + /// Portal 类型。 /// 租户 ID。 /// 角色 ID 集合。 /// 取消标记。 /// 角色列表。 - public Task> GetByIdsAsync(long tenantId, IEnumerable roleIds, CancellationToken cancellationToken = default) + public Task> GetByIdsAsync( + PortalType portal, + long? tenantId, + IEnumerable roleIds, + CancellationToken cancellationToken = default) => dbContext.Roles .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null) + .Where(x => x.Portal == portal && x.TenantId == tenantId && roleIds.Contains(x.Id) && x.DeletedAt == null) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); /// /// 按关键字搜索角色。 /// + /// Portal 类型。 /// 租户 ID。 /// 搜索关键字。 /// 取消标记。 /// 角色列表。 - public Task> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default) + public Task> SearchAsync(PortalType portal, long? tenantId, string? keyword, CancellationToken cancellationToken = default) { // 1. 构建基础查询 var query = dbContext.Roles .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.DeletedAt == null); + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null); if (!string.IsNullOrWhiteSpace(keyword)) { // 2. 追加关键字过滤 @@ -107,14 +120,15 @@ public sealed class EfRoleRepository(IdentityDbContext dbContext) : IRoleReposit /// /// 软删除角色。 /// - /// 角色 ID。 + /// Portal 类型。 /// 租户 ID。 + /// 角色 ID。 /// 取消标记。 /// 异步任务。 - public async Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default) + public async Task DeleteAsync(PortalType portal, long? tenantId, long roleId, 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.Portal == portal && x.TenantId == tenantId, cancellationToken); if (entity != null) { // 2. 标记删除时间 diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs index 3bb62af..1cd4534 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/EfUserRoleRepository.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -12,42 +13,58 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol /// /// 根据用户 ID 集合获取用户角色映射。 /// + /// Portal 类型。 /// 租户 ID。 /// 用户 ID 集合。 /// 取消标记。 /// 用户角色映射列表。 - public Task> GetByUserIdsAsync(long tenantId, IEnumerable userIds, CancellationToken cancellationToken = default) + public Task> GetByUserIdsAsync( + PortalType portal, + long? tenantId, + IEnumerable userIds, + CancellationToken cancellationToken = default) => dbContext.UserRoles .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.DeletedAt == null && userIds.Contains(x.UserId)) + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && userIds.Contains(x.UserId)) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); /// /// 获取指定用户的角色集合。 /// + /// Portal 类型。 /// 租户 ID。 /// 用户 ID。 /// 取消标记。 /// 用户角色列表。 - public Task> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default) + public Task> GetByUserIdAsync( + PortalType portal, + long? tenantId, + long userId, + CancellationToken cancellationToken = default) => dbContext.UserRoles .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.DeletedAt == null && x.UserId == userId) + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && x.UserId == userId) .ToListAsync(cancellationToken) .ContinueWith(t => (IReadOnlyList)t.Result, cancellationToken); /// /// 替换指定用户的角色集合。 /// + /// Portal 类型。 /// 租户 ID。 /// 用户 ID。 /// 角色 ID 集合。 /// 取消标记。 /// 异步任务。 - public async Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable roleIds, CancellationToken cancellationToken = default) + public async Task ReplaceUserRolesAsync( + PortalType portal, + long? tenantId, + long userId, + IEnumerable roleIds, + CancellationToken cancellationToken = default) { // 1. 使用执行策略保障一致性 var strategy = dbContext.Database.CreateExecutionStrategy(); @@ -58,7 +75,7 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol // 2. 读取当前角色映射 var existing = await dbContext.UserRoles .IgnoreQueryFilters() - .Where(x => x.TenantId == tenantId && x.UserId == userId) + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.UserId == userId) .ToListAsync(cancellationToken); // 3. 去重并构建目标集合 @@ -90,6 +107,7 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol .Where(roleId => !existingRoleMap.ContainsKey(roleId)) .Select(roleId => new UserRole { + Portal = portal, TenantId = tenantId, UserId = userId, RoleId = roleId @@ -105,15 +123,16 @@ public sealed class EfUserRoleRepository(IdentityDbContext dbContext) : IUserRol /// /// 统计指定角色下的用户数量。 /// + /// Portal 类型。 /// 租户 ID。 /// 角色 ID。 /// 取消标记。 /// 用户数量。 - public Task CountUsersByRoleAsync(long tenantId, long roleId, CancellationToken cancellationToken = default) + public Task CountUsersByRoleAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default) => dbContext.UserRoles .IgnoreQueryFilters() .AsNoTracking() - .Where(x => x.TenantId == tenantId && x.DeletedAt == null && x.RoleId == roleId) + .Where(x => x.Portal == portal && x.TenantId == tenantId && x.DeletedAt == null && x.RoleId == roleId) .CountAsync(cancellationToken); /// diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs index 33656c8..e786235 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Persistence/IdentityDbContext.cs @@ -2,11 +2,11 @@ using MassTransit; using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore.Metadata.Builders; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -17,9 +17,8 @@ public sealed class IdentityDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 管理后台用户集合。 @@ -93,7 +92,12 @@ public sealed class IdentityDbContext( /// 实体构建器。 private static void ConfigureIdentityUser(EntityTypeBuilder builder) { - builder.ToTable("identity_users"); + builder.ToTable("identity_users", t => + { + t.HasCheckConstraint( + "CK_identity_users_Portal_Tenant", + "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)"); + }); builder.HasKey(x => x.Id); builder.Property(x => x.Account).HasMaxLength(64).IsRequired(); builder.Property(x => x.DisplayName).HasMaxLength(64).IsRequired(); @@ -110,18 +114,33 @@ public sealed class IdentityDbContext( .IsRowVersion() .IsConcurrencyToken() .HasColumnType("bytea"); - builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Portal).HasConversion().IsRequired(); + builder.Property(x => x.TenantId); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => x.TenantId); - builder.HasIndex(x => new { x.TenantId, x.Account }).IsUnique(); + builder.HasIndex(x => x.TenantId).HasFilter("\"Portal\" = 1"); + + builder.HasIndex(x => x.Account) + .IsUnique() + .HasFilter("\"Portal\" = 0"); + builder.HasIndex(x => new { x.TenantId, x.Account }) + .IsUnique() + .HasFilter("\"Portal\" = 1"); + + builder.HasIndex(x => x.Phone) + .IsUnique() + .HasFilter("\"Portal\" = 0 AND \"Phone\" IS NOT NULL"); builder.HasIndex(x => new { x.TenantId, x.Phone }) .IsUnique() - .HasFilter("\"Phone\" IS NOT NULL"); + .HasFilter("\"Portal\" = 1 AND \"Phone\" IS NOT NULL"); + + builder.HasIndex(x => x.Email) + .IsUnique() + .HasFilter("\"Portal\" = 0 AND \"Email\" IS NOT NULL"); builder.HasIndex(x => new { x.TenantId, x.Email }) .IsUnique() - .HasFilter("\"Email\" IS NOT NULL"); + .HasFilter("\"Portal\" = 1 AND \"Email\" IS NOT NULL"); } /// @@ -146,23 +165,36 @@ public sealed class IdentityDbContext( private static void ConfigureRole(EntityTypeBuilder builder) { - builder.ToTable("roles"); + builder.ToTable("roles", t => + { + t.HasCheckConstraint( + "CK_roles_Portal_Tenant", + "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)"); + }); builder.HasKey(x => x.Id); - builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Portal).HasConversion().IsRequired(); + builder.Property(x => x.TenantId); builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); builder.Property(x => x.Code).HasMaxLength(64).IsRequired(); builder.Property(x => x.Description).HasMaxLength(256); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => x.TenantId); - builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + + builder.HasIndex(x => x.Code) + .IsUnique() + .HasFilter("\"Portal\" = 0"); + builder.HasIndex(x => new { x.TenantId, x.Code }) + .IsUnique() + .HasFilter("\"Portal\" = 1"); + builder.HasIndex(x => x.TenantId) + .HasFilter("\"Portal\" = 1"); } private static void ConfigurePermission(EntityTypeBuilder builder) { builder.ToTable("permissions"); builder.HasKey(x => x.Id); - builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Portal).HasConversion().IsRequired(); builder.Property(x => x.ParentId).IsRequired(); builder.Property(x => x.SortOrder).IsRequired(); builder.Property(x => x.Type).HasMaxLength(16).IsRequired(); @@ -171,9 +203,8 @@ public sealed class IdentityDbContext( builder.Property(x => x.Description).HasMaxLength(256); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => x.TenantId); - builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder }); - builder.HasIndex(x => new { x.TenantId, x.Code }).IsUnique(); + builder.HasIndex(x => new { x.Portal, x.ParentId, x.SortOrder }); + builder.HasIndex(x => x.Code).IsUnique(); } private static void ConfigureRoleTemplate(EntityTypeBuilder builder) @@ -200,35 +231,59 @@ public sealed class IdentityDbContext( private static void ConfigureUserRole(EntityTypeBuilder builder) { - builder.ToTable("user_roles"); + builder.ToTable("user_roles", t => + { + t.HasCheckConstraint( + "CK_user_roles_Portal_Tenant", + "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)"); + }); builder.HasKey(x => x.Id); - builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Portal).HasConversion().IsRequired(); + builder.Property(x => x.TenantId); builder.Property(x => x.UserId).IsRequired(); builder.Property(x => x.RoleId).IsRequired(); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => x.TenantId); - builder.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }).IsUnique(); + builder.HasIndex(x => new { x.UserId, x.RoleId }) + .IsUnique() + .HasFilter("\"Portal\" = 0"); + builder.HasIndex(x => new { x.TenantId, x.UserId, x.RoleId }) + .IsUnique() + .HasFilter("\"Portal\" = 1"); + builder.HasIndex(x => x.TenantId) + .HasFilter("\"Portal\" = 1"); } private static void ConfigureRolePermission(EntityTypeBuilder builder) { - builder.ToTable("role_permissions"); + builder.ToTable("role_permissions", t => + { + t.HasCheckConstraint( + "CK_role_permissions_Portal_Tenant", + "(\"Portal\" = 0 AND \"TenantId\" IS NULL) OR (\"Portal\" = 1 AND \"TenantId\" IS NOT NULL)"); + }); builder.HasKey(x => x.Id); - builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Portal).HasConversion().IsRequired(); + builder.Property(x => x.TenantId); builder.Property(x => x.RoleId).IsRequired(); builder.Property(x => x.PermissionId).IsRequired(); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => x.TenantId); - builder.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }).IsUnique(); + builder.HasIndex(x => new { x.RoleId, x.PermissionId }) + .IsUnique() + .HasFilter("\"Portal\" = 0"); + builder.HasIndex(x => new { x.TenantId, x.RoleId, x.PermissionId }) + .IsUnique() + .HasFilter("\"Portal\" = 1"); + builder.HasIndex(x => x.TenantId) + .HasFilter("\"Portal\" = 1"); } private static void ConfigureMenuDefinition(EntityTypeBuilder builder) { builder.ToTable("menu_definitions"); builder.HasKey(x => x.Id); - builder.Property(x => x.TenantId).IsRequired(); + builder.Property(x => x.Portal).HasConversion().IsRequired(); builder.Property(x => x.ParentId).IsRequired(); builder.Property(x => x.Name).HasMaxLength(64).IsRequired(); builder.Property(x => x.Path).HasMaxLength(256).IsRequired(); @@ -243,6 +298,6 @@ public sealed class IdentityDbContext( builder.Property(x => x.AuthListJson).HasColumnType("text"); ConfigureAuditableEntity(builder); ConfigureSoftDeleteEntity(builder); - builder.HasIndex(x => new { x.TenantId, x.ParentId, x.SortOrder }); + builder.HasIndex(x => new { x.Portal, x.ParentId, x.SortOrder }); } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs index 97167ef..8e14d14 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Repositories/EfMenuRepository.cs @@ -1,5 +1,6 @@ using Microsoft.EntityFrameworkCore; using TakeoutSaaS.Domain.Identity.Entities; +using TakeoutSaaS.Domain.Identity.Enums; using TakeoutSaaS.Domain.Identity.Repositories; using TakeoutSaaS.Infrastructure.Identity.Persistence; @@ -10,15 +11,13 @@ namespace TakeoutSaaS.Infrastructure.Identity.Repositories; /// public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuRepository { - private const long PlatformRootTenantId = 1000000000001; - /// - public Task> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default) + public Task> GetByPortalAsync(PortalType portal, CancellationToken cancellationToken = default) { return dbContext.MenuDefinitions .AsNoTracking() .IgnoreQueryFilters() - .Where(x => x.TenantId == PlatformRootTenantId && x.DeletedAt == null) + .Where(x => x.Portal == portal && x.DeletedAt == null) .OrderBy(x => x.ParentId) .ThenBy(x => x.SortOrder) .ToListAsync(cancellationToken) @@ -26,13 +25,13 @@ public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuReposit } /// - public Task FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default) + public Task FindByIdAsync(long id, PortalType portal, CancellationToken cancellationToken = default) { return dbContext.MenuDefinitions .AsNoTracking() .IgnoreQueryFilters() .FirstOrDefaultAsync( - x => x.Id == id && x.TenantId == PlatformRootTenantId && x.DeletedAt == null, + x => x.Id == id && x.Portal == portal && x.DeletedAt == null, cancellationToken); } @@ -50,11 +49,11 @@ public sealed class EfMenuRepository(IdentityDbContext dbContext) : IMenuReposit } /// - public async Task DeleteAsync(long id, long tenantId, CancellationToken cancellationToken = default) + public async Task DeleteAsync(long id, PortalType portal, CancellationToken cancellationToken = default) { // 1. 查询目标 var entity = await dbContext.MenuDefinitions - .FirstOrDefaultAsync(x => x.Id == id && x.TenantId == tenantId, cancellationToken); + .FirstOrDefaultAsync(x => x.Id == id && x.Portal == portal, cancellationToken); // 2. 存在则删除 if (entity is not null) diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs index 71c3080..2b7a4fe 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Identity/Services/JwtTokenService.cs @@ -71,25 +71,33 @@ public sealed class JwtTokenService(IRefreshTokenStore refreshTokenStore, IOptio /// Claims 集合 private static List BuildClaims(CurrentUserProfile profile) { + // 1. 构建基础身份 Claim var userId = profile.UserId.ToString(); var claims = new List { new(JwtRegisteredClaimNames.Sub, userId), new(ClaimTypes.NameIdentifier, userId), new(JwtRegisteredClaimNames.UniqueName, profile.Account), - new("tenant_id", profile.TenantId.ToString()), new(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()) }; + // 2. (空行后) 仅租户端账号写入 tenant_id Claim + if (profile.TenantId.HasValue) + { + claims.Add(new Claim("tenant_id", profile.TenantId.Value.ToString())); + } + + // 3. (空行后) 可选写入商户上下文 Claim if (profile.MerchantId.HasValue) { claims.Add(new Claim("merchant_id", profile.MerchantId.Value.ToString())); } + // 4. (空行后) 写入角色与权限 Claim claims.AddRange(profile.Roles.Select(role => new Claim(ClaimTypes.Role, role))); - claims.AddRange(profile.Permissions.Select(permission => new Claim("permission", permission))); + // 5. (空行后) 返回 Claim 集合 return claims; } } diff --git a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs index 37918e4..a34444d 100644 --- a/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs +++ b/src/Infrastructure/TakeoutSaaS.Infrastructure/Logs/Persistence/TakeoutLogsDbContext.cs @@ -7,7 +7,6 @@ using TakeoutSaaS.Infrastructure.Common.Persistence; using TakeoutSaaS.Shared.Abstractions.Ids; using TakeoutSaaS.Shared.Abstractions.Security; using TakeoutSaaS.Shared.Abstractions.Tenancy; -using Microsoft.AspNetCore.Http; namespace TakeoutSaaS.Infrastructure.Logs.Persistence; @@ -18,9 +17,8 @@ public sealed class TakeoutLogsDbContext( DbContextOptions options, ITenantProvider tenantProvider, ICurrentUserAccessor? currentUserAccessor = null, - IIdGenerator? idGenerator = null, - IHttpContextAccessor? httpContextAccessor = null) - : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator, httpContextAccessor) + IIdGenerator? idGenerator = null) + : TenantAwareDbContext(options, tenantProvider, currentUserAccessor, idGenerator) { /// /// 租户审计日志集合。