Add comprehensive database integrity audit report and fix critical bugs in commission pool charging flow
- Introduced a detailed audit report for the CMS database integrity, highlighting issues related to data entry, code bugs, and stored procedures. - Fixed double-charge issue in the commission pool during club membership activation. - Updated stored procedures to ensure correct pool calculations across different weeks. - Enhanced the network tree feature to include activation type and package details. - Improved UI for the network tree display and resolved pagination issues in the discount store.
This commit is contained in:
@@ -0,0 +1,356 @@
|
|||||||
|
# گزارش ممیزی صحت دادههای دیتابیس CMS
|
||||||
|
|
||||||
|
**تاریخ بررسی:** 1405/01/28 (2026-04-17)
|
||||||
|
**فایل بکاپ:** `dbbkup/CMS-20260417.sql` (4.9MB, 21,397 خط)
|
||||||
|
**تعداد جداول:** ~47 جدول | **تعداد کاربران:** 115 | **تعداد سفارشات:** 72
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## فهرست مطالب
|
||||||
|
|
||||||
|
1. [خلاصه اجرایی](#خلاصه-اجرایی)
|
||||||
|
2. [اصلاحیه مهم — PaymentStatus](#اصلاحیه-مهم)
|
||||||
|
3. [دسته ۱ — ورود دستی / مهاجرت دیتا](#دسته-۱--ورود-دستی--مهاجرت-دیتا)
|
||||||
|
4. [دسته ۲ — باگهای کد](#دسته-۲--باگهای-کد)
|
||||||
|
5. [دسته ۳ — وضعیت Stored Procedureها](#دسته-۳--وضعیت-stored-procedureها)
|
||||||
|
6. [آمار کلی جداول](#آمار-کلی-جداول)
|
||||||
|
7. [خلاصه مالی](#خلاصه-مالی)
|
||||||
|
8. [اقدامات پیشنهادی](#اقدامات-پیشنهادی)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## خلاصه اجرایی
|
||||||
|
|
||||||
|
بکاپ دیتابیس CMS در تاریخ 17 آوریل 2026 تحلیل شد. تحلیل شامل بررسی صحت دادهها، ارجاعات خارجی (FK)، زنجیره مالی، و تطبیق با کد سورس C# و Stored Procedureها بود.
|
||||||
|
|
||||||
|
**وضعیت کلی:** سیستم در حال مهاجرت از ساختار استاتیک به پکیجمحور بوده. بخش عمده مشکلات ناشی از ورود دستی داده و مهاجرت سیستم دایا است. چند باگ کد نیز در SP کمیسیون و Worker دایا شناسایی شد.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## اصلاحیه مهم
|
||||||
|
|
||||||
|
> **`PaymentStatus=0` در enum کد یعنی `Success` نه `Pending`!**
|
||||||
|
>
|
||||||
|
> ```csharp
|
||||||
|
> // PaymentStatus.cs
|
||||||
|
> Success = 0,
|
||||||
|
> Reject = 1,
|
||||||
|
> Pending = 2
|
||||||
|
> ```
|
||||||
|
>
|
||||||
|
> بنابراین تمام 72 سفارش واقعاً **موفق** هستند. این مشکل نیست.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## دسته ۱ — ورود دستی / مهاجرت دیتا
|
||||||
|
|
||||||
|
### 1A) موجودی 56M بدون فلگ `HasReceivedDayaCredit`
|
||||||
|
|
||||||
|
**شدت:** 🟠 متوسط
|
||||||
|
**علت:** ورود دستی / مهاجرت
|
||||||
|
|
||||||
|
- **23 کاربر** دقیقاً 56,000,000 ریال در `Balance` دارند ولی `HasReceivedDayaCredit = 0`
|
||||||
|
- فقط **16 کاربر** از طریق Daya Worker صحیح اعتبار گرفتند (`HasReceivedDayaCredit = 1`)
|
||||||
|
- بقیه احتمالاً دستی شارژ شدند بدون ثبت تاریخچه
|
||||||
|
|
||||||
|
**کاربران آسیبپذیر:**
|
||||||
|
|
||||||
|
| UserId | نام | Balance | HasReceivedDayaCredit |
|
||||||
|
|--------|-----|---------|----------------------|
|
||||||
|
| 51 | مرتضی اینالو | 56,000,000 | 0 |
|
||||||
|
| 58 | وحید حقگو | 56,000,000 | 0 |
|
||||||
|
| 87 | امیررضا محمدی | 56,000,000 | 0 |
|
||||||
|
| 43 | کریم خادمی | 56,000,000 | 0 |
|
||||||
|
| 52 | حمیدرضا اسمعیلی | 56,000,000 | 0 |
|
||||||
|
| 88 | کریم رعیتپیشه | 56,000,000 | 0 |
|
||||||
|
| 91 | رحیم رعیتپیشه | 56,000,000 | 0 |
|
||||||
|
| 93 | ریحانه سادات هاشمینصر | 56,000,000 | 0 |
|
||||||
|
| 99 | هستی خادمی | 56,000,000 | 0 |
|
||||||
|
| 110 | علی وفائی | 56,000,000 | 0 |
|
||||||
|
| 113 | کاوس بیگاینالو | 56,000,000 | 0 |
|
||||||
|
| 119 | علیرضا کریمیپیروز | 56,000,000 | 0 |
|
||||||
|
| 122 | ابوالقاسم عابدی | 56,000,000 | 0 |
|
||||||
|
| 123 | ناصر کریمیپیروز | 56,000,000 | 0 |
|
||||||
|
| 124 | محمدرضا باغجری | 56,000,000 | 0 |
|
||||||
|
| 126 | امیرعباس میرزایی | 56,000,000 | 0 |
|
||||||
|
| 138 | سیما اکبرزاده | 56,000,000 | 0 |
|
||||||
|
| 139 | مسعود توسلیان | 56,000,000 | 0 |
|
||||||
|
| 142 | صغری شبانکاره | 56,000,000 | 0 |
|
||||||
|
| 170 | لیلا خدارحمی | 56,000,000 | 0 |
|
||||||
|
| 172 | مهرافشان زاهدنیا | 56,000,000 | 0 |
|
||||||
|
| 175 | ناهید حسنزاده | 56,000,000 | 0 |
|
||||||
|
| 176 | مهریدخت میکانیکی | 56,000,000 | 0 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1B) کد ملی تکراری — اکانتهای تستی
|
||||||
|
|
||||||
|
**شدت:** 🟡 پایین
|
||||||
|
**علت:** ورود دستی / تست
|
||||||
|
|
||||||
|
| کد ملی | تعداد اکانت | UserIdها | نام |
|
||||||
|
|--------|------------|----------|-----|
|
||||||
|
| مشترک #1 | 6 | 9, 11, 12, 13, 40, 41 | مهدی مرجانی |
|
||||||
|
| مشترک #2 | 3 | 7, 8, 10 | مهدی صیفی |
|
||||||
|
| مشترک #3 | 2 | 42, 43 | کریم خادمی |
|
||||||
|
| مشترک #4 | 2 | 50, 51 | مرتضی اینالو |
|
||||||
|
| مشترک #5 | 2 | 120, 190 | عسلی |
|
||||||
|
|
||||||
|
- شماره موبایل تکراری: `09038888074` بین کاربران 120 و 190
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1C) `NetworkInfos` خالی — مهاجرت صحیح انجام شده
|
||||||
|
|
||||||
|
**شدت:** ✅ مشکل نیست
|
||||||
|
|
||||||
|
جدول `NetworkInfos` خالی است چون دادههای شبکه به فیلدهای مستقیم `Users` مهاجرت شدند:
|
||||||
|
- `Users.NetworkParentId` ← FK به والد شبکه
|
||||||
|
- `Users.LegPosition` ← Left(0) / Right(1)
|
||||||
|
|
||||||
|
مهاجرت در `20250601_MigrateParentIdToNetworkParentId.sql` انجام شده. entity `NetworkInfo` در C# وجود ندارد. SPها هم از `Users.NetworkParentId` استفاده میکنند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1D) `PasswordHash = NULL` برای تمام 115 کاربر
|
||||||
|
|
||||||
|
**شدت:** 🟡 نیاز به بررسی
|
||||||
|
**علت:** احتمالاً بکاپ شامل فیلد پسورد نشده، یا سیستم OTP/موبایل استفاده میکند
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1E) دورههای عضویت باشگاه — `PaidAmount = 0`
|
||||||
|
|
||||||
|
**شدت:** 🟡 نیاز به بررسی
|
||||||
|
|
||||||
|
- 87 دوره `ClubMembershipCycles` همه `PaidAmount = 0`
|
||||||
|
- ممکن است عضویت باشگاه خودکار با خرید پکیج فعال شود (نه پرداخت جداگانه)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## دسته ۲ — باگهای کد
|
||||||
|
|
||||||
|
### 2A) تراکنشهای تکراری دایا — Race Condition در `CheckAndProcessDayaLoansCommandHandler`
|
||||||
|
|
||||||
|
**شدت:** 🔴 بحرانی
|
||||||
|
**فایل:** `CMSMicroservice.Application/DayaLoanCQ/Commands/CheckAndProcessDayaLoans/CheckAndProcessDayaLoansCommandHandler.cs`
|
||||||
|
|
||||||
|
**یافتهها:**
|
||||||
|
- **109 رکورد `DayaLoanContracts`** ولی فقط **16 کاربر** `HasReceivedDayaCredit=1`
|
||||||
|
- **64 تراکنش** با «دریافت اعتبار دایا» ساخته شده ولی فقط **11 رکورد `UserPackagePurchases`**
|
||||||
|
- تراکنشها با `RefId` منحصربهفرد ساخته شدند (مثل `C4_T8579002`) — همه در `2025-11-19 01:24:24` ایجاد شدند
|
||||||
|
|
||||||
|
**تحلیل ریشهای:**
|
||||||
|
- Daya Worker (Hangfire هر 15 دقیقه) احتمالاً برای بعضی کاربران **چند بار** اجرا شده
|
||||||
|
- `CreateTransaction` و `DayaLoanContract` ساخته شده ولی `HasReceivedDayaCredit=true` ست نشده (exception بعد از SaveChanges اول ولی قبل از SaveChanges دوم)
|
||||||
|
- یا: چون همه در یک لحظه ساخته شدند (`2025-11-19 01:24:24`)، ممکن است **یک بار bulk import دستی** بوده
|
||||||
|
|
||||||
|
**ریسک:** کاربرانی که `HasReceivedDayaCredit=0` دارند ممکن است **دوباره** از Worker اعتبار بگیرند.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2B) SP `sp_CalculateWeeklyCommissionPool` — `DistributedAmount` آپدیت نمیشود
|
||||||
|
|
||||||
|
**شدت:** 🔴 بحرانی
|
||||||
|
**فایل:** `dbbkup/CMS-20260417.sql` خط ~18830
|
||||||
|
|
||||||
|
در Step 10 (آپدیت نهایی Pool):
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- کد فعلی (باگدار):
|
||||||
|
UPDATE CMS.WeeklyCommissionPools
|
||||||
|
SET
|
||||||
|
IsCalculated = 1,
|
||||||
|
CalculatedAt = @CalculatedAt,
|
||||||
|
TotalBalances = @TotalBalances,
|
||||||
|
ValuePerBalance = @ValuePerBalance,
|
||||||
|
LastModified = @CalculatedAt,
|
||||||
|
LastModifiedBy = 'SP'
|
||||||
|
WHERE Id = @PoolId;
|
||||||
|
```
|
||||||
|
|
||||||
|
**مشکل:** فیلد `DistributedAmount` **هرگز مقداردهی نمیشود** و 0 باقی میماند.
|
||||||
|
|
||||||
|
**نتیجه در دیتا:**
|
||||||
|
- 15 استخر، مجموع `TotalPoolAmount = 2,016,000,000` ریال
|
||||||
|
- همه `DistributedAmount = 0`
|
||||||
|
- ولی 46 پرداخت واقعاً ثبت و به `NetworkBalance` اضافه شدند
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2C) `UserWalletChangeLogs` خالی
|
||||||
|
|
||||||
|
**شدت:** 🟠 متوسط
|
||||||
|
**فایل:** SP Step 9 + `CalculateWeeklyCommissionPoolCommandHandler.cs`
|
||||||
|
|
||||||
|
- SP باید در Step 9 لاگ تغییرات کیفپول را در `UserWalletChangeLogs` ذخیره کند
|
||||||
|
- جدول **صفر رکورد** دارد
|
||||||
|
- **احتمال 1:** SP هرگز Step 9 را درست اجرا نکرده
|
||||||
|
- **احتمال 2:** ORM Strategy (نه SP) استفاده شده و آن `UserWalletChangeLogs` نمینویسد
|
||||||
|
- **احتمال 3:** لاگها در حین ForceRecalculate حذف شدند
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2D) `WalletHistory.ChangeType = NULL` در تمام 239 رکورد
|
||||||
|
|
||||||
|
**شدت:** 🟠 متوسط
|
||||||
|
**فایل:** `UserOrderService.cs` و `PackageService.cs` — هرجا `UserWalletHistory` ساخته میشود
|
||||||
|
|
||||||
|
- فیلد `ChangeType` هرگز ست نمیشود
|
||||||
|
- کد از `IsIncrease` (bool) برای تفکیک واریز/برداشت استفاده میکند
|
||||||
|
- `ChangeType` احتمالاً فیلد قدیمی deprecated شده
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2E) `NetworkWeeklyBalances.WeeklyCommissionPoolId = NULL` (805 رکورد)
|
||||||
|
|
||||||
|
**شدت:** 🟡 پایین
|
||||||
|
|
||||||
|
- SP مقدار `WeeklyPoolContribution = 0` ثبت میکند و PoolId ست نمیشود
|
||||||
|
- ارتباط بین `NetworkWeeklyBalances` و `WeeklyCommissionPools` از طریق `WeekDefinitionId` برقرار است، نه FK مستقیم
|
||||||
|
- **عملاً مشکل عملکردی ایجاد نمیکند** ولی tracking سختتر میشود
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2F) 30 سفارش کیفپولی — بررسی WalletHistory
|
||||||
|
|
||||||
|
**شدت:** 🟠 نیاز به تأیید
|
||||||
|
|
||||||
|
- 30 سفارش با `PaymentMethod=1` (Wallet) ثبت شدند
|
||||||
|
- اولین بررسی نشان داد «هیچ برداشتی ثبت نشده» — **اما** این بررسی بر اساس `ChangeType` بود که همه NULL هستند
|
||||||
|
- **باید بر اساس `IsIncrease=0` (false = decrease)** دوباره بررسی شود
|
||||||
|
- `SubmitShopBuyOrder()` در کد `UserWalletHistory` میسازد — احتمالاً رکوردها وجود دارند ولی `ChangeType` NULL است
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## دسته ۳ — وضعیت Stored Procedureها
|
||||||
|
|
||||||
|
### `GetNetworkTree`
|
||||||
|
|
||||||
|
| آیتم | وضعیت |
|
||||||
|
|------|--------|
|
||||||
|
| از `Users.NetworkParentId` استفاده میکند | ✅ صحیح (بعد از مهاجرت) |
|
||||||
|
| JOIN با `ClubMemberships` | ✅ صحیح |
|
||||||
|
| `MAXRECURSION 0` | ✅ صحیح |
|
||||||
|
| فیلتر `IsDeleted = 0` | ✅ صحیح |
|
||||||
|
|
||||||
|
### `sp_CalculateWeeklyBalances`
|
||||||
|
|
||||||
|
| آیتم | وضعیت |
|
||||||
|
|------|--------|
|
||||||
|
| پارامتر `@PackageId` از `Packages.IsBasePackage` | ✅ صحیح |
|
||||||
|
| `MaxBalancesPerLeg` و `MaxNetworkLevel` از Package | ✅ صحیح |
|
||||||
|
| Carryover از هفته قبل | ✅ صحیح |
|
||||||
|
| CTE recursive برای چپ/راست | ✅ صحیح |
|
||||||
|
| `TotalBalances = MIN(left, right)` | ✅ صحیح |
|
||||||
|
| `SubordinateBalances` محاسبه | ✅ صحیح |
|
||||||
|
| 805 رکورد تولید شده | ✅ کار میکند |
|
||||||
|
|
||||||
|
### `sp_CalculateWeeklyCommissionPool`
|
||||||
|
|
||||||
|
| آیتم | وضعیت |
|
||||||
|
|------|--------|
|
||||||
|
| `ValuePerBalance = TotalPoolAmount / TotalBalances` | ✅ صحیح |
|
||||||
|
| ایجاد `UserCommissionPayouts` | ✅ صحیح (46 رکورد) |
|
||||||
|
| ثبت `CommissionPayoutHistories` | ✅ صحیح |
|
||||||
|
| شارژ `NetworkBalance` کیفپول | ✅ صحیح |
|
||||||
|
| آپدیت `DistributedAmount` در Pool | ❌ **انجام نمیشود** |
|
||||||
|
| ثبت `UserWalletChangeLogs` | ⚠️ نامشخص |
|
||||||
|
| ForceRecalculate — Revert | ✅ منطق صحیح |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## آمار کلی جداول
|
||||||
|
|
||||||
|
| جدول | تعداد | وضعیت |
|
||||||
|
|------|--------|--------|
|
||||||
|
| Users | 115 | |
|
||||||
|
| UserWallets | 115 | |
|
||||||
|
| UserWalletHistories | 239 | ChangeType همه NULL |
|
||||||
|
| UserWalletChangeLogs | 0 | ⚠️ خالی |
|
||||||
|
| UserOrders | 72 | همه PaymentStatus=0 (Success) |
|
||||||
|
| FactorDetails | 177 | |
|
||||||
|
| Transactions | 175 | 64 تراکنش دایا |
|
||||||
|
| PaymentTransactions | 22 | فقط DiscountOrders + شارژ |
|
||||||
|
| Products | 163 | |
|
||||||
|
| Categories | 14 | |
|
||||||
|
| InventoryItems | 175 | |
|
||||||
|
| StockMovements | 242 | 11 chain issue |
|
||||||
|
| ClubMemberships | 88 | همه IsActive=1 |
|
||||||
|
| ClubMembershipCycles | 87 | همه PaidAmount=0 |
|
||||||
|
| UserClubFeatures | 249 | |
|
||||||
|
| NetworkInfos | 0 | ✅ deprecated — مهاجرت شده |
|
||||||
|
| NetworkWeeklyBalances | 805 | PoolId همه NULL |
|
||||||
|
| WeeklyCommissionPools | 15 | DistributedAmount همه 0 |
|
||||||
|
| UserCommissionPayouts | 46 | Status=3, مبالغ کلان |
|
||||||
|
| WeekDefinitions | 59 | |
|
||||||
|
| Packages | 2 | Base=56M, Secondary=5.6M |
|
||||||
|
| DayaLoanContracts | 109 | |
|
||||||
|
| UserPackagePurchases | 11 | |
|
||||||
|
| DiscountOrders | 13 | |
|
||||||
|
| DiscountOrderDetails | 14 | |
|
||||||
|
| DiscountCategories | 8 | |
|
||||||
|
| OrderVATs | 44 | ✅ محاسبات صحیح |
|
||||||
|
| UserAddresses | 127 | |
|
||||||
|
| Roles | 3 | user, admin, Administrator |
|
||||||
|
| UserRoles | 119 | 115 user + 2 admin + 2 Administrator |
|
||||||
|
| ProductImages | 4 | |
|
||||||
|
| ShippingMethods | 0 | ⚠️ خالی |
|
||||||
|
| SitePages | 0 | ⚠️ خالی |
|
||||||
|
| SystemConfigurations | 0 | ⚠️ خالی |
|
||||||
|
| Coupons | 0 | ⚠️ خالی |
|
||||||
|
| ProductProperties | 0 | ⚠️ خالی |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## خلاصه مالی
|
||||||
|
|
||||||
|
### موجودیهای کل سیستم
|
||||||
|
|
||||||
|
| فیلد | مبلغ (ریال) |
|
||||||
|
|------|-------------|
|
||||||
|
| مجموع `Balance` کل کیفپولها | 2,548,684,394 |
|
||||||
|
| مجموع `NetworkBalance` (کمیسیون) | 453,599,993 |
|
||||||
|
| مجموع `DiscountBalance` (تخفیف) | 7,326,400,000 |
|
||||||
|
| مجموع سفارشات (UserOrders) | 1,039,410,812 |
|
||||||
|
| مجموع استخر کمیسیون (WeeklyPools) | 2,016,000,000 |
|
||||||
|
|
||||||
|
### پکیجها
|
||||||
|
|
||||||
|
| Package | قیمت | IsBase | MaxBalancesPerLeg | MaxNetworkLevel | DiscountMultiplier |
|
||||||
|
|---------|-------|--------|-------------------|-----------------|-------------------|
|
||||||
|
| Package 1 | 56,000,000 | ✅ | 300 | 1,000,000 | 2.0 |
|
||||||
|
| Package 4 | 5,600,000 | ❌ | 30 | 1,000,000 | 2.0 |
|
||||||
|
|
||||||
|
### ارجاعات شکسته (FK)
|
||||||
|
|
||||||
|
| ارجاع | تعداد |
|
||||||
|
|-------|--------|
|
||||||
|
| FactorDetails → OrderId ناموجود | 2 (DetailId=22,23 → OrderId=21) |
|
||||||
|
| InventoryItems ≠ آخرین StockMovement | 3 |
|
||||||
|
| StockMovement chain breaks | 11 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## اقدامات پیشنهادی
|
||||||
|
|
||||||
|
### اولویت بالا (انجام ندهید تا بررسی بیشتر)
|
||||||
|
|
||||||
|
1. **فیکس SP `sp_CalculateWeeklyCommissionPool`:** اضافه کردن `DistributedAmount` به UPDATE نهایی
|
||||||
|
2. **بررسی Daya Worker:** race condition در `CheckAndProcessDayaLoansCommandHandler` — ممکن است تراکنش تکراری بسازد
|
||||||
|
3. **23 کاربر با 56M بدون فلگ دایا:** تعیین اینکه آیا دستی شارژ شدند یا از Worker — سپس اصلاح `HasReceivedDayaCredit`
|
||||||
|
|
||||||
|
### اولویت متوسط
|
||||||
|
|
||||||
|
4. **WalletHistory ChangeType:** تأیید اینکه deprecated شده و `IsIncrease` جایگزین است
|
||||||
|
5. **UserWalletChangeLogs خالی:** بررسی اینکه ORM Strategy استفاده شده یا SP Strategy
|
||||||
|
6. **اکانتهای تکراری:** تصمیمگیری درباره 13 اکانت تکراری (حذف/ادغام)
|
||||||
|
|
||||||
|
### اولویت پایین
|
||||||
|
|
||||||
|
7. **جداول خالی:** SystemConfigurations, ShippingMethods, SitePages — آیا باید از seed پر شوند؟
|
||||||
|
8. **StockMovement chain issues:** 11 ناسازگاری — آیا از ورود دستی موجودی بوده؟
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*این گزارش فقط مستندات یافتهها است. هیچ تغییری در کد یا دیتابیس اعمال نشده است.*
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
# TECH-06 — جریان شارژ Pool کمیسیون هفتگی
|
||||||
|
|
||||||
|
> تاریخ: ۱۴۰۵/۰۲/۱۰
|
||||||
|
> وضعیت: **باگ شناساییشده — منتظر Fix**
|
||||||
|
> مرتبط با: `ActivateClubMembershipCommandHandler.cs` · `AcceptClubMembershipContractCommandHandler.cs` · `CreateManualPaymentCommandHandler.cs`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۱. مسیر مشتری جدید (اولین خرید پکیج)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor U as کاربر (FrontOffice)
|
||||||
|
participant CB as PaymentCallback.razor
|
||||||
|
participant PI as Profile/Index.razor
|
||||||
|
participant CD as ClubMembershipContractDialog
|
||||||
|
participant CMS as CMS (gRPC)
|
||||||
|
|
||||||
|
U->>CB: بازگشت از درگاه<br/>?type=package&orderId=X&Authority=Y
|
||||||
|
CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority)
|
||||||
|
|
||||||
|
Note over CMS: PackageService.VerifyPackagePurchase()
|
||||||
|
CMS->>CMS: تأیید با درگاه ✓
|
||||||
|
CMS->>CMS: ActivateClubMembership(ForceActivation=false)
|
||||||
|
|
||||||
|
Note over CMS: isNewMembership = true
|
||||||
|
CMS->>CMS: ClubMembership(IsActive=false) ایجاد
|
||||||
|
CMS->>CMS: ClubMembershipCycle #1 ایجاد
|
||||||
|
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌
|
||||||
|
|
||||||
|
CMS-->>CB: Success=true
|
||||||
|
CB->>CB: RefreshToken<br/>HasPurchasedPackage=true<br/>IsClubMemberActive=false
|
||||||
|
|
||||||
|
U->>PI: کلیک "بازگشت به پروفایل"
|
||||||
|
PI->>PI: OnAfterRenderAsync<br/>CheckAndShowClubContractModal()
|
||||||
|
|
||||||
|
Note over PI: HasPurchasedPackage=true<br/>AND IsClubMemberActive=false → نمایش مودال
|
||||||
|
|
||||||
|
PI->>CD: DialogService.ShowAsync (غیرقابل بستن)
|
||||||
|
U->>CD: مطالعه قرارداد + درخواست OTP
|
||||||
|
CD->>CMS: CreateNewOtpToken(purpose=signClubContract)
|
||||||
|
CMS-->>CD: OTP ارسال شد
|
||||||
|
U->>CD: وارد کردن OTP ۶ رقمی
|
||||||
|
CD->>CMS: AcceptClubMembershipContract(otp, signGuid)
|
||||||
|
|
||||||
|
Note over CMS: AcceptClubMembershipContractCommandHandler
|
||||||
|
CMS->>CMS: IsActive == false → guard رد میشه ✓
|
||||||
|
CMS->>CMS: IsActive = true
|
||||||
|
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ دوم ❌
|
||||||
|
|
||||||
|
CMS-->>CD: Success=true
|
||||||
|
CD->>PI: dialog.Close(Ok)
|
||||||
|
PI->>PI: LoadUserAuthInfo → IsClubMemberActive=true
|
||||||
|
```
|
||||||
|
|
||||||
|
> **نتیجه**: Pool برای عضو جدید **۲ برابر** شارژ میشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۲. مسیر خرید مجدد (بعد از تکمیل چرخه Magic)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor U as کاربر (FrontOffice)
|
||||||
|
participant CB as PaymentCallback.razor
|
||||||
|
participant PI as Profile/Index.razor
|
||||||
|
participant CMS as CMS (gRPC)
|
||||||
|
|
||||||
|
Note over CMS: وضعیت: IsActive=true<br/>IsCurrentCycle=true (باگ B6 — ریست نشده)
|
||||||
|
|
||||||
|
U->>CB: بازگشت از درگاه (خرید مجدد)
|
||||||
|
CB->>CMS: CustomerVerifyPackagePurchase(orderId, authority)
|
||||||
|
|
||||||
|
CMS->>CMS: ActivateClubMembership(ForceActivation=false)
|
||||||
|
Note over CMS: isNewMembership = false<br/>existingMembership.IsActive=true<br/>hasCurrentCycle=true
|
||||||
|
|
||||||
|
CMS->>CMS: return true زودهنگام ❌
|
||||||
|
|
||||||
|
Note over CMS: Cycle جدید ساخته نمیشه ❌<br/>Pool شارژ نمیشه ❌
|
||||||
|
|
||||||
|
CMS-->>CB: Success=true
|
||||||
|
CB->>CB: RefreshToken → IsClubMemberActive=true
|
||||||
|
|
||||||
|
U->>PI: بازگشت به پروفایل
|
||||||
|
PI->>PI: IsClubMemberActive=true<br/>→ مودال نمایش داده نمیشه ✓
|
||||||
|
|
||||||
|
Note over PI,CMS: Pool هرگز شارژ نشد ❌<br/>Cycle جدید وجود ندارد ❌
|
||||||
|
```
|
||||||
|
|
||||||
|
> **نتیجه**: Pool برای خرید مجدد **هرگز** شارژ نمیشود. ریشه مشکل: باگ B6 — `IsCurrentCycle` هنگام خروج از Magic ریست نمیشود.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۳. مسیر ادمین (BackOffice — فعالسازی دستی)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor A as ادمین (BackOffice)
|
||||||
|
participant DL as ActivateClubDialog.razor
|
||||||
|
participant CMS as CMS (gRPC)
|
||||||
|
|
||||||
|
A->>DL: باز کردن دیالوگ فعالسازی برای کاربر X
|
||||||
|
DL->>DL: انتخاب UserId و PackageId
|
||||||
|
A->>DL: کلیک "تایید و فعالسازی"
|
||||||
|
|
||||||
|
DL->>CMS: ActivateClubMembership(UserId=X, ForceActivation=true)
|
||||||
|
Note over CMS: ActivateClubMembershipCommandHandler<br/>skip همه validationهای مالی
|
||||||
|
|
||||||
|
alt کاربر جدید (isNewMembership=true)
|
||||||
|
CMS->>CMS: ClubMembership(IsActive=false) ایجاد
|
||||||
|
CMS->>CMS: Cycle #1 ایجاد
|
||||||
|
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ اول ❌
|
||||||
|
Note over CMS: کاربر هنوز عضو فعال نیست!<br/>IsActive=false
|
||||||
|
|
||||||
|
Note over A,CMS: کاربر باید به FO رود و قرارداد امضا کند
|
||||||
|
Note over A,CMS: AcceptContract → Pool += fee ← شارژ دوم ❌
|
||||||
|
else خرید مجدد (isNewMembership=false، IsCurrentCycle ریست شده)
|
||||||
|
CMS->>CMS: IsActive=true, hasCurrentCycle=false → ادامه میدهد
|
||||||
|
CMS->>CMS: Cycle جدید ایجاد
|
||||||
|
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ یک بار ✅
|
||||||
|
end
|
||||||
|
|
||||||
|
CMS-->>DL: Empty (success)
|
||||||
|
DL->>A: "عضویت با موفقیت فعال شد"
|
||||||
|
Note over A,CMS: AcceptContract از BO هرگز فراخوانی نمیشود
|
||||||
|
```
|
||||||
|
|
||||||
|
> **نتیجه**: ادمین برای کاربر جدید نیز باعث double-charge میشود (چون کاربر بعداً از FO قرارداد امضا میکند). برای خرید مجدد رفتار درست است.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۵. خلاصه باگها
|
||||||
|
|
||||||
|
| سناریو | Pool شارژ واقعی | Pool شارژ انتظاری | Cycle ساخته میشود | وضعیت |
|
||||||
|
|--------|----------------|-------------------|--------------------|--------|
|
||||||
|
| مشتری جدید (IPG) | **2×fee** | 1×fee | ✅ بله | ❌ Double-charge |
|
||||||
|
| خرید مجدد مشتری | **0×fee** | 1×fee | ❌ خیر (B6) | ❌ هرگز شارژ نمیشود |
|
||||||
|
| ادمین — ForceActivate کاربر جدید | **2×fee** | 1×fee | ✅ بله | ❌ Double-charge |
|
||||||
|
| ادمین — ForceActivate خرید مجدد | **1×fee** | 1×fee | ✅ بله | ✅ درست |
|
||||||
|
| ادمین — ManualPayment (پرداخت دستی) | **1×fee** | 1×fee | ❌ خیر | ⚠️ Pool درست، ولی والدین امتیاز نمیگیرند |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۵. ریشه مشکلات
|
||||||
|
|
||||||
|
### باگ A — Double-charge در عضو جدید
|
||||||
|
**فایل**: `ActivateClubMembershipCommandHandler.cs` — بخش Pool (خط ~۳۱۱)
|
||||||
|
**علت**: هنگامی که `isNewMembership=true`، Pool شارژ میشود؛ بعداً `AcceptContract` هم Pool را شارژ میکند.
|
||||||
|
**Fix**: شارژ Pool در `ActivateClubMembership` را فقط برای `!isNewMembership` انجام بده:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ⭐ 8. اضافه کردن مبلغ به Pool هفته جاری
|
||||||
|
// عضو جدید: Pool توسط AcceptClubMembershipContract شارژ میشه (هنگام امضای قرارداد)
|
||||||
|
// خرید مجدد: قرارداد مجدد امضا نمیشه — Pool همینجا شارژ میشه
|
||||||
|
if (!isNewMembership)
|
||||||
|
{
|
||||||
|
// ... کد موجود شارژ Pool ...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### باگ B6 — خرید مجدد کار نمیکند
|
||||||
|
**فایل**: `UserOrderService.cs` — بخش خروج از Magic
|
||||||
|
**علت**: هنگام خروج از Magic، `cycle.IsCurrentCycle` به `false` ریست نمیشود → `ActivateClubMembership` با `hasCurrentCycle=true` زودهنگام برمیگردد.
|
||||||
|
**Fix**: در `ExitMagicMode`:
|
||||||
|
```csharp
|
||||||
|
cycle.IsCurrentCycle = false; // ← اضافه شود
|
||||||
|
```
|
||||||
|
|
||||||
|
### باگ C — پرداخت دستی: Cycle هرگز ساخته نمیشود
|
||||||
|
**فایل**: `CreateManualPaymentCommandHandler.cs`
|
||||||
|
**علت**: پرداخت دستی `ActivateClubMembership` را صدا نمیزند → هیچ `ClubMembershipCycle` ساخته نمیشود → SP این کاربر را به عنوان "عضو جدید" برای والدینش حساب نمیکند.
|
||||||
|
**تأثیر**: Pool یکبار شارژ میشود (توسط AcceptContract ✓) ولی balance والدین در sp_CalculateWeeklyBalances افزایش نمییابد (چون Cycle ندارد ❌).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۴. مسیر پرداخت دستی (BackOffice — ManualPayment)
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
actor A as ادمین (BackOffice)
|
||||||
|
participant DL as ManualPaymentDialog.razor
|
||||||
|
participant CMS as CMS (gRPC)
|
||||||
|
actor U as کاربر (FrontOffice)
|
||||||
|
participant PI as Profile/Index.razor
|
||||||
|
participant CD as ClubMembershipContractDialog
|
||||||
|
|
||||||
|
A->>DL: باز کردن دیالوگ پرداخت دستی
|
||||||
|
DL->>DL: انتخاب کاربر + پکیج + نوع پرداخت + تصویر رسید
|
||||||
|
A->>DL: کلیک "ثبت پرداخت"
|
||||||
|
|
||||||
|
DL->>CMS: CreateManualPayment(userId, packageId, type, referenceNumber)
|
||||||
|
Note over CMS: CreateManualPaymentCommandHandler
|
||||||
|
|
||||||
|
CMS->>CMS: Transaction(DepositExternal1) ایجاد
|
||||||
|
CMS->>CMS: ManualPayment(Status=Approved) ایجاد ← بدون نیاز به تایید دو مرحله
|
||||||
|
CMS->>CMS: wallet.Balance += package.Price
|
||||||
|
CMS->>CMS: wallet.DiscountBalance += package.Price × DiscountMultiplier
|
||||||
|
CMS->>CMS: user.PackagePurchaseMethod = DirectPurchase
|
||||||
|
|
||||||
|
Note over CMS: ❌ ActivateClubMembership صدا زده نمیشود<br/>❌ ClubMembershipCycle ساخته نمیشود<br/>❌ Pool شارژ نمیشود
|
||||||
|
|
||||||
|
CMS-->>DL: ManualPaymentId
|
||||||
|
DL->>A: "پرداخت دستی با موفقیت ثبت شد"
|
||||||
|
|
||||||
|
Note over A,U: کاربر باید به FO مراجعه کند
|
||||||
|
U->>PI: ورود به پروفایل
|
||||||
|
PI->>PI: OnAfterRenderAsync → CheckAndShowClubContractModal()
|
||||||
|
Note over PI: HasPurchasedPackage=true (PackagePurchaseMethod=DirectPurchase)<br/>IsClubMemberActive=false → نمایش مودال
|
||||||
|
|
||||||
|
PI->>CD: DialogService.ShowAsync (غیرقابل بستن)
|
||||||
|
U->>CD: امضای قرارداد + OTP
|
||||||
|
CD->>CMS: AcceptClubMembershipContract(otp, signGuid)
|
||||||
|
|
||||||
|
Note over CMS: AcceptClubMembershipContractCommandHandler
|
||||||
|
CMS->>CMS: user.ClubMembership == null → isNewMembership = true
|
||||||
|
CMS->>CMS: ClubMembership(IsActive=true) ایجاد
|
||||||
|
CMS->>CMS: ⚡ Pool += ActivationFee ← شارژ یکبار ✅
|
||||||
|
|
||||||
|
Note over CMS: ❌ ClubMembershipCycle هرگز ساخته نمیشود<br/>(AcceptContract از Cycle خبری ندارد)
|
||||||
|
|
||||||
|
CMS-->>CD: Success=true
|
||||||
|
CD->>PI: dialog.Close(Ok)
|
||||||
|
```
|
||||||
|
|
||||||
|
> **نتیجه**:
|
||||||
|
> - Pool: **1×** شارژ میشود ✅ (درست)
|
||||||
|
> - `ClubMembershipCycle`: **هرگز ساخته نمیشود** ❌
|
||||||
|
> - در `sp_CalculateWeeklyBalances`: کاربر `IsActive=true` دارد → خودش میتواند کمیسیون دریافت کند ✅
|
||||||
|
> - ولی والدین این کاربر **هیچ "عضو جدید" برای این هفته دریافت نمیکنند** ❌ (چون SP از `ClubMembershipCycles.PackagePurchasedAt` میخواند)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## ۶. validation داشبورد کمیسیون
|
||||||
|
|
||||||
|
پس از رفع باگ A، validation باید از **فقط یک منبع** استفاده کند:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// درست: فقط ClubMembershipCycles.PackagePurchasedAt
|
||||||
|
// این جدول برای هر خرید (چه جدید چه مجدد) یک رکورد دارد
|
||||||
|
var activations = await _context.ClubMembershipCycles
|
||||||
|
.CountAsync(c => c.PackageId == packageId
|
||||||
|
&& c.PackagePurchasedAt >= weekDef.StartDate
|
||||||
|
&& c.PackagePurchasedAt < weekDef.EndDate);
|
||||||
|
```
|
||||||
|
|
||||||
|
> قبل از رفع باگ A، validation فعلی (firstActivations + cycleActivations) تصادفاً با double-charge جبران میشد.
|
||||||
@@ -0,0 +1,334 @@
|
|||||||
|
# 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
|
||||||
|
<!-- ستون نوع فعالسازی -->
|
||||||
|
<PropertyColumn Property="x => x.IsNewActivation" Title="نوع فعالسازی">
|
||||||
|
@if (context.Item.IsNewActivation == true)
|
||||||
|
{
|
||||||
|
<MudChip Color="Color.Success">🆕 عضو جدید</MudChip>
|
||||||
|
}
|
||||||
|
else if (context.Item.IsNewActivation == false)
|
||||||
|
{
|
||||||
|
<MudChip Color="Color.Secondary">🔄 خرید مجدد</MudChip>
|
||||||
|
}
|
||||||
|
</PropertyColumn>
|
||||||
|
|
||||||
|
<!-- ستون پکیج -->
|
||||||
|
<PropertyColumn Property="x => x.PackageName" Title="پکیج" />
|
||||||
|
```
|
||||||
|
|
||||||
|
**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>()
|
||||||
|
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<GetDiscountProductsRequest, GetDiscountProductsQuery>()
|
||||||
|
.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
|
||||||
Reference in New Issue
Block a user