feat: add IsActive field to UserClubFeatures for admin management

This commit is contained in:
masoodafar-web
2025-12-12 01:40:26 +03:30
parent aa66ca10c8
commit ff1c1d5d61
68 changed files with 11456 additions and 198 deletions
@@ -28,9 +28,17 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
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 => x.NetworkParentId.HasValue)
.Where(x => activeClubMemberUserIds.Contains(x.Id))
.Select(x => new { x.Id })
.ToListAsync(cancellationToken);
@@ -47,7 +55,7 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
.ToDictionaryAsync(x => x.UserId, cancellationToken);
var balancesList = new List<NetworkWeeklyBalance>();
var calculatedAt = DateTime.UtcNow;
var calculatedAt = DateTime.Now;
// خواندن یکباره Configuration ها (بهینه‌سازی - به جای N query)
var configs = await _context.SystemConfigurations
@@ -57,15 +65,15 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
x.Key == "Commission.MaxWeeklyBalancesPerLeg" ||
x.Key == "Commission.MaxNetworkLevel"))
.ToDictionaryAsync(x => x.Key, x => x.Value, cancellationToken);
var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
// var activationFee = long.Parse(configs.GetValueOrDefault("Club.ActivationFee", "25000000"));
// var poolPercent = decimal.Parse(configs.GetValueOrDefault("Commission.WeeklyPoolContributionPercent", "20")) / 100m;
// سقف تعادل هفتگی برای هر دست (نه کل) - 300 برای چپ + 300 برای راست = حداکثر 600 تعادل
var maxBalancesPerLeg = int.Parse(configs.GetValueOrDefault("Commission.MaxWeeklyBalancesPerLeg", "300"));
// حداکثر عمق شبکه برای شمارش اعضا (15 لول)
var maxNetworkLevel = int.Parse(configs.GetValueOrDefault("Commission.MaxNetworkLevel", "15"));
foreach (var user in usersInNetwork)
foreach (var user in usersInNetwork.OrderBy(o=>o.Id))
{
// دریافت باقیمانده هفته قبل
var leftCarryover = 0;
@@ -84,29 +92,34 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
var leftTotal = leftNewMembers + leftCarryover;
var rightTotal = rightNewMembers + rightCarryover;
// ✅ اصلاح شده: اعمال سقف روی هر دست جداگانه (نه روی کل)
// سقف 300 برای دست چپ + 300 برای دست راست = حداکثر 600 تعادل در هفته
var cappedLeftTotal = Math.Min(leftTotal, maxBalancesPerLeg);
var cappedRightTotal = Math.Min(rightTotal, maxBalancesPerLeg);
// ✅ مرحله 1: محاسبه تعادل اولیه (قبل از اعمال سقف)
// تعادل = کمترین مقدار بین چپ و راست
// مثال: چپ=500، راست=600 → تعادل=500
var totalBalances = Math.Min(leftTotal, rightTotal);
// محاسبه تعادل (کمترین مقدار بعد از اعمال سقف)
var totalBalances = Math.Min(cappedLeftTotal, cappedRightTotal);
// ✅ مرحله 2: محاسبه باقیمانده (قبل از سقف)
// باقیمانده = اضافه‌ای که یک طرف دارد
// مثال: چپ=500، راست=600، تعادل=500
// → باقی چپ = 500-500 = 0
// → باقی راست = 600-500 = 100 (می‌رود برای هفته بعد)
var leftRemainder = leftTotal - totalBalances;
var rightRemainder = rightTotal - totalBalances;
// محاسبه باقیمانده برای هفته بعد
// باقیمانده = مقداری که از سقف هر دست رد شده
// مثال: چپ=350، راست=450، سقف=300
// cappedLeft = MIN(350, 300) = 300
// cappedRight = MIN(450, 300) = 300
// totalBalances = MIN(300, 300) = 300
// leftRemainder = 350 - 300 = 50 (مازاد سقف)
// rightRemainder = 450 - 300 = 150 (مازاد سقف)
var leftRemainder = leftTotal - cappedLeftTotal;
var rightRemainder = rightTotal - cappedRightTotal;
// ✅ مرحله 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 (از بین می‌رود)
// محاسبه سهم استخر (20% از مجموع فعال‌سازی‌های جدید کل شبکه)
// طبق گفته دکتر: کل افراد جدید در شبکه × هزینه فعال‌سازی × 20%
var totalNewMembers = leftNewMembers + rightNewMembers;
var weeklyPoolContribution = (long)(totalNewMembers * activationFee * poolPercent);
// ⚠️ توجه: تعادل زیرمجموعه در این مرحله محاسبه نمیشه
// چون هنوز تمام تعادل‌ها محاسبه نشدن
// بعد از ذخیره همه تعادل‌ها، در یک حلقه دوم محاسبه خواهد شد
var balance = new NetworkWeeklyBalance
{
@@ -122,19 +135,26 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
// مجموع
LeftLegTotal = leftTotal,
RightLegTotal = rightTotal,
TotalBalances = totalBalances, // تعادل واقعی بعد از اعمال سقف روی هر دست
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 = weeklyPoolContribution,
WeeklyPoolContribution = 0, // Pool در مرحله بعد محاسبه میشه
CalculatedAt = calculatedAt,
IsExpired = false
};
@@ -145,6 +165,26 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
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;
}
@@ -248,4 +288,64 @@ public class CalculateWeeklyBalancesCommandHandler : IRequestHandler<CalculateWe
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;
}
}