using MediatR; using System.Collections.Concurrent; using TakeoutSaaS.Application.App.Tenants.Commands; using TakeoutSaaS.Application.App.Tenants.Dto; using TakeoutSaaS.Application.App.Tenants; 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.Ids; using TakeoutSaaS.Shared.Abstractions.Tenancy; namespace TakeoutSaaS.Application.App.Tenants.Handlers; /// /// 租户初次绑定订阅处理器。 /// public sealed class BindInitialTenantSubscriptionCommandHandler( ITenantRepository tenantRepository, IIdGenerator idGenerator, ITenantProvider tenantProvider) : IRequestHandler { private static readonly ConcurrentDictionary TenantLocks = new(); /// public async Task Handle(BindInitialTenantSubscriptionCommand request, CancellationToken cancellationToken) { // 1. 校验租户上下文 var currentTenantId = tenantProvider.GetCurrentTenantId(); if (currentTenantId == 0 || currentTenantId != request.TenantId) { throw new BusinessException(ErrorCodes.Forbidden, "租户上下文不匹配,请在请求头 X-Tenant-Id 指定目标租户"); } // 1.2 获取租户级幂等锁,避免并发重复创建 var tenantLock = GetTenantLock(request.TenantId); await tenantLock.WaitAsync(cancellationToken); try { // 2. 获取租户 var tenant = await tenantRepository.FindByIdAsync(request.TenantId, cancellationToken) ?? throw new BusinessException(ErrorCodes.NotFound, "租户不存在"); // 3. 幂等校验:若已存在订阅则直接返回 var existing = await tenantRepository.GetActiveSubscriptionAsync(request.TenantId, cancellationToken); if (existing is not null) { return existing.ToSubscriptionDto() ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅读取失败"); } // 4. 创建 0 个月订阅(待支付/待生效) var now = DateTime.UtcNow; var subscription = new TenantSubscription { Id = idGenerator.NextId(), TenantId = tenant.Id, TenantPackageId = request.TenantPackageId, EffectiveFrom = now, EffectiveTo = now, NextBillingDate = now, Status = SubscriptionStatus.Pending, AutoRenew = request.AutoRenew, Notes = "初次绑定订阅" }; // 5. 记录订阅与历史 await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken); await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory { Id = idGenerator.NextId(), TenantId = tenant.Id, TenantSubscriptionId = subscription.Id, FromPackageId = request.TenantPackageId, ToPackageId = request.TenantPackageId, ChangeType = SubscriptionChangeType.New, EffectiveFrom = now, EffectiveTo = now, Amount = null, Currency = null, Notes = "初次绑定订阅(0 个月)" }, cancellationToken); // 6. 记录审计日志 await tenantRepository.AddAuditLogAsync(new TenantAuditLog { TenantId = tenant.Id, Action = TenantAuditAction.SubscriptionUpdated, Title = "初次绑定订阅", Description = $"套餐 {request.TenantPackageId},时长 0 月" }, cancellationToken); // 7. 保存变更 await tenantRepository.SaveChangesAsync(cancellationToken); // 8. 返回 DTO return subscription.ToSubscriptionDto() ?? throw new BusinessException(ErrorCodes.InternalServerError, "订阅绑定失败"); } finally { // 9. 释放幂等锁 tenantLock.Release(); } } // 获取或创建租户级幂等锁实例。 private static SemaphoreSlim GetTenantLock(long tenantId) => TenantLocks.GetOrAdd(tenantId, _ => new SemaphoreSlim(1, 1)); }