feat: add subordinate network tree query with security checks
This commit is contained in:
+18
@@ -0,0 +1,18 @@
|
||||
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
|
||||
|
||||
/// <summary>
|
||||
/// Query برای دریافت درخت شبکه یک زیرمجموعه
|
||||
/// با چک امنیتی که فقط زیرمجموعههای خود کاربر قابل مشاهده باشند
|
||||
/// </summary>
|
||||
public record GetSubordinateTreeQuery : IRequest<GetSubordinateTreeResponseDto>
|
||||
{
|
||||
/// <summary>
|
||||
/// شناسه کاربر زیرمجموعه که میخواهیم درختش را ببینیم
|
||||
/// </summary>
|
||||
public long TargetUserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// حداکثر عمق درخت (پیشفرض: 3)
|
||||
/// </summary>
|
||||
public int MaxDepth { get; init; } = 3;
|
||||
}
|
||||
+106
@@ -0,0 +1,106 @@
|
||||
using CMSMicroservice.Protobuf.Protos.NetworkMembership;
|
||||
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
||||
|
||||
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
|
||||
|
||||
/// <summary>
|
||||
/// Handler برای دریافت درخت زیرمجموعه
|
||||
/// امنیت: کاربر باید احراز هویت شده باشد (توکن معتبر)
|
||||
/// CMS خودش چک میکند که آیا targetUserId در زیرمجموعههای کاربر توکن هست یا نه
|
||||
/// </summary>
|
||||
public class GetSubordinateTreeQueryHandler : IRequestHandler<GetSubordinateTreeQuery, GetSubordinateTreeResponseDto>
|
||||
{
|
||||
private readonly IApplicationContractContext _context;
|
||||
private readonly ICurrentUserService _currentUserService;
|
||||
|
||||
public GetSubordinateTreeQueryHandler(
|
||||
IApplicationContractContext context,
|
||||
ICurrentUserService currentUserService)
|
||||
{
|
||||
_context = context;
|
||||
_currentUserService = currentUserService;
|
||||
}
|
||||
|
||||
public async Task<GetSubordinateTreeResponseDto> 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
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Build hierarchical tree from flat list
|
||||
/// </summary>
|
||||
private NetworkNodeDto? BuildTreeFromFlatList(List<NetworkTreeNodeModel> 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<NetworkTreeNodeModel> allNodes,
|
||||
Dictionary<long, NetworkTreeNodeModel> 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));
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
using FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetMyNetworkTree;
|
||||
|
||||
namespace FrontOffice.BFF.Application.NetworkMembershipCQ.Queries.GetSubordinateTree;
|
||||
|
||||
/// <summary>
|
||||
/// Response برای درخت زیرمجموعه - ساختار مشابه GetMyNetworkTreeResponseDto
|
||||
/// </summary>
|
||||
public class GetSubordinateTreeResponseDto
|
||||
{
|
||||
/// <summary>
|
||||
/// نود ریشه (کاربر زیرمجموعه انتخاب شده)
|
||||
/// </summary>
|
||||
public NetworkNodeDto? RootNode { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// تعداد کل اعضا در این زیردرخت
|
||||
/// </summary>
|
||||
public int TotalMembers { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// عمق فعلی درخت
|
||||
/// </summary>
|
||||
public int CurrentDepth { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// آیا کاربر میتواند به عقب برگردد (root خودش نیست)
|
||||
/// </summary>
|
||||
public bool CanGoBack { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// شناسه parent برای بازگشت به عقب
|
||||
/// </summary>
|
||||
public long? ParentUserId { get; set; }
|
||||
}
|
||||
@@ -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<ProtoDto.GetMyNetworkTreeRequest, GetMyNetworkTreeQuery>()
|
||||
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
|
||||
|
||||
config.NewConfig<ProtoDto.GetSubordinateTreeRequest, GetSubordinateTreeQuery>()
|
||||
.Map(dest => dest.TargetUserId, src => src.TargetUserId)
|
||||
.Map(dest => dest.MaxDepth, src => src.MaxDepth > 0 ? src.MaxDepth : 3);
|
||||
|
||||
// Response mappings - Tree
|
||||
config.NewConfig<GetMyNetworkTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>()
|
||||
.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<GetSubordinateTreeResponseDto, ProtoDto.GetMyNetworkTreeResponse>()
|
||||
.Map(dest => dest.RootNode, src => src.RootNode)
|
||||
.Map(dest => dest.TotalMembers, src => src.TotalMembers)
|
||||
.Map(dest => dest.CurrentDepth, src => src.CurrentDepth);
|
||||
|
||||
config.NewConfig<NetworkNodeDto, ProtoDto.NetworkNodeModel>()
|
||||
.Map(dest => dest.UserId, src => src.UserId)
|
||||
.Map(dest => dest.FullName, src => src.FullName)
|
||||
|
||||
@@ -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<GetMyNetworkTreeRequest, GetMyNetworkTreeQuery, GetMyNetworkTreeResponse>(request, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت درخت شبکه یک زیرمجموعه (با چک امنیتی)
|
||||
/// </summary>
|
||||
public override async Task<GetMyNetworkTreeResponse> GetSubordinateTree(GetSubordinateTreeRequest request, ServerCallContext context)
|
||||
{
|
||||
return await _dispatchRequestToCQRS.Handle<GetSubordinateTreeRequest, GetSubordinateTreeQuery, GetMyNetworkTreeResponse>(request, context);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// دریافت آمار شبکه کاربر جاری
|
||||
/// </summary>
|
||||
|
||||
+14
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user