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:
@@ -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)
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user