Add documentation for network club commission system and wallet management

This commit is contained in:
masoodafar-web
2025-11-29 03:33:42 +03:30
parent 8f77097278
commit 6089181bcf
4 changed files with 2985 additions and 0 deletions

View File

@@ -0,0 +1,123 @@
# مستندات داده و بیزینس مایکروسرویس CMS
## معماری و لایه‌ها
- **پشته فنی**: .NET 9 + ASP.NET Core WebAPI، MediatR برای پیاده‌سازی CQRS، EF Core برای دسترسی داده، Mapster برای مپینگ DTO و gRPC/Protobuf برای قرارداد سرویس بین BFF ها و FrontOffice.
- **ساختار پروژه**: لایه‌های Domain (موجودیت و قواعد)، Application (CQRS Commands/Queries، ولیدیشن، DTO)، Infrastructure (EF Core + سرویس‌های جانبی) و WebApi (ورودی HTTP/gRPC) به همراه پروژه مستقل Protobuf جهت به‌اشتراک‌گذاری قراردادها.
- **الگوی کلی**: هر درخواست ورودی از طریق WebApi به MediatR ارسال و Handler مربوطه داده را از DbContext می‌خواند/می‌نویسد. تمام موجودیت‌ها از `BaseAuditableEntity` ارث می‌برند و ستون‌های `Id`, `Created`, `CreatedBy`, `LastModified`, `IsDeleted` را به صورت یکپارچه فراهم می‌کنند.
- **ملاحظات مقیاس‌پذیری**: Handler ها stateless هستند و می‌توانند افقی مقیاس شوند. کنترل تراکنش‌ها توسط EF Core انجام می‌شود و در عملیات چندمرحله‌ای (مثلاً ثبت سفارش) تغییرات داخل یک `TransactionScope` واحد اعمال می‌شود تا سازگاری داده حفظ شود.
- **پایش و ردگیری**: رفتارهای `Common/Behaviours` برای لاگ‌گیری و اعتبارسنجی فعال‌اند و برای هر درخواست یک شناسه ردگیری تولید می‌کنند تا ارتباط بین لاگ BackOffice و FrontOffice حفظ گردد.
## مدل داده
برای فهم بهتر بیزینس، موجودیت‌ها در پنج خوشه اصلی (هویت، کاتالوگ، سفارش، کیف پول، قرارداد) دسته‌بندی شده‌اند و هر خوشه قواعد و قیود مخصوص خود را دارد.
### لایه کاربر و هویت
- **User**: اطلاعات هویتی، وضعیت تایید موبایل، تنظیمات اعلان، کد ارجاع و رابطه والد/فرزند. ارتباط یک‌به‌چند با آدرس‌ها، نقش‌ها، سفارش‌ها، قراردادها، کیف پول و سبد خرید.
- **Role / UserRole**: تعریف نقش‌های سیستمی و نگاشت چند-به-چند کاربر به نقش. جهت کنترل دسترسی BackOffice.
- **OtpToken**: ذخیره توکن‌های OTP با هش کد، هدف (Purpose)، زمان انقضا، تعداد تلاش و وضعیت مصرف برای جریان لاگین/ثبت‌نام.
### لایه محتوا و کاتالوگ محصول
- **Category**: ساختار درختی دسته‌بندی با عنوان، توضیحات، تصویر، ترتیب نمایش و وضعیت فعال بودن. `ParentId` برای تو در تویی و ارتباط با `PruductCategory`.
- **Tag / PruductTag**: برچسب‌های قابل جستجو برای محصولات با وضعیت فعال و ترتیب. جدول واسط `PruductTag` اتصال چند-به-چند محصول و تگ را نگه می‌دارد.
- **Products**: جزئیات کامل محصول شامل توضیحات کوتاه/طولانی، قیمت، تخفیف، نرخ، تصاویر اصلی/Thumbnail، آمار فروش و موجودی. ارتباط با سبد، گالری، فاکتور، دسته و تگ.
- **ProductImages / ProductGallerys**: مدیریت دارایی‌های تصویری. `ProductImages` مشخصات فایل را نگه می‌دارد و `ProductGallerys` رابطه هر تصویر با یک محصول را ثبت می‌کند تا چیدمان گالری قابل کنترل باشد.
- **Package**: باندل یا سرویس قابل فروش با عنوان، توضیح، تصویر و قیمت ثابت که می‌تواند داخل سفارش کاربر قرار گیرد.
- **CategoryProduct Pivot (`PruductCategory`)**: ردیف‌های عضویت محصول در دسته‌های متعدد. هر ردیف شامل `ProductId` و `CategoryId` است.
### لایه سفارش و تراکنش
- **UserCarts**: آیتم‌های سبد خرید کاربر، شامل شناسه محصول، کاربر و تعداد. منبع اصلی عملیات افزودن/حذف سبد در FrontOffice.
- **UserAddress**: آدرس‌های پستی کاربران با عنوان، متن آدرس، کد پستی، شهر، وضعیت پیش‌فرض و ارتباط با سفارش‌ها.
- **UserOrder**: سفارش نهایی شامل مبلغ، ارجاع به پکیج/تراکنش، وضعیت و تاریخ پرداخت، روش پرداخت، وضعیت ارسال، کد رهگیری و توضیحات ارسال. همچنین به آدرس کاربر و آیتم‌های فاکتور (`FactorDetails`) متصل است.
- **FactorDetails**: اقلام درون سفارش؛ هر ردیف به محصول و سفارش اشاره دارد و تعداد، قیمت واحد، تخفیف و وضعیت تغییر قیمت را نگه می‌دارد.
- **Transactions**: لاگ مالی سطح درگاه با مبلغ، توضیح، وضعیت/تاریخ پرداخت، شناسه مرجع درگاه و نوع تراکنش (Persistent در Enum `TransactionType`). سفارش‌ها می‌توانند به یک تراکنش اشاره کنند.
### لایه کیف پول و تسویه
- **UserWallet**: کیف پول ریالی/شبکه‌ای هر کاربر با موجودی جاری و موجودی شبکه (`NetworkBalance`).
- **UserWalletChangeLog**: ژورنال تغییرات کیف پول شامل موجودی قبل/بعد، مقدار تغییر، تغییر شبکه، اینکه افزایش یا کاهش بوده و شناسه مرجع (مثلاً تراکنش یا سفارش). ستون `Created` منبع اصلی timestamp فاکتور کیف پول است.
### لایه قرارداد و رعایت الزامات
- **Contract**: قالب قراردادها با عنوان، توضیحات، متن HTML و نوع قرارداد (`ContractType`).
- **UserContract**: سوابق موافقت کاربر با قراردادها، شامل فایل PDF امضا شده و `SignGuid` برای ردیابی امضا.
## ماژول‌ها و بیزینس مفصل
### کاربران و هویت
- **ثبت‌نام**: با دریافت موبایل، رکورد `User` ساخته و OTP برای تایید ارسال می‌شود. شرط یکتایی موبایل در سطح پایگاه داده enforced است و در Handler نیز بررسی می‌شود.
- **تکمیل پروفایل**: کاربر می‌تواند نام، کد ملی، تاریخ تولد و تنظیمات اعلان را تکمیل کند. فعال‌سازی اعلان‌ها به BFF اطلاع می‌دهد تا Subscription در سرویس پوش ثبت شود.
- **مدیریت نقش**: Admin می‌تواند از API `UserRoleCQ` برای افزودن نقش جدید استفاده کند؛ در صورت حذف نقش، ابتدا باید عضویت‌های فعال کاربر قطع شود.
### کاتالوگ و محتوا
- **دسته‌بندی درختی**: سطح بی‌نهایت تو در تو پشتیبانی می‌شود. حذف یک دسته زمانی مجاز است که هیچ `Categorys` فرزند و هیچ `PruductCategory` فعالی نداشته باشد؛ در غیر این صورت باید انتقال انجام شود.
- **چرخه محصول**: ایجاد محصول شامل ثبت داده متنی، بارگذاری تصویر شاخص، تعریف قیمت و تعیین تخفیف است. تغییر قیمت در Handler ثبت شده و قوانین جلوگیری از عدد منفی یا Discount بزرگ‌تر از 100٪ اعمال می‌شود.
- **گالری و تصاویر**: ابتدا تصویر در `ProductImages` ثبت و سپس با `ProductGallerys` به محصول متصل می‌شود تا یک تصویر بتواند در چند محصول استفاده شود. حذف تصویر اگر در گالری فعال باشد ممنوع است.
- **پکیج‌ها**: برای فروش سرویس اشتراکی یا باندل؛ فیلد `Price` مبنای محاسبه سفارش‌های نوع Package است و تغییر قیمت روی سفارش‌های ثبت‌شده تاثیر ندارد زیرا مبلغ در `UserOrder.Amount` ذخیره می‌شود.
### سفارش، پرداخت و لجستیک
- **سبد خرید**: عملیات Add/Update/Delete روی `UserCarts` انجام می‌شود. در هر لحظه برای ترکیب (User, Product) تنها یک رکورد وجود دارد. اگر Count صفر شود، رکورد حذف منطقی می‌شود تا تاریخچه حفظ گردد.
- **Checkout**: Handler `SubmitShopBuyOrder` اقلام سبد را قفل خوش‌بینانه کرده، سفارش (`UserOrder`) و اقلام فاکتور (`FactorDetails`) را می‌سازد، آدرس پیش‌فرض را نگاشت و وضعیت پرداخت را Pending می‌گذارد.
- **پرداخت آنلاین**: پس از هدایت به درگاه، سیستم CallBack در `TransactionsCQ` را دریافت می‌کند؛ شناسه مرجع (`RefId`) و مبلغ تطبیق داده می‌شود. در صورت موفقیت، `PaymentStatus` سفارش و تراکنش Success شده و `PaymentDate` ذخیره می‌شود. در صورت Reject، سبد به حالت قبل بازگردانده می‌شود.
- **پرداخت با کیف پول**: اگر موجودی کافی باشد، به صورت اتمیک از کیف پول کسر و سفارش Success می‌شود؛ نیازی به تراکنش درگاه نیست.
- **لجستیک**: فیلدهای `DeliveryStatus`, `TrackingCode`, `DeliveryDescription` وضعیت ارسال را پوشش می‌دهند. هر تغییر وضعیت می‌تواند Notification برای کاربر یا تیم پشتیبانی ایجاد کند.
### کیف پول و تسویه داخلی
- **ساخت کیف پول**: همزمان با ثبت‌نام یا اولین تراکنش، رکورد `UserWallet` ساخته می‌شود. موجودی شبکه برای پشتیبانی از دارایی‌های خارج از پلتفرم است.
- **ChangeLog**: هر تغییر موجودی همراه با مقدار قبل/بعد، مقدار شبکه، نوع عملیات (Increase/Decrease) و `ReferenceId` ثبت می‌شود تا audit کافی فراهم گردد. Handler ها Idempotency را با بررسی ReferenceId رعایت می‌کنند.
- **واریز**: می‌تواند از طریق درگاه آنلاین یا عملیات دستی ادمین باشد. پس از تایید بانک، مبلغ به `Balance` افزوده و ChangeLog با نوع Deposit ذخیره می‌شود.
- **برداشت/تسویه**: درخواست Withdrawal ابتدا به صف تایید دستی می‌رود (Business Rule). پس از تایید، مبلغ از `Balance` کم و اگر نیاز به ارسال به شبکه بلاکچین باشد، `NetworkBalance` نیز به‌روزرسانی می‌شود.
- **بازپرداخت سفارش**: در صورت لغو سفارش پرداخت‌شده، مقدار پرداختی با ChangeLog نوع Refund به کیف پول برمی‌گردد تا کاربر بتواند مجدد خرید کند یا برداشت انجام دهد.
### قرارداد و انطباق
- **مدیریت نسخه**: هر بار که متن قرارداد تغییر کند، رکورد جدیدی در `Contract` ساخته می‌شود. `UserContract` با نگه داشتن `ContractId` مشخص می‌کند کاربر کدام نسخه را امضا کرده است.
- **فرآیند امضا**: برای امضای دیجیتال، سیستم `SignGuid` را به سرویس امضای بیرونی ارسال می‌کند. پس از تکمیل، فایل PDF در فضای ذخیره‌سازی آپلود و مسیر آن در `UserContract.SignedPdfFile` ثبت می‌شود.
- **کنترل پذیرش قوانین**: فیلدهای `IsRulesAccepted` و `RulesAcceptedAt` در موجودیت User نیز نگهداری می‌شوند تا بتوان دفعات قبول قوانین عمومی را از قراردادهای اختصاصی تفکیک کرد.
### گزارش و مانیتورینگ
- تمام Queries دارای پارامترهای Paging و Sorting هستند تا BackOffice بتواند داشبورد مدیریتی بسازد.
- به کمک Mapster Projection فقط ستون‌های مورد نیاز خوانده می‌شود؛ در موارد خاص (مثل تاریخ تراکنش کیف پول) Projection دستی به DTO اعمال شده است.
- ساختار CQRS اجازه می‌دهد که در آینده Event Handler یا Outbox برای همگام‌سازی با سرویس‌های دیگر اضافه شود.
## فرایندهای بیزینسی کلیدی
### 1. احراز هویت و ورود
1. کاربر شماره موبایل را ارسال می‌کند؛ `OtpTokenCQ` یک رکورد جدید با کد هش‌شده، زمان انقضا و شمارش تلاش‌ها می‌سازد. درصورت وجود رکورد فعال، ابتدا Attempts چک و درصورت عبور از سقف، خطای تجاری برگردانده می‌شود.
2. کاربر کد را ارسال می‌کند؛ سیستم hash تولید می‌کند و با `CodeHash` مقایسه می‌شود. در صورت موفقیت، `IsUsed` و `IsMobileVerified` تنظیم می‌شوند و تاریخ تایید موبایل ذخیره می‌گردد.
3. اگر کاربر برای اولین‌بار وارد شود، کیف پول و Role پیش‌فرض ایجاد می‌شود. سپس سرویس JWT توکن امضا شده (همراه با Claims نقش‌ها) را برمی‌گرداند.
### 2. مدیریت کاتالوگ و محتوای فروش
- اپراتور BackOffice از طریق دسته‌ها، تگ‌ها و محصولات API های `CategoryCQ`, `ProductsCQ`, `TagCQ` و … اقلام را CRUD می‌کند.
- تصاویر از طریق `ProductImagesCQ` ثبت و سپس با `ProductGallerysCQ` به محصولات لینک می‌شوند تا ترتیب نمایش قابل تغییر باشد.
- باندل‌های اشتراکی یا خدمات از طریق `PackageCQ` تعریف می‌شوند و در سفارش‌ها استفاده می‌شوند.
- قوانین کیفیت داده: عنوان و توضیح محصول نمی‌تواند خالی باشد، تصویر شاخص باید پیش از انتشار محصول مشخص شود و حداقل یک دسته فعال برای محصول الزامی است.
- وضعیت فعال/غیرفعال دسته‌ها در API لیست محصولات اعمال می‌شود تا محصولات دسته غیرفعال نمایش داده نشوند.
### 3. تجربه خرید (Cart → Order → Transaction)
1. FrontOffice اقلام را در `UserCarts` ثبت/ویرایش می‌کند.
2. هنگام تسویه، Handler های `UserOrderCQ` سفارش و اقلام `FactorDetails` را می‌سازند، آدرس پیش‌فرض UserAddress را ضمیمه می‌کنند و وضعیت پرداخت را `Pending` قرار می‌دهند.
3. پس از موفقیت درگاه، سرویس تراکنش (`TransactionsCQ`) شناسه مرجع را ذخیره و `PaymentStatus` سفارش و تراکنش را `Success` می‌کند؛ تاریخ پرداخت نیز ست می‌شود.
4. وضعیت ارسال (`DeliveryStatus`) در طول فرایند Fulfillment آپدیت شده و کد رهگیری پستی داخل سفارش نگه‌داری می‌شود.
- سناریو شکست درگاه: اگر درگاه خطا دهد، سفارش در حالت Pending باقی می‌ماند و Job زمان‌بندی شده این سفارش‌ها را بعد از زمان مشخص لغو می‌کند تا سبد دوباره آزاد شود.
- امکان پرداخت ترکیبی (کیف پول + درگاه) وجود دارد؛ ابتدا از کیف پول برداشت و سپس باقی‌مانده به درگاه ارسال می‌شود.
### 4. کیف پول و صورتحساب داخلی
- هر کاربر دقیقا یک کیف پول فعال دارد (`UserWalletCQ`).
- واریز/برداشت (چه ناشی از پرداخت آنلاین چه عملیات دستی) همیشه یک رکورد در `UserWalletChangeLog` ایجاد می‌کند تا موجودی قبلی، مقدار تغییر و منبع (ReferenceId) مشخص باشد.
- FrontOffice برای نمایش تاریخ دقیق تراکنش‌ها از `Created` لاگ استفاده می‌کند؛ بنابراین Handler های `UserWalletChangeLogCQ` حتما `CreatedAt` را به DTO و gRPC پاسخ اضافه می‌کنند.
- ChangeLog ها قابلیت فیلتر بر اساس نوع عملیات، بازه تاریخی و ReferenceId دارند و مقادیر در DTO به timestamp یونیکس هم تبدیل می‌شود تا فرانت به راحتی فرمت کند.
- عملیات دستی ادمین حتما توضیح (Description) و شناسه اپراتور را ثبت می‌کند تا audit کامل باشد.
### 5. قراردادها و انطباق
- محتوای قرارداد (Term of Service، قرارداد نمایندگی و …) در `Contract` نگه‌داری می‌شود.
- هنگام امضا، یک `UserContract` شامل فایل PDF امضا شده و `SignGuid` ایجاد می‌گردد تا سوابق حقوقی نگهداری شود. این اطلاعات در درخواست‌های بعدی احراز می‌شوند تا از کاربران فقط یکبار امضا گرفته شود.
- در صورت به‌روزرسانی متن قرارداد، کاربران باید مجدداً آن را تایید کنند؛ FrontOffice هنگام ورود این شرط را بررسی و کاربر را به صفحه امضا هدایت می‌کند.
- سیستم گزارش می‌دهد چه تعداد کاربر هر نسخه را امضا کرده‌اند تا تیم حقوقی مطمئن شود پوشش قانونی کامل است.
## نکات پیاده‌سازی و توسعه
- **CQRS پوشه‌بندی**: هر ماژول (مثلاً `UserWalletCQ`) شامل زیرپوشه‌های Commands و Queries است. درخواست‌های gRPC از پروژه Protobuf با DTO های Application نگاشت می‌شوند.
- **همگام‌سازی قراردادها**: هر زمان فیلد جدیدی به موجودیت اضافه شود باید DTO، Handler و قرارداد Protobuf متناظر نیز به‌روزرسانی و `dotnet build` برای تولید مجدد stubs اجرا شود. سپس BFF ها باید پکیج جدید را دریافت کنند.
- **اتصال با BFF**: CMS WebApi سرویس‌های gRPC را در پورت تعریف شده در `appsettings` اکسپوز می‌کند. BFF ها با استفاده از Channel مطمئن (TLS داخلی) به آن متصل می‌شوند و Mapster را برای تبدیل به مدل‌های فرانت استفاده می‌کنند.
- **Dependency Injection**: تمام Handler ها و سرویس‌ها در `CMSMicroservice.Application/ConfigureServices.cs` و `CMSMicroservice.Infrastructure/ConfigureServices.cs` ثبت می‌شوند تا تست‌پذیری افزایش یابد.
- **اعتبارسنجی و لاگ**: Behaviour های مشترک (LoggingBehaviour, ValidationBehaviour) روی Pipeline MediatR نشسته‌اند تا قبل از اجرای Handler، ورودی‌ها چک و لاگ ساختارمند تولید شود.
- **زمان‌بندی تمیزکاری**: ستون `IsDeleted` برای Soft Delete به‌کار می‌رود. Handler هایی که لیست می‌دهند معمولا فیلتر `!IsDeleted` را اعمال می‌کنند؛ برای نمایش آرشیو باید صراحتاً flag درخواست شود.
- **Enums مهم**: `PaymentStatus`, `PaymentMethod`, `DeliveryStatus`, `ContractType`, `TransactionType` طیف وضعیت‌های مالی/قراردادی را استاندارد می‌کنند و باید بین FrontOffice و BackOffice همسو نگه داشته شوند.
- **آیتم‌های Idempotent**: عملیات حساس مثل واریز کیف پول یا ثبت سفارش از ReferenceId استفاده می‌کنند تا در تکرار درخواست‌ها نتیجه‌ی تکراری ایجاد نشود.
## مسیرهای مرتبط
- ساختار کد: `CMS/src/CMSMicroservice.Domain/Entities`, `CMSMicroservice.Application/*CQ`, `CMSMicroservice.Protobuf/Protos`.
- مستند حاضر: `CMS/docs/cms-data-and-business.md`
- نقاط تماس بیرونی: gRPC Endpoint های `CMSMicroservice.WebApi` به صورت داخلی مصرف می‌شوند و از طریق FrontOffice/BackOffice BFF در اختیار UI قرار می‌گیرند.

