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

10 KiB
Raw Blame History

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)
  3. تغییرات FrontOffice
  4. معماری GuestActionGate
  5. فلوچارت تجربه کاربر
  6. فایل‌های تغییر یافته

هدف و خلاصه

هدف این session:

  1. نمایش ۶ محصول پرفروش معمولی + ۶ محصول پرفروش فروشگاه اعتباری در لندینگ پیج FrontOffice، زیر هدر اصلی (۳ محصول در هر ردیف، دو section مجزا).
  2. دسترسی guest (کاربر بدون لاگین) به مرور محصولات برای پرزنت به مشتریان بالقوه.
  3. Hybrid auth flow: کاربر guest محصولات را می‌بیند؛ اگر روی "افزودن به سبد" کلیک کرد، مودال لاگین باز می‌شود و پس از ورود موفق، عمل به صورت خودکار انجام می‌شود.

تغییرات CMS (Backend)

۱. discountproduct.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

public string? SortBy { get; set; }

۴. GetDiscountProductsQueryHandler.cs

// قبل: همیشه 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)

// 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 می‌کند:

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 ثبت شد:

services.AddScoped<GuestActionGate>();

۲. ProductService.cs

public Task<ProductListResult> GetTopSellingAsync(int count = 6)
    => GetProductsPagedAsync(sortBy: "SaleCount desc", page: 1, pageSize: count);

۳. DiscountProductService.cs

// پارامتر جدید به 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):

var topRegTask  = ProductService.GetTopSellingAsync(6);
var topDiscTask = DiscountProductService.GetTopSellingAsync(6);
var featuredPostsTask = BlogPostService.GetFeaturedPostsAsync(2);
await Task.WhenAll(topRegTask, topDiscTask, featuredPostsTask);

Cart actions با GuestActionGate:

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 AddToCartGuestGate.RunAsync(...)
Store/ProductDetail.razor.cs AddToCart و RemoveFromCartGuestGate.RunAsync(...)
DiscountStore/Products.razor.cs AddToCartGuestGate.RunAsync(...)
DiscountStore/ProductDetail.razor.cs AddToCartGuestGate.RunAsync(...)

صفحات Cart و Checkout (Soft Auth Gate):

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 متنی)

- شارژ ۵۶ میلیون تومان کیف پول فروشگاه اعتباری
+ شارژ برابر ارزش پکیج فعال در کیف پول فروشگاه اعتباری

متن 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)