First commit of the Hedena Gold product (separate from gold-bank): branding, package names, and deploy defaults aligned to the new brand; legacy Henza trees stay local-only via .gitignore.
11 KiB
طرح — صفحهبندی واقعی کاتالوگ (۱۶ هزار رکورد)
تاریخ: ۱۴۰۵/۰۷/۲۰ (2026-10-12) — طرح؛ منتظر تأیید
مالک: «دیتاها در اپ واقعی تا ۱۶ هزار رکورد میرود و گرفتن همه یکجا اشتباه است، حتماً باید پیجینیشن داشته باشد.»
موافقم. با این عدد، وضعیت فعلی قابل دفاع نیست. این سند میگوید دقیقاً چه چیزی
شکسته است، چرا کار از «اضافه کردن take/skip» بزرگتر است، و با چه ترتیبی
بیریسک انجام میشود.
۱. وضعیت امروز (بررسیشده در کد، نه حدس)
| لایه | واقعیت | مدرک |
|---|---|---|
| مسیر API | GET /catalog/products هیچ پارامتری نمیپذیرد |
catalog.controller.ts:10-12 |
| کوئری | یک findMany بدون take/skip، همراه variants+images+category+brand+collections |
catalog.service.ts:19-42 |
| کش سرور | کل خروجی، ۳۰ ثانیه در حافظه، با هر نوشتن باطل | catalog-cache.service.ts |
| «صفحهبندی» اپ | PAGE_SIZE = 20 فقط یک پنجره رندر روی آرایهی کامل است: slice(0, visibleCount) |
SearchView.vue:204-206، NewestView.vue:55-57 |
| در هر اسکرول | صفر درخواست، ۲۰ کارت بیشتر رندر میشود | همان |
ایندکسها — یافتهی مهم: مدل Product هیچ ایندکسی ندارد (نه
isActive، نه categoryId، نه brandId، نه createdAt) و ProductVariant
ایندکسی روی productId ندارد. با ۱۶ هزار رکورد، صفحهبندی بدون ایندکس همان
کندی را میدهد؛ پس ایندکس بخشی از این کار است، نه یک بهبود جداگانه.
۲. چرا کار از «اضافه کردن take/skip» بزرگتر است
هفت مصرفکننده روی فرض «کل کاتالوگ در حافظه است» نوشته شدهاند. سهتا بحرانیاند — هر سه کل لیست را میگیرند تا یک چیز پیدا کنند:
| جا | الان چه میکند | با ۱۶ هزار رکورد |
|---|---|---|
useProductDetail |
کل کاتالوگ را میگیرد تا یک محصول را find کند |
بازکردن هر محصول = دانلود کل کاتالوگ |
cartStore.init |
کل کاتالوگ برای همگامسازی ۲-۳ قلم سبد | هر بار باز شدن اپ |
configStore.init |
کل کاتالوگ فقط برای لیست دستهها | صفحه خانه |
SearchView |
متن/دسته/برند/بازه وزن را روی کلاینت فیلتر میکند | فیلتر روی دادهی ناقص = نتیجه غلط |
Filters.vue |
گزینههای فیلتر را از کل لیست مشتق میکند | گزینهها ناقص میشوند |
FavoritesView |
علاقهمندیها را با find در کل لیست پیدا میکند |
علاقهمندیِ خارج از صفحه دیده نمیشود |
useOrderItemProduct |
عنوان محصولِ آیتم سفارش را از کل لیست میخواند | سفارش قدیمی بیعنوان میشود |
نتیجه: اگر فقط take/skip به مسیر اضافه شود، جستجو و فیلتر بیصدا غلط
میشوند (روی ۲۰ رکورد اول فیلتر میکنند) و این بدتر از کندی است. پس هر
مصرفکننده باید مسیر مخصوص خودش را بگیرد.
۳. طراحی پیشنهادی
(الف) چهار مسیر، بهجای یک مسیر
| مسیر | برای چه | چرا جدا |
|---|---|---|
GET /catalog/products?page=&limit=&search=&categoryId=&brandId=&minGram=&maxGram=&sort= |
لیست و جستجو | فیلتر و صفحهبندی سمت سرور؛ تنها راه درست بودن نتیجه |
GET /catalog/products/:id |
صفحه محصول | یک رکورد، نه ۱۶ هزار |
GET /catalog/facets |
گزینههای فیلتر (دستهها، برندها، بازه وزن) | کوچک، distinct/aggregate؛ جایگزین مشتقکردن از کل لیست |
POST /catalog/products/by-ids |
سبد، علاقهمندی، آیتمهای سفارش | چند شناسه مشخص در یک درخواست |
شکل پاسخ لیست: { rows, total, page, limit }. عمداً از TableService
استفاده نمیکنم: آن برای قرارداد XTable پنل ادمین است و شکل پاسخش
({rows, query:{...}}) برای اپ بیمورد پیچیده است.
(ب) سازگاری عقبرو — مهم
مسیر فعلی نباید بشکند: پنل ادمین، اسکریپتهای smoke و نسخههای نصبشدهی اپ از آن استفاده میکنند.
- بدون
page/limit⇒ رفتار امروز (کل لیست). پس هیچ مصرفکنندهای نمیشکند. - با
page/limit⇒ پاسخ صفحهبندیشده. - در لاگ، درخواست بدون
pageروی کاتالوگ بزرگ هشدار میدهد تا بفهمیم چه کسی هنوز قدیمی است.
(ج) کش سرور — بازطراحی
کش فعلی یک خروجی را نگه میدارد (value: {at, data}). با پارامترها این
نادرست میشود: صفحهٔ ۲ جواب صفحهٔ ۱ را میگیرد.
پیشنهاد: کلید کش = رشتهی پارامترهای نرمالشده، با سقف تعداد کلید (مثلاً ۵۰، LRU) تا حافظه بینهایت رشد نکند. باطلسازی موجود (middleware پریزما) همه را با هم پاک میکند — که درست است، چون هر نوشتنی میتواند هر صفحهای را عوض کند.
⚠️ سقف کلید لازم است: بدون آن، هر ترکیب فیلترِ کاربران یک ردیف کش میسازد و حافظهی سرور بهتدریج پر میشود.
(د) ایندکسها (مهاجرت)
Product: @@index([isActive, createdAt]) ← لیست و مرتبسازی پیشفرض
@@index([categoryId])
@@index([brandId])
ProductVariant: @@index([productId]) ← الان نیست؛ هر include یک اسکن
ProductImage: @@index([productId])
جستجوی متنی: contains فارسی روی ۱۶ هزار ردیف با ایندکس معمولی سریع نمیشود.
پیشنهاد فعلی: همان contains با ایندکس بالا (برای ۱۶ هزار قابل قبول است).
اگر کند بود، مرحلهی بعد pg_trgm است — ولی اول اندازهگیری، بعد اضافه کردن.
(ه) سمت اپ
| کار | تغییر |
|---|---|
catalogStore |
از «یک آرایهی کامل» به «صفحات + total»؛ load() صفحه میگیرد، loadMore() صفحه بعد |
SearchView / NewestView |
IntersectionObserver بهجای زیاد کردن visibleCount، درخواست صفحه بعد میزند؛ فیلتر/جستجو با debounce به سرور میرود و از صفحه ۱ شروع میکند |
Filters.vue |
گزینهها از /catalog/facets |
useProductDetail |
GET /catalog/products/:id |
cartStore.init |
by-ids با شناسههای سبد |
FavoritesView |
by-ids با شناسههای علاقهمندی |
useOrderItemProduct |
by-ids، یا بهتر: عنوان از خود OrderItem snapshot (اگر دارد) |
configStore |
دستهها از /catalog/facets |
اندازه صفحه: همان ۲۰ که الان پنجرهی رندر است، تا تجربه عوض نشود.
۴. ترتیب اجرا (هر مرحله قابل تست و بیشکست)
| مرحله | کار | وضعیت |
|---|---|---|
| ۱ | مهاجرت ایندکسها | ✅ انجام شد (cbb93de) — Product هیچ ایندکسی نداشت |
| ۲ | مسیرهای سرور + تست | ✅ انجام شد (cbb93de) — صفحهبندی opt-in، فیلتر سمت سرور، ۱۷ تست |
| ۳ | صفحه محصول، سبد، علاقهمندی، دستهها، برچسب آیتم سفارش | ✅ انجام شد — پنج مصرفکننده منتقل شد |
| ۴ | SearchView / NewestView / rails خانه → صفحهبندی واقعی |
⏳ باقی — پرریسکترین (فیلتر + اسکرول) |
| ۵ | حذف خوانش کل کاتالوگ و هشدار لاگ | ⏳ فقط وقتی هیچ مصرفکنندهای نمانده |
یادداشت مرحله ۳ (انجامشده)
«برچسب آیتم سفارش» در طرح اولیه نبود و سر راه پیدا شد: OrderItem فقط
productVariantId دارد نه productId، پس ?ids= جوابش نمیداد. سرور همان را
با یک include برمیگرداند — که برای محصول حذفشده هم کار میکند، برعکس روش
قبلی که ردیف را خالی میگذاشت.
یک باگ هم در حین کار ساخته و گرفته شد: انتقال دستهها به /facets باعث شد
memory.init() دیگر کاتالوگ را بار نزند و rails خانه خالی شود؛ HomeView حالا
صریحاً کاتالوگ را بار میکند.
مرحله ۴ هنوز باقی است — یعنی rails خانه، جستجو و «جدیدترین» همچنان کل کاتالوگ را میگیرند. پس تا آن مرحله، سود واقعی ۱۶ هزار رکوردی کامل نشده.
۵. دو چیز که باید بدانی
۱. مرجع صفحهبندی سرور داشت. ApiMobileController برای لیست محصولات
صفحهبندی میفرستاد؛ پورت فعلی آن را به کلاینت منتقل کرده. پس این کار
بازگشت به رفتار مرجع است، نه قابلیت جدید — در ماتریس بهعنوان STUB-03 ثبت
است.
۲. ریسک واقعی این کار، جستجو و فیلتر است. امروز روی کلاینت فوری و دقیق کار میکنند. منتقلشدن به سرور یعنی: debounce، حالت بارگذاری، و «نتیجهای نیست» که باید با صفحهی خالی اشتباه نشود. مرحله ۴ را عمداً آخر گذاشتم و پیشنهادم این است که وقتی رسیدیم، یک بار با هم در مرورگر امتحانش کنیم — چون این بخش تنها جایی است که تست واحد کفایت نمیکند.
برای تصمیم تو: مرحله ۱ و ۲ کاملاً بیریسکاند و هیچچیز را نمیشکنند. اگر بگویی «برو»، از آنها شروع میکنم و بعد از هر مرحله گزارش میدهم. اگر عجله داری، مرحله ۳ همانجایی است که کاربر فوراً تفاوت را حس میکند (باز شدن صفحه محصول).