Implement Persian Date Conversion and Enhance User Network Information Service
- Added PersianDateTimeService for converting Gregorian dates to Persian format in the BackOffice frontend. - Updated multiple frontend pages (Dashboard, UserPayouts, WorkerControl, UserNetworkInfo) to utilize the new Persian date service. - Enhanced GetUserNetworkPositionDto with 28+ new fields for comprehensive user network data. - Updated GetUserNetworkPositionQueryHandler to include new methods for calculating network statistics. - Modified Protobuf messages to accommodate the new fields, increasing from 14 to 42. - Refined week number calculation algorithm to ensure consistency across C# and SQL implementations. - Created new CSV and Excel files for binary plan calculations. - Ensured all changes are tested and validated for accuracy and performance.
This commit is contained in:
@@ -1,3 +1,86 @@
|
||||
# BackOffice.BFF
|
||||
|
||||
BackOffice BFF
|
||||
> Backend For Frontend layer برای BackOffice UI
|
||||
|
||||
## 📋 خلاصه
|
||||
|
||||
BackOffice.BFF لایه واسط بین BackOffice UI و CMS microservices است که:
|
||||
- درخواستهای UI را aggregate میکند
|
||||
- قراردادهای gRPC اختصاصی ارائه میدهد
|
||||
- منطق سطح BFF را پیادهسازی میکند
|
||||
|
||||
## 🏗️ معماری
|
||||
|
||||
### Protobuf Projects (Own Contracts)
|
||||
|
||||
BackOffice.BFF از قراردادهای Protobuf **اختصاصی خودش** استفاده میکند:
|
||||
|
||||
| Project | Version | Namespace | Purpose |
|
||||
|---------|---------|-----------|----------|
|
||||
| BackOffice.BFF.ClubMembership.Protobuf | 0.0.6 | Foursat.BackOffice.BFF.ClubMembership.Protos | باشگاه مشتریان |
|
||||
| BackOffice.BFF.Commission.Protobuf | 0.0.6 | Foursat.BackOffice.BFF.Commission.Protos | کمیسیون |
|
||||
| BackOffice.BFF.Configuration.Protobuf | 1.0.6 | Foursat.BackOffice.BFF.Configuration.Protos | تنظیمات |
|
||||
| BackOffice.BFF.NetworkMembership.Protobuf | 0.0.6 | Foursat.BackOffice.BFF.NetworkMembership.Protos | شبکه |
|
||||
|
||||
**تغییر معماری (۱۷ آذر ۱۴۰۴)**:
|
||||
- ❌ **قبلا**: استفاده مستقیم از `CMSMicroservice.Protobuf` (Anti-Pattern)
|
||||
- ✅ **حالا**: Protobuf اختصاصی با namespace مجزا
|
||||
- ✅ **مزایا**: جدایی concerns، versioning مستقل، کاهش coupling
|
||||
|
||||
### GrpcServices Mode
|
||||
|
||||
همه پروژههای Protobuf با `GrpcServices="Both"` پیکربندی شدهاند:
|
||||
- **Server**: Base classes برای پیادهسازی در BFF
|
||||
- **Client**: Client classes برای استفاده در BackOffice UI
|
||||
|
||||
### HTTP Annotations (Swagger)
|
||||
|
||||
همه 33 endpoint با HTTP annotations پیادهسازی شدهاند:
|
||||
```protobuf
|
||||
import "google/api/annotations.proto";
|
||||
|
||||
rpc GetClubMembershipById(GetClubMembershipByIdRequest) returns (GetClubMembershipByIdResponse) {
|
||||
option (google.api.http) = { get: "/GetClubMembershipById" };
|
||||
}
|
||||
```
|
||||
|
||||
**Package**: Google.Api.CommonProtos v2.10.0
|
||||
|
||||
## 🔧 Mapster Configuration
|
||||
|
||||
### Immutable Type Handling
|
||||
|
||||
Protobuf messages دارای فیلدهای immutable هستند. از `MapWith()` استفاده کنید:
|
||||
|
||||
```csharp
|
||||
config.NewConfig<GetNetworkTreeResponseDto, GetNetworkTreeResponse>()
|
||||
.MapWith(src => new GetNetworkTreeResponse {
|
||||
Items = { src.Items.Select(x => new NetworkTreeNodeModel {
|
||||
UserId = x.UserId,
|
||||
FirstName = x.FirstName,
|
||||
// ...
|
||||
}) }
|
||||
});
|
||||
```
|
||||
|
||||
**Profiles**:
|
||||
- NetworkMembershipProfile.cs
|
||||
- ProductsProfile.cs
|
||||
|
||||
## 📦 Package Publishing
|
||||
|
||||
برای publish به GitLab registry:
|
||||
|
||||
```bash
|
||||
cd BackOffice.BFF.{Module}.Protobuf
|
||||
dotnet pack -c Release
|
||||
# Auto-push via PushToFourSat target
|
||||
```
|
||||
|
||||
**Registry**: https://git.afrino.co/api/packages/FourSat/nuget
|
||||
|
||||
## 🔗 Related Docs
|
||||
|
||||
- [Architecture Patterns](../../../02-ARCHITECTURE/README.md)
|
||||
- [API Coverage](api-coverage.md)
|
||||
- [Protobuf Dependencies](protobuf-dependencies.md)
|
||||
@@ -4,7 +4,7 @@
|
||||
[]()
|
||||
[]()
|
||||
|
||||
## 📊 Project Status (2025-12-01)
|
||||
## 📊 Project Status (2025-12-18)
|
||||
|
||||
**Overall Progress**: 85% Complete (7/10 phases)
|
||||
**Production Readiness**: 95%
|
||||
@@ -32,9 +32,15 @@
|
||||
|
||||
---
|
||||
|
||||
## 🚀 Recent Updates (2025-12-01)
|
||||
## 🚀 Recent Updates (2025-12-18 / ۲۸ آذر)
|
||||
|
||||
### Email & SMS Notifications - COMPLETED ✅
|
||||
### Entity Configuration - Persian Encoding Fix ✅
|
||||
- ✅ **Geography Entities**: Country, State, City
|
||||
- ✅ **Change**: All string columns now `NVARCHAR` with `Persian_100_CI_AI` collation
|
||||
- ✅ **Migration**: `FixPersianCollation_Geography`
|
||||
- ✅ **Fixes**: Persian characters display correctly in Geography tables
|
||||
|
||||
### Previous Updates (2025-12-01)
|
||||
- ✅ **MailKit 4.14.1** for Email (SMTP with HTML templates)
|
||||
- ✅ **Kavenegar 1.2.5** for SMS (Iranian SMS gateway)
|
||||
- ✅ User.Email field added with migration
|
||||
|
||||
@@ -0,0 +1,281 @@
|
||||
# Club Membership Migration Scripts
|
||||
|
||||
**Created**: 2025-12-09
|
||||
**Purpose**: مهاجرت کاربران موجود به سیستم باشگاه مشتریان
|
||||
**Location**: `/dbbkup/`
|
||||
|
||||
---
|
||||
|
||||
## 📋 Overview
|
||||
|
||||
این اسکریپتها کاربرانی که قبل از راهاندازی سیستم باشگاه مشتریان، مبلغ 56 میلیون ریال شارژ کردهاند را بهطور خودکار عضو باشگاه میکنند.
|
||||
|
||||
---
|
||||
|
||||
## 📄 Scripts
|
||||
|
||||
### 1. MigrateUsersToClubMembership.sql (نسخه کامل)
|
||||
|
||||
**Path**: `/dbbkup/MigrateUsersToClubMembership.sql`
|
||||
|
||||
**Features**:
|
||||
- ✅ بررسی `UserWalletChangeLogs` برای محاسبه مجموع شارژها
|
||||
- ✅ Fallback به `Transactions` اگر Logs خالی بود
|
||||
- ✅ ثبت تاریخ دقیق اولین شارژ بهعنوان `ActivatedAt`
|
||||
- ✅ Skip کاربرانی که قبلاً عضو باشگاه هستند
|
||||
- ✅ Transaction-safe (هر کاربر یک transaction جداگانه)
|
||||
- ✅ گزارش کامل (موفقیتها + خطاها)
|
||||
|
||||
**What It Does**:
|
||||
```sql
|
||||
-- برای هر کاربر با شارژ >= 56M:
|
||||
1. INSERT INTO ClubMemberships (UserId, ActivatedAt=FirstChargeDate, InitialContribution=25M)
|
||||
2. INSERT INTO ClubMembershipHistories (Action=0, Reason='فعالسازی خودکار - مهاجرت')
|
||||
3. INSERT INTO UserClubFeatures (ClubFeatureId IN (1,2,3,4), Notes='اعطا شده خودکار')
|
||||
```
|
||||
|
||||
**Sample Output**:
|
||||
```
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ شروع فرآیند انتقال کاربران به باشگاه مشتریان ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
|
||||
تاریخ و زمان اجرا: 2025-12-09 16:30:00.0000000
|
||||
مبلغ سهم استخر: 25,000,000 ریال
|
||||
|
||||
─────────────────────────────────────────────────────────────────
|
||||
📊 تعداد کاربران کاندید: 45
|
||||
─────────────────────────────────────────────────────────────────
|
||||
🔄 شروع ثبت عضویتها...
|
||||
|
||||
✓ کاربر 1001 (علی محمدی - 1234567890): عضویت با ID 501 ایجاد شد.
|
||||
✓ کاربر 1002 (سارا احمدی - 0987654321): عضویت با ID 502 ایجاد شد.
|
||||
...
|
||||
|
||||
─────────────────────────────────────────────────────────────────
|
||||
╔═══════════════════════════════════════════════════════════════╗
|
||||
║ گزارش نهایی مهاجرت ║
|
||||
╚═══════════════════════════════════════════════════════════════╝
|
||||
|
||||
تعداد کل کاندیدها: 45
|
||||
تعداد قبلاً عضو: 0
|
||||
تعداد پردازش شده: 45
|
||||
تعداد خطا: 0
|
||||
مجموع سهم استخر: 1,125,000,000 ریال
|
||||
|
||||
✓ فرآیند مهاجرت با موفقیت به پایان رسید.
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. MigrateUsersToClubMembership_Simple.sql (نسخه ساده)
|
||||
|
||||
**Path**: `/dbbkup/MigrateUsersToClubMembership_Simple.sql`
|
||||
|
||||
**Features**:
|
||||
- ✅ بررسی موجودی فعلی (`UserWallets.Balance` >= 56M)
|
||||
- ✅ سریعتر از نسخه کامل
|
||||
- ✅ برای سیستمهایی که تاریخچه شارژ ندارند
|
||||
- ✅ همان Transaction safety
|
||||
|
||||
**Difference**:
|
||||
```sql
|
||||
-- نسخه کامل:
|
||||
SUM(uwcl.ChangeValue) >= 56000000 -- از تاریخچه
|
||||
|
||||
-- نسخه ساده:
|
||||
uw.Balance >= 56000000 -- از موجودی فعلی
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 Technical Details
|
||||
|
||||
### Transaction Strategy
|
||||
|
||||
**قبلی (اشتباه)**:
|
||||
```sql
|
||||
BEGIN TRANSACTION; -- یک transaction بزرگ
|
||||
-- 100 INSERT...
|
||||
COMMIT TRANSACTION;
|
||||
```
|
||||
❌ با cursor سازگار نیست! → `log file overflow`
|
||||
|
||||
**فعلی (صحیح)**:
|
||||
```sql
|
||||
WHILE @@FETCH_STATUS = 0
|
||||
BEGIN
|
||||
BEGIN TRANSACTION; -- transaction جداگانه
|
||||
INSERT ClubMemberships;
|
||||
INSERT ClubMembershipHistories;
|
||||
INSERT UserClubFeatures (4 rows);
|
||||
COMMIT TRANSACTION; -- برای هر کاربر
|
||||
END
|
||||
```
|
||||
✅ هر کاربر مستقل → اگر یکی خطا داد، بقیه commit میشوند
|
||||
|
||||
---
|
||||
|
||||
### Schema Compatibility
|
||||
|
||||
**تغییرات از Schema واقعی**:
|
||||
1. ❌ حذف `User.ClubMembershipId` (این ستون وجود نداره!)
|
||||
2. ✅ رابطه: `ClubMemberships.UserId → Users.Id` (یکطرفه)
|
||||
3. ✅ `Action` از نوع `INT` است (نه `NVARCHAR`):
|
||||
- `0` = Activated
|
||||
- `1` = Deactivated
|
||||
|
||||
**Unicode Encoding**:
|
||||
```sql
|
||||
-- اشتباه (encoding خراب):
|
||||
N'فارسی' -- در SELECT باز هم خراب میشه!
|
||||
|
||||
-- درست:
|
||||
CAST(N'فعالسازی خودکار' AS NVARCHAR(500))
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Data Flow
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 1. Query: Users with TotalCharge >= 56M │
|
||||
│ Sources: UserWalletChangeLogs OR Transactions │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 2. Filter: Skip users already in ClubMemberships │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 3. For Each User (in cursor): │
|
||||
│ BEGIN TRANSACTION │
|
||||
│ ├─ INSERT ClubMembership │
|
||||
│ │ (UserId, ActivatedAt=FirstCharge, │
|
||||
│ │ InitialContribution=25M) │
|
||||
│ ├─ INSERT ClubMembershipHistory │
|
||||
│ │ (Action=0, Reason='مهاجرت دادهها') │
|
||||
│ └─ INSERT UserClubFeatures (x4) │
|
||||
│ (ClubFeatureId IN (1,2,3,4)) │
|
||||
│ COMMIT TRANSACTION │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
↓
|
||||
┌─────────────────────────────────────────────────────────┐
|
||||
│ 4. Report: Success count, Errors, Summary │
|
||||
└─────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ Configuration Variables
|
||||
|
||||
```sql
|
||||
DECLARE @InitialContribution BIGINT = 25000000; -- 25M به صندوق
|
||||
DECLARE @ChargeAmount BIGINT = 56000000; -- 56M شارژ
|
||||
DECLARE @CurrentDateTime DATETIME2(7) = SYSDATETIME();
|
||||
```
|
||||
|
||||
**Adjustable**:
|
||||
- `@ChargeAmount`: تغییر حداقل مبلغ شارژ
|
||||
- `@InitialContribution`: تغییر سهم استخر
|
||||
|
||||
---
|
||||
|
||||
## 🧪 Testing Queries
|
||||
|
||||
### 1. شمارش کاربران واجد شرایط
|
||||
|
||||
```sql
|
||||
-- نسخه کامل:
|
||||
SELECT COUNT(DISTINCT u.Id)
|
||||
FROM [CMS].[Users] u
|
||||
INNER JOIN [CMS].[UserWallets] uw ON uw.UserId = u.Id
|
||||
INNER JOIN [CMS].[UserWalletChangeLogs] uwcl ON uwcl.WalletId = uw.Id
|
||||
WHERE u.IsDeleted = 0
|
||||
AND uwcl.IsIncrease = 1
|
||||
AND uwcl.ChangeValue > 0
|
||||
GROUP BY u.Id
|
||||
HAVING SUM(uwcl.ChangeValue) >= 56000000;
|
||||
|
||||
-- نسخه ساده:
|
||||
SELECT COUNT(*)
|
||||
FROM [CMS].[Users] u
|
||||
INNER JOIN [CMS].[UserWallets] uw ON uw.UserId = u.Id
|
||||
LEFT JOIN [CMS].[ClubMemberships] cm ON cm.UserId = u.Id
|
||||
WHERE u.IsDeleted = 0
|
||||
AND cm.Id IS NULL
|
||||
AND uw.Balance >= 56000000;
|
||||
```
|
||||
|
||||
### 2. تأیید ویژگیهای ثبت شده
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
cm.Id AS MembershipId,
|
||||
cm.UserId,
|
||||
u.FirstName + ' ' + u.LastName AS FullName,
|
||||
cm.ActivatedAt,
|
||||
COUNT(ucf.Id) AS FeaturesCount
|
||||
FROM [CMS].[ClubMemberships] cm
|
||||
INNER JOIN [CMS].[Users] u ON u.Id = cm.UserId
|
||||
LEFT JOIN [CMS].[UserClubFeatures] ucf ON ucf.ClubMembershipId = cm.Id
|
||||
WHERE cm.Created >= '2025-12-09' -- امروز
|
||||
GROUP BY cm.Id, cm.UserId, u.FirstName, u.LastName, cm.ActivatedAt
|
||||
HAVING COUNT(ucf.Id) != 4; -- باید 4 تا باشه!
|
||||
```
|
||||
|
||||
### 3. چک کردن History
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
h.UserId,
|
||||
u.FirstName + ' ' + u.LastName AS FullName,
|
||||
h.Action,
|
||||
h.Reason,
|
||||
h.Created
|
||||
FROM [CMS].[ClubMembershipHistories] h
|
||||
INNER JOIN [CMS].[Users] u ON u.Id = h.UserId
|
||||
WHERE h.CreatedBy = 'MigrationScript'
|
||||
ORDER BY h.Created DESC;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚨 Error Handling
|
||||
|
||||
**Script Behavior**:
|
||||
- ✅ هر transaction جداگانه → اگر یک کاربر fail شد، بقیه commit میشوند
|
||||
- ✅ خطاها در `@ProcessLog` ذخیره میشوند
|
||||
- ✅ گزارش نهایی شامل لیست کامل خطاها
|
||||
|
||||
**Common Errors**:
|
||||
1. **"Invalid column 'UserName'"** → ستون وجود نداره (باید `FirstName + LastName`)
|
||||
2. **"Invalid column 'ClubMembershipId'"** → در جدول `Users` نیست
|
||||
3. **"Conversion failed 'Activated'"** → باید `0` باشه نه `'Activated'`
|
||||
4. **"Transaction cannot be committed"** → نباید `SET XACT_ABORT ON` باشه با cursor
|
||||
|
||||
---
|
||||
|
||||
## 📝 Notes
|
||||
|
||||
1. **Idempotent**: اجرای مجدد اسکریپت، کاربران قبلی را skip میکند
|
||||
2. **Rollback-Safe**: اگر کل script fail شد، چیزی commit نمیشه
|
||||
3. **Performance**: برای 1000+ کاربر، ممکنه 5-10 دقیقه طول بکشه
|
||||
4. **Logging**: تمام عملیاتها با `CreatedBy = 'MigrationScript'` قابل شناسایی هستند
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Post-Migration Checklist
|
||||
|
||||
- [ ] شمارش کاربران مهاجرت شده = تعداد موردانتظار
|
||||
- [ ] تمام اعضای جدید 4 ویژگی دارند (`UserClubFeatures.Count = 4`)
|
||||
- [ ] همه `ClubMembershipHistories` با `Action = 0` ثبت شدهاند
|
||||
- [ ] مجموع `InitialContribution` با `ClubMemberships.Count × 25M` برابره
|
||||
- [ ] هیچ خطایی در گزارش نهایی نیست (`@ErrorCount = 0`)
|
||||
|
||||
---
|
||||
|
||||
**Last Updated**: 2025-12-09
|
||||
**Author**: Migration Script Generator
|
||||
**Version**: 1.0
|
||||
@@ -0,0 +1,310 @@
|
||||
# مستندات سیستم کمیسیون (Commission System)
|
||||
|
||||
> **آخرین بروزرسانی**: ۲۹ آذر ۱۴۰۴ (19 December 2025)
|
||||
|
||||
---
|
||||
|
||||
## 📋 خلاصه
|
||||
|
||||
سیستم کمیسیون مسئول محاسبه، ذخیره و پرداخت کمیسیونهای کاربران بر اساس ساختار شبکه بازاریابی است.
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ موجودیتها (Entities)
|
||||
|
||||
### WeekDefinition
|
||||
جدول مرجع برای تعریف هفتههای مالی:
|
||||
|
||||
```csharp
|
||||
public class WeekDefinition : BaseAuditableEntity
|
||||
{
|
||||
public int WeekOrder { get; set; } // شماره ترتیبی هفته
|
||||
public int Year { get; set; } // سال میلادی
|
||||
public int PersianYear { get; set; } // سال شمسی
|
||||
public DateTime StartDate { get; set; } // تاریخ شروع
|
||||
public DateTime EndDate { get; set; } // تاریخ پایان
|
||||
public string StartDatePersian { get; set; } // تاریخ شروع شمسی
|
||||
public string EndDatePersian { get; set; } // تاریخ پایان شمسی
|
||||
public bool IsActive { get; set; } // آیا هفته جاری است
|
||||
}
|
||||
```
|
||||
|
||||
### NetworkWeeklyBalance
|
||||
تعادل هفتگی شاخه چپ و راست کاربر:
|
||||
|
||||
```csharp
|
||||
public class NetworkWeeklyBalance : BaseAuditableEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long WeekDefinitionId { get; set; } // FK به WeekDefinition
|
||||
public long LeftBalance { get; set; } // امتیاز شاخه چپ
|
||||
public long RightBalance { get; set; } // امتیاز شاخه راست
|
||||
|
||||
// Navigation Properties
|
||||
public virtual User User { get; set; }
|
||||
public virtual WeekDefinition WeekDefinition { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**Index**: `(UserId, WeekDefinitionId)` - Unique
|
||||
|
||||
### WeeklyCommissionPool
|
||||
استخر کمیسیون هفتگی:
|
||||
|
||||
```csharp
|
||||
public class WeeklyCommissionPool : BaseAuditableEntity
|
||||
{
|
||||
public long WeekDefinitionId { get; set; } // FK به WeekDefinition
|
||||
public long TotalPoolAmount { get; set; } // مجموع استخر
|
||||
public long DistributedAmount { get; set; } // مقدار توزیع شده
|
||||
public int TotalBalances { get; set; } // تعداد کل تعادلها
|
||||
public long PerBalanceAmount { get; set; } // مبلغ هر تعادل
|
||||
public bool IsFinalized { get; set; } // آیا نهایی شده
|
||||
|
||||
// Navigation Property
|
||||
public virtual WeekDefinition WeekDefinition { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### UserCommissionPayout
|
||||
رکورد پرداخت کمیسیون به کاربر:
|
||||
|
||||
```csharp
|
||||
public class UserCommissionPayout : BaseAuditableEntity
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public long WeekDefinitionId { get; set; } // FK به WeekDefinition
|
||||
public int BalancesEarned { get; set; } // تعداد تعادلهای کسب شده
|
||||
public long Amount { get; set; } // مبلغ کمیسیون
|
||||
public CommissionPayoutStatus Status { get; set; } // وضعیت پرداخت
|
||||
public DateTime? PaidAt { get; set; } // تاریخ پرداخت
|
||||
|
||||
// Navigation Properties
|
||||
public virtual User User { get; set; }
|
||||
public virtual WeekDefinition WeekDefinition { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
**وضعیتها (Status)**:
|
||||
- `Created` - ایجاد شده
|
||||
- `Paid` - پرداخت به کیف پول
|
||||
- `WithdrawalRequested` - درخواست برداشت
|
||||
- `Withdrawn` - برداشت شده
|
||||
- `Cancelled` - لغو شده
|
||||
|
||||
### CommissionPayoutHistory
|
||||
تاریخچه تغییرات وضعیت پرداخت:
|
||||
|
||||
```csharp
|
||||
public class CommissionPayoutHistory : BaseAuditableEntity
|
||||
{
|
||||
public long UserCommissionPayoutId { get; set; }
|
||||
public long WeekDefinitionId { get; set; } // FK به WeekDefinition
|
||||
public CommissionPayoutStatus FromStatus { get; set; }
|
||||
public CommissionPayoutStatus ToStatus { get; set; }
|
||||
public string? Notes { get; set; }
|
||||
|
||||
// Navigation Properties
|
||||
public virtual UserCommissionPayout UserCommissionPayout { get; set; }
|
||||
public virtual WeekDefinition WeekDefinition { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### WorkerExecutionLog
|
||||
لاگ اجرای Worker های محاسبه کمیسیون:
|
||||
|
||||
```csharp
|
||||
public class WorkerExecutionLog : BaseAuditableEntity
|
||||
{
|
||||
public string WorkerName { get; set; } // نام Worker
|
||||
public long? WeekDefinitionId { get; set; } // FK به WeekDefinition (nullable)
|
||||
public DateTime StartedAt { get; set; } // زمان شروع
|
||||
public DateTime? CompletedAt { get; set; } // زمان پایان
|
||||
public bool IsSuccess { get; set; } // موفقیت
|
||||
public string? ErrorMessage { get; set; } // پیام خطا
|
||||
public int ProcessedCount { get; set; } // تعداد پردازش شده
|
||||
|
||||
// Navigation Property
|
||||
public virtual WeekDefinition? WeekDefinition { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔗 روابط (Relationships)
|
||||
|
||||
```
|
||||
WeekDefinition (1) ─────┬──── (*) NetworkWeeklyBalance
|
||||
├──── (*) WeeklyCommissionPool
|
||||
├──── (*) UserCommissionPayout
|
||||
├──── (*) CommissionPayoutHistory
|
||||
└──── (*) WorkerExecutionLog
|
||||
|
||||
User (1) ───────────────┬──── (*) NetworkWeeklyBalance
|
||||
└──── (*) UserCommissionPayout
|
||||
|
||||
UserCommissionPayout (1) ──── (*) CommissionPayoutHistory
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📡 Proto Models
|
||||
|
||||
### UserCommissionPayoutModel
|
||||
```protobuf
|
||||
message UserCommissionPayoutModel {
|
||||
int64 id = 1;
|
||||
int64 user_id = 2;
|
||||
string user_full_name = 3;
|
||||
int64 week_definition_id = 4; // شناسه هفته
|
||||
int32 balances_earned = 5; // تعداد تعادل
|
||||
int64 amount = 6; // مبلغ
|
||||
int32 status = 7; // وضعیت
|
||||
google.protobuf.Timestamp paid_at = 8;
|
||||
google.protobuf.Timestamp created = 9;
|
||||
string mobile = 10;
|
||||
string week_display_name = 11; // نام نمایشی هفته
|
||||
}
|
||||
```
|
||||
|
||||
### UserWeeklyBalanceModel
|
||||
```protobuf
|
||||
message UserWeeklyBalanceModel {
|
||||
int64 user_id = 1;
|
||||
int64 week_definition_id = 2; // شناسه هفته
|
||||
int64 left_balance = 3;
|
||||
int64 right_balance = 4;
|
||||
string start_date_persian = 5;
|
||||
string end_date_persian = 6;
|
||||
int32 year = 7;
|
||||
int32 week_order = 8;
|
||||
bool is_active = 9;
|
||||
string week_display_name = 10; // نام نمایشی هفته
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🎯 نامگذاری فیلدها
|
||||
|
||||
### قبل از مایگریشن (Legacy)
|
||||
```
|
||||
WeekNumber: "2025-01" (string)
|
||||
GregorianWeekNumber: "2025-01" (string)
|
||||
PersianWeekNumber: "1403-40" (string)
|
||||
WeekLabel: "هفته 1 - 1403/10/01"
|
||||
```
|
||||
|
||||
### بعد از مایگریشن (Current)
|
||||
```
|
||||
WeekDefinitionId: 42 (long) // FK به جدول WeekDefinition
|
||||
WeekDisplayName: "هفته 1 - 1403/10/01" // ساخته شده از WeekDefinition
|
||||
```
|
||||
|
||||
**فرمول WeekDisplayName**:
|
||||
```csharp
|
||||
$"هفته {WeekDefinition.WeekOrder} - {WeekDefinition.StartDatePersian}"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📊 Query Examples
|
||||
|
||||
### دریافت کمیسیونهای کاربر
|
||||
```csharp
|
||||
var payouts = await _context.UserCommissionPayouts
|
||||
.Include(p => p.WeekDefinition)
|
||||
.Where(p => p.UserId == userId)
|
||||
.OrderByDescending(p => p.WeekDefinition.WeekOrder)
|
||||
.Select(p => new {
|
||||
p.Id,
|
||||
p.WeekDefinitionId,
|
||||
WeekDisplayName = $"هفته {p.WeekDefinition.WeekOrder} - {p.WeekDefinition.StartDatePersian}",
|
||||
p.BalancesEarned,
|
||||
p.Amount,
|
||||
p.Status
|
||||
})
|
||||
.ToListAsync();
|
||||
```
|
||||
|
||||
### دریافت تعادل هفتگی
|
||||
```csharp
|
||||
var balance = await _context.NetworkWeeklyBalances
|
||||
.Include(b => b.WeekDefinition)
|
||||
.Where(b => b.UserId == userId && b.WeekDefinitionId == weekDefinitionId)
|
||||
.Select(b => new {
|
||||
b.WeekDefinitionId,
|
||||
WeekDisplayName = $"هفته {b.WeekDefinition.WeekOrder} - {b.WeekDefinition.StartDatePersian}",
|
||||
b.LeftBalance,
|
||||
b.RightBalance,
|
||||
b.WeekDefinition.StartDatePersian,
|
||||
b.WeekDefinition.EndDatePersian
|
||||
})
|
||||
.FirstOrDefaultAsync();
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ⚠️ ملاحظات مایگریشن
|
||||
|
||||
### EF Migration
|
||||
```bash
|
||||
# ایجاد migration
|
||||
dotnet ef migrations add MigrateWeekNumberToWeekDefinitionId \
|
||||
-p CMSMicroservice.Infrastructure \
|
||||
-s CMSMicroservice.WebApi
|
||||
|
||||
# اجرای migration
|
||||
dotnet ef database update \
|
||||
-p CMSMicroservice.Infrastructure \
|
||||
-s CMSMicroservice.WebApi
|
||||
```
|
||||
|
||||
### Data Migration Script
|
||||
```sql
|
||||
-- Step 1: Add new column
|
||||
ALTER TABLE NetworkWeeklyBalances ADD WeekDefinitionId BIGINT NULL;
|
||||
|
||||
-- Step 2: Populate from WeekDefinitions
|
||||
UPDATE nwb
|
||||
SET nwb.WeekDefinitionId = wd.Id
|
||||
FROM NetworkWeeklyBalances nwb
|
||||
INNER JOIN WeekDefinitions wd ON
|
||||
CONCAT(wd.Year, '-', RIGHT('0' + CAST(wd.WeekOrder AS VARCHAR), 2)) = nwb.WeekNumber;
|
||||
|
||||
-- Step 3: Add FK constraint
|
||||
ALTER TABLE NetworkWeeklyBalances
|
||||
ADD CONSTRAINT FK_NetworkWeeklyBalances_WeekDefinitions
|
||||
FOREIGN KEY (WeekDefinitionId) REFERENCES WeekDefinitions(Id);
|
||||
|
||||
-- Step 4: Drop old column (after verification)
|
||||
ALTER TABLE NetworkWeeklyBalances DROP COLUMN WeekNumber;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 تغییرات API
|
||||
|
||||
### Request Changes
|
||||
```
|
||||
// قبل
|
||||
GET /api/commission/payouts?weekNumber=2025-01
|
||||
|
||||
// بعد
|
||||
GET /api/commission/payouts?weekDefinitionId=42
|
||||
```
|
||||
|
||||
### Response Changes
|
||||
```json
|
||||
// قبل
|
||||
{
|
||||
"weekNumber": "2025-01",
|
||||
"weekLabel": "هفته 1 - 1403/10/01"
|
||||
}
|
||||
|
||||
// بعد
|
||||
{
|
||||
"weekDefinitionId": 42,
|
||||
"weekDisplayName": "هفته 1 - 1403/10/01"
|
||||
}
|
||||
```
|
||||
@@ -21,7 +21,53 @@
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Recent Updates (2024-12-04)
|
||||
## 🆕 Recent Updates (2025-12-09)
|
||||
|
||||
### ✅ Club Membership Auto-Features Enhancement
|
||||
|
||||
**Date**: 2025-12-09
|
||||
**Feature**: اختصاص خودکار ویژگیهای باشگاه به اعضای جدید
|
||||
|
||||
**Changes**:
|
||||
1. **`ActivateClubMembershipCommandHandler.cs`**:
|
||||
- بعد از ایجاد `ClubMembership` و ثبت `ClubMembershipHistory`
|
||||
- بهطور خودکار 4 ویژگی باشگاه (`ClubFeatureId IN (1,2,3,4)`) در جدول `UserClubFeatures` ثبت میشود
|
||||
- فقط برای عضویتهای جدید (`isNewMembership = true`)
|
||||
- با `Notes = "اعطا شده بهطور خودکار هنگام فعالسازی"`
|
||||
|
||||
2. **Migration Script**:
|
||||
- `MigrateUsersToClubMembership.sql`: اسکریپت مهاجرت کاربران با ≥56M شارژ به باشگاه
|
||||
- شامل:
|
||||
- ایجاد `ClubMembership` (با `ActivatedAt` = تاریخ اولین شارژ)
|
||||
- ثبت `ClubMembershipHistory` (با `Action = 0` = Activated)
|
||||
- ایجاد 4 رکورد `UserClubFeatures` برای هر کاربر
|
||||
- نسخه Simple: بر اساس موجودی فعلی (`UserWallets.Balance`)
|
||||
|
||||
**Business Logic**:
|
||||
```csharp
|
||||
// بعد از SaveChanges برای History:
|
||||
if (isNewMembership)
|
||||
{
|
||||
var clubFeatures = await _context.ClubFeatures
|
||||
.Where(f => !f.IsDeleted && new long[] { 1, 2, 3, 4 }.Contains(f.Id))
|
||||
.ToListAsync(cancellationToken);
|
||||
|
||||
var userClubFeatures = clubFeatures.Select(feature => new UserClubFeature
|
||||
{
|
||||
UserId = user.Id,
|
||||
ClubMembershipId = entity.Id,
|
||||
ClubFeatureId = feature.Id,
|
||||
GrantedAt = activationDate,
|
||||
Notes = "اعطا شده بهطور خودکار هنگام فعالسازی"
|
||||
}).ToList();
|
||||
|
||||
_context.UserClubFeatures.AddRange(userClubFeatures);
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🆕 Previous Updates (2024-12-04)
|
||||
|
||||
### ✅ Phase 9: Club Discount Shop System Implementation (Complete)
|
||||
|
||||
@@ -642,6 +688,8 @@ if (vatEnabled) {
|
||||
- ✅ `ActivateClubMembershipCommand` - Activate user's club membership
|
||||
- Creates new or reactivates existing membership
|
||||
- Records history with Activated action
|
||||
- **اضافه شده 2025-12-09**: اختصاص خودکار 4 ویژگی باشگاه (`UserClubFeatures`) برای اعضای جدید
|
||||
- `ClubFeatureId IN (1, 2, 3, 4)` بهطور خودکار ثبت میشوند
|
||||
- ✅ `DeactivateClubMembershipCommand` - Deactivate membership
|
||||
- Sets IsActive = false, records history
|
||||
- ✅ `UpdateClubMembershipCommand` - Update membership details
|
||||
|
||||
@@ -0,0 +1,642 @@
|
||||
# Network Tree - Activation Week Feature
|
||||
|
||||
## نمای کلی (Overview)
|
||||
|
||||
این سند تغییرات مربوط به افزودن قابلیت فیلتر و نمایش هفته فعالسازی در درخت شبکه را توضیح میدهد.
|
||||
|
||||
**تاریخ پیادهسازی:** دسامبر 2025
|
||||
|
||||
**تغییرات کلیدی:**
|
||||
- اضافه شدن فیلد `IsActivatedInTargetWeek` برای flagging (به جای filtering)
|
||||
- حذف فیلتر سمت Backend و انتقال به UI
|
||||
- نمایش بصری وضعیت فعالسازی در درخت
|
||||
|
||||
---
|
||||
|
||||
## منطق کسبوکار (Business Logic)
|
||||
|
||||
### رویکرد قبلی (❌ Removed)
|
||||
- فیلتر میکرد و فقط نودهایی که در هفته هدف فعال شدهاند نمایش داده میشدند
|
||||
- مشکل: کاربران نمیتوانستند کل ساختار شبکه را ببینند
|
||||
|
||||
### رویکرد جدید (✅ Current)
|
||||
- **همه نودها نمایش داده میشوند** (بدون فیلتر در دیتابیس)
|
||||
- هر نود یک flag دارد: `IsActivatedInTargetWeek`
|
||||
- UI از این flag برای نمایش بصری استفاده میکند
|
||||
|
||||
### محاسبه هفته فعالسازی
|
||||
|
||||
```csharp
|
||||
private static int CalculateWeekNumber(DateTimeOffset date)
|
||||
{
|
||||
var persianCalendar = new PersianCalendar();
|
||||
int year = persianCalendar.GetYear(date.DateTime);
|
||||
int dayOfYear = persianCalendar.GetDayOfYear(date.DateTime);
|
||||
int weekNumber = (dayOfYear - 1) / 7 + 1;
|
||||
|
||||
return int.Parse($"{year}{weekNumber:D2}");
|
||||
// مثال: 140352 = سال 1403، هفته 52
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## تغییرات Backend
|
||||
|
||||
### 1. DTO Changes
|
||||
|
||||
**فایل:** `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/NetworkTreeDto.cs`
|
||||
|
||||
```csharp
|
||||
public class NetworkTreeDto
|
||||
{
|
||||
// ... existing fields
|
||||
public string? ActivationWeekNumber { get; set; }
|
||||
public bool IsActivatedInTargetWeek { get; set; } // ✅ NEW
|
||||
public DateTimeOffset UserCreated { get; set; }
|
||||
public NetworkTreeDto? LeftChild { get; set; }
|
||||
public NetworkTreeDto? RightChild { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Query Handler Changes
|
||||
|
||||
**فایل:** `CMSMicroservice.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeQueryHandler.cs`
|
||||
|
||||
#### تغییر در BuildTree Method
|
||||
|
||||
```csharp
|
||||
private NetworkTreeDto BuildTree(
|
||||
User user,
|
||||
int currentDepth,
|
||||
int maxDepth,
|
||||
string? requestActivationWeekNumber) // ✅ پارامتر اضافه شد
|
||||
{
|
||||
// محاسبه هفته فعالسازی
|
||||
string? activationWeekNumber = null;
|
||||
bool isActivatedInTargetWeek = false;
|
||||
|
||||
if (user.ClubMembership?.ActivatedAt != null)
|
||||
{
|
||||
activationWeekNumber = CalculateWeekNumber(user.ClubMembership.ActivatedAt.Value)
|
||||
.ToString();
|
||||
|
||||
// چک کردن اینکه آیا در هفته هدف فعال شده
|
||||
if (!string.IsNullOrEmpty(requestActivationWeekNumber))
|
||||
{
|
||||
isActivatedInTargetWeek = activationWeekNumber == requestActivationWeekNumber;
|
||||
}
|
||||
}
|
||||
|
||||
var node = new NetworkTreeDto
|
||||
{
|
||||
// ... existing fields
|
||||
ActivationWeekNumber = activationWeekNumber,
|
||||
IsActivatedInTargetWeek = isActivatedInTargetWeek, // ✅ تنظیم flag
|
||||
};
|
||||
|
||||
// ... recursive calls
|
||||
}
|
||||
```
|
||||
|
||||
#### حذف فیلتر از GetFilteredChildren
|
||||
|
||||
**قبل (❌):**
|
||||
```csharp
|
||||
private IEnumerable<User> GetFilteredChildren(
|
||||
IEnumerable<User> children,
|
||||
bool? isClubActive,
|
||||
string? activationWeekNumber)
|
||||
{
|
||||
var query = children.AsQueryable();
|
||||
|
||||
if (isClubActive.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.ClubMembership != null &&
|
||||
u.ClubMembership.IsActive == isClubActive.Value);
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(activationWeekNumber))
|
||||
{
|
||||
// ❌ فیلتر میکرد
|
||||
query = query.Where(u => /* filter logic */);
|
||||
}
|
||||
|
||||
return query.ToList();
|
||||
}
|
||||
```
|
||||
|
||||
**بعد (✅):**
|
||||
```csharp
|
||||
private IEnumerable<User> GetFilteredChildren(
|
||||
IEnumerable<User> children,
|
||||
bool? isClubActive)
|
||||
{
|
||||
var query = children.AsQueryable();
|
||||
|
||||
// فقط فیلتر IsClubActive باقی ماند
|
||||
if (isClubActive.HasValue)
|
||||
{
|
||||
query = query.Where(u => u.ClubMembership != null &&
|
||||
u.ClubMembership.IsActive == isClubActive.Value);
|
||||
}
|
||||
|
||||
return query.ToList();
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Proto Definition
|
||||
|
||||
**فایل:** `CMSMicroservice.Protobuf/Protos/networkmembership.proto`
|
||||
|
||||
```protobuf
|
||||
message NetworkTreeNodeModel {
|
||||
int64 user_id = 1;
|
||||
string user_name = 2;
|
||||
optional int64 parent_id = 3;
|
||||
optional int32 network_leg = 4;
|
||||
optional int32 network_level = 5;
|
||||
optional bool is_active = 6;
|
||||
optional google.protobuf.Timestamp joined_at = 7;
|
||||
optional google.protobuf.Timestamp club_activated_at = 8;
|
||||
bool is_club_active = 9;
|
||||
string activation_week_number = 10;
|
||||
bool is_activated_in_target_week = 11; // ✅ NEW
|
||||
google.protobuf.Timestamp user_created = 12;
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Mapping
|
||||
|
||||
**فایل:** `CMSMicroservice.WebApi/Common/Mappings/NetworkMembershipProfile.cs`
|
||||
|
||||
```csharp
|
||||
var protoNode = new NetworkTreeNodeModel
|
||||
{
|
||||
UserId = node.UserId,
|
||||
UserName = node.UserName,
|
||||
ParentId = node.ParentId,
|
||||
NetworkLeg = node.NetworkLeg,
|
||||
NetworkLevel = node.NetworkLevel,
|
||||
IsActive = node.IsActive,
|
||||
JoinedAt = node.JoinedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(node.JoinedAt.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
ClubActivatedAt = node.ClubActivatedAt.HasValue
|
||||
? Timestamp.FromDateTime(DateTime.SpecifyKind(node.ClubActivatedAt.Value, DateTimeKind.Utc))
|
||||
: null,
|
||||
IsClubActive = node.IsClubActive,
|
||||
ActivationWeekNumber = node.ActivationWeekNumber ?? string.Empty,
|
||||
IsActivatedInTargetWeek = node.IsActivatedInTargetWeek, // ✅ NEW
|
||||
UserCreated = Timestamp.FromDateTime(DateTime.SpecifyKind(node.UserCreated, DateTimeKind.Utc))
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## تغییرات BFF
|
||||
|
||||
### Proto & Mapping
|
||||
|
||||
همان تغییرات در CMS در BFF هم اعمال شد:
|
||||
|
||||
**فایلها:**
|
||||
- `BackOffice.BFF.Application/NetworkMembershipCQ/Queries/GetNetworkTree/GetNetworkTreeResponseDto.cs`
|
||||
- `BackOffice.BFF.WebApi/Common/Mappings/NetworkMembershipProfile.cs`
|
||||
- `Protobufs/networkmembership.proto`
|
||||
|
||||
```csharp
|
||||
public class NetworkTreeNodeDto
|
||||
{
|
||||
// ... existing properties
|
||||
public bool IsActivatedInTargetWeek { get; set; } // ✅ NEW
|
||||
public string ActivationWeekNumber { get; set; } = string.Empty;
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## تغییرات Frontend
|
||||
|
||||
### 1. Razor Component
|
||||
|
||||
**فایل:** `BackOffice/Pages/Network/NetworkTreeViewer.razor`
|
||||
|
||||
#### تغییر در ستون "وضعیت"
|
||||
|
||||
**قبل (❌):**
|
||||
```razor
|
||||
<PropertyColumn Property="x => x.IsActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
@if (context.Item.IsActive!=null) {
|
||||
<MudChip Color="@((bool)context.Item.IsActive ? Color.Success : Color.Error)">
|
||||
@((bool)context.Item.IsActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
```
|
||||
|
||||
**بعد (✅):**
|
||||
```razor
|
||||
<PropertyColumn Property="x => x.IsClubActive" Title="وضعیت">
|
||||
<CellTemplate>
|
||||
<MudChip T="string"
|
||||
Color="@(context.Item.IsClubActive ? Color.Success : Color.Error)"
|
||||
Size="Size.Small">
|
||||
@(context.Item.IsClubActive ? "فعال" : "غیرفعال")
|
||||
</MudChip>
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
```
|
||||
|
||||
#### ارسال داده به JavaScript
|
||||
|
||||
```csharp
|
||||
private async Task RenderTree()
|
||||
{
|
||||
if (_treeData == null || !_treeData.Nodes.Any()) return;
|
||||
|
||||
var jsNodes = _treeData.Nodes.Select(n => new
|
||||
{
|
||||
userId = n.UserId,
|
||||
userName = n.UserName,
|
||||
parentId = n.ParentId,
|
||||
networkLevel = n.NetworkLevel,
|
||||
networkLeg = n.NetworkLeg,
|
||||
isActive = n.IsClubActive, // ✅ تغییر به IsClubActive
|
||||
isClubActive = n.IsClubActive,
|
||||
isActivatedInTargetWeek = n.IsActivatedInTargetWeek, // ✅ NEW
|
||||
activationWeekNumber = _activationWeekFilter ?? "", // ✅ فیلتر UI
|
||||
clubActivatedAt = n.ClubActivatedAt?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? "",
|
||||
userCreated = n.UserCreated?.ToDateTime().ToLocalTime().ToString("yyyy/MM/dd") ?? ""
|
||||
}).ToArray();
|
||||
|
||||
await JS.InvokeVoidAsync("NetworkTreeViewer.initialize", "network-tree-container", jsNodes);
|
||||
}
|
||||
```
|
||||
|
||||
**نکته مهم:** `activationWeekNumber` از فیلتر UI گرفته میشود (`_activationWeekFilter`) نه از Backend.
|
||||
|
||||
### 2. JavaScript Visualization
|
||||
|
||||
**فایل:** `BackOffice/wwwroot/js/network-tree.js`
|
||||
|
||||
#### منطق رنگ نود (دایره)
|
||||
|
||||
```javascript
|
||||
node.append('circle')
|
||||
.attr('r', 8)
|
||||
.style('fill', d => {
|
||||
// اگر هفتهای انتخاب نشده، همه سبز
|
||||
if (!d.data.activationWeekNumber || d.data.activationWeekNumber === '') {
|
||||
return '#4caf50';
|
||||
}
|
||||
// اگر در هفته هدف فعال شده، سبز، وگرنه قرمز
|
||||
return d.data.isActivatedInTargetWeek ? '#4caf50' : '#f44336';
|
||||
})
|
||||
.style('stroke', '#fff')
|
||||
.style('stroke-width', 2)
|
||||
.style('cursor', 'pointer');
|
||||
```
|
||||
|
||||
#### منطق رنگ تایتل (نام کاربر)
|
||||
|
||||
```javascript
|
||||
node.append('text')
|
||||
.attr('dy', -15)
|
||||
.attr('text-anchor', 'middle')
|
||||
.style('font-size', '12px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('fill', d => d.data.isClubActive ? '#424242' : '#9e9e9e')
|
||||
.text(d => d.data.userName || `User ${d.data.userId}`);
|
||||
```
|
||||
|
||||
#### اضافه کردن فیلدها به buildHierarchy
|
||||
|
||||
```javascript
|
||||
buildHierarchy: function(nodes) {
|
||||
// ...
|
||||
const nodeMap = new Map();
|
||||
nodes.forEach(node => {
|
||||
nodeMap.set(node.userId, {
|
||||
userId: node.userId,
|
||||
userName: node.userName,
|
||||
parentId: node.parentId,
|
||||
level: node.networkLevel,
|
||||
networkLeg: node.networkLeg,
|
||||
isActive: node.isActive,
|
||||
isClubActive: node.isClubActive, // ✅ NEW
|
||||
isActivatedInTargetWeek: node.isActivatedInTargetWeek, // ✅ NEW
|
||||
activationWeekNumber: node.activationWeekNumber, // ✅ NEW
|
||||
clubActivatedAt: node.clubActivatedAt,
|
||||
userCreated: node.userCreated,
|
||||
children: []
|
||||
});
|
||||
});
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
#### Legend (راهنمای رنگها)
|
||||
|
||||
```javascript
|
||||
// Legend for title colors (club status)
|
||||
legend.append('text')
|
||||
.attr('x', 0)
|
||||
.attr('y', 0)
|
||||
.style('font-size', '12px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('fill', '#424242')
|
||||
.text('باشگاه فعال');
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', 0)
|
||||
.attr('y', 20)
|
||||
.style('font-size', '12px')
|
||||
.style('font-weight', 'bold')
|
||||
.style('fill', '#9e9e9e')
|
||||
.text('باشگاه غیرفعال');
|
||||
|
||||
// Legend for circles (week status)
|
||||
legend.append('circle')
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 50)
|
||||
.attr('r', 6)
|
||||
.style('fill', '#4caf50');
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', 12)
|
||||
.attr('y', 54)
|
||||
.style('font-size', '12px')
|
||||
.text('فعال در هفته هدف');
|
||||
|
||||
legend.append('circle')
|
||||
.attr('cx', 0)
|
||||
.attr('cy', 75)
|
||||
.attr('r', 6)
|
||||
.style('fill', '#f44336');
|
||||
|
||||
legend.append('text')
|
||||
.attr('x', 12)
|
||||
.attr('y', 79)
|
||||
.style('font-size', '12px')
|
||||
.text('خارج از هفته هدف');
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## رفتار UI
|
||||
|
||||
### حالت 1: بدون فیلتر هفته
|
||||
|
||||
**وضعیت:** `_activationWeekFilter` خالی است
|
||||
|
||||
**رفتار:**
|
||||
- **دایرهها:** همه سبز (#4caf50)
|
||||
- **تایتل:** مشکی (#424242) برای باشگاه فعال، خاکستری (#9e9e9e) برای باشگاه غیرفعال
|
||||
|
||||
### حالت 2: با فیلتر هفته
|
||||
|
||||
**وضعیت:** مثلاً `_activationWeekFilter = "140352"`
|
||||
|
||||
**رفتار:**
|
||||
- **دایرهها:**
|
||||
- سبز (#4caf50) → کاربران فعال شده در هفته 52 سال 1403
|
||||
- قرمز (#f44336) → کاربران فعال شده در هفتههای دیگر
|
||||
- **تایتل:** همچنان بر اساس `isClubActive`
|
||||
|
||||
### حالت 3: فیلتر IsClubActive
|
||||
|
||||
این فیلتر در سمت Backend اعمال میشود و نودهای غیرفعال را حذف میکند.
|
||||
|
||||
---
|
||||
|
||||
## Flow Diagram
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ User Interface │
|
||||
│ ┌────────────────┐ ┌──────────────────┐ │
|
||||
│ │ IsClubActive │ │ActivationWeek │ │
|
||||
│ │ Filter │ │ Filter │ │
|
||||
│ └────────┬───────┘ └────────┬─────────┘ │
|
||||
└───────────┼──────────────────┼────────────────────────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Backend (CMS) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ GetNetworkTreeQueryHandler │ │
|
||||
│ │ │ │
|
||||
│ │ 1. GetFilteredChildren (IsClubActive filter only) │ │
|
||||
│ │ 2. BuildTree (calculate IsActivatedInTargetWeek) │ │
|
||||
│ │ 3. Return ALL nodes with flags │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ BFF Layer │
|
||||
│ - Proto mapping │
|
||||
│ - Pass-through to Frontend │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (Blazor) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ NetworkTreeViewer.razor │ │
|
||||
│ │ │ │
|
||||
│ │ - Prepare data with UI filter (_activationWeekFilter)│ │
|
||||
│ │ - Send to JavaScript │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└───────────────────────────┬─────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ JavaScript (D3.js) │
|
||||
│ ┌──────────────────────────────────────────────────────┐ │
|
||||
│ │ network-tree.js │ │
|
||||
│ │ │ │
|
||||
│ │ - Apply visual logic: │ │
|
||||
│ │ * Circle color by activationWeekNumber + flag │ │
|
||||
│ │ * Title color by isClubActive │ │
|
||||
│ └──────────────────────────────────────────────────────┘ │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Request
|
||||
|
||||
```csharp
|
||||
public class GetNetworkTreeRequest
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public int? MaxDepth { get; set; }
|
||||
public bool? IsClubActive { get; set; } // Backend filter
|
||||
public string? ActivationWeekNumber { get; set; } // For flag calculation only
|
||||
}
|
||||
```
|
||||
|
||||
### Response
|
||||
|
||||
```csharp
|
||||
public class NetworkTreeDto
|
||||
{
|
||||
public long UserId { get; set; }
|
||||
public string UserName { get; set; }
|
||||
public long? ParentId { get; set; }
|
||||
public int? NetworkLeg { get; set; }
|
||||
public int? NetworkLevel { get; set; }
|
||||
public bool? IsActive { get; set; } // Deprecated
|
||||
public DateTime? JoinedAt { get; set; }
|
||||
public DateTime? ClubActivatedAt { get; set; }
|
||||
public bool IsClubActive { get; set; } // ✅ Use this
|
||||
public string? ActivationWeekNumber { get; set; }
|
||||
public bool IsActivatedInTargetWeek { get; set; } // ✅ NEW
|
||||
public DateTimeOffset UserCreated { get; set; }
|
||||
public NetworkTreeDto? LeftChild { get; set; }
|
||||
public NetworkTreeDto? RightChild { get; set; }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Testing Scenarios
|
||||
|
||||
### Test 1: بدون فیلتر
|
||||
**Input:**
|
||||
- `IsClubActive`: null
|
||||
- `ActivationWeekNumber`: null
|
||||
|
||||
**Expected:**
|
||||
- همه نودها نمایش داده شوند
|
||||
- همه دایرهها سبز
|
||||
- تایتلها بر اساس IsClubActive
|
||||
|
||||
### Test 2: فیلتر باشگاه فعال
|
||||
**Input:**
|
||||
- `IsClubActive`: true
|
||||
- `ActivationWeekNumber`: null
|
||||
|
||||
**Expected:**
|
||||
- فقط نودهای با باشگاه فعال
|
||||
- همه دایرهها سبز
|
||||
- همه تایتلها مشکی
|
||||
|
||||
### Test 3: فیلتر هفته
|
||||
**Input:**
|
||||
- `IsClubActive`: null
|
||||
- `ActivationWeekNumber`: "140352"
|
||||
|
||||
**Expected:**
|
||||
- همه نودها نمایش داده شوند
|
||||
- دایره سبز: فعال شده در هفته 52
|
||||
- دایره قرمز: فعال شده در هفتههای دیگر
|
||||
- تایتلها بر اساس IsClubActive
|
||||
|
||||
### Test 4: ترکیب فیلترها
|
||||
**Input:**
|
||||
- `IsClubActive`: true
|
||||
- `ActivationWeekNumber`: "140352"
|
||||
|
||||
**Expected:**
|
||||
- فقط نودهای با باشگاه فعال
|
||||
- دایره سبز: فعال شده در هفته 52
|
||||
- دایره قرمز: فعال شده در هفتههای دیگر
|
||||
- همه تایتلها مشکی (چون همه باشگاه فعال دارند)
|
||||
|
||||
---
|
||||
|
||||
## Performance Considerations
|
||||
|
||||
### Database Query
|
||||
- ✅ فیلتر `ActivationWeekNumber` از Query حذف شد
|
||||
- ✅ فقط فیلتر `IsClubActive` در سمت دیتابیس
|
||||
- ⚠️ ممکن است تعداد نودهای بیشتری بازگردانده شود
|
||||
|
||||
### Memory
|
||||
- Backend همه نودها را میفرستد
|
||||
- Frontend/JavaScript فیلتر بصری اعمال میکند
|
||||
- برای درختهای بسیار بزرگ (>1000 نود) ممکن است نیاز به pagination باشد
|
||||
|
||||
### UI Rendering
|
||||
- D3.js برای درختهای متوسط (<500 نود) عملکرد خوبی دارد
|
||||
- برای بهبود عملکرد میتوان از virtualization استفاده کرد
|
||||
|
||||
---
|
||||
|
||||
## Migration Notes
|
||||
|
||||
### Breaking Changes
|
||||
- ❌ `IsActive` deprecated است → استفاده از `IsClubActive`
|
||||
- ✅ فیلد جدید `IsActivatedInTargetWeek` اضافه شد
|
||||
|
||||
### Backward Compatibility
|
||||
- Proto field numbers حفظ شدهاند
|
||||
- Response structure تغییر نکرده (فقط فیلد جدید اضافه شده)
|
||||
|
||||
### Deployment Steps
|
||||
1. Deploy Backend (CMS) با Proto جدید
|
||||
2. Deploy BFF با Proto جدید
|
||||
3. Deploy Frontend با visualization جدید
|
||||
4. تست تمام scenarios
|
||||
|
||||
---
|
||||
|
||||
## نکات مهم (Key Points)
|
||||
|
||||
### ✅ Do's
|
||||
- از `IsClubActive` برای وضعیت باشگاه استفاده کنید
|
||||
- `IsActivatedInTargetWeek` فقط برای نمایش بصری است
|
||||
- فیلتر UI را از Razor به JS بفرستید (`_activationWeekFilter`)
|
||||
|
||||
### ❌ Don'ts
|
||||
- از `IsActive` استفاده نکنید (deprecated)
|
||||
- `ActivationWeekNumber` را از Backend برای UI filtering استفاده نکنید
|
||||
- فیلتر `ActivationWeekNumber` را در Query اعمال نکنید
|
||||
|
||||
### 💡 Best Practices
|
||||
- همیشه فیلتر UI و Backend flag را sync نگه دارید
|
||||
- برای درختهای بزرگ از lazy loading استفاده کنید
|
||||
- Legend را همیشه با منطق UI sync کنید
|
||||
|
||||
---
|
||||
|
||||
## فایلهای تغییر یافته
|
||||
|
||||
### Backend (CMS)
|
||||
- ✅ `NetworkTreeDto.cs` - اضافه `IsActivatedInTargetWeek`
|
||||
- ✅ `GetNetworkTreeQueryHandler.cs` - محاسبه flag + حذف فیلتر
|
||||
- ✅ `networkmembership.proto` - اضافه field 11
|
||||
- ✅ `NetworkMembershipProfile.cs` - mapping فیلد جدید
|
||||
|
||||
### BFF
|
||||
- ✅ `GetNetworkTreeResponseDto.cs` - اضافه property
|
||||
- ✅ `NetworkMembershipProfile.cs` - mapping
|
||||
- ✅ `networkmembership.proto` - sync با CMS
|
||||
|
||||
### Frontend
|
||||
- ✅ `NetworkTreeViewer.razor` - تغییر `IsActive` → `IsClubActive`
|
||||
- ✅ `NetworkTreeViewer.razor` - اضافه `isActivatedInTargetWeek` به jsNodes
|
||||
- ✅ `network-tree.js` - منطق رنگ نود بر اساس flag
|
||||
- ✅ `network-tree.js` - منطق رنگ تایتل بر اساس `isClubActive`
|
||||
- ✅ `network-tree.js` - Legend جدید
|
||||
|
||||
---
|
||||
|
||||
## مراجع (References)
|
||||
|
||||
- [Binary Tree Guide](../../01-BUSINESS/binary-tree-guide.md)
|
||||
- [Network Commission System](../../01-BUSINESS/network-commission-system.md)
|
||||
- [CMS API Coverage](./api-coverage.md)
|
||||
|
||||
---
|
||||
|
||||
**تاریخ ایجاد:** 14 دسامبر 2025
|
||||
**آخرین بهروزرسانی:** 14 دسامبر 2025
|
||||
**نویسنده:** Development Team
|
||||
@@ -59,6 +59,7 @@ FrontOffice.BFF/
|
||||
- `GetMyNetworkPosition` - موقعیت کاربر در شبکه
|
||||
- `GetMyNetworkStatistics` - آمار شبکه
|
||||
- `GetMyNetworkTree` - درخت شبکه
|
||||
- `GetSubordinateTree` - درخت زیرمجموعه (NEW - ۲۸ آذر)
|
||||
|
||||
### ClubMembershipCQ
|
||||
عضویت باشگاه مشتریان
|
||||
@@ -107,4 +108,5 @@ dotnet run --project FrontOffice.BFF.WebApi
|
||||
```
|
||||
|
||||
## Last Updated
|
||||
January 2025 - Added DiscountShop integration (4 gRPC clients, 7 handlers, Proto service)
|
||||
- **28 آذر ۱۴۰۴**: Added `GetSubordinateTree` handler for viewing subordinate network trees
|
||||
- **January 2025**: Added DiscountShop integration (4 gRPC clients, 7 handlers, Proto service)
|
||||
Reference in New Issue
Block a user