diff --git a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj index ff3298e..dd5c36f 100644 --- a/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj +++ b/src/CMSMicroservice.Application/CMSMicroservice.Application.csproj @@ -11,6 +11,7 @@ + diff --git a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs index a4e43ff..849830a 100644 --- a/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs +++ b/src/CMSMicroservice.Application/Common/Interfaces/IApplicationDbContext.cs @@ -2,6 +2,7 @@ using CMSMicroservice.Domain.Entities.Payment; using CMSMicroservice.Domain.Entities.Order; using CMSMicroservice.Domain.Entities.DiscountShop; using CMSMicroservice.Domain.Entities.Geography; +using Microsoft.EntityFrameworkCore.Infrastructure; namespace CMSMicroservice.Application.Common.Interfaces; @@ -60,5 +61,10 @@ public interface IApplicationDbContext DbSet States { get; } DbSet Cities { get; } + /// + /// دسترسی به DatabaseFacade برای اجرای raw SQL و Stored Procedures + /// + DatabaseFacade Database { get; } + Task SaveChangesAsync(CancellationToken cancellationToken = default); } \ No newline at end of file diff --git a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs index a349938..44bbcb9 100644 --- a/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs +++ b/src/CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs @@ -1,144 +1,178 @@ +using System.Data; +using System.Data.Common; +using Microsoft.EntityFrameworkCore; + namespace CMSMicroservice.Application.NetworkMembershipCQ.Queries.GetNetworkTree; public class GetNetworkTreeQueryHandler : IRequestHandler { private readonly IApplicationDbContext _context; - private readonly IWeekDefinitionRepository _weekDefinitionRepository; + private readonly ILogger _logger; public GetNetworkTreeQueryHandler( IApplicationDbContext context, - IWeekDefinitionRepository weekDefinitionRepository) + ILogger logger) { _context = context; - _weekDefinitionRepository = weekDefinitionRepository; + _logger = logger; } public async Task Handle(GetNetworkTreeQuery request, CancellationToken cancellationToken) { - var rootUser = await _context.Users - .AsNoTracking() - .FirstOrDefaultAsync(x => x.Id == request.UserId, cancellationToken); - - if (rootUser == null) + try { - return null; - } + // دریافت نتایج flat از Stored Procedure + var flatNodes = await ExecuteStoredProcedureAsync(request, cancellationToken); + + if (flatNodes == null || !flatNodes.Any()) + { + return null; + } - var tree = await BuildTree(rootUser.Id, request.MaxDepth, 0, cancellationToken, request); - return tree; + // تبدیل نتایج flat به ساختار درختی + var tree = BuildTreeFromFlatNodes(flatNodes); + return tree; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error executing GetNetworkTree for UserId: {UserId}", request.UserId); + throw; + } } - private async Task BuildTree(long userId, int maxDepth, int currentDepth, CancellationToken cancellationToken, GetNetworkTreeQuery request) + /// + /// اجرای Stored Procedure و دریافت نتایج + /// + private async Task> ExecuteStoredProcedureAsync( + GetNetworkTreeQuery request, + CancellationToken cancellationToken) { - // دریافت کاربر با اطلاعات باشگاه مشتریان - var user = await _context.Users - .Include(u => u.ClubMembership) - .AsNoTracking() - .FirstOrDefaultAsync(x => x.Id == userId, cancellationToken); - - if (user == null) + var results = new List(); + + var connection = _context.Database.GetDbConnection(); + + try { - throw new NotFoundException(nameof(User), userId); + if (connection.State != ConnectionState.Open) + { + await connection.OpenAsync(cancellationToken); + } + + using var command = connection.CreateCommand(); + command.CommandText = "[CMS].[GetNetworkTree]"; + command.CommandType = CommandType.StoredProcedure; + command.CommandTimeout = 120; // 2 minutes timeout for large trees + + // Parameters - use DbParameter for provider-agnostic code + var rootUserIdParam = command.CreateParameter(); + rootUserIdParam.ParameterName = "@RootUserId"; + rootUserIdParam.DbType = DbType.Int64; + rootUserIdParam.Value = request.UserId; + command.Parameters.Add(rootUserIdParam); + + var maxDepthParam = command.CreateParameter(); + maxDepthParam.ParameterName = "@MaxDepth"; + maxDepthParam.DbType = DbType.Int32; + maxDepthParam.Value = request.MaxDepth > 0 ? request.MaxDepth : 100; + command.Parameters.Add(maxDepthParam); + + var isClubActiveParam = command.CreateParameter(); + isClubActiveParam.ParameterName = "@IsClubActive"; + isClubActiveParam.DbType = DbType.Boolean; + isClubActiveParam.Value = request.IsClubActive.HasValue ? request.IsClubActive.Value : DBNull.Value; + command.Parameters.Add(isClubActiveParam); + + var weekParam = command.CreateParameter(); + weekParam.ParameterName = "@ActivationWeekDefinitionId"; + weekParam.DbType = DbType.Int64; + weekParam.Value = request.ActivationWeekDefinitionId.HasValue ? request.ActivationWeekDefinitionId.Value : DBNull.Value; + command.Parameters.Add(weekParam); + + using var reader = await command.ExecuteReaderAsync(cancellationToken); + + while (await reader.ReadAsync(cancellationToken)) + { + var node = new NetworkTreeNodeDto + { + UserId = reader.GetInt64(reader.GetOrdinal("UserId")), + Mobile = reader.IsDBNull(reader.GetOrdinal("Mobile")) ? null : reader.GetString(reader.GetOrdinal("Mobile")), + FirstName = reader.IsDBNull(reader.GetOrdinal("FirstName")) ? null : reader.GetString(reader.GetOrdinal("FirstName")), + LastName = reader.IsDBNull(reader.GetOrdinal("LastName")) ? null : reader.GetString(reader.GetOrdinal("LastName")), + LegPosition = reader.IsDBNull(reader.GetOrdinal("LegPosition")) ? null : reader.GetInt32(reader.GetOrdinal("LegPosition")), + ParentId = reader.IsDBNull(reader.GetOrdinal("ParentId")) ? null : reader.GetInt64(reader.GetOrdinal("ParentId")), + NetworkLevel = reader.GetInt32(reader.GetOrdinal("NetworkLevel")), + ClubActivatedAt = reader.IsDBNull(reader.GetOrdinal("ClubActivatedAt")) ? null : reader.GetDateTime(reader.GetOrdinal("ClubActivatedAt")), + IsClubActive = reader.GetBoolean(reader.GetOrdinal("IsClubActive")), + ActivationWeekDefinitionId = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDefinitionId")) ? null : reader.GetInt64(reader.GetOrdinal("ActivationWeekDefinitionId")), + ActivationWeekDisplayName = reader.IsDBNull(reader.GetOrdinal("ActivationWeekDisplayName")) ? null : reader.GetString(reader.GetOrdinal("ActivationWeekDisplayName")), + IsActivatedInTargetWeek = reader.GetBoolean(reader.GetOrdinal("IsActivatedInTargetWeek")), + UserCreated = new DateTimeOffset(reader.GetDateTime(reader.GetOrdinal("UserCreated"))) + }; + + results.Add(node); + } + } + finally + { + // Connection is managed by DbContext, don't close it here } - // محاسبه شماره هفته فعال‌سازی - long? activationWeekDefinitionId = null; - string? activationWeekDisplayName = null; - bool isActivatedInTargetWeek = false; - - if (user.ClubMembership?.ActivatedAt != null) + _logger.LogInformation("GetNetworkTree SP returned {Count} nodes for UserId: {UserId}", results.Count, request.UserId); + return results; + } + + /// + /// تبدیل لیست flat به ساختار درختی + /// + private NetworkTreeDto? BuildTreeFromFlatNodes(List flatNodes) + { + if (!flatNodes.Any()) return null; + + // Dictionary برای دسترسی سریع به نودها + var nodeDict = new Dictionary(); + + // ایجاد همه نودها + foreach (var flatNode in flatNodes) { - // activationWeekDefinitionId = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value); - var week = _weekDefinitionRepository.GetWeekByDate(user.ClubMembership.ActivatedAt.Value); - activationWeekDefinitionId = week.Id; - activationWeekDisplayName = week.DisplayName; - - // بررسی آیا در هفته هدف فعال شده است - if (request.ActivationWeekDefinitionId!=null) + nodeDict[flatNode.UserId] = new NetworkTreeDto { - isActivatedInTargetWeek = activationWeekDefinitionId == request.ActivationWeekDefinitionId; + UserId = flatNode.UserId, + Mobile = flatNode.Mobile, + FirstName = flatNode.FirstName, + LastName = flatNode.LastName, + LegPosition = flatNode.LegPosition.HasValue ? (NetworkLeg)flatNode.LegPosition.Value : null, + CurrentDepth = flatNode.NetworkLevel, + ClubActivatedAt = flatNode.ClubActivatedAt, + IsClubActive = flatNode.IsClubActive, + ActivationWeekDefinitionId = flatNode.ActivationWeekDefinitionId, + ActivationWeekDisplayName = flatNode.ActivationWeekDisplayName, + IsActivatedInTargetWeek = flatNode.IsActivatedInTargetWeek, + UserCreated = flatNode.UserCreated + }; + } + + // برقراری ارتباط Parent-Child + foreach (var flatNode in flatNodes) + { + if (flatNode.ParentId.HasValue && nodeDict.ContainsKey(flatNode.ParentId.Value)) + { + var parent = nodeDict[flatNode.ParentId.Value]; + var child = nodeDict[flatNode.UserId]; + + // تعیین موقعیت چپ یا راست + if (flatNode.LegPosition == (int)NetworkLeg.Left) + { + parent.LeftChild = child; + } + else if (flatNode.LegPosition == (int)NetworkLeg.Right) + { + parent.RightChild = child; + } } } - var node = new NetworkTreeDto - { - UserId = user.Id, - Mobile = user.Mobile, - FirstName = user.FirstName, - LastName = user.LastName, - LegPosition = user.LegPosition, - CurrentDepth = currentDepth, - ClubActivatedAt = user.ClubMembership?.ActivatedAt, - IsClubActive = user.ClubMembership?.IsActive ?? false, - ActivationWeekDefinitionId = activationWeekDefinitionId, - ActivationWeekDisplayName = activationWeekDisplayName, - IsActivatedInTargetWeek = isActivatedInTargetWeek, - UserCreated = user.Created - }; - - // اگر به حداکثر عمق رسیدیم، دیگر فرزندان را نمی‌خوانیم - if (currentDepth >= maxDepth) - { - return node; - } - - // پیدا کردن فرزندان (چپ و راست) - var children = await GetFilteredChildren(userId, request, cancellationToken); - - var leftChild = children.FirstOrDefault(c => c.LegPosition == NetworkLeg.Left); - if (leftChild != null) - { - node.LeftChild = await BuildTree(leftChild.Id, maxDepth, currentDepth + 1, cancellationToken, request); - } - - var rightChild = children.FirstOrDefault(c => c.LegPosition == NetworkLeg.Right); - if (rightChild != null) - { - node.RightChild = await BuildTree(rightChild.Id, maxDepth, currentDepth + 1, cancellationToken, request); - } - - return node; + // پیدا کردن ریشه (اولین نود با Level=0) + var rootNode = flatNodes.FirstOrDefault(n => n.NetworkLevel == 0); + return rootNode != null ? nodeDict[rootNode.UserId] : null; } - - /// - /// دریافت فرزندان با اعمال فیلترها - /// - private async Task> GetFilteredChildren(long parentId, GetNetworkTreeQuery request, CancellationToken cancellationToken) - { - var query = _context.Users - .Include(u => u.ClubMembership) - .AsNoTracking() - .Where(x => x.NetworkParentId == parentId); - - // اعمال فیلتر IsClubActive - if (request.IsClubActive.HasValue) - { - query = query.Where(u => - u.ClubMembership != null && - u.ClubMembership.IsDeleted == false && - u.ClubMembership.IsActive == request.IsClubActive.Value); - } - - return await query.ToListAsync(cancellationToken); - } - - /// - /// محاسبه شماره هفته از تاریخ - /// - // private long CalculateWeekNumber(DateTime date) - // { - // // First try to get from repository cache - // var weekDef = _weekDefinitionRepository.GetWeekByDate(date); - // if (weekDef != null) - // { - // return weekDef.Id; - // } - // - // // Fallback: use repository's calculation method - // // return _weekDefinitionRepository.CalculateGregorianWeekNumber(date); - // return 0; - // } - - - }