-- ============================================= -- Stored Procedure: sp_CalculateWeeklyBalances -- Description: محاسبه تعادل‌های هفتگی شبکه باینری با استفاده از CTE -- Author: System -- Created: 2025-12-19 -- ============================================= CREATE OR ALTER PROCEDURE [CMS].[sp_CalculateWeeklyBalances] @WeekDefinitionId BIGINT, @ForceRecalculate BIT = 0, @PackageId BIGINT = NULL, -- پکیج خاص (اگر NULL از پکیج پایه استفاده می‌کند) @InputMaxBalancesPerLeg INT = NULL, -- سقف تعادل هر پا (از Package entity) @InputMaxNetworkLevel INT = NULL, -- حداکثر عمق شبکه (از Package entity) @RowCount INT OUTPUT AS BEGIN SET NOCOUNT ON; SET XACT_ABORT ON; DECLARE @ErrorMessage NVARCHAR(4000); DECLARE @StartDate DATETIME; DECLARE @EndDate DATETIME; DECLARE @PreviousWeekDefinitionId BIGINT; DECLARE @MaxBalancesPerLeg INT; DECLARE @MaxNetworkLevel INT; DECLARE @CalculatedAt DATETIME = GETDATE(); -- تعیین PackageId پیش‌فرض (پکیج پایه) IF @PackageId IS NULL BEGIN SELECT TOP 1 @PackageId = Id FROM CMS.Packages WHERE IsBasePackage = 1 AND IsDeleted = 0; END -- خواندن تنظیمات از Package entity اگر پارامتر داده نشده IF @InputMaxBalancesPerLeg IS NOT NULL SET @MaxBalancesPerLeg = @InputMaxBalancesPerLeg; ELSE SELECT @MaxBalancesPerLeg = ISNULL(MaxBalancesPerLeg, 300) FROM CMS.Packages WHERE Id = @PackageId; IF @InputMaxNetworkLevel IS NOT NULL SET @MaxNetworkLevel = @InputMaxNetworkLevel; ELSE SELECT @MaxNetworkLevel = ISNULL(MaxNetworkLevel, 15) FROM CMS.Packages WHERE Id = @PackageId; -- fallbackهای امن SET @MaxBalancesPerLeg = ISNULL(@MaxBalancesPerLeg, 300); SET @MaxNetworkLevel = ISNULL(@MaxNetworkLevel, 15); BEGIN TRY BEGIN TRANSACTION; -- ============================================= -- 1. اعتبارسنجی WeekDefinitionId -- ============================================= IF NOT EXISTS (SELECT 1 FROM CMS.WeekDefinitions WHERE Id = @WeekDefinitionId) BEGIN SET @ErrorMessage = N'هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' در سیستم تعریف نشده است'; RAISERROR(@ErrorMessage, 16, 1); RETURN; END -- دریافت بازه تاریخی هفته SELECT @StartDate = StartDate, @EndDate = EndDate FROM CMS.WeekDefinitions WHERE Id = @WeekDefinitionId; -- ============================================= -- 2. بررسی محاسبه قبلی -- ============================================= IF EXISTS (SELECT 1 FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId) BEGIN IF @ForceRecalculate = 0 BEGIN SET @ErrorMessage = N'تعادل‌های هفته ' + CAST(@WeekDefinitionId AS NVARCHAR(20)) + N' قبلاً محاسبه شده است. برای محاسبه مجدد از ForceRecalculate استفاده کنید'; RAISERROR(@ErrorMessage, 16, 1); RETURN; END -- حذف محاسبات قبلی DELETE FROM CMS.NetworkWeeklyBalances WHERE WeekDefinitionId = @WeekDefinitionId; END -- ============================================= -- 3. پیدا کردن هفته قبل -- ============================================= SELECT TOP 1 @PreviousWeekDefinitionId = Id FROM CMS.WeekDefinitions WHERE WeekOrder = (SELECT WeekOrder - 1 FROM CMS.WeekDefinitions WHERE Id = @WeekDefinitionId) AND IsActive = 1; -- ============================================= -- 4. تنظیمات پکیج (از پارامترها خوانده می‌شود) -- ============================================= -- @MaxBalancesPerLeg و @MaxNetworkLevel قبلاً مقداردهی شده‌اند -- ============================================= -- 5. ایجاد جدول موقت برای نتایج -- ============================================= CREATE TABLE #Balances ( UserId BIGINT PRIMARY KEY, LeftLegNewMembers INT DEFAULT 0, RightLegNewMembers INT DEFAULT 0, LeftLegCarryover INT DEFAULT 0, RightLegCarryover INT DEFAULT 0, LeftLegTotal INT DEFAULT 0, RightLegTotal INT DEFAULT 0, TotalBalances INT DEFAULT 0, LeftLegRemainder INT DEFAULT 0, RightLegRemainder INT DEFAULT 0, FlushedPerSide INT DEFAULT 0, TotalFlushed INT DEFAULT 0, SubordinateBalances INT DEFAULT 0 ); -- ============================================= -- 6. دریافت کاربران فعال باشگاه -- ============================================= INSERT INTO #Balances (UserId) SELECT DISTINCT u.Id FROM CMS.Users u INNER JOIN CMS.ClubMemberships cm ON cm.UserId = u.Id AND cm.IsActive = 1 WHERE cm.PackageId = @PackageId; -- ============================================= -- 7. دریافت باقیمانده هفته قبل -- ============================================= IF @PreviousWeekDefinitionId IS NOT NULL BEGIN UPDATE b SET b.LeftLegCarryover = ISNULL(nb.LeftLegRemainder, 0), b.RightLegCarryover = ISNULL(nb.RightLegRemainder, 0) FROM #Balances b LEFT JOIN CMS.NetworkWeeklyBalances nb ON nb.UserId = b.UserId AND nb.WeekDefinitionId = @PreviousWeekDefinitionId AND nb.PackageId = @PackageId; END -- ============================================= -- 8. محاسبه اعضای جدید هر پا با CTE (بهینه!) -- ============================================= -- CTE برای شمارش اعضای جدید شاخه چپ ;WITH LeftLegCTE AS ( -- فرزند مستقیم چپ (لول 1) SELECT u.NetworkParentId AS ParentId, u.Id AS ChildId, 1 AS Level FROM CMS.Users u WHERE u.LegPosition = 0 -- Left AND u.NetworkParentId IS NOT NULL UNION ALL -- زیرمجموعه‌های بازگشتی SELECT cte.ParentId, u.Id AS ChildId, cte.Level + 1 AS Level FROM CMS.Users u INNER JOIN LeftLegCTE cte ON u.NetworkParentId = cte.ChildId WHERE cte.Level < @MaxNetworkLevel ), LeftNewMembers AS ( SELECT cte.ParentId AS UserId, COUNT(*) AS NewMembersCount FROM LeftLegCTE cte INNER JOIN CMS.ClubMemberships cm ON cm.UserId = cte.ChildId AND cm.IsActive = 1 WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate GROUP BY cte.ParentId ) UPDATE b SET b.LeftLegNewMembers = ISNULL(lnm.NewMembersCount, 0) FROM #Balances b LEFT JOIN LeftNewMembers lnm ON lnm.UserId = b.UserId OPTION (MAXRECURSION 100); -- CTE برای شمارش اعضای جدید شاخه راست ;WITH RightLegCTE AS ( -- فرزند مستقیم راست (لول 1) SELECT u.NetworkParentId AS ParentId, u.Id AS ChildId, 1 AS Level FROM CMS.Users u WHERE u.LegPosition = 1 -- Right AND u.NetworkParentId IS NOT NULL UNION ALL -- زیرمجموعه‌های بازگشتی SELECT cte.ParentId, u.Id AS ChildId, cte.Level + 1 AS Level FROM CMS.Users u INNER JOIN RightLegCTE cte ON u.NetworkParentId = cte.ChildId WHERE cte.Level < @MaxNetworkLevel ), RightNewMembers AS ( SELECT cte.ParentId AS UserId, COUNT(*) AS NewMembersCount FROM RightLegCTE cte INNER JOIN CMS.ClubMemberships cm ON cm.UserId = cte.ChildId AND cm.IsActive = 1 WHERE cm.ActivatedAt >= @StartDate AND cm.ActivatedAt <= @EndDate GROUP BY cte.ParentId ) UPDATE b SET b.RightLegNewMembers = ISNULL(rnm.NewMembersCount, 0) FROM #Balances b LEFT JOIN RightNewMembers rnm ON rnm.UserId = b.UserId OPTION (MAXRECURSION 100); -- ============================================= -- 9. محاسبه تعادل‌ها -- ============================================= UPDATE #Balances SET -- مجموع هر پا LeftLegTotal = LeftLegNewMembers + LeftLegCarryover, RightLegTotal = RightLegNewMembers + RightLegCarryover; UPDATE #Balances SET -- تعادل = MIN(چپ، راست) TotalBalances = CASE WHEN LeftLegTotal <= RightLegTotal THEN LeftLegTotal ELSE RightLegTotal END; UPDATE #Balances SET -- باقیمانده برای هفته بعد LeftLegRemainder = LeftLegTotal - TotalBalances, RightLegRemainder = RightLegTotal - TotalBalances, -- فلش (اگر تعادل > سقف) FlushedPerSide = CASE WHEN TotalBalances > @MaxBalancesPerLeg THEN TotalBalances - @MaxBalancesPerLeg ELSE 0 END; UPDATE #Balances SET -- اعمال سقف TotalBalances = CASE WHEN TotalBalances > @MaxBalancesPerLeg THEN @MaxBalancesPerLeg ELSE TotalBalances END, -- مجموع فلش از دو طرف TotalFlushed = FlushedPerSide * 2; -- ============================================= -- 10. محاسبه تعادل زیرمجموعه با CTE -- ============================================= ;WITH SubordinateCTE AS ( -- فرزندان مستقیم (لول 1) SELECT u.NetworkParentId AS ParentId, u.Id AS ChildId, 1 AS Level FROM CMS.Users u WHERE u.NetworkParentId IS NOT NULL UNION ALL -- زیرمجموعه‌های بازگشتی SELECT cte.ParentId, u.Id AS ChildId, cte.Level + 1 AS Level FROM CMS.Users u INNER JOIN SubordinateCTE cte ON u.NetworkParentId = cte.ChildId WHERE cte.Level < @MaxNetworkLevel ), SubordinateBalances AS ( SELECT cte.ParentId AS UserId, SUM(b.TotalBalances) AS TotalSubordinateBalances FROM SubordinateCTE cte INNER JOIN #Balances b ON b.UserId = cte.ChildId GROUP BY cte.ParentId ) UPDATE b SET b.SubordinateBalances = ISNULL(sb.TotalSubordinateBalances, 0) FROM #Balances b LEFT JOIN SubordinateBalances sb ON sb.UserId = b.UserId OPTION (MAXRECURSION 100); -- ============================================= -- 11. Insert نتایج در جدول اصلی -- ============================================= INSERT INTO CMS.NetworkWeeklyBalances ( UserId, WeekDefinitionId, PackageId, LeftLegNewMembers, RightLegNewMembers, LeftLegCarryover, RightLegCarryover, LeftLegTotal, RightLegTotal, TotalBalances, LeftLegRemainder, RightLegRemainder, FlushedPerSide, TotalFlushed, SubordinateBalances, LeftLegBalances, RightLegBalances, WeeklyPoolContribution, CalculatedAt, IsExpired, IsDeleted, Created, CreatedBy, LastModified, LastModifiedBy ) SELECT UserId, @WeekDefinitionId, @PackageId, LeftLegNewMembers, RightLegNewMembers, LeftLegCarryover, RightLegCarryover, LeftLegTotal, RightLegTotal, TotalBalances, LeftLegRemainder, RightLegRemainder, FlushedPerSide, TotalFlushed, SubordinateBalances, LeftLegTotal, -- LeftLegBalances (deprecated) RightLegTotal, -- RightLegBalances (deprecated) 0, -- WeeklyPoolContribution @CalculatedAt, 0, -- IsExpired 0, -- IsDeleted @CalculatedAt, -- Created 'SP', -- CreatedBy @CalculatedAt, -- LastModified 'SP' -- LastModifiedBy FROM #Balances; SET @RowCount = @@ROWCOUNT; -- پاکسازی DROP TABLE #Balances; COMMIT TRANSACTION; END TRY BEGIN CATCH IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION; -- پاکسازی در صورت خطا IF OBJECT_ID('tempdb..#Balances') IS NOT NULL DROP TABLE #Balances; SET @ErrorMessage = ERROR_MESSAGE(); RAISERROR(@ErrorMessage, 16, 1); END CATCH END GO