# TECH-08 — لاگ Session 1404/02/23 (2026-05-13) > نوع سند: **گزارش کار** > تاریخ: ۱۴۰۵/۰۲/۲۳ > مرتبط با: CMS · FrontOffice > کامیت CMS: `683ed37` (branch: `kub-stage`) > کامیت FrontOffice: `231da2c` (branch: `kub-stage`) --- ## فهرست مطالب 1. [هدف و خلاصه](#هدف-و-خلاصه) 2. [تغییرات CMS (Backend)](#تغییرات-cms-backend) 3. [تغییرات FrontOffice](#تغییرات-frontoffice) 4. [معماری GuestActionGate](#معماری-guestactiongate) 5. [فلوچارت تجربه کاربر](#فلوچارت-تجربه-کاربر) 6. [فایل‌های تغییر یافته](#فایلهای-تغییر-یافته) --- ## هدف و خلاصه هدف این session: 1. **نمایش ۶ محصول پرفروش معمولی + ۶ محصول پرفروش فروشگاه اعتباری** در لندینگ پیج FrontOffice، زیر هدر اصلی (۳ محصول در هر ردیف، دو section مجزا). 2. **دسترسی guest** (کاربر بدون لاگین) به مرور محصولات برای پرزنت به مشتریان بالقوه. 3. **Hybrid auth flow**: کاربر guest محصولات را می‌بیند؛ اگر روی "افزودن به سبد" کلیک کرد، مودال لاگین باز می‌شود و پس از ورود موفق، عمل به صورت خودکار انجام می‌شود. --- ## تغییرات CMS (Backend) ### ۱. `discountproduct.proto` ```proto // اضافه شده به GetDiscountProductsRequest google.protobuf.StringValue sort_by = 9; // اضافه شده به DiscountProductDto int32 sale_count = 12; ``` **چرا:** برای واکشی پرفروش‌ترین محصولات فروشگاه اعتباری باید امکان sort بر اساس `sale_count` وجود داشته باشد. قبلاً این فیلد در DTO برگردانده نمی‌شد. ### ۲. `CMSMicroservice.Protobuf.csproj` نسخه از `0.0.195` به `0.0.196` بالا رفت تا پکیج NuGet جدید publish شود. ### ۳. `GetDiscountProductsQuery.cs` ```csharp public string? SortBy { get; set; } ``` ### ۴. `GetDiscountProductsQueryHandler.cs` ```csharp // قبل: همیشه OrderByDescending(p => p.Created) // بعد: dynamic sort با fallback if (!string.IsNullOrEmpty(request.SortBy)) query = query.ApplyOrder(request.SortBy); else query = query.OrderByDescending(p => p.Created); // و در SELECT: SaleCount = p.SaleCount, ``` از extension method موجود `ApplyOrder` (کتابخانه `System.Linq.Dynamic.Core`) استفاده شد تا نیازی به تغییر جداگانه نباشد. ### ۵. `DiscountProductProfile.cs` (Mapster) ```csharp // Request mapping .Map(dest => dest.SortBy, src => string.IsNullOrEmpty(src.SortBy) ? null : src.SortBy) // Response mapping SaleCount = p.SaleCount, ``` --- ## تغییرات FrontOffice ### ۱. `GuestActionGate.cs` (فایل جدید) ``` FrontOffice.Main/Utilities/GuestActionGate.cs ``` سرویس utility جدید که هر action نیازمند لاگین را wrap می‌کند: ```csharp public async Task RunAsync(Func action) { if (await _authService.IsAuthenticatedAsync()) { await action(); return true; } await _authDialogService.ShowAuthDialogAsync(); if (await _authService.IsAuthenticatedAsync()) { await action(); return true; } return false; } ``` در `ConfigureServices.cs` به صورت Scoped ثبت شد: ```csharp services.AddScoped(); ``` ### ۲. `ProductService.cs` ```csharp public Task GetTopSellingAsync(int count = 6) => GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count); ``` ### ۳. `DiscountProductService.cs` ```csharp // پارامتر جدید به GetProductsAsync اضافه شد public async Task GetProductsAsync( ..., string? sortBy = null) { if (!string.IsNullOrWhiteSpace(sortBy)) request.SortBy = sortBy; ... } public Task GetTopSellingAsync(int count = 6) => GetProductsAsync(page: 1, pageSize: count, sortBy: "SaleCount desc"); ``` ### ۴. `Index.razor` و `Index.razor.cs` دو section جدید در لندینگ پیج زیر hero اضافه شد: **Section 1 — محصولات پرفروش معمولی:** - عنوان: "محصولات پرفروش" - ۶ کارت (۳ در هر ردیف با MudGrid) - هر کارت: تصویر، نام، قیمت با VAT، دکمه "افزودن به سبد" - دکمه "بیشتر" → `/products` **Section 2 — محصولات پرفروش فروشگاه اعتباری:** - عنوان: "فروشگاه اعتباری" - ۶ کارت (۳ در هر ردیف) - هر کارت: تصویر، نام، قیمت، درصد تخفیف - دکمه "بیشتر" → `/discount-store` **Loading state:** در حین بارگذاری یک spinner نشان داده می‌شود و سپس section‌ها fade-in می‌شوند. **Data loading (parallel):** ```csharp var topRegTask = ProductService.GetTopSellingAsync(6); var topDiscTask = DiscountProductService.GetTopSellingAsync(6); var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2); await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask); ``` **Cart actions با GuestActionGate:** ```csharp private async Task AddRegularToCart(Product p) => await GuestGate.RunAsync(() => Cart.Add(p, 1)); private async Task AddDiscountToCart(DiscountProductCard p) => await GuestGate.RunAsync(() => DiscountCart.AddAsync(p.Id)); ``` ### ۵. Hybridize کردن صفحات موجود #### صفحات لیست و جزئیات محصول (GuestActionGate): | فایل | تغییر | |------|-------| | `Store/Products.razor.cs` | `AddToCart` → `GuestGate.RunAsync(...)` | | `Store/ProductDetail.razor.cs` | `AddToCart` و `RemoveFromCart` → `GuestGate.RunAsync(...)` | | `DiscountStore/Products.razor.cs` | `AddToCart` → `GuestGate.RunAsync(...)` | | `DiscountStore/ProductDetail.razor.cs` | `AddToCart` → `GuestGate.RunAsync(...)` | #### صفحات Cart و Checkout (Soft Auth Gate): ```csharp protected override async Task OnInitializedAsync() { if (!await AuthService.IsAuthenticatedAsync()) { await AuthDialogService.ShowAuthDialogAsync(); } // ادامه بارگذاری... } ``` این pattern روی: - `Store/Cart.razor.cs` - `Store/CheckoutSummary.razor.cs` - `DiscountStore/Cart.razor.cs` - `DiscountStore/Checkout.razor.cs` اعمال شد. اگر guest مستقیماً وارد سبد خرید شود، مودال لاگین نشان داده می‌شود. ### ۶. `MembershipPage.razor` (fix متنی) ```diff - شارژ ۵۶ میلیون تومان کیف پول فروشگاه اعتباری + شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری ``` متن hardcode‌شده با مقدار دینامیک جایگزین شد. --- ## معماری GuestActionGate ``` کاربر کلیک می‌کند │ ▼ GuestActionGate.RunAsync(action) │ ├─► آیا لاگین است؟ ──YES──► action() اجرا می‌شود ✅ │ NO │ ▼ AuthDialogService.ShowAuthDialogAsync() (مودال OTP باز می‌شود) │ ├─► آیا لاگین شد؟ ──YES──► action() اجرا می‌شود ✅ │ NO (بستن مودال) │ ▼ return false (هیچ اتفاقی نمی‌افتد) ❌ ``` این pattern **defense-in-depth** است: `CartService.Add` هم به تنهایی چک `IsAuthenticatedAsync` دارد؛ `GuestActionGate` لایه UX روی آن اضافه می‌کند. --- ## فلوچارت تجربه کاربر ``` کاربر وارد لندینگ پیج می‌شود (بدون لاگین) │ ├─► ۶ محصول پرفروش معمولی نمایش داده می‌شود ├─► ۶ محصول پرفروش اعتباری نمایش داده می‌شود │ ├─► "بیشتر" کلیک → /products یا /discount-store │ (صفحات لیست کامل، بدون لاگین قابل مرور) │ ├─► روی محصول کلیک → صفحه جزئیات │ (بدون لاگین قابل مشاهده) │ └─► "افزودن به سبد" کلیک │ ▼ مودال لاگین (OTP) │ ├─► ورود موفق → محصول به سبد اضافه می‌شود ✅ └─► بستن مودال → هیچ اتفاقی نمی‌افتد ``` --- ## فایل‌های تغییر یافته ### CMS — کامیت `683ed37` ``` src/CMSMicroservice.Protobuf/Protos/discountproduct.proto (+2) src/CMSMicroservice.Protobuf/CMSMicroservice.Protobuf.csproj (~2) src/CMSMicroservice.Application/DiscountShopCQ/Queries/ GetDiscountProducts/GetDiscountProductsQuery.cs (+1) GetDiscountProducts/GetDiscountProductsQueryHandler.cs (+7 -3) src/CMSMicroservice.WebApi/Common/Mappings/DiscountProductProfile.cs (+3) ``` ### FrontOffice — کامیت `231da2c` ``` src/FrontOffice.Main/Utilities/GuestActionGate.cs (NEW +42) src/FrontOffice.Main/ConfigureServices.cs (+1) src/FrontOffice.Main/Utilities/ProductService.cs (+3) src/FrontOffice.Main/Utilities/DiscountProductService.cs (+8) src/FrontOffice.Main/Pages/Index.razor (+~180) src/FrontOffice.Main/Pages/Index.razor.cs (+45) src/FrontOffice.Main/Pages/Store/Products.razor.cs (+5) src/FrontOffice.Main/Pages/Store/ProductDetail.razor.cs (+5) src/FrontOffice.Main/Pages/Store/Cart.razor.cs (+8) src/FrontOffice.Main/Pages/Store/CheckoutSummary.razor.cs (+8) src/FrontOffice.Main/Pages/DiscountStore/Products.razor.cs (+5) src/FrontOffice.Main/Pages/DiscountStore/ProductDetail.razor.cs (+5) src/FrontOffice.Main/Pages/DiscountStore/Cart.razor.cs (+8) src/FrontOffice.Main/Pages/DiscountStore/Checkout.razor.cs (+8) src/FrontOffice.Main/Pages/Club/MembershipPage.razor (~1) ```