From 0d64b61169ae57752677ae3fb976ddb3fb4fc85e Mon Sep 17 00:00:00 2001 From: movingsam Date: Thu, 19 Feb 2026 21:43:24 +0800 Subject: [PATCH] refactor(console): migrate tenant management to TenantManager pattern - Replace TenantRepository with TenantManager (ASP.NET Identity style) - Change TenantId from long to int (auto-increment) - Add TenantStore with CRUD operations - Update TenantService, UserService, RoleService to use TenantManager - Add Tenant entity with TenantStatus enum - Update DTOs and controllers for int tenant IDs --- Controllers/TenantsController.cs | 16 +- Datas/ApplicationDbContext.cs | 12 +- .../TenantEntityTypeConfiguration.cs | 36 ++++ Managers/TenantManager.cs | 88 ++++++++ Models/Dtos/TenantDto.cs | 4 +- Models/Dtos/TenantQueryDto.cs | 2 +- Models/Entities/ApplicationRole.cs | 2 +- Models/Entities/ApplicationUser.cs | 3 +- Models/Entities/Tenant.cs | 25 +++ Program.cs | 6 +- Repositories/UserRepository.cs | 4 +- Services/H5LinkService.cs | 17 +- Services/RoleService.cs | 11 +- Services/TenantService.cs | 117 +++++++---- Services/UserService.cs | 32 +-- Stores/ITenantStore.cs | 41 ++++ Stores/TenantStore.cs | 196 ++++++++++++++++++ 17 files changed, 519 insertions(+), 93 deletions(-) create mode 100644 EntityConfigurations/TenantEntityTypeConfiguration.cs create mode 100644 Managers/TenantManager.cs create mode 100644 Models/Entities/Tenant.cs create mode 100644 Stores/ITenantStore.cs create mode 100644 Stores/TenantStore.cs diff --git a/Controllers/TenantsController.cs b/Controllers/TenantsController.cs index db4eaa3..fd1eb0c 100644 --- a/Controllers/TenantsController.cs +++ b/Controllers/TenantsController.cs @@ -68,7 +68,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(typeof(TenantDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task> GetTenant(long id) + public async Task> GetTenant(int id) { try { @@ -100,7 +100,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task>> GetTenantUsers(long tenantId) + public async Task>> GetTenantUsers(int tenantId) { try { @@ -132,7 +132,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task>> GetTenantRoles(long tenantId) + public async Task>> GetTenantRoles(int tenantId) { try { @@ -164,7 +164,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(typeof(TenantSettingsDto), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task> GetTenantSettings(long id) + public async Task> GetTenantSettings(int id) { try { @@ -196,7 +196,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task UpdateTenantSettings(long id, [FromBody] TenantSettingsDto settings) + public async Task UpdateTenantSettings(int id, [FromBody] TenantSettingsDto settings) { try { @@ -253,7 +253,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task UpdateTenant(long id, [FromBody] UpdateTenantDto dto) + public async Task UpdateTenant(int id, [FromBody] UpdateTenantDto dto) { try { @@ -284,7 +284,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task DeleteTenant(long id) + public async Task DeleteTenant(int id) { try { @@ -316,7 +316,7 @@ public class TenantsController : ControllerBase [ProducesResponseType(typeof(H5LinkResult), StatusCodes.Status200OK)] [ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)] [ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)] - public async Task> GetH5Link(long id) + public async Task> GetH5Link(int id) { try { diff --git a/Datas/ApplicationDbContext.cs b/Datas/ApplicationDbContext.cs index b23bad5..5097451 100644 --- a/Datas/ApplicationDbContext.cs +++ b/Datas/ApplicationDbContext.cs @@ -1,12 +1,14 @@ using Fengling.Console.Models.Entities; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; +using System.Reflection; namespace Fengling.Console.Datas; public class ApplicationDbContext(DbContextOptions options) : IdentityDbContext(options) { + public DbSet Tenants { get; set; } public DbSet AccessLogs { get; set; } public DbSet AuditLogs { get; set; } @@ -14,19 +16,13 @@ public class ApplicationDbContext(DbContextOptions options { base.OnModelCreating(builder); + builder.ApplyConfigurationsFromAssembly(Assembly.GetExecutingAssembly()); + builder.Entity(entity => { entity.Property(e => e.RealName).HasMaxLength(100); entity.Property(e => e.Phone).HasMaxLength(20); entity.HasIndex(e => e.Phone).IsUnique(); - - entity.OwnsOne(e => e.TenantInfo, navigationBuilder => - { - navigationBuilder.Property(e => e.TenantCode).HasColumnName("TenantCode"); - navigationBuilder.Property(e => e.TenantId).HasColumnName("TenantId"); - navigationBuilder.Property(e => e.TenantName).HasColumnName("TenantName"); - navigationBuilder.WithOwner(); - }); }); builder.Entity(entity => { entity.Property(e => e.Description).HasMaxLength(200); }); diff --git a/EntityConfigurations/TenantEntityTypeConfiguration.cs b/EntityConfigurations/TenantEntityTypeConfiguration.cs new file mode 100644 index 0000000..05f9d9c --- /dev/null +++ b/EntityConfigurations/TenantEntityTypeConfiguration.cs @@ -0,0 +1,36 @@ +namespace Fengling.Console.EntityConfigurations; + +using Fengling.Console.Models.Entities; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Metadata.Builders; + +public class TenantEntityTypeConfiguration : IEntityTypeConfiguration +{ + public void Configure(EntityTypeBuilder builder) + { + builder.ToTable("Tenants"); + builder.HasKey(x => x.Id); + + builder.Property(x => x.Id) + .ValueGeneratedOnAdd(); + + builder.Property(x => x.TenantCode) + .IsRequired() + .HasMaxLength(50); + + builder.Property(x => x.Name) + .IsRequired() + .HasMaxLength(100); + + builder.Property(x => x.ContactName) + .IsRequired() + .HasMaxLength(50); + + builder.Property(x => x.ContactEmail) + .IsRequired() + .HasMaxLength(100); + + builder.HasIndex(x => x.TenantCode).IsUnique(); + builder.HasIndex(x => x.Name); + } +} diff --git a/Managers/TenantManager.cs b/Managers/TenantManager.cs new file mode 100644 index 0000000..6b5e8be --- /dev/null +++ b/Managers/TenantManager.cs @@ -0,0 +1,88 @@ +using Fengling.Console.Models.Entities; +using Fengling.Console.Stores; +using Microsoft.AspNetCore.Identity; + +namespace Fengling.Console.Managers; + +public interface ITenantManager +{ + Task FindByIdAsync(int tenantId, CancellationToken cancellationToken = default); + Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantCode = null, TenantStatus? status = null, CancellationToken cancellationToken = default); + Task GetCountAsync(string? name = null, string? tenantCode = null, TenantStatus? status = null, CancellationToken cancellationToken = default); + Task CreateAsync(Tenant tenant, CancellationToken cancellationToken = default); + Task UpdateAsync(Tenant tenant, CancellationToken cancellationToken = default); + Task DeleteAsync(Tenant tenant, CancellationToken cancellationToken = default); + Task GetUserCountAsync(int tenantId, CancellationToken cancellationToken = default); + Task SetTenantCodeAsync(Tenant tenant, string code, CancellationToken cancellationToken = default); +} + +public class TenantManager : ITenantManager +{ + private readonly ITenantStore _store; + + public TenantManager(ITenantStore store) + { + _store = store; + } + + public virtual async Task FindByIdAsync(int tenantId, CancellationToken cancellationToken = default) + { + return await _store.FindByIdAsync(tenantId, cancellationToken); + } + + public virtual async Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default) + { + return await _store.FindByTenantCodeAsync(tenantCode, cancellationToken); + } + + public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return await _store.GetAllAsync(cancellationToken); + } + + public virtual async Task> GetPagedAsync(int page, int pageSize, string? name = null, + string? tenantCode = null, TenantStatus? status = null, CancellationToken cancellationToken = default) + { + return await _store.GetPagedAsync(page, pageSize, name, tenantCode, status, cancellationToken); + } + + public virtual async Task GetCountAsync(string? name = null, string? tenantCode = null, + TenantStatus? status = null, CancellationToken cancellationToken = default) + { + return await _store.GetCountAsync(name, tenantCode, status, cancellationToken); + } + + public virtual async Task CreateAsync(Tenant tenant, CancellationToken cancellationToken = default) + { + return await _store.CreateAsync(tenant, cancellationToken); + } + + public virtual async Task UpdateAsync(Tenant tenant, CancellationToken cancellationToken = default) + { + return await _store.UpdateAsync(tenant, cancellationToken); + } + + public virtual async Task DeleteAsync(Tenant tenant, CancellationToken cancellationToken = default) + { + return await _store.DeleteAsync(tenant, cancellationToken); + } + + public virtual async Task GetUserCountAsync(int tenantId, CancellationToken cancellationToken = default) + { + return await _store.GetUserCountAsync(tenantId, cancellationToken); + } + + public virtual async Task SetTenantCodeAsync(Tenant tenant, string code, CancellationToken cancellationToken = default) + { + var existing = await _store.FindByTenantCodeAsync(code, cancellationToken); + if (existing != null && existing.Id != tenant.Id) + { + return IdentityResult.Failed(new IdentityError { Description = "租户编码已存在" }); + } + + await _store.SetTenantCodeAsync(tenant, code, cancellationToken); + return IdentityResult.Success; + } +} diff --git a/Models/Dtos/TenantDto.cs b/Models/Dtos/TenantDto.cs index 9024e0a..bb10c1d 100644 --- a/Models/Dtos/TenantDto.cs +++ b/Models/Dtos/TenantDto.cs @@ -1,10 +1,10 @@ -using Fengling.Platform.Domain.AggregatesModel.TenantAggregate; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Models.Dtos; public class TenantDto { - public long Id { get; set; } + public int Id { get; set; } public string TenantCode { get; set; } = ""; public string Name { get; set; } = ""; public string ContactName { get; set; } = ""; diff --git a/Models/Dtos/TenantQueryDto.cs b/Models/Dtos/TenantQueryDto.cs index deb694a..c3148cd 100644 --- a/Models/Dtos/TenantQueryDto.cs +++ b/Models/Dtos/TenantQueryDto.cs @@ -1,4 +1,4 @@ -using Fengling.Platform.Domain.AggregatesModel.TenantAggregate; +using Fengling.Console.Models.Entities; namespace Fengling.Console.Models.Dtos; diff --git a/Models/Entities/ApplicationRole.cs b/Models/Entities/ApplicationRole.cs index 4a33b87..b5aa616 100644 --- a/Models/Entities/ApplicationRole.cs +++ b/Models/Entities/ApplicationRole.cs @@ -6,7 +6,7 @@ public class ApplicationRole : IdentityRole { public string? Description { get; set; } public DateTime CreatedTime { get; set; } = DateTime.UtcNow; - public long? TenantId { get; set; } + public int? TenantId { get; set; } public bool IsSystem { get; set; } public string? DisplayName { get; set; } public List? Permissions { get; set; } diff --git a/Models/Entities/ApplicationUser.cs b/Models/Entities/ApplicationUser.cs index 6999923..3478cf3 100644 --- a/Models/Entities/ApplicationUser.cs +++ b/Models/Entities/ApplicationUser.cs @@ -1,4 +1,3 @@ -using Fengling.Platform.Domain.AggregatesModel.TenantAggregate; using Microsoft.AspNetCore.Identity; namespace Fengling.Console.Models.Entities; @@ -7,7 +6,7 @@ public class ApplicationUser : IdentityUser { public string? RealName { get; set; } public string? Phone { get; set; } - public TenantInfo TenantInfo { get; set; } = null!; + public int TenantId { get; set; } public DateTime CreatedTime { get; set; } = DateTime.UtcNow; public DateTime? UpdatedTime { get; set; } public bool IsDeleted { get; set; } diff --git a/Models/Entities/Tenant.cs b/Models/Entities/Tenant.cs new file mode 100644 index 0000000..845a24a --- /dev/null +++ b/Models/Entities/Tenant.cs @@ -0,0 +1,25 @@ +namespace Fengling.Console.Models.Entities; + +public class Tenant +{ + public int Id { get; set; } + public string TenantCode { get; set; } = string.Empty; + public string Name { get; set; } = string.Empty; + public string ContactName { get; set; } = string.Empty; + public string ContactEmail { get; set; } = string.Empty; + public string? ContactPhone { get; set; } + public int? MaxUsers { get; set; } + public string? Description { get; set; } + public TenantStatus Status { get; set; } = TenantStatus.Active; + public DateTime? ExpiresAt { get; set; } + public DateTime CreatedAt { get; set; } = DateTime.UtcNow; + public DateTime? UpdatedAt { get; set; } + public bool IsDeleted { get; set; } +} + +public enum TenantStatus +{ + Active = 1, + Inactive = 2, + Frozen = 3 +} diff --git a/Program.cs b/Program.cs index 5b5bb21..54d8619 100644 --- a/Program.cs +++ b/Program.cs @@ -1,6 +1,8 @@ using System.Reflection; using Fengling.Console.Repositories; using Fengling.Console.Services; +using Fengling.Console.Stores; +using Fengling.Console.Managers; using Fengling.Platform.Infrastructure.Repositories; using OpenIddict.Abstractions; using Microsoft.EntityFrameworkCore; @@ -38,9 +40,11 @@ builder.Services.AddHttpContextAccessor(); builder.Services.AddHttpClient(); builder.Services.AddScoped(); builder.Services.AddScoped(); -builder.Services.AddScoped(); builder.Services.AddScoped(); +builder.Services.AddScoped, TenantStore>(); +builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); diff --git a/Repositories/UserRepository.cs b/Repositories/UserRepository.cs index 285e1c3..8b724bf 100644 --- a/Repositories/UserRepository.cs +++ b/Repositories/UserRepository.cs @@ -45,7 +45,7 @@ public class UserRepository : IUserRepository if (!string.IsNullOrEmpty(tenantCode)) { - query = query.Where(u => u.TenantInfo.TenantCode.Contains(tenantCode)); + query = query.Where(u => u.TenantId.ToString().Contains(tenantCode)); } return await query @@ -71,7 +71,7 @@ public class UserRepository : IUserRepository if (!string.IsNullOrEmpty(tenantCode)) { - query = query.Where(u => u.TenantInfo.TenantCode.Contains(tenantCode)); + query = query.Where(u => u.TenantId.ToString().Contains(tenantCode)); } return await query.CountAsync(); diff --git a/Services/H5LinkService.cs b/Services/H5LinkService.cs index a246725..749f45c 100644 --- a/Services/H5LinkService.cs +++ b/Services/H5LinkService.cs @@ -1,15 +1,14 @@ using Fengling.Console.Models.Entities; -using Fengling.Console.Repositories; -using Fengling.Platform.Infrastructure.Repositories; -using Fengling.Platform.Domain.AggregatesModel.TenantAggregate; +using Fengling.Console.Managers; using Microsoft.Extensions.Configuration; using QRCoder; +using Tenant = Fengling.Console.Models.Entities.Tenant; namespace Fengling.Console.Services; public interface IH5LinkService { - Task GenerateH5LinkAsync(long tenantId); + Task GenerateH5LinkAsync(int tenantId); } public class H5LinkResult @@ -20,18 +19,18 @@ public class H5LinkResult public class H5LinkService : IH5LinkService { - private readonly ITenantRepository _tenantRepository; + private readonly ITenantManager _tenantManager; private readonly IConfiguration _configuration; - public H5LinkService(ITenantRepository tenantRepository, IConfiguration configuration) + public H5LinkService(ITenantManager tenantManager, IConfiguration configuration) { - _tenantRepository = tenantRepository; + _tenantManager = tenantManager; _configuration = configuration; } - public async Task GenerateH5LinkAsync(long tenantId) + public async Task GenerateH5LinkAsync(int tenantId) { - var tenant = await _tenantRepository.GetByIdAsync(tenantId) + var tenant = await _tenantManager.FindByIdAsync(tenantId) ?? throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); var link = GenerateLink(tenant); diff --git a/Services/RoleService.cs b/Services/RoleService.cs index 10f458c..676cbbd 100644 --- a/Services/RoleService.cs +++ b/Services/RoleService.cs @@ -1,5 +1,6 @@ using Fengling.Console.Models.Dtos; using Fengling.Console.Repositories; +using Fengling.Console.Managers; using Microsoft.AspNetCore.Identity; using System.Security.Claims; using Fengling.Console.Datas; @@ -24,6 +25,7 @@ public interface IRoleService public class RoleService : IRoleService { private readonly IRoleRepository _repository; + private readonly ITenantManager _tenantManager; private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly ApplicationDbContext _context; @@ -31,12 +33,14 @@ public class RoleService : IRoleService public RoleService( IRoleRepository repository, + ITenantManager tenantManager, UserManager userManager, RoleManager roleManager, ApplicationDbContext context, IHttpContextAccessor httpContextAccessor) { _repository = repository; + _tenantManager = tenantManager; _userManager = userManager; _roleManager = roleManager; _context = context; @@ -103,14 +107,15 @@ public class RoleService : IRoleService foreach (var user in users) { var roles = await _userManager.GetRolesAsync(user); + var tenant = user.TenantId > 0 ? await _tenantManager.FindByIdAsync(user.TenantId) : null; userDtos.Add(new UserDto { Id = user.Id, UserName = user.UserName, Email = user.Email, RealName = user.RealName, - TenantCode = user.TenantInfo.TenantCode, - TenantName = user.TenantInfo.TenantName, + TenantCode = user.TenantId.ToString(), + TenantName = tenant?.Name ?? "", Roles = roles.ToList(), EmailConfirmed = user.EmailConfirmed, IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow, @@ -128,7 +133,7 @@ public class RoleService : IRoleService Name = dto.Name, DisplayName = dto.DisplayName, Description = dto.Description, - TenantId = dto.TenantId, + TenantId = dto.TenantId.HasValue ? (int?)dto.TenantId.Value : null, Permissions = dto.Permissions, IsSystem = false, CreatedTime = DateTime.UtcNow diff --git a/Services/TenantService.cs b/Services/TenantService.cs index 6b90da7..5bb39d5 100644 --- a/Services/TenantService.cs +++ b/Services/TenantService.cs @@ -1,11 +1,11 @@ +using Fengling.Console.Managers; using Fengling.Console.Models.Dtos; using Fengling.Console.Repositories; -using Fengling.Platform.Infrastructure.Repositories; using Microsoft.AspNetCore.Identity; using System.Security.Claims; using Fengling.Console.Datas; using Fengling.Console.Models.Entities; -using Fengling.Platform.Domain.AggregatesModel.TenantAggregate; +using TenantStatus = Fengling.Console.Models.Entities.TenantStatus; namespace Fengling.Console.Services; @@ -13,18 +13,18 @@ public interface ITenantService { Task<(IEnumerable Items, int TotalCount)> GetTenantsAsync(int page, int pageSize, string? name = null, string? tenantCode = null, TenantStatus? status = null); - Task GetTenantAsync(long id); - Task> GetTenantUsersAsync(long tenantId); - Task> GetTenantRolesAsync(long tenantId); - Task GetTenantSettingsAsync(long id); - Task UpdateTenantSettingsAsync(long id, TenantSettingsDto settings); + Task GetTenantAsync(int id); + Task> GetTenantUsersAsync(int tenantId); + Task> GetTenantRolesAsync(int tenantId); + Task GetTenantSettingsAsync(int id); + Task UpdateTenantSettingsAsync(int id, TenantSettingsDto settings); Task CreateTenantAsync(CreateTenantDto dto); - Task UpdateTenantAsync(long id, UpdateTenantDto dto); - Task DeleteTenantAsync(long id); + Task UpdateTenantAsync(int id, UpdateTenantDto dto); + Task DeleteTenantAsync(int id); } public class TenantService( - ITenantRepository repository, + ITenantManager tenantManager, IUserRepository userRepository, IRoleRepository roleRepository, UserManager userManager, @@ -35,13 +35,13 @@ public class TenantService( public async Task<(IEnumerable Items, int TotalCount)> GetTenantsAsync (int page, int pageSize, string? name = null, string? tenantCode = null, TenantStatus? status = null) { - var tenants = await repository.GetPagedAsync(page, pageSize, name, tenantCode, status); - var totalCount = await repository.CountAsync(name, tenantCode, status); + var tenants = await tenantManager.GetPagedAsync(page, pageSize, name, tenantCode, status); + var totalCount = await tenantManager.GetCountAsync(name, tenantCode, status); var tenantDtos = new List(); foreach (var tenant in tenants) { - var userCount = await repository.GetUserCountAsync(tenant.Id); + var userCount = await tenantManager.GetUserCountAsync(tenant.Id); tenantDtos.Add(new TenantDto { Id = tenant.Id, @@ -62,9 +62,9 @@ public class TenantService( return (tenantDtos, totalCount); } - public async Task GetTenantAsync(long id) + public async Task GetTenantAsync(int id) { - var tenant = await repository.GetByIdAsync(id); + var tenant = await tenantManager.FindByIdAsync(id); if (tenant == null) return null; return new TenantDto @@ -76,7 +76,7 @@ public class TenantService( ContactEmail = tenant.ContactEmail, ContactPhone = tenant.ContactPhone, MaxUsers = tenant.MaxUsers, - UserCount = await repository.GetUserCountAsync(tenant.Id), + UserCount = await tenantManager.GetUserCountAsync(tenant.Id), Status = tenant.Status, ExpiresAt = tenant.ExpiresAt, Description = tenant.Description, @@ -84,9 +84,9 @@ public class TenantService( }; } - public async Task> GetTenantUsersAsync(long tenantId) + public async Task> GetTenantUsersAsync(int tenantId) { - var tenant = await repository.GetByIdAsync(tenantId); + var tenant = await tenantManager.FindByIdAsync(tenantId); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); @@ -104,8 +104,8 @@ public class TenantService( UserName = user.UserName, Email = user.Email, RealName = user.RealName, - TenantCode = user.TenantInfo.TenantCode, - TenantName = user.TenantInfo.TenantName, + TenantCode = user.TenantId.ToString(), + TenantName = tenant?.Name ?? "", Roles = roles.ToList(), EmailConfirmed = user.EmailConfirmed, IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow, @@ -116,9 +116,9 @@ public class TenantService( return userDtos; } - public async Task> GetTenantRolesAsync(long tenantId) + public async Task> GetTenantRolesAsync(int tenantId) { - var tenant = await repository.GetByIdAsync(tenantId); + var tenant = await tenantManager.FindByIdAsync(tenantId); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {tenantId} not found"); @@ -133,9 +133,9 @@ public class TenantService( }); } - public async Task GetTenantSettingsAsync(long id) + public async Task GetTenantSettingsAsync(int id) { - var tenant = await repository.GetByIdAsync(id); + var tenant = await tenantManager.FindByIdAsync(id); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {id} not found"); @@ -152,9 +152,9 @@ public class TenantService( }; } - public async Task UpdateTenantSettingsAsync(long id, TenantSettingsDto settings) + public async Task UpdateTenantSettingsAsync(int id, TenantSettingsDto settings) { - var tenant = await repository.GetByIdAsync(id); + var tenant = await tenantManager.FindByIdAsync(id); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {id} not found"); @@ -165,10 +165,27 @@ public class TenantService( public async Task CreateTenantAsync(CreateTenantDto dto) { - var tenant = new Tenant(dto.TenantCode, dto.Name, dto.ContactName, dto.ContactEmail, - dto.ContactPhone,dto.MaxUsers,dto.Description,dto.ExpiresAt); + var tenant = new Tenant + { + TenantCode = dto.TenantCode, + Name = dto.Name, + ContactName = dto.ContactName, + ContactEmail = dto.ContactEmail, + ContactPhone = dto.ContactPhone, + MaxUsers = dto.MaxUsers, + Description = dto.Description, + ExpiresAt = dto.ExpiresAt, + Status = TenantStatus.Active, + CreatedAt = DateTime.UtcNow + }; - await repository.AddAsync(tenant); + var result = await tenantManager.CreateAsync(tenant); + + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + throw new InvalidOperationException($"Failed to create tenant: {errors}"); + } await CreateAuditLog("tenant", "create", "Tenant", tenant.Id, tenant.Name, null, System.Text.Json.JsonSerializer.Serialize(dto)); @@ -190,9 +207,9 @@ public class TenantService( }; } - public async Task UpdateTenantAsync(long id, UpdateTenantDto dto) + public async Task UpdateTenantAsync(int id, UpdateTenantDto dto) { - var tenant = await repository.GetByIdAsync(id); + var tenant = await tenantManager.FindByIdAsync(id); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {id} not found"); @@ -200,9 +217,19 @@ public class TenantService( var oldValue = System.Text.Json.JsonSerializer.Serialize(tenant); - tenant.UpdateInfo(dto.Name,dto.ContactName,dto.ContactEmail,dto.ContactPhone); + tenant.Name = dto.Name; + tenant.ContactName = dto.ContactName; + tenant.ContactEmail = dto.ContactEmail; + tenant.ContactPhone = dto.ContactPhone; + tenant.UpdatedAt = DateTime.UtcNow; - await repository.UpdateAsync(tenant); + var result = await tenantManager.UpdateAsync(tenant); + + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + throw new InvalidOperationException($"Failed to update tenant: {errors}"); + } await CreateAuditLog("tenant", "update", "Tenant", tenant.Id, tenant.Name, oldValue, System.Text.Json.JsonSerializer.Serialize(tenant)); @@ -216,7 +243,7 @@ public class TenantService( ContactEmail = tenant.ContactEmail, ContactPhone = tenant.ContactPhone, MaxUsers = tenant.MaxUsers, - UserCount = await repository.GetUserCountAsync(tenant.Id), + UserCount = await tenantManager.GetUserCountAsync(tenant.Id), Status = tenant.Status, ExpiresAt = tenant.ExpiresAt, Description = tenant.Description, @@ -224,9 +251,9 @@ public class TenantService( }; } - public async Task DeleteTenantAsync(long id) + public async Task DeleteTenantAsync(int id) { - var tenant = await repository.GetByIdAsync(id); + var tenant = await tenantManager.FindByIdAsync(id); if (tenant == null) { throw new KeyNotFoundException($"Tenant with ID {id} not found"); @@ -242,26 +269,32 @@ public class TenantService( await context.SaveChangesAsync(); } - tenant.Delete();; - await repository.UpdateAsync(tenant); + tenant.IsDeleted = true; + var result = await tenantManager.UpdateAsync(tenant); + + if (!result.Succeeded) + { + var errors = string.Join(", ", result.Errors.Select(e => e.Description)); + throw new InvalidOperationException($"Failed to delete tenant: {errors}"); + } await CreateAuditLog("tenant", "delete", "Tenant", tenant.Id, tenant.Name, oldValue); } - private async Task CreateAuditLog(string operation, string action, string targetType, long? targetId, string? targetName, string? oldValue = null, string? newValue = null) + private async Task CreateAuditLog(string operation, string action, string targetType, int? targetId, string? targetName, string? oldValue = null, string? newValue = null) { var httpContext = httpContextAccessor.HttpContext; var userName = httpContext?.User?.FindFirstValue(ClaimTypes.NameIdentifier) ?? httpContext?.User?.Identity?.Name ?? "system"; - var tenantId = httpContext?.User?.FindFirstValue("TenantId"); + var tenantIdClaim = httpContext?.User?.FindFirstValue("TenantId"); var log = new AuditLog { Operator = userName, - TenantId = tenantId, + TenantId = tenantIdClaim, Operation = operation, Action = action, TargetType = targetType, - TargetId = targetId, + TargetId = targetId.HasValue ? (long)targetId.Value : null, TargetName = targetName, IpAddress = httpContext?.Connection?.RemoteIpAddress?.ToString() ?? "unknown", Status = "success", diff --git a/Services/UserService.cs b/Services/UserService.cs index e9d9ea0..8a94dc3 100644 --- a/Services/UserService.cs +++ b/Services/UserService.cs @@ -1,11 +1,11 @@ using Fengling.Console.Models.Dtos; using Fengling.Console.Repositories; -using Fengling.Platform.Infrastructure.Repositories; +using Fengling.Console.Managers; using Microsoft.AspNetCore.Identity; using System.Security.Claims; using Fengling.Console.Datas; using Fengling.Console.Models.Entities; -using Fengling.Platform.Domain.AggregatesModel.TenantAggregate; +using Tenant = Fengling.Console.Models.Entities.Tenant; namespace Fengling.Console.Services; @@ -21,7 +21,7 @@ public interface IUserService public class UserService( IUserRepository repository, - ITenantRepository tenantRepository, + ITenantManager tenantManager, UserManager userManager, RoleManager roleManager, ApplicationDbContext context, @@ -37,6 +37,7 @@ public class UserService( foreach (var user in users) { var roles = await userManager.GetRolesAsync(user); + var tenant = user.TenantId > 0 ? await tenantManager.FindByIdAsync(user.TenantId) : null; userDtos.Add(new UserDto { Id = user.Id, @@ -44,8 +45,8 @@ public class UserService( Email = user.Email, RealName = user.RealName, Phone = user.Phone, - TenantCode = user.TenantInfo.TenantCode, - TenantName = user.TenantInfo.TenantName, + TenantCode = user.TenantId.ToString(), + TenantName = tenant?.Name ?? "", Roles = roles.ToList(), EmailConfirmed = user.EmailConfirmed, IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow, @@ -62,6 +63,7 @@ public class UserService( if (user == null) return null; var roles = await userManager.GetRolesAsync(user); + var tenant = user.TenantId > 0 ? await tenantManager.FindByIdAsync(user.TenantId) : null; return new UserDto { Id = user.Id, @@ -69,8 +71,8 @@ public class UserService( Email = user.Email, RealName = user.RealName, Phone = user.Phone, - TenantCode = user.TenantInfo.TenantCode, - TenantName = user.TenantInfo.TenantName, + TenantCode = user.TenantId.ToString(), + TenantName = tenant?.Name ?? "", Roles = roles.ToList(), EmailConfirmed = user.EmailConfirmed, IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow, @@ -80,12 +82,12 @@ public class UserService( public async Task CreateUserAsync(CreateUserDto dto) { - var tenantId = dto.TenantId ?? 0; + var tenantId = dto.TenantId.HasValue ? (int)dto.TenantId.Value : 0; Tenant? tenant = null; if (tenantId != 0) { - tenant = await tenantRepository.GetByIdAsync(tenantId); + tenant = await tenantManager.FindByIdAsync(tenantId); if (tenant == null) { throw new InvalidOperationException("Invalid tenant ID"); @@ -98,7 +100,7 @@ public class UserService( Email = dto.Email, RealName = dto.RealName, Phone = dto.Phone, - TenantInfo = new TenantInfo(tenant!), + TenantId = tenant?.Id ?? 0, EmailConfirmed = dto.EmailConfirmed, CreatedTime = DateTime.UtcNow }; @@ -137,8 +139,8 @@ public class UserService( Email = user.Email, RealName = user.RealName, Phone = user.Phone, - TenantCode = user.TenantInfo.TenantCode, - TenantName = user.TenantInfo.TenantName, + TenantCode = user.TenantId.ToString(), + TenantName = tenant?.Name ?? "", Roles = roles.ToList(), EmailConfirmed = user.EmailConfirmed, IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow, @@ -175,6 +177,8 @@ public class UserService( await context.SaveChangesAsync(); + var tenant = user.TenantId > 0 ? await tenantManager.FindByIdAsync(user.TenantId) : null; + await CreateAuditLog("user", "update", "User", user.Id, user.UserName, oldValue, System.Text.Json.JsonSerializer.Serialize(user)); var roles = await userManager.GetRolesAsync(user); @@ -185,8 +189,8 @@ public class UserService( Email = user.Email, RealName = user.RealName, Phone = user.Phone, - TenantCode = user.TenantInfo.TenantCode, - TenantName = user.TenantInfo.TenantName, + TenantCode = user.TenantId.ToString(), + TenantName = tenant?.Name ?? "", Roles = roles.ToList(), EmailConfirmed = user.EmailConfirmed, IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow, diff --git a/Stores/ITenantStore.cs b/Stores/ITenantStore.cs new file mode 100644 index 0000000..6718f33 --- /dev/null +++ b/Stores/ITenantStore.cs @@ -0,0 +1,41 @@ +using Fengling.Console.Models.Entities; +using Microsoft.AspNetCore.Identity; + +namespace Fengling.Console.Stores; + +public interface ITenantStore : IDisposable where TTenant : class +{ + Task FindByIdAsync(int tenantId, CancellationToken cancellationToken = default); + Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default); + Task> GetAllAsync(CancellationToken cancellationToken = default); + Task> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantCode = null, TenantStatus? status = null, CancellationToken cancellationToken = default); + Task GetCountAsync(string? name = null, string? tenantCode = null, TenantStatus? status = null, CancellationToken cancellationToken = default); + Task CreateAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task UpdateAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task DeleteAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetUserCountAsync(int tenantId, CancellationToken cancellationToken = default); + + Task GetTenantCodeAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetNameAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetContactNameAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetContactEmailAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetContactPhoneAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetMaxUsersAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetDescriptionAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetStatusAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetExpiresAtAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetCreatedAtAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetUpdatedAtAsync(TTenant tenant, CancellationToken cancellationToken = default); + Task GetIsDeletedAsync(TTenant tenant, CancellationToken cancellationToken = default); + + Task SetTenantCodeAsync(TTenant tenant, string code, CancellationToken cancellationToken = default); + Task SetNameAsync(TTenant tenant, string name, CancellationToken cancellationToken = default); + Task SetContactNameAsync(TTenant tenant, string name, CancellationToken cancellationToken = default); + Task SetContactEmailAsync(TTenant tenant, string email, CancellationToken cancellationToken = default); + Task SetContactPhoneAsync(TTenant tenant, string? phone, CancellationToken cancellationToken = default); + Task SetMaxUsersAsync(TTenant tenant, int? maxUsers, CancellationToken cancellationToken = default); + Task SetDescriptionAsync(TTenant tenant, string? description, CancellationToken cancellationToken = default); + Task SetStatusAsync(TTenant tenant, TenantStatus status, CancellationToken cancellationToken = default); + Task SetExpiresAtAsync(TTenant tenant, DateTime? expiresAt, CancellationToken cancellationToken = default); + Task SetIsDeletedAsync(TTenant tenant, bool isDeleted, CancellationToken cancellationToken = default); +} diff --git a/Stores/TenantStore.cs b/Stores/TenantStore.cs new file mode 100644 index 0000000..2a3bccc --- /dev/null +++ b/Stores/TenantStore.cs @@ -0,0 +1,196 @@ +using Fengling.Console.Datas; +using Fengling.Console.Models.Entities; +using Microsoft.AspNetCore.Identity; +using Microsoft.EntityFrameworkCore; + +namespace Fengling.Console.Stores; + +public class TenantStore : ITenantStore +{ + private readonly ApplicationDbContext _context; + private readonly DbSet _tenants; + + public TenantStore(ApplicationDbContext context) + { + _context = context; + _tenants = context.Tenants; + } + + public void Dispose() { } + + public virtual Task FindByIdAsync(int tenantId, CancellationToken cancellationToken = default) + { + return _tenants.FirstOrDefaultAsync(t => t.Id == tenantId, cancellationToken); + } + + public virtual Task FindByTenantCodeAsync(string tenantCode, CancellationToken cancellationToken = default) + { + return _tenants.FirstOrDefaultAsync(t => t.TenantCode == tenantCode, cancellationToken); + } + + public virtual async Task> GetAllAsync(CancellationToken cancellationToken = default) + { + return (IList)await _tenants.ToListAsync(cancellationToken); + } + + public virtual async Task> GetPagedAsync(int page, int pageSize, string? name = null, + string? tenantCode = null, TenantStatus? status = null, CancellationToken cancellationToken = default) + { + var query = _tenants.AsQueryable(); + + if (!string.IsNullOrEmpty(name)) + query = query.Where(t => t.Name.Contains(name)); + + if (!string.IsNullOrEmpty(tenantCode)) + query = query.Where(t => t.TenantCode.Contains(tenantCode)); + + if (status.HasValue) + query = query.Where(t => t.Status == status); + + return await query + .OrderByDescending(t => t.CreatedAt) + .Skip((page - 1) * pageSize) + .Take(pageSize) + .ToListAsync(cancellationToken); + } + + public virtual async Task GetCountAsync(string? name = null, string? tenantCode = null, + TenantStatus? status = null, CancellationToken cancellationToken = default) + { + var query = _tenants.AsQueryable(); + + if (!string.IsNullOrEmpty(name)) + query = query.Where(t => t.Name.Contains(name)); + + if (!string.IsNullOrEmpty(tenantCode)) + query = query.Where(t => t.TenantCode.Contains(tenantCode)); + + if (status.HasValue) + query = query.Where(t => t.Status == status); + + return await query.CountAsync(cancellationToken); + } + + public virtual async Task CreateAsync(Tenant tenant, CancellationToken cancellationToken = default) + { + _tenants.Add(tenant); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task UpdateAsync(Tenant tenant, CancellationToken cancellationToken = default) + { + tenant.UpdatedAt = DateTime.UtcNow; + _tenants.Update(tenant); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task DeleteAsync(Tenant tenant, CancellationToken cancellationToken = default) + { + _tenants.Remove(tenant); + await _context.SaveChangesAsync(cancellationToken); + return IdentityResult.Success; + } + + public virtual async Task GetUserCountAsync(int tenantId, CancellationToken cancellationToken = default) + { + return await _context.Users.CountAsync(u => u.TenantId == tenantId && !u.IsDeleted, cancellationToken); + } + + public virtual Task GetTenantCodeAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.TenantCode); + + public virtual Task GetNameAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.Name); + + public virtual Task GetContactNameAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.ContactName); + + public virtual Task GetContactEmailAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.ContactEmail); + + public virtual Task GetContactPhoneAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.ContactPhone); + + public virtual Task GetMaxUsersAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.MaxUsers); + + public virtual Task GetDescriptionAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.Description); + + public virtual Task GetStatusAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.Status); + + public virtual Task GetExpiresAtAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.ExpiresAt); + + public virtual Task GetCreatedAtAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.CreatedAt); + + public virtual Task GetUpdatedAtAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.UpdatedAt); + + public virtual Task GetIsDeletedAsync(Tenant tenant, CancellationToken cancellationToken = default) + => Task.FromResult(tenant.IsDeleted); + + public virtual Task SetTenantCodeAsync(Tenant tenant, string code, CancellationToken cancellationToken = default) + { + tenant.TenantCode = code; + return Task.CompletedTask; + } + + public virtual Task SetNameAsync(Tenant tenant, string name, CancellationToken cancellationToken = default) + { + tenant.Name = name; + return Task.CompletedTask; + } + + public virtual Task SetContactNameAsync(Tenant tenant, string name, CancellationToken cancellationToken = default) + { + tenant.ContactName = name; + return Task.CompletedTask; + } + + public virtual Task SetContactEmailAsync(Tenant tenant, string email, CancellationToken cancellationToken = default) + { + tenant.ContactEmail = email; + return Task.CompletedTask; + } + + public virtual Task SetContactPhoneAsync(Tenant tenant, string? phone, CancellationToken cancellationToken = default) + { + tenant.ContactPhone = phone; + return Task.CompletedTask; + } + + public virtual Task SetMaxUsersAsync(Tenant tenant, int? maxUsers, CancellationToken cancellationToken = default) + { + tenant.MaxUsers = maxUsers; + return Task.CompletedTask; + } + + public virtual Task SetDescriptionAsync(Tenant tenant, string? description, CancellationToken cancellationToken = default) + { + tenant.Description = description; + return Task.CompletedTask; + } + + public virtual Task SetStatusAsync(Tenant tenant, TenantStatus status, CancellationToken cancellationToken = default) + { + tenant.Status = status; + return Task.CompletedTask; + } + + public virtual Task SetExpiresAtAsync(Tenant tenant, DateTime? expiresAt, CancellationToken cancellationToken = default) + { + tenant.ExpiresAt = expiresAt; + return Task.CompletedTask; + } + + public virtual Task SetIsDeletedAsync(Tenant tenant, bool isDeleted, CancellationToken cancellationToken = default) + { + tenant.IsDeleted = isDeleted; + return Task.CompletedTask; + } +}