4218d08597
Co-authored-by: Cursor <cursoragent@cursor.com>
304 lines
10 KiB
Markdown
304 lines
10 KiB
Markdown
# 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)
|
||
```
|