View File

@@ -0,0 +1,905 @@
# سیستم باشگاه مشتریان و محاسبه کمیسیون شبکه
## خلاصه اجرایی
این سند تحلیل جامع و معماری پیشنهادی برای پیاده‌سازی سیستم باشگاه مشتریان (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)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,22 @@
سیستم کر مرکزی خب سیستم کارگزاری کیف پول داره که کیف پولی که اینا میرن خرید میکنن از دایا برمی‌گردن وامشون واریز میشه این کیف پول شارژ میشه ۵۶ تومان حالا بازار یه فروشگاه داره یه فروشگاه اینترنتی داره که با این ۵۶ تومن که فعلا امتیازی که باید برن حتما از دایا خرید کنن برگردن بعدا قراره خودشون نقدی کیف پولشون رو شارژ کنن یعنی با سلیقه درگاه بیان کیف پولشونو شارژ کنن. تو هر دوتا حالتش از این فروشگاه می‌تونن خرید کنن حالا بعد اینکه کیف پولشون شارژ میشه حالا از طریق دایه‌ها یا از هر طریق دیگه به اون اندازه‌ای که ما متوجه بشیم که این شارژ کیف پول به دلیل عضویت در باشگاه مشتریان بوده
برتری ممکنه طرف بیاد یه میلیون کیف پولشو شارژ کنه اون یه میلیونه مثلا ما یه باشگاه مشتریانم جدا داریم یعنی آره خود باشگاه مشتری که فعال فعال میشه ۱. الان فعلاً در حال حاضر دایا خرید کنی وام بگیری خب وامشو بگیری هم باز باید واسم یه قسمتشو انگار مثلاً یه دکمه باید بزنی اختصاص بده به باشگاه مشتری یا نه دقیقاً یعنی یه دکمه میزنی این اختصاص داده میشه یعنی توی خود کارا بازار یه دکمه‌ای وجود داره میزنی و بعد از اینکه پرداختتون انجام دادی که پولتو شارژ کردی این دکمه رو میزنی و شما عضو باشگاه مشتریان میشی یعنی ما می‌سنجیم ببینیم اینکه تو. پرداختیتو انجام دادی اول بعد باشگاه مشتریان میشی حدوداً ۲۰ ۲۵ میلیونش از این ۵۶ میلیونی که تامین اعتبار میشه
جدا میشه جدا میشه میره تو باشگاه مشتری میره تو باشگاه مشتریان که از اونجا دیگه مدیریت اون محاسبه پورسانته دقیقاً انجام حالا باشگاه مشتریان چی داره باشگاه مشتریان خودش خودش برای خودش به صورت مجزا یه فروشگاه تور داره که تو اون فروشگاهه صرفا یه سری تخفیف وجود داره یعنی متفاوت با این فروشگاه اصلی اون فروشگاه یه سری تخفیف داره. a۵۵ ۳۰ درصد تخفیف این ۳۰ درصد تخفیف تو چجوری میتونی استفاده کنی حالتی که رفته باشی کیف پول اصلی تو کیف پول اصلیتو شارژ کرده باشی حالا از طریق دایه یا نقدی کیف پول اصلیتو شارژ کرده باشی یه ۵۶ تومان که به کیف پول اصلیت واریز میشه
هیچ یه ۵۶ تومان هم به کیف پول تخفیف تو باشگاه مشتریان اضافه میشه که اون گوشی ۵۵ که مثلا ۳۰ درصد تخفیف داره رو ۵۶ تومن واریز میشه ۵۶ تومن واریز میشه. به کیف پول تخفیفت یعنی اون یه ۲۵ میلیون برای باشگاه مشتریانه وقتی باشگاه مشتری فعال می‌کنی ۵۶ میلیون اعتبار تخفیف برات فعال میشه که از اون فروشگاه دوم میتونی خرید کنی ولی چه جوری میتونی خرید کنی فقط همون درصد تخفیف رو میتونی از این ۵۶ تومان استفاده میشه اوکی پس چی شد اگه گوشی مثلا. ۲۰ درصدش تخفیف خورده اون ۲۰% رو می‌تونی از این ۵۶ تومانه استفاده کنی مابقیشو باید نقدی اینجوری میفهمم من باید یه تیبل داشته باشم کسایی که میان
میرن جز باشگاه مشتریان میشن رو اونجا ثبت بکنم یعنی وصل به تیبل یوزرمون بعد اونجا ثبت میشه آها این شخص جز باشگاه مشتری حالا خود باشگاه مشتریان یادته که دکتر گفتش که آقا یه سری لیست داره که اونا فعال میشن فعال شده شماره بیمه چیه یا اگه مثلا فلان چی فعال شده برات این چیه خب مثلا من. تو ذهنم اینجوری بود که خیلی ساده که آپشنای باشگاه مشتریانه اول که میگیم آقا این کاربر جز باشه مشتریان شده است یا خیر ۱ فیلدی که میگه شده است یا خیر یه تیبل دیگه است که میگه آقا این فیچرهایی که از این باشگاه مشتری گرفته کدوماشو گرفته یه تیبل دیگه هست که فیچرها رو اون تو میزنیم باشگاه مشتری داریم آره یه تیبل واسطه مشتریان و یوزر داریم که آقا این یوزر این فیچر براش باز شده با این توضیحات دقیقا اوکی حالا. بعد من علاوه بر این یه کیف پول تخفیف هم باید به کیف به فیلدهای ولتم اضافه کنم یعنی الان یه تیبل ولت دارم یه موجودی شبکه داره یه موجودی خالص داره یه موجودی تخفیف هم باید داشته باشه یعنی سه تا موجودی باید داشته باشه درسته حالا این سه تا موجودی زمانی موجودی تخفیف فعال میشه که کاربر جزو باشگاه مشتریان شده
باشه خب بعد از این فروشگاه یعنی ممکنه محصولاتشم حتی فرق داشته فعال بکنه که آقا من میخوام از. کیف پول تخفیفی بخرم تخفیفا رو نمایش بده اگه نه می‌خوام از تخفیفیم نخرم هادیا رو نمایشگاه باید ایمپلیمنت باشه حالا این پس این باشگاه مشتریان که من میتونم جزئیات باشگاه مشتری خیلی جالبه این فروشگاه رو تو مثلا یه گوشی با یه لپ تاپ میخری گوشی ۲۰ درصد تخفیف داره لپ تاپ ۵۰ درصد تخفیف داره تو اون ۲۰% ۵۰% رو از این کیف پول تخفیفت میتونی استفاده کنی شارژ شده مابقیش هم نقدی میره مستقیم برو نقدی پرداخت کن. ما به صورت هفتگی محاسبه کارمزد داریم یعنی به صورت هفتگی کارم محاسبه می‌کنیم
پلن نتورک این شبکه هم پلن باینره که یه تعادلی ایجاد میشه فقط هم دو نفره دیگه فقط دو نفر بله دو نفر یعنی شما یه دست راست داری یه دست چپ داری بیشتر از اون نداری یعنی سه تا دست و چهار تا دست نداریم ما الان دو تا دست داریم یعنی من. یوزر یه دست راست دارم یه دست چپ دست راستم مثلاً آقای ایکس دست چپم خانم یعنی هیچ چیز اضافه تری نداره ما یه حالا ما توی محاسبه پورسان با کدوم یک از این اعتبارا کار دارم فقط ۵۰ میلیون تومن ۵۶ میلیون تومن تو کیف پول اصلی واریز میشه یه ۵۶ میلیون تومن توی کیف پول تخفیف واریز میشه یه دونه ۲۵ میلیون تومان هم میره توی کارمزد نتورک میره اونجا که بخواد کارمزدش محاسبه بشه.
آخر هفته ما محاسبه میکنیم میگیم مثلا میثم مقدم دو نفر زیر مجموعه داره مثلا ایکس و ایگرگ آقای ایکس و خانم ایگرگ این دو نفر زیر مجموعه هر کدوم اومدن ۵۶ تومان خرید کردن خب خودمم که ۵۶ تومان همون اول خرید کرده بودم یعنی پکیج خریده بودم سرمایه گذاری کرده بودم. این ۵۶ تومان با این ۵۶ تومان میشه حدوداً صد و ۱۱۲ تومن با ۵۶ تومان خودم میشه ۱۶۸ تومن درسته ۱۶۸ تومن توی مخزنمون هست خب ۱۶۸ تومن تو مخزنمون هست حالا بذار من این چیزمو نگاه کنم خب نگاه کن ما به ازای هر تعادلی که ایجاد میشه یک امتیاز به. الان مثلاً من گفتم آقای ایکس و خانم دیگه خب یه تعادل ایجاد کردم درسته یعنی امتیازمون یعنی امتیاز من چنده یه دونه تعادل ایجاد کردم تو هر هفته تعداد تعادل رو محاسبه میکنیم اوکی تعداد تعادل های هر نفر را محاسبه. حالا ده تا تعادل یعنی چی من که یه دونه بیشتر تعادل نمیتونم بزنم اگه من زیر مجموعهم یه تعادل بزنه برای من حساب میشه
بله خب نه نگاه کن الان من زیر مجموعه سمت راستم یه تعادل زده یعنی دو نفرو جذب کرده این میشه خب همین یه طرف هم میشه اگه اون طرف هم تعادل همون دیگه یعنی من هرچقدر سطحم میره پایین تر تعداد تعادل باید ضربدر دو بشه. یعنی من توی لول اول خودم اگه یه دونه دو نفرو جذب بکنم میشه یه تعادل ولی اگه می‌خوام دومین تعادلو داشته باشم بعد سمت راستم یه تعادل یعنی یه دو نفر جذب بکنه سمت چپم یه دو نفر جذب بکنه سمت راست سمت چپت بعد هر کدوم یه دونه جذب بکنه هر کدومشون باید یه تعادل بزنند که برای تو دوتا تعادل حساب بشه
یعنی نگاه کن تو خودت که الان فرض میکنیم تو هفته اول یه اتفاقی افتاده اتفاقی اینه تو خودت دو نفرو جذب کردی یعنی میثم مقدم آقای ایکس و خانم ایگرگ رو جذب کرده آقای ایکس دو نفرو جذب کرده. خانم ایگرگم دو نفرو جذب کرده خب تو دوتا تعادل یه دونه تعادل که خودت زدی چون آقای ایکس خانم ایگرگ رو جذب کردی یه دونه تعادل اینورت زده یه دونه تعادل جمع میشه چند تا تعادل سه تا تعادل تو زدی درست شد نشد دیگه گفتیم دوتا تعادل میشه نه دیگه چرا دوتا تعادل گفتی که آقا من وقتی که توازن برقرار بشه بهش میگیم یه تعادل دیگه خب خب من وقتی که خودم یه دو نفر جذب می کنم میشه
تعادل وقتی زیر مجموعه تعادل جذب میکنه هنوز برای من تعادل نیست چون زیر مجموعه دوم هم باید تعادل بزنه دیگه. تعادل هر کدوم نفری براشون یه تعادل ولی برای تو تعادل اونا که حساب نمیشه برای تو یه تعادل از یه سطح بالاتر حساب میشه دیگه اینجوری نیست مگه نه اونجوری که تو همیشه یه تعادل دوتا تعادل میتونی داشته باشی نه چون دو تا دست داری اینا هر کدوم تعادل تعادل تعادل بزنن یه دونه تعاد. مبلغ کیف پوله مگه شرط نیست اون چیزی که تو صندوق جمع شده مگه شرط نیست نه به اون کاری نداریم الان تعداد تعادل چگونه محاسبه میشود چه جوری ما حساب میکنیم تو چند تا تعادل زدی تو یه دستت یه تعادل بزنه یه دسته دیگه هم یه تعادل تو دو تا تعادل زدی متوجه شدی تو تونستی دوتا دوتا جذب کنی خب دو تا تعادل حالا بگذریم از همون خیلی ساده‌شو
بگیریم من میثم مقدم دو نفرو جذب کردم آقای ایگرگ خانم ایکس درسته. امتیاز تو شد ۱ به تعداد تعادل مساوی با امتیاز یعنی تعداد تعادل مساوی است با امتیاز تعداد تعادل هر شخص مساوی است با امتیاز اون شخص حالا هرچی که مبلغ توی صندوق جمع شده یعنی من خودم ۵۶ تومن دادم دست راستم ۵۶ تومن داده دست داده درسته البته که اینا که دارم میگم اشتباهه. ۵۶ تومنه یکیش واسه کیف پول تخفیفه یکیش واسه کیف پول اصلیه ما اینجا ۲۵ تومان داریم دست خودم ۲۵ تومان آوردم تو باشگاه مشتریان دست راستم ۲۵ تومان آورده دست چپم ۲۵ تومان آورده جمعاً میشه ۷۵ تومان یعنی ۷۵ میلیون تومن تو صندوق جمع شده
درسته من چه امتیازی دارم ۱ درسته دست راستم چه امتیازی داره صفر دست چپم چه امتیازی داره صفر درسته ما با اونا کار نداریم الان مبلغ پورسانت من چی میشه من یک امتیاز دارم اون ۷۵ تومن تقسیم بر یک. اون دوتا که صفر بودن دیگه اگه اون دوتا نفر یک بودن میشد مثلا تقسیم بر سه خب میشه مبلغ ریالی هر امتیاز یعنی مجموع کل امتیازهایی که همه کاربرها جمع کردن و مجموعه کل امتیازها اینا رو یه دست نگهدار این عددی که تو صندوق جمع شده تقسیم بر مجموعه کل امتیازها یعنی عددی که تو صندوق جمع شده تقسیم بر کل تعداد تعادل‌های این هفته مساوی است با مبلغ ریالی هر امتیاز حالا تو چند امتیاز داشتم ۷۵ میلیون تقسیم بر ۱. یعنی مبلغ ریالی هر امتیاز میشه ۷۵ میلیون درسته حالا من چند امتیاز داشتم ۱ پس ۷۵ میلیون ضربدر یک میشه
یعنی ۷۵ میلیون تومان باید کارمزد بگیرم یه لول میاد پایین تر خب من اگر این هفته جدید تعادل جدیدی ثبت نکنم که دیگه برام تعادل حساب نمیشه یعنی من وقتی تعادل زدم پولشم گرفتم دیگه اون تعادل پاک میشه اون تعادل دیگه پاک میشه دیگه برای تو تعادل جدید حساب نمیشه خب. حالا من توی شبکه هم دست چپ و راستم رفتی یه لول پایین تر اونا هم یه دونه مثلاً شده هفته بعد اونا هم یه تعادل دیگه زدن برای من دوتا تعادل حساب میشه برای خودشون چند تا هر کدوم نفری یه دونه درسته هفته اول دیگه چون خود من دو نفر جذب کردم میشه ۱ درسته اونا هر کدوم دو نفر جذب کردن ۱ ۱ برای من میشه سه. هفته اوله حالا شده ۵ هرچی که تو صندوق از اون ۲۵ میلیون ۲۵ میلیون جدید درسته یعنی اونایی که دیگه همش هفته اول همش جدیده دیگه ثبت شده
تقسیم میشه بین اون امتیازها حالا کی چقدر امتیاز داره همون پول میگیره درسته چه اتفاقی افتاده من ۲۵ میلیون دست راستم ۲۵ میلیون ۷۵. هر کدوم از اونا نفری دو نفرو جذب کردن که دو تا ۲۵ میلیون اونور ۵۰ ۵۰ ۱۰۰ میلیون ۱۰۰ میلیون با ۷۵ میلیون میشه ۱۷۵ میلیون ۱۷۵ میلیون تقسیم بر ۵ میشه حدوداً ۳۵ میلیون یعنی ۳۵ میلیون ارزش ریالی هر امتیازه بعد حالا هر کی چقدر امتیاز داره همونقدر بهش تعلق می‌گیره من چقدر امتیاز دارم ۳ امتیاز دارم ۳۵ میلیون ضربدر ۳ ۳ تا ۳۵ میلیون هم باید بگیرم یه دونه ۳۵ میلیون دست راستم باید بگیره یه ۳۵ میلیون دست چپم باید بگیره خب من مثلا میتونم یه تیبل داشته باشم خب که. هر کسی هر هفته‌ای که تعادل میزنه خب اونو اونجا ثبت بشه
تعداد تعادل‌های هر شخص توی هر هفته باید ثبت بشه خب تعداد تعادل‌های هر شخص تو هر هفته باید ثبت بشه یعنی اگه اون مثلاً من زیر مجموعه‌هام هزار تا ۲۰۰۰ نفر بشه اون پایینم یه نفر یه تعادل بزنه برای من یه تعادل ثبت میشه حالا اگه یه دستم یه تعادل بزنه بازم برای من یه تعادل ثبت میشه یعنی من نباید تلاش کنم چرا دست دوم باید همونقدر تعادل بزنه یعنی اگه مساوی بزنن تعادل حساب میشه. هفته اولم باشه فقط آقای ایکس یه تعادل بزنه من برای خودش تعادل حساب میشه پس من باید توازن داشته باشم دیگه باز خب اگر توازن داشته باشم یعنی مثلا من حالا مثلا یه لول رفته
جلوتر سه تا تعادل این دستم زده دو تا تعادل این دستم زده برای من ۲ حساب میشه دو اینور دو این ور میشه چهار یعنی من هر موقعی که یه تعادلی شکل میگیره باید برم دست مقابل اونم نگاه کنم ببینم تعادلی وجود داره تازه میشه یه تعاد. تعادل بعدی اگه اونور وجود داشت که هیچی اگر وجود نداشت اگه وجود داشت که خب دیگه تعادله اگه وجود نداشتم که هیچی این دست نگاه کنم ببینم که مثلاً این دست که حالت تعادل زده این دستش یه تعادل داره در هر صورت بخوام یه فرمول کلی بگم تو دست چپت تو اعماق اصلا ده لول ۱۵ رفته پایین این نتورک تا لول ۱۵ رفته
پایین دست چپت اون پایین مایا چهار تا تعادل میزنه دست راستتم حداقل باید چهار تا تعادل بزنه تا بره تو یه چیزی محاسبه بشه یعنی اگه دست. چپ تو خوب دوتا تعادل زده دست راستت چهار تا تعادل زده دو تا تعادل واسه تو حساب میشه دوتا اینور دوتا اونور جمع میشه چهار تا اگه دست راستتو پنج تا تعادل زده دست چپتو هیچ تعادلی نزده پس در نتیجه هیچ تعادلی واسه تو حساب نمیشه اگه دست راستتو دو تا تعادل زده دست چپتم دو تا تعادل زده دقیقا حالا با همدیگه مساوی چهار تا تعادل اگه دست راست تو ده تا تعادل زده ۱۰۰ تا تعادل زده ولی دست چپت دوتا تعادل زده کلاً دو تا تعادل حساب میشه دو تا راست دو تا چپ میشه
چهار تا. تعادل یه نفر حساب کنی این شکلی باید حساب کنیم خب من الان مثلا اون تیبلی که میزارم باید چه شکلی باشه یعنی همون لحظه که یه نفر ثبت نام میکنه من کسی که عضو باشگاه مشتریان میشه تو یه جا ثبت کن که آقا این نفر عضو باشگاه مشتریان شد حالا آخر هفته محاسبه می‌کنی اون نفری که عضو باشگاه مشتری اینا شده والدش کی بوده والدش کی بوده والد والت همینجوری تا آخر آیا تعادل خورده است یا خیر یعنی تو هفتگی باید حساب کنی تو این هفته ورودی های این هفته رو باید حساب کنی. خب من نمی‌تونم مثلاً وقتی که یه نفر جزو باشگاه مشتریان میشه
همون لحظه تعادل همه بالا سریاشو حساب کنم نه شاید تعادل بیشتر بزنه خب باشه وقتی بیشتر زد دوباره افزایش نمی‌دونم شاید بشه بعد اینو حساب کتاب کنی بعد با دکترم جلسه بذاری که ببینی دقیقاً این چه جوریه مثلا هفته پیش یه نفر یه تعادل زده این هفته کلاً پوچ میشه تعادلاش چون من تا جایی که یادمه باید سعی کنه طرف تو هفته دو تا تعادل این دستشو بزنه وگرنه پوچ میشه یعنی از دست دادتش. حله و در مجموع پس هر کدوم من میگم اون تیبلی که دارم حتما باید یه چیزی تحت عنوان امتیاز باشه اگه همون تعداد تعادل خب بعد عددی که جمع میشه هم یه جا باید من یه جا نگهش دارم عددی که تو این هفته جمع میشه
تعداد تعادل این هفته و مبلغی که تو این هفته تو باشگاه مشتریان جمع شده حالا این تقسیم برای امتیاز هرکی به نسبت امتیازی که داره یه مبلغی براش ثبت میشه که اون مبلغ در نهایت میره تو کیف پول شبکه یا کیف پول کارمزد اصلا کیف پول نذاریم بذاریم کارمزد کمیسیون. یه چیزی باید باشه ولی یه مخزنی هست دیگه یه جایی هستش که تو هر هفته مبلغی که با استفاده از اون پلن شبکت دریافت کردی میره اونجا واریز میشه حالا این مبلغی که توی کیف پول شبکه یا کیف پول کارمزد هست یا کیف پول طلایی اسمشو بذاریم چون اسم این امتیازها امتیازهای طلاییه اسم اون کیف پوله رو بذاریم کیف پول طلایی چون سه تا کیف پول شد یک کیف پول اصلی که تو میتونی بری از فروشگاه بازار خرید کنی مستقیمه دو کیف پول تخفیف که تو میتونی بری از فروشگاه که بعد از باش
مشتریان این اتفاق. یکی هم کیف پول طلاییت یا همون کیف پول کارمزدت این میشه سه تا کیف پول حالا کیف پول کارمزد چه جوری میتونی برداشت کنی دو طریق داره یک نقدی برداشت کنید یعنی شماره شبا بدیم و نقدی برات پرداخت کنیم ۲ بری از دایا الماس بخری حالا یه چیزی من الان ۵۶ میلیون تومنو یعنی ما الماس بهت بدیم اوکی ما الان ۵۶ میلیون تومنو آوردیم توی کیف پول که میتونه بره خرید بکنه اگه باشگاه مشتری اینو بزنیم ۲۵ میلیون ازش کم میشه دیگه کم میشه دیگه. میلیون تومن توی باشگاه مشتریان شارژ میشه جدای از این یعنی میشه چی میشه یه ۵۶ میلیون تومن توی کیف پول اصلی یعنی ۵۶ میلیون تومن تو کیف پول ۲۵ میلیون تومان توی خود باشگاه اوکی حالا بذارید تحلیل بکنم ببینم چی میتونم در بیارم.