refactor: 管理端去租户过滤并Portal化RBAC菜单
This commit is contained in:
Submodule TakeoutSaaS.Docs updated: 79fcefaebc...65fecf74e8
@@ -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<PagedResult<PermissionDto>>.Ok(result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取权限树。
|
||||
/// </summary>
|
||||
/// <param name="keyword">关键字(可选)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限树列表。</returns>
|
||||
[HttpGet("tree")]
|
||||
/// <summary>
|
||||
/// 获取权限树。
|
||||
/// </summary>
|
||||
/// <param name="portal">Portal 类型(Admin/Tenant)。</param>
|
||||
/// <param name="keyword">关键字(可选)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限树列表。</returns>
|
||||
[HttpGet("tree")]
|
||||
[PermissionAuthorize("identity:permission:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<IReadOnlyList<PermissionTreeDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<IReadOnlyList<PermissionTreeDto>>> Tree([FromQuery] string? keyword, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<IReadOnlyList<PermissionTreeDto>>> 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);
|
||||
|
||||
@@ -45,12 +45,14 @@ public sealed class StoresController(IMediator mediator) : BaseApiController
|
||||
[PermissionAuthorize("store:read")]
|
||||
[ProducesResponseType(typeof(ApiResponse<PagedResult<StoreDto>>), StatusCodes.Status200OK)]
|
||||
public async Task<ApiResponse<PagedResult<StoreDto>>> 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<StoreDto>), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(ApiResponse<object>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<StoreDto>> Detail(long storeId, CancellationToken cancellationToken)
|
||||
public async Task<ApiResponse<StoreDto>> 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
|
||||
|
||||
@@ -43,6 +43,7 @@ public sealed class SubscriptionsController(IMediator mediator) : BaseApiControl
|
||||
/// 查看订阅详情(含套餐信息、配额使用、变更历史)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="includeDeleted">是否包含已软删除数据。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>订阅详情或未找到。</returns>
|
||||
[HttpGet("{subscriptionId:long}")]
|
||||
@@ -51,10 +52,15 @@ public sealed class SubscriptionsController(IMediator mediator) : BaseApiControl
|
||||
[ProducesResponseType(typeof(ApiResponse<SubscriptionDetailDto>), StatusCodes.Status404NotFound)]
|
||||
public async Task<ApiResponse<SubscriptionDetailDto>> 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
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class BatchUpdateBusinessHoursCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<BatchUpdateBusinessHoursCommandHandler> logger)
|
||||
: IRequestHandler<BatchUpdateBusinessHoursCommand, IReadOnlyList<StoreBusinessHourDto>>
|
||||
{
|
||||
@@ -27,9 +23,7 @@ public sealed class BatchUpdateBusinessHoursCommandHandler(
|
||||
public async Task<IReadOnlyList<StoreBusinessHourDto>> 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, "门店不存在");
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class CalculateStoreFeeQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IStoreFeeCalculationService feeCalculationService)
|
||||
: IRequestHandler<CalculateStoreFeeQuery, StoreFeeCalculationResultDto>
|
||||
{
|
||||
@@ -26,16 +22,14 @@ public sealed class CalculateStoreFeeQueryHandler(
|
||||
public async Task<StoreFeeCalculationResultDto> 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,
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class CheckStoreDeliveryZoneQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IDeliveryZoneService deliveryZoneService)
|
||||
: IRequestHandler<CheckStoreDeliveryZoneQuery, StoreDeliveryCheckResultDto>
|
||||
{
|
||||
@@ -25,16 +20,14 @@ public sealed class CheckStoreDeliveryZoneQueryHandler(
|
||||
public async Task<StoreDeliveryCheckResultDto> 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. (空行后) 计算距离
|
||||
|
||||
@@ -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;
|
||||
/// 门店资质完整性检查处理器。
|
||||
/// </summary>
|
||||
public sealed class CheckStoreQualificationsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<CheckStoreQualificationsQuery, StoreQualificationCheckResultDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreQualificationCheckResultDto> 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());
|
||||
|
||||
@@ -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<CreateStoreCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreCommand, StoreDto>
|
||||
{
|
||||
@@ -29,11 +24,7 @@ public sealed class CreateStoreCommandHandler(
|
||||
public async Task<StoreDto> 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, "商户不存在");
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class CreateStoreDeliveryZoneCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IGeoJsonValidationService geoJsonValidationService,
|
||||
ILogger<CreateStoreDeliveryZoneCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreDeliveryZoneCommand, StoreDeliveryZoneDto>
|
||||
@@ -28,9 +24,7 @@ public sealed class CreateStoreDeliveryZoneCommandHandler(
|
||||
public async Task<StoreDeliveryZoneDto> 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, "门店不存在");
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class CreateStoreHolidayCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<CreateStoreHolidayCommandHandler> logger)
|
||||
: IRequestHandler<CreateStoreHolidayCommand, StoreHolidayDto>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
private readonly ILogger<CreateStoreHolidayCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreHolidayDto> 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);
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreDeliveryZoneCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<DeleteStoreDeliveryZoneCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreDeliveryZoneCommand, bool>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
private readonly ILogger<DeleteStoreDeliveryZoneCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class DeleteStoreHolidayCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<DeleteStoreHolidayCommandHandler> logger)
|
||||
: IRequestHandler<DeleteStoreHolidayCommand, bool>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
private readonly ILogger<DeleteStoreHolidayCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// 门店详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreByIdQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<GetStoreByIdQuery, StoreDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreDto?> 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// 获取门店费用配置处理器。
|
||||
/// </summary>
|
||||
public sealed class GetStoreFeeQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<GetStoreFeeQuery, StoreFeeDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreFeeDto?> 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
|
||||
|
||||
@@ -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;
|
||||
/// 营业时段列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreBusinessHoursQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<ListStoreBusinessHoursQuery, IReadOnlyList<StoreBusinessHourDto>>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreBusinessHourDto>> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// 配送区域列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreDeliveryZonesQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<ListStoreDeliveryZonesQuery, IReadOnlyList<StoreDeliveryZoneDto>>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreDeliveryZoneDto>> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// 门店节假日列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreHolidaysQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<ListStoreHolidaysQuery, IReadOnlyList<StoreHolidayDto>>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreHolidayDto>> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// 门店资质列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListStoreQualificationsQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<ListStoreQualificationsQuery, IReadOnlyList<StoreQualificationDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<StoreQualificationDto>> 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();
|
||||
|
||||
@@ -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;
|
||||
/// 门店列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchStoresQueryHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
IStoreRepository storeRepository)
|
||||
: IRequestHandler<SearchStoresQuery, PagedResult<StoreDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<StoreDto>> 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<StoreDto>(items, request.Page, request.PageSize, stores.Count);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<UpdateStoreCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreCommand, StoreDto?>
|
||||
{
|
||||
@@ -27,9 +23,7 @@ public sealed class UpdateStoreCommandHandler(
|
||||
public async Task<StoreDto?> 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;
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreDeliveryZoneCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IGeoJsonValidationService geoJsonValidationService,
|
||||
ILogger<UpdateStoreDeliveryZoneCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreDeliveryZoneCommand, StoreDeliveryZoneDto?>
|
||||
@@ -28,9 +24,7 @@ public sealed class UpdateStoreDeliveryZoneCommandHandler(
|
||||
public async Task<StoreDeliveryZoneDto?> 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;
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreFeeCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<UpdateStoreFeeCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreFeeCommand, StoreFeeDto>
|
||||
{
|
||||
@@ -27,9 +23,7 @@ public sealed class UpdateStoreFeeCommandHandler(
|
||||
public async Task<StoreFeeDto> 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
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class UpdateStoreHolidayCommandHandler(
|
||||
IStoreRepository storeRepository,
|
||||
ITenantProvider tenantProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<UpdateStoreHolidayCommandHandler> logger)
|
||||
: IRequestHandler<UpdateStoreHolidayCommand, StoreHolidayDto?>
|
||||
{
|
||||
private readonly IStoreRepository _storeRepository = storeRepository;
|
||||
private readonly ITenantProvider _tenantProvider = tenantProvider;
|
||||
private readonly IHttpContextAccessor _httpContextAccessor = httpContextAccessor;
|
||||
private readonly ILogger<UpdateStoreHolidayCommandHandler> _logger = logger;
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<StoreHolidayDto?> 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);
|
||||
|
||||
@@ -12,4 +12,9 @@ public sealed class GetStoreByIdQuery : IRequest<StoreDto?>
|
||||
/// 门店 ID。
|
||||
/// </summary>
|
||||
public long StoreId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含已软删除数据。
|
||||
/// </summary>
|
||||
public bool IncludeDeleted { get; init; }
|
||||
}
|
||||
|
||||
@@ -10,6 +10,11 @@ namespace TakeoutSaaS.Application.App.Stores.Queries;
|
||||
/// </summary>
|
||||
public sealed class SearchStoresQuery : IRequest<PagedResult<StoreDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// 租户 ID(为空则查询全部租户)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户 ID(可选)。
|
||||
/// </summary>
|
||||
@@ -40,6 +45,11 @@ public sealed class SearchStoresQuery : IRequest<PagedResult<StoreDto>>
|
||||
/// </summary>
|
||||
public string? Keyword { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含已软删除数据。
|
||||
/// </summary>
|
||||
public bool IncludeDeleted { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码。
|
||||
/// </summary>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class BatchExtendSubscriptionsCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<BatchExtendSubscriptionsCommandHandler> logger)
|
||||
: IRequestHandler<BatchExtendSubscriptionsCommand, BatchExtendResult>
|
||||
@@ -23,8 +21,6 @@ public sealed class BatchExtendSubscriptionsCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<BatchExtendResult> Handle(BatchExtendSubscriptionsCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
|
||||
|
||||
var successCount = 0;
|
||||
var failures = new List<BatchFailureItem>();
|
||||
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class BatchSendReminderCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<BatchSendReminderCommandHandler> logger)
|
||||
: IRequestHandler<BatchSendReminderCommand, BatchSendReminderResult>
|
||||
@@ -23,16 +21,13 @@ public sealed class BatchSendReminderCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<BatchSendReminderResult> Handle(BatchSendReminderCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var ignoreTenantFilter = SubscriptionTenantAccess.ShouldIgnoreTenantFilter(httpContextAccessor);
|
||||
|
||||
var successCount = 0;
|
||||
var failures = new List<BatchFailureItem>();
|
||||
|
||||
// 查询所有订阅及租户信息
|
||||
var subscriptions = await subscriptionRepository.FindByIdsWithTenantAsync(
|
||||
request.SubscriptionIds,
|
||||
cancellationToken,
|
||||
ignoreTenantFilter: ignoreTenantFilter);
|
||||
cancellationToken);
|
||||
|
||||
foreach (var subscriptionId in request.SubscriptionIds)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class ChangeSubscriptionPlanCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IIdGenerator idGenerator,
|
||||
IMediator mediator)
|
||||
: IRequestHandler<ChangeSubscriptionPlanCommand, SubscriptionDetailDto?>
|
||||
@@ -23,13 +21,10 @@ public sealed class ChangeSubscriptionPlanCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<SubscriptionDetailDto?> 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class ExtendSubscriptionCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IIdGenerator idGenerator,
|
||||
IMediator mediator)
|
||||
: IRequestHandler<ExtendSubscriptionCommand, SubscriptionDetailDto?>
|
||||
@@ -23,13 +21,10 @@ public sealed class ExtendSubscriptionCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<SubscriptionDetailDto?> 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// 订阅详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetSubscriptionDetailQueryHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
ISubscriptionRepository subscriptionRepository)
|
||||
: IRequestHandler<GetSubscriptionDetailQuery, SubscriptionDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<SubscriptionDetailDto?> 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))
|
||||
|
||||
@@ -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;
|
||||
/// 订阅分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class GetSubscriptionListQueryHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
ISubscriptionRepository subscriptionRepository)
|
||||
: IRequestHandler<GetSubscriptionListQuery, PagedResult<SubscriptionListDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<PagedResult<SubscriptionListDto>> 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
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class ProcessAutoRenewalCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ITenantBillingRepository billingRepository,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<ProcessAutoRenewalCommandHandler> logger)
|
||||
@@ -23,8 +21,6 @@ public sealed class ProcessAutoRenewalCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<ProcessAutoRenewalResult> 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. 遍历候选订阅,生成账单
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class ProcessRenewalRemindersCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ITenantNotificationRepository notificationRepository,
|
||||
IIdGenerator idGenerator,
|
||||
ILogger<ProcessRenewalRemindersCommandHandler> logger)
|
||||
@@ -26,8 +24,6 @@ public sealed class ProcessRenewalRemindersCommandHandler(
|
||||
/// <inheritdoc />
|
||||
public async Task<ProcessRenewalRemindersResult> 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)
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class ProcessSubscriptionExpiryCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<ProcessSubscriptionExpiryCommandHandler> logger)
|
||||
: IRequestHandler<ProcessSubscriptionExpiryCommand, ProcessSubscriptionExpiryResult>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<ProcessSubscriptionExpiryResult> 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)
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class UpdateSubscriptionCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMediator mediator)
|
||||
: IRequestHandler<UpdateSubscriptionCommand, SubscriptionDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<SubscriptionDetailDto?> 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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class UpdateSubscriptionStatusCommandHandler(
|
||||
ISubscriptionRepository subscriptionRepository,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
IMediator mediator)
|
||||
: IRequestHandler<UpdateSubscriptionStatusCommand, SubscriptionDetailDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<SubscriptionDetailDto?> 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)
|
||||
{
|
||||
|
||||
@@ -12,4 +12,9 @@ public sealed record GetSubscriptionDetailQuery : IRequest<SubscriptionDetailDto
|
||||
/// 订阅 ID。
|
||||
/// </summary>
|
||||
public long SubscriptionId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含已软删除数据。
|
||||
/// </summary>
|
||||
public bool IncludeDeleted { get; init; }
|
||||
}
|
||||
|
||||
@@ -40,6 +40,11 @@ public sealed record GetSubscriptionListQuery : IRequest<PagedResult<Subscriptio
|
||||
/// </summary>
|
||||
public bool? AutoRenew { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否包含已软删除数据。
|
||||
/// </summary>
|
||||
public bool IncludeDeleted { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 页码(从 1 开始)。
|
||||
/// </summary>
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,6 +7,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed class CurrentUserProfile
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
@@ -23,7 +30,7 @@ public sealed class CurrentUserProfile
|
||||
/// <summary>
|
||||
/// 所属租户 ID。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属商户 ID(平台管理员为空)。
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class PermissionDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(固定权限时为基准租户)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级权限 ID。
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed record PermissionTreeDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 权限 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级权限 ID。
|
||||
/// </summary>
|
||||
@@ -42,7 +42,7 @@ public sealed record PermissionTreeDto
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 权限编码(租户内唯一)。
|
||||
/// 权限编码(全局唯一)。
|
||||
/// </summary>
|
||||
public string Code { get; init; } = string.Empty;
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
|
||||
/// <summary>
|
||||
@@ -5,15 +7,20 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record RoleDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID。
|
||||
/// </summary>
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
public long TenantId { get; init; }
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class RoleDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID(雪花,序列化为字符串)。
|
||||
/// </summary>
|
||||
@@ -15,10 +21,10 @@ public sealed class RoleDto
|
||||
public long Id { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record UserDetailDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
@@ -16,10 +21,10 @@ public sealed record UserDetailDto
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户 ID。
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace TakeoutSaaS.Application.Identity.Contracts;
|
||||
/// </summary>
|
||||
public sealed record UserListItemDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
@@ -16,10 +21,10 @@ public sealed record UserListItemDto
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID。
|
||||
/// 租户 ID(Portal=Tenant 必填;Portal=Admin 为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录账号。
|
||||
|
||||
@@ -15,10 +15,10 @@ public sealed class UserPermissionDto
|
||||
public long UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 租户 ID(雪花,序列化为字符串)。
|
||||
/// 租户 ID(雪花,序列化为字符串;平台管理员为空)。
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(SnowflakeIdJsonConverter))]
|
||||
public long TenantId { get; init; }
|
||||
[JsonConverter(typeof(NullableSnowflakeIdJsonConverter))]
|
||||
public long? TenantId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// 商户 ID(雪花,序列化为字符串,可空)。
|
||||
|
||||
@@ -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(
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<long>.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<long>()
|
||||
: (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<long, string[]>(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<Role>()
|
||||
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
||||
|
||||
// 5. 组装用户角色编码列表
|
||||
|
||||
@@ -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(
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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, "至少保留一个管理员");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
/// 创建菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class CreateMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<CreateMenuCommand, MenuDefinitionDto>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -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(),
|
||||
|
||||
@@ -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;
|
||||
/// 创建权限处理器。
|
||||
/// </summary>
|
||||
public sealed class CreatePermissionCommandHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<CreatePermissionCommand, PermissionDto>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
/// <returns>创建后的角色 DTO。</returns>
|
||||
public async Task<RoleDto> 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,
|
||||
|
||||
@@ -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, "至少保留一个管理员");
|
||||
|
||||
@@ -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;
|
||||
/// 删除菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class DeleteMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<DeleteMenuCommand, bool>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
/// 删除权限处理器。
|
||||
/// </summary>
|
||||
public sealed class DeletePermissionCommandHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<DeletePermissionCommand, bool>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
/// <returns>执行结果。</returns>
|
||||
public async Task<bool> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<Role>()
|
||||
: 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<long>()
|
||||
: (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<string>()
|
||||
: (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,
|
||||
|
||||
@@ -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<UserPermissionDto?> 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<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> 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<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> 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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
/// 菜单列表查询处理器。
|
||||
/// </summary>
|
||||
public sealed class ListMenusQueryHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<ListMenusQuery, IReadOnlyList<MenuDefinitionDto>>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<IReadOnlyList<MenuDefinitionDto>> 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();
|
||||
|
||||
@@ -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;
|
||||
/// 菜单详情查询处理器。
|
||||
/// </summary>
|
||||
public sealed class MenuDetailQueryHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<MenuDetailQuery, MenuDefinitionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public async Task<MenuDefinitionDto?> 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;
|
||||
|
||||
@@ -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。
|
||||
/// </summary>
|
||||
/// <param name="existing">已存在的菜单实体。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="name">菜单名称。</param>
|
||||
/// <param name="payload">菜单 DTO 载荷。</param>
|
||||
/// <returns>菜单定义 DTO。</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
/// 权限树查询处理器。
|
||||
/// </summary>
|
||||
public sealed class PermissionTreeQueryHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<PermissionTreeQuery, IReadOnlyList<PermissionTreeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -22,17 +20,16 @@ public sealed class PermissionTreeQueryHandler(
|
||||
/// <returns>权限树列表。</returns>
|
||||
public async Task<IReadOnlyList<PermissionTreeDto>> 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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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, "用户不存在");
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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(
|
||||
/// <inheritdoc />
|
||||
public async Task<RoleDetailDto?> 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<Domain.Identity.Entities.Permission>()
|
||||
: 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,
|
||||
|
||||
@@ -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<UserListItemDto>(Array.Empty<UserListItemDto>(), 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<long, string[]>(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<Role>()
|
||||
: await roleRepository.GetByIdsAsync(tenantId, roleIds, cancellationToken);
|
||||
: await roleRepository.GetByIdsAsync(portal, tenantId, roleIds, cancellationToken);
|
||||
var roleCodeMap = roles.ToDictionary(role => role.Id, role => role.Code, EqualityComparer<long>.Default);
|
||||
|
||||
// 5. 组装用户角色编码列表
|
||||
|
||||
@@ -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;
|
||||
/// 权限分页查询处理器。
|
||||
/// </summary>
|
||||
public sealed class SearchPermissionsQueryHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<SearchPermissionsQuery, PagedResult<PermissionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -23,9 +21,8 @@ public sealed class SearchPermissionsQueryHandler(
|
||||
/// <returns>分页结果。</returns>
|
||||
public async Task<PagedResult<PermissionDto>> 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,
|
||||
|
||||
@@ -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(
|
||||
/// <returns>分页结果。</returns>
|
||||
public async Task<PagedResult<RoleDto>> 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<RoleDto>(items, request.Page, request.PageSize, roles.Count);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<PagedResult<UserPermissionDto>> 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<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||||
PortalType portal,
|
||||
long tenantId,
|
||||
IReadOnlyCollection<Domain.Identity.Entities.IdentityUser> 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<Domain.Identity.Entities.Role>()
|
||||
: 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<long>.Default);
|
||||
|
||||
// 3. 查询角色-权限关系
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<Domain.Identity.Entities.RolePermission>()
|
||||
: 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<Domain.Identity.Entities.Permission>()
|
||||
: await permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
: await permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
|
||||
@@ -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. 返回用户详情
|
||||
|
||||
@@ -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;
|
||||
/// 更新菜单处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdateMenuCommandHandler(
|
||||
IMenuRepository menuRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IMenuRepository menuRepository)
|
||||
: IRequestHandler<UpdateMenuCommand, MenuDefinitionDto?>
|
||||
{
|
||||
/// <inheritdoc />
|
||||
@@ -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. 更新字段
|
||||
|
||||
@@ -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;
|
||||
/// 更新权限处理器。
|
||||
/// </summary>
|
||||
public sealed class UpdatePermissionCommandHandler(
|
||||
IPermissionRepository permissionRepository,
|
||||
ITenantProvider tenantProvider)
|
||||
IPermissionRepository permissionRepository)
|
||||
: IRequestHandler<UpdatePermissionCommand, PermissionDto?>
|
||||
{
|
||||
/// <summary>
|
||||
@@ -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,
|
||||
|
||||
@@ -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(
|
||||
/// <returns>更新后的角色 DTO 或 null。</returns>
|
||||
public async Task<RoleDto?> 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,
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class PermissionTreeQuery : IRequest<IReadOnlyList<PermissionTreeDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Portal 类型。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; } = PortalType.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 关键字(可选)。
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
/// </summary>
|
||||
public sealed class SearchPermissionsQuery : IRequest<PagedResult<PermissionDto>>
|
||||
{
|
||||
/// <summary>
|
||||
/// Portal 类型。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; init; } = PortalType.Admin;
|
||||
|
||||
/// <summary>
|
||||
/// 搜索关键字。
|
||||
/// </summary>
|
||||
|
||||
@@ -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<CurrentUserProfile> 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<string[]> ResolveUserRolesAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> 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<string>();
|
||||
}
|
||||
|
||||
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<string[]> ResolveUserPermissionsAsync(long tenantId, long userId, CancellationToken cancellationToken)
|
||||
private async Task<string[]> 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<string>();
|
||||
}
|
||||
|
||||
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<string>();
|
||||
}
|
||||
|
||||
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<Dictionary<long, (string[] roles, string[] permissions)>> ResolveRolesAndPermissionsAsync(
|
||||
long tenantId,
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IReadOnlyCollection<IdentityUser> 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<Role>()
|
||||
: 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<long>.Default);
|
||||
|
||||
var rolePermissions = roleIds.Length == 0
|
||||
? Array.Empty<RolePermission>()
|
||||
: 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<Permission>()
|
||||
: await _permissionRepository.GetByIdsAsync(tenantId, permissionIds, cancellationToken);
|
||||
: await _permissionRepository.GetByIdsAsync(permissionIds, cancellationToken);
|
||||
var permissionCodeMap = permissions.ToDictionary(p => p.Id, p => p.Code, comparer: EqualityComparer<long>.Default);
|
||||
|
||||
var rolePermissionsLookup = rolePermissions
|
||||
|
||||
@@ -4,10 +4,20 @@ using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 管理后台账户实体(平台管理员、租户管理员或商户员工)。
|
||||
/// 后台账户实体(按 Portal 区分平台管理员与租户后台账号)。
|
||||
/// </summary>
|
||||
public sealed class IdentityUser : MultiTenantEntityBase
|
||||
public sealed class IdentityUser : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 账号所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 登录账号。
|
||||
/// </summary>
|
||||
|
||||
@@ -1,12 +1,18 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Shared.Abstractions.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
/// <summary>
|
||||
/// 管理端菜单定义。
|
||||
/// 后台菜单定义(按 Portal 区分 Admin/Tenant 两套菜单树)。
|
||||
/// </summary>
|
||||
public sealed class MenuDefinition : MultiTenantEntityBase
|
||||
public sealed class MenuDefinition : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 菜单所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级菜单 ID,根节点为 0。
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 权限定义。
|
||||
/// </summary>
|
||||
public sealed class Permission : MultiTenantEntityBase
|
||||
public sealed class Permission : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 权限所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 父级权限 ID,根节点为 0。
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 角色定义。
|
||||
/// </summary>
|
||||
public sealed class Role : MultiTenantEntityBase
|
||||
public sealed class Role : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 角色所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色名称。
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 角色-权限关系。
|
||||
/// </summary>
|
||||
public sealed class RolePermission : MultiTenantEntityBase
|
||||
public sealed class RolePermission : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 关系所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 角色 ID。
|
||||
/// </summary>
|
||||
|
||||
@@ -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;
|
||||
/// <summary>
|
||||
/// 用户-角色关系。
|
||||
/// </summary>
|
||||
public sealed class UserRole : MultiTenantEntityBase
|
||||
public sealed class UserRole : AuditableEntityBase
|
||||
{
|
||||
/// <summary>
|
||||
/// 关系所属 Portal。
|
||||
/// </summary>
|
||||
public PortalType Portal { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 所属租户 ID(Portal=Tenant 时必填;Portal=Admin 时必须为空)。
|
||||
/// </summary>
|
||||
public long? TenantId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 用户 ID。
|
||||
/// </summary>
|
||||
|
||||
18
src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs
Normal file
18
src/Domain/TakeoutSaaS.Domain/Identity/Enums/PortalType.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace TakeoutSaaS.Domain.Identity.Enums;
|
||||
|
||||
/// <summary>
|
||||
/// 后台端类型(用于区分平台管理端与租户管理端)。
|
||||
/// </summary>
|
||||
public enum PortalType
|
||||
{
|
||||
/// <summary>
|
||||
/// 平台管理端(Admin)。
|
||||
/// </summary>
|
||||
Admin = 0,
|
||||
|
||||
/// <summary>
|
||||
/// 租户管理端(Tenant)。
|
||||
/// </summary>
|
||||
Tenant = 1
|
||||
}
|
||||
|
||||
@@ -74,14 +74,6 @@ public interface IIdentityUserRepository
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> FindByIdAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(忽略租户过滤器,仅用于只读查询)。
|
||||
/// </summary>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> FindByIdIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,返回可跟踪实体)。
|
||||
/// </summary>
|
||||
@@ -90,28 +82,13 @@ public interface IIdentityUserRepository
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> GetForUpdateAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,忽略租户过滤器)。
|
||||
/// </summary>
|
||||
/// <remarks>用于跨租户场景(如平台生成的重置密码链接)。</remarks>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> GetForUpdateIgnoringTenantAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 获取后台用户(用于更新,包含已删除数据)。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户或 null。</returns>
|
||||
Task<IdentityUser?> GetForUpdateIncludingDeletedAsync(
|
||||
long tenantId,
|
||||
long userId,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
Task<IdentityUser?> GetForUpdateIncludingDeletedAsync(long userId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按租户与关键字查询后台用户列表(仅读)。
|
||||
@@ -126,12 +103,10 @@ public interface IIdentityUserRepository
|
||||
/// 分页查询后台用户列表。
|
||||
/// </summary>
|
||||
/// <param name="filter">查询过滤条件。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤器。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>分页结果。</returns>
|
||||
Task<(IReadOnlyList<IdentityUser> Items, int Total)> SearchPagedAsync(
|
||||
IdentityUserSearchFilter filter,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
@@ -149,14 +124,12 @@ public interface IIdentityUserRepository
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="userIds">用户 ID 集合。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤器。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>后台用户列表。</returns>
|
||||
Task<IReadOnlyList<IdentityUser>> GetForUpdateByIdsAsync(
|
||||
long tenantId,
|
||||
IEnumerable<long> userIds,
|
||||
bool includeDeleted,
|
||||
bool ignoreTenantFilter = false,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// 按租户获取菜单列表。
|
||||
/// 按 Portal 获取菜单列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<MenuDefinition>> GetByTenantAsync(long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task<IReadOnlyList<MenuDefinition>> GetByPortalAsync(PortalType portal, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 查询菜单。
|
||||
/// </summary>
|
||||
Task<MenuDefinition?> FindByIdAsync(long id, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="id">菜单 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task<MenuDefinition?> FindByIdAsync(long id, PortalType portal, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增菜单。
|
||||
@@ -30,7 +36,10 @@ public interface IMenuRepository
|
||||
/// <summary>
|
||||
/// 删除菜单。
|
||||
/// </summary>
|
||||
Task DeleteAsync(long id, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="id">菜单 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task DeleteAsync(long id, PortalType portal, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 持久化变更。
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
using TakeoutSaaS.Domain.Identity.Enums;
|
||||
using TakeoutSaaS.Domain.Identity.Entities;
|
||||
|
||||
namespace TakeoutSaaS.Domain.Identity.Repositories;
|
||||
|
||||
/// <summary>
|
||||
/// 权限仓储。
|
||||
/// 权限仓储(全局权限定义,按 Portal 区分)。
|
||||
/// </summary>
|
||||
public interface IPermissionRepository
|
||||
{
|
||||
@@ -11,46 +12,42 @@ public interface IPermissionRepository
|
||||
/// 根据 ID 查询权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限实体或 null。</returns>
|
||||
Task<Permission?> FindByIdAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Permission?> FindByIdAsync(long permissionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据编码查询权限。
|
||||
/// </summary>
|
||||
/// <param name="code">权限编码。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限实体或 null。</returns>
|
||||
Task<Permission?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Permission?> FindByCodeAsync(string code, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据编码集合查询权限列表。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="codes">权限编码集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限集合。</returns>
|
||||
Task<IReadOnlyList<Permission>> GetByCodesAsync(long tenantId, IEnumerable<string> codes, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Permission>> GetByCodesAsync(IEnumerable<string> codes, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据 ID 集合查询权限列表。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="permissionIds">权限 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限集合。</returns>
|
||||
Task<IReadOnlyList<Permission>> GetByIdsAsync(long tenantId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Permission>> GetByIdsAsync(IEnumerable<long> permissionIds, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按关键字搜索权限。
|
||||
/// 按 Portal 与关键字搜索权限。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="keyword">关键字。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>权限集合。</returns>
|
||||
Task<IReadOnlyList<Permission>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Permission>> SearchAsync(PortalType portal, string? keyword, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增权限。
|
||||
@@ -72,10 +69,9 @@ public interface IPermissionRepository
|
||||
/// 删除权限。
|
||||
/// </summary>
|
||||
/// <param name="permissionId">权限 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步操作任务。</returns>
|
||||
Task DeleteAsync(long permissionId, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(long permissionId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 保存仓储变更。
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// 根据角色 ID 集合获取角色权限关系。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="roleIds">角色 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色权限关系列表。</returns>
|
||||
Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<RolePermission>> GetByRoleIdsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IEnumerable<long> roleIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 批量新增角色权限关系。
|
||||
@@ -27,12 +33,18 @@ public interface IRolePermissionRepository
|
||||
/// <summary>
|
||||
/// 替换角色的权限集合。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="permissionIds">权限 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步操作任务。</returns>
|
||||
Task ReplaceRolePermissionsAsync(long tenantId, long roleId, IEnumerable<long> permissionIds, CancellationToken cancellationToken = default);
|
||||
Task ReplaceRolePermissionsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
long roleId,
|
||||
IEnumerable<long> permissionIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 提交持久化变更。
|
||||
|
||||
@@ -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 查询角色。
|
||||
/// </summary>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色实体或 null。</returns>
|
||||
Task<Role?> FindByIdAsync(long roleId, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Role?> FindByIdAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 根据编码查询角色。
|
||||
/// </summary>
|
||||
/// <param name="code">角色编码。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色实体或 null。</returns>
|
||||
Task<Role?> FindByCodeAsync(string code, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task<Role?> FindByCodeAsync(PortalType portal, long? tenantId, string code, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 批量获取角色列表。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="roleIds">角色 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色集合。</returns>
|
||||
Task<IReadOnlyList<Role>> GetByIdsAsync(long tenantId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Role>> GetByIdsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IEnumerable<long> roleIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 按关键字搜索角色。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="keyword">关键字。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>角色集合。</returns>
|
||||
Task<IReadOnlyList<Role>> SearchAsync(long tenantId, string? keyword, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<Role>> SearchAsync(PortalType portal, long? tenantId, string? keyword, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 新增角色。
|
||||
@@ -63,10 +72,11 @@ public interface IRoleRepository
|
||||
/// 删除角色。
|
||||
/// </summary>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步操作任务。</returns>
|
||||
Task DeleteAsync(long roleId, long tenantId, CancellationToken cancellationToken = default);
|
||||
Task DeleteAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 保存仓储变更。
|
||||
|
||||
@@ -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
|
||||
/// <summary>
|
||||
/// 批量获取指定用户的角色关系。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="userIds">用户 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>用户角色关系集合。</returns>
|
||||
Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(long tenantId, IEnumerable<long> userIds, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<UserRole>> GetByUserIdsAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
IEnumerable<long> userIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 获取单个用户的角色关系。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>指定用户的角色关系列表。</returns>
|
||||
Task<IReadOnlyList<UserRole>> GetByUserIdAsync(long tenantId, long userId, CancellationToken cancellationToken = default);
|
||||
Task<IReadOnlyList<UserRole>> GetByUserIdAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
long userId,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 替换用户的角色列表。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="userId">用户 ID。</param>
|
||||
/// <param name="roleIds">角色 ID 集合。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>异步操作任务。</returns>
|
||||
Task ReplaceUserRolesAsync(long tenantId, long userId, IEnumerable<long> roleIds, CancellationToken cancellationToken = default);
|
||||
Task ReplaceUserRolesAsync(
|
||||
PortalType portal,
|
||||
long? tenantId,
|
||||
long userId,
|
||||
IEnumerable<long> roleIds,
|
||||
CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 统计指定角色下的用户数量。
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="portal">Portal 类型。</param>
|
||||
/// <param name="tenantId">租户 ID(Portal=Tenant 必填;Portal=Admin 必须为空)。</param>
|
||||
/// <param name="roleId">角色 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <returns>用户数量。</returns>
|
||||
Task<int> CountUsersByRoleAsync(long tenantId, long roleId, CancellationToken cancellationToken = default);
|
||||
Task<int> CountUsersByRoleAsync(PortalType portal, long? tenantId, long roleId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 提交持久化变更。
|
||||
|
||||
@@ -11,7 +11,11 @@ public interface IStoreRepository
|
||||
/// <summary>
|
||||
/// 依据标识获取门店。
|
||||
/// </summary>
|
||||
Task<Store?> FindByIdAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<Store?> FindByIdAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取指定商户的门店列表。
|
||||
@@ -22,14 +26,14 @@ public interface IStoreRepository
|
||||
/// 按租户筛选门店列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<Store>> 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);
|
||||
|
||||
/// <summary>
|
||||
@@ -51,12 +55,20 @@ public interface IStoreRepository
|
||||
/// <summary>
|
||||
/// 获取门店营业时段。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<IReadOnlyList<StoreBusinessHour>> GetBusinessHoursAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店费用配置。
|
||||
/// </summary>
|
||||
Task<StoreFee?> GetStoreFeeAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<StoreFee?> GetStoreFeeAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 新增门店费用配置。
|
||||
@@ -71,7 +83,11 @@ public interface IStoreRepository
|
||||
/// <summary>
|
||||
/// 获取门店资质列表。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<StoreQualification>> GetQualificationsAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<IReadOnlyList<StoreQualification>> GetQualificationsAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 依据标识获取门店资质。
|
||||
@@ -111,22 +127,38 @@ public interface IStoreRepository
|
||||
/// <summary>
|
||||
/// 获取门店配送区域配置。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<IReadOnlyList<StoreDeliveryZone>> GetDeliveryZonesAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 依据标识获取配送区域。
|
||||
/// </summary>
|
||||
Task<StoreDeliveryZone?> FindDeliveryZoneByIdAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="deliveryZoneId">配送区域 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<StoreDeliveryZone?> FindDeliveryZoneByIdAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店节假日配置。
|
||||
/// </summary>
|
||||
Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="storeId">门店 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<IReadOnlyList<StoreHoliday>> GetHolidaysAsync(long storeId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 依据标识获取节假日配置。
|
||||
/// </summary>
|
||||
Task<StoreHoliday?> FindHolidayByIdAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="holidayId">节假日配置 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
Task<StoreHoliday?> FindHolidayByIdAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default, bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取门店桌台区域。
|
||||
@@ -276,12 +308,18 @@ public interface IStoreRepository
|
||||
/// <summary>
|
||||
/// 删除配送区域。
|
||||
/// </summary>
|
||||
Task DeleteDeliveryZoneAsync(long deliveryZoneId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="deliveryZoneId">配送区域 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task DeleteDeliveryZoneAsync(long deliveryZoneId, long? tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除节假日。
|
||||
/// </summary>
|
||||
Task DeleteHolidayAsync(long holidayId, long tenantId, CancellationToken cancellationToken = default);
|
||||
/// <param name="holidayId">节假日配置 ID。</param>
|
||||
/// <param name="tenantId">租户 ID(为空则不做租户过滤)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
Task DeleteHolidayAsync(long holidayId, long? tenantId, CancellationToken cancellationToken = default);
|
||||
|
||||
/// <summary>
|
||||
/// 删除桌台区域。
|
||||
|
||||
@@ -15,60 +15,60 @@ public interface ISubscriptionRepository
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>订阅实体,未找到返回 null。</returns>
|
||||
Task<TenantSubscription?> FindByIdAsync(
|
||||
long subscriptionId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 按 ID 列表批量查询订阅。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionIds">订阅 ID 列表。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>订阅实体列表。</returns>
|
||||
Task<IReadOnlyList<TenantSubscription>> FindByIdsAsync(
|
||||
IEnumerable<long> subscriptionIds,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 分页查询订阅列表(含关联信息)。
|
||||
/// </summary>
|
||||
/// <param name="filter">查询过滤条件。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>分页结果。</returns>
|
||||
Task<(IReadOnlyList<SubscriptionWithRelations> Items, int Total)> SearchPagedAsync(
|
||||
SubscriptionSearchFilter filter,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 获取订阅详情(含关联信息)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionId">订阅 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>订阅详情信息。</returns>
|
||||
Task<SubscriptionDetailInfo?> GetDetailAsync(
|
||||
long subscriptionId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 按 ID 列表批量查询订阅(含租户信息)。
|
||||
/// </summary>
|
||||
/// <param name="subscriptionIds">订阅 ID 列表。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>订阅与租户信息列表。</returns>
|
||||
Task<IReadOnlyList<SubscriptionWithTenant>> FindByIdsWithTenantAsync(
|
||||
IEnumerable<long> subscriptionIds,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 查询自动续费候选订阅(活跃 + 开启自动续费 + 即将到期)。
|
||||
@@ -76,13 +76,13 @@ public interface ISubscriptionRepository
|
||||
/// <param name="now">当前时间(UTC)。</param>
|
||||
/// <param name="renewalThreshold">续费阈值时间(UTC),到期时间小于等于该时间视为候选。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>候选订阅集合(含套餐信息)。</returns>
|
||||
Task<IReadOnlyList<AutoRenewalCandidate>> FindAutoRenewalCandidatesAsync(
|
||||
DateTime now,
|
||||
DateTime renewalThreshold,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 查询续费提醒候选订阅(活跃 + 未开启自动续费 + 到期时间落在指定日期范围)。
|
||||
@@ -90,25 +90,25 @@ public interface ISubscriptionRepository
|
||||
/// <param name="startOfDay">筛选开始时间(UTC,含)。</param>
|
||||
/// <param name="endOfDay">筛选结束时间(UTC,不含)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>候选订阅集合(含租户与套餐信息)。</returns>
|
||||
Task<IReadOnlyList<RenewalReminderCandidate>> FindRenewalReminderCandidatesAsync(
|
||||
DateTime startOfDay,
|
||||
DateTime endOfDay,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 查询已到期仍处于 Active 的订阅(用于进入宽限期)。
|
||||
/// </summary>
|
||||
/// <param name="now">当前时间(UTC)。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>到期订阅集合。</returns>
|
||||
Task<IReadOnlyList<TenantSubscription>> FindExpiredActiveSubscriptionsAsync(
|
||||
DateTime now,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
/// <summary>
|
||||
/// 查询宽限期已结束的订阅(用于自动暂停)。
|
||||
@@ -116,13 +116,13 @@ public interface ISubscriptionRepository
|
||||
/// <param name="now">当前时间(UTC)。</param>
|
||||
/// <param name="gracePeriodDays">宽限期天数。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>宽限期到期订阅集合。</returns>
|
||||
Task<IReadOnlyList<TenantSubscription>> FindGracePeriodExpiredSubscriptionsAsync(
|
||||
DateTime now,
|
||||
int gracePeriodDays,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
#endregion
|
||||
|
||||
@@ -177,12 +177,12 @@ public interface ISubscriptionRepository
|
||||
/// </summary>
|
||||
/// <param name="tenantId">租户 ID。</param>
|
||||
/// <param name="cancellationToken">取消标记。</param>
|
||||
/// <param name="ignoreTenantFilter">是否忽略租户过滤(用于平台级查询/任务)。</param>
|
||||
/// <param name="includeDeleted">是否包含已删除数据。</param>
|
||||
/// <returns>配额使用列表。</returns>
|
||||
Task<IReadOnlyList<TenantQuotaUsage>> GetQuotaUsagesAsync(
|
||||
long tenantId,
|
||||
CancellationToken cancellationToken = default,
|
||||
bool ignoreTenantFilter = false);
|
||||
bool includeDeleted = false);
|
||||
|
||||
#endregion
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user