docs: add session log for 2026-05-13 (guest browsing + top-seller landing sections)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
masoodafar-web
2026-05-13 19:50:15 +03:30
parent 1db77b1a1b
commit 4218d08597
+303
View File
@@ -0,0 +1,303 @@
# 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<bool> RunAsync(Func<Task> 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<GuestActionGate>();
```
### ۲. `ProductService.cs`
```csharp
public Task<ProductListResult> GetTopSellingAsync(int count = 6)
=> GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count);
```
### ۳. `DiscountProductService.cs`
```csharp
// پارامتر جدید به GetProductsAsync اضافه شد
public async Task<DiscountProductListResult> GetProductsAsync(
..., string? sortBy = null)
{
if (!string.IsNullOrWhiteSpace(sortBy))
request.SortBy = sortBy;
...
}
public Task<DiscountProductListResult> 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)
```