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() }; } }