feat(network): add latest package leg totals to network statistics response
Build and Deploy to Kubernetes / build-and-deploy (push) Successful in 7m51s

- Introduced new properties for GoldLeftLegTotal, GoldRightLegTotal, SilverLeftLegTotal, and SilverRightLegTotal in GetNetworkStatisticsResponseDto.
- Implemented LoadLatestPackageLegScoresAsync method in GetNetworkStatisticsQueryHandler to fetch the latest leg totals for the user.
- Updated Protobuf definitions to include new fields for leg totals in GetMyNetworkStatisticsResponse.
- Bumped Protobuf project version to reflect the addition of new features.
This commit is contained in:
masoodafar-web
2026-08-27 21:28:03 +03:30
parent ea1d2d4400
commit 87d61abc5b
5 changed files with 88 additions and 96 deletions
@@ -1,4 +1,5 @@
using CMSMicroservice.Domain.Enums; using CMSMicroservice.Domain.Enums;
using Microsoft.EntityFrameworkCore;
namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics; namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkStatistics;
@@ -124,6 +125,9 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
RightCount = x.RightCount RightCount = x.RightCount
}).ToList(); }).ToList();
var (goldLeft, goldRight, silverLeft, silverRight) =
await LoadLatestPackageLegScoresAsync(userId, cancellationToken);
return new GetNetworkStatisticsResponseDto return new GetNetworkStatisticsResponseDto
{ {
TotalMembers = totalMembers, TotalMembers = totalMembers,
@@ -136,10 +140,79 @@ public class GetNetworkStatisticsQueryHandler : IRequestHandler<GetNetworkStatis
MaxDepth = maxDepth, MaxDepth = maxDepth,
LevelDistribution = levelDistribution, LevelDistribution = levelDistribution,
MonthlyGrowth = monthlyGrowth, MonthlyGrowth = monthlyGrowth,
TopUsers = topUserModels TopUsers = topUserModels,
GoldLeftLegTotal = goldLeft,
GoldRightLegTotal = goldRight,
SilverLeftLegTotal = silverLeft,
SilverRightLegTotal = silverRight
}; };
} }
/// <summary>
/// آخرین Left/RightLegTotal طلایی و نقره‌ای فقط برای خود کاربر (نوار آمار شجره).
/// </summary>
private async Task<(int GoldLeft, int GoldRight, int SilverLeft, int SilverRight)> LoadLatestPackageLegScoresAsync(
long userId,
CancellationToken cancellationToken)
{
var packages = await _context.Packages
.AsNoTracking()
.Where(p => !p.IsDeleted && p.IsActive)
.Select(p => new { p.Id, p.Title, p.IsBasePackage, p.MaxBalancesPerLeg })
.ToListAsync(cancellationToken);
var goldPackageId = packages.FirstOrDefault(p => p.IsBasePackage)?.Id
?? packages.FirstOrDefault(p => p.Title.Contains("طلایی") || p.Title.Contains("طلايی"))?.Id;
var silverPackageId = packages.FirstOrDefault(p =>
!p.IsBasePackage && (p.Title.Contains("نقره") || p.MaxBalancesPerLeg <= 30))?.Id
?? packages.FirstOrDefault(p => !p.IsBasePackage)?.Id;
var packageIds = new List<long>();
if (goldPackageId.HasValue) packageIds.Add(goldPackageId.Value);
if (silverPackageId.HasValue && silverPackageId != goldPackageId)
packageIds.Add(silverPackageId.Value);
if (packageIds.Count == 0)
return (0, 0, 0, 0);
var balances = await _context.NetworkWeeklyBalances
.AsNoTracking()
.Where(b => b.UserId == userId && packageIds.Contains(b.PackageId))
.Select(b => new { b.PackageId, b.WeekDefinitionId, b.LeftLegTotal, b.RightLegTotal })
.ToListAsync(cancellationToken);
int goldLeft = 0, goldRight = 0, silverLeft = 0, silverRight = 0;
if (goldPackageId.HasValue)
{
var gold = balances
.Where(b => b.PackageId == goldPackageId.Value)
.OrderByDescending(b => b.WeekDefinitionId)
.FirstOrDefault();
if (gold != null)
{
goldLeft = gold.LeftLegTotal;
goldRight = gold.RightLegTotal;
}
}
if (silverPackageId.HasValue)
{
var silver = balances
.Where(b => b.PackageId == silverPackageId.Value)
.OrderByDescending(b => b.WeekDefinitionId)
.FirstOrDefault();
if (silver != null)
{
silverLeft = silver.LeftLegTotal;
silverRight = silver.RightLegTotal;
}
}
return (goldLeft, goldRight, silverLeft, silverRight);
}
/// <summary> /// <summary>
/// Recursively get all descendants of a user /// Recursively get all descendants of a user
/// </summary> /// </summary>
@@ -13,6 +13,14 @@ public class GetNetworkStatisticsResponseDto
public List<LevelDistributionModel> LevelDistribution { get; set; } = new(); public List<LevelDistributionModel> LevelDistribution { get; set; } = new();
public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new(); public List<MonthlyGrowthModel> MonthlyGrowth { get; set; } = new();
public List<TopNetworkUserModel> TopUsers { get; set; } = new(); public List<TopNetworkUserModel> TopUsers { get; set; } = new();
/// <summary>آخرین LeftLegTotal پکیج طلایی برای همین کاربر</summary>
public int GoldLeftLegTotal { get; set; }
public int GoldRightLegTotal { get; set; }
public int SilverLeftLegTotal { get; set; }
public int SilverRightLegTotal { get; set; }
public string WeakerLeg => LeftLegCount <= RightLegCount ? "Left" : "Right";
} }
public class LevelDistributionModel public class LevelDistributionModel
@@ -41,11 +41,6 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
// تبدیل نتایج flat به ساختار درختی // تبدیل نتایج flat به ساختار درختی
var tree = BuildTreeFromFlatNodes(flatNodes); var tree = BuildTreeFromFlatNodes(flatNodes);
if (tree != null)
{
await EnrichWithPackageLegScoresAsync(tree, flatNodes, cancellationToken);
}
return tree; return tree;
} }
catch (Exception ex) catch (Exception ex)
@@ -198,93 +193,4 @@ public class GetNetworkTreeQueryHandler : IRequestHandler<GetNetworkTreeQuery, N
var rootNode = flatNodes.FirstOrDefault(n => n.NetworkLevel == 0); var rootNode = flatNodes.FirstOrDefault(n => n.NetworkLevel == 0);
return rootNode != null ? nodeDict[rootNode.UserId] : null; return rootNode != null ? nodeDict[rootNode.UserId] : null;
} }
/// <summary>
/// آخرین Left/RightLegTotal طلایی و نقره‌ای برای هر نود درخت (برنامه‌ریزی پای چپ/راست).
/// </summary>
private async Task EnrichWithPackageLegScoresAsync(
NetworkTreeDto root,
List<NetworkTreeNodeDto> flatNodes,
CancellationToken cancellationToken)
{
var userIds = flatNodes.Select(n => n.UserId).Distinct().ToList();
if (userIds.Count == 0) return;
var packages = await _context.Packages
.AsNoTracking()
.Where(p => !p.IsDeleted && p.IsActive)
.Select(p => new { p.Id, p.Title, p.IsBasePackage, p.MaxBalancesPerLeg })
.ToListAsync(cancellationToken);
var goldPackageId = packages.FirstOrDefault(p => p.IsBasePackage)?.Id
?? packages.FirstOrDefault(p => p.Title.Contains("طلایی") || p.Title.Contains("طلايی"))?.Id;
var silverPackageId = packages.FirstOrDefault(p =>
!p.IsBasePackage && (p.Title.Contains("نقره") || p.MaxBalancesPerLeg <= 30))?.Id
?? packages.FirstOrDefault(p => !p.IsBasePackage)?.Id;
if (!goldPackageId.HasValue && !silverPackageId.HasValue)
{
_logger.LogWarning("GetNetworkTree: no gold/silver packages found for leg score enrichment");
return;
}
var packageIds = new List<long>();
if (goldPackageId.HasValue) packageIds.Add(goldPackageId.Value);
if (silverPackageId.HasValue && silverPackageId != goldPackageId)
packageIds.Add(silverPackageId.Value);
var rawBalances = await _context.NetworkWeeklyBalances
.AsNoTracking()
.Where(b => userIds.Contains(b.UserId) && packageIds.Contains(b.PackageId))
.Select(b => new LegScoreRow(
b.UserId,
b.PackageId,
b.WeekDefinitionId,
b.LeftLegTotal,
b.RightLegTotal))
.ToListAsync(cancellationToken);
var latestByUserPackage = rawBalances
.GroupBy(b => (b.UserId, b.PackageId))
.ToDictionary(
g => g.Key,
g => g.OrderByDescending(x => x.WeekDefinitionId).First());
ApplyLegScores(root, goldPackageId, silverPackageId, latestByUserPackage);
}
private static void ApplyLegScores(
NetworkTreeDto node,
long? goldPackageId,
long? silverPackageId,
Dictionary<(long UserId, long PackageId), LegScoreRow> latestByUserPackage)
{
if (goldPackageId.HasValue &&
latestByUserPackage.TryGetValue((node.UserId, goldPackageId.Value), out var gold))
{
node.GoldLeftLegTotal = gold.LeftLegTotal;
node.GoldRightLegTotal = gold.RightLegTotal;
}
if (silverPackageId.HasValue &&
latestByUserPackage.TryGetValue((node.UserId, silverPackageId.Value), out var silver))
{
node.SilverLeftLegTotal = silver.LeftLegTotal;
node.SilverRightLegTotal = silver.RightLegTotal;
}
if (node.LeftChild != null)
ApplyLegScores(node.LeftChild, goldPackageId, silverPackageId, latestByUserPackage);
if (node.RightChild != null)
ApplyLegScores(node.RightChild, goldPackageId, silverPackageId, latestByUserPackage);
}
private sealed record LegScoreRow(
long UserId,
long PackageId,
long WeekDefinitionId,
int LeftLegTotal,
int RightLegTotal);
} }
@@ -3,7 +3,7 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<Version>0.0.210</Version> <Version>0.0.211</Version>
<DebugType>None</DebugType> <DebugType>None</DebugType>
<DebugSymbols>False</DebugSymbols> <DebugSymbols>False</DebugSymbols>
<GeneratePackageOnBuild>False</GeneratePackageOnBuild> <GeneratePackageOnBuild>False</GeneratePackageOnBuild>
@@ -328,6 +328,11 @@ message GetMyNetworkStatisticsResponse
string my_network_leg = 11; string my_network_leg = 11;
string my_referral_code = 12; string my_referral_code = 12;
CustomerLastMemberModel last_member = 13; CustomerLastMemberModel last_member = 13;
// آخرین NetworkWeeklyBalance خود کاربر — برای نوار آمار پایین شجره (D3)
int32 gold_left_leg_total = 14;
int32 gold_right_leg_total = 15;
int32 silver_left_leg_total = 16;
int32 silver_right_leg_total = 17;
} }
message CustomerLastMemberModel message CustomerLastMemberModel