Files
docs/technical/TECH-08-SESSION-LOG-20260513.md

304 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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)
```