906 lines
29 KiB
Markdown
906 lines
29 KiB
Markdown
|
|
# سیستم باشگاه مشتریان و محاسبه کمیسیون شبکه
|
|||
|
|
|
|||
|
|
## خلاصه اجرایی
|
|||
|
|
این سند تحلیل جامع و معماری پیشنهادی برای پیادهسازی سیستم باشگاه مشتریان (Club Membership) و محاسبه کمیسیون شبکهای (MLM Binary Plan) را ارائه میدهد. این سیستم امکان مدیریت سه نوع کیف پول، فروشگاه اختصاصی با تخفیف، و توزیع عادلانه کمیسیون بر اساس تعادل شبکه را فراهم میکند.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱. مفاهیم کلیدی
|
|||
|
|
|
|||
|
|
### ۱.۱ کیف پولهای سهگانه
|
|||
|
|
هر کاربر سه نوع کیف پول دارد:
|
|||
|
|
|
|||
|
|
1. **کیف پول اصلی (Balance)**: برای خرید از فروشگاه عمومی بازار
|
|||
|
|
2. **کیف پول تخفیف (DiscountBalance)**: فقط برای خرید از فروشگاه باشگاه مشتریان (محدود به درصد تخفیف محصولات)
|
|||
|
|
3. **کیف پول طلایی/کارمزد (NetworkBalance)**: دریافتی از کمیسیون شبکهای - قابل برداشت نقدی یا خرید الماس از دایا
|
|||
|
|
|
|||
|
|
### ۱.۲ فعالسازی عضویت
|
|||
|
|
- کاربر ۵۶ میلیون تومان پرداخت میکند (از طریق دایا یا درگاه)
|
|||
|
|
- سیستم به صورت خودکار:
|
|||
|
|
- `Balance += 56M` (کیف پول اصلی)
|
|||
|
|
- `DiscountBalance += 56M` (کیف پول تخفیف)
|
|||
|
|
- کاربر دکمه «عضویت در باشگاه» را میزند:
|
|||
|
|
- `25M` به استخر کمیسیون هفتگی اضافه میشود
|
|||
|
|
- کاربر در شبکه باینری (Binary Tree) قرار میگیرد
|
|||
|
|
|
|||
|
|
### ۱.۳ شبکه باینری (Binary MLM Plan)
|
|||
|
|
- هر کاربر حداکثر دو زیرمجموعه دارد: **دست راست** و **دست چپ**
|
|||
|
|
- تعادل (Balance): زمانی که هر دو شاخه دارای اعضای جدید شوند، یک تعادل ایجاد میشود
|
|||
|
|
- **فرمول تعادل**: `UserBalances = MIN(LeftLegBalances, RightLegBalances)`
|
|||
|
|
- تعادلها به صورت هفتگی محاسبه و بعد از توزیع کمیسیون، ریست میشوند
|
|||
|
|
|
|||
|
|
### ۱.۴ محاسبه کمیسیون هفتگی
|
|||
|
|
```text
|
|||
|
|
مبلغ ریالی هر امتیاز = (مجموع مبالغ استخر) ÷ (مجموع تعادلهای کل سیستم)
|
|||
|
|
کمیسیون هر کاربر = (تعداد تعادل کاربر) × (مبلغ ریالی هر امتیاز)
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**مثال**:
|
|||
|
|
- کاربر A: خودش ۱ تعادل + زیرمجموعههایش ۲ تعادل = **۳ امتیاز**
|
|||
|
|
- استخر هفتگی: `175M`
|
|||
|
|
- مجموع امتیازهای سیستم: `5`
|
|||
|
|
- ارزش هر امتیاز: `175M ÷ 5 = 35M`
|
|||
|
|
- کمیسیون کاربر A: `3 × 35M = 105M`
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۲. موجودیتهای جدید (Domain Entities)
|
|||
|
|
|
|||
|
|
### ۲.۱ `ClubMembership` (عضویت باشگاه مشتریان)
|
|||
|
|
```csharp
|
|||
|
|
public class ClubMembership : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
public virtual User User { get; set; }
|
|||
|
|
|
|||
|
|
public bool IsActive { get; set; }
|
|||
|
|
public DateTime? ActivatedAt { get; set; }
|
|||
|
|
|
|||
|
|
// مبلغ اولیه پرداختی برای فعالسازی (معمولاً ۲۵ میلیون)
|
|||
|
|
public long InitialContribution { get; set; }
|
|||
|
|
|
|||
|
|
// مجموع درآمد کارمزد تاکنون
|
|||
|
|
public long TotalEarned { get; set; }
|
|||
|
|
|
|||
|
|
public virtual ICollection<UserClubFeature> UserClubFeatures { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۲.۲ `ClubFeature` (امکانات باشگاه)
|
|||
|
|
```csharp
|
|||
|
|
public class ClubFeature : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public string Title { get; set; }
|
|||
|
|
public string? Description { get; set; }
|
|||
|
|
|
|||
|
|
public bool IsActive { get; set; }
|
|||
|
|
|
|||
|
|
public int? RequiredPoints { get; set; }
|
|||
|
|
public int SortOrder { get; set; }
|
|||
|
|
|
|||
|
|
public virtual ICollection<UserClubFeature> UserClubFeatures { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۲.۳ `UserClubFeature` (امتیاز/فیچرهای فعال برای کاربر)
|
|||
|
|
```csharp
|
|||
|
|
public class UserClubFeature : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
public virtual User User { get; set; }
|
|||
|
|
|
|||
|
|
public long ClubFeatureId { get; set; }
|
|||
|
|
public virtual ClubFeature ClubFeature { get; set; }
|
|||
|
|
|
|||
|
|
public DateTime GrantedAt { get; set; }
|
|||
|
|
public string? Notes { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۲.۴ `NetworkWeeklyBalance` (تعادل هفتگی شبکه)
|
|||
|
|
```csharp
|
|||
|
|
public class NetworkWeeklyBalance : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
public virtual User User { get; set; }
|
|||
|
|
|
|||
|
|
// مثلاً "2025-W48"
|
|||
|
|
public string WeekNumber { get; set; }
|
|||
|
|
|
|||
|
|
public int LeftLegBalances { get; set; }
|
|||
|
|
public int RightLegBalances { get; set; }
|
|||
|
|
public int TotalBalances { get; set; }
|
|||
|
|
|
|||
|
|
// مبلغی که این کاربر همان هفته به استخر اضافه کرده (معمولاً InitialContribution)
|
|||
|
|
public long WeeklyPoolContribution { get; set; }
|
|||
|
|
|
|||
|
|
public DateTime? CalculatedAt { get; set; }
|
|||
|
|
public bool IsExpired { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۲.۵ `WeeklyCommissionPool` (استخر کمیسیون هفتگی)
|
|||
|
|
```csharp
|
|||
|
|
public class WeeklyCommissionPool : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public string WeekNumber { get; set; }
|
|||
|
|
|
|||
|
|
public long TotalPoolAmount { get; set; }
|
|||
|
|
public int TotalBalances { get; set; }
|
|||
|
|
public long ValuePerBalance { get; set; }
|
|||
|
|
|
|||
|
|
public bool IsCalculated { get; set; }
|
|||
|
|
public DateTime? CalculatedAt { get; set; }
|
|||
|
|
|
|||
|
|
public virtual ICollection<UserCommissionPayout> UserCommissionPayouts { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۲.۶ `UserCommissionPayout` (پرداخت کمیسیون به کاربر)
|
|||
|
|
```csharp
|
|||
|
|
public class UserCommissionPayout : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
public virtual User User { get; set; }
|
|||
|
|
|
|||
|
|
public string WeekNumber { get; set; }
|
|||
|
|
|
|||
|
|
public long WeeklyPoolId { get; set; }
|
|||
|
|
public virtual WeeklyCommissionPool WeeklyPool { get; set; }
|
|||
|
|
|
|||
|
|
public int BalancesEarned { get; set; }
|
|||
|
|
public long ValuePerBalance { get; set; }
|
|||
|
|
public long TotalAmount { get; set; }
|
|||
|
|
|
|||
|
|
public CommissionPayoutStatus Status { get; set; }
|
|||
|
|
|
|||
|
|
public DateTime? PaidAt { get; set; }
|
|||
|
|
|
|||
|
|
public WithdrawalMethod? WithdrawalMethod { get; set; }
|
|||
|
|
public string? IbanNumber { get; set; }
|
|||
|
|
public DateTime? WithdrawnAt { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ۲.۷ موجودیتهای History (جداول لاگ)
|
|||
|
|
|
|||
|
|
#### ۲.۷.۱ `ClubMembershipHistory`
|
|||
|
|
لاگ تغییرات مهم روی عضویت باشگاه (فعالسازی، غیرفعالسازی، ویرایش):
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public class ClubMembershipHistory : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long ClubMembershipId { get; set; }
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
|
|||
|
|
public bool OldIsActive { get; set; }
|
|||
|
|
public bool NewIsActive { get; set; }
|
|||
|
|
|
|||
|
|
public long? OldInitialContribution { get; set; }
|
|||
|
|
public long? NewInitialContribution { get; set; }
|
|||
|
|
|
|||
|
|
// Activated / Deactivated / Updated / ManualFix
|
|||
|
|
public string Action { get; set; }
|
|||
|
|
public string? Reason { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### ۲.۷.۲ `NetworkMembershipHistory`
|
|||
|
|
برای اینکه همیشه بدانیم «چه کسی زیرمجموعهی کی شده، چه زمانی، و اگر بعداً جابهجا شد چه اتفاقی افتاده»:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public class NetworkMembershipHistory : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
|
|||
|
|
public long? OldParentId { get; set; }
|
|||
|
|
public long? NewParentId { get; set; }
|
|||
|
|
|
|||
|
|
public NetworkLeg? OldLegPosition { get; set; }
|
|||
|
|
public NetworkLeg? NewLegPosition { get; set; }
|
|||
|
|
|
|||
|
|
// Join / Move / Remove
|
|||
|
|
public string Action { get; set; }
|
|||
|
|
public string? Reason { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- هر بار `RecordNetworkJoin` یا `UpdateNetworkPosition` صدا زده میشود، باید یک رکورد در این جدول نوشته شود.
|
|||
|
|
- این جدول مرجع اصلی برای بازسازی درخت شبکه در زمانهای گذشته است.
|
|||
|
|
|
|||
|
|
#### ۲.۷.۳ `CommissionPayoutHistory`
|
|||
|
|
برای لاگ کامل همهی تغییرات روی پرداخت کمیسیونها (ایجاد، ویرایش دستی، تغییر وضعیت، برداشت و ...):
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public class CommissionPayoutHistory : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long UserCommissionPayoutId { get; set; }
|
|||
|
|
public long UserId { get; set; }
|
|||
|
|
public string WeekNumber { get; set; }
|
|||
|
|
|
|||
|
|
public long AmountBefore { get; set; }
|
|||
|
|
public long AmountAfter { get; set; }
|
|||
|
|
|
|||
|
|
public CommissionPayoutStatus OldStatus { get; set; }
|
|||
|
|
public CommissionPayoutStatus NewStatus { get; set; }
|
|||
|
|
|
|||
|
|
// Created / Paid / WithdrawRequested / Withdrawn / Cancelled / ManualFix
|
|||
|
|
public string Action { get; set; }
|
|||
|
|
public string? PerformedBy { get; set; } // UserId یا System
|
|||
|
|
public string? Reason { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
- اگر بعداً بفهمیم یک پرداخت اشتباه بوده و اصلاحش کنیم، اینجا قابل ردیابی است.
|
|||
|
|
- برای گزارشگیری Audit کامل پرداختها، این جدول استفاده میشود.
|
|||
|
|
|
|||
|
|
#### ۲.۷.۴ `SystemConfigurationHistory`
|
|||
|
|
تاریخچه تغییرات تنظیمات (Config) برای اینکه بعداً بدانیم در هر زمان چه محدودیتی فعال بوده:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public class SystemConfigurationHistory : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public long ConfigurationId { get; set; }
|
|||
|
|
|
|||
|
|
public ConfigurationScope Scope { get; set; }
|
|||
|
|
public string Key { get; set; }
|
|||
|
|
|
|||
|
|
public string OldValue { get; set; }
|
|||
|
|
public string NewValue { get; set; }
|
|||
|
|
|
|||
|
|
public string? Reason { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ۲.۸ موجودیتهای Configuration (تنظیمات پویا)
|
|||
|
|
|
|||
|
|
#### ۲.۸.۱ `ConfigurationScope` (Enum)
|
|||
|
|
```csharp
|
|||
|
|
public enum ConfigurationScope
|
|||
|
|
{
|
|||
|
|
System = 0,
|
|||
|
|
Network = 1,
|
|||
|
|
Club = 2,
|
|||
|
|
Commission = 3
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### ۲.۸.۲ `SystemConfiguration`
|
|||
|
|
جدولی برای نگهداری تنظیمات پویا. هم تنظیمات عمومی سیستم، هم تنظیمات مخصوص شبکه، باشگاه و کمیسیون:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public class SystemConfiguration : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
public ConfigurationScope Scope { get; set; } // System / Network / Club / Commission
|
|||
|
|
|
|||
|
|
// مثل: "MaxWeeklyBalancesPerUser", "MinContributionAmount", ...
|
|||
|
|
public string Key { get; set; }
|
|||
|
|
|
|||
|
|
// مقدار بهصورت رشته - تفسیر در لایه Application
|
|||
|
|
public string Value { get; set; }
|
|||
|
|
|
|||
|
|
// برای UI و Validation (Int / Decimal / Bool / String / Json)
|
|||
|
|
public string? DataType { get; set; }
|
|||
|
|
|
|||
|
|
public string? Description { get; set; }
|
|||
|
|
public bool IsActive { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
**مثال کانفیگهای مرتبط با شبکه:**
|
|||
|
|
|
|||
|
|
- `Scope = Network`, `Key = "MaxWeeklyBalancesPerUser"`, `Value = "300"`
|
|||
|
|
- `Scope = Network`, `Key = "MaxChildrenPerLeg"`, `Value = "1"`
|
|||
|
|
- `Scope = Commission`, `Key = "DefaultInitialContribution"`, `Value = "25000000"`
|
|||
|
|
|
|||
|
|
> نکته: هر بار که مقدار `SystemConfiguration` تغییر میکند، یک رکورد در `SystemConfigurationHistory` ثبت میشود تا تنظیمات گذشته قابل ردیابی باشد.
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
### ۲.۹ Enums جدید
|
|||
|
|
```csharp
|
|||
|
|
public enum CommissionPayoutStatus
|
|||
|
|
{
|
|||
|
|
Pending = 0,
|
|||
|
|
Paid = 1,
|
|||
|
|
WithdrawRequested = 2,
|
|||
|
|
Withdrawn = 3,
|
|||
|
|
Cancelled = 4
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum WithdrawalMethod
|
|||
|
|
{
|
|||
|
|
Cash = 0,
|
|||
|
|
Diamond = 1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
public enum NetworkLeg
|
|||
|
|
{
|
|||
|
|
Left = 0,
|
|||
|
|
Right = 1
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۳. تغییرات در موجودیتهای موجود
|
|||
|
|
|
|||
|
|
### ۳.۱ `User`
|
|||
|
|
افزودن فیلدهای مربوط به شبکه باینری و ناوبری:
|
|||
|
|
|
|||
|
|
```csharp
|
|||
|
|
public class User : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
// ...
|
|||
|
|
|
|||
|
|
public long? NetworkParentId { get; set; }
|
|||
|
|
public virtual User? NetworkParent { get; set; }
|
|||
|
|
|
|||
|
|
public NetworkLeg? LegPosition { get; set; }
|
|||
|
|
|
|||
|
|
public virtual ICollection<User> NetworkChildren { get; set; }
|
|||
|
|
|
|||
|
|
public virtual ClubMembership? ClubMembership { get; set; }
|
|||
|
|
public virtual ICollection<NetworkWeeklyBalance> NetworkWeeklyBalances { get; set; }
|
|||
|
|
public virtual ICollection<UserCommissionPayout> CommissionPayouts { get; set; }
|
|||
|
|
|
|||
|
|
public virtual ICollection<UserClubFeature> UserClubFeatures { get; set; }
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۳.۲ `UserWallet`
|
|||
|
|
```csharp
|
|||
|
|
public class UserWallet : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
// موجودی ریالی اصلی
|
|||
|
|
public long Balance { get; set; }
|
|||
|
|
|
|||
|
|
// موجودی شبکه/کارمزد (کیف پول طلایی)
|
|||
|
|
public long NetworkBalance { get; set; }
|
|||
|
|
|
|||
|
|
// موجودی تخفیف (فقط برای خرید از فروشگاه باشگاه)
|
|||
|
|
public long DiscountBalance { get; set; }
|
|||
|
|
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۳.۳ `Products`
|
|||
|
|
```csharp
|
|||
|
|
public class Product : BaseAuditableEntity
|
|||
|
|
{
|
|||
|
|
// ...
|
|||
|
|
|
|||
|
|
// آیا این محصول فقط در فروشگاه باشگاه موجود است
|
|||
|
|
public bool IsClubExclusive { get; set; }
|
|||
|
|
|
|||
|
|
// درصد تخفیف باشگاه (0 تا 100)
|
|||
|
|
public int ClubDiscountPercent { get; set; }
|
|||
|
|
|
|||
|
|
// ...
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۳.۴ `UserWalletChangeLog`
|
|||
|
|
افزودن نوع جدید تراکنش:
|
|||
|
|
```csharp
|
|||
|
|
public enum TransactionType
|
|||
|
|
{
|
|||
|
|
// ...
|
|||
|
|
|
|||
|
|
NetworkCommission = 10, // دریافت کمیسیون شبکه
|
|||
|
|
ClubActivation = 11, // فعالسازی عضویت باشگاه
|
|||
|
|
DiscountWalletCharge = 12, // شارژ کیف پول تخفیف
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۴. معماری ماژولهای جدید (Application / CQRS)
|
|||
|
|
|
|||
|
|
### ۴.۱ `ClubMembershipCQ/`
|
|||
|
|
#### Commands
|
|||
|
|
- **ActivateClubMembership**: فعالسازی عضویت باشگاه (کسر ۲۵ میلیون و اضافه به استخر)
|
|||
|
|
- **DeactivateClubMembership**: غیرفعالسازی عضویت
|
|||
|
|
- **UpdateClubMembership**: بهروزرسانی اطلاعات عضویت
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
- **GetUserClubStatus**: دریافت وضعیت عضویت کاربر
|
|||
|
|
- **GetAllClubMembersByFilter**: لیست اعضای باشگاه با فیلتر
|
|||
|
|
|
|||
|
|
### ۴.۲ `ClubFeatureCQ/`
|
|||
|
|
#### Commands
|
|||
|
|
- **CreateClubFeature**: ایجاد فیچر جدید
|
|||
|
|
- **UpdateClubFeature**: ویرایش فیچر
|
|||
|
|
- **DeleteClubFeature**: حذف فیچر
|
|||
|
|
- **GrantFeatureToUser**: فعالسازی فیچر برای کاربر
|
|||
|
|
- **RevokeFeatureFromUser**: غیرفعالسازی فیچر از کاربر
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
- **GetAllClubFeatures**: لیست تمام فیچرها
|
|||
|
|
- **GetUserClubFeatures**: لیست فیچرهای فعال یک کاربر
|
|||
|
|
|
|||
|
|
### ۴.۳ `NetworkBalanceCQ/`
|
|||
|
|
#### Commands
|
|||
|
|
- **RecordNetworkJoin**: ثبت ورود کاربر به شبکه باینری (تعیین والد و شاخه)
|
|||
|
|
- حتماً باید یک رکورد در `NetworkMembershipHistory` ایجاد کند.
|
|||
|
|
- **UpdateNetworkPosition**: تغییر موقعیت در شبکه (مدیریتی)
|
|||
|
|
- هر تغییر، یک رکورد History.
|
|||
|
|
- **CalculateWeeklyBalances**: محاسبه تعادلهای هفتگی (فراخوانی از Worker)
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
- **GetUserNetworkTree**: دریافت درخت زیرمجموعههای کاربر (چند سطح)
|
|||
|
|
- **GetUserWeeklyBalances**: دریافت تعادلهای هفتگی یک کاربر
|
|||
|
|
- **GetNetworkStatistics**: آمار کلی شبکه (تعداد اعضا، عمق، تعادل)
|
|||
|
|
|
|||
|
|
### ۴.۴ `CommissionPoolCQ/`
|
|||
|
|
#### Commands
|
|||
|
|
- **InitializeWeeklyPool**: ایجاد استخر جدید برای هفته
|
|||
|
|
- **AddToWeeklyPool**: افزودن مبلغ به استخر هفتگی (هنگام فعالسازی عضویت)
|
|||
|
|
- **CalculatePoolValue**: محاسبه ارزش هر امتیاز
|
|||
|
|
- **DistributeCommissions**: توزیع کمیسیونها به کاربران (Worker)
|
|||
|
|
- **CloseWeeklyPool**: بستن استخر پس از توزیع
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
- **GetCurrentWeekPool**: دریافت اطلاعات استخر هفته جاری
|
|||
|
|
- **GetPoolHistory**: تاریخچه استخرهای قبلی با فیلتر
|
|||
|
|
|
|||
|
|
### ۴.۵ `CommissionPayoutCQ/`
|
|||
|
|
#### Commands
|
|||
|
|
- **CreatePayoutRecord**: ثبت پرداخت کمیسیون (اتوماتیک از Worker)
|
|||
|
|
- همراه با ایجاد رکورد در `CommissionPayoutHistory` (Action = Created).
|
|||
|
|
- **RequestWithdrawal**: درخواست برداشت کمیسیون (نقدی یا الماس)
|
|||
|
|
- History با Action = WithdrawRequested.
|
|||
|
|
- **ProcessWithdrawal**: پردازش درخواست برداشت (تایید/رد ادمین)
|
|||
|
|
- تغییر Status + History.
|
|||
|
|
- **CancelPayout**: لغو پرداخت
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
- **GetUserCommissionHistory**: تاریخچه کمیسیونهای دریافتی کاربر
|
|||
|
|
- **GetPendingWithdrawals**: لیست درخواستهای برداشت در انتظار (برای ادمین)
|
|||
|
|
- **GetCommissionSummary**: خلاصه درآمد کمیسیون (مجموع، ماهانه، سالانه)
|
|||
|
|
|
|||
|
|
### ۴.۶ `ConfigurationCQ/`
|
|||
|
|
#### Commands
|
|||
|
|
- **SetConfigurationValue**: ثبت/ویرایش یک تنظیم (SystemConfiguration)
|
|||
|
|
- هر تغییر باید در `SystemConfigurationHistory` ثبت شود.
|
|||
|
|
- **DeactivateConfiguration**: غیرفعالسازی یک تنظیم
|
|||
|
|
|
|||
|
|
#### Queries
|
|||
|
|
- **GetConfigurationValue**: دریافت مقدار یک Key
|
|||
|
|
- **GetConfigurationByScope**: لیست تنظیمات یک Scope (مثلاً Network)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۵. Background Worker/Job (محاسبات هفتگی)
|
|||
|
|
|
|||
|
|
### ۵.۱ `WeeklyNetworkCommissionWorker`
|
|||
|
|
**زمانبندی**: هر یکشنبه ساعت ۲۳:۵۹ (یا دوشنبه ۰۰:۰۱)
|
|||
|
|
|
|||
|
|
**مراحل اجرایی (High-level):**
|
|||
|
|
|
|||
|
|
#### گام ۱: بستن هفته قبل و ایجاد استخر جدید
|
|||
|
|
```csharp
|
|||
|
|
var currentWeek = GetCurrentWeekNumber(); // مثلاً "2025-W48"
|
|||
|
|
var previousWeek = GetPreviousWeekNumber();
|
|||
|
|
|
|||
|
|
await CloseWeeklyPool(previousWeek);
|
|||
|
|
await InitializeWeeklyPool(currentWeek);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### گام ۲: محاسبه تعادلهای شبکه
|
|||
|
|
```csharp
|
|||
|
|
var maxBalancesPerUser = GetConfig<int>("MaxWeeklyBalancesPerUser", scope: ConfigurationScope.Network);
|
|||
|
|
|
|||
|
|
var activeMembers = await GetActiveClubMembers();
|
|||
|
|
|
|||
|
|
foreach (var member in activeMembers)
|
|||
|
|
{
|
|||
|
|
var leftBalances = await CalculateLegBalances(member.UserId, NetworkLeg.Left, previousWeek);
|
|||
|
|
var rightBalances = await CalculateLegBalances(member.UserId, NetworkLeg.Right, previousWeek);
|
|||
|
|
|
|||
|
|
var totalBalances = Math.Min(leftBalances, rightBalances);
|
|||
|
|
|
|||
|
|
// اعمال محدودیت کانفیگ (مثلاً حداکثر 300 تعادل برای هر کاربر)
|
|||
|
|
if (totalBalances > maxBalancesPerUser)
|
|||
|
|
totalBalances = maxBalancesPerUser;
|
|||
|
|
|
|||
|
|
await RecordWeeklyBalance(new NetworkWeeklyBalance {
|
|||
|
|
UserId = member.UserId,
|
|||
|
|
WeekNumber = previousWeek,
|
|||
|
|
LeftLegBalances = leftBalances,
|
|||
|
|
RightLegBalances = rightBalances,
|
|||
|
|
TotalBalances = totalBalances,
|
|||
|
|
WeeklyPoolContribution = member.InitialContribution,
|
|||
|
|
CalculatedAt = DateTime.UtcNow
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### الگوریتم بازگشتی محاسبه تعادل شاخه
|
|||
|
|
```csharp
|
|||
|
|
private async Task<int> CalculateLegBalances(long userId, NetworkLeg leg, string weekNumber)
|
|||
|
|
{
|
|||
|
|
var children = await GetNetworkChildren(userId, leg);
|
|||
|
|
int totalBalances = 0;
|
|||
|
|
|
|||
|
|
foreach (var child in children)
|
|||
|
|
{
|
|||
|
|
var childMembership = await GetClubMembership(child.Id);
|
|||
|
|
if (childMembership != null && IsInWeek(childMembership.ActivatedAt, weekNumber))
|
|||
|
|
{
|
|||
|
|
totalBalances++;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
var childLeftBalances = await CalculateLegBalances(child.Id, NetworkLeg.Left, weekNumber);
|
|||
|
|
var childRightBalances = await CalculateLegBalances(child.Id, NetworkLeg.Right, weekNumber);
|
|||
|
|
|
|||
|
|
totalBalances += Math.Min(childLeftBalances, childRightBalances);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return totalBalances;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### گام ۳: محاسبه استخر و ارزش امتیاز
|
|||
|
|
```csharp
|
|||
|
|
var totalPoolAmount = await SumPoolContributions(previousWeek);
|
|||
|
|
var totalBalances = await SumTotalBalances(previousWeek);
|
|||
|
|
|
|||
|
|
var valuePerBalance = totalBalances > 0 ? totalPoolAmount / totalBalances : 0;
|
|||
|
|
|
|||
|
|
await UpdatePoolValue(previousWeek, totalPoolAmount, totalBalances, valuePerBalance);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### گام ۴: توزیع کمیسیونها
|
|||
|
|
```csharp
|
|||
|
|
var weeklyBalances = await GetWeeklyBalances(previousWeek);
|
|||
|
|
|
|||
|
|
foreach (var balance in weeklyBalances.Where(b => b.TotalBalances > 0))
|
|||
|
|
{
|
|||
|
|
var payoutAmount = balance.TotalBalances * valuePerBalance;
|
|||
|
|
|
|||
|
|
var payout = new UserCommissionPayout {
|
|||
|
|
UserId = balance.UserId,
|
|||
|
|
WeekNumber = previousWeek,
|
|||
|
|
BalancesEarned = balance.TotalBalances,
|
|||
|
|
ValuePerBalance = valuePerBalance,
|
|||
|
|
TotalAmount = payoutAmount,
|
|||
|
|
Status = CommissionPayoutStatus.Pending
|
|||
|
|
};
|
|||
|
|
await CreatePayoutRecord(payout); // داخلش CommissionPayoutHistory هم ثبت میشود
|
|||
|
|
|
|||
|
|
await AddToNetworkBalance(balance.UserId, payoutAmount);
|
|||
|
|
|
|||
|
|
await RecordWalletChange(new UserWalletChangeLog {
|
|||
|
|
WalletId = balance.UserId,
|
|||
|
|
// PreviousBalance / AfterBalance پر میشود
|
|||
|
|
Amount = payoutAmount,
|
|||
|
|
TransactionType = TransactionType.NetworkCommission,
|
|||
|
|
ReferenceId = payout.Id.ToString()
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
payout.Status = CommissionPayoutStatus.Paid;
|
|||
|
|
payout.PaidAt = DateTime.UtcNow;
|
|||
|
|
await UpdatePayout(payout);
|
|||
|
|
|
|||
|
|
await AddCommissionHistory(payout, "Paid");
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
#### گام ۵: ریست تعادلها
|
|||
|
|
```csharp
|
|||
|
|
await ExpireWeeklyBalances(previousWeek);
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۶. لاجیک فروشگاه و سبد خرید
|
|||
|
|
|
|||
|
|
### ۶.۱ نمایش محصولات
|
|||
|
|
```csharp
|
|||
|
|
var query = _context.Products.Where(p => !p.IsDeleted);
|
|||
|
|
|
|||
|
|
if (!user.ClubMembership?.IsActive ?? true)
|
|||
|
|
{
|
|||
|
|
query = query.Where(p => !p.IsClubExclusive);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// اگر کاربر عضو است، قیمت با تخفیف باشگاه محاسبه میشود
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۶.۲ استفاده از کیف پول تخفیف در Checkout
|
|||
|
|
(خلاصهسازی شده – در کد اصلی از DiscountBalance استفاده میشود و ChangeLog ثبت میگردد.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۷. سناریوی کامل فعالسازی عضویت
|
|||
|
|
|
|||
|
|
### مرحله ۱: شارژ اولیه
|
|||
|
|
```text
|
|||
|
|
کاربر → پرداخت ۵۶ میلیون (دایا/درگاه)
|
|||
|
|
↓
|
|||
|
|
UserWallet.Balance += 56,000,000
|
|||
|
|
UserWallet.DiscountBalance += 56,000,000
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### مرحله ۲: فعالسازی عضویت
|
|||
|
|
```text
|
|||
|
|
کاربر → کلیک روی دکمه «عضویت در باشگاه»
|
|||
|
|
↓
|
|||
|
|
API: ActivateClubMembership
|
|||
|
|
↓
|
|||
|
|
1. ایجاد رکورد ClubMembership:
|
|||
|
|
- IsActive = true
|
|||
|
|
- InitialContribution = 25,000,000
|
|||
|
|
|
|||
|
|
2. افزودن به استخر هفتگی:
|
|||
|
|
- WeeklyCommissionPool.TotalPoolAmount += 25,000,000
|
|||
|
|
|
|||
|
|
3. تعیین موقعیت در شبکه:
|
|||
|
|
- User.NetworkParentId = والد
|
|||
|
|
- User.LegPosition = Left یا Right
|
|||
|
|
|
|||
|
|
4. ثبت ChangeLog برای استخر:
|
|||
|
|
- TransactionType = ClubActivation
|
|||
|
|
|
|||
|
|
5. ثبت ClubMembershipHistory:
|
|||
|
|
- Action = "Activated"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### مرحله ۳: محاسبه هفتگی (Worker)
|
|||
|
|
(مطابق بخش ۵)
|
|||
|
|
|
|||
|
|
### مرحله ۴: برداشت کمیسیون
|
|||
|
|
```text
|
|||
|
|
کاربر → درخواست برداشت
|
|||
|
|
↓
|
|||
|
|
API: RequestWithdrawal (Cash یا Diamond)
|
|||
|
|
↓
|
|||
|
|
ادمین → تایید درخواست
|
|||
|
|
↓
|
|||
|
|
1. اگر Cash:
|
|||
|
|
- واریز به حساب بانکی
|
|||
|
|
- NetworkBalance -= مبلغ
|
|||
|
|
|
|||
|
|
2. اگر Diamond:
|
|||
|
|
- خرید الماس از دایا
|
|||
|
|
- NetworkBalance -= مبلغ
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
همراه با ثبت رکورد در `CommissionPayoutHistory` (Action = WithdrawRequested / Withdrawn).
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۸. پروتوباف و gRPC Services
|
|||
|
|
|
|||
|
|
### ۸.۱ `clubmembership.proto`
|
|||
|
|
```protobuf
|
|||
|
|
syntax = "proto3";
|
|||
|
|
import "google/protobuf/timestamp.proto";
|
|||
|
|
|
|||
|
|
package clubmembership;
|
|||
|
|
|
|||
|
|
service ClubMembershipService {
|
|||
|
|
rpc ActivateMembership (ActivateMembershipRequest) returns (ActivateMembershipResponse);
|
|||
|
|
rpc GetClubStatus (GetClubStatusRequest) returns (GetClubStatusResponse);
|
|||
|
|
rpc GrantFeature (GrantFeatureRequest) returns (GrantFeatureResponse);
|
|||
|
|
rpc GetUserFeatures (GetUserFeaturesRequest) returns (GetUserFeaturesResponse);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message ActivateMembershipRequest {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
int64 contribution_amount = 2;
|
|||
|
|
int64 network_parent_id = 3;
|
|||
|
|
NetworkLeg leg_position = 4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message ActivateMembershipResponse {
|
|||
|
|
bool success = 1;
|
|||
|
|
string message = 2;
|
|||
|
|
ClubMembershipDto membership = 3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetClubStatusRequest {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetClubStatusResponse {
|
|||
|
|
bool is_member = 1;
|
|||
|
|
ClubMembershipDto membership = 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message ClubMembershipDto {
|
|||
|
|
int64 id = 1;
|
|||
|
|
int64 user_id = 2;
|
|||
|
|
bool is_active = 3;
|
|||
|
|
google.protobuf.Timestamp activated_at = 4;
|
|||
|
|
int64 initial_contribution = 5;
|
|||
|
|
int64 total_earned = 6;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enum NetworkLeg {
|
|||
|
|
LEFT = 0;
|
|||
|
|
RIGHT = 1;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۸.۲ `networkbalance.proto`
|
|||
|
|
```protobuf
|
|||
|
|
syntax = "proto3";
|
|||
|
|
|
|||
|
|
package networkbalance;
|
|||
|
|
|
|||
|
|
service NetworkBalanceService {
|
|||
|
|
rpc GetNetworkTree (GetNetworkTreeRequest) returns (GetNetworkTreeResponse);
|
|||
|
|
rpc GetWeeklyBalances (GetWeeklyBalancesRequest) returns (GetWeeklyBalancesResponse);
|
|||
|
|
rpc GetNetworkStats (GetNetworkStatsRequest) returns (GetNetworkStatsResponse);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetNetworkTreeRequest {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
int32 max_depth = 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetNetworkTreeResponse {
|
|||
|
|
NetworkNodeDto root = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message NetworkNodeDto {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
string full_name = 2;
|
|||
|
|
NetworkLeg leg_position = 3;
|
|||
|
|
bool is_active = 4;
|
|||
|
|
repeated NetworkNodeDto children = 5;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetWeeklyBalancesRequest {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
string week_number = 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetWeeklyBalancesResponse {
|
|||
|
|
int32 left_leg_balances = 1;
|
|||
|
|
int32 right_leg_balances = 2;
|
|||
|
|
int32 total_balances = 3;
|
|||
|
|
int64 pool_contribution = 4;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### ۸.۳ `commissionpayout.proto`
|
|||
|
|
```protobuf
|
|||
|
|
syntax = "proto3";
|
|||
|
|
import "google/protobuf/timestamp.proto";
|
|||
|
|
|
|||
|
|
package commissionpayout;
|
|||
|
|
|
|||
|
|
service CommissionPayoutService {
|
|||
|
|
rpc RequestWithdrawal (RequestWithdrawalRequest) returns (RequestWithdrawalResponse);
|
|||
|
|
rpc GetCommissionHistory (GetCommissionHistoryRequest) returns (GetCommissionHistoryResponse);
|
|||
|
|
rpc GetPendingWithdrawals (GetPendingWithdrawalsRequest) returns (GetPendingWithdrawalsResponse);
|
|||
|
|
rpc ProcessWithdrawal (ProcessWithdrawalRequest) returns (ProcessWithdrawalResponse);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message RequestWithdrawalRequest {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
int64 amount = 2;
|
|||
|
|
WithdrawalMethod method = 3;
|
|||
|
|
string iban_number = 4;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message RequestWithdrawalResponse {
|
|||
|
|
bool success = 1;
|
|||
|
|
string message = 2;
|
|||
|
|
int64 request_id = 3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetCommissionHistoryRequest {
|
|||
|
|
int64 user_id = 1;
|
|||
|
|
int32 page_number = 2;
|
|||
|
|
int32 page_size = 3;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message GetCommissionHistoryResponse {
|
|||
|
|
repeated CommissionPayoutDto payouts = 1;
|
|||
|
|
int32 total_count = 2;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
message CommissionPayoutDto {
|
|||
|
|
int64 id = 1;
|
|||
|
|
string week_number = 2;
|
|||
|
|
int32 balances_earned = 3;
|
|||
|
|
int64 value_per_balance = 4;
|
|||
|
|
int64 total_amount = 5;
|
|||
|
|
CommissionPayoutStatus status = 6;
|
|||
|
|
google.protobuf.Timestamp paid_at = 7;
|
|||
|
|
WithdrawalMethod withdrawal_method = 8;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enum WithdrawalMethod {
|
|||
|
|
CASH = 0;
|
|||
|
|
DIAMOND = 1;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
enum CommissionPayoutStatus {
|
|||
|
|
PENDING = 0;
|
|||
|
|
PAID = 1;
|
|||
|
|
WITHDRAW_REQUESTED = 2;
|
|||
|
|
WITHDRAWN = 3;
|
|||
|
|
CANCELLED = 4;
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۹. نکات حیاتی و بهترین رویهها
|
|||
|
|
|
|||
|
|
### ۹.۱ یکپارچگی شبکه باینری
|
|||
|
|
- هر کاربر حداکثر دو فرزند (یکی Left، یکی Right)
|
|||
|
|
- هنگام اضافه کردن فرزند، کنترل Race Condition
|
|||
|
|
- حذف کاربر نباید ساختار شبکه را خراب کند
|
|||
|
|
|
|||
|
|
### ۹.۲ Transaction Management
|
|||
|
|
- Worker باید تمام مراحل را در یک TransactionScope انجام دهد
|
|||
|
|
- در صورت شکست، Rollback کامل
|
|||
|
|
|
|||
|
|
### ۹.۳ Idempotency
|
|||
|
|
- محاسبه هفتگی برای یک WeekNumber فقط یکبار
|
|||
|
|
- بررسی `WeeklyCommissionPool.IsCalculated` قبل از شروع
|
|||
|
|
|
|||
|
|
### ۹.۴ Performance
|
|||
|
|
- Caching درخت شبکه برای کاربران پرحجم
|
|||
|
|
- Index روی `WeekNumber`, `UserId`, `NetworkParentId`
|
|||
|
|
|
|||
|
|
### ۹.۵ Audit و Compliance
|
|||
|
|
- همه تغییرات کیف پول در `UserWalletChangeLog`
|
|||
|
|
- همه پرداختهای کمیسیون در `UserCommissionPayout` + `CommissionPayoutHistory`
|
|||
|
|
- تغییرات شبکه در `NetworkMembershipHistory`
|
|||
|
|
- تغییرات تنظیمات در `SystemConfigurationHistory`
|
|||
|
|
|
|||
|
|
### ۹.۶ Security
|
|||
|
|
- محدودیت تعداد درخواست برداشت
|
|||
|
|
- تایید دو مرحلهای برای برداشتهای بالا
|
|||
|
|
- Audit Log برای عملیات حساس
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۰. مراحل پیادهسازی (Roadmap)
|
|||
|
|
(مطابق نسخه قبلی – فاز ۱ تا ۶)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۱. متریکهای کلیدی (KPIs)
|
|||
|
|
- تعداد اعضای فعال باشگاه
|
|||
|
|
- مجموع کمیسیونهای پرداختی هر ماه
|
|||
|
|
- میانگین تعادل هر کاربر در هفته
|
|||
|
|
- نرخ تبدیل به عضویت باشگاه
|
|||
|
|
- زمان اجرای Worker، تعداد خطاها، عمق درخت، حجم داده History و …
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۲. سوالات متداول (FAQ)
|
|||
|
|
(همان سوالات قبلی + میتوان سوالات مربوط به سقف تعادل و تنظیمات را اضافه کرد.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۳. ضمیمه: مثال عددی کامل
|
|||
|
|
(مثال دو هفتهای A, B, C, D, E, F, G مثل نسخه قبلی.)
|
|||
|
|
|
|||
|
|
---
|
|||
|
|
|
|||
|
|
## ۱۴. مسیرهای مرتبط
|
|||
|
|
- Domain: `CMS/src/CMSMicroservice.Domain/Entities/`
|
|||
|
|
- Application: `CMS/src/CMSMicroservice.Application/ClubMembershipCQ/`, `NetworkBalanceCQ/`, `CommissionPoolCQ/`, `CommissionPayoutCQ/`, `ConfigurationCQ/`
|
|||
|
|
- Protobuf: `CMS/src/CMSMicroservice.Protobuf/Protos/`
|
|||
|
|
- Worker: `CMS/src/CMSMicroservice.Infrastructure/BackgroundJobs/`
|
|||
|
|
- مستند حاضر: `CMS/docs/network-club-commission-system.md`
|
|||
|
|
|
|||
|
|
**نسخه**: 1.1
|
|||
|
|
**تاریخ**: 2025-11-29
|
|||
|
|
**نویسنده**: تیم توسعه CMS
|
|||
|
|
**وضعیت**: آماده پیادهسازی (با History و Config)
|