refactor: 管理端去租户过滤并Portal化RBAC菜单

This commit is contained in:
2026-01-29 10:46:49 +00:00
parent ea9c20d8a9
commit b3639ff34b
115 changed files with 1106 additions and 1092 deletions

View File

@@ -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, "门店不存在");

View File

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

View File

@@ -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. (空行后) 计算距离

View File

@@ -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());

View File

@@ -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, "商户不存在");

View File

@@ -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, "门店不存在");

View File

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

View File

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

View File

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

View File

@@ -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);
}
}

View File

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

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,4 +12,9 @@ public sealed class GetStoreByIdQuery : IRequest<StoreDto?>
/// 门店 ID。
/// </summary>
public long StoreId { get; init; }
/// <summary>
/// 是否包含已软删除数据。
/// </summary>
public bool IncludeDeleted { get; init; }
}

View File

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

View File

@@ -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);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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. 遍历候选订阅,生成账单

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,4 +12,9 @@ public sealed record GetSubscriptionDetailQuery : IRequest<SubscriptionDetailDto
/// 订阅 ID。
/// </summary>
public long SubscriptionId { get; init; }
/// <summary>
/// 是否包含已软删除数据。
/// </summary>
public bool IncludeDeleted { get; init; }
}

View File

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

View File

@@ -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");
}
}

View File

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

View File

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