# جدا شدن جدول ادمین‌ها از کارشناسان فروش ## چه چیزی عوض شد | | قبل | بعد | |---|---|---| | پرسنل پنل (مدیر ارشد، تأییدکننده، سرپرست کارگاه، حسابدار) | جدول `AdminUser` | جدول جدید **`Admin`** | | کارشناس فروش | جدول `AdminUser` | جدول `AdminUser` (**بدون هیچ تغییری**) | | یکتایی شماره موبایل | یک شماره = فقط یک نقش | یکتا **در هر جدول جدا**؛ یک شماره می‌تواند هم ادمین باشد هم کارشناس | ادمین فقط برای پنل است. کارشناس فقط برای اپ‌ها. توکن اپ کارشناس هیچ‌وقت به پنل دسترسی نمی‌دهد (`JwtStrategy` فقط در `Admin` می‌گردد). ## مایگریشن — `20261026120000_split_admin_table` فقط **افزودنی** است. به `AdminUser` (کارشناس‌ها) چیزی نمی‌نویسد، حذف نمی‌کند و قفل نمی‌کند. 1. جدول `Admin` را می‌سازد. 2. ردیف‌های غیرکارشناس `AdminUser` را با **همان `id`** در `Admin` کپی می‌کند (توکن‌های صادرشده و همهٔ ستون‌های «چه کسی انجام داد» معتبر می‌مانند). 3. کلید خارجی‌های «ثبت‌کننده» را از `AdminUser` به `Admin` منتقل می‌کند. **نام ستون‌ها و دیتا عوض نمی‌شود**، فقط مقصد constraint: `LedgerEntry.postedByAdminUserId`، `OrderStatusEvent` و `PreOrderStatusEvent.actorAdminUserId`، `BranchAllocation.allocatorAdminUserId`، `SmsMessage` و `Notification.sentByAdminUserId`، `UploadedFile.uploadedByAdminUserId`، `Workshop.supervisorId`، `AdminSession.adminUserId`. 4. به‌صورت `NOT VALID` اضافه و بعد validate می‌شوند؛ اگر ردیف قدیمی‌ای به id غیرادمین اشاره کند، deploy شکست نمی‌خورد و فقط یک `NOTICE` چاپ می‌شود. بدون تغییر ماندند (چون دربارهٔ کارشناس‌اند): `Order.salesAgentId`، `Customer.referredByAgentId`، `SalesExpertProfile.adminUserId`. قابل اجرای مجدد است (idempotent). ### آزمایش‌شده روی کپی دیتابیس با ۱۲۰۰۰ کارشناس: چک‌سام (md5) تمام ردیف‌های `SALES_EXPERT` قبل و بعد از مایگریشن **یکسان** بود. زنجیرهٔ کامل مایگریشن‌ها روی دیتابیس خالی اجرا شد و `prisma migrate diff` هیچ اختلافی بین `schema.prisma` و دیتابیس نشان نداد. ## قبل از دیپلوی روی سرور اصلی 1. از دیتابیس **بکاپ** بگیرید. 2. (اختیاری، فقط‌خواندنی) ببینید ردیفی هست که به id غیرادمین اشاره کند؛ برای هر جدول: ```sql -- نمونه برای دفتر حساب؛ برای بقیهٔ جدول‌ها/ستون‌ها همین الگو SELECT count(*) FROM "LedgerEntry" l WHERE l."postedByAdminUserId" IS NOT NULL AND NOT EXISTS (SELECT 1 FROM "Admin" a WHERE a."id" = l."postedByAdminUserId"); ``` قبل از اجرای مایگریشن `Admin` هنوز وجود ندارد؛ این کوئری را بعد از دیپلوی بزنید. جواب `0` یعنی همه چیز سالم است. 3. ابتدا مایگریشن (با بوت شدن API جدید خودکار اجرا می‌شود)، سپس همان کد. چون additive است، کد قدیمی در فاصله هم کار می‌کند. ## یک محدودیت تا «فاز ۲» ردیف‌های قدیمی ادمین‌ها در `AdminUser` **باقی می‌مانند** (غیرفعال، بدون استفاده در کد). شمارهٔ آن‌ها هنوز در `AdminUser` قفل است، پس برای **ادمین‌های موجود** ساختن کارشناس با همان شماره خطای «قبلاً ثبت شده» می‌دهد. (ادمین‌های تازه‌ساخته و شماره‌های جدید مشکلی ندارند.) برای آزاد کردن آن شماره‌ها، بعد از اطمینان از درستی کار: `apps/api/prisma/manual/20261026_remove_dormant_admin_rows.sql` — دستی است، عمداً داخل `migrations/` نیست، فقط ردیف‌های **غیرکارشناس** را که در `Admin` کپی دارند حذف می‌کند، و اگر تعداد کارشناس‌ها یک عدد هم تغییر کند خودش rollback می‌کند. ## برگشت (Rollback) کد قبلی را دوباره deploy کنید. چیزی حذف نشده، پس کد قدیمی همهٔ ادمین‌های پیشین را در `AdminUser` پیدا می‌کند. ادمین‌هایی که **بعد** از جدا شدن ساخته شده‌اند فقط در `Admin` هستند و باید دوباره ساخته شوند. ## تغییر رفتار پنل - جدول «کاربران» فقط ادمین‌ها را نشان می‌دهد؛ کارشناس‌ها از بخش کارشناسان. - نقش «کارشناس فروش» از لیست نقش‌های فرم ادمین حذف شد؛ فرم کارشناس همان `POST admin/users` را با `role: SALES_EXPERT` می‌زند و در `AdminUser` ساخته می‌شود. - نقش یک ادمین به کارشناس (و برعکس) قابل تغییر نیست، چون دو جدول جدا هستند. - کارشناس دیگر نمی‌تواند با رمز خودش وارد پنل شود (قبلاً چون در همان جدول بود، می‌توانست).