Files
TakeoutSaaS.TenantApi/src/Application/TakeoutSaaS.Application/App/Tenants/Handlers/BindInitialTenantSubscriptionCommandHandler.cs

113 lines
4.5 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// 租户初次绑定订阅处理器。
/// </summary>
public sealed class BindInitialTenantSubscriptionCommandHandler(
ITenantRepository tenantRepository,
IIdGenerator idGenerator,
ITenantProvider tenantProvider)
: IRequestHandler<BindInitialTenantSubscriptionCommand, TenantSubscriptionDto>
{
private static readonly ConcurrentDictionary<long, SemaphoreSlim> TenantLocks = new();
/// <inheritdoc />
public async Task<TenantSubscriptionDto> 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));
}