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; /// /// 配额校验处理器。 /// public sealed class CheckTenantQuotaCommandHandler( ITenantRepository tenantRepository, ITenantPackageRepository packageRepository, ITenantQuotaUsageRepository quotaUsageRepository, ITenantProvider tenantProvider) : IRequestHandler { /// public async Task 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); } }