diff --git a/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQuery.cs b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQuery.cs new file mode 100644 index 0000000..c24d3e3 --- /dev/null +++ b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQuery.cs @@ -0,0 +1,18 @@ +namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree; + +/// +/// Query برای دریافت درخت شبکه یک زیرمجموعه +/// با چک امنیتی که فقط زیرمجموعه‌های خود کاربر قابل مشاهده باشند +/// +public record GetSubordinateTreeQuery : IRequest +{ + /// + /// شناسه کاربر زیرمجموعه که می‌خواهیم درختش را ببینیم + /// + public long TargetUserId { get; init; } + + /// + /// حداکثر عمق درخت (پیش‌فرض: 3) + /// + public int MaxDepth { get; init; } = 3; +} diff --git a/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQueryHandler.cs b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQueryHandler.cs new file mode 100644 index 0000000..2bfa377 --- /dev/null +++ b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeQueryHandler.cs @@ -0,0 +1,106 @@ +using CMSMicroservice.Protobuf.Protos.NetworkMembership; +using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; + +namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree; + +/// +/// Handler برای دریافت درخت زیرمجموعه +/// امنیت: کاربر باید احراز هویت شده باشد (توکن معتبر) +/// CMS خودش چک می‌کند که آیا targetUserId در زیرمجموعه‌های کاربر توکن هست یا نه +/// +public class GetSubordinateTreeQueryHandler : IRequestHandler +{ + private readonly IApplicationContractContext _context; + private readonly ICurrentUserService _currentUserService; + + public GetSubordinateTreeQueryHandler( + IApplicationContractContext context, + ICurrentUserService currentUserService) + { + _context = context; + _currentUserService = currentUserService; + } + + public async Task Handle(GetSubordinateTreeQuery request, CancellationToken cancellationToken) + { + var currentUserId = _currentUserService.UserId ?? throw new UnauthorizedAccessException("User not authenticated"); + var targetUserId = request.TargetUserId; + + // اگر کاربر درخت خودش را می‌خواهد + if (targetUserId == currentUserId || targetUserId == 0) + { + targetUserId = currentUserId; + } + + var cmsRequest = new GetNetworkTreeRequest + { + UserId = targetUserId, + MaxDepth = Math.Clamp(request.MaxDepth, 1, 15) + }; + + var response = await _context.NetworkMemberships.GetNetworkTreeAsync(cmsRequest, cancellationToken: cancellationToken); + var rootNode = BuildTreeFromFlatList(response.Nodes.ToList(), targetUserId); + + return new GetSubordinateTreeResponseDto + { + RootNode = rootNode, + TotalMembers = response.Nodes.Count, + CurrentDepth = CalculateDepth(rootNode), + CanGoBack = targetUserId != currentUserId, + ParentUserId = targetUserId != currentUserId ? currentUserId : null + }; + } + + /// + /// Build hierarchical tree from flat list + /// + private NetworkNodeDto? BuildTreeFromFlatList(List flatNodes, long rootUserId) + { + if (flatNodes == null || flatNodes.Count == 0) + return null; + + var nodeDict = flatNodes.ToDictionary(n => n.UserId); + var rootCmsNode = flatNodes.FirstOrDefault(n => n.UserId == rootUserId); + + if (rootCmsNode == null) + return null; + + return BuildNodeRecursive(rootCmsNode, flatNodes, nodeDict, 0); + } + + private NetworkNodeDto BuildNodeRecursive( + NetworkTreeNodeModel cmsNode, + List allNodes, + Dictionary nodeDict, + int level) + { + var leftChild = allNodes.FirstOrDefault(n => + n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 0); + var rightChild = allNodes.FirstOrDefault(n => + n.ParentId.HasValue && n.ParentId.Value == cmsNode.UserId && n.NetworkLeg == 1); + + var position = level == 0 ? "Root" : (cmsNode.NetworkLeg == 0 ? "Left" : "Right"); + + return new NetworkNodeDto + { + UserId = cmsNode.UserId, + FullName = cmsNode.UserName ?? string.Empty, + Mobile = string.Empty, + Avatar = null, + Position = position, + Level = level, + IsActive = cmsNode.IsActive, + JoinedAt = cmsNode.JoinedAt?.ToDateTime(), + IsClubActive = cmsNode.IsClubActive, + ActivationWeekNumber = cmsNode.ActivationWeekNumber, + LeftChild = leftChild != null ? BuildNodeRecursive(leftChild, allNodes, nodeDict, level + 1) : null, + RightChild = rightChild != null ? BuildNodeRecursive(rightChild, allNodes, nodeDict, level + 1) : null + }; + } + + private int CalculateDepth(NetworkNodeDto? node) + { + if (node == null) return 0; + return 1 + Math.Max(CalculateDepth(node.LeftChild), CalculateDepth(node.RightChild)); + } +} diff --git a/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeResponseDto.cs b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeResponseDto.cs new file mode 100644 index 0000000..ae9cfa8 --- /dev/null +++ b/src/FrontOffice.BFF.Application/NetworkMembershipCQ/Queries/GetSubordinateTree/GetSubordinateTreeResponseDto.cs @@ -0,0 +1,34 @@ +using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; + +namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree; + +/// +/// Response برای درخت زیرمجموعه - ساختار مشابه GetMyNetworkTreeResponseDto +/// +public class GetSubordinateTreeResponseDto +{ + /// + /// نود ریشه (کاربر زیرمجموعه انتخاب شده) + /// + public NetworkNodeDto? RootNode { get; set; } + + /// + /// تعداد کل اعضا در این زیردرخت + /// + public int TotalMembers { get; set; } + + /// + /// عمق فعلی درخت + /// + public int CurrentDepth { get; set; } + + /// + /// آیا کاربر می‌تواند به عقب برگردد (root خودش نیست) + /// + public bool CanGoBack { get; set; } + + /// + /// شناسه parent برای بازگشت به عقب + /// + public long? ParentUserId { get; set; } +} diff --git a/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs b/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs index 5a62b29..9ba00de 100644 --- a/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs +++ b/src/FrontOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs @@ -1,6 +1,7 @@ using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition; +using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree; using Google.Protobuf.WellKnownTypes; using ProtoDto = FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; @@ -14,12 +15,22 @@ public class NetworkMembershipProfile : IRegister config.NewConfig() .Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3); + config.NewConfig() + .Map(dest => dest.TargetUserId, src => src.TargetUserId) + .Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3); + // Response mappings - Tree config.NewConfig() .Map(dest => dest.RootNode, src => src.RootNode) .Map(dest => dest.TotalMembers, src => src.TotalMembers) .Map(dest => dest.CurrentDepth, src => src.CurrentDepth); + // Subordinate tree uses same response type + config.NewConfig() + .Map(dest => dest.RootNode, src => src.RootNode) + .Map(dest => dest.TotalMembers, src => src.TotalMembers) + .Map(dest => dest.CurrentDepth, src => src.CurrentDepth); + config.NewConfig() .Map(dest => dest.UserId, src => src.UserId) .Map(dest => dest.FullName, src => src.FullName) diff --git a/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs b/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs index 4d025a8..33092e8 100644 --- a/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs +++ b/src/FrontOffice.BFF.WebApi/Services/NetworkMembershipService.cs @@ -2,6 +2,7 @@ using FrontOffice.BFF.WebApi.Common.Services; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkStatistics; using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkPosition; +using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree; using FrontOffice.BFF.NetworkMembership.Protobuf.Protos.NetworkMembership; namespace FrontOffice.BFF.WebApi.Services; @@ -26,6 +27,14 @@ public class NetworkMembershipService : NetworkMembershipContract.NetworkMembers return await _dispatchRequestToCQRS.Handle(request, context); } + /// + /// دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی) + /// + public override async Task GetSubordinateTree(GetSubordinateTreeRequest request, ServerCallContext context) + { + return await _dispatchRequestToCQRS.Handle(request, context); + } + /// /// دریافت آمار شبکه کاربر جاری /// diff --git a/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto b/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto index 7a386a3..41be677 100644 --- a/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto +++ b/src/Protobufs/FrontOffice.BFF.NetworkMembership.Protobuf/Protos/networkmembership.proto @@ -19,6 +19,13 @@ service NetworkMembershipContract }; }; + // دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی) + rpc GetSubordinateTree(GetSubordinateTreeRequest) returns (GetMyNetworkTreeResponse){ + option (google.api.http) = { + get: "/NetworkMembership/SubordinateTree/{target_user_id}" + }; + }; + // دریافت آمار شبکه کاربر جاری rpc GetMyNetworkStatistics(google.protobuf.Empty) returns (GetMyNetworkStatisticsResponse){ option (google.api.http) = { @@ -40,6 +47,13 @@ message GetMyNetworkTreeRequest int32 max_depth = 1; // حداکثر عمق درخت (پیش‌فرض: 3) } +// ============ GetSubordinateTree ============ +message GetSubordinateTreeRequest +{ + int64 target_user_id = 1; // شناسه زیرمجموعه مورد نظر + int32 max_depth = 2; // حداکثر عمق درخت (پیش‌فرض: 3) +} + message GetMyNetworkTreeResponse { NetworkNodeModel root_node = 1;