- Add Tahesab/Zarrin accounting sync module with admin panel UI - Add SMS relay driver as default provider, refactor SMS into driver pattern - Update customer services, orders, products, and catalog queries - Rebuild user-app and user-app-v2 dist assets - Add Prisma migration for accounting tables - Update docker-compose and env examples - Update parity docs (DECISIONS, ISSUES, PROGRESS) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
61 KiB
نگاشت دیتابیس legacy → Prisma
سند نگاشت، نه پلن اجرا. هیچ کدی بر اساس این سند نوشته نشده. پیشنیاز ابزار مهاجرت دادهی
GO-LIVE-PLAN.md§۷.
منبع: oapi-struncture.sql — دامپ Navicat، تاریخ ۰۵/۱۰/۲۰۲۶
مقصد: apps/api/prisma/schema.prisma — ۴۳ مدل، ۴۷۷ فیلد
تاریخ نگارش: ۱۴ مهر ۱۴۰۵ (۲۰۲۶-۱۰-۰۶)
۰. مشخصات منبع
| ویژگی | مقدار |
|---|---|
| موتور | MySQL 8.0.42 (8.0.42-0ubuntu0.20.04.1) |
| سرور | Henza · localhost:3305 |
| اسکیما | oapi |
| جداول | ۸۰ |
| ستونها | ۱٬۰۸۷ |
| Engine | InnoDB (هر ۸۰ جدول) |
| محتوا | فقط ساختار — صفر INSERT |
این فایل شکافی را پر کرد که قبلاً باز بود: جداول دوران 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. اینها اگر منتقل نشوند، برای همیشه از بین
میروند. token_mobile در ۱۰ فایل استفاده میشود؛ بدون آن، اعلان push به
گوشیهای موجود قطع میشود.
🔶 تصمیم مالک لازم است برای گروه (ج).
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به enum (additive، با §G تضاد ندارد) — ۱۴ ردیف درست منتقل میشوند - نگاشت ردشدهها به
UNVERIFIEDو پذیرفتن اینکه در صف برمیگردند
و ۱۰ ردیف ناسازگار (1,2 و 3,2) هر تصمیمی بگیرید، نیاز به قاعدهی
صریح دارند: گام دوم تأیید شده ولی گام اول نه. در legacy این حالت ممکن بود
چون دو ستون مستقلاند و هیچ محدودیتی بینشان نیست.
۵.۷ — 🔴 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 لازم است
«بله» — علاقهمندیها مهماند.
یک 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 روی رشتهی خالی یعنی
فقط یک ردیف میتواند '' باشد.
🔶 تصمیم فوری لازم: nationalCode باید String? شود (nullable). این
یک migration افزایشی روی مقصد است. بدون آن مهاجرت مشتری غیرممکن است.
۳. گِردکردن رخ میدهد — ۲۴ + ۳۲ ردیف.
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
🔴 مهاجرت میشکند. دو راه: passwordHash nullable شود، یا مهاجرت یک
hash تصادفیِ غیرقابلاستفاده بگذارد. گزینهی دوم امنتر است — ستون
اجباری میماند و هیچکس نمیتواند با رمز خالی وارد شود.
⚠️ آن ۵ رمز $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-precheck-2.sql — چهار سؤالی
که خروجی اول باز گذاشت. مثل فایل اول: فقط COUNT/SUM، صفر نوشتن، هیچ
ستون متنی در خروجی (تنها استثنا: products_productimage.w که یک برچسب
اندازه است، نه دادهی شخصی).
| # | سؤال | چرا مسدودکننده است |
|---|---|---|
| ۱ | مبلغ سند مالی در کدام ستون است؟ | remain/rial_remain همه NULLاند. بدون جواب، LedgerEntry.amount خالی میماند و کل دفتر مالی بیمبلغ منتقل میشود |
| ۲ | آیا «مسئول فروش برنده» برای ۲۷ گروه تکراری کافی است؟ | اگر گروهی کارشناس نداشته باشد، قاعده بیاثر است |
| ۳ | سه ستون تصویر → چهار نقش ProductImageRole |
نگاشت غلط = تصویر غلط روی محصول |
| ۴ | تأیید §۵.۷ — reagent_id به customers اشاره میکند؟ |
اگر غلط باشد، رابطهی کارشناس معرف از بین میرود |
۱۱. جمعبندی — کارهای لازم قبل از نوشتن ابزار
🔴 مسدودکننده (بدون اینها ابزار در میانه میشکند یا داده خراب میکند)
| # | مورد | § |
|---|---|---|
| ۱ | ✅ انجام شد — ۲۷ گروه موبایل، ۰ کد ملی (§۱۰.۴) | |
| ۱.۱ | 🔴 nationalCode باید nullable شود — هر ۱٬۵۹۵ ردیف تهی است |
۱۰.۴ |
| ۱.۲ | 🔴 passwordHash — ۱٬۵۹۰ تهی؛ hash تصادفی یا nullable |
۱۰.۴ |
| ۱.۳ | 🔴 مبلغ سند مالی کجاست؟ cashes.remain همه NULL |
۱۰.۶ |
| ۱.۴ | 🔴 VerificationStep حالت REJECTED ندارد — ۱۴ ردیف |
۵.۵ |
| ۱.۵ | 🔴 ترتیب دومرحلهای — کارشناسان قبل از مشتریان | ۵.۷ |
| ۲ | آستانهی تطبیق عددی جای «صفر اختلاف» — تأیید شد: ۵۶ ردیف گِرد میشوند | ۴ · ۱۰.۴ |
| ۳ | قاعدهی WHERE deleted_at IS NULL — حیاتیتر از انتظار: ۴۵٪ واریانتها |
۵.۸ · ۱۰.۴ |
| ۴ | orders_orders |
✅ حل — به تأخیر افتاد (§۷.۲) |
| ۵ | تصحیح D0: fname/lname مستقیم، تجزیه نشود |
۲ |
| ۶ | allow-list ستونهای نوشتنی — ۷ ستون مقصد-تنها لمس نشوند | ۱۰.۵ |
| ۷ | کلید تطبیق ردیفهای موجود مقصد | ۱۰.۵ |
🔶 تصمیم مالک
| # | مورد | § |
|---|---|---|
| ۱ | ۴ ستون دادهی غیرقابلبازسازی (birth_date, marriage_date, token_mobile, new_city) |
۵.۲ |
| ۲ | verificationStep — تفکیک STEP_TWO_SUBMITTED از VERIFIED |
۵.۵ |
| ۳ | type IS NULL چه CustomerKind شود |
۵.۳ |
| ۴ | fav |
✅ حل — لازم است، migration افزایشی (§۱۰) |
| ۴.۱ | 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 (ترتیب دستهها)، نقص #۱۹