131 lines
4.9 KiB
C#
131 lines
4.9 KiB
C#
using MediatR;
|
|
using TakeoutSaaS.Application.App.Tenants.Commands;
|
|
using TakeoutSaaS.Application.App.Tenants.Dto;
|
|
using TakeoutSaaS.Domain.Tenants.Entities;
|
|
using TakeoutSaaS.Domain.Tenants.Enums;
|
|
using TakeoutSaaS.Domain.Tenants.Repositories;
|
|
using TakeoutSaaS.Shared.Abstractions.Constants;
|
|
using TakeoutSaaS.Shared.Abstractions.Exceptions;
|
|
using TakeoutSaaS.Shared.Abstractions.Tenancy;
|
|
|
|
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
|
|
|
|
/// <summary>
|
|
/// 配额校验处理器。
|
|
/// </summary>
|
|
public sealed class CheckTenantQuotaCommandHandler(
|
|
ITenantRepository tenantRepository,
|
|
ITenantPackageRepository packageRepository,
|
|
ITenantQuotaUsageRepository quotaUsageRepository,
|
|
ITenantProvider tenantProvider)
|
|
: IRequestHandler<CheckTenantQuotaCommand, QuotaCheckResultDto>
|
|
{
|
|
/// <inheritdoc />
|
|
public async Task<QuotaCheckResultDto> Handle(CheckTenantQuotaCommand request, CancellationToken cancellationToken)
|
|
{
|
|
// 1. 校验请求参数
|
|
if (request.Delta <= 0)
|
|
{
|
|
throw new BusinessException(ErrorCodes.BadRequest, "配额消耗量必须大于 0");
|
|
}
|
|
|
|
// 2. 校验租户上下文
|
|
var currentTenantId = tenantProvider.GetCurrentTenantId();
|
|
if (currentTenantId == 0 || currentTenantId != request.TenantId)
|
|
{
|
|
throw new BusinessException(ErrorCodes.Forbidden, "租户上下文不匹配,请在请求头 X-Tenant-Id 指定目标租户");
|
|
}
|
|
|
|
// 3. 获取租户与当前订阅
|
|
_ = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken)
|
|
?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在");
|
|
|
|
var subscription = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken);
|
|
if (subscription == null || subscription.EffectiveTo <= DateTime.UtcNow)
|
|
{
|
|
throw new BusinessException(ErrorCodes.Conflict, "订阅不存在或已到期");
|
|
}
|
|
|
|
var package = await packageRepository.FindByIdAsync(subscription.TenantPackageId, cancellationToken)
|
|
?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
|
|
|
|
var limit = ResolveLimit(package, request.QuotaType);
|
|
|
|
// 4. 加载配额使用记录并计算
|
|
var usage = await quotaUsageRepository.FindAsync(request.TenantId, request.QuotaType, cancellationToken)
|
|
?? new TenantQuotaUsage
|
|
{
|
|
TenantId = request.TenantId,
|
|
QuotaType = request.QuotaType,
|
|
LimitValue = limit ?? 0,
|
|
UsedValue = 0,
|
|
ResetCycle = ResolveResetCycle(request.QuotaType)
|
|
};
|
|
|
|
var usedAfter = usage.UsedValue + request.Delta;
|
|
if (limit.HasValue && usedAfter > (decimal)limit.Value)
|
|
{
|
|
usage.LimitValue = limit.Value;
|
|
await PersistUsageAsync(usage, quotaUsageRepository, cancellationToken);
|
|
throw new BusinessException(ErrorCodes.Conflict, $"{request.QuotaType} 配额不足");
|
|
}
|
|
|
|
// 5. 更新使用并保存
|
|
usage.LimitValue = limit ?? usage.LimitValue;
|
|
usage.UsedValue = usedAfter;
|
|
usage.ResetCycle ??= ResolveResetCycle(request.QuotaType);
|
|
|
|
await PersistUsageAsync(usage, quotaUsageRepository, cancellationToken);
|
|
|
|
// 6. 返回结果
|
|
return new QuotaCheckResultDto
|
|
{
|
|
QuotaType = request.QuotaType,
|
|
Limit = limit,
|
|
Used = usage.UsedValue,
|
|
Remaining = limit.HasValue ? limit.Value - usage.UsedValue : null
|
|
};
|
|
}
|
|
|
|
private static decimal? ResolveLimit(TenantPackage package, TenantQuotaType quotaType)
|
|
{
|
|
return quotaType switch
|
|
{
|
|
TenantQuotaType.StoreCount => package.MaxStoreCount,
|
|
TenantQuotaType.AccountCount => package.MaxAccountCount,
|
|
TenantQuotaType.Storage => package.MaxStorageGb,
|
|
TenantQuotaType.SmsCredits => package.MaxSmsCredits,
|
|
TenantQuotaType.DeliveryOrders => package.MaxDeliveryOrders,
|
|
_ => null
|
|
};
|
|
}
|
|
|
|
private static string ResolveResetCycle(TenantQuotaType quotaType)
|
|
{
|
|
return quotaType switch
|
|
{
|
|
TenantQuotaType.SmsCredits => "monthly",
|
|
TenantQuotaType.DeliveryOrders => "monthly",
|
|
_ => "lifetime"
|
|
};
|
|
}
|
|
|
|
private static async Task PersistUsageAsync(
|
|
TenantQuotaUsage usage,
|
|
ITenantQuotaUsageRepository quotaUsageRepository,
|
|
CancellationToken cancellationToken)
|
|
{
|
|
// 判断是否为新增。
|
|
if (usage.Id == 0)
|
|
{
|
|
await quotaUsageRepository.AddAsync(usage, cancellationToken);
|
|
}
|
|
else
|
|
{
|
|
await quotaUsageRepository.UpdateAsync(usage, cancellationToken);
|
|
}
|
|
|
|
await quotaUsageRepository.SaveChangesAsync(cancellationToken);
|
|
}
|
|
}
|