feat(auth): extract Tenant to Platform domain
- Add Fengling.Platform domain and infrastructure projects - Move Tenant aggregate from AuthService/Console to Platform.Domain - Add TenantRepository and SeedData to Platform - Remove duplicate Tenant/TenantInfo models from AuthService and Console - Update controllers and services to use Platform.Domain.Tenant - Add new migrations for PlatformDbContext BREAKING CHANGE: Tenant entity now uses strongly-typed ID (TenantId)
This commit is contained in:
parent
9516e1cd93
commit
74122b2c8c
@ -1,5 +1,7 @@
|
||||
namespace Fengling.Console.Controllers;
|
||||
|
||||
using Fengling.Console.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 租户管理控制器
|
||||
/// 提供租户的增删改查以及租户用户、角色、配置管理功能
|
||||
@ -10,11 +12,13 @@ namespace Fengling.Console.Controllers;
|
||||
public class TenantsController : ControllerBase
|
||||
{
|
||||
private readonly ITenantService _tenantService;
|
||||
private readonly IH5LinkService _h5LinkService;
|
||||
private readonly ILogger<TenantsController> _logger;
|
||||
|
||||
public TenantsController(ITenantService tenantService, ILogger<TenantsController> logger)
|
||||
public TenantsController(ITenantService tenantService, IH5LinkService h5LinkService, ILogger<TenantsController> logger)
|
||||
{
|
||||
_tenantService = tenantService;
|
||||
_h5LinkService = h5LinkService;
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
@ -298,4 +302,36 @@ public class TenantsController : ControllerBase
|
||||
return StatusCode(500, new { message = ex.Message });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成H5访问链接和二维码
|
||||
/// </summary>
|
||||
/// <param name="id">租户ID</param>
|
||||
/// <returns>H5链接和二维码Base64</returns>
|
||||
/// <response code="200">成功返回链接和二维码</response>
|
||||
/// <response code="404">租户不存在</response>
|
||||
/// <response code="500">服务器内部错误</response>
|
||||
[HttpGet("{id}/h5-link")]
|
||||
[Produces("application/json")]
|
||||
[ProducesResponseType(typeof(H5LinkResult), StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status404NotFound)]
|
||||
[ProducesResponseType(typeof(object), StatusCodes.Status500InternalServerError)]
|
||||
public async Task<ActionResult<H5LinkResult>> GetH5Link(long id)
|
||||
{
|
||||
try
|
||||
{
|
||||
var result = await _h5LinkService.GenerateH5LinkAsync(id);
|
||||
return Ok(result);
|
||||
}
|
||||
catch (KeyNotFoundException ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Tenant not found: {TenantId}", id);
|
||||
return NotFound(new { message = ex.Message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error generating H5 link for tenant {TenantId}", id);
|
||||
return StatusCode(500, new { message = "Failed to generate H5 link" });
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,6 @@ namespace Fengling.Console.Datas;
|
||||
public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
|
||||
: IdentityDbContext<ApplicationUser, ApplicationRole, long>(options)
|
||||
{
|
||||
public DbSet<Tenant> Tenants { get; set; }
|
||||
public DbSet<AccessLog> AccessLogs { get; set; }
|
||||
public DbSet<AuditLog> AuditLogs { get; set; }
|
||||
|
||||
@ -23,27 +22,16 @@ public class ApplicationDbContext(DbContextOptions<ApplicationDbContext> options
|
||||
|
||||
entity.OwnsOne(e => e.TenantInfo, navigationBuilder =>
|
||||
{
|
||||
navigationBuilder.Property(e => e.Id).HasColumnName("TenantId");
|
||||
navigationBuilder.Property(e => e.TenantId).HasColumnName("TenantCode");
|
||||
navigationBuilder.Property(e => e.Name).HasColumnName("TenantName");
|
||||
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<ApplicationRole>(entity => { entity.Property(e => e.Description).HasMaxLength(200); });
|
||||
|
||||
builder.Entity<Tenant>(entity =>
|
||||
{
|
||||
entity.HasKey(e => e.Id);
|
||||
entity.HasIndex(e => e.TenantId).IsUnique();
|
||||
entity.Property(e => e.TenantId).HasMaxLength(50);
|
||||
entity.Property(e => e.Name).HasMaxLength(100);
|
||||
entity.Property(e => e.ContactName).HasMaxLength(50);
|
||||
entity.Property(e => e.ContactEmail).HasMaxLength(100);
|
||||
entity.Property(e => e.ContactPhone).HasMaxLength(20);
|
||||
entity.Property(e => e.Status).HasMaxLength(20);
|
||||
entity.Property(e => e.Description).HasMaxLength(500);
|
||||
});
|
||||
|
||||
|
||||
builder.Entity<AccessLog>(entity =>
|
||||
{
|
||||
|
||||
@ -25,11 +25,14 @@
|
||||
<PackageReference Include="OpenIddict.Server" />
|
||||
<PackageReference Include="OpenIddict.Server.AspNetCore" />
|
||||
<PackageReference Include="Swashbuckle.AspNetCore" />
|
||||
<PackageReference Include="SkiaSharp" />
|
||||
<PackageReference Include="QRCoder" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!-- <ProjectReference Include="..\Fengling.AuthService\Fengling.AuthService.csproj" />-->
|
||||
<ProjectReference Include="..\YarpGateway\YarpGateway.csproj" />
|
||||
<ProjectReference Include="..\Fengling.Platform\Fengling.Platform.Infrastructure\Fengling.Platform.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@ -1,63 +0,0 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
|
||||
namespace Fengling.Console.Models.Entities;
|
||||
|
||||
public class Tenant
|
||||
{
|
||||
private long _id;
|
||||
private string _tenantId;
|
||||
private string _name;
|
||||
|
||||
[Key]
|
||||
public long Id
|
||||
{
|
||||
get => _id;
|
||||
set => _id = value;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
[Required]
|
||||
public string TenantId
|
||||
{
|
||||
get => _tenantId;
|
||||
set => _tenantId = value;
|
||||
}
|
||||
|
||||
[MaxLength(100)]
|
||||
[Required]
|
||||
public string Name
|
||||
{
|
||||
get => _name;
|
||||
set => _name = value;
|
||||
}
|
||||
|
||||
[MaxLength(50)]
|
||||
[Required]
|
||||
public string ContactName { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(100)]
|
||||
[Required]
|
||||
[EmailAddress]
|
||||
public string ContactEmail { get; set; } = string.Empty;
|
||||
|
||||
[MaxLength(20)]
|
||||
public string? ContactPhone { get; set; }
|
||||
|
||||
public int? MaxUsers { get; set; }
|
||||
|
||||
public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
|
||||
|
||||
[MaxLength(500)]
|
||||
public string? Description { get; set; }
|
||||
|
||||
[MaxLength(20)]
|
||||
public string Status { get; set; } = "active";
|
||||
|
||||
public DateTime? ExpiresAt { get; set; }
|
||||
|
||||
public DateTime? UpdatedAt { get; set; }
|
||||
|
||||
public bool IsDeleted { get; set; }
|
||||
|
||||
public TenantInfo Info => new(Id, TenantId, Name);
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
namespace Fengling.Console.Models.Entities;
|
||||
|
||||
public record TenantInfo(long Id, string TenantId, string Name);
|
||||
@ -1,16 +1,17 @@
|
||||
using Fengling.Console.Models.Entities;
|
||||
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
|
||||
|
||||
namespace Fengling.Console.Repositories;
|
||||
|
||||
public interface ITenantRepository
|
||||
{
|
||||
Task<Tenant?> GetByIdAsync(long id);
|
||||
Task<Tenant?> GetByTenantIdAsync(string tenantId);
|
||||
Task<Tenant?> GetByTenantCodeAsync(string tenantCode);
|
||||
Task<IEnumerable<Tenant>> GetAllAsync();
|
||||
Task<IEnumerable<Tenant>> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null);
|
||||
Task<int> CountAsync(string? name = null, string? tenantId = null, string? status = null);
|
||||
Task<IEnumerable<Tenant>> GetPagedAsync(int page, int pageSize, string? name = null, string? tenantCode = null, TenantStatus? status = null);
|
||||
Task<int> CountAsync(string? name = null, string? tenantCode = null, TenantStatus? status = null);
|
||||
Task AddAsync(Tenant tenant);
|
||||
Task UpdateAsync(Tenant tenant);
|
||||
Task DeleteAsync(Tenant tenant);
|
||||
Task<int> GetUserCountAsync(long tenantId);
|
||||
Task<int> GetUserCountAsync(TenantId tenantId);
|
||||
}
|
||||
|
||||
@ -1,19 +1,21 @@
|
||||
using Fengling.Console.Datas;
|
||||
using Fengling.Console.Models.Entities;
|
||||
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
|
||||
using Fengling.Platform.Infrastructure;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Fengling.Console.Repositories;
|
||||
|
||||
public class TenantRepository(ApplicationDbContext context) : ITenantRepository
|
||||
public class TenantRepository(PlatformDbContext context,ApplicationDbContext identityDbContext) : ITenantRepository
|
||||
{
|
||||
public async Task<Tenant?> GetByIdAsync(long id)
|
||||
{
|
||||
return await context.Tenants.FindAsync(id);
|
||||
}
|
||||
|
||||
public async Task<Tenant?> GetByTenantIdAsync(string tenantId)
|
||||
public async Task<Platform.Domain.AggregatesModel.TenantAggregate.Tenant?> GetByTenantCodeAsync(string tenantCode)
|
||||
{
|
||||
return await context.Tenants.FirstOrDefaultAsync(t => t.TenantId == tenantId);
|
||||
return await context.Tenants.FirstOrDefaultAsync(t => t.TenantCode == tenantCode);
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Tenant>> GetAllAsync()
|
||||
@ -22,7 +24,7 @@ public class TenantRepository(ApplicationDbContext context) : ITenantRepository
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<Tenant>> GetPagedAsync(int page, int pageSize, string? name = null,
|
||||
string? tenantId = null, string? status = null)
|
||||
string? tenantCode = null, TenantStatus? status = null)
|
||||
{
|
||||
var query = context.Tenants.AsQueryable();
|
||||
|
||||
@ -31,12 +33,12 @@ public class TenantRepository(ApplicationDbContext context) : ITenantRepository
|
||||
query = query.Where(t => t.Name.Contains(name));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tenantId))
|
||||
if (!string.IsNullOrEmpty(tenantCode))
|
||||
{
|
||||
query = query.Where(t => t.TenantId.Contains(tenantId));
|
||||
query = query.Where(t => t.TenantCode.Contains(tenantCode));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.Status == status);
|
||||
}
|
||||
@ -48,7 +50,7 @@ public class TenantRepository(ApplicationDbContext context) : ITenantRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(string? name = null, string? tenantId = null, string? status = null)
|
||||
public async Task<int> CountAsync(string? name = null, string? tenantCode = null, TenantStatus? status = null)
|
||||
{
|
||||
var query = context.Tenants.AsQueryable();
|
||||
|
||||
@ -57,12 +59,12 @@ public class TenantRepository(ApplicationDbContext context) : ITenantRepository
|
||||
query = query.Where(t => t.Name.Contains(name));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tenantId))
|
||||
if (!string.IsNullOrEmpty(tenantCode))
|
||||
{
|
||||
query = query.Where(t => t.TenantId.Contains(tenantId));
|
||||
query = query.Where(t => t.TenantCode.Contains(tenantCode));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(status))
|
||||
if (status.HasValue)
|
||||
{
|
||||
query = query.Where(t => t.Status == status);
|
||||
}
|
||||
@ -88,8 +90,8 @@ public class TenantRepository(ApplicationDbContext context) : ITenantRepository
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task<int> GetUserCountAsync(long tenantId)
|
||||
public async Task<int> GetUserCountAsync(TenantId tenantId)
|
||||
{
|
||||
return await context.Users.CountAsync(u => u.TenantInfo.Id == tenantId && !u.IsDeleted);
|
||||
return await identityDbContext.Users.CountAsync(u => u.TenantInfo.TenantId == tenantId && !u.IsDeleted);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -28,7 +28,8 @@ public class UserRepository : IUserRepository
|
||||
return await _context.Users.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<IEnumerable<ApplicationUser>> GetPagedAsync(int page, int pageSize, string? userName = null, string? email = null, string? tenantId = null)
|
||||
public async Task<IEnumerable<ApplicationUser>> GetPagedAsync(int page, int pageSize, string? userName = null,
|
||||
string? email = null, string? tenantCode = null)
|
||||
{
|
||||
var query = _context.Users.AsQueryable();
|
||||
|
||||
@ -42,9 +43,9 @@ public class UserRepository : IUserRepository
|
||||
query = query.Where(u => u.Email != null && u.Email.Contains(email));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tenantId))
|
||||
if (!string.IsNullOrEmpty(tenantCode))
|
||||
{
|
||||
query = query.Where(u => u.TenantInfo.Id.ToString() == tenantId);
|
||||
query = query.Where(u => u.TenantInfo.TenantCode.Contains(tenantCode));
|
||||
}
|
||||
|
||||
return await query
|
||||
@ -54,7 +55,7 @@ public class UserRepository : IUserRepository
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<int> CountAsync(string? userName = null, string? email = null, string? tenantId = null)
|
||||
public async Task<int> CountAsync(string? userName = null, string? email = null, string? tenantCode = null)
|
||||
{
|
||||
var query = _context.Users.AsQueryable();
|
||||
|
||||
@ -68,9 +69,9 @@ public class UserRepository : IUserRepository
|
||||
query = query.Where(u => u.Email != null && u.Email.Contains(email));
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(tenantId))
|
||||
if (!string.IsNullOrEmpty(tenantCode))
|
||||
{
|
||||
query = query.Where(u => u.TenantInfo.Id.ToString() == tenantId);
|
||||
query = query.Where(u => u.TenantInfo.TenantCode.Contains(tenantCode));
|
||||
}
|
||||
|
||||
return await query.CountAsync();
|
||||
|
||||
@ -4,12 +4,14 @@ using Microsoft.AspNetCore.Identity;
|
||||
using System.Security.Claims;
|
||||
using Fengling.Console.Datas;
|
||||
using Fengling.Console.Models.Entities;
|
||||
using Fengling.Platform.Domain.AggregatesModel.TenantAggregate;
|
||||
|
||||
namespace Fengling.Console.Services;
|
||||
|
||||
public interface ITenantService
|
||||
{
|
||||
Task<(IEnumerable<TenantDto> Items, int TotalCount)> GetTenantsAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null);
|
||||
Task<(IEnumerable<TenantDto> Items, int TotalCount)> GetTenantsAsync(int page, int pageSize, string? name = null,
|
||||
string? tenantCode = null, TenantStatus? status = null);
|
||||
Task<TenantDto?> GetTenantAsync(long id);
|
||||
Task<IEnumerable<UserDto>> GetTenantUsersAsync(long tenantId);
|
||||
Task<IEnumerable<object>> GetTenantRolesAsync(long tenantId);
|
||||
@ -29,10 +31,11 @@ public class TenantService(
|
||||
IHttpContextAccessor httpContextAccessor)
|
||||
: ITenantService
|
||||
{
|
||||
public async Task<(IEnumerable<TenantDto> Items, int TotalCount)> GetTenantsAsync(int page, int pageSize, string? name = null, string? tenantId = null, string? status = null)
|
||||
public async Task<(IEnumerable<TenantDto> 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, tenantId, status);
|
||||
var totalCount = await repository.CountAsync(name, tenantId, status);
|
||||
var tenants = await repository.GetPagedAsync(page, pageSize, name, tenantCode, status);
|
||||
var totalCount = await repository.CountAsync(name, tenantCode, status);
|
||||
|
||||
var tenantDtos = new List<TenantDto>();
|
||||
foreach (var tenant in tenants)
|
||||
@ -41,7 +44,7 @@ public class TenantService(
|
||||
tenantDtos.Add(new TenantDto
|
||||
{
|
||||
Id = tenant.Id,
|
||||
TenantId = tenant.TenantId,
|
||||
TenantCode = tenant.TenantCode,
|
||||
Name = tenant.Name,
|
||||
ContactName = tenant.ContactName,
|
||||
ContactEmail = tenant.ContactEmail,
|
||||
@ -66,7 +69,7 @@ public class TenantService(
|
||||
return new TenantDto
|
||||
{
|
||||
Id = tenant.Id,
|
||||
TenantId = tenant.TenantId,
|
||||
TenantCode = tenant.TenantCode,
|
||||
Name = tenant.Name,
|
||||
ContactName = tenant.ContactName,
|
||||
ContactEmail = tenant.ContactEmail,
|
||||
@ -100,8 +103,8 @@ public class TenantService(
|
||||
UserName = user.UserName,
|
||||
Email = user.Email,
|
||||
RealName = user.RealName,
|
||||
TenantId = user.TenantInfo.Id,
|
||||
TenantName = user.TenantInfo.Name,
|
||||
TenantCode = user.TenantInfo.TenantCode,
|
||||
TenantName = user.TenantInfo.TenantName,
|
||||
Roles = roles.ToList(),
|
||||
EmailConfirmed = user.EmailConfirmed,
|
||||
IsActive = !user.LockoutEnabled || user.LockoutEnd == null || user.LockoutEnd < DateTimeOffset.UtcNow,
|
||||
@ -156,33 +159,23 @@ public class TenantService(
|
||||
throw new KeyNotFoundException($"Tenant with ID {id} not found");
|
||||
}
|
||||
|
||||
await CreateAuditLog("tenant", "update", "TenantSettings", tenant.Id, tenant.TenantId, null, System.Text.Json.JsonSerializer.Serialize(settings));
|
||||
await CreateAuditLog("tenant", "update", "TenantSettings", tenant.Id, tenant.Name, null, System.Text.Json.JsonSerializer.Serialize(settings));
|
||||
}
|
||||
|
||||
public async Task<TenantDto> CreateTenantAsync(CreateTenantDto dto)
|
||||
{
|
||||
var tenant = new Tenant
|
||||
{
|
||||
TenantId = dto.TenantId,
|
||||
Name = dto.Name,
|
||||
ContactName = dto.ContactName,
|
||||
ContactEmail = dto.ContactEmail,
|
||||
ContactPhone = dto.ContactPhone,
|
||||
MaxUsers = dto.MaxUsers,
|
||||
Description = dto.Description,
|
||||
Status = dto.Status,
|
||||
ExpiresAt = dto.ExpiresAt,
|
||||
CreatedAt = DateTime.UtcNow
|
||||
};
|
||||
var tenant = new Tenant(dto.TenantCode, dto.Name, dto.ContactName, dto.ContactEmail,
|
||||
dto.ContactPhone,dto.MaxUsers,dto.Description,dto.ExpiresAt);
|
||||
|
||||
await repository.AddAsync(tenant);
|
||||
|
||||
await CreateAuditLog("tenant", "create", "Tenant", tenant.Id, tenant.TenantId, null, System.Text.Json.JsonSerializer.Serialize(dto));
|
||||
await CreateAuditLog("tenant", "create", "Tenant", tenant.Id, tenant.Name, null,
|
||||
System.Text.Json.JsonSerializer.Serialize(dto));
|
||||
|
||||
return new TenantDto
|
||||
{
|
||||
Id = tenant.Id,
|
||||
TenantId = tenant.TenantId,
|
||||
TenantCode = tenant.TenantCode,
|
||||
Name = tenant.Name,
|
||||
ContactName = tenant.ContactName,
|
||||
ContactEmail = tenant.ContactEmail,
|
||||
@ -206,24 +199,17 @@ public class TenantService(
|
||||
|
||||
var oldValue = System.Text.Json.JsonSerializer.Serialize(tenant);
|
||||
|
||||
tenant.Name = dto.Name;
|
||||
tenant.ContactName = dto.ContactName;
|
||||
tenant.ContactEmail = dto.ContactEmail;
|
||||
tenant.ContactPhone = dto.ContactPhone;
|
||||
tenant.MaxUsers = dto.MaxUsers;
|
||||
tenant.Description = dto.Description;
|
||||
tenant.Status = dto.Status;
|
||||
tenant.ExpiresAt = dto.ExpiresAt;
|
||||
tenant.UpdatedAt = DateTime.UtcNow;
|
||||
tenant.UpdateInfo(dto.Name,dto.ContactName,dto.ContactEmail,dto.ContactPhone);
|
||||
|
||||
await repository.UpdateAsync(tenant);
|
||||
|
||||
await CreateAuditLog("tenant", "update", "Tenant", tenant.Id, tenant.TenantId, oldValue, System.Text.Json.JsonSerializer.Serialize(tenant));
|
||||
await CreateAuditLog("tenant", "update", "Tenant", tenant.Id, tenant.Name, oldValue,
|
||||
System.Text.Json.JsonSerializer.Serialize(tenant));
|
||||
|
||||
return new TenantDto
|
||||
{
|
||||
Id = tenant.Id,
|
||||
TenantId = tenant.TenantId,
|
||||
TenantCode = tenant.TenantCode,
|
||||
Name = tenant.Name,
|
||||
ContactName = tenant.ContactName,
|
||||
ContactEmail = tenant.ContactEmail,
|
||||
@ -255,10 +241,10 @@ public class TenantService(
|
||||
await context.SaveChangesAsync();
|
||||
}
|
||||
|
||||
tenant.IsDeleted = true;
|
||||
tenant.Delete();;
|
||||
await repository.UpdateAsync(tenant);
|
||||
|
||||
await CreateAuditLog("tenant", "delete", "Tenant", tenant.Id, tenant.TenantId, oldValue);
|
||||
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)
|
||||
|
||||
Loading…
Reference in New Issue
Block a user