# TECH-07 — لاگ کامل Session 1404/02/10 (2026-04-30) > نوع سند: **گزارش کار** > تاریخ: ۱۴۰۵/۰۲/۱۰ > مرتبط با: CMS · BackOffice · FrontOffice · Database --- ## فهرست مطالب 1. [بخش اول — رفع باگ Double-Charge Pool](#۱-رفع-باگ-double-charge-pool) 2. [بخش دوم — بررسی داده‌های هفته‌های ۲۲ و ۲۳](#۲-بررسی-داده‌های-هفته‌های-۲۲-و-۲۳) 3. [بخش سوم — ویژگی Network Tree (اطلاعات هفتگی)](#۳-ویژگی-network-tree-نوع-فعالسازی--پکیج) 4. [بخش چهارم — بهبود UI نمودار درختی](#۴-بهبود-ui-نمودار-درختی) 5. [بخش پنجم — رفع باگ Pagination فروشگاه تخفیف](#۵-رفع-باگ-pagination-فروشگاه-تخفیف) 6. [خلاصه فایل‌های تغییریافته](#خلاصه-فایل‌های-تغییریافته) 7. [وظایف باقی‌مانده (Pending)](#وظایف-باقی‌مانده) --- ## ۱. رفع باگ Double-Charge Pool ### مشکل در جریان فعال‌سازی عضویت باشگاه، Pool کمیسیون هفتگی **دوبار** شارژ می‌شد: - بار اول: در `ActivateClubMembership` (از طریق `VerifyPackagePurchase`) - بار دوم: در `AcceptClubMembershipContract` (تأیید قرارداد توسط کاربر) همچنین `CreateManualPayment` هم یک مسیر مستقل داشت که بدون Check هفته، Pool اشتباه را شارژ می‌کرد. ### ریشه مشکل تابع `GetOrCreateCurrentWeeklyPool` بدون در نظر گرفتن هفته واقعی `PackagePurchasedAt`، Pool هفته جاری را انتخاب می‌کرد. ### فایل‌های اصلاح‌شده #### `ActivateClubMembershipCommandHandler.cs` ```csharp // قبل: همیشه Pool هفته جاری را شارژ می‌کرد // بعد: فقط یک‌بار در محل صحیح (AcceptContract) شارژ می‌شود // حذف: شارژ Pool از داخل ActivateClubMembership (for isNewMembership scenario) ``` #### `AcceptClubMembershipContractCommandHandler.cs` ```csharp // اضافه: بررسی هفته قرارداد — اگر هفته PackagePurchasedAt با هفته جاری فرق دارد // از Pool هفته مناسب استفاده می‌کند نه Pool هفته جاری ``` #### `CreateManualPaymentCommandHandler.cs` ```csharp // اصلاح: Cross-week fix — Pool هفته صحیح بر اساس تاریخ پرداخت دستی ``` #### `sp_CalculateWeeklyCommissionPool.sql` (SP در Infrastructure) ```sql -- اصلاح: IsCurrentCycle check برای جلوگیری از Double-Count -- هر کاربر فقط یک‌بار در محاسبه Pool شمرده می‌شود ``` --- ## ۲. بررسی داده‌های هفته‌های ۲۲ و ۲۳ ### تشخیص با اجرای diagnostic SQL روی DB، دو anomaly کشف شد: #### هفته ۲۲ — Pool Ghost (PoolId=10056) | فیلد | مقدار | |------|-------| | TotalPoolAmount | 2,520,000 | | AllCycles | 0 | | ریشه | هانیه سادات عشاقی (UserId=189) — خرید 1404/01/12 (هفته ۲۱) ولی AcceptContract در 17:02 دقیقه بعد Pool هفته ۲۲ را شارژ کرد | **دلیل:** CreatedAt و ModifiedAt timestamp مغایرت داشت — Pool در هفته ۲۲ ایجاد شد اما Cycle در هفته ۲۱ بود. **اصلاح دستی DB (Pending):** ```sql UPDATE CMS.WeeklyCommissionPools SET TotalPoolAmount = 0, LastModified = GETUTCDATE() WHERE Id = 10056 AND WeekDefinitionId = 22; ``` #### هفته ۲۳ — Pool ناقص (PoolId=10054) | فیلد | مقدار | |------|-------| | TotalPoolAmount | 0 | | IsCalculated | False | | ریشه | محمدصادق عسلی (UserId=190) — خرید هفته ۲۳، Cycle وجود دارد ولی Pool=0 (قبل از fix) | **اصلاح دستی DB (Pending):** ```sql UPDATE CMS.WeeklyCommissionPools SET TotalPoolAmount = 2520000, LastModified = GETUTCDATE() WHERE Id = 10054 AND WeekDefinitionId = 23; ``` --- ## ۳. ویژگی Network Tree (نوع فعالسازی + پکیج) ### هدف صفحه `/network/tree` در BackOffice باید در هر node نشان دهد: - آیا این کاربر در هفته انتخابی **عضو جدید** بوده یا **تمدید کرده**؟ - نام پکیجی که خریداری کرده؟ ### پیاده‌سازی Full-Stack #### الف) SP_GetNetworkTree (dbbkup/SP_GetNetworkTree.sql) ```sql -- اضافه شد: OUTER APPLY ( SELECT TOP 1 cc.* FROM CMS.ClubMembershipCycles cc WHERE cc.ClubMembershipId = cm.Id AND cc.PackagePurchasedAt >= @WeekStartDate AND cc.PackagePurchasedAt < @WeekEndDate AND (@ActivationWeekDefinitionId IS NULL OR @WeekStartDate IS NOT NULL) ) AS cc_target -- ستون‌های جدید در output: IsActivatedInTargetWeek -- آیا در هفته انتخابی فعال شده؟ IsNewActivation -- 1=اولین فعالسازی (CycleNumber=1), 0=تمدید, NULL=بدون Cycle PackageName -- نام پکیج اون هفته PackageId -- شناسه پکیج ``` **نکته:** منطق هفته‌بندی از `cm.ActivatedAt` به `Cycle.PackagePurchasedAt` تغییر کرد. **Deploy SP:** ``` SP مستقیم روی DB اجرا شد (نه EmbeddedResource Infrastructure) اجرا شد در: /tmp/DbDiag با C# script تأیید شد: SELECT OBJECT_ID('[CMS].[GetNetworkTree]') → موجود ``` #### ب) Application Layer **`NetworkTreeNodeDto.cs`** — فیلدهای جدید: ```csharp bool? IsNewActivation string? PackageName long? PackageId ``` **`NetworkTreeDto.cs`** — همین فیلدها **`GetNetworkTreeQueryHandler.cs`**: ```csharp // خواندن از DataReader: IsNewActivation = reader.IsDBNull(reader.GetOrdinal("IsNewActivation")) ? null : reader.GetInt32(reader.GetOrdinal("IsNewActivation")) == 1, PackageName = reader["PackageName"] as string, PackageId = reader.IsDBNull(reader.GetOrdinal("PackageId")) ? null : reader.GetInt64(reader.GetOrdinal("PackageId")) ``` #### ج) Proto (networkmembership.proto) ```protobuf // NetworkTreeNodeModel — فیلدهای جدید: google.protobuf.BoolValue is_new_activation = 22; string package_name = 23; google.protobuf.Int64Value package_id = 24; ``` **NuGet Package:** `Foursat.CMSMicroservice.Protobuf` → از `0.0.194` به **`0.0.195`** bump و push شد. #### د) Mapping (NetworkMembershipProfile.cs) ```csharp PackageName = node.PackageName ?? string.Empty, PackageId = node.PackageId.HasValue ? node.PackageId.Value : null, IsNewActivation = node.IsNewActivation.HasValue ? node.IsNewActivation.Value : null ``` #### هـ) BackOffice — NetworkTreeViewer.razor **DataGrid — دو ستون جدید:** ```razor @if (context.Item.IsNewActivation == true) { 🆕 عضو جدید } else if (context.Item.IsNewActivation == false) { 🔄 خرید مجدد } ``` **JS (jsNodes):** ```js isNewActivation: n.IsNewActivation, packageName: n.PackageName ?? "" ``` --- ## ۴. بهبود UI نمودار درختی ### مشکل اولیه بج «🆕 جدید» با `position: absolute` از گوشه کارت بیرون می‌زد و با محتوای دیگر برخورد می‌کرد. ### فایل‌های تغییریافته #### `admin-org-chart.js` (wwwroot/js) **ساختار کارت بازنویسی شد:** ``` ┌─────────────────────────────┐ │ [Avatar] نام کاربر │ │ پکیج نقره... │ ← inline زیر اسم │ L13 چپ عضو جدید │ ← pill در meta row ├─────────────────────────────┤ │ ✓ فعال 1404/12/24 │ └─────────────────────────────┘ ``` **تغییرات:** - `activationTypeBadge` (absolute positioning) → `activationTypePill` (inline span) - `highlightBadge` (✨ floating) → حذف شد - `packageBadge` به زیر اسم کاربر منتقل شد (نه footer) - ابعاد کارت: `160×80` → `178×92` px #### `admin-org-chart.css` (wwwroot/css) ```css /* جدید: activation pill به جای badge */ .admin-node-card .activation-pill { /* inline flex */ } .admin-node-card .new-member-pill { background: #e8f5e9; color: #2e7d32; border: 1px solid #a5d6a7; } .admin-node-card .renewal-pill { background: #ede7f6; color: #5e35b1; border: 1px solid #b39ddb; } /* بهبود: پکیج روشن‌تر */ .admin-node-card .package-name-badge { background: #eceff1; color: #546e7a; border: 1px solid #b0bec5; } ``` --- ## ۵. رفع باگ Pagination فروشگاه تخفیف ### مشکل در صفحه `/discount-store/products`، دکمه «نمایش محصولات بیشتر» کار نمی‌کرد — همیشه صفحه اول برمی‌گشت. ### ریشه مشکل **فایل غایب:** `DiscountProductProfile.cs` (Mapster) وجود نداشت. **جریان mapping:** ``` GetDiscountProductsRequest (proto) ↓ request.Adapt() GetDiscountProductsQuery ``` بدون profile، auto-mapping دو مشکل داشت: 1. `request.SearchQuery (string)` → `query.SearchTerm (string?)` → نامتطابق نام، NULL می‌شد 2. `request.PageNumber (int)` → `query.PaginationQuery.PageNumber` — **Mapster نمی‌توانست به nested object مپ کند** → `PaginationQuery = null` → default: `PageNumber=1` همیشه! ### راه‌حل **فایل جدید:** `CMS/src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs` ```csharp config.NewConfig() .Map(dest => dest.SearchTerm, src => string.IsNullOrEmpty(src.SearchQuery) ? null : src.SearchQuery) .Map(dest => dest.CategoryId, src => src.CategoryId != null ? src.CategoryId.Value : (long?)null) .Map(dest => dest.IsActive, src => src.IsActive != null ? src.IsActive.Value : (bool?)null) .Map(dest => dest.PaginationQuery, src => new PaginationState { PageNumber = src.PageNumber > 0 ? src.PageNumber : 1, PageSize = src.PageSize > 0 ? src.PageSize : 12 }); ``` همچنین `GetDiscountProductsResponseDto → GetDiscountProductsResponse` هم به صورت صریح مپ شد تا `MetaData` و `Models` درست انتقال یابند. --- ## خلاصه فایل‌های تغییریافته | فایل | نوع تغییر | پروژه | |------|-----------|-------| | `ActivateClubMembershipCommandHandler.cs` | Fix — حذف Double-Charge | CMS Application | | `AcceptClubMembershipContractCommandHandler.cs` | Fix — Cross-week Pool | CMS Application | | `CreateManualPaymentCommandHandler.cs` | Fix — Cross-week Pool | CMS Application | | `sp_CalculateWeeklyCommissionPool.sql` | Fix — IsCurrentCycle | CMS Infrastructure | | `SP_GetNetworkTree.sql` | Feature — IsNewActivation, PackageName, PackageId | DB/dbbkup | | `NetworkTreeNodeDto.cs` | Feature — فیلدهای جدید | CMS Application | | `NetworkTreeDto.cs` | Feature — فیلدهای جدید | CMS Application | | `GetNetworkTreeQueryHandler.cs` | Feature — خواندن فیلدهای جدید | CMS Application | | `networkmembership.proto` | Feature — ۳ فیلد جدید در NetworkTreeNodeModel | Protobuf | | `NetworkMembershipProfile.cs` | Feature — mapping فیلدهای جدید | CMS WebApi | | `NetworkTreeViewer.razor` | Feature — DataGrid ستون‌های جدید | BackOffice | | `admin-org-chart.js` | Feature+Fix — inline pill، پکیج زیر اسم | BackOffice wwwroot | | `admin-org-chart.css` | Feature+Fix — استایل pill‌های مرتب | BackOffice wwwroot | | `DiscountProductProfile.cs` | Fix — Pagination mapping صحیح | CMS WebApi (جدید) | ### NuGet Package | پکیج | نسخه قبل | نسخه جدید | |------|----------|-----------| | `Foursat.CMSMicroservice.Protobuf` | 0.0.194 | **0.0.195** | --- ## وظایف باقی‌مانده ### ضروری — اصلاح داده‌های DB ```sql BEGIN TRANSACTION; -- هفته ۲۲: Pool Ghost (هانیه سادات عشاقی ← AcceptContract هفته اشتباه) UPDATE CMS.WeeklyCommissionPools SET TotalPoolAmount = 0, LastModified = GETUTCDATE() WHERE Id = 10056 AND WeekDefinitionId = 22; -- هفته ۲۳: Pool ناقص (محمدصادق عسلی ← Pool قبل از Fix ایجاد شده بود) UPDATE CMS.WeeklyCommissionPools SET TotalPoolAmount = 2520000, LastModified = GETUTCDATE() WHERE Id = 10054 AND WeekDefinitionId = 23; COMMIT; ``` ### بهبود آینده - [ ] `SP_GetNetworkTree.sql` به Infrastructure EmbeddedResource اضافه شود (auto-deploy) - [ ] `DiscountProductDto` در Application — اضافه کردن فیلد `Created` از DB - [ ] تست pagination فروشگاه پس از restart CMS