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
@@ -0,0 +1,283 @@
using System.Globalization;
using System.Transactions;
using MediatR;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
using CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyCommissionPool;
using CMSMicroservice.Application.CommissionCQ.Commands.ProcessUserPayouts;
using CMSMicroservice.Application.Common.Interfaces;
namespace CMSMicroservice.Infrastructure.BackgroundJobs;
/// <summary>
/// Background Worker برای محاسبه و توزیع کمیسیون‌های هفتگی شبکه
/// زمان اجرا: هر یکشنبه ساعت 23:59
/// </summary>
public class WeeklyNetworkCommissionWorker : BackgroundService
{
private readonly ILogger<WeeklyNetworkCommissionWorker> _logger;
private readonly IServiceProvider _serviceProvider;
private readonly IAlertService _alertService;
private Timer? _timer;
public WeeklyNetworkCommissionWorker(
ILogger<WeeklyNetworkCommissionWorker> logger,
IServiceProvider serviceProvider,
IAlertService alertService)
{
_logger = logger;
_serviceProvider = serviceProvider;
_alertService = alertService;
}
protected override Task ExecuteAsync(CancellationToken stoppingToken)
{
_logger.LogInformation("Weekly Network Commission Worker started at: {Time} (Local Time)", DateTime.Now);
// محاسبه زمان تا یکشنبه بعدی ساعت 23:59
var now = DateTime.Now;
var nextSunday = GetNextSunday(now);
var nextRunTime = new DateTime(nextSunday.Year, nextSunday.Month, nextSunday.Day, 23, 59, 0);
var delay = nextRunTime - now;
if (delay.TotalMilliseconds < 0)
{
// اگر زمان گذشته باشد، یکشنبه بعدی
nextRunTime = nextRunTime.AddDays(7);
delay = nextRunTime - now;
}
_logger.LogInformation("Next execution scheduled for: {NextRun}", nextRunTime);
// تنظیم timer برای اجرا در زمان مشخص و تکرار هفتگی
_timer = new Timer(
callback: async _ => await ExecuteWeeklyCalculationAsync(stoppingToken),
state: null,
dueTime: delay,
period: TimeSpan.FromDays(7) // هر 7 روز یکبار
);
return Task.CompletedTask;
}
/// <summary>
/// محاسبه تاریخ یکشنبه بعدی
/// </summary>
private static DateTime GetNextSunday(DateTime from)
{
var daysUntilSunday = ((int)DayOfWeek.Sunday - (int)from.DayOfWeek + 7) % 7;
if (daysUntilSunday == 0)
{
// اگر امروز یکشنبه است و ساعت گذشته، یکشنبه بعدی
if (from.TimeOfDay > new TimeSpan(23, 59, 0))
{
daysUntilSunday = 7;
}
}
return from.Date.AddDays(daysUntilSunday);
}
/// <summary>
/// اجرای محاسبات هفتگی کمیسیون
/// </summary>
private async Task ExecuteWeeklyCalculationAsync(CancellationToken cancellationToken)
{
var executionId = Guid.NewGuid();
var startTime = DateTime.Now; // استفاده از Local Time
_logger.LogInformation("=== Starting Weekly Commission Calculation [{ExecutionId}] at {Time} (Local Time) ===",
executionId, startTime);
try
{
using var scope = _serviceProvider.CreateScope();
var mediator = scope.ServiceProvider.GetRequiredService<IMediator>();
var context = scope.ServiceProvider.GetRequiredService<IApplicationDbContext>();
// دریافت شماره هفته قبل (هفته‌ای که باید محاسبه شود)
var previousWeekNumber = GetPreviousWeekNumber();
var currentWeekNumber = GetCurrentWeekNumber();
_logger.LogInformation("Processing week: {WeekNumber}", previousWeekNumber);
// ===== IDEMPOTENCY CHECK =====
// بررسی اینکه آیا این هفته قبلاً محاسبه شده یا نه
var existingPool = await context.WeeklyCommissionPools
.AsNoTracking()
.FirstOrDefaultAsync(x => x.WeekNumber == previousWeekNumber, cancellationToken);
if (existingPool?.IsCalculated == true)
{
_logger.LogWarning(
"Week {WeekNumber} already calculated. Skipping execution [{ExecutionId}]",
previousWeekNumber, executionId);
return;
}
// ===== TRANSACTION SCOPE =====
// تمام مراحل باید داخل یک تراکنش باشند برای Atomicity
using var transaction = new TransactionScope(
TransactionScopeOption.Required,
new TransactionOptions
{
IsolationLevel = IsolationLevel.ReadCommitted,
Timeout = TimeSpan.FromMinutes(30) // برای شبکه‌های بزرگ
},
TransactionScopeAsyncFlowOption.Enabled);
int balancesCalculated = 0;
long poolValue = 0;
int payoutsProcessed = 0;
try
{
// مرحله 1: محاسبه تعادل‌های شبکه
_logger.LogInformation("Step 1/4: Calculating network balances for week {WeekNumber}", previousWeekNumber);
balancesCalculated = await mediator.Send(new CalculateWeeklyBalancesCommand
{
WeekNumber = previousWeekNumber,
ForceRecalculate = false
}, cancellationToken);
_logger.LogInformation("Network balances calculated: {Count} users processed", balancesCalculated);
// مرحله 2: محاسبه استخر کمیسیون و ارزش هر امتیاز
_logger.LogInformation("Step 2/4: Calculating commission pool for week {WeekNumber}", previousWeekNumber);
poolValue = await mediator.Send(new CalculateWeeklyCommissionPoolCommand
{
WeekNumber = previousWeekNumber,
ForceRecalculate = false
}, cancellationToken);
_logger.LogInformation("Commission pool calculated. Value per balance: {Value:N0} Rials", poolValue);
// مرحله 3: توزیع کمیسیون‌ها به کاربران
_logger.LogInformation("Step 3/4: Processing user payouts for week {WeekNumber}", previousWeekNumber);
payoutsProcessed = await mediator.Send(new ProcessUserPayoutsCommand
{
WeekNumber = previousWeekNumber,
ForceReprocess = false
}, cancellationToken);
_logger.LogInformation("User payouts processed: {Count} payouts created", payoutsProcessed);
// ===== مرحله 4 (گام 5 در مستندات): ریست/Expire کردن تعادل‌های هفته قبل =====
_logger.LogInformation("Step 4/4: Expiring weekly balances for week {WeekNumber}", previousWeekNumber);
var balancesToExpire = await context.NetworkWeeklyBalances
.Where(x => x.WeekNumber == previousWeekNumber && !x.IsExpired)
.ToListAsync(cancellationToken);
foreach (var balance in balancesToExpire)
{
balance.IsExpired = true;
}
await context.SaveChangesAsync(cancellationToken);
_logger.LogInformation("Expired {Count} balance records", balancesToExpire.Count);
// Commit Transaction
transaction.Complete();
var duration = DateTime.Now - startTime; // محاسبه مدت زمان با Local Time
_logger.LogInformation(
"=== Weekly Commission Calculation Completed Successfully [{ExecutionId}] ===" +
"\n Week: {WeekNumber}" +
"\n Users Processed: {UserCount}" +
"\n Value Per Balance: {ValuePerBalance:N0} Rials" +
"\n Payouts Created: {PayoutCount}" +
"\n Balances Expired: {ExpiredCount}" +
"\n Duration: {Duration:mm\\:ss}",
executionId,
previousWeekNumber,
balancesCalculated,
poolValue,
payoutsProcessed,
balancesToExpire.Count,
duration
);
// Send success notification to admin
using var successScope = _serviceProvider.CreateScope();
var alertService = successScope.ServiceProvider.GetRequiredService<IAlertService>();
await alertService.SendSuccessNotificationAsync(
"Weekly Commission Completed",
$"Week {previousWeekNumber}: {payoutsProcessed} payouts, {balancesToExpire.Count} balances expired");
// TODO: Send notifications to users who received commission
// await NotifyUsersAboutPayouts(payoutsProcessed, previousWeekNumber);
}
catch (Exception innerEx)
{
_logger.LogError(innerEx,
"Transaction failed during step execution. Rolling back. [{ExecutionId}]",
executionId);
// Transaction will auto-rollback when scope is disposed without Complete()
throw;
}
}
catch (Exception ex)
{
_logger.LogCritical(ex,
"!!! CRITICAL ERROR in Weekly Commission Calculation [{ExecutionId}] !!!" +
"\n Week: {WeekNumber}" +
"\n Message: {Message}" +
"\n StackTrace: {StackTrace}" +
"\n Please investigate immediately!",
executionId,
GetPreviousWeekNumber(),
ex.Message,
ex.StackTrace);
// ===== ERROR HANDLING & ALERTING =====
// در محیط production باید Alert/Notification ارسال شود
using var errorScope = _serviceProvider.CreateScope();
var alertService = errorScope.ServiceProvider.GetRequiredService<IAlertService>();
await alertService.SendCriticalAlertAsync(
"Weekly Commission Worker Failed",
$"Worker execution {executionId} failed for week {GetPreviousWeekNumber()}",
ex,
cancellationToken);
// TODO: Retry logic با exponential backoff
// await RetryWithExponentialBackoff(() => ExecuteWeeklyCalculationAsync(cancellationToken));
}
}
/// <summary>
/// دریافت شماره هفته جاری (فرمت ISO 8601: YYYY-Www)
/// </summary>
private static string GetCurrentWeekNumber()
{
var today = DateTime.Today;
var calendar = CultureInfo.CurrentCulture.Calendar;
var weekNumber = calendar.GetWeekOfYear(
today,
CalendarWeekRule.FirstFourDayWeek,
DayOfWeek.Monday
);
return $"{today.Year}-W{weekNumber:D2}";
}
/// <summary>
/// دریافت شماره هفته قبل
/// </summary>
private static string GetPreviousWeekNumber()
{
var lastWeek = DateTime.Today.AddDays(-7);
var calendar = CultureInfo.CurrentCulture.Calendar;
var weekNumber = calendar.GetWeekOfYear(
lastWeek,
CalendarWeekRule.FirstFourDayWeek,
DayOfWeek.Monday
);
return $"{lastWeek.Year}-W{weekNumber:D2}";
}
public override void Dispose()
{
_timer?.Dispose();
base.Dispose();
}
}