362 lines
16 KiB
C#
362 lines
16 KiB
C#
using CMSMicroservice.Domain.Common;
|
||
|
||
namespace CMSMicroservice.Application.CommissionCQ.Commands.CalculateWeeklyBalances;
|
||
|
||
public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWeeklyBalancesCommand, int>
|
||
{
|
||
private readonly IApplicationDbContext _context;
|
||
private readonly IWeekDefinitionRepository _weekRepository;
|
||
|
||
public CalculateWeeklyBalancesCommandHandler(
|
||
IApplicationDbContext context,
|
||
IWeekDefinitionRepository weekRepository)
|
||
{
|
||
_context = context;
|
||
_weekRepository = weekRepository;
|
||
}
|
||
|
||
public async Task<int> Handle(CalculateWeeklyBalancesCommand request, CancellationToken cancellationToken)
|
||
{
|
||
|
||
var regorianWeekNumber = _weekRepository.GetGregorianWeekNumber(request.WeekDefinitionId);
|
||
if (regorianWeekNumber == null)
|
||
{
|
||
throw new InvalidOperationException($"هفته {request.WeekDefinitionId} در سیستم تعریف نشده است");
|
||
}
|
||
|
||
// بررسی وجود محاسبه قبلی
|
||
var existingBalances = await _context.NetworkWeeklyBalances
|
||
.Where(x => x.WeekDefinitionId == request.WeekDefinitionId)
|
||
.ToListAsync(cancellationToken);
|
||
|
||
if (existingBalances.Any() && !request.ForceRecalculate)
|
||
{
|
||
throw new InvalidOperationException($"تعادلهای هفته {request.WeekDefinitionId} قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید");
|
||
}
|
||
|
||
// حذف محاسبات قبلی در صورت ForceRecalculate
|
||
if (existingBalances.Any())
|
||
{
|
||
_context.NetworkWeeklyBalances.RemoveRange(existingBalances);
|
||
await _context.SaveChangesAsync(cancellationToken);
|
||
}
|
||
|
||
// ⭐ دریافت همه کاربرانی که عضو فعال باشگاه هستند
|
||
// بدون محدودیت زمانی - همه اعضای فعال کلاب باید کمیسیون بگیرند
|
||
var activeClubMemberUserIds = await _context.ClubMemberships
|
||
.Where(c => c.IsActive)
|
||
.Select(c => c.UserId)
|
||
.ToHashSetAsync(cancellationToken);
|
||
|
||
// دریافت کاربران فعال در شبکه که عضو باشگاه هستند
|
||
// نکته: شرط NetworkParentId.HasValue نداریم چون ریشه شبکه (اولین نفر) هم باید حساب بشه
|
||
var usersInNetwork = await _context.Users
|
||
.Where(x => activeClubMemberUserIds.Contains(x.Id))
|
||
.Select(x => new { x.Id })
|
||
.ToListAsync(cancellationToken);
|
||
|
||
// دریافت باقیماندههای هفته قبل
|
||
var previousWeekDefinitionId = GetPreviousWeekDefinitionId(request.WeekDefinitionId);
|
||
Dictionary<long, (int LeftLegRemainder, int RightLegRemainder)> previousWeekCarryovers;
|
||
|
||
if (previousWeekDefinitionId.HasValue)
|
||
{
|
||
previousWeekCarryovers = await _context.NetworkWeeklyBalances
|
||
.Where(x => x.WeekDefinitionId == previousWeekDefinitionId.Value)
|
||
.ToDictionaryAsync(
|
||
x => x.UserId,
|
||
x => (x.LeftLegRemainder, x.RightLegRemainder),
|
||
cancellationToken);
|
||
}
|
||
else
|
||
{
|
||
previousWeekCarryovers = new Dictionary<long, (int, int)>();
|
||
}
|
||
|
||
var balancesList = new List<NetworkWeeklyBalance>();
|
||
var calculatedAt = DateTime.Now;
|
||
|
||
// استفاده از SystemConstants (استاتیک - بدون کوئری به دیتابیس)
|
||
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
|
||
var maxBalancesPerLeg = SystemConstants.CommissionMaxWeeklyBalancesPerLeg;
|
||
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
|
||
var maxNetworkLevel = SystemConstants.CommissionMaxNetworkLevel;
|
||
|
||
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
|
||
{
|
||
// دریافت باقیمانده هفته قبل
|
||
var leftCarryover = 0;
|
||
var rightCarryover = 0;
|
||
if (previousWeekCarryovers.ContainsKey(user.Id))
|
||
{
|
||
leftCarryover = previousWeekCarryovers[user.Id].LeftLegRemainder;
|
||
rightCarryover = previousWeekCarryovers[user.Id].RightLegRemainder;
|
||
}
|
||
|
||
// محاسبه تعداد اعضای جدید در این هفته (تا maxNetworkLevel لول پایینتر)
|
||
var leftNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Left, request.WeekDefinitionId, maxNetworkLevel, cancellationToken);
|
||
var rightNewMembers = await CountNewMembersInLeg(user.Id, NetworkLeg.Right, request.WeekDefinitionId, maxNetworkLevel, cancellationToken);
|
||
|
||
// محاسبه مجموع هر پا (جدید + باقیمانده)
|
||
var leftTotal = leftNewMembers + leftCarryover;
|
||
var rightTotal = rightNewMembers + rightCarryover;
|
||
|
||
// ✅ مرحله 1: محاسبه تعادل اولیه (قبل از اعمال سقف)
|
||
// تعادل = کمترین مقدار بین چپ و راست
|
||
// مثال: چپ=500، راست=600 → تعادل=500
|
||
var totalBalances = Math.Min(leftTotal, rightTotal);
|
||
|
||
// ✅ مرحله 2: محاسبه باقیمانده (قبل از سقف)
|
||
// باقیمانده = اضافهای که یک طرف دارد
|
||
// مثال: چپ=500، راست=600، تعادل=500
|
||
// → باقی چپ = 500-500 = 0
|
||
// → باقی راست = 600-500 = 100 (میرود برای هفته بعد)
|
||
var leftRemainder = leftTotal - totalBalances;
|
||
var rightRemainder = rightTotal - totalBalances;
|
||
|
||
// ✅ مرحله 3: اعمال سقف 300 (برای امتیاز نهایی)
|
||
// از تعادل، فقط 300 از هر طرف حساب میشود
|
||
// مثال: تعادل=500 → امتیاز=300
|
||
// از چپ: 300 حساب میشود، 200 فلش میشود
|
||
// از راست: 300 حساب میشود، 200 فلش میشود
|
||
// جمع فلش = 400 (از بین میرود)
|
||
var cappedBalances = Math.Min(totalBalances, maxBalancesPerLeg);
|
||
|
||
// ✅ مرحله 4: محاسبه فلش (از هر دو طرف)
|
||
var flushedPerSide = totalBalances - cappedBalances; // 500-300=200
|
||
var totalFlushed = flushedPerSide * 2; // 200×2=400 (از بین میرود)
|
||
|
||
// ⚠️ توجه: تعادل زیرمجموعه در این مرحله محاسبه نمیشه
|
||
// چون هنوز تمام تعادلها محاسبه نشدن
|
||
// بعد از ذخیره همه تعادلها، در یک حلقه دوم محاسبه خواهد شد
|
||
|
||
var balance = new NetworkWeeklyBalance
|
||
{
|
||
UserId = user.Id,
|
||
WeekDefinitionId = request.WeekDefinitionId,
|
||
|
||
// اطلاعات جدید
|
||
LeftLegNewMembers = leftNewMembers,
|
||
RightLegNewMembers = rightNewMembers,
|
||
LeftLegCarryover = leftCarryover,
|
||
RightLegCarryover = rightCarryover,
|
||
|
||
// مجموع
|
||
LeftLegTotal = leftTotal,
|
||
RightLegTotal = rightTotal,
|
||
TotalBalances = cappedBalances, // امتیاز نهایی بعد از اعمال سقف 300
|
||
|
||
// باقیمانده برای هفته بعد (اضافهای که یک طرف دارد)
|
||
LeftLegRemainder = leftRemainder,
|
||
RightLegRemainder = rightRemainder,
|
||
|
||
// فلش (از دست رفته)
|
||
FlushedPerSide = flushedPerSide,
|
||
TotalFlushed = totalFlushed,
|
||
|
||
// تعادل زیرمجموعه - فعلاً 0 (بعد از ذخیره همه تعادلها محاسبه میشه)
|
||
SubordinateBalances = 0,
|
||
|
||
// فیلدهای قدیمی (deprecated) - برای سازگاری با کدهای قبلی
|
||
#pragma warning disable CS0618
|
||
LeftLegBalances = leftTotal,
|
||
RightLegBalances = rightTotal,
|
||
#pragma warning restore CS0618
|
||
|
||
WeeklyPoolContribution = 0, // Pool در مرحله بعد محاسبه میشه
|
||
CalculatedAt = calculatedAt,
|
||
IsExpired = false
|
||
};
|
||
|
||
balancesList.Add(balance);
|
||
}
|
||
|
||
await _context.NetworkWeeklyBalances.AddRangeAsync(balancesList, cancellationToken);
|
||
await _context.SaveChangesAsync(cancellationToken);
|
||
|
||
// ⭐ مرحله 2: محاسبه تعادل زیرمجموعه برای هر کاربر (تا 15 لول)
|
||
// حالا که همه تعادلها ذخیره شدن، میتونیم تعادل زیرمجموعه رو حساب کنیم
|
||
var balancesDictionary = balancesList.ToDictionary(x => x.UserId);
|
||
|
||
foreach (var balance in balancesList)
|
||
{
|
||
var subordinateBalances = await CalculateSubordinateBalancesAsync(
|
||
balance.UserId,
|
||
balancesDictionary,
|
||
maxNetworkLevel,
|
||
cancellationToken
|
||
);
|
||
|
||
balance.SubordinateBalances = subordinateBalances;
|
||
}
|
||
|
||
// ذخیره تعادلهای زیرمجموعه
|
||
_context.NetworkWeeklyBalances.UpdateRange(balancesList);
|
||
await _context.SaveChangesAsync(cancellationToken);
|
||
|
||
return balancesList.Count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// شناسه هفته قبل را از WeekDefinitionRepository میگیرد
|
||
/// </summary>
|
||
private long? GetPreviousWeekDefinitionId(long currentWeekDefinitionId)
|
||
{
|
||
var currentWeekNumber = _weekRepository.GetGregorianWeekNumber(currentWeekDefinitionId);
|
||
if (currentWeekNumber == null) return null;
|
||
|
||
var previousWeekNumber = _weekRepository.GetPreviousWeekNumber(currentWeekNumber);
|
||
if (previousWeekNumber == null) return null;
|
||
|
||
return _weekRepository.GetWeekDefinitionId(previousWeekNumber);
|
||
}
|
||
|
||
/// <summary>
|
||
/// شمارش اعضای جدیدی که در این هفته به یک پا اضافه شدند
|
||
/// تا maxLevel لول پایینتر شمارش میشود
|
||
/// </summary>
|
||
private async Task<int> CountNewMembersInLeg(long userId, NetworkLeg leg, long WeekDefinitionId, int maxLevel, CancellationToken cancellationToken)
|
||
{
|
||
// تبدیل WeekNumber به بازه تاریخی
|
||
var startDateEndDate =_weekRepository.GetWeekDateRange(WeekDefinitionId);
|
||
|
||
// شمارش تمام اعضای زیرمجموعه که در این هفته فعال شدند (تا maxLevel لول)
|
||
var count = await CountNewMembersRecursive(userId, leg, startDateEndDate.Value.startDate, startDateEndDate.Value.endDate, 0, maxLevel, cancellationToken);
|
||
|
||
return count;
|
||
}
|
||
|
||
/// <summary>
|
||
/// شمارش بازگشتی اعضای جدید در یک پا
|
||
/// محدودیت عمق: تا maxLevel لول پایینتر شمارش میشود
|
||
/// </summary>
|
||
private async Task<int> CountNewMembersRecursive(
|
||
long userId,
|
||
NetworkLeg leg,
|
||
DateTime startDate,
|
||
DateTime endDate,
|
||
int currentLevel,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// ⭐ محدودیت عمق: اگر به حداکثر لول رسیدیم، توقف
|
||
if (currentLevel >= maxLevel)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
// پیدا کردن فرزند مستقیم در پای مورد نظر
|
||
var child = await _context.Users
|
||
.FirstOrDefaultAsync(x => x.NetworkParentId == userId && x.LegPosition == leg, cancellationToken);
|
||
|
||
if (child == null)
|
||
{
|
||
return 0;
|
||
}
|
||
|
||
var count = 0;
|
||
|
||
// اگر فرزند در این هفته فعال شده، 1 امتیاز
|
||
var membership = await _context.ClubMemberships
|
||
.FirstOrDefaultAsync(x => x.UserId == child.Id && x.IsActive, cancellationToken);
|
||
|
||
if (membership?.ActivatedAt >= startDate && membership?.ActivatedAt <= endDate)
|
||
{
|
||
count = 1;
|
||
}
|
||
|
||
// جمع کردن اعضای جدید از پای چپ و راست فرزند (با افزایش لول)
|
||
var childLeft = await CountNewMembersRecursive(child.Id, NetworkLeg.Left, startDate, endDate, currentLevel + 1, maxLevel, cancellationToken);
|
||
var childRight = await CountNewMembersRecursive(child.Id, NetworkLeg.Right, startDate, endDate, currentLevel + 1, maxLevel, cancellationToken);
|
||
|
||
return count + childLeft + childRight;
|
||
}
|
||
|
||
/// <summary>
|
||
/// تبدیل شماره هفته به بازه تاریخی از WeekDefinitionRepository
|
||
/// </summary>
|
||
// private (DateTime startDate, DateTime endDate) GetWeekDateRange(string weekNumber)
|
||
// {
|
||
// var dateRange = _weekRepository.GetWeekDateRange(weekNumber);
|
||
// if (dateRange.HasValue)
|
||
// {
|
||
// return dateRange.Value;
|
||
// }
|
||
//
|
||
// // Fallback: محاسبه دستی اگر در کش نبود
|
||
// var parts = weekNumber.Split('-');
|
||
// var year = int.Parse(parts[0]);
|
||
// var week = int.Parse(parts[1].Replace("W", ""));
|
||
//
|
||
// var jan1 = new DateTime(year, 1, 1);
|
||
// var jan1DayOfWeek = (int)jan1.DayOfWeek;
|
||
// var daysToFirstSaturday = jan1DayOfWeek == 6 ? 0 : (6 - jan1DayOfWeek + 7) % 7;
|
||
// var firstSaturday = jan1.AddDays(daysToFirstSaturday);
|
||
//
|
||
// var weekStart = firstSaturday.AddDays((week - 1) * 7);
|
||
// var weekEnd = weekStart.AddDays(6).AddHours(23).AddMinutes(59).AddSeconds(59);
|
||
//
|
||
// return (weekStart, weekEnd);
|
||
// }
|
||
|
||
/// <summary>
|
||
/// محاسبه مجموع تعادلهای زیرمجموعه یک کاربر تا maxLevel لول پایینتر
|
||
/// </summary>
|
||
private async Task<int> CalculateSubordinateBalancesAsync(
|
||
long userId,
|
||
Dictionary<long, NetworkWeeklyBalance> allBalances,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// پیدا کردن همه زیرمجموعهها تا maxLevel لول
|
||
var subordinates = await GetSubordinatesRecursive(userId, 1, maxLevel, cancellationToken);
|
||
|
||
// جمع تعادلهای آنها
|
||
var totalSubordinateBalances = 0;
|
||
foreach (var subordinateId in subordinates)
|
||
{
|
||
if (allBalances.ContainsKey(subordinateId))
|
||
{
|
||
totalSubordinateBalances += allBalances[subordinateId].TotalBalances;
|
||
}
|
||
}
|
||
|
||
return totalSubordinateBalances;
|
||
}
|
||
|
||
/// <summary>
|
||
/// پیدا کردن بازگشتی زیرمجموعهها تا maxLevel لول
|
||
/// </summary>
|
||
private async Task<List<long>> GetSubordinatesRecursive(
|
||
long userId,
|
||
int currentLevel,
|
||
int maxLevel,
|
||
CancellationToken cancellationToken)
|
||
{
|
||
// محدودیت عمق
|
||
if (currentLevel > maxLevel)
|
||
{
|
||
return new List<long>();
|
||
}
|
||
|
||
var result = new List<long>();
|
||
|
||
// پیدا کردن فرزندان مستقیم
|
||
var children = await _context.Users
|
||
.Where(x => x.NetworkParentId == userId)
|
||
.Select(x => x.Id)
|
||
.ToListAsync(cancellationToken);
|
||
|
||
result.AddRange(children);
|
||
|
||
// بازگشت برای هر فرزند
|
||
foreach (var childId in children)
|
||
{
|
||
var grandChildren = await GetSubordinatesRecursive(childId, currentLevel + 1, maxLevel, cancellationToken);
|
||
result.AddRange(grandChildren);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
}
|