using MediatR;
using Microsoft.AspNetCore.Identity;
using Microsoft.Extensions.Logging;
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.Repositories;
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.Security;
using TakeoutSaaS.Shared.Abstractions.Tenancy;
namespace TakeoutSaaS.Application.App.Tenants.Handlers;
///
/// 后台手动新增租户处理器。
///
public sealed class CreateTenantManuallyCommandHandler(
ITenantRepository tenantRepository,
ITenantPackageRepository tenantPackageRepository,
IIdentityUserRepository identityUserRepository,
IRoleRepository roleRepository,
IPasswordHasher passwordHasher,
IIdGenerator idGenerator,
IMediator mediator,
ITenantContextAccessor tenantContextAccessor,
ICurrentUserAccessor currentUserAccessor,
ILogger logger)
: IRequestHandler
{
///
public async Task Handle(CreateTenantManuallyCommand request, CancellationToken cancellationToken)
{
// 1. 校验订阅时长
if (request.DurationMonths <= 0)
{
throw new BusinessException(ErrorCodes.ValidationFailed, "订阅时长必须大于 0");
}
// 2. 校验租户编码唯一性
var normalizedCode = request.Code.Trim();
if (await tenantRepository.ExistsByCodeAsync(normalizedCode, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"租户编码 {normalizedCode} 已存在");
}
// 3. (空行后) 校验联系人手机号唯一性(仅当填写时)
if (!string.IsNullOrWhiteSpace(request.ContactPhone))
{
var normalizedPhone = request.ContactPhone.Trim();
if (await tenantRepository.ExistsByContactPhoneAsync(normalizedPhone, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"手机号 {normalizedPhone} 已注册");
}
}
// 4. (空行后) 校验管理员账号唯一性
var normalizedAccount = request.AdminAccount.Trim();
if (await identityUserRepository.ExistsByAccountAsync(normalizedAccount, cancellationToken))
{
throw new BusinessException(ErrorCodes.Conflict, $"账号 {normalizedAccount} 已存在");
}
// 5. (空行后) 校验套餐存在且可用
var package = await tenantPackageRepository.FindByIdAsync(request.TenantPackageId, cancellationToken)
?? throw new BusinessException(ErrorCodes.NotFound, "套餐不存在");
if (!package.IsActive)
{
throw new BusinessException(ErrorCodes.BadRequest, "套餐未启用,无法绑定订阅");
}
// 6. (空行后) 计算订阅生效与到期时间(UTC)
var now = DateTime.UtcNow;
var subscriptionEffectiveFrom = request.SubscriptionEffectiveFrom ?? now;
var subscriptionEffectiveTo = subscriptionEffectiveFrom.AddMonths(request.DurationMonths);
// 7. (空行后) 构建租户与订阅
var tenantId = idGenerator.NextId();
var tenant = new Tenant
{
Id = tenantId,
Code = normalizedCode,
Name = request.Name.Trim(),
ShortName = request.ShortName,
LegalEntityName = request.LegalEntityName,
Industry = request.Industry,
LogoUrl = request.LogoUrl,
CoverImageUrl = request.CoverImageUrl,
Website = request.Website,
Country = request.Country,
Province = request.Province,
City = request.City,
Address = request.Address,
ContactName = request.ContactName,
ContactPhone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(),
ContactEmail = request.ContactEmail,
Status = request.TenantStatus,
EffectiveFrom = subscriptionEffectiveFrom,
EffectiveTo = subscriptionEffectiveTo,
SuspendedAt = request.SuspendedAt,
SuspensionReason = request.SuspensionReason,
Tags = request.Tags,
Remarks = request.Remarks
};
// 8. (空行后) 构建订阅实体
var subscription = new TenantSubscription
{
Id = idGenerator.NextId(),
TenantId = tenantId,
TenantPackageId = request.TenantPackageId,
EffectiveFrom = subscriptionEffectiveFrom,
EffectiveTo = subscriptionEffectiveTo,
NextBillingDate = request.NextBillingDate ?? subscriptionEffectiveTo,
Status = request.SubscriptionStatus,
AutoRenew = request.AutoRenew,
ScheduledPackageId = request.ScheduledPackageId,
Notes = request.SubscriptionNotes
};
// 9. (空行后) 构建认证资料(默认直接通过)
var actorName = currentUserAccessor.IsAuthenticated
? $"user:{currentUserAccessor.UserId}"
: "system";
var verification = new TenantVerificationProfile
{
Id = idGenerator.NextId(),
TenantId = tenantId,
Status = request.VerificationStatus,
BusinessLicenseNumber = request.BusinessLicenseNumber,
BusinessLicenseUrl = request.BusinessLicenseUrl,
LegalPersonName = request.LegalPersonName,
LegalPersonIdNumber = request.LegalPersonIdNumber,
LegalPersonIdFrontUrl = request.LegalPersonIdFrontUrl,
LegalPersonIdBackUrl = request.LegalPersonIdBackUrl,
BankAccountName = request.BankAccountName,
BankAccountNumber = request.BankAccountNumber,
BankName = request.BankName,
AdditionalDataJson = request.AdditionalDataJson,
SubmittedAt = request.SubmittedAt ?? now,
ReviewedAt = request.ReviewedAt ?? now,
ReviewedBy = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
ReviewedByName = string.IsNullOrWhiteSpace(request.ReviewedByName) ? actorName : request.ReviewedByName,
ReviewRemarks = request.ReviewRemarks
};
// 10. (空行后) 写入审计日志与订阅历史
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
{
TenantId = tenantId,
Action = TenantAuditAction.RegistrationSubmitted,
Title = "后台手动创建",
Description = $"绑定套餐 {request.TenantPackageId},订阅 {request.DurationMonths} 月",
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
OperatorName = actorName,
PreviousStatus = null,
CurrentStatus = tenant.Status
}, cancellationToken);
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
{
TenantId = tenantId,
Action = TenantAuditAction.SubscriptionUpdated,
Title = "订阅初始化",
Description = $"生效:{subscription.EffectiveFrom:yyyy-MM-dd HH:mm:ss},到期:{subscription.EffectiveTo:yyyy-MM-dd HH:mm:ss}",
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
OperatorName = actorName,
PreviousStatus = null,
CurrentStatus = tenant.Status
}, cancellationToken);
await tenantRepository.AddAuditLogAsync(new TenantAuditLog
{
TenantId = tenantId,
Action = TenantAuditAction.VerificationApproved,
Title = "认证已通过",
Description = request.ReviewRemarks,
OperatorId = currentUserAccessor.UserId == 0 ? null : currentUserAccessor.UserId,
OperatorName = actorName,
PreviousStatus = null,
CurrentStatus = tenant.Status
}, cancellationToken);
await tenantRepository.AddSubscriptionHistoryAsync(new TenantSubscriptionHistory
{
TenantId = tenantId,
TenantSubscriptionId = subscription.Id,
FromPackageId = request.TenantPackageId,
ToPackageId = request.TenantPackageId,
ChangeType = SubscriptionChangeType.New,
EffectiveFrom = subscription.EffectiveFrom,
EffectiveTo = subscription.EffectiveTo,
Amount = null,
Currency = null,
Notes = request.SubscriptionNotes
}, cancellationToken);
// 11. (空行后) 持久化租户、订阅与认证资料
await tenantRepository.AddTenantAsync(tenant, cancellationToken);
await tenantRepository.AddSubscriptionAsync(subscription, cancellationToken);
await tenantRepository.UpsertVerificationProfileAsync(verification, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
// 12. (空行后) 临时切换租户上下文,保证身份与权限写入正确
var previousContext = tenantContextAccessor.Current;
tenantContextAccessor.Current = new TenantContext(tenant.Id, tenant.Code, "manual-create");
try
{
// 13. 创建租户管理员账号
var adminUser = new IdentityUser
{
TenantId = tenant.Id,
Account = normalizedAccount,
DisplayName = request.AdminDisplayName.Trim(),
PasswordHash = string.Empty,
Phone = string.IsNullOrWhiteSpace(request.ContactPhone) ? null : request.ContactPhone.Trim(),
Email = string.IsNullOrWhiteSpace(request.ContactEmail) ? null : request.ContactEmail.Trim(),
MerchantId = request.AdminMerchantId,
Avatar = request.AdminAvatar
};
adminUser.PasswordHash = passwordHasher.HashPassword(adminUser, request.AdminPassword);
await identityUserRepository.AddAsync(adminUser, cancellationToken);
await identityUserRepository.SaveChangesAsync(cancellationToken);
// 14. (空行后) 初始化租户管理员角色模板并绑定角色
await mediator.Send(new InitializeRoleTemplatesCommand
{
TemplateCodes = new[] { "tenant-admin" }
}, cancellationToken);
var tenantAdminRole = await roleRepository.FindByCodeAsync("tenant-admin", tenant.Id, cancellationToken);
if (tenantAdminRole != null)
{
await mediator.Send(new AssignUserRolesCommand
{
UserId = adminUser.Id,
RoleIds = new[] { tenantAdminRole.Id }
}, cancellationToken);
}
// 15. (空行后) 回写租户所有者账号
tenant.PrimaryOwnerUserId = adminUser.Id;
await tenantRepository.UpdateTenantAsync(tenant, cancellationToken);
await tenantRepository.SaveChangesAsync(cancellationToken);
}
finally
{
// 16. 恢复上下文
tenantContextAccessor.Current = previousContext;
}
// 17. (空行后) 返回创建结果
logger.LogInformation("已后台手动创建租户 {TenantCode}", tenant.Code);
return new TenantDetailDto
{
Tenant = TenantMapping.ToDto(tenant, subscription, verification),
Verification = verification.ToVerificationDto(),
Subscription = subscription.ToSubscriptionDto(),
Package = package.ToDto()
};
}
}