feat: Add monitoring alerts skeleton and enhance worker with notifications

This commit is contained in:
masoodafar-web
2025-11-30 20:18:10 +03:30
parent 55fa71e09b
commit 199e7e99d1
23 changed files with 5038 additions and 1168 deletions
@@ -1,12 +1,23 @@
using CMSMicroservice.Domain.Events;
using CMSMicroservice.Application.Common.Interfaces;
using Microsoft.Extensions.Logging;
namespace CMSMicroservice.Application.UserCQ.Commands.CreateNewUser;
public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand, CreateNewUserResponseDto>
{
private readonly IApplicationDbContext _context;
private readonly INetworkPlacementService _networkPlacementService;
private readonly ILogger<CreateNewUserCommandHandler> _logger;
public CreateNewUserCommandHandler(IApplicationDbContext context)
public CreateNewUserCommandHandler(
IApplicationDbContext context,
INetworkPlacementService networkPlacementService,
ILogger<CreateNewUserCommandHandler> logger)
{
_context = context;
_networkPlacementService = networkPlacementService;
_logger = logger;
}
public async Task<CreateNewUserResponseDto> Handle(CreateNewUserCommand request,
@@ -15,9 +26,71 @@ public class CreateNewUserCommandHandler : IRequestHandler<CreateNewUserCommand,
var entity = request.Adapt<User>();
entity.ReferralCode = UtilExtensions.Generate(digits: 10, firstDigitNonZero: true);
// === تنظیم Network Binary Tree ===
// اگر ParentId تنظیم شده، باید NetworkParentId و LegPosition هم Set بشن
if (request.ParentId.HasValue)
{
// محاسبه LegPosition برای Binary Tree
var legPosition = await _networkPlacementService.CalculateLegPositionAsync(
request.ParentId.Value,
cancellationToken);
if (legPosition.HasValue)
{
// Parent می‌تواند فرزند جدید بپذیرد
entity.NetworkParentId = request.ParentId.Value;
entity.LegPosition = legPosition.Value;
_logger.LogInformation(
"User {UserId} placed in Binary Tree: Parent={ParentId}, Leg={Leg}",
entity.Id, entity.NetworkParentId, entity.LegPosition);
}
else
{
// Parent پر است! باید Auto-Placement کنیم یا Error بدیم
_logger.LogWarning(
"Parent {ParentId} has no available leg! Finding alternative parent...",
request.ParentId.Value);
var availableParent = await _networkPlacementService.FindAvailableParentAsync(
request.ParentId.Value,
cancellationToken);
if (availableParent.HasValue)
{
var newLegPosition = await _networkPlacementService.CalculateLegPositionAsync(
availableParent.Value,
cancellationToken);
entity.NetworkParentId = availableParent.Value;
entity.LegPosition = newLegPosition!.Value;
_logger.LogInformation(
"User {UserId} auto-placed under alternative Parent={ParentId}, Leg={Leg}",
entity.Id, entity.NetworkParentId, entity.LegPosition);
}
else
{
// هیچ جای خالی در Binary Tree پیدا نشد!
_logger.LogError(
"No available parent found in network for ParentId={ParentId}",
request.ParentId.Value);
throw new InvalidOperationException(
$"شبکه Parent با شناسه {request.ParentId.Value} پر است و نمی‌تواند کاربر جدید بپذیرد.");
}
}
}
else
{
// کاربر Root است (بدون Parent)
_logger.LogInformation("Creating root user without Parent");
}
await _context.Users.AddAsync(entity, cancellationToken);
entity.AddDomainEvent(new CreateNewUserEvent(entity));
await _context.SaveChangesAsync(cancellationToken);
return entity.Adapt<CreateNewUserResponseDto>();
}
}
@@ -0,0 +1,18 @@
using MediatR;
namespace CMSMicroservice.Application.UserCQ.Commands.MigrateNetworkParentId;
/// <summary>
/// Command for manual migration of ParentId → NetworkParentId
/// این Command در صورتی که Seeder اجرا نشده یا نیاز به اجرای دستی باشد، استفاده می‌شود
/// </summary>
public record MigrateNetworkParentIdCommand : IRequest<MigrateNetworkParentIdResult>;
public record MigrateNetworkParentIdResult
{
public bool Success { get; init; }
public int MigratedCount { get; init; }
public int SkippedCount { get; init; }
public List<string> ValidationErrors { get; init; } = new();
public string Message { get; init; } = string.Empty;
}
@@ -0,0 +1,139 @@
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.Common.Interfaces;
using CMSMicroservice.Domain.Enums;
namespace CMSMicroservice.Application.UserCQ.Commands.MigrateNetworkParentId;
public class MigrateNetworkParentIdCommandHandler : IRequestHandler<MigrateNetworkParentIdCommand, MigrateNetworkParentIdResult>
{
private readonly IApplicationDbContext _context;
private readonly ILogger<MigrateNetworkParentIdCommandHandler> _logger;
public MigrateNetworkParentIdCommandHandler(
IApplicationDbContext context,
ILogger<MigrateNetworkParentIdCommandHandler> logger)
{
_context = context;
_logger = logger;
}
public async Task<MigrateNetworkParentIdResult> Handle(MigrateNetworkParentIdCommand request, CancellationToken cancellationToken)
{
_logger.LogInformation("=== Starting Manual ParentId → NetworkParentId Migration ===");
var errors = new List<string>();
// Step 1: Check if already migrated
var alreadyMigrated = await _context.Users
.Where(u => u.ParentId != null && u.NetworkParentId != null)
.AnyAsync(cancellationToken);
if (alreadyMigrated)
{
_logger.LogWarning("⚠️ Migration already completed!");
return new MigrateNetworkParentIdResult
{
Success = false,
Message = "Migration already completed. All users with ParentId have NetworkParentId."
};
}
// Step 2: Find users to migrate
var usersToMigrate = await _context.Users
.Where(u => u.ParentId != null && u.NetworkParentId == null)
.OrderBy(u => u.Id)
.ToListAsync(cancellationToken);
if (usersToMigrate.Count == 0)
{
return new MigrateNetworkParentIdResult
{
Success = true,
Message = "No users to migrate. All done!"
};
}
// Step 3: Group by ParentId
var parentGroups = usersToMigrate.GroupBy(u => u.ParentId);
int migratedCount = 0;
int skippedCount = 0;
foreach (var group in parentGroups)
{
var parentId = group.Key;
var children = group.OrderBy(u => u.Id).ToList();
if (children.Count > 2)
{
var warning = $"Parent {parentId} has {children.Count} children! Taking first 2 only.";
_logger.LogWarning(warning);
errors.Add(warning);
skippedCount += (children.Count - 2);
children = children.Take(2).ToList();
}
// Assign NetworkParentId and LegPosition
for (int i = 0; i < children.Count && i < 2; i++)
{
var child = children[i];
child.NetworkParentId = parentId;
child.LegPosition = i == 0 ? NetworkLeg.Left : NetworkLeg.Right;
migratedCount++;
}
}
// Step 4: Save changes
await _context.SaveChangesAsync(cancellationToken);
_logger.LogInformation("✅ Migration Completed! Migrated={Migrated}, Skipped={Skipped}",
migratedCount, skippedCount);
// Step 5: Validate
await ValidateAsync(errors, cancellationToken);
return new MigrateNetworkParentIdResult
{
Success = true,
MigratedCount = migratedCount,
SkippedCount = skippedCount,
ValidationErrors = errors,
Message = $"Migration completed successfully. Migrated: {migratedCount}, Skipped: {skippedCount}"
};
}
private async Task ValidateAsync(List<string> errors, CancellationToken cancellationToken)
{
// Check orphaned nodes
var orphanedUsers = await _context.Users
.Where(u => u.NetworkParentId != null &&
!_context.Users.Any(p => p.Id == u.NetworkParentId))
.Select(u => u.Id)
.ToListAsync(cancellationToken);
if (orphanedUsers.Any())
{
var error = $"Found {orphanedUsers.Count} orphaned users: {string.Join(", ", orphanedUsers)}";
_logger.LogError(error);
errors.Add(error);
}
// Check binary tree violation
var parentsWithTooManyChildren = await _context.Users
.Where(u => u.NetworkParentId != null)
.GroupBy(u => u.NetworkParentId)
.Select(g => new { ParentId = g.Key, Count = g.Count() })
.Where(x => x.Count > 2)
.ToListAsync(cancellationToken);
if (parentsWithTooManyChildren.Any())
{
var error = $"Binary tree violation! {parentsWithTooManyChildren.Count} parents have >2 children";
_logger.LogError(error);
errors.Add(error);
}
}
}