19 KiB
تاریخچهی کامل پورت — henza_admin → gold-banking-monorepo
این سند خلاصهی کامل و دقیق تمام کارهایی است که تا این لحظه روی پروژه انجام شده، از شروع پروژه تا آخرین commit. برای جزئیات ریزتر batch به batch به
ADMIN-PORT-PLAN.mdمراجعه کنید — آن سند مرجع فنی زنده است، این سند خلاصهی روایی و تاریخی.
تاریخ نگارش: ۱۳ شهریور ۱۴۰۵ (۲۰۲۶-۰۹-۱۳)
۱. هدف پروژه
پورت کامل و بدون تغییر پنل ادمین legacy جواهرات/طلا (henza_admin، Vue 2 +
Vuesax + قالب Vuexy) به apps/admin-panel در مونوریپوی
gold-banking-monorepo (Vue 3 + Vite + Pinia + ant-design-vue)، همراه با
ساخت بکاند NestJS متناظر برای هر بخش.
قوانین صریح کارفرما (که در تمام مسیر رعایت شدند)
- «همه رو با دقت و بدون دست خوردن میخوام» — پورت وفادار، بدون تغییر منطق
- باگهای legacy نباید ساکت اصلاح شوند — باید عیناً پورت و جداگانه گزارش شوند
- حفظ دقیق قرارداد اعداد فارسی/لاتین و رفتار RTL همانطور که در legacy بود
henza_server/وhenza_pwa/(و بعداًhenza_admin/) فقط خواندنی — هرگز تغییر داده نشوند- اول پلن، بعد اجرا batch به batch
- پشتیبانی از هر دو حالت ادمین و شعبه (
isBranch()) - ACL «دقیقا پایبند به ساختار» — وفاداری کامل به مدل دانهریز
permission(rid, ut)در legacy - لاگین واقعی بکاند، نه mock
- حذف ~۳۹۴ فایل دمو الگوی Vuexy
- «بکاند همراه هر بخش» — هر batch با endpoint های خودش میآید
- «REST تمیز + لایهی نگاشت» در فرانتاند
۲. مراحل اصلی پروژه (Phase 0 تا ممیزی نهایی)
Phase 0-2: زیرساخت
- راهاندازی مونوریپو با pnpm workspaces + Turborepo
- طراحی schema اولیهی Prisma (اکنون ۳۵ مدل)
- ساخت لایهی auth واقعی:
@nestjs/jwt@12+@nestjs/passport@10.0.3(pinشده برای سازگاری با NestJS 10) + bcryptjs - تبدیل مکانیکی روترهای legacy (
router/admin.js+router/branch.js) به TypeScript، با حفظ ۱۰۰٪ مسیرها، نامها،meta.roleو breadcrumb ها - تبدیل مکانیکی
permissionConst.ts— تمام ۱۸۱ برچسب فارسی بایتبهبایت یکسان (با diff تأیید شد)
Batch های ۳ تا ۱۵: پورت view به view
پورت کامل ۶۹ view به همراه بکاند متناظر هر batch. جزئیات کامل هر batch
(فایلها، تصمیمها، مشکلات) در ADMIN-PORT-PLAN.md ثبت شده.
تصمیمهای کلیدی کارفرما در طول مسیر
- کامپوننت مشترک بهجای کپی فایلبهفایل برای ۵ لیست عمدهی تقریباً یکسان و ۴ fork از XTable
- توکنهای طراحی از خودِ legacy ادمین (
secondary #836447، فونتp-yek) — نه هماهنگ با اپلیکیشن کاربر که قبلاً ساخته شده بود
ممیزی نهایی (Phase 16)
پس از تکمیل ۶۹/۶۹ view، یک ممیزی جامع اجرا شد که یافتههای معماری مهمی را آشکار کرد:
- «فاکتور» در legacy در واقع تخصیص موجودی شعبه است، نه فاکتور فروش
- ردیفهای «صندوق» در legacy همان
LedgerEntryهستند - هر دو داشبورد عمدتاً کامنتشدهی دمو Vuexy بودند
- ApexCharts import شده ولی هرگز در markup زنده استفاده نشده
- ۳۶ باگ/کوییرک legacy ثبت و مستند شد
- ۶ رفتار مخرب دادهی legacy عمداً پورت نشدند (حذف سند تاییدشده، ویرایش مبلغ تاییدشده، حذف دستهبندی دارای وابسته، ایجاد چرخه در دستهبندی، یتیمکردن ردیابی ممیزی) — هرکدام با پیام ۴۰۰ فارسی جایگزین شد
۳. بستن ۷ شکاف schema (جلسهی اخیر)
پس از ممیزی نهایی، ۷ شکاف باقیمانده در schema شناسایی و به ترتیب بسته شد.
برای هر شکاف، منطق مستقیماً از سرور legacy (henza_server) خوانده شد،
نه فقط از view ها — این روش یک فرض غلط قبلی را هم آشکار کرد.
| # | شکاف | راهحل |
|---|---|---|
| ۱ | تفکیک بنکدار/مغازهدار | enum CustomerKind با ۵ مقدار (Customer::$TYPES سرور) |
| ۲ | تفکیک سفارش تک/عمده | enum OrderChannel (RETAIL | WHOLESALE) |
| ۳ | ستون بایگانی مجزا | Customer.isArchived |
| ۴ | دو سوییچ مجزای مشتری | Customer.canPlaceOrders جدا از isActive |
| ۵ | ستونهای تجاری مشتری | phone, address, storeName, pasazhName, zone, memberCode |
| ۶ | endpoint آپلود فایل | مدل UploadedFile + POST admin/files/create + سرو استاتیک /uploads/ |
| ۷ | آواتار کارشناس فروش | Customer.avatarFileId (رابطهی نامدار CustomerAvatar به UploadedFile) |
یافتههای مهم از خواندن مستقیم سرور legacy
منبع: AdminController::getBanker/getShopkeeper/getCustomer و
MajorCustomerController::indexArchive/indexVerify در henza_server.
banker(بنکدار) →type=4·shopkeeper(حضوری) →type=3فقط · لیست اصلی عمده (اپلیکیشن) →type=2- اصلاح یک فرض قبلی: پیشتر نوشته شده بود که لیست اصلی عمده «بدون فیلتر» است. این درست نبود؛ خوانده و تصحیح شد.
- هر سه لیست نوع، فیلتر
is_archive=0هم دارند — یعنی بایگانیکردن یک مشتری او را از همهی لیستهای نوع حذف میکند، نه فقط از یکی. Customer::$TYPESسرور یک مقدار پنجم دارد (۵ سرپرست کارگاه) که فرمbulkCustomer.vueهرگز نشان نمیداد. enum جدید هر پنج مقدار را دارد، اما فرم پورتشده عمداً همان چهارتای legacy را نشان میدهد (برای وفاداری کامل به رفتار دیدهشده در UI).- سفارش تک و عمده در legacy در دو جدول کاملاً جدا بودند (
ordersوorders_orders)؛ هیچکدام از دو کنترلر فیلتر نوع نداشتند چون خود جدول تفکیککننده بود، پس دو لیست هرگز همپوشانی نداشتند.Order.channelهمان نقش را در یک مدل واحد بازی میکند. - آواتار در legacy یک FK بود (
image_id) نه یک URL ساده، و در پاسخ بهشکل تودرتویimage: { id, image }برمیگشت — هر دو نیمهی این قرارداد عیناً حفظ شد.
دو رفتار legacy که عمداً پورت نشدند
۱. آپلود بدون احراز هویت. در legacy، file/create هیچ middleware نداشت
و گروه auth:api هم در routes/admin.php:12 کامنت بود — یعنی آپلود
ناشناس میپذیرفت. این یک باگ امنیتی بود، نه قابلیت؛ در پورت، ادمینِ
واردشده الزامی است. rid اختصاصی هم ساخته نشد چون واژگان ACL موجود در
legacy ورودی media/file ندارد.
۲. ارسال پیامک درون toggleActive. legacy هنگام فعالسازی مستقیم SMS
میفرستاد. این به ماژول اعلانها موکول شد (درگاه پیامک هنوز واقعاً وصل
نیست، طبق تصمیم قبلی).
۳. باگ در بارگذاری آواتار. مسیر لود legacy در
ReagentCustomer.vue:280 بدون محافظ به data.image.id دست میزد، پس باز
کردن یک کارشناس فروش بدون عکس throw میکرد (که خودِ catch میبلعید و
فرم نیمهپر میماند). در پورت این خواندن guard شده است.
تأیید اجرایی این بخش
لیستها: عمده(t2)=۱ · بنکدار(t4)=۲ · حضوری(t3)=۲ · بایگانی=۱ · تاییدنشده=۲
سوییچها: toggle-sts فقط sts را جابهجا کرد؛ toggle-active فقط active
بایگانی: ردیف از لیست نوع خارج و به بایگانی رفت، بدون دستخوردن active
سفارش: خردهفروشی=۳ · عمده=۱
آپلود: ۲۰۰ + فایل از URL خودش بایتبهبایت برگشت؛ mime نامعتبر/حجم
زیاد/بدون فایل → ۴۰۰ فارسی؛ درخواست ناشناس → ۴۰۱
آواتار: آپلود → ساخت با image_id → خواندن image:{id,image} → پیشنمایش
۲۰۰ → حذف → هر دو null؛ مشتری بدون آواتار → null (نه خطا)
ممیزی: ۵۹/۵۹ endpoint خواندنی = ۲۰۰ · ACL ۱۴۴/۱۴۴ رید · ۱۵۴ مسیر ادمین
بیلد: api ✓ · admin ✓ · branch ✓ · هر دو typecheck ✓
تستهای نوشتن فقط روی مشتریهای تستیای انجام شد که در همان جلسه ساخته و بعد حذف شدند — دادهی seed اصلی هرگز دستخورد نکرد.
۴. لاگین تستی حالت توسعه (جلسهی جاری)
طبق درخواست صریح، شمارهی ۰۹۱۲۸۰۸۱۳۹۲ برای ورود آزمایشی در هر دو اپلیکیشن مجاز شد — فقط برای محیط توسعه:
- پنل ادمین: یک
AdminUserبا نقش super admin (userTypeId: 85) ساخته شد؛ رمز عبور دقیقاً برابر خودِ شماره تلفن است. - اپلیکیشن کاربر (user-app): همان شماره بهعنوان یک
Customerتاییدشده (VERIFIED) اضافه شد. جریان OTP درLoginView.vueاز قبل stub بود (هر کد ۴ رقمی برای یک مشتریِ واقعیِ منطبق پذیرفته میشود)، پس این مشتری بهمحض ورود شماره در گام اول شناسایی و قابل ورود است. - هر دو ردیف داخل خودِ
prisma/seed.tsبا کامنت DEV-ONLY مشخص شدهاند و هرگز در استقرار واقعی اجرا نمیشوند — این فایل فقط با اجرای دستیِnpx prisma db seedروی دیتابیس محلی اجرا میشود.
✅ فعال و تأییدشده. npx prisma db seed اجرا شد و هر دو ردیف در
دیتابیس محلی ساخته شدند. لاگین بهصورت زنده تست شد:
POST /api/admin/auth/login { phone: '09128081392', password: '09128081392' }
→ 200، super admin، ۱۴۴ rid، JWT صادر شد
GET /api/customers
→ مشتری «کاربر تست توسعه» با mobile=09128081392 در لیست موجود و قابل
شناسایی توسط گام اول OTP در LoginView.vue
۵. نقد پورت و رفع مشکلات پنل ادمین
کارفرما گزارش داد که پنل ادمین «هم دیزاین هم کارکرد» مشکل دارد و خواست که خودم پورت خودم را نقد کنم. دو ممیزی مستقل (ظاهر + کارکرد) اجرا شد و یافتههای کلیدیشان را با خواندن مستقیم فایلها تأیید کردم.
سه اشتباه اصلی که مرتکب شده بودم
۱. فرض کردم کلاسهای Tailwind کار میکنند. legacy یک بیلد کامل Tailwind
داشت (tailwind.config.js، ~۲۸KB) با مقیاس سفارشی mb-base: 2.2rem. من
tailwindcss را در apps/admin-panel نصب نکردم و postcss.config هم
نگذاشتم، ولی ~۱۸۵ کلاس Tailwind در قالبها باقی ماند. بعد بهجای رفع ریشهای،
همان چند کلاس را ۱۲۴ بار در <style scoped> فایلها کپی کردم — و در این
کپیکاری mb-base را 1.5rem نوشتم (۳۲٪ تنگتر از legacy).
۲. مرز CSS scoped را نادیده گرفتم. ۱۷ مورد icon-class="w-5 h-5 text-primary"
به TooltipBtn پاس داده میشد، ولی استایل scoped والد هرگز به فرزند نمیرسد و
هیچ :deep() در پروژه نبود. h-5 هم اصلاً هیچجا تعریف نشده بود. یعنی آن
۱۷ آیکون بیرنگ و بیاندازه رندر میشدند.
۳. لایهی نگاشت را ناقص گذاشتم. ۱۸ مسیر فرانت به بکاند نمیرسید — در حالی که مسیر بکاند هر ۱۸ مورد از قبل وجود داشت. فقط ورودی مپ را ننوشته بودم.
بدترین مورد: تخریب داده
در bulkCustomer.vue (و ReagentCustomer.vue) هنگام ویرایش، Object.assign
فقط ۹ فیلد را برمیگرداند و store_name/pasazh_name/mcode/phone را جا
میانداخت؛ آنها '' میماندند، فرم کل ...form را spread میکرد، و بکاند
if (data.x !== undefined) دارد — پس هر بار ذخیرهی یک مشتری موجود، این
چهار ستون را پاک میکرد.
آنچه انجام شد
| مرحله | کار |
|---|---|
| ۱ | رفع تخریب داده: پرکردن کامل فرم روی ویرایش + ارسال تفاضلی (فقط فیلدهای تغییرکرده) در هر دو فرم |
| ۲ | ۹ الگو + ۲ ورودی دقیق به endpoints.ts → هر ۱۸ endpoint مرده زنده شد |
| ۳ | assets/styles/utilities.less سراسری با مقادیر legacy؛ حذف جراحی ۱۲۴ کپی تکراری از ۳۹ فایل |
| ۴ | <a-config-provider :locale="faIR" direction="rtl"> در App.vue |
| ۵ | reagent_id و password (با bcrypt) در بکاند ذخیره میشوند |
| ۶ | دکمهی «افزودن کاربر»، رفع تایپوی color-success، صادقسازی mainLayoutType |
نکات فنی مهم
- گریدها عمداً محلی ماندند. مقادیر
minmaxدر.grid-2/3/4بین فایلها فرق دارد (۲۰۰ تا ۳۲۰ پیکسل)؛ یکسانسازیشان چیدمان ~۲۶ فرم را بیصدا عوض میکرد. فقط قواعدی حذف شدند که بدنهشان دقیقاً با نسخهی سراسری یکی بود. - سه قاعده RTL-flipped هستند و همانطور ماندند:
.ml-1/.ml-3در پورتmargin-rightمیدهند و.mr-4میدهدmargin-left— چون پنل RTL است. در فایل سراسری هم عیناً حفظ شد و کامنتگذاری شد که «اصلاح» نشود. percentعمداً دستنخورده ماند.Customerاصلاً ستونpercentندارد؛ این عدد ازAdminUser.wagePercentکارشناس معرف میآید. یعنی ویرایشش از فرم مشتری، کمیسیون آن کارشناس را برای همهی مشتریانش عوض میکرد. نیاز به تصمیم شما دارد.- چیدمان افقی حالت شعبه:
themeConfigمقدارhorizontalمیداد ولیMain.vueهمیشه منوی عمودی رندر میکرد. کامپوننتهایHorizontalNavMenu(۴ فایل) هرگز پورت نشدهاند، پس بهجای ساختن بیسروصدای آنها، نوع به'vertical'محدود شد تا کد ادعای دروغ نکند. پورت چیدمان افقی کار جداست.
تأیید اجرایی
دادهرفتگی: ویرایش فقط-نام → ۰ فیلد از دست رفت (قبلاً هر ۴ پاک میشد)
Stage 5: reagent ذخیره شد · password یک hash واقعی bcrypt شد
endpointها: هر ۱۰ شکل بازیابیشده → ۲۰۰ (قبلاً ۴۰۴)
typecheck: admin ✓ · api ✓
بیلد: admin ✓ · branch ✓
تستهای نوشتن فقط روی مشتریای انجام شد که خودم ساختم و بعد حذف کردم.
۶. ابعاد نهایی کد (تا این لحظه)
| معیار | مقدار |
|---|---|
فایلهای .vue پورتشده (views) |
۸۰ |
| ماژولهای بکاند NestJS | ۲۵ |
| مدلهای Prisma | ۳۵ |
| ورودیهای دقیق لایهی نگاشت (endpoints.ts) | ۷۷ + ۲۶ الگو |
| باگ/کوییرک legacy ثبتشده | ۳۶ |
| مسیرهای ادمین نگاشتشده | ۱۵۴ |
| پوشش ACL (rid) | ۱۴۴ / ۱۴۴ |
| کاهش حجم با کامپوننت مشترک (۵ لیست عمده + ۲ جزئیات مشتری + ۲ فرم تخفیف) | ۴٬۶۷۶ خط → ۱٬۱۳۹ خط |
۷. آنچه این تاریخچه نمیتواند تأیید کند
⚠️ ظاهر رندرشده در مرورگر — مهمتر از همیشه. هیچ view ای چشمی دیده نشده؛ همهی تأییدها API و بیلد و typecheck بوده. دو تغییر اخیر بیشترین ریسک بصری را دارند و حتماً باید چشمی دیده شوند:
direction="rtl"روی ConfigProvider.ant.lessدستی برای ant ۱.x نوشته شده؛ حالا ant ۳ هم RTL خودش را اعمال میکند. احتمال تداخل یا دوباراعمالشدن هست و بیلد آن را نمیگیرد.mb-baseاز1.5remبه2.2remرفت (مقدار واقعی legacy). فاصلهی بین کارتها در ۱۳ view عمداً بازتر میشود — این تغییر خواستهشده است، نه باگ.
بهعلاوه آواتار، اسلایدر، و فرم دستهبندی که uploader واقعی گرفتند.
⚠️ مسیرهای نوشتن با نقشهای کمدسترسی. دودتست فقط روی مسیرهای خواندنی انجام شد تا داده خراب نشود؛ مسیرهای نوشتن در همان batch خودشان تست شدند اما نه بهصورت ماتریسی روی همهی نقشها.
⚠️ درگاه پیامک واقعی وصل نیست — پیام ثبت میشود اما ارسال نمیشود.
مرجع
برای جزئیات فنی کامل هر batch — شامل کد دقیق تغییرات، دلایل هر تصمیم
معماری، و متن کامل دیالوگ تصمیمگیری — به
docs/ADMIN-PORT-PLAN.md مراجعه کنید.