102 KiB
نگاشت دیتابیس legacy → Prisma
سند نگاشت، نه پلن اجرا. هیچ کدی بر اساس این سند نوشته نشده. پیشنیاز ابزار مهاجرت دادهی
GO-LIVE-PLAN.md§۷.
منبع اول: oapi-struncture.sql — دامپ Navicat فقط ساختار، تاریخ ۰۵/۱۰/۲۰۲۶
منبع دوم: oapi.sql — دامپ کامل با داده، تاریخ ۰۴/۱۰/۲۰۲۶، ۲۷۴٬۷۷۳ سطر
مقصد: apps/api/prisma/schema.prisma — ۴۳ مدل، ۴۷۷ فیلد
تاریخ نگارش: ۱۴ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۰۶) · بهروز ۱۵ مهر ۱۴۰۵ (دیتای واقعی)
۰. مشخصات منبع
| ویژگی | مقدار |
|---|---|
| موتور | MySQL 8.0.42 (8.0.42-0ubuntu0.20.04.1) |
| سرور | Henza · localhost:3305 |
| اسکیما | oapi |
| جداول | ۸۰ |
| ستونها | ۱٬۰۸۷ |
| Engine | InnoDB (هر ۸۰ جدول) |
| محتوا | دامپ کامل — oapi.sql دارد INSERT |
۰.۱ شمارش ردیفهای کلیدی (از oapi.sql)
| جدول | ردیف | یادداشت |
|---|---|---|
customers |
۱٬۶۱۲ | AUTO_INCREMENT=1720 — ۱۰۸ شکاف |
cashes |
۶۴۸ | اسناد دفتر مالی |
products_productvarient |
۷٬۴۴۳ | شامل حذفشدهها |
product |
۳٬۲۷۲ | محصولات |
image |
۱۷٬۹۵۵ | فایلهای فیزیکی: ۱۷٬۸۲۶ (در storage/) |
products_productimage |
۵٬۴۱۷ | رابطه product→image |
users |
۲۲ | ادمین/کارشناسان |
orders_orders |
۵۲۰ | سفارشها (به تأخیر افتاد §۷.۲) |
wallets |
۸۰ | کیفپول خردهفروشی |
fav |
۲۵۱ | مورد علاقه (migration شد §۱۰) |
این فایل شکافی را پر کرد که قبلاً باز بود: جداول دوران Django
(product، products_productvarient، category_collection و …) هیچ migration
لاراولی نداشتند و فقط در دیتابیس زنده تعریف شده بودند. موتور دیتابیس هم تا
پیش از این حدس بود؛ اکنون قطعی است: MySQL.
⚠️ دامپ داده ندارد. پس این سند نگاشت ساختار است. آستانههای تطبیق §۷
GO-LIVE و شمارش ردیفهای تکراری (§۳ همین سند) دادهی واقعی لازم دارند و
با این فایل قابل انجام نیستند.
۱. تفکیک ۸۰ جدول → ۵۰ زنده، ۳۰ مرده
«زنده» = مدل Eloquent دارد ($table صریح، یا نام جمعِ پیشفرض لاراول).
| دسته | تعداد | تصمیم |
|---|---|---|
| زنده — در دامنهی کار | ۴۱ | مهاجرت میشوند |
| زنده — فروشگاه خرده (D13) | ۹ | حفظ، بدون پورت |
| نسخهی پشتیبان دستی | ۶ | نادیده |
| بازماندهی Django | ۱۱ | نادیده |
| زیرساخت لاراول | ۳ | نادیده |
۱.۱ — جداول پشتیبان دستی (۶) — نادیده
customers_copy1 customers_copy2 products_productvarient_copy1
cities5 citiesB cities_copy11
customers_copy1 و customers_copy2 هر دو ۴۹ ستون دارند، برابر با
customers. snapshot دستی قبل از یک تغییرند. customers_test (۴۰ ستون) مدل
CustomerTest دارد ولی جدول آزمایشی است.
⚠️ قبل از نادیدهگرفتن، تعداد ردیف هر سه customers* شمرده شود. اگر
copy2 ردیفهایی دارد که customers ندارد، یعنی جایی ردیف حذف شده و این کپی
آخرین نسخهی آن است.
۱.۲ — بازماندهی Django (۱۱) — نادیده
accounts_effectivedate accounts_profile accounts_rolepermission auth_permission
discount_discountcode discount_pricerule
orders_fulfillment orders_sendtime
products_comment products_promotion products_variantpromotion
هیچکدام مدل Eloquent ندارند. accounts_profile با ۴۳ ستون بزرگترینشان است
(sales_ceiling، commission، profit) — سامانهی کارشناس فروشِ نسخهی
Django، که لاراول با customers.type=1 جایش را گرفت.
⚠️ orders_sendtime از orders_orders.send_time_id ارجاع میشود. اگر
زمان ارسال سفارشهای تاریخی مهم است، این جدول مرده نیست.
۱.۳ — فروشگاه خرده (۹) — حفظ، بدون پورت (D13)
wallets transactions transaction_metas transactions_cheque
transactions_deposittocard transactions_financialreport
transactions_meltedgold transactions_refund discount_codes
طبق D13 نگه داشته میشوند. TransactionOrder و transaction_orders مدل
دارند، ولی transactions_* ندارند.
۲. 🔴 fname/lname از قبل وجود دارند — D0 بر فرض غلط بنا شد
جدول customers هر سه ستون را دارد:
fname varchar(50) NULL
lname varchar(50) NULL
name varchar(75) NULL
و users هم همین سه را دارد (fname, lname, name — هر سه varchar(255)).
DECISIONS.md §D0 ثبت کرده که fullName باید «به firstName/lastName
شکسته شود» با backfill و «گزارش ردیفهای چندتوکنی». چیزی برای شکستن نیست.
legacy از اول جدا داشته.
| نگاشت صحیح | نگاشت غلط |
|---|---|
fname → firstName |
name با توکن اول/بقیه |
lname → lastName |
|
name → fullName |
اگر از name تجزیه کنیم، دادهی درست را با حدس خراب میکنیم.
تلهی trigger
customer_sync_full_name در Postgres یک trigger است و طبق
PROGRESS.md رفتارش این است: اگر هر سه را یکجا بنویسی، اجزا برنده میشوند
و fullName ارسالی دور ریخته میشود. برای مهاجرت این دقیقاً رفتار مطلوب
است (اجزا منبع حقیقتاند) — ولی اگر name در legacy با fname + ' ' + lname
نخواند، مقدار name در مهاجرت گم میشود.
⚠️ قبل از مهاجرت شمرده شود: WHERE name != CONCAT(fname,' ',lname). اگر
صفر نیست، تصمیم مالک لازم است کدام برنده باشد.
۳. 🔴 هیچ UNIQUE روی mobile و ncode نیست
کل ایندکسهای customers:
PRIMARY KEY (`id`) USING BTREE
همین. هیچ unique index دیگری، و هیچ foreign key.
ولی Prisma اعلام کرده:
mobile String @unique
nationalCode String @unique
نتیجه: به احتمال زیاد در دادهی تولید موبایل یا کد ملی تکراری وجود دارد. مهاجرت در میانه با خطای unique متوقف میشود — و چون در میانه است، نیمی از داده منتقل شده.
GO-LIVE §۷ تکرار شماره بین Customer و AdminUser را ثبت کرده، ولی
تکرار داخل خود customers را ندیده. این یک شکاف تازه است.
کوئریهای شمارش — در فایل جداگانه
📄 migration-precheck.sql — ۹ بخش،
فقط COUNT/SUM، صفر INSERT/UPDATE/DELETE. هیچ ستون متنی در
خروجی ظاهر نمیشود، پس دادهی مشتری از سرور بیرون نمیآید.
mysql -u USER -p -h HOST -P 3305 oapi < migration-precheck.sql > out.txt
نمونهی مختصر از بخش ۲ همان فایل:
-- ۱) موبایل تکراری
SELECT mobile, COUNT(*) c FROM customers
WHERE deleted_at IS NULL GROUP BY mobile HAVING c > 1;
-- ۲) کد ملی تکراری (رشتهی خالی هم تکرار است، برخلاف NULL)
SELECT ncode, COUNT(*) c FROM customers
WHERE deleted_at IS NULL AND ncode IS NOT NULL AND ncode != ''
GROUP BY ncode HAVING c > 1;
-- ۳) موبایل یا کد ملی تهی — Prisma هر دو را NOT NULL میخواهد
SELECT
SUM(mobile IS NULL OR mobile = '') AS no_mobile,
SUM(ncode IS NULL OR ncode = '') AS no_ncode
FROM customers WHERE deleted_at IS NULL;
-- ۴) تداخل شماره با کارشناسان و ادمینها (D21: «کارشناس برنده است»)
SELECT c.mobile FROM customers c
JOIN users u ON u.mobile = c.mobile
WHERE c.deleted_at IS NULL AND u.deleted_at IS NULL;
nationalCode در Prisma String بدون ? است — اجباری. اگر کوئری ۳
عددی غیرصفر برگرداند، یا schema باید nullable شود، یا مالک باید قاعدهی
جایگزین بدهد.
۴. 🔴 ۱۱۰ ستون پول و وزن double هستند — صفر decimal
double/float : ۱۱۰ ستون
decimal : ۱ ستون (فقط users.permission — که مرده است، §۶)
نمونهها: customers.remain, customers.rial_remain, cashes.remain,
cashes.balance, orders.w, orders.wage, order_items.rash_weight,
products_productvarient.grams.
Prisma درستکاری کرده: Decimal(14,4) برای گرم، Decimal(20,2) برای ریال.
ولی GO-LIVE §۷ شرط تطبیق را «صفر اختلاف» گذاشته — و این ذاتاً
دستنیافتنی است.
double در IEEE 754 عدد 12.3 را واقعاً 12.3000000000000007105…
ذخیره میکند. تبدیل به Decimal(14,4) آن را به 12.3000 گِرد میکند. در یک
ردیف بیاهمیت است؛ در جمعِ هزاران ردیفِ دفتر، اختلاف انباشته میشود.
پیشنهاد — آستانهی واقعبینانه جای صفر مطلق
| بررسی | آستانهی فعلی §۷ | آستانهی پیشنهادی |
|---|---|---|
| بدهی طلای هر مشتری | صفر اختلاف | ` |
| بدهی ریالی هر مشتری | صفر اختلاف | ` |
| جمع کل بدهی طلا | — | ` |
بدون این، ابزار مهاجرت همیشه قرمز گزارش میدهد و کسی به گزارشش اعتماد نمیکند — که بدتر از نداشتنِ گزارش است.
⚠️ rial_* در legacy bigint است (نه double) — یعنی ریال صحیح، بدون
اعشار. ولی Prisma Decimal(20,2) گرفته. این سمت مشکلی ندارد (دقت بیشتر)،
ولی تأیید میکند که fiatDebtRial هرگز اعشار واقعی ندارد. نقص #۱۹ در
PWA-PORT-PLAN.md («بررسی واحد») اینجا پاسخ میگیرد: واحد ریال است، صحیح.
۵. customers — نگاشت ۴۹ ستون
۳۲ ستون نگاشت دارند، ۱۷ ستون خانه ندارند.
۵.۱ — نگاشتشده (۳۲)
| legacy | نوع | Prisma | یادداشت |
|---|---|---|---|
id |
bigint |
— | در جدول نگاشت legacyId نگه داشته شود (§۷ GO-LIVE) |
fname |
varchar(50) |
firstName |
§۲ — مستقیم، تجزیه نشود |
lname |
varchar(50) |
lastName |
§۲ |
name |
varchar(75) |
fullName |
trigger بازنویسی میکند — §۲ |
mobile |
varchar(20) |
mobile |
§۳ — unique نیست در مبدأ |
ncode |
varchar(20) |
nationalCode |
§۳ — unique نیست، و در Prisma اجباری است |
password |
varchar(255) |
passwordHash |
فرمت hash تأیید شود (bcrypt؟) |
city_id |
int |
cityId |
نگاشت id لازم |
type |
tinyint |
kind |
§۵.۳ |
sts |
tinyint |
canPlaceOrders |
1→true, -1→false |
active |
tinyint |
isActive |
سوییچ مجزا از sts |
is_archive |
tinyint(1) |
isArchived |
|
gram_level |
double |
goldCreditLimitGrams |
§۴ |
remain |
double |
goldDebtGrams |
§۴ — بدهی طلا |
rial_remain |
bigint |
fiatDebtRial |
§۴ — ریال صحیح |
percent |
double |
makingChargeOffsetPercent |
D9 — آفست اجرت |
reagent_id |
int |
referredByAgentId |
🔴 → customers.id، نه users.id · §۵.۷ |
phone |
varchar(15) |
phone |
|
address |
varchar(255) |
address |
|
store_name |
varchar(255) |
storeName |
|
pasazh_name |
varchar(255) |
pasazhName |
|
zone |
varchar(255) |
zone |
|
mcode |
varchar(30) |
memberCode |
|
image_id |
int |
avatarFileId |
FK → UploadedFile |
ncode_image_id |
int |
nationalIdImageUrl |
FK → رشته · §۵.۴ |
business_license_image_id |
int |
businessLicenseUrl |
FK → رشته · §۵.۴ |
shop_image_id |
int |
shopImageUrl |
FK → رشته · §۵.۴ |
sts_verify_step_one |
tinyint(1) |
verificationStep |
§۵.۵ — دو ستون → یک enum |
sts_verify_step_two |
tinyint(1) |
verificationStep |
§۵.۵ |
created_at |
timestamp |
createdAt |
|
updated_at |
timestamp |
updatedAt |
|
deleted_at |
timestamp |
— | §۵.۶ — soft-delete در Prisma وجود ندارد |
۵.۲ — 🟡 ۱۷ ستون بیخانه
هر ۱۷ مورد در کد legacy فعالاً استفاده میشوند (با grep تأیید شد). اگر ابزار مهاجرت بیخبر نوشته شود، بیصدا دور ریخته میشوند.
| ستون | نوع | فایلهای استفادهکننده | چیست |
|---|---|---|---|
input |
double |
— | جمع واردهی طلا |
output |
double |
— | جمع صادرهی طلا |
rial_input |
bigint |
— | جمع واردهی ریال |
rial_output |
bigint |
— | جمع صادرهی ریال |
grams_sell |
double |
۳ | گرم فروختهشده |
total_order |
int |
۶ | شمارندهی سفارش |
open_order |
int |
۵ | شمارندهی سفارش باز |
child |
int |
? | شمارندهی زیرمجموعه |
notify_count |
int |
۵ | شمارندهی اعلان خواندهنشده |
count_login |
bigint |
۲ | شمارندهی ورود |
token_mobile |
varchar(255) |
۱۰ | توکن push گوشی |
birth_date |
timestamp |
۴ | تولد — منطق تبریک |
marriage_date |
timestamp |
۳ | سالگرد |
new_city |
varchar(255) |
۴ | شهر دستی وقتی city_id تهی است |
api_token |
varchar(60) |
— | auth قدیم |
send_password |
varchar(255) |
۴ | رمز ارسالی با پیامک |
rnd |
varchar(60) |
۴ | nonce |
سه گروه با تصمیم متفاوت:
الف) input/output/rial_input/rial_output — طرف دیگر دفتر.
Prisma فقط remain را بهعنوان ستون دارد. در مدل جدید جمع وارده/صادره باید
از LedgerEntry محاسبه شود. این بهتر است — ولی یعنی هنگام مهاجرت باید
تأیید شود که SUM(ledger) == remain legacy. اگر نخواند، legacy خودش ناسازگار
بوده و باید گزارش شود، نه اصلاح.
ب) شمارندههای denormalized (total_order, open_order, notify_count,
child, count_login, grams_sell). قابل محاسبه از دادهی دیگرند. اضافه
نکردنشان درست است، به شرطی که جایی که legacy نشانشان میداد، مقصد هم
محاسبهشان کند — وگرنه عدد صفر نشان داده میشود.
ج) دادهی واقعیِ غیرقابلبازسازی — birth_date, marriage_date,
token_mobile, new_city.
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — دو منتقل، دو حذف
| ستون | ردیف | تصمیم | دلیل مالک |
|---|---|---|---|
birth_date |
۹۱۷ | ✅ منتقل | «نرخ پرشدگی بالای ۵۵٪ · کمپینهای وفاداری و پیامکی» |
new_city |
۱۷۶ | ✅ منتقل | «خالی ماندن شهر ۱۷۶ مشتری نقص دیتا ایجاد میکند» |
token_mobile |
۹۹ | ❌ حذف | «توکنها اغلب منقضی شدهاند · زباله دیتایی» |
marriage_date |
۵ | ❌ حذف | «۵ ردیف توجیه معماری ندارد» |
هر دو حذف قابلدفاعاند. توکن push با تغییر کلید FCM/APNs بیاثر میشود، و ۵ ردیف ارزش یک ستون دائمی را ندارد.
migration افزایشی لازم:
model Customer {
birthDate DateTime? // legacy birth_date
// new_city → §۵.۹
}
🔶 new_city — «کدام فیلد» هنوز باز است
مالک گفت «یا در فیلد شهر بنشیند یا در یک فیلد آدرس متنی مپ شود». کدامیک، به داده بستگی دارد — §۵.۹.
api_token, send_password, rnd نیازی نیست منتقل شوند — سامانهی auth
جدید JWT است.
۵.۳ — type → CustomerKind
Customer::$TYPES در legacy پنج مقدار دارد:
| legacy | برچسب | CustomerKind |
|---|---|---|
1 |
کارشناس فروش | → AdminUser + SalesExpertProfile (نه Customer) · D17 |
2 |
مغازهدار اپلیکیشن | APP_SHOPKEEPER |
3 |
مغازهدار حضوری | WALK_IN_SHOPKEEPER |
4 |
بنکدار | BANKER |
5 |
سرپرست کارگاه | WORKSHOP_SUPERVISOR |
| — | — | RETAIL ➕ مقصد-تنها (از login، §۸) |
NULL |
— | 🔶 بینگاشت |
type در مبدأ nullable بدون default است. Prisma
@default(APP_SHOPKEEPER) دارد. پس NULL بیصدا «مغازهدار اپلیکیشن»
میشود. قبل از مهاجرت SELECT COUNT(*) WHERE type IS NULL شمرده شود.
⚠️ type=1 به جدول دیگری میرود (AdminUser). یعنی مهاجرت customers
دوشاخه است و جدول نگاشت باید بداند هر legacyId به کدام جدول رفته.
۵.۴ — سه FK تصویر → سه رشته
| legacy | نوع | Prisma | نوع |
|---|---|---|---|
ncode_image_id |
int → image.id |
nationalIdImageUrl |
String? |
business_license_image_id |
int → image.id |
businessLicenseUrl |
String? |
shop_image_id |
int → image.id |
shopImageUrl |
String? |
در legacy اینها FK به جدول image بودند. در Prisma رشتهی URL شدند.
پس مهاجرت باید image.image (مسیر فایل، char(100)) را بخواند و URL بسازد.
⚠️ image_id (آواتار) متفاوت رفتار میکند: در Prisma هنوز FK است
(avatarFileId → UploadedFile). یعنی چهار ستون تصویر، دو الگوی مختلف.
این ناسازگاری در مقصد است، ولی عوضکردنش الان کار این سند نیست — فقط ثبت
میشود.
فایلهای تصویر هم باید کپی شوند (§۷ GO-LIVE) — جدول image فقط مسیر دارد.
۵.۵ — 🔴 دو ستون تأیید boolean نیستند — سهحالتهاند
تصحیح ۲۰۲۶-۱۰-۰۶: نسخهی اول این سند فرض کرده بود این دو tinyint(1)
مقدار 0/1 دارند. غلط بود. دادهی واقعی و
MajorCustomerController.php:106-135 نشان میدهند مقادیر 1, 2, 3
هستند:
| مقدار | معنا | عارضه در legacy |
|---|---|---|
1 |
در انتظار بررسی | — |
2 |
تأیید شد | step_one=2 → active=1 + api_token + پیامک |
3 |
رد شد | step_one=3 → active=-1 · step_two=3 → پیامک رد |
NULL |
هنوز وارد نشده | — |
توزیع واقعی (۱٬۵۹۵ ردیف زنده)
one,two |
تعداد | معنا | VerificationStep |
|---|---|---|---|
2,2 |
۱٬۴۶۵ | هر دو تأیید | VERIFIED |
1,NULL |
۹۰ | گام ۱ در انتظار | STEP_ONE_SUBMITTED |
2,NULL |
۲۵ | گام ۱ تأیید، گام ۲ وارد نشده | STEP_ONE_SUBMITTED |
1,2 |
۷ | 🔴 ناسازگار — گام ۲ تأیید ولی گام ۱ در انتظار | ؟ |
3,2 |
۳ | 🔴 ناسازگار — گام ۱ رد ولی گام ۲ تأیید | ؟ |
2,1 |
۱ | گام ۲ در انتظار | STEP_TWO_SUBMITTED |
2,3 |
۲ | گام ۲ رد | ؟ |
3,3 |
۲ | هر دو رد | ؟ |
🔴 VerificationStep حالت «رد شد» ندارد. مقادیرش
UNVERIFIED / STEP_ONE_SUBMITTED / STEP_TWO_SUBMITTED / VERIFIED
است — هیچ REJECTED. پس ۷ ردیف ردشده (2,3 + 3,3 + بخشی از 3,2)
جایی برای رفتن ندارند و اگر UNVERIFIED شوند، از «ردشده» به
«بررسینشده» برمیگردند — یعنی دوباره در صف بررسی ظاهر میشوند.
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — REJECTED + علت رد
«رویکرد شما ناقص است؛ حالت «رد شد» باید همراه با فیلد «علت رد» اضافه شود. انتقال این افراد به حالت «در انتظار» کاملاً مردود است چون بار عملیاتی و خطای انسانی ایجاد میکند.»
پذیرفته شد. انتقال به UNVERIFIED رد شد.
دو تغییر افزایشی روی مقصد:
enum VerificationStep {
UNVERIFIED
STEP_ONE_SUBMITTED
STEP_TWO_SUBMITTED
VERIFIED
REJECTED // ← افزوده
}
model Customer {
// ...
verificationRejectionReason String? // ← افزوده
verificationRejectedAt DateTime? // ← افزوده
}
🔴 ولی legacy علت رد را ذخیره نمیکند
مالک خواست «در صورت وجود در سیستم قدیم» علت هم منتقل شود. بررسی شد: وجود ندارد.
هر ۴۹ ستون customers خوانده شد — هیچ ستون reason, note,
description, comment یا مشابهی ندارد. جستوجوی کل دامپ هم نشان داد
canceled_reason فقط روی orders_orders است و reason روی یک جدول
transactions_*.
و در MajorCustomerController.php:129-134، هنگام رد، legacy فقط یک
پیامک ثابت میفرستد:
'اطلاعات شما تایید نشده است.'
'لطفا با پیشتیبانی تماس حاصل فرمایید، تا مشکل برطرف شده و ثبتنام شما تکمیل شود.'
متن ثابت است — هیچ علتی در آن نیست و هیچجا ذخیره نمیشود. یعنی در سیستم قدیم هم پشتیبان علت را نمیدانست؛ مشتری باید تلفن میزد.
پس نتیجهی عملی
ستون verificationRejectionReason |
✅ ساخته میشود (خواست مالک · برای آینده) |
| مقدارش در مهاجرت ۷ ردیف | متن ثابت مالک (پایین) — علت واقعی در مبدأ نیست |
verificationRejectedAt |
NULL — legacy زمان رد را هم ثبت نمیکرد |
⚠️ این یک شکاف دادهی ذاتی است، نه نقص ابزار مهاجرت. علت رد آن ۷ مشتری در سیستم قدیم هرگز ذخیره نشده بود و قابل بازیابی نیست. ستون ساخته میشود تا از این پس پر شود.
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — یادداشت خودکار درج شود
«فیلد علت برای این ۱۴ ردیف نباید کامپلت NULL رها شود.»
(عدد درست ۷ است: ۲ رد در گام دو، ۳ در گام یک، ۲ در هر دو. عدد ۱۴ را من اشتباه جمع کرده بودم و مالک بر اساس آن نوشت؛ تصمیم تغییری نمیکند.)
مقدار دقیق که ابزار درج میکند:
رد شده در سیستم قدیم — فاقد شرح علت ثبتشده
| ستون | مقدار |
|---|---|
verificationStep |
REJECTED |
verificationRejectionReason |
متن بالا (نه NULL) |
verificationRejectedAt |
NULL — legacy زمان رد را ثبت نمیکرد |
⚠️ قاعدهی ابزار: این متن فقط روی ردیفهایی درج شود که مهاجرت
REJECTEDشان میکند. اگر ابزار دوباره اجرا شود، نباید روی مشتریای که
در سیستم جدید رد شده و علت واقعی دارد بازنویسی کند. شرط:
WHERE verificationRejectionReason IS NULL — یا طبق §۱۰.۵، این ستون
هم در allow-list فقط برای ردیفهای تازه نوشته شود.
۱۰ ردیف ناسازگار
1,2 (۷ ردیف) و 3,2 (۳ ردیف) — گام دوم تأیید شده ولی گام اول نه.
در legacy ممکن بود چون دو ستون مستقلاند و هیچ محدودیتی بینشان نیست.
قاعدهی پیشنهادی: گام اول حاکم است.
1,2→STEP_ONE_SUBMITTED(گام اول هنوز بررسی نشده)3,2→REJECTED(گام اول رد شده، پس کل پرونده رد است)
دلیل: در legacy step_one=3 ستون active را -1 میکرد، یعنی مشتری
غیرفعال میشد. پس رد گام اول واقعاً پرونده را میبست.
۵.۹ — new_city چیست و کجا بنشیند
new_city یک نام شهر بهصورت متن آزاد است، مستقل از city_id.
AdminController.php:296-298 و Major/CustomerController.php:36 نشان
میدهند هرگاه فرم فیلد city را پر بفرستد، عیناً در new_city ذخیره
میشود — بدون هیچ تطبیقی با جدول cities.
و MajorCustomerController.php:36 ثابت میکند خوانده میشود:
داشبورد مشتری new_city را در select دارد. پس ستون زنده است، نه
بازمانده.
چرا ۱۷۶ ردیف دارد: جدول cities فقط ۱۵ ردیف دارد (§۱۰.۴). پس
بسیاری از شهرهای واقعی در آن نیستند و اپراتور نام را دستی تایپ میکرده.
دو حالت ممکن، دو راهحل متفاوت
| اگر | یعنی | راهحل |
|---|---|---|
اکثر ۱۷۶ ردیف city_id تهی دارند |
شهرشان در cities نیست |
ستون متنی لازم است |
اکثرشان city_id هم دارند |
new_city تکراری است |
میتوان رهایش کرد |
| متنشان با عنوان یک شهر موجود یکی است | قابل تبدیل به city_id |
نگاشت به cityId |
🔶 یک کوئری لازم است —
migration-precheck-3.sql. تا جوابش نیاید،
نمیدانم ستون تازه بسازم یا به cityId نگاشت کنم. ساختن ستونی که لازم
نیست، همانقدر بد است که نساختن ستونی که لازم است.
⚠️ اگر ستون متنی لازم شد، اسمش cityNameRaw پیشنهاد میشود، نه
newCity — چون «new» در legacy معنای تاریخی داشت (ستون بعداً اضافه شده)
و در مقصد گمراهکننده است.
birth_date — یک بررسی سلامت
birth_date نوعش timestamp است. ولی چون legacy تاریخها را جاهای دیگر
بهصورت رشتهی جلالی ذخیره میکرد (§۸ — recive_date)، احتمال دارد
اینجا هم تاریخ جلالی بهاشتباه بهعنوان میلادی وارد شده باشد.
اگر سالها در بازهی ۱۳۰۰–۱۴۰۰ باشند، یعنی جلالی در ستون میلادی و تبدیل لازم است. اگر ۱۹۳۰–۲۰۱۰ باشند، سالماند. کوئریاش در همان فایل سوم است.
۵.۷ — 🔴 reagent_id به customers اشاره میکند، نه users
کوئری پیشبررسی ۱٬۵۳۴ ارجاع یتیم از customers.reagent_id به
users.id گزارش کرد — یعنی تقریباً همهی ۱٬۵۹۵ ردیف.
علت: کوئری من به جدول غلط join زده بود، نه اینکه داده خراب باشد.
Customer.php:119-122:
public function reagent()
{
return $this->belongsTo('App\Models\Customer'); // ← Customer، نه User
}
کارشناس فروش در legacy یک ردیف در customers با type=1 است (۵۶ ردیف،
§۵.۳) — نه یک ردیف در users. پس reagent_id یک خودارجاع است.
پس در مقصد
Customer.referredByAgentId به AdminUser اشاره میکند، چون D17 حکم
کرده کارشناس فروش AdminUser + SalesExpertProfile شود. یعنی مهاجرت باید
دو مرحلهای باشد:
- اول ۵۶ ردیف
customers.type=1→AdminUser+SalesExpertProfile، وlegacyCustomerId → adminUserIdدر جدول نگاشت ثبت شود - بعد بقیهی مشتریها، و
reagent_idاز همان جدول نگاشت ترجمه شود — نه ازusers
🔴 اگر این ترتیب رعایت نشود، referredByAgentId همهی مشتریها NULL
میشود و رابطهی «کارشناس معرف» کامل از بین میرود.
⚠️ mobile_collision_customer_vs_user = 10 یعنی ۱۰ شماره در هر دو جدول
هست. چون کارشناس در customers است، این ۱۰ مورد تداخل واقعیاند و
D21 («کارشناس برنده است») دقیقاً برایشان نوشته شده.
۵.۸ — 🟡 deleted_at در Prisma وجود ندارد
هر جدول زندهی legacy ستون deleted_at دارد (soft-delete لاراول).
هیچ مدل Prisma معادلی ندارد.
یعنی ردیفهای حذفشدهی نرم:
- یا منتقل نمیشوند (پیشنهاد —
WHERE deleted_at IS NULL) - یا منتقل میشوند و ناگهان زنده میشوند 🔴
گزینهی دوم فاجعه است: مشتری حذفشده برمیگردد، سفارش حذفشده در گزارش ظاهر میشود، سند مالی حذفشده در موجودی حساب میشود.
قاعدهی اجباری ابزار مهاجرت: هر SELECT باید WHERE deleted_at IS NULL
داشته باشد. و تعداد ردیفهای حذفشدهی هر جدول در گزارش تطبیق بیاید تا
معلوم باشد چه حجمی عمداً جا مانده.
۶. users → AdminUser — و یک ستون خراب
| legacy | نوع | Prisma | یادداشت |
|---|---|---|---|
fname/lname/name |
varchar(255) |
firstName/lastName |
D17 |
mobile |
varchar(255) |
— | بررسی تداخل با customers (§۳) |
password |
varchar(255) |
passwordHash |
|
ut |
int unsigned |
userTypeId → UserType |
کلید ACL |
city_id, branch_id |
int |
— | نگاشت id |
token_mobile, notify_count |
🟡 بیخانه | مثل §۵.۲ | |
api_token, remember_token |
— | لازم نیست | |
permission |
decimal(16,16) |
— | 🔴 ستون خراب — زیر |
🔴 users.permission decimal(16,16) — ستونی که کار نمیکند
decimal(16,16) یعنی ۱۶ رقم کل، ۱۶ رقم اعشار — پس صفر رقم صحیح.
این ستون فقط میتواند مقداری بین 0.0000… و 0.9999… نگه دارد. هر عدد
صحیحی مثل 1 در آن سرریز میکند.
خوشبختانه مرده است: ACL واقعی از جدول permission(rid, ut) میخواند —
AdminController.php:74 → Permission::whereUt($user->ut)->get().
نگاشت: نادیده. ثبت میشود چون نشان میدهد اسکیمای legacy در جاهایی بازماندهی بیاستفاده دارد، و اگر کسی بعداً این ستون را ببیند فکر میکند دادهی معنیدار است.
permission → Permission + UserType
permission (rid varchar(255), ut int) -- ۲ ستون، بدون PK
جدول دوستونیِ خالصِ نگاشت. ۱۴۴ rid در مقصد (acl.constants.ts) پوشش داده
شدهاند. بدون primary key — پس ردیف تکراری ممکن است؛ هنگام مهاجرت
DISTINCT شود.
۷. سفارشها — دو جدول جدا → یک مدل
legacy دو جدول کاملاً مستقل دارد، و هیچکدام فیلتر نوع ندارند چون خودِ جدول تفکیککننده است:
| جدول | ستون | نقش | OrderChannel |
|---|---|---|---|
orders |
۱۹ | عمده (B2B) | WHOLESALE |
orders_orders |
۳۷ | خرده (Django-era) | RETAIL |
۷.۱ — orders → Order (عمده)
| legacy | نوع | Prisma | یادداشت |
|---|---|---|---|
customer_id |
int |
customerId |
نگاشت id |
reagent_id |
int |
— | کارشناس سفارش |
type |
tinyint |
— | 🔶 نوع ثبت (تلفنی/تلگرام/…) |
w |
double |
وزن | §۴ |
wage |
double |
اجرت | §۴ |
percent |
double |
درصد اجرت | §۴ · معنای سوم percent |
sts |
tinyint |
OrderStatus |
§۷.۳ |
confirmed |
tinyint(1) |
— | 🔶 جدا از sts |
input/output/remain |
double |
— | طرف دفتر |
price, rial_input, discount |
§۴ | ||
city_id |
int |
— | |
deleted_at |
timestamp |
— | §۵.۶ |
۷.۲ — orders_orders → Order با channel=RETAIL
۳۷ ستون، شامل billing_address, shipping_address, browser_ip,
buyer_accepts_marketing, email, total_tax, use_wallet,
payment_after_wallet, gold_price, gold_price_original.
طبق D13 فروشگاه خرده خارج از دامنه است — پس این جدول حفظ میشود،
پورت نمیشود. ولی Order.channel مقدار RETAIL دارد، یعنی مقصد جای
نگهداشتنش را دارد.
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۶ — به تأخیر افتاد
«الان مهم نیست و در زمان مناسب بهش ورود میکنیم برای تکفروشی.»
orders_orders در این مرحله منتقل نمیشود. نتایج:
- 🟢 خطر تعارض
sts=7فعلاً منتفی است (§۷.۳). سفارش تحویلشده به کنسلشده تبدیل نمیشود، چون هیچ ردیفی منتقل نمیشود. - 🟢
orders_orderitem(۲۹ ستون) هم منتقل نمیشود. - 🟢
orders_sendtimeوorders_fulfillmentهم بیاثر میشوند. - 🟡
OrderChannel.RETAILخالی میماند — انتظارش همین است، باگ نیست. - 🔴 جدول در MySQL دستنخورده نگه داشته شود. وقتی تکفروشی وارد کار شد،
منبع همین جدول است. هیچ
DROPوTRUNCATEرویorders_orders،orders_orderitem،orders_sendtime.
⚠️ بدهی فنیِ ثبتشده: هر وقت تکفروشی وارد شود، جدول نگاشت وضعیتِ
دومی لازم است چون sts در دو جدول معانی متضاد دارد (§۷.۳). این
نمیتواند با همان نگاشت عمده انجام شود.
۷.۳ — 🔴 دو enum وضعیت متضاد (MON-07 در سطح داده)
Order::$STS (عمده):
| # | برچسب | OrderStatus |
|---|---|---|
0 |
معلق | SUSPENDED |
1 |
ثبت سفارش | PLACED |
2 |
تایید مدیر سیستم | ADMIN_APPROVED |
3 |
درحال تامین | SOURCING |
4 |
درحال آماده سازی | PREPARING |
5 |
تحویل مسئول فروش | DELIVERED_TO_AGENT |
6 |
تحویل مشتری | DELIVERED_TO_CUSTOMER |
7 |
CANCELLED |
SingleCustomerOrders::$STS (خرده) — اعداد یکسان، معانی متفاوت:
| # | برچسب خرده | تعارض |
|---|---|---|
1 |
ثبت سفارش | ✅ یکی |
2 |
تایید مدیر سیستم | ✅ یکی |
3 |
در حال تامین | ✅ یکی |
4 |
در حال آماده سازی | ✅ یکی |
5 |
خروج از انبار | 🔴 ≠ تحویل مسئول فروش |
6 |
ارسال | 🔴 ≠ تحویل مشتری |
7 |
تحویل مشتری | 🔴 ≠ کنسل |
7 در یکی «تحویل مشتری» و در دیگری «کنسل شده» است.
OrderStatus در Prisma فقط enum عمده را دارد. پس اگر orders_orders
منتقل شود، sts=7 یعنی سفارشِ تحویلشده به CANCELLED تبدیل میشود —
یک سفارش موفق به کنسلشده. و 5/6 معنای اشتباه میگیرند.
🔶 اگر تصمیم §۷.۲ «منتقل نشود» باشد، این خطر منتفی است. اگر «منتقل شود»، جدول نگاشت دوم و جداگانه لازم است.
۷.۴ — اقلام سفارش
order_items (۲۱ ستون) → OrderItem (۱۱ فیلد).
| legacy | OrderItem |
|---|---|
rash_weight |
rashWeightGramsSnapshot |
percent |
wagePercentSnapshot |
discount |
discountPercentSnapshot |
w |
computedWeightGrams |
rial_pay |
flatRialCharge |
qty |
quantity |
variant_id |
productVariantId |
🔴 original_rash_weight و original_percent خانه ندارند.
وجودِ جفت original_* در legacy یعنی مقدار اولیه برای ممیزی نگه داشته
میشد — تا معلوم باشد وزن یا درصد بعداً دستکاری شده است یا نه.
OrderItem فقط یک نسخه دارد (…Snapshot). پس این تاریخچهی ممیزی
در مهاجرت از بین میرود. 🔶 تصمیم مالک.
🟡 بیخانهی دیگر: product_id (مقصد فقط variantId دارد),
product_title, variant_title (عنوان لحظهی خرید), price, wage,
sts, note.
⚠️ product_title/variant_title در legacy snapshot عنوان بودند —
اگر نام محصول بعداً عوض شود، فاکتور قدیمی نام قدیمی را نشان میداد. مقصد
عنوان را از رابطه میخواند، یعنی فاکتورهای تاریخی با تغییر نام محصول
عوض میشوند.
orders_orderitem (۲۹ ستون، خرده) — تابع تصمیم §۷.۲.
۸. اسناد مالی — cashes → LedgerEntry
cashes با ۳۳ ستون. Cash::$TYPES → LedgerSourceType:
| legacy | برچسب | LedgerSourceType |
|---|---|---|
9 |
سفارش | ORDER_STOCK |
1 |
آب شده | REPAYMENT_MELTED_GOLD |
2 |
حواله | REPAYMENT_TRANSFER |
3 |
چک | REPAYMENT_CHEQUE |
4 |
پولی | REPAYMENT_CASH |
| — | — | ORDER_PRE_ORDER ➕ |
| — | — | WALLET_BONUS ➕ (D13) |
| — | — | ADJUSTMENT ➕ |
نگاشت تمیز است. $STS: -1 بررسینشده · 1 ثبت سند.
$CONFIRMED: 0/1.
🔴 فقط اسناد confirmed=1 در موجودی حساب میشوند (D24). پس مهاجرت باید
confirmed را حفظ کند، نه اینکه همه را تأییدشده فرض کند.
ستونهای مخصوص نوع سند — ✅ مقصد پوشش داده
LedgerEntry عمداً همین ستونها را دارد:
| legacy | LedgerEntry |
|---|---|
ayar |
karatPerMille (Int?) |
lab |
labName |
ang |
hallmark |
account_name |
accountName |
bank_name |
bankName |
to |
transferTo |
original_value |
originalValue |
price |
priceRial |
price_per_gram |
pricePerGramRialAtEntry |
recive_date |
receivedAt (DateTime?) |
note |
note |
confirmed |
confirmed + confirmedAt |
order_id |
orderId |
remain/input/output |
amount + direction |
🟡 بیخانه: balance, value, no (شمارهی سند), proceed_at, title,
user_id, sts, discount.
🔴 ayar در مبدأ varchar(20) است، در مقصد Int?. یعنی اگر عیار
بهصورت متن («۱۸ عیار» یا «750») ذخیره شده، تبدیل عددی لازم است و مقادیر
غیرعددی میشکنند.
🔴 recive_date و proceed_at در مبدأ varchar(255) هستند، نه
timestamp — به احتمال زیاد تاریخ جلالی بهصورت رشتهی فارسی. مقصد
receivedAt را DateTime? گرفته. پس تجزیهی جلالی → میلادی لازم است، نه
CAST. و proceed_at خانه ندارد.
login → Customer با kind=RETAIL
login (۱۹ ستون) جدول مشتری خرده است (fname, lname, percent,
user_type, birth_day, token_sms). طبق D13 حفظ، بدون کار.
۹. کاتالوگ
product → Product (۳۵ ستون)
نگاشتشدنی: title, body_html, product_code, is_active, disabled,
brand_id, branch_id, min_gram, max_gram, show_type,
delivery_type, message_long_delivery, offer_day, is_exist,
has_warranty, desk_note.
🟡 بیخانهی احتمالی: metafields_global_title_tag,
metafields_global_description_tag, tags, meta, supplier_id,
published_at, number_of_purchase, count_fav, count_order,
count_view, inventory_id, indicator_id_id.
⚠️ indicator_id_id و mobile_pic_id و file_id — سه ستون تصویر با
نامگذاری ناهمگون (_id_id دوتایی، بازماندهی Django). در Prisma
ProductImage با ProductImageRole (FEATURED, FEATURED_MOBILE,
GALLERY, GALLERY_MOBILE) جایشان را گرفته. نگاشت سه ستون → نقشها
باید صریح نوشته شود.
products_productvarient → ProductVariant (۲۵ ستون)
⚠️ غلط املایی در نام جدول: varient (صحیح: variant). در SQL مهاجرت
باید عیناً products_productvarient نوشته شود.
🔴 Inventory با I بزرگ. ستون در دامپ `Inventory` int است، ولی
ProductVariant.php:36 در $fillable نوشته 'inventory' (کوچک).
MySQL در لینوکس نام ستون را case-insensitive میگیرد، پس در legacy کار
میکند. Postgres اینطور نیست. اگر کسی کوئری مهاجرت را با inventory
بنویسد، MySQL قبول میکند — ولی هر ابزار سختگیرتری خطا میدهد. در
کوئریها `Inventory` با I بزرگ و داخل backtick نوشته شود.
نگاشت بقیه: grams, rial_pay (اجرت ریالی/سنگ), percentage_pay,
barcode, color (smallint — جدول رنگ لازم), qty, is_pre_order,
is_active, width/length/height, position, product_id_id.
category_collection → Category (۲۴ ستون)
parent_id_id → سلسلهمراتب. ⚠️ image_id_id و image_id و
icon_id — سه ستون تصویر، یکی با _id_id دوتایی. و type و
category_type هر دو وجود دارند.
🔶 ترتیب ۹ دستهی خانه با id عددی هاردکد شده (Q5 در PWA-PORT-PLAN.md).
جدول نگاشت legacyId → id برای این اجباری است.
category_collect (۹ ستون) → CollectionProduct — جدول واسط.
⚠️ سه FK دارد (collection_id_id, post_id_id, product_id_id) که
post_id_id در مقصد معادلی ندارد.
image → UploadedFile (۲۰ ستون)
image (char(100)) مسیر فایل · original_name, ext, width, height.
🟡 بیخانه: resize, resize_path, resize_watermark,
resize_watermark_path, resize_small, resize_small_path, banner,
position, link.
legacy شش ستون برای نسخههای تغییراندازهیافته داشت. اگر مقصد اینها را ندارد، باید تصاویر بازتولید شوند یا فقط نسخهی اصلی بماند.
cities → City (۱۱ ستون)
parent, level, has_children → سلسلهمراتب.
⚠️ percent و percent_aux — معنای چهارم percent!
MIGRATION-STATE.md سه معنا را فهرست کرده؛ این چهارمی است: اضافهبهای اجرت
بهتفکیک شهر.
✅ مقصد هر دو را دارد: City.percent و City.percentAux، هر دو
Decimal(6,3)?، با کامنت صریح که از cities.percent/percent_aux آمدهاند.
نگاشت تمیز است.
branches/workshops → Branch/Workshop
هر دو ۹ ستون، ساختار یکسان (name, address, phone, user_id,
is_active). نگاشت تمیز.
۱۰. بقیهی جداول زنده
| legacy | ستون | مقصد | یادداشت |
|---|---|---|---|
custom_orders |
۱۶ | PreOrder |
weight, color, size, color_stone, color_mina, workshop_id |
custom_order_logs |
PreOrderStatusEvent |
||
order_activity |
OrderStatusEvent |
||
comments |
۹ | Comment |
reply_to خودارجاع |
major_promotion |
۱۵ | Promotion |
type_customer, use_only, discount_percent/discount_price |
major_promotion_ignores |
PromotionExclusion |
||
promotion_products |
۱۳ | PromotionExclusion? |
🔶 ابهام |
inventory_branches |
۹ | BranchAllocation |
count_item, sum_weight |
inventory_transactions |
۹ | BranchAllocationItem |
|
messages |
۹ | SmsMessage |
⚠️ massage — غلط املایی |
notify |
۱۳ | Notification + NotificationRecipient |
read, type_send |
message_notification |
۹ | Notification |
🔶 دو جدول اعلان |
config_statics |
۶ | Setting |
key/value · شامل percent کیف پول (D13) |
fav |
۲ | ❌ مدل ندارد | PWA-PORT-PLAN میگوید جدول تازه لازم است |
logs |
۱۲ | CustomerActivityLog |
|
apk_log |
۴ | 🟡 بیخانه | ip, version — آمار نسخهی اپ |
جداول کوچکی که مقصد ندارند — ✅ بررسی شد
هر شش جدول مدل Eloquent دارند و $fillableشان خوانده شد:
| legacy | $fillable |
چیست | مقصد |
|---|---|---|---|
fav |
customer_id, product_id |
علاقهمندی | ❌ ندارد |
cinies |
title, icon_id, is_active, description, collection_id |
«سینی» — گروهبندی نمایشی محصول | ❌ ندارد |
ciny_products |
product_id, ciny_id |
واسط سینی↔محصول | ❌ ندارد |
kids |
phone, name, num |
فهرست فرزندان (تبریک تولد؟) | ❌ ندارد |
links |
url, counter |
شمارندهی کلیک لینک | ❌ ندارد |
request_fashions |
name, mobile |
درخواست «مد» — فرم تماس | ❌ ندارد |
request_logs |
customer_id, ip, device, data, user_agent, route |
لاگ درخواست | تقریباً CustomerActivityLog |
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۶ — علاقهمندیها لازماند
«بله» — علاقهمندیها مهماند.
🔴 تصحیح ۲۰۲۶-۱۰-۰۷: منبع درست fav نیست. fav مال فروشگاه خرده است
(Customer\FavController، guard api-singleCustomer). اپ عمده
major_favorites (۳٬۳۰۵ ردیف) را میخواند — §۱۰.۷. متن زیر دربارهی
fav نوشته شده؛ تصمیم مالک و مدل CustomerFavorite همان میمانند، فقط منبع
عوض شد.
یک migration افزایشی لازم است. چون additive است، با PROTECTED.md §G
تضادی ندارد و قابلیت موجودی را لمس نمیکند.
مدل پیشنهادی — سادهترین چیزی که legacy را پوشش میدهد:
model CustomerFavorite {
customerId String
customer Customer @relation(fields: [customerId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
createdAt DateTime @default(now())
@@id([customerId, productId])
}
⚠️ fav در legacy فقط دو ستون دارد و هیچ PK ندارد — پس ردیف تکراری
ممکن است. @@id([customerId, productId]) این را میبندد، ولی مهاجرت باید
DISTINCT بزند وگرنه روی کلید مرکب میشکند.
⚠️ Product.countFav در legacy یک شمارندهی denormalized بود
(product.count_fav). اگر مقصد آن را ندارد، از CustomerFavorite محاسبه
شود — نه اینکه صفر نشان داده شود (§۵.۲ گروه ب).
🔶 cinies/ciny_products هنوز تصمیم ندارند. ساختار نمایشیاند
(title, icon_id, collection_id) — اگر صفحهی خانهی اپ از آنها
میخواند، نبودشان چیدمان را خالی میکند. این دادهی کاربر نیست، پس
فوریتش کمتر از fav است.
kids, links, request_fashions کماهمیتاند و رهاکردنشان قابلدفاع است.
۱۰.۴ — نتایج پیشبررسی روی دادهی واقعی (۲۰۲۶-۱۰-۰۶)
خروجی migration-precheck.sql. این بخش فرضهای
بخشهای قبل را با عدد واقعی تأیید یا رد میکند.
حجم کار — کوچکتر از انتظار
| جدول | زنده | حذفشدهی نرم | نکته |
|---|---|---|---|
customers |
۱٬۶۱۶ | ۲۱ | |
users |
۲۲ | ۰ | ادمینها |
orders |
۵۰۸ | ۲۰۸ | ۲۹٪ حذفشده |
order_items |
۱٬۰۶۷ | ۴۵۳ | ۳۰٪ |
cashes |
۶۴۸ | ۲۶۵ | ۲۹٪ |
product |
۳٬۲۸۷ | ۲٬۰۱۵ | ۳۸٪ حذفشده |
variant |
۷٬۴۷۴ | ۶٬۱۹۴ | 🔴 ۴۵٪ حذفشده |
image |
۱۸٬۰۷۷ | ۰ | فایلها باید کپی شوند |
messages |
۲۴٬۴۸۰ | ۰ | بزرگترین جدول |
fav |
۲۵۱ | — | |
permission |
۷۲۸ | — | ۱۴۴ rid × چند ut |
🔴 soft-delete حجم بالایی دارد — ۶٬۱۹۴ واریانت و ۲٬۰۱۵ محصول حذفشدهاند.
§۵.۸ میگوید مقصد deleted_at ندارد. پس قاعدهی
WHERE deleted_at IS NULL نه یک احتیاط، بلکه تفاوت بین ۱٬۲۸۰ و ۷٬۴۷۴
واریانت است. بدون آن، کاتالوگ شش برابر میشود و محصولات حذفشده زنده
برمیگردند.
✅ خبر خوب — سه نگرانی منتفی شد
| نگرانی | پیشبینی | واقعیت |
|---|---|---|
| کد ملی تکراری | محتمل | ۰ گروه ✅ |
name ناسازگار با اجزا |
نامعلوم | فقط ۴ ردیف ✅ |
| اجزای نام تهی | نامعلوم | ۰ ✅ — §۲ کامل تأیید شد |
fav تکراری |
محتمل | ۰ ✅ |
permission تکراری |
محتمل | ۰ ✅ |
ayar غیرعددی |
محتمل | ۰ از ۳۳ ✅ تبدیل به Int بیخطر |
یتیم city_id/image_id/سفارش/سند |
نامعلوم | ۰ ✅ |
customers_copy1/2 ردیف یکتا |
محتمل | ۰ ✅ بیخطر نادیده بگیرید |
§۲ قطعی شد: empty_fname=0, empty_lname=0, parts_empty_name_set=0.
یعنی هر ۱٬۵۹۵ ردیف اجزای نام پر دارند و تجزیهی name مطلقاً لازم
نیست. D0 با اطمینان کامل تصحیح میشود.
🔴 سه مسدودکنندهی تأییدشده
۱. موبایل تکراری — ۲۷ گروه، ۵۹ ردیف.
@unique روی mobile قطعاً میشکند. تصمیم مالک: «مسئول فروش برنده است».
🔶 ولی اگر هر دو ردیفِ تکراری مشتری عادی باشند (نه کارشناس)، این قاعده
جواب نمیدهد. قاعدهی پشتیبان لازم است — پیشنهاد: ردیف با
updated_at جدیدتر برنده، بقیه با پسوند در جدول نگاشت ثبت شوند تا دستی
بررسی شوند.
۲. کد ملی تهی — ۱٬۵۹۵ از ۱٬۵۹۵ ردیف... یعنی همه.
empty_ncode = 1595 و n_customers = 1595. هیچ مشتری کد ملی ندارد.
ولی Prisma:
nationalCode String @unique // ← NOT NULL
🔴 مهاجرت روی اولین ردیف میشکند. و @unique روی رشتهی خالی یعنی
فقط یک ردیف میتواند '' باشد.
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — nullable شود
«وقتی دیتاست مبدا فاقد این داده است، هیچ گزینه دیگری جز Nullable کردن فیلد وجود ندارد. ساختن کد ملی فیک تخلف دیتایی است و مانع از احراز هویتهای قانونی بعدی میشود.»
nationalCode String? @unique // بود: String @unique
⚠️ نکتهی فنی که به نفع این تصمیم است: در Postgres مقدار NULL در
ایندکس @unique تکراری حساب نمیشود. پس هزار ردیف با
nationalCode = NULL بیمشکل کنار هم مینشینند — ولی اگر بهجای NULL
رشتهی خالی '' بگذاریم، فقط یکی جا میشود.
🔴 قاعدهی اجباری ابزار: '' را به NULL تبدیل کند، نه اینکه خالی
بفرستد.
۳. گِردکردن رخ میدهد — ۲۴ + ۳۲ ردیف.
gold_needs_rounding = 24 و order_weight_needs_rounding = 32. یعنی §۴
درست بود: «صفر اختلاف» دستنیافتنی است و آستانهی عددی لازم است.
مبنای تطبیق برای ابزار:
sum_gold_debt 427.5041945329501 ← ۱۶ رقم معنادار، شاهد خودِ مشکل
sum_rial_debt 250000
sum_credit_limit 423623.33999999997 ← .33999999997 یعنی 423623.34
n_nonzero_gold 35 n_negative_gold 12
⚠️ ۱۲ مشتری بدهی طلای منفی دارند (طلبکارند). مقصد Decimal علامتدار
است، پس مشکلی نیست — ولی تطبیق باید علامت را هم چک کند.
🔴 cashes — همهی ستونهای مبلغ NULL هستند
cash_amount_needs_rounding NULL
confirmed_only sum_gold=NULL sum_rial=NULL n=375
SUM() فقط وقتی NULL برمیگرداند که هیچ ردیف غیرNULL نباشد. یعنی
cashes.remain و cashes.rial_remain در هر ۶۴۸ ردیف NULL هستند.
پس مبلغ سند مالی در این دو ستون نیست. جای دیگری است — value,
original_value, input/output, یا price. §۸ نگاشت
remain/input/output → amount + direction را پیشنهاد داده بود؛
این نگاشت باید بازبینی شود.
🔶 یک کوئری تکمیلی لازم است (§۱۰.۶) تا معلوم شود مبلغ در کدام ستون است.
بدون این، LedgerEntry.amount صفر یا NULL پر میشود و کل دفتر مالی
خالی منتقل میشود.
✅ نکتهی خوب: confirmed=1 در ۳۷۵ و confirmed=0 در ۸ ردیف — D24
(«فقط تأییدشدهها در موجودی») قابل اجراست. مجموع ۳۸۳ از ۶۴۸، پس ۲۶۵ ردیف
confirmed تهی دارند که با حذفشدهها همخوانی دارد.
🔴 رمز عبور — ۱٬۵۹۰ از ۱٬۵۹۵ تهی
pw_bcrypt 5 pw_other 0 pw_empty 1590
تقریباً هیچ مشتری رمز ندارد. منطقی است: ورود مشتری با OTP است، نه رمز. ولی Prisma:
passwordHash String // ← NOT NULL
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — hash غیرقابلورود، nullable نشود
«برداشتن اجبار رمز (Nullable کردن پسورد) یک ضدالگوی امنیتی است و احتمال بایپس شدن احراز هویت در باگهای آینده را بالا میبرد. نباید صرفاً یک رشته تصادفی پلین یا هش ضعیف بگذارید.»
passwordHash اجباری میماند. مقدارش برای ۱٬۵۹۰ ردیف یک hash
عامدانه غیرقابلورود میشود.
🔴 تفکیک مهمی که مالک درست روی آن تأکید کرد: «رمز تصادفی» و «hash غیرقابلورود» یکی نیستند.
| روش | نتیجه |
|---|---|
| ❌ رشتهی تصادفیِ خام در ستون | bcrypt.compare() همیشه false میدهد — ولی قالبش نامعتبر است و بعضی کتابخانهها در این حالت throw میکنند یا رفتار نامعین دارند |
| ❌ hash یک رمز تصادفی | یک رمز واقعی وجود دارد که کار میکند؛ اگر جایی لاگ شده باشد، در دسترس است |
| ✅ قالب نامعتبرِ عامدانه | هیچ رمزی به آن نمیخورد، و قالبش صریحاً «غیرفعال» را اعلام میکند |
روش پیشنهادی — پیشوند ! که در Django و Rails قرارداد است:
!OTP_ONLY_NO_PASSWORD_LOGIN
چرا کار میکند: bcrypt.compare(anyPassword, "!OTP_ONLY...") هرگز true
نمیدهد چون رشته با $2a$/$2b$/$2y$ شروع نمیشود، پس هیچ رمزی
نمیتواند با آن مطابقت کند. و برخلاف رشتهی تصادفی، خواندنی است —
هر کسی که ستون را ببیند میفهمد این کاربر فقط OTP دارد.
⚠️ بررسی کد مقصد — نگرانی باریکتر از آن است که بهنظر میرسد
مالک نگران «بایپس شدن احراز هویت در باگهای آینده» بود. کد مقصد خوانده شد و دو واقعیت پیدا شد:
۱. مشتری اصلاً مسیر ورود با رمز ندارد.
grep روی کل apps/api/src نشان داد تنها دو جای bcrypt.compare روی
رمز اینهاست: auth.service.ts:103 و :135 — و هر دو داخل
AuthService.login() هستند که prisma.adminUser.findUnique() میخواند
(auth.service.ts:40)، نه customer.
ورود مشتری در customer-auth.service.ts است و تنها bcrypt.compare
آن روی challenge.codeHash است (خط ۳۶۱) — کد OTP، نه رمز.
Customer.passwordHash در هیچ مسیر احراز هویتی خوانده نمیشود.
۲. مقصد از قبل passwordHash: '' مینویسد.
در پنج جا: customer-auth.service.ts:261,
admin-customers.service.ts:626,690, accounting-sync.service.ts:211,
admin-branches.service.ts:330.
یعنی مشتریهایی که همین حالا از طریق OTP یا پنل ادمین ساخته میشوند، رشتهی خالی دارند. این وضع موجود است، نه چیزی که مهاجرت معرفی کند.
پس نتیجه
| مقدار پیشنهادی مهاجرت | !OTP_ONLY_NO_PASSWORD_LOGIN |
| ریسک بایپس | صفر — مشتری مسیر ورود با رمز ندارد |
| چرا با این حال انجام شود | دفاع در عمق (خواست مالک) + خوانا بودن |
✅ و یک سود جانبی: چون مقصد الان '' مینویسد، این روش بهتر از
وضع موجود است. پیشنهاد میکنم آن پنج جا هم به همین مقدار تغییر کنند تا
یکدست شود — ولی این تغییر کد است، نه مهاجرت، و اجازهی جداگانه
میخواهد.
✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷ — محافظ در لایهی سرویس
«تست واحد الزامی است؛ اگر متد کرش کند، باید در لایه سرویس مقصد چک شود که اگر فرمت هش نامعتبر یا پیشوند
!بود، مستقیماً بدون فراخوانی bcrypt خروجی false برگردد.»
پذیرفته شد — و این روش از تستکردن تنها بهتر است، چون به رفتار کتابخانه وابسته نمیماند:
// قبل از هر فراخوانی bcrypt
if (!passwordHash || !passwordHash.startsWith('$2')) {
return { ok: false }; // اکانت فقط-OTP یا هش نامعتبر
}
return { ok: await bcrypt.compare(dto.password, passwordHash) };
جای اعمال: auth.service.ts:103 و :135 (هر دو مسیر verifyPassword).
⚠️ این محافظ از '' فعلی هم محافظت میکند — یعنی باگی را میبندد
که همین حالا وجود دارد، مستقل از مهاجرت.
🔶 تست واحد همچنان لازم است (خواست مالک)، ولی نقشش عوض شد: دیگر «آیا bcrypt کرش میکند؟» نیست، بلکه «آیا محافظ قبل از bcrypt جلو را میگیرد؟». در این محیط نتوانستم اجرا کنم (بررسیکنندهی سمت سرور خطا داد)، پس ادعا نمیکنم تست شده است.
🔶 تسک رفکتور جداگانه — ثبت شد
«تغییر آن پنج جا در اسکوپ پایپلاین مهاجرت نیست، اما به عنوان یک تسک رفکتور ثبت شود تا در یک مرج ریکوئست جداگانه اصلاح گردد.»
ثبت شد. پنج جایی که passwordHash: '' مینویسند:
| فایل | خط |
|---|---|
customer-auth.service.ts |
۲۶۱ |
admin-customers.service.ts |
۶۲۶ · ۶۹۰ |
accounting-sync.service.ts |
۲۱۱ |
admin-branches.service.ts |
۳۳۰ |
خارج از اسکوپ مهاجرت. PR جداگانه، بعد از مهاجرت.
⚠️ آن ۵ رمز $2y$ (bcrypt) هستند و مقصد هم bcrypt است، پس بدون
تغییر منتقل میشوند و همانطور کار میکنند.
enum ها — دو نکته
type IS NULL → ۵۸ ردیف. §۵.۳ هشدار داده بود. این ۵۸ بیصدا
APP_SHOPKEEPER میشوند. 🔶 تصمیم مالک: درست است یا قاعدهی دیگری لازم
است؟ (۵۸ از ۱٬۶۱۶ ≈ ۳.۶٪)
sts و active مقدار -1 دارند، نه 0.
sts: 1→۱۱۹۱, -1→۴۰۴ و active: 1→۱۳۶۸, -1→۲۲۷.
نگاشت 1→true, -1→false که §۵.۱ نوشته بود درست است ✅.
orders.sts هر ۷ مقدار 0..6 را دارد، هیچ 7 — پس نگاشت
OrderStatus کامل است ✅ و CANCELLED در عمده استفاده نشده.
cashes.type هر ۵ مقدار (1,2,3,4,9) موجودند و همه نگاشت دارند ✅.
تکفروشی (به تأخیر افتاده) — تأیید خطر
orders_orders 520 live retail_sts: 7 → ۳ ردیف
۳ ردیف sts=7 وجود دارند — یعنی خطر §۷.۳ واقعی بود: اگر روزی منتقل
شوند، ۳ سفارشِ «تحویل مشتری» به CANCELLED تبدیل میشوند. ثبت شد برای
آینده.
⚠️ ۱۴ یتیم در fav
fav.customer_id → customers ۱۴ یتیم دارد. چون fav خودش PK ندارد و FK
هم نیست، اینها ردیفهاییاند که مشتریشان حذف شده. در مهاجرت باید
نادیده گرفته شوند وگرنه FK مقصد میشکند.
۱۰.۵ — 🔴 مهاجرت merge است، نه import
تصمیم مالک ۲۰۲۶-۱۰-۰۶: دادهی حسابداری و تصویر واریانت در سیستم جدید واقعی است. پس دیتابیس مقصد خالی نیست.
این فرض بنیادی سند را عوض میکند:
import (فرض اولیه) |
merge (واقعیت) |
|
|---|---|---|
| مقصد | خالی | دادهی واقعی دارد |
| خطا در میانه | دور بریز، از نو | 🔴 دادهی واقعی خراب شد |
| تکرارپذیری | آزاد | فقط با idempotency واقعی |
UPDATE کامل ردیف |
بیخطر | 🔴 ممنوع |
ستونهایی که ابزار مهاجرت هرگز نباید بنویسد
اینها در legacy هیچ معادلی ندارند و از سامانههای مقصد پر میشوند.
یک upsert معمولی یا UPDATE کامل، همه را NULL میکند.
| ستون | مدل | پر میشود از | اگر پاک شود |
|---|---|---|---|
accountingGoldBalance |
Customer |
نرمافزار حسابداری (D27) | موجودی طلای مشتری گم میشود |
accountingRialBalance |
Customer |
نرمافزار حسابداری | موجودی ریالی گم میشود |
accountingBalanceAt |
Customer |
نرمافزار حسابداری | زمان آخرین sync گم میشود |
accountingStockWeight |
Product |
نرمافزار حسابداری | موجودی وزنی کارساخته گم میشود |
accountingStockCount |
Product |
نرمافزار حسابداری | موجودی تعدادی گم میشود |
accountingStockAt |
Product |
نرمافزار حسابداری | زمان آخرین sync گم میشود |
imageId |
ProductVariant |
انتخاب دستی ادمین | تصویر هر واریانت گم میشود |
بهعلاوه سه جدول کامل که هیچ مبدأ legacy ندارند و باید کاملاً نادیده
گرفته شوند: AccountingLink, AccountingOutbox, AccountingIssue.
🔴 ProductVariant.imageId — حدس زده نشود
products_productvarient در legacy هیچ ستون تصویر ندارد (۲۵ ستون،
بررسی شد). پس این قابلیت مقصد-تنها است و PROTECTED.md §B آن را
«additive only» اعلام کرده.
قاعده: مهاجرت imageId را لمس نمیکند — نه مینویسد، نه NULL میکند.
اگر ابزار بخواهد از file_id محصول حدس بزند، تصویرِ انتخابیِ ادمین با
حدس جایگزین میشود.
⚠️ ProductImage با onDelete: SetNull به واریانت وصل است. پس اگر مهاجرت
تصاویر محصول را حذف و از نو بسازد (الگوی رایج در sync)، imageId هر
واریانت بیصدا NULL میشود — بدون هیچ خطایی. تصاویر باید
افزایشی بهروز شوند، نه جایگزین.
قاعدهی اجباری ابزار
ابزار مهاجرت فقط ستونهایی را مینویسد که نگاشت صریح legacy دارند. فهرست ستونهای نوشتنی سفید است (allow-list)، نه سیاه.
همان الگوی «ارسال تفاضلی» که در CHANGELOG-PORT-HISTORY.md §۵ برای فرم
مشتری اعمال شد — چون همان اشتباه (Object.assign ناقص) آنجا هم چهار ستون
را پاک میکرد.
✅ کلید تطبیق — تصمیم مالک ۲۰۲۶-۱۰-۰۶
| سؤال | جواب |
|---|---|
| کلید تطبیق | mobile |
| در تعارض کدام برنده | مسئول فروش |
| اهمیت | کم — دادههای مقصد فعلاً روی لوکالاند |
نتیجهی عملی: چون دادهی مقصد لوکال است و اهمیتش کم، ریسک merge
بسیار کمتر از آن است که §۱۰.۵ فرض کرده بود. ولی قاعدهی allow-list
همچنان لازم است، به دو دلیل:
- همان ابزار بعداً روی staging و تولید اجرا میشود (
GO-LIVE §۸)، آنجا داده واقعی است - تصویر واریانت و اتصال حسابداری همین الان واقعی هستند (گفتهی مالک)
⚠️ تداخل mobile با کلیدِ تطبیق تعارض دارد. ۲۷ گروه موبایل تکراری در
مبدأ وجود دارد (§۱۰.۴). اگر کلید تطبیق mobile باشد، برای ۵۹ ردیف
معلوم نیست به کدام ردیف مقصد وصل شوند. قاعدهی «مسئول فروش برنده» فقط
۱۰ مورد تداخل customers↔users را حل میکند، نه ۲۷ گروه داخلی را.
🔶 قاعدهی پشتیبان پیشنهادی: در گروه تکراری، ردیف با updated_at جدیدتر
برنده؛ بقیه منتقل میشوند ولی در گزارش «نیاز به بررسی دستی» فهرست شوند.
چون اهمیت کم است، میتوان این را به گزارش سپرد و دستی حل کرد.
۱۰.۵.۵ — فهرست کامل تغییرات افزایشی مقصد
همهی تصمیمهای مالک تا ۲۰۲۶-۱۰-۰۷، در یک migration جمع میشوند.
همه افزایشیاند — هیچ ستونی حذف یا تغییر نوع نمیدهد، پس با
PROTECTED.md §G و قابلیتهای حسابداری/تصویر واریانت/پیامک تضادی
ندارند.
model Customer {
nationalCode String? @unique // بود: String — §۱۰.۴
birthDate DateTime? // legacy birth_date — §۵.۲
verificationRejectionReason String? // §۵.۵
verificationRejectedAt DateTime?
// cityNameRaw String? // §۵.۹ — منتظر جواب کوئری سوم
favorites CustomerFavorite[] // §۱۰
}
enum VerificationStep {
UNVERIFIED
STEP_ONE_SUBMITTED
STEP_TWO_SUBMITTED
VERIFIED
REJECTED // §۵.۵
}
model CustomerFavorite { // §۱۰ — legacy fav
customerId String
customer Customer @relation(fields: [customerId], references: [id])
productId String
product Product @relation(fields: [productId], references: [id])
createdAt DateTime @default(now())
@@id([customerId, productId])
}
و یک تغییر کد (نه schema) در auth.service.ts:103,135 — محافظ پیشوند
$2 قبل از bcrypt.compare (§۱۰.۴).
⚠️ طبق D14: migrate dev در توسعه، و SQL idempotent
(IF NOT EXISTS) چون کانتینر مهاجرتها را خودش اجرا میکند (D15).
✅ وضعیت اجرا — ۲۰۲۶-۱۰-۰۷
شاخه: migration/legacy-data (از 19d490e؛ release/delivery دست نخورد).
| مورد | وضعیت | تأیید |
|---|---|---|
migration 20261007120000_migration_prep_additive |
✅ اعمال شد | migrate diff دیتابیس زنده ↔ schema: خالی |
۴ ستون Customer |
✅ همه nullable | information_schema |
CustomerFavorite |
✅ ساخته شد | to_regclass |
VerificationStep.REJECTED |
✅ | pg_enum · Postgres 15.19 |
نوع پارامتر accounting-sync.service.ts:233 |
✅ string | null |
رفتار درگاهها صفر تغییر |
محافظ $2 در auth.service.ts |
✅ compareLocalHash() |
۴ تست تازه |
ممیزی مصرفکنندگان REJECTED |
✅ | پایین |
| تستها | ✅ ۱۷۸ از ۱۷۸ | ۱ suite از قبل خراب — پایین |
| API زنده | ✅ | عمومی ۲۰۰ · ادمین بیتوکن ۴۰۱ · رمز غلط ۴۰۱ · رمز درست ۲۰۰ |
🔴 خطر فرایندی کشفشده — فایل migration = اعمال خودکار
migration بدون تأیید اعمال شد — نه توسط من. زنجیره:
- سرور توسعه با
nest start --watchاجرا میشود - ویرایش هر فایلی زیر
src/سرور را ریاستارت میکند - در boot،
bootstrap.ts:35تابعrunMigrations()را صدا میزند که هر فایل معلق درprisma/migrationsرا اعمال میکند (D15)
ویرایش accounting-sync.service.ts (برای رفع خطای نوع) ریاستارت را زد و
migration در 13:42:36 در _goldbank_migrations ثبت شد — پیش از تأیید
مالک. نتیجه درست و تأییدشده است، ولی روال «اول نشان بده، بعد اجرا کن»
(D0) روی این ماشین کار نمیکند.
قاعده از این پس: تا تأیید نشده، migration بیرون از
prisma/migrations نوشته شود (مثلاً docs/ یا scratchpad)، یا سرور
توسعه متوقف باشد. فقط پس از تأیید به پوشه منتقل شود.
⚠️ دفتر CLI پریزما ۹ migration عقب است (از قبل)
prisma migrate status نُه migration را «اعمالنشده» میداند، از
20260922000000_customer_otp تا همین تازه — چون این ماشین migration ها را
از مسیر runner خود اپ (_goldbank_migrations) گرفته، نه CLI. دیتابیس
کامل است (diff خالی). پیامد: روی این ماشین prisma migrate dev اجرا
نشود — میخواهد ۹ فایل را دوباره اجرا کند. همگامسازی با
prisma migrate resolve --applied <name> برای هر ۹ مورد ممکن است، ولی در
_prisma_migrations مینویسد و تأیید مالک میخواهد (D14).
ممیزی مصرفکنندگان REJECTED
| جا | پیش از رفع | وضعیت |
|---|---|---|
admin-customers.service.ts verifyStepOne() |
🔴 default → 2 (پذیرفته). نما سوییچهای سفارش/حساب را روی step_one === 2 باز میکند → مشتری ردشده سوییچ باز میگرفت |
✅ صریحاً 3 |
همان، verifyStepTwo() |
default → 3 (درست، تصادفی) |
✅ صریح |
ProfileView.vue هر دو اپ |
برچسب نداشت → کلمهی انگلیسی REJECTED نمایش داده میشد |
✅ «رد شده» |
فیلتر «تاییدنشدهها» (:221) |
فقط STEP_ONE/TWO_SUBMITTED |
✅ بدون تغییر — ردشدهها در صف برنمیگردند (خواست مالک) |
| روتر هر دو اپ · دکمهی تکمیل پروفایل | ردشده مثل UNVERIFIED رفتار میشود |
✅ مطابق legacy — پایین |
default در هر دو تابع حذف شد تا switch جامع باشد: اگر روزی مقدار
ششمی به enum اضافه شود، TypeScript خطا میدهد و همین باگ بیصدا تکرار
نمیشود.
🔶 دو سؤال برای مالک — از ممیزی
۱. رد در سیستم جدید REJECTED نمینویسد.
verify() در admin-customers.service.ts:955-969 هنگام رد:
گام ۱ → UNVERIFIED، گام ۲ → STEP_ONE_SUBMITTED. یعنی ۷ مشتری مهاجر
REJECTED میشوند، ولی هر ردِ تازه در پنل همان کاری را میکند که مالک
برای مهاجرت «کاملاً مردود» خواند (برگشت به صف). و
verificationRejectionReason که مالک «برای نیازمندیهای آتی» خواست، از
هیچ مسیری پر نمیشود — فرم ادمین فیلد علت ندارد.
پیشنهاد: verify() هنگام رد REJECTED + verificationRejectedAt
بنویسد، و یک فیلد اختیاری علت به دکمهی رد اضافه شود. تغییر رفتار یک
جریان زنده است — تأیید لازم.
۲. مشتری ردشده با ارسال دوبارهی مدارک خودکار تأیید میشود.
legacy دکمهی تکمیل پروفایل را فقط وقتی sts_verify_step_two برابر ۱ یا
۲ بود پنهان میکرد (طبق کامنت پورت در ProfileView.vue:122-126)، پس ردشده (۳) آن را
میدید؛ و ارسال، بیدرنگ VERIFIED میکند چون legacy خط «منتظر
کارشناس» را کامنت کرده بود (customer-profile.service.ts:16-19). پورت
همین است. نتیجه: «رد» در عمل یک «دوباره بفرست» است، نه بستن پرونده.
اگر مالک میخواهد رد واقعاً ببندد، این تصمیم محصولی است، نه پورت.
⚠️ sms.service.spec.ts از قبل خراب است
دو خطای TS2339: Property 'platform' does not exist (خطوط ۸۳، ۱۰۲). این
فایل و sms.service.ts در این شاخه دست نخوردهاند — خرابی در commit
19d490e است. به همین دلیل آن suite کامپایل نمیشود؛ ۱۷۸ تست بقیه سبزند.
خارج از اسکوپ این کار، ثبت میشود.
۱۰.۶ — پرسش دوم از دیتابیس
📄 migration-precheck-2.sql — چهار سؤالی
که خروجی اول باز گذاشت. مثل فایل اول: فقط COUNT/SUM، صفر نوشتن، هیچ
ستون متنی در خروجی (تنها استثنا: products_productimage.w که یک برچسب
اندازه است، نه دادهی شخصی).
| # | سؤال | چرا مسدودکننده است |
|---|---|---|
| ۱ | ✅ حل — §۱۰.۷ | |
| ۲ | آیا «مسئول فروش برنده» برای ۲۷ گروه تکراری کافی است؟ | اگر گروهی کارشناس نداشته باشد، قاعده بیاثر است |
| ۳ | سه ستون تصویر → چهار نقش ProductImageRole |
نگاشت غلط = تصویر غلط روی محصول |
| ۴ | تأیید §۵.۷ — reagent_id به customers اشاره میکند؟ |
اگر غلط باشد، رابطهی کارشناس معرف از بین میرود |
۱۰.۷ — یافتههای دامپ کامل (oapi.sql — ۲۰۲۶-۱۰-۰۷)
oapi.sql (دامپ Navicat با داده، تاریخ ۰۴/۱۰/۲۰۲۶، ۶۳ مگابایت) در یک MySQL
محلی روی Docker بارگذاری و هر ادعا با کوئری روی همان داده بررسی شد. این
بخش ابتدا توسط نشست موازی gold-bank-79 نوشته شد. چند ادعای آن نسخه
غلط بود و اینجا تصحیح شده است (جدول پایین).
⚠️ دامپ دو روز قدیمیتر از اجرای پیشبررسی روی سرور زنده است (مثلاً ۷٬۴۴۳ تنوع اینجا در برابر ۷٬۴۷۴ زنده). برای ساخت و آزمون کافی است. برای مهاجرت واقعی دامپ تازه گرفته شود.
oapi.sql دادهی شخصی مشتریان است: در .gitignore قرار گرفت و هرگز commit
نشده است.
تصحیح ادعاهای نسخهی قبلی این بخش
| ادعای قبلی | واقعیت (کوئری روی داده) |
|---|---|
remain/rial_remain صفرند |
NULL هستند — در هر ۳۸۳ ردیف زنده؛ ساختار DEFAULT NULL دارد |
new_city شناسهی عددی cities5 است |
هر ۱۷۲ مقدار نام شهرِ تایپشده است؛ صفر مقدار عددی — §۵.۹ |
birth_date همه بین ۱۹۵۰ و ۲۰۱۰، بدون نیاز به پالایش |
۳۵۷ مورد بعد از ۲۰۱۰ تا ۲۰۳۱؛ ۱۹۰ مورد برابر تاریخ ثبتنام |
type ۳ و ۴ و ۵ نامعلوم، نیاز به تأیید مالک |
در Customer::$TYPES (Customer.php:85-92) تعریف شدهاند — §۵.۳ |
LedgerEntry.goldGrams = value، rialAmount = price |
این فیلدها وجود ندارند؛ مدل amount + currency + direction است |
| شمارش انواع سند: ۹ → ۴۸۲، ۲ → ۶۳، … | شامل حذفشدهها بود؛ زنده: ۹ → ۲۸۰، ۲ → ۳۷، ۱ → ۳۳، ۴ → ۳۲، ۳ → ۱ |
علاقهمندی = fav (۲۵۱) |
fav مال فروشگاه خرده است (guard api-singleCustomer)؛ اپ عمده major_favorites (۳٬۳۰۵) — پایین |
درست بود: مبلغ سند در value (گرم) و price (ریال) است؛ پوشهی تصاویر
F:\New folder (2)\storage با ۱۷٬۸۳۲ فایل (۱.۸ گیگابایت) وجود دارد.
🔴 فرمول بدهی legacy — با داده تأیید شد
CustomerController::updateRemain (Api/CustomerController.php:42-51):
remain = Σ orders.input (sts > 4) − Σ cashes.value (confirmed = 1, type 1–4)
rial_remain = Σ orders.rial_input (sts > 4)
هر دو مدل SoftDeletes دارند، پس حذفشدهها بیروناند. بازمحاسبه روی
دامپ: طلا ۱٬۵۹۰ از ۱٬۵۹۱ مشتری، ریال ۱٬۵۹۱ از ۱٬۵۹۱ با ماندهی ذخیرهشده
برابر است. جمع کل ۴۲۷٫۵۰۴۲ گرم — همان عدد پیشبررسی روی سرور زنده.
سه پیامد که قبلاً دیده نشده بود:
- سند نوع ۹ («سفارش») در بدهی نیست. بدهی از جدول
ordersمیآید؛ نوع ۹ فقط آینهی آن در فهرست است. انتقال هر دو = دوبار شمردن بدهی هر سفارش. - ریال هرگز بدهی ریالی را کم نمیکند. پرداخت پولی (نوع ۴) با قیمت روز به
گرم تبدیل میشود (
price ÷ (price_per_gram ÷ 4.331802)— قیمت هر مثقال) و بدهی طلا را کم میکند. - چک (نوع ۳) اثری در مانده ندارد — مقدار گرمیاش صفر است.
تنها استثنا — مشتری #125: ماندهی ذخیرهشده −۹٫۸۹۵۱ گرم، بازمحاسبه صفر.
تنها سندش (۴۸٫۳۴ گرم آبشده) در ۲۰۲۱ حذف شده و مشتری در ۲۰۲۲ دوباره
ویرایش شده — مانده بهاحتمال زیاد دستی وارد شده (ستون در $fillable
بود، SEC-14). ✅ تصمیم مالک ۲۰۲۶-۱۰-۰۷: همان −۹٫۸۹۵۱ منتقل شود، بهصورت
یک سند صریح «تراز انتقالی».
✅ تاریخ تولد — پالایش (تصمیم مالک ۲۰۲۶-۱۰-۰۷)
از ۹۱۵ تاریخ ثبتشده فقط ۵۴۱ واقعی است:
| دسته | تعداد | چرا کنار گذاشته شد |
|---|---|---|
| برابر روز ثبتنام | ۱۹۰ | پیشفرض فرم — در سال ۲۰۲۵ از ۱۷۶ مورد، ۱۶۵ |
| ساعت دارد یا سال بعد از ۲۰۰۸ | ۱۸۴ | تاریخ تولد نمیتواند باشد |
| نگه داشته شد | ۵۴۱ |
✅ شهر — نگاشت دوگانه (تصمیم مالک ۲۰۲۶-۱۰-۰۷)
ثبتنام اپ روی همه city_id = 5 (تهران) میگذاشت
(Major/CustomerController.php:21) و شهر واقعی را در new_city بهصورت
متن. از ۹۲ مشتری که هر دو را دارند، ۹۱ همان تهرانِ اجباریاند.
- متن تایپشده هست → عیناً در
Customer.cityNameRaw؛cityIdفقط با تطابق دقیق نام (پس از یکسانسازی «ي/ك») — تهرانِ اجباری نادیده گرفته میشود - متن نیست → همان
city_idقدیم
یکسانسازی «ي/ك» اهمیت داشت: تطابق با جدول ۱۵تایی از ۳۲ به ۷۲ رسید.
نقشها — تداخل شماره (config_statics)
نوع کاربر در legacy یک ردیف config_statics است (UserController.php:26).
عناوین: ۸۵ مدیر · ۸۶ حسابدار · ۱۰۲ ویرایشگر · ۱۰۴ مدیر اصلی · ۱۰۵ مدیران
· ۱۰۸ انباردار · ۱۳۲ بیکار · ۱۳۵ سئو · ۱۳۹ مدیر شعبه.
🔴 در مقصد (seed) ۸۶ = کارشناس فروش. کپی شمارهبهشماره دسترسی ۷۱ گانهی حسابدار را به همهی کارشناسان میداد. ابزار با عنوان نگاشت میکند (۸۶ حسابدار → ۸۷ حسابدار)؛ فقط ۸۵ با ۸۵ برابر گرفته شده (هر دو نقش تمامدسترسیاند).
۷ کارشناس فروش با موبایل یک کاربر ادمین مشترکاند — ۳ تا مدیر ارشد. قاعدهی «کارشناس برنده» روی آنها مدیر ارشد را به کارشناس تنزل میداد و از پنل بیرون میکرد. ابزار نقش ادمین را نگه میدارد و معرفیها به همان ادمین وصل میشوند.
علاقهمندیها — جدول درست major_favorites است
fav (۲۵۱) را Customer\FavController زیر guard api-singleCustomer
(ورود مشتری خرده) مینویسد؛ customer_id آن به جدول دیگری اشاره دارد. اپ
عمده Major\Favorite = major_favorites (۳٬۳۰۵ ردیف) را میخواند
(ApiMobileController.php:73-465). ساختار CustomerFavorite بیتغییر جواب
میدهد؛ فقط منبع عوض شد. کامنت جدول در دیتابیس هنوز «legacy fav» میگوید —
در migration بعدی اصلاح شود.
سایر یافتهها
- زمانها ساعت تهراناند (
config/app.php:70→Asia/Tehran). ابزار با قواعد ICU تبدیل میکند، با ساعت تابستانی ایران تا ۲۰۲۲. - ۴۷۳ از ۶۱۰ ردیف سفارش زنده به تنوعِ حذفشده اشاره دارند → تنوع و محصول
حذفشدهی ارجاعشده با
isActive = falseمنتقل میشوند. - همهی ۱٬۲۵۰ محصولِ چنددسته هم در درخت سایت (نوع ۱) هم اپ (نوع ۲)
هستند؛ مقصد یک
categoryIdدارد که اپ نشان میدهد → دستهی اپ نگه داشته میشود. - ۲۷ گروه موبایل تکراری: در هیچکدام بیش از یک ردیف پول ندارد و هیچ کارشناسی درگیر نیست → ادغام از نظر مالی بیخطر است.
- سرپرست کارگاه در legacy مشتری است (
belongsTo(Customer))، در مقصد ادمین → پیوند ساخته نمیشود. - مشتری
#594بهجای موبایل کلمهی «تست» دارد؛ با یک سفارش (#666، وضعیت ۱، بدون بدهی) کنار گذاشته میشود. - ۶ تنوع اجرت ریالی (
rial_pay) دارند؛ مقصد فیلدی برایش ندارد. - عدد ردشدهها ۷ است، نه ۱۴ (۲ گام دو، ۳ گام یک، ۲ هر دو). عدد ۱۴ در گزارش قبلی اشتباه جمع شده بود.
۱۰.۸ — ابزار انتقال (apps/api/scripts/legacy-import/)
اجرا
cd apps/api
npx ts-node --transpile-only scripts/legacy-import/index.ts \
--source "mysql://USER:PASS@HOST:PORT/oapi" \
--target "postgresql://USER:PASS@HOST:5432/goldbank_import_test" \
--images "F:/New folder (2)/storage" --upload-dir "<UPLOAD_DIR مقصد>" \
--report ./legacy-import-report.md --yes
بدون --yes فقط پیشبررسی میکند و چیزی نمینویسد. --target هرگز از
.env خوانده نمیشود. اگر نام دیتابیس مقصد import یا staging نداشته
باشد، --confirm-db=<همان نام> هم لازم است.
قواعد ایمنی (همه در کد)
- منبع فقط خوانده میشود.
- قبل از اجرا وجود ستونهای migration های آمادهسازی چک میشود.
- هرگز نمینویسد:
accounting*مشتری و محصول،ProductVariant.imageId، جداولAccounting*. هیچProductImageی حذف نمیشود. - ردیفی که از قبل در مقصد بوده (
matched) فقط فیلدهای خالیاش پر میشود. - مستقیم از Prisma مینویسد، نه از سرویسها: نه پیامکی میرود، نه چیزی به نرمافزار حسابداری صف میشود.
- نگاشت شناسهها در schema جدای
legacy_import.id_mapدر مقصد است (بیرون ازpublic، پس تغییر schema اپ نیست). اجرای دوباره بهروزرسانی میکند، تکرار نمیسازد؛ اجرایی که نیمهکاره ماند با اجرای بعدی تمام میشود. بعد از جابهجایی:DROP SCHEMA legacy_import CASCADE— یا نگه داشته شود برای redirect لینکهای قدیمی محصول.
نتیجهی آزمون — ۲۰۲۶-۱۰-۰۷
مقصد: goldbank_import_test، کپی دیتابیس توسعه با ۵۹ مشتری و ۲۸ محصول
واقعی (تا ادغام هم آزموده شود). سه اجرای پیاپی:
| نتیجه | |
|---|---|
| مشتری ساختهشده | ۱٬۵۰۳ · ادغام با موجود ۱۳ · ادغام تکراری ۳۲ |
| بدهی طلا، مشتریهای ساختهشده | ۱٬۵۰۳ از ۱٬۵۰۳ برابر legacy (±۰٫۰۰۰۱ گرم) |
| بدهی ریال | ۱٬۵۰۳ از ۱٬۵۰۳ |
| جمع بدهی طلا | legacy ۴۲۷٫۵۰۳۹ · مقصد ۴۲۷٫۵۰۴۱ (دو گِرد ۰٫۰۰۰۱) |
| سند تراز انتقالی | فقط مشتری #125: بستانکار ۹٫۸۹۵۱ |
| دادهی حسابداری (۵۱ مشتری، ۱۸ محصول، ۷۴ پیوند) | بایتبهبایت یکسان با دیتابیس توسعه |
imageId روی تنوع وارداتی |
۰ |
| کارشناس | ۴۹ ساخته شد با پروفایل · ۸ حساب معاملاتی · ۷ ادمین تنزل نیافت |
| محصول / تنوع | ۱٬۴۷۳ / ۱٬۶۱۲ (شامل ۲۱۵ / ۳۴۷ حذفشدهی غیرفعال برای تاریخچه) |
| سفارش / ردیف | ۲۹۹ از ۳۰۰ (#666 مشتری تست) / ۶۰۸ |
| سند مالی | ۸۶ بدهی سفارش + ۹۵ پرداخت تأییدشده + ۸ تأییدنشده |
| علاقهمندی | ۸۷۴ (۵۳۴ به محصول حذفشده و ۵۹ به مشتری منتقلنشده کنار رفت) |
| سفارش ساختنی | ۲۰ از ۲۰ |
| تصویر | ۶٬۱۹۶ فایل (۵۱۸ مگابایت) کپی شد · ۳ فایل در storage قدیم نبود |
| اجرای دوم و سوم | صفر ردیف تکراری |
آنچه این ابزار عمداً منتقل نمیکند
خردهفروشی (orders_orders، login، fav، wallets، transactions* —
D13 و §۷.۲)، تاریخچهی وضعیت سفارش (order_activity)، تخفیفها
(major_promotion*)، اعلانها، تاریخچهی پیامک (messages)، لاگها،
نظرات، cinies. هرکدام لازم شد، قدم جدا اضافه میشود.
۱۰.۹ — «میزان فروش» و کارشناسِ هر سفارش (۲۰۲۶-۱۰-۰۸)
مشکل: کارفرما در مقایسهی چشمی دید که «میزان فروش» چند کارشناس با سیستم قدیم فرق دارد (مثلاً موسوی کیانی و ملاطایفه). دو علت داشت:
۱. سیستم جدید سفارشهای خود کارشناس را نمیشمرد. قدیم میشمرد:
updateGramSell: customer_id = X OR reagent_id = X.
۲. سیستم جدید هر سفارش را به کارشناسِ فعلیِ مشتری نسبت میداد. قدیم آن را
به کارشناسِ زمان ثبت سفارش (orders.reagent_id) نسبت میداد. در
ApiMobileController.php:601 این مقدار یکبار نوشته میشود و دیگر عوض نمیشود.
حکم کارفرما: رفتار قدیم دقیقاً حفظ شود. هر سفارش کارشناسِ خودش را در لحظهی ثبت نگه دارد، تا با جابهجایی بعدی مشتری، آمار فروش و کمیسیونِ گذشته عوض نشود. «هیچ دیتایی نباید مغایرت داشته باشد».
پیادهسازی:
- پایگاه داده: مایگریشن
20261008120000_order_sales_agentکه فقط افزایشی است.Order.salesAgentIdبرای کارشناس سفارش.Order.referrerCustomerIdفقط برای تاریخچه: یک مشتری غیرکارشناس، ۱۷ سفارش سال ۲۰۲۲. قدیم آنها را در «میزان فروش» همان مشتری میشمرد.
- ثبت سفارش: کارشناسِ همان لحظهی مشتری را ذخیره میکند.
- ابزار انتقال:
orders.reagent_idرا عیناً میآورد. - محاسبه:
agent-orders.tsدر هر چهار جا (لیست، جزئیات، داشبورد، اپ کارشناس). - صفحهی مشتری: «میزان فروش» به آن اضافه شد؛ در پورت جا افتاده بود.
- فهرست سفارشهای زیرمجموعه در اپ کارشناس هم طبق همین قانون است
(
getChildOrder). - مدیرهایی که قبلاً کارشناس بودند: پروفایل کارشناسیشان نگه داشته شد تا سفارشهای خودشان به آنها وصل بماند. نقش و دسترسیشان تغییری نکرد.
نتیجه (reconcile-sales، روی نسخهی آزمایشی):
- فروش: ۱٬۵۵۷ از ۱٬۵۵۸ نفر دقیقاً برابر فرمول خود سیستم قدیم است. ردیفهای تکراریِ ادغامشده با هم مقایسه شدند.
- بدهی: همهی ۱٬۵۰۳ نفر بدون تغییر ماندند.
دو مورد که کارفرما تأیید کرد (۲۰۲۶-۱۰-۰۸، «درستن»):
- عدد کهنه در صفحهی قدیم (۹ نفر): صفحهی قدیم عدد ذخیرهشدهای را نشان
میدهد که فقط در بعضی رویدادها دوباره حساب میشود.
- نفرات: #62، #66، #622، #596، #597، #1138، #1198، #1418، #1579.
- سیستم جدید عددِ فرمولِ خود سیستم قدیم را نشان میدهد. همین درست است.
- ۱۵٫۲۵۵ گرم برای #413: سفارش #666 مال حساب آزمایشی «تست» است که بدون موبایل واقعی است و منتقل نمیشود. #413 الان مدیر است و در هیچ صفحهای این عدد را نمیبیند. کنار گذاشتن آن پذیرفته شد.
همزمان در همین تغییر (بدون اثر روی داده):
- تاریخ ثبت در همهی جدولهای پنل شمسی شد، به شکل
1405/5/3-16:02. - «وضعیت ثبت سفارش» دوباره برچسب سبز/قرمزِ «فعال/عدم سفارش» را نشان میدهد.
۱۱. جمعبندی — کارهای لازم قبل از نوشتن ابزار
🔴 مسدودکننده (بدون اینها ابزار در میانه میشکند یا داده خراب میکند)
| # | مورد | § |
|---|---|---|
| ۱ | ✅ انجام شد — ۲۷ گروه موبایل، ۰ کد ملی (§۱۰.۴) | |
| ۱.۱ | nationalCode |
✅ حل — nullable شود (§۱۰.۴) |
| ۱.۲ | passwordHash |
✅ حل — hash غیرقابلورود (§۱۰.۴) |
| ۱.۳ | ✅ حل — فرمول بدهی legacy با داده تأیید شد (§۱۰.۷) | |
| ۱.۴ | REJECTED |
✅ حل — enum + دو ستون علت (§۵.۵) |
| ۱.۵ | ✅ در ابزار — کارشناسان قبل از مشتریان (§۱۰.۸) | |
| ۱.۶ | birth_date |
✅ ��ل — پالایش، ۵۴۱ از ۹۱۵ (§۱۰.۷) |
| ۱.۷ | customers.type ۳/۴/۵ |
✅ سؤال لازم نبود — در Customer::$TYPES تعریف شدهاند (§۵.۳) |
| ۲ | ✅ در ابزار — ±۰٫۰۰۰۱ گرم، ±۱ ریال (§۱۰.۸) | |
| ۳ | deleted_at |
✅ در ابزار — بهجز حذفشدههای ارجاعشده در سفارش (§۱۰.۷) |
| ۴ | orders_orders |
✅ حل — به تأخیر افتاد (§۷.۲) |
| ۵ | ✅ در ابزار — fname/lname مستقیم |
|
| ۶ | ✅ در ابزار — حسابداری بایتبهبایت یکسان ماند (§۱۰.۸) | |
| ۷ | ✅ موبایل (مالک) — ادمین تنزل نمییابد (§۱۰.۷) |
🔶 تصمیم مالک
| # | مورد | § |
|---|---|---|
| ۱ | ✅ حل — تولد و شهر منتقل، توکن و سالگرد حذف (§۵.۲) | |
| ۱.۱ | new_city |
✅ حل — نگاشت دوگانه: متن در cityNameRaw + cityId با تطابق دقیق (مالک · §۱۰.۷) |
| ۲ | STEP_TWO_SUBMITTED از VERIFIED |
✅ حل (§۵.۵) |
| ۳ | type IS NULL |
✅ APP_SHOPKEEPER — ثبتنام اپ type = 2 میگذارد (Major/CustomerController.php:44)؛ NULL قدیمیتر از آن است |
| ۴ | ✅ حل — از major_favorites، نه fav (§۱۰.۷) |
|
| ۴.۲ | #125 |
✅ همان −۹٫۸۹۵۱ با سند تراز انتقالی (مالک · §۱۰.۷) |
| ۴.۳ | 🔶 رد در پنل جدید REJECTED نمینویسد و مشتری ردشده با ارسال دوباره خودکار تأیید میشود |
§۱۰.۵.۵ |
| ۴.۴ | 🔶 مدیر ارشدی که کارشناس هم بود — نقش ادمین نگه داشته شد؛ آیا حساب کارشناسی هم لازم است؟ | §۱۰.۷ |
| ۴.۱ | cinies/ciny_products — چیدمان نمایشی |
۱۰ |
| ۵ | شمارندههای denormalized محاسبه شوند یا صفر بمانند | ۵.۲ |
| ۶ | نسخههای تغییراندازهیافتهی تصویر | ۹ |
| ۷ | original_rash_weight/original_percent — تاریخچهی ممیزی قلم سفارش |
۷.۴ |
| ۸ | snapshot عنوان محصول در فاکتورهای تاریخی | ۷.۴ |
✅ یافتههای جانبی (ثبت، بدون کار)
users.permission decimal(16,16)ستونی است که ریاضیاً کار نمیکند — مرده (§۶)products_productvarient.Inventoryبا I بزرگ، برخلاف$fillable(§۹)- نام جدول
varientغلط املایی دارد (§۹) messages.massageغلط املایی دارد (§۱۰)cities.percentمعنای چهارمpercentاست (§۹)orders_histroyغلط املایی دارد- صفر foreign key در کل ۸۰ جدول — یکپارچگی ارجاعی در اپ بود، نه دیتابیس
۱۲. آنچه این سند تأیید نمیکند
⚠️ دامپ داده ندارد. هر عددی در این سند از ساختار آمده. تعداد ردیف، تکرار، تهیبودن و اختلاف محاسباتی همه دادهی واقعی لازم دارند.
⚠️ ayar و recive_date/proceed_at بر اساس نوع ستون تحلیل شدند، نه
محتوا. اینکه عیار متنی است یا عددی، و تاریخ جلالی است یا میلادی، فقط با
دیدن داده قطعی میشود.
⚠️ ستونبهستونِ کامل فقط برای customers نوشته شد (۴۹/۴۹). بقیهی
جداول در سطح «نگاشتشدنی / بیخانه / مبهم» بررسی شدند. برای جداول بزرگ
(cashes ۳۳، product ۳۵، orders_orders ۳۷) اگر لازم است، ستونبهستون
در ادامه نوشته میشود.
⚠️ موتور مقصد Postgres است و مبدأ MySQL — تفاوتهای حساسیت به بزرگی
حرف، tinyint(1) در برابر boolean، و timestamp با/بدون منطقهی زمانی
هنگام نوشتن ابزار باید یکبهیک بررسی شوند.
مرجع
GO-LIVE-PLAN.md§۷ — ابزار مهاجرت و شرطهای تطبیقparity/DECISIONS.md— D0, D9, D13, D17, D21, D24parity/MIGRATION-STATE.md— سه معنایpercentparity/ISSUES.md— MON-07 (دو enum وضعیت)PWA-PORT-PLAN.md— Q5 (ترتیب دستهها)، نقص #۱۹