تصميم قاعدة بيانات سوق السيارات المستعملة: الجداول والبيانات التجريبية والمعاملات ونماذج التطبيق، 100 مثال من الصفر إلى الاحتراف
يربط سوق السيارات المستعملة بين المشترين والبائعين والتجار حول أصل واحد باهظ الثمن، فريد، ويُنقل ملكيته قانونياً. لكل سيارة رقم هيكل (VIN) وسجل عدادات وتقرير فحص وتسجيل يجب أن يتغير مالكه بشكل صحيح. تصمم هذه المقالة قاعدة بيانات لهذا السوق من مخطط فارغ حتى إصدار يراعي بيئة الإنتاج عبر 100 مثال. ستحدد قواعد العمل، وتنشئ جداول المستخدمين والتجار، وكتالوجاً للسيارات يضم الماركات والطرازات والإصدارات، وإعلانات بيع مع صور، وفحوصات وقراءات عداد، وفهارس للبحث، وعروضاً وعروضاً مضادة، وطلبات بمسار حالات واضح، ومدفوعات مسجلة كدفتر حسابات، وخطوات نقل الملكية مع المستندات، والتمويل والضمانات، والمراجعات وبلاغات الاحتيال، والمعاملات التي تحفظ كل ذلك متسقاً عند الاستخدام المتزامن. يتضمن كل جزء بيانات تجريبية، وما يحدث داخل المعاملة عند تنفيذها، ونموذجاً لشاشة التطبيق التي تستخدمها. ملاحظة صريحة: لم يكن MySQL مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة، لذلك لم تُنفَّذ الأمثلة المئة هنا. يتبع SQL صياغة MySQL 8.4، لكن شغّل كل جزء بنفسك على قاعدة بيانات اختبار قبل الاعتماد عليه.
ما الذي تحتاجه
MySQL 8.4 (أو Docker لتشغيله داخل حاوية)، وعميل SQL مثل MySQL Workbench أو أداة سطر الأوامر mysql، وفهم أساسي للجداول والمفاتيح والربط بينها. استخدم أشخاصاً وبريداً إلكترونياً وأرقام هيكل وهمية، ولا تستخدم أبداً بيانات عملاء حقيقية، ولا تلصق كلمات مرور أو مفاتيح API أو بيانات مصرفية حقيقية في السكربتات.
كيفية استخدام المثال
تعمل أمثلة SQL على MySQL 8.4 في قاعدة بيانات اختبار وبالترتيب المذكور. أمثلة HTML نماذج شاشات تُفتح في المتصفح. أمثلة JSON تبيّن شكل بيانات الواجهة البرمجية، وأمثلة bash تستخدم الأمرين mysql وcurl.
الموضوع 1: قواعد العمل والإعداد
ابدأ بالقواعد لا بالجداول. من يحق له نشر سيارة، ومن يحق له شراؤها، وما الذي يجب أن يكون صحيحاً قبل نشر السيارة، وما الذي يجب ألا يحدث أبداً، مثل بيع السيارة نفسها مرتين. تفرض قاعدة البيانات هذه القواعد حتى لا يستطيع خطأ في التطبيق كسرها.
الأطراف وما يحتاجونه
يبحث المشترون ويحفظون المفضلات ويقدمون العروض. ينشر البائعون السيارات ويردّون على العروض. ينشر التجار سيارات كثيرة ويحملون شارة التوثيق. يقيّم المفتشون السيارات، ويراجع المديرون البلاغات. لكل دور صلاحياته وشاشاته الخاصة.
القواعد التي يجب أن تحفظها قاعدة البيانات
لا يمكن بيع السيارة إلا لمشترٍ واحد في الوقت نفسه. لا يُنشر الإعلان إلا بعد فحص ناجح. يُحتجز العربون حتى يكتمل نقل الملكية. تُسجل كل تغييرات السعر والحالة. اكتب هذه القواعد أولاً، ثم قرر أيها تفرضه قيود CHECK والمفاتيح الأجنبية والمعاملات.
مجموعة الأحرف وقاعدة البيانات
أنشئ قاعدة البيانات بترميز utf8mb4 حتى يُحفظ النص التايلاندي والرموز التعبيرية والأسماء كما كُتبت تماماً. استخدم مجموعة الأحرف نفسها في كل الجداول، لأن خلط الترميزات يسبب نصاً مشوهاً وأخطاء مقارنة.
رسوم توضيحية
مثال 1: اكتب قواعد العمل أولاً
من يفعل ماذا، والقواعد التي يجب أن تفرضها قاعدة البيانات.
{
"buyers": "search, favorite, make an offer, pay a deposit, buy",
"sellers": "list a car with photos and a price, accept or reject offers",
"dealers": "list many cars, get a verified badge after a document check",
"rules": [
"a car can be sold to one buyer only",
"a deposit is held until the ownership transfer is done",
"every price change and status change is recorded"
]
}
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 2: إنشاء قاعدة البيانات
استخدم utf8mb4 لحفظ النص التايلاندي والرموز التعبيرية بشكل صحيح.
CREATE DATABASE IF NOT EXISTS carmarket
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE carmarket;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 3: جدول المستخدمين
جدول حسابات واحد للمشترين والبائعين والتجار والموظفين.
CREATE TABLE users (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
email VARCHAR(190) NOT NULL UNIQUE,
phone VARCHAR(20) NULL,
password_hash CHAR(60) NOT NULL,
full_name VARCHAR(120) NOT NULL,
role ENUM('buyer','seller','dealer','inspector','admin') NOT NULL DEFAULT 'buyer',
is_active TINYINT(1) NOT NULL DEFAULT 1,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 4: مستخدمون تجريبيون
حسابات وهمية مع تجزئات كلمات مرور نائبة.
INSERT INTO users (email, phone, password_hash, full_name, role) VALUES
('[email protected]', '0810000001', '$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Somchai Example', 'seller'),
('[email protected]', '0810000002', '$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Malee Example', 'buyer'),
('[email protected]','0810000003', '$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Prime Auto Example', 'dealer'),
('[email protected]','0810000004','$2b$12$exampleexampleexampleexampleexampleexampleexampleexa', 'Inspector Example', 'inspector');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 5: جدول التجار
تفاصيل النشاط التجاري منفصلة عن حساب الدخول.
CREATE TABLE dealers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL UNIQUE,
business_name VARCHAR(160) NOT NULL,
tax_id CHAR(13) NOT NULL UNIQUE,
is_verified TINYINT(1) NOT NULL DEFAULT 0,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 6: تاجر تجريبي
ربط نشاط تجاري معتمد بحساب المستخدم.
INSERT INTO dealers (user_id, business_name, tax_id, is_verified)
SELECT id, 'Prime Auto Example Co., Ltd.', '0105500000001', 1
FROM users WHERE email = '[email protected]';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 2: المستخدمون والتجار والعناوين
كل من يسجل الدخول له صف واحد في جدول users مع دور محدد. يحمل التجار تفاصيل نشاطهم التجاري في جدول خاص بهم، وتتيح العناوين البحث عن السيارات حسب المحافظة. الفصل بين هذه الجداول يجعل كل جدول صغيراً وكل قاعدة سهلة التحقق.
جدول حسابات واحد بأدوار
يحتفظ جدول users بالبريد الإلكتروني وتجزئة كلمة المرور والدور. يحدد الدور، مثل بائع أو مشترٍ أو تاجر أو مفتش أو مدير، الشاشات والإجراءات المسموح بها. لا تخزن كلمة المرور نصاً عادياً أبداً، بل خزّن تجزئة bcrypt فقط.
التجار كجدول منفصل
التاجر مستخدم مسجل نشاطه التجاري، لذلك يُحفظ اسم النشاط والرقم الضريبي في جدول dealers ويشير إلى users بمفتاح أجنبي. لا يُضبط حقل is_verified إلا بعد أن يتحقق الموظفون من المستندات، ولا يعرض الشارة إلا التجار الموثقون.
العناوين والتفضيلات
تُحفظ المحافظة في كل عنوان وفي كل إعلان، ولها فهرس حتى يكون البحث عن محافظة واحدة سريعاً. تُحفظ تفضيلات المشتري، مثل الميزانية ونوع الوقود، بمعزل عن الحساب حتى يمكن تغييرها دون لمس الحساب.
مثال 7: العناوين
خزّن المواقع حتى يتمكن البحث من التصفية حسب المحافظة.
CREATE TABLE addresses (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
line1 VARCHAR(200) NOT NULL,
district VARCHAR(100) NOT NULL,
province VARCHAR(100) NOT NULL,
postal_code CHAR(5) NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_addresses_province (province)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 8: عنوان تجريبي
عنوان بائع في تشونبوري.
INSERT INTO addresses (user_id, line1, district, province, postal_code)
SELECT id, '99/1 Example Road', 'Mueang', 'Chonburi', '20000'
FROM users WHERE email = '[email protected]';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 9: تفضيلات المشتري
تذكّر الميزانية ونوع الوقود المفضل.
CREATE TABLE buyer_preferences (
user_id BIGINT UNSIGNED PRIMARY KEY,
max_budget DECIMAL(12,2) NULL,
preferred_fuel ENUM('petrol','diesel','hybrid','ev','lpg','cng') NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 10: جلسات الدخول
انتهِ صلاحية الجلسات واحذفها مع المستخدم.
CREATE TABLE sessions (
id CHAR(64) PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
expires_at DATETIME NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_sessions_expires (expires_at)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 11: ماركات السيارات
صف واحد لكل ماركة، والاسم فريد.
CREATE TABLE car_makes (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(60) NOT NULL UNIQUE
) ENGINE=InnoDB;
INSERT INTO car_makes (name) VALUES ('Toyota'), ('Honda'), ('Mazda'), ('Isuzu'), ('Mitsubishi');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 12: طرازات السيارات
كل طراز ينتمي إلى ماركة واحدة فقط.
CREATE TABLE car_models (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
make_id SMALLINT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
body_type ENUM('sedan','hatchback','suv','pickup','van') NOT NULL,
UNIQUE KEY uq_model (make_id, name),
FOREIGN KEY (make_id) REFERENCES car_makes(id)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 13: طرازات تجريبية
أدخل الطرازات مع ربطها بجدول الماركات.
INSERT INTO car_models (make_id, name, body_type)
SELECT m.id, x.name, x.body
FROM car_makes m
JOIN (SELECT 'Toyota' AS make, 'Camry' AS name, 'sedan' AS body
UNION ALL SELECT 'Toyota', 'Yaris Ativ', 'sedan'
UNION ALL SELECT 'Honda', 'City', 'sedan'
UNION ALL SELECT 'Honda', 'CR-V', 'suv'
UNION ALL SELECT 'Mazda', 'CX-5', 'suv'
UNION ALL SELECT 'Isuzu', 'D-Max', 'pickup') x ON x.make = m.name;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 14: الإصدارات (Trim)
الطراز نفسه يأتي بإصدارات بمحركات مختلفة.
CREATE TABLE car_trims (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
model_id INT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
year_from SMALLINT UNSIGNED NOT NULL,
year_to SMALLINT UNSIGNED NULL,
engine_cc SMALLINT UNSIGNED NOT NULL,
fuel ENUM('petrol','diesel','hybrid','ev','lpg','cng') NOT NULL,
transmission ENUM('auto','manual','cvt') NOT NULL,
seats TINYINT UNSIGNED NOT NULL DEFAULT 5,
FOREIGN KEY (model_id) REFERENCES car_models(id),
INDEX idx_trims_model_fuel (model_id, fuel)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 3: كتالوج السيارات: الماركات والطرازات والإصدارات
يحفظ الكتالوج حقائق الطراز مرة واحدة بدلاً من تكرارها في كل إعلان. للماركة طرازات كثيرة، وللطراز إصدارات كثيرة، ولكل إصدار المحرك والوقود وناقل الحركة الذي يقارنه المشتري فعلياً.
الماركة والطراز والإصدار
الماركة مثل Toyota تملك طرازات مثل Camry. الإصدار مثل G 2.5 نسخة من ذلك الطراز لها محركها وناقل حركتها الخاص. يمكن أن يكون للطراز نفسه عدة إصدارات في سنوات مختلفة، لذلك ينبغي أن تُحدد نطاقات السنوات على مستوى الإصدار.
الخيارات والعلاقات متعددة لمتعدد
الخيارات مثل فتحة السقف أو الكاميرا الخلفية ليست موجودة في كل إصدار. يربط جدول وسيط الخيارات بالإصدارات، فيمكن أن يظهر خيار واحد في إصدارات كثيرة، ويمكن أن يملك إصدار واحد خيارات كثيرة.
لماذا يهم الكتالوج
عندما يختار البائع إصداراً، تأتي بيانات المحرك والوقود وعدد المقاعد من الكتالوج. لا يستطيع البائع كتابتها بشكل خاطئ، ويمكن للبحث حسب الوقود أو ناقل الحركة أن يعمل بثقة لأن القيم مخزنة كرموز لا كنص حر.
مثال 15: إصدار في الكتالوج
إصدار بنزين أوتوماتيك لنطاق سنوات محدد.
INSERT INTO car_trims (model_id, name, year_from, year_to, engine_cc, fuel, transmission)
SELECT id, 'G 2.5', 2018, 2023, 2487, 'petrol', 'auto'
FROM car_models WHERE name = 'Camry';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 16: إصدار هجين
إصدار هجين بناقل CVT وبدون سنة نهاية.
INSERT INTO car_trims (model_id, name, year_from, year_to, engine_cc, fuel, transmission)
SELECT id, 'Hybrid Premium', 2019, NULL, 1798, 'hybrid', 'cvt'
FROM car_models WHERE name = 'Camry';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 17: الخيارات
فتحة السقف والكاميرا ومساعد البقاء في المسار والمقاعد الجلدية كرموز.
CREATE TABLE car_options (
id SMALLINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
code VARCHAR(40) NOT NULL UNIQUE,
label VARCHAR(80) NOT NULL
) ENGINE=InnoDB;
INSERT INTO car_options (code, label) VALUES
('sunroof', 'Sunroof'), ('rear_camera', 'Rear camera'), ('lane_assist', 'Lane assist'), ('leather', 'Leather seats');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 18: خيارات الإصدارات
اربط الخيارات بالإصدارات بجدول علاقة متعدد لمتعدد.
CREATE TABLE trim_options (
trim_id INT UNSIGNED NOT NULL,
option_id SMALLINT UNSIGNED NOT NULL,
PRIMARY KEY (trim_id, option_id),
FOREIGN KEY (trim_id) REFERENCES car_trims(id) ON DELETE CASCADE,
FOREIGN KEY (option_id) REFERENCES car_options(id)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 19: إعلانات البيع
صف واحد لكل سيارة فعلية للبيع، مع قواعد CHECK.
CREATE TABLE car_listings (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
seller_id BIGINT UNSIGNED NOT NULL,
trim_id INT UNSIGNED NOT NULL,
vin CHAR(17) NOT NULL UNIQUE,
plate VARCHAR(20) NULL,
year SMALLINT UNSIGNED NOT NULL,
mileage_km INT UNSIGNED NOT NULL,
color VARCHAR(40) NOT NULL,
asking_price DECIMAL(12,2) NOT NULL,
status ENUM('draft','pending_review','active','reserved','sold','removed') NOT NULL DEFAULT 'draft',
province VARCHAR(100) NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (seller_id) REFERENCES users(id),
FOREIGN KEY (trim_id) REFERENCES car_trims(id),
CHECK (asking_price > 0),
CHECK (mileage_km >= 0)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 20: إعلان نشط تجريبي
كامري 2019 للبيع في تشونبوري.
INSERT INTO car_listings (seller_id, trim_id, vin, plate, year, mileage_km, color, asking_price, status, province)
SELECT u.id, t.id, 'JTNB11HK103000001', 'กข 1234', 2019, 48250, 'White', 765000.00, 'active', 'Chonburi'
FROM users u, car_trims t
WHERE u.email = '[email protected]' AND t.name = 'G 2.5' AND t.year_from = 2018
LIMIT 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 21: صور السيارة
خزّن روابط الصور بترتيب عرض ثابت.
CREATE TABLE car_images (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
url VARCHAR(500) NOT NULL,
position TINYINT UNSIGNED NOT NULL DEFAULT 0,
FOREIGN KEY (listing_id) REFERENCES car_listings(id) ON DELETE CASCADE,
UNIQUE KEY uq_image_position (listing_id, position)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 22: صور تجريبية
ثلاث صور للإعلان الأول.
INSERT INTO car_images (listing_id, url, position) VALUES
(1, 'https://cdn.example.com/cars/1/front.jpg', 0),
(1, 'https://cdn.example.com/cars/1/side.jpg', 1),
(1, 'https://cdn.example.com/cars/1/interior.jpg', 2);
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 4: الإعلانات والصور
الإعلان هو سيارة واحدة حقيقية معروضة للبيع، ولها رقم هيكلها وعدادها ولونها وسعرها وحالتها الخاصة. تُحفظ الصور في جدول منفصل بترتيب ثابت، فتكون الصورة الأولى دائماً للواجهة الأمامية، وتُحمّل المعرض بالترتيب نفسه في كل مرة.
صف واحد لكل سيارة حقيقية
لكل سيارة رقم هيكل فريد، فلا يمكن نشر السيارة نفسها مرتين. تتنقل الحالة من مسودة إلى قيد المراجعة، ثم إلى نشطة أو محجوزة أو مباعة أو مزالة. تمنع قواعد CHECK تخزين سعر سالب أو عداد سالب.
تغيير الإعلان بأمان
يحق للبائع وحده تغيير السعر. ضع التحقق من البائع في عبارة UPDATE نفسها التي تجري التغيير، حتى لا تكون هناك فجوة بين التحقق والكتابة. انقل الحالة فقط عبر الخطوات المسموح بها، وسجّل كل تغيير.
الصور وصفحة التفاصيل
خزّن رابط الصورة وترتيبها، لا ملف الصورة نفسه. تحمّل صفحة التفاصيل السيارة والبائع والصور في استعلام واحد، ويوضح النموذج كيف تُرتب النتيجة على الهاتف وعلى الحاسوب المكتبي.
مثال 23: إعلان مسودة
يُنشئ البائع السيارة قبل نشرها.
INSERT INTO car_listings (seller_id, trim_id, vin, year, mileage_km, color, asking_price, status, province)
SELECT u.id, t.id, 'JTNB11HK103000002', 2020, 31000, 'Silver', 720000.00, 'draft', 'Bangkok'
FROM users u, car_trims t
WHERE u.email = '[email protected]' AND t.name = 'G 2.5'
LIMIT 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 24: تغيير السعر بصفة المالك
تحقق من البائع في الاستعلام نفسه.
UPDATE car_listings
SET asking_price = 745000.00
WHERE id = 2 AND seller_id = (SELECT id FROM users WHERE email = '[email protected]');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 25: تقدّم حالة الإعلان
اسمح بالتغيير فقط من الخطوة السابقة.
UPDATE car_listings
SET status = 'pending_review'
WHERE id = 2 AND status = 'draft';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 26: الإعلانات النشطة في محافظة
اربط الكتالوج لعرض الماركة والطراز والإصدار.
SELECT l.id, l.year, l.mileage_km, l.asking_price, t.name AS trim, m.name AS model, mk.name AS make
FROM car_listings l
JOIN car_trims t ON t.id = l.trim_id
JOIN car_models m ON m.id = t.model_id
JOIN car_makes mk ON mk.id = m.make_id
WHERE l.status = 'active' AND l.province = 'Chonburi'
ORDER BY l.asking_price;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 27: صفحة تفاصيل الإعلان
حمّل السيارة والصور والبائع في استعلام واحد.
SELECT l.*, u.full_name AS seller_name, GROUP_CONCAT(i.url ORDER BY i.position) AS photos
FROM car_listings l
JOIN users u ON u.id = l.seller_id
LEFT JOIN car_images i ON i.listing_id = l.id
WHERE l.id = 1
GROUP BY l.id;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 28: نموذج: بطاقة نتيجة البحث
بطاقة السيارة مع السعر والمواصفات وزر العرض.
<article class="car-card">
<img src="https://cdn.example.com/cars/1/front.jpg" alt="Toyota Camry 2019 white">
<h3>Toyota Camry G 2.5 (2019)</h3>
<p class="meta">48,250 km · Auto · Petrol · Chonburi</p>
<p class="price">฿765,000</p>
<button>Make an offer</button>
<button class="icon" aria-label="Save to favorites">♡</button>
</article>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 29: نموذج: صفحة التفاصيل
المعرض والملخص والأزرار على شاشة واحدة.
<main class="detail">
<section class="gallery">
<img src="front.jpg" alt="Front view">
<div class="thumbs"><img src="side.jpg" alt="Side view"><img src="interior.jpg" alt="Interior"></div>
</section>
<aside class="summary">
<h1>Toyota Camry G 2.5 (2019)</h1>
<p class="price">฿765,000</p>
<ul>
<li>Mileage: 48,250 km</li>
<li>Color: White</li>
<li>Province: Chonburi</li>
<li>Inspection score: <strong>4.5 / 5</strong></li>
</ul>
<button class="primary">Make an offer</button>
<button>Ask the seller</button>
</aside>
</main>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 30: نموذج: الألوان والمسافات
قيم التصميم كمتغيرات CSS.
<style>
:root {
--brand: #1f5fbf;
--ink: #1b1f2a;
--muted: #6b7280;
--radius: 12px;
--gap: 16px;
}
.car-card { border: 1px solid #e5e7eb; border-radius: var(--radius); padding: var(--gap); }
.price { color: var(--brand); font-size: 1.25rem; font-weight: 700; }
.meta { color: var(--muted); }
</style>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 5: الفحص وحالة السيارة
قيمة السيارة المستعملة تساوي حالتها فقط. يقيّم المفتش السيارة، ويصنّف كل جزء، ويرفق تقريراً. لا يُنشر الإعلان إلا إذا اجتاز الفحص، فيرى المشتري الحقائق نفسها التي سجّلها المفتش.
الفحص وبنوده
للفحص درجة إجمالية من 0 إلى 5 وتقرير PDF. لكل جزء، مثل المحرك أو الفرامل أو الإطارات، صف مستقل يحمل تصنيفاً: جيد، مقبول، يحتاج إصلاحاً، أو يحتاج استبدالاً. تفسّر التصنيفات الدرجة، فيرى المشتري أين تكمن المشكلات.
قراءات العداد
احفظ كل قراءة للمسافة من البائع والمفتش وسجل الصيانة. إذا كانت القراءة الجديدة أقل من قراءة قديمة، فربما أُعيد ضبط العداد إلى الخلف، لذلك يضع النظام علامة على الإعلان للمراجعة بدلاً من عرض رقم خاطئ.
النشر بعد الفحص
ينتقل الإعلان إلى نشط فقط عندما يوجد فحص بدرجة 3.0 على الأقل. القاعدة موجودة في قاعدة البيانات وفي التطبيق معاً، فلا يستطيع أي مسار برمجي نشر سيارة لم تُفحص.
مثال 31: الفحص الفني
يمنح مفتش معتمد درجة لكل سيارة.
CREATE TABLE inspections (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
inspector_id BIGINT UNSIGNED NOT NULL,
overall_score DECIMAL(2,1) NOT NULL,
report_url VARCHAR(500) NULL,
inspected_at DATETIME NOT NULL,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (inspector_id) REFERENCES users(id),
CHECK (overall_score BETWEEN 0 AND 5)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 32: مثال على الفحص
درجة 4.5 من 5 مع تقرير PDF.
INSERT INTO inspections (listing_id, inspector_id, overall_score, report_url, inspected_at)
SELECT 1, u.id, 4.5, 'https://cdn.example.com/reports/1.pdf', NOW()
FROM users u WHERE u.email = '[email protected]';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 33: بنود الفحص
صنّف كل قطعة: جيدة أو مقبولة أو تحتاج إصلاحاً أو استبدالاً.
CREATE TABLE inspection_items (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
inspection_id BIGINT UNSIGNED NOT NULL,
part VARCHAR(60) NOT NULL,
grade ENUM('good','fair','repair','replace') NOT NULL,
note VARCHAR(255) NULL,
FOREIGN KEY (inspection_id) REFERENCES inspections(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 34: بنود فحص تجريبية
فرامل بنسبة 40 بالمئة وخدش على الباب.
INSERT INTO inspection_items (inspection_id, part, grade, note) VALUES
(1, 'engine', 'good', 'No leaks'),
(1, 'brakes', 'fair', 'Front pads at 40 percent'),
(1, 'tyres', 'good', 'Above 4 mm tread'),
(1, 'paint', 'repair', 'Small scratch on the rear door');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 35: إيجاد القطع التي تحتاج إصلاحاً
عدّ المشكلات في كل إعلان.
SELECT i.listing_id, COUNT(*) AS problems
FROM inspection_items ii
JOIN inspections i ON i.id = ii.inspection_id
WHERE ii.grade IN ('repair','replace')
GROUP BY i.listing_id;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 36: النشر بعد اجتياز الفحص فقط
لا ينشر الإعلان إلا بدرجة ناجحة.
UPDATE car_listings l
SET l.status = 'active'
WHERE l.id = 2
AND l.status = 'pending_review'
AND EXISTS (SELECT 1 FROM inspections i WHERE i.listing_id = l.id AND i.overall_score >= 3.0);
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 37: نموذج: تقرير الفحص
التقييمات الظاهرة في صفحة الإعلان.
<section class="inspection">
<h2>Inspection report</h2>
<p>Overall score <strong>4.5 / 5</strong> · Inspected 12 Oct 2026</p>
<ul class="grades">
<li class="good">Engine: good</li>
<li class="fair">Brakes: fair, front pads at 40%</li>
<li class="good">Tyres: good</li>
<li class="repair">Paint: small scratch on the rear door</li>
</ul>
<a href="https://cdn.example.com/reports/1.pdf">Download the full PDF</a>
</section>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 38: قراءات العداد
احتفظ بكل قراءة لاكتشاف إعادة ضبط العداد.
CREATE TABLE mileage_readings (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
reading_km INT UNSIGNED NOT NULL,
source ENUM('seller','inspector','service_record') NOT NULL,
read_at DATETIME NOT NULL,
FOREIGN KEY (listing_id) REFERENCES car_listings(id) ON DELETE CASCADE,
INDEX idx_mileage_listing_time (listing_id, read_at)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 6: البحث والفلاتر والأداء
يبحث المشترون حسب المحافظة والسعر والسنة والوقود وناقل الحركة، ويتوقعون النتائج في أقل من ثانية. تجعل الفهارس على الأعمدة الواردة في WHERE وORDER BY ذلك ممكناً، وتُبقي الصفحات العميقة سريعة بالترقيم بالمفتاح keyset.
فهارس تطابق الاستعلام
يخدم الفهرس المركب على الحالة والمحافظة والسعر والسنة البحث الشائع. ترتيب الأعمدة مهم: ضع أعمدة المساواة أولاً وعمود المدى أخيراً. يبيّن EXPLAIN ما إذا كان الفهرس مستخدماً، ويبيّن EXPLAIN ANALYZE الزمن الفعلي.
البحث النصي الكامل والفلاتر المرتبطة
يدعم فهرس FULLTEXT على الوصف كلمات مثل فتحة السقف أو الجلد. يُحفظ الوقود وناقل الحركة في جدول الإصدارات، لذلك يربط البحث هذا الجدول، ويستخدم الربط فهرس الإصدار.
الترقيم بالمفتاح والبحوث المحفوظة
يقرأ OFFSET كل الصفوف السابقة ويتخلص منها، فتصبح الصفحات العميقة أبطأ. يتذكر الترقيم بالمفتاح آخر وقت إنشاء ومعرّف ويكمل من هناك. يحفظ البحث المحفوظ الفلاتر بصيغة JSON حتى يتلقى المشتري الإعلانات الجديدة المطابقة.
مثال 39: فهرس للبحث
افهرس الحالة والمحافظة والسعر والسنة معاً.
CREATE INDEX idx_listings_search
ON car_listings (status, province, asking_price, year);
EXPLAIN SELECT id, asking_price FROM car_listings
WHERE status = 'active' AND province = 'Chonburi' AND asking_price BETWEEN 500000 AND 900000;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 40: البحث في النص الكامل
ابحث في الوصف باستخدام MATCH AGAINST.
ALTER TABLE car_listings ADD COLUMN description TEXT NULL,
ADD FULLTEXT INDEX ft_listings_description (description);
SELECT id, asking_price FROM car_listings
WHERE status = 'active' AND MATCH(description) AGAINST ('sunroof leather' IN NATURAL LANGUAGE MODE);
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 41: التصفية حسب الوقود وناقل الحركة
اربط جدول الإصدارات للبحث عن سيارات هجينة CVT.
SELECT l.id, l.asking_price, t.fuel, t.transmission
FROM car_listings l
JOIN car_trims t ON t.id = l.trim_id
WHERE l.status = 'active' AND t.fuel = 'hybrid' AND t.transmission = 'cvt'
ORDER BY l.asking_price
LIMIT 20;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 42: الترقيم بالمفتاح keyset
تصفّح النتائج دون OFFSET المكلف.
SELECT id, asking_price, created_at
FROM car_listings
WHERE status = 'active'
AND (created_at, id) < ('2026-10-01 10:00:00', 1000)
ORDER BY created_at DESC, id DESC
LIMIT 20;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 43: عمليات البحث المحفوظة
خزّن الفلاتر التي اختارها المشتري بصيغة JSON.
CREATE TABLE saved_searches (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
filters JSON NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB;
INSERT INTO saved_searches (user_id, filters)
SELECT id, JSON_OBJECT('make','Toyota','fuel','hybrid','max_price',900000)
FROM users WHERE email = '[email protected]';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 44: نموذج: الشريط الجانبي للفلاتر
فلاتر الماركة والسعر والسنة والوقود مع زر عرض.
<aside class="filters">
<h3>Filters</h3>
<label>Make <select><option>Toyota</option><option>Honda</option></select></label>
<label>Price (THB) <input type="range" min="100000" max="2000000"></label>
<label>Year from <input type="number" value="2018"></label>
<fieldset>
<legend>Fuel</legend>
<label><input type="checkbox" checked> Hybrid</label>
<label><input type="checkbox"> Petrol</label>
</fieldset>
<button>Show 24 cars</button>
</aside>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 45: عدّ السيارات حسب الماركة
اعرض الأعداد بجوار كل خيار.
SELECT mk.name AS make, COUNT(*) AS cars
FROM car_listings l
JOIN car_trims t ON t.id = l.trim_id
JOIN car_models m ON m.id = t.model_id
JOIN car_makes mk ON mk.id = m.make_id
WHERE l.status = 'active'
GROUP BY mk.name
ORDER BY cars DESC;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 46: فحص خطة الاستعلام
استخدم EXPLAIN ANALYZE لمعرفة هل يُستخدم الفهرس.
EXPLAIN ANALYZE
SELECT id FROM car_listings
WHERE status = 'active' AND province = 'Chonburi'
ORDER BY asking_price
LIMIT 20;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 7: العروض والمزايدة
العرض هو اقتراح المشتري لسعر سيارة مع وقت انتهاء. يبقى سعر الطلب كما هو، فيستطيع عدة مشترين تقديم عروض، ويستطيع البائع قبول واحد ورفض الباقي، أو الرد بسعر جديد.
العروض في جدول مستقل
العرض صف يحمل الإعلان والمشتري والمبلغ والحالة ووقت الانتهاء. عرض البائع المضاد صف جديد يشير إلى عرض المشتري، فتبقى المفاوضات كلها ظاهرة في السجل.
الحد الأدنى للسعر والتحقق
يستطيع البائع تحديد حد أدنى للسعر. قارن العرض بالحد الأدنى قبل إظهاره للبائع كعرض مقبول. سجّل القرار مع سببه، حتى يمكن الرد على النزاعات لاحقاً.
الانتهاء والنموذج
تعلّم مهمة مجدولة العروض القديمة بأنها منتهية. يعرض مربع تقديم العرض وقت الانتهاء ويشرح ما يستطيع البائع فعله، فيعرف المشتري ما الذي سيحدث بعد ذلك.
مثال 47: العروض
يقترح المشتري سعراً، ويبقى سعر الإعلان دون تغيير.
CREATE TABLE offers (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
buyer_id BIGINT UNSIGNED NOT NULL,
amount DECIMAL(12,2) NOT NULL,
status ENUM('open','accepted','rejected','withdrawn','expired') NOT NULL DEFAULT 'open',
expires_at DATETIME NOT NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (buyer_id) REFERENCES users(id),
INDEX idx_offers_listing_status (listing_id, status),
CHECK (amount > 0)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 48: عرض تجريبي
يعرض المشتري 720,000 بات لمدة 48 ساعة.
INSERT INTO offers (listing_id, buyer_id, amount, expires_at)
SELECT 1, u.id, 720000.00, DATE_ADD(NOW(), INTERVAL 48 HOUR)
FROM users u WHERE u.email = '[email protected]';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 49: نموذج: نافذة تقديم العرض
حقل المبلغ وملاحظة الانتهاء وزر الإرسال.
<dialog id="offer">
<form method="dialog">
<h2>Make an offer</h2>
<p>Asking price: ฿765,000</p>
<label>Your offer (THB)
<input type="number" min="1" step="1000" value="720000" required>
</label>
<p class="hint">The offer expires in 48 hours. The seller can accept, reject or counter.</p>
<button value="cancel">Cancel</button>
<button value="send" class="primary">Send offer</button>
</form>
</dialog>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 50: رفض عرض منخفض
قارن العرض بأدنى سعر يسمح به البائع.
ALTER TABLE car_listings ADD COLUMN min_offer DECIMAL(12,2) NULL;
UPDATE car_listings SET min_offer = 700000.00 WHERE id = 1;
SELECT CASE WHEN 650000.00 < min_offer THEN 'reject: below minimum' ELSE 'accept for review' END AS decision
FROM car_listings WHERE id = 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 51: قبول العرض في معاملة واحدة
اقفل السيارة واقبل العرض واحجز الإعلان معاً.
START TRANSACTION;
SELECT id, status FROM car_listings WHERE id = 1 FOR UPDATE;
UPDATE offers SET status = 'accepted' WHERE id = 1 AND status = 'open';
UPDATE car_listings SET status = 'reserved' WHERE id = 1 AND status = 'active';
COMMIT;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 52: رفض العروض الأخرى
عندما يُقبل عرض، تُغلق بقية العروض للسيارة نفسها.
UPDATE offers
SET status = 'rejected'
WHERE listing_id = 1 AND status = 'open' AND id <> 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 53: إنهاء صلاحية العروض القديمة
تغلق مهمة مجدولة العروض التي تجاوزت موعدها.
UPDATE offers
SET status = 'expired'
WHERE status = 'open' AND expires_at < NOW();
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 54: عرض مضاد من البائع
خزّن العرض المضاد كعرض جديد مرتبط بعرض المشتري.
ALTER TABLE offers ADD COLUMN parent_offer_id BIGINT UNSIGNED NULL,
ADD COLUMN made_by ENUM('buyer','seller') NOT NULL DEFAULT 'buyer';
INSERT INTO offers (listing_id, buyer_id, amount, expires_at, parent_offer_id, made_by)
SELECT 1, buyer_id, 735000.00, DATE_ADD(NOW(), INTERVAL 24 HOUR), id, 'seller'
FROM offers WHERE id = 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 8: الطلبات ومسار الحالات
عندما يقبل البائع عرضاً، يُنشأ طلب. يمر الطلب عبر مجموعة ثابتة من الحالات، ويُسجَّل كل تغيير، فيستطيع موظفو الدعم دائماً أن يروا كيف وصلت عملية البيع إلى حالتها الحالية.
طلب مؤكد واحد لكل سيارة
عمود listing_id في جدول الطلبات عليه قيد UNIQUE، لذلك ترفض قاعدة البيانات طلباً ثانياً لسيارة واحدة حتى لو وصل طلبان في اللحظة نفسها. يُحسب العربون المستحق من السعر النهائي، وهنا 10 بالمئة.
إنشاء الطلب في المعاملة نفسها
قبول العرض وحجز الإعلان وإدراج الطلب تتم داخل معاملة واحدة. إذا فشلت أي خطوة، تُتراجع كلها، فلا تُحجز سيارة دون طلب، ولا يوجد طلب دون سيارة محجوزة.
سجل الحالات والخطوات المسموحة
يُكتب كل تغيير في الطلب إلى جدول السجل مع الحالة القديمة والجديدة والفاعل والوقت. يتحقق UPDATE من الحالة الحالية، فلا يستطيع الطلب القفز من بانتظار العربون مباشرة إلى مكتمل.
مثال 55: جدول الطلبات
طلب شراء مؤكد واحد لكل سيارة، مع السعر النهائي والعربون.
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL UNIQUE,
buyer_id BIGINT UNSIGNED NOT NULL,
seller_id BIGINT UNSIGNED NOT NULL,
final_price DECIMAL(12,2) NOT NULL,
deposit_due DECIMAL(12,2) NOT NULL,
status ENUM('pending_deposit','deposit_paid','documents_pending','completed','cancelled','refunded') NOT NULL DEFAULT 'pending_deposit',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (buyer_id) REFERENCES users(id),
FOREIGN KEY (seller_id) REFERENCES users(id),
CHECK (final_price > 0),
CHECK (deposit_due <= final_price)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 56: إنشاء الطلب من العرض المقبول
أدخل الطلب داخل المعاملة نفسها التي تقبل العرض.
START TRANSACTION;
SELECT status FROM car_listings WHERE id = 1 FOR UPDATE;
INSERT INTO orders (listing_id, buyer_id, seller_id, final_price, deposit_due)
SELECT o.listing_id, o.buyer_id, l.seller_id, o.amount, ROUND(o.amount * 0.10, 2)
FROM offers o JOIN car_listings l ON l.id = o.listing_id
WHERE o.id = 1 AND o.status = 'accepted';
COMMIT;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 57: سجل حالات الطلب
سجّل كل تغيير في الحالة مع من قام به ومتى.
CREATE TABLE order_status_history (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
from_status VARCHAR(30) NULL,
to_status VARCHAR(30) NOT NULL,
actor_id BIGINT UNSIGNED NULL,
changed_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
INDEX idx_history_order (order_id, changed_at)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 58: تقديم الطلب خطوة واحدة
غيّر الحالة فقط من الخطوة المتوقعة.
UPDATE orders
SET status = 'deposit_paid'
WHERE id = 1 AND status = 'pending_deposit';
INSERT INTO order_status_history (order_id, from_status, to_status, actor_id)
VALUES (1, 'pending_deposit', 'deposit_paid', 2);
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 59: إلغاء الطلب وإعادة السيارة
أعد الإعلان إلى نشط عند إلغاء الطلب مبكراً.
START TRANSACTION;
UPDATE orders SET status = 'cancelled' WHERE id = 1 AND status IN ('pending_deposit','deposit_paid');
UPDATE car_listings SET status = 'active' WHERE id = 1 AND status = 'reserved';
COMMIT;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 60: نموذج: صفحة تقدم الطلب
الخطوات من قبول العرض حتى إتمام البيع.
<section class="order">
<h2>Order #1 · Toyota Camry G 2.5 (2019)</h2>
<ol class="steps">
<li class="done">Offer accepted</li>
<li class="done">Deposit paid ฿76,500</li>
<li class="current">Documents in progress</li>
<li>Ownership transferred</li>
<li>Completed</li>
</ol>
<button>Cancel order</button>
<p class="hint">You can cancel until the documents step starts.</p>
</section>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 9: المدفوعات كدفتر حسابات
لا يُخزَّن المال أبداً كرقم واحد يُعدَّل مراراً. كل دفعة واسترداد ورصيد متبقٍّ هو صف في دفتر الحسابات. تُحسب المجاميع من الصفوف، فيبقى السجل كاملاً دائماً والأرقام متوازنة دائماً.
صف واحد لكل حركة مالية
للدفعة نوع، مثل عربون أو رصيد أو استرداد، ومبلغ، ومرجع من مزود الدفع، وحالة. مرجع المزود فريد UNIQUE، فلا يمكن تسجيل الدفعة نفسها مرتين، ويربط الصف بسجل البنك أو مزود الدفع نفسه.
إشعارات تصل مرتين
قد يرسل مزود الدفع الإشعار نفسه أكثر من مرة. يغيّر التحديث الحالة فقط عندما يكون الصف ما زال معلقاً، فلا تغيّر الرسالة الثانية شيئاً. هذا يجعل المعالج آمناً عند إعادة المحاولة.
الاسترداد وصافي المدفوع
الاسترداد صف جديد، وليس دفعة سالبة تكتب فوق القديمة. صافي المدفوع للطلب هو مجموع الدفعات الناجحة مطروحاً منه الاستردادات الناجحة، ويوقف فحص ما قبل الإصدار الإصدار إذا تجاوز صافي المدفوع السعر النهائي.
مثال 61: جدول المدفوعات
كل حركة مال صف واحد مع مرجع من مزود الدفع.
CREATE TABLE payments (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
kind ENUM('deposit','balance','refund') NOT NULL,
amount DECIMAL(12,2) NOT NULL,
provider VARCHAR(40) NOT NULL,
provider_ref VARCHAR(100) NOT NULL UNIQUE,
status ENUM('pending','succeeded','failed') NOT NULL DEFAULT 'pending',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (order_id) REFERENCES orders(id),
CHECK (amount > 0)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 62: تسجيل العربون قبل استدعاء الدفع
خزّن الدفعة كمعلقة أولاً ثم حدّثها.
INSERT INTO payments (order_id, kind, amount, provider, provider_ref, status)
VALUES (1, 'deposit', 76500.00, 'example-pay', 'ref-0001', 'pending');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 63: معالجة إشعار دفع مكرر
حدّث مرة واحدة، فالرسالة المكررة لا تغيّر شيئاً.
UPDATE payments
SET status = 'succeeded'
WHERE provider_ref = 'ref-0001' AND status = 'pending';
-- 0 rows changed on the second call, so the order is not moved twice
UPDATE orders SET status = 'deposit_paid'
WHERE id = 1 AND status = 'pending_deposit'
AND (SELECT status FROM payments WHERE provider_ref = 'ref-0001') = 'succeeded';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 64: صافي المدفوع لكل طلب
اجمع العربون والمتبقي واطرح المبالغ المستردة من سجل الحسابات.
SELECT o.id,
o.final_price,
COALESCE(SUM(CASE WHEN p.kind IN ('deposit','balance') AND p.status = 'succeeded' THEN p.amount END), 0)
- COALESCE(SUM(CASE WHEN p.kind = 'refund' AND p.status = 'succeeded' THEN p.amount END), 0) AS net_paid
FROM orders o
LEFT JOIN payments p ON p.order_id = o.id
WHERE o.id = 1
GROUP BY o.id, o.final_price;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 65: استرداد العربون
أدخل صف استرداد مرتبطاً بالعربون الأصلي.
INSERT INTO payments (order_id, kind, amount, provider, provider_ref, status)
SELECT order_id, 'refund', amount, provider, CONCAT('refund-', provider_ref), 'pending'
FROM payments
WHERE order_id = 1 AND kind = 'deposit' AND status = 'succeeded';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 66: نموذج: شاشة دفع العربون
مبلغ العربون وملاحظة الحجز وزر الدفع.
<form class="payment">
<h2>Pay the deposit</h2>
<p>Deposit (10%): <strong>฿76,500</strong></p>
<p class="hint">Held until the ownership transfer is done. Refunded if the sale fails.</p>
<label>Card number <input name="card" autocomplete="cc-number" placeholder="•••• •••• •••• ••••"></label>
<button type="submit" class="primary">Pay ฿76,500</button>
</form>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 10: نقل الملكية والمستندات
في كثير من الدول لا يتغير مالك السيارة إلا في مكتب النقل، وبأوراق موقعة. تنمذج قاعدة البيانات هذا كقائمة من الخطوات، لكل خطوة مستنداتها، فيرى المشتري بالضبط ما تبقّى، ويتحقق الموظفون من كل مستند قبل أن تُعلَّم السيارة مباعة.
الخطوات كصفوف
لكل خطوة نقل رقم واسم وحالة: قيد التنفيذ، أو قيد العمل، أو مكتملة، أو متوقفة. لا يكتمل الطلب إلا عندما تكتمل كل الخطوات، وهذا يُتحقق منه في المعاملة نفسها التي تعلّم السيارة مباعة.
المستندات والتحقق
لكل مستند نوع، مثل عقد البيع أو دفتر التسجيل، ورابط الملف، والشخص الذي رفعه، والوقت الذي تم التحقق فيه. لا تكتمل الخطوة إلا إذا تم التحقق من مستندها، فلا تكفي صورة لنموذج غير مُتحقق منه.
قائمة المشتري
يعرض النموذج للمشتري قائمة بعلامات الصح وزر رفع. تُولَّد القائمة من جدول الخطوات، فتتفق الشاشة وقاعدة البيانات دائماً على ما اكتمل.
مثال 67: خطوات نقل الملكية
صف واحد لكل خطوة في تغيير المالك.
CREATE TABLE transfer_steps (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
step_no TINYINT UNSIGNED NOT NULL,
name VARCHAR(80) NOT NULL,
status ENUM('todo','doing','done','blocked') NOT NULL DEFAULT 'todo',
done_at DATETIME NULL,
UNIQUE KEY uq_step (order_id, step_no),
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 68: الخطوات القياسية للطلب
خمس خطوات من عقد البيع حتى تسليم المفاتيح.
INSERT INTO transfer_steps (order_id, step_no, name) VALUES
(1, 1, 'Seller signs the sale agreement'),
(1, 2, 'Buyer checks the registration book'),
(1, 3, 'Land Transport Office appointment booked'),
(1, 4, 'Ownership changed at the office'),
(1, 5, 'Balance paid and keys handed over');
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 69: المستندات
خزّن كل مستند مرفوع مع نوعه ووقت التحقق منه.
CREATE TABLE documents (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
doc_type ENUM('sale_agreement','registration_book','id_card','tax_receipt','transfer_form') NOT NULL,
file_url VARCHAR(500) NOT NULL,
uploaded_by BIGINT UNSIGNED NOT NULL,
verified_at DATETIME NULL,
FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE,
FOREIGN KEY (uploaded_by) REFERENCES users(id)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 70: أكمل الخطوة فقط بمستند موثّق
تحقق من المستند قبل وضع علامة الإنجاز على الخطوة.
UPDATE transfer_steps s
SET s.status = 'done', s.done_at = NOW()
WHERE s.order_id = 1 AND s.step_no = 1
AND EXISTS (SELECT 1 FROM documents d
WHERE d.order_id = s.order_id AND d.doc_type = 'sale_agreement' AND d.verified_at IS NOT NULL);
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 71: نموذج: قائمة مراجعة النقل
خانات اختيار لكل خطوة مع زر رفع.
<ul class="checklist">
<li><input type="checkbox" checked disabled> Seller signed the sale agreement</li>
<li><input type="checkbox" checked disabled> Registration book checked</li>
<li><input type="checkbox"> Book an appointment at the office</li>
<li><input type="checkbox"> Ownership changed</li>
<li><input type="checkbox"> Balance paid</li>
</ul>
<button>Upload a document</button>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 72: غيّر المالك في النهاية
أكمل الطلب فقط عندما تكتمل كل الخطوات.
START TRANSACTION;
UPDATE transfer_steps SET status = 'done', done_at = NOW()
WHERE order_id = 1 AND step_no = 4 AND status = 'doing';
UPDATE orders SET status = 'completed'
WHERE id = 1 AND status = 'documents_pending'
AND NOT EXISTS (SELECT 1 FROM transfer_steps WHERE order_id = 1 AND status <> 'done');
UPDATE car_listings SET status = 'sold' WHERE id = 1 AND status = 'reserved';
COMMIT;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 11: التمويل والضمانات
لا يدفع معظم المشترين السعر الكامل نقداً. القرض البنكي طلب مرتبط بالطلب، والضمان يحمي المشتري لفترة من الأشهر أو الكيلومترات بعد البيع.
طلبات التمويل
ينتمي طلب التمويل إلى طلب شراء ويسجل البنك والمبلغ والمدة والحالة: مقدّم، أو موافق عليه، أو مرفوض، أو مسحوب. تُحدّد المدة بقاعدة CHECK، مثل 6 إلى 84 شهراً، فلا يمكن إدخال قروض مستحيلة.
ضمانات تبدأ من تاريخ البيع
يُنشأ الضمان فقط عندما يكتمل الطلب. يخزّن عدد الأشهر وحد الكيلومترات وتاريخ البداية. يقارن التحقق من بقاء الضمان سارياً التاريخ والعداد معاً، فيحصل المشتري على الإجابة الصحيحة.
عرض العرض بوضوح
تعرض صفحة الطلب القسط الشهري كتقدير وشروط الضمان بلغة واضحة. تقلل المعلومات الواضحة النزاعات بعد البيع، وهذا أرخص من معالجة الشكاوى لاحقاً.
مثال 73: طلبات التمويل
يطلب المشتري قرضاً من بنك لشراء السيارة.
CREATE TABLE financing_applications (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
bank VARCHAR(80) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
term_months SMALLINT UNSIGNED NOT NULL,
status ENUM('submitted','approved','declined','withdrawn') NOT NULL DEFAULT 'submitted',
FOREIGN KEY (order_id) REFERENCES orders(id),
CHECK (term_months BETWEEN 6 AND 84)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 74: طلب تجريبي
طلب قرض لمدة 60 شهراً بقيمة 600,000 بات.
INSERT INTO financing_applications (order_id, bank, amount, term_months)
VALUES (1, 'Example Bank', 600000.00, 60);
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 75: الضمانات
ضمان واحد لكل طلب، بعدد الأشهر والكيلومترات.
CREATE TABLE warranties (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL UNIQUE,
months SMALLINT UNSIGNED NOT NULL,
km_limit INT UNSIGNED NOT NULL,
starts_on DATE NOT NULL,
FOREIGN KEY (order_id) REFERENCES orders(id)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 76: ابدأ الضمان عند إتمام البيع
أنشئ الضمان من الطلب المكتمل.
INSERT INTO warranties (order_id, months, km_limit, starts_on)
SELECT id, 6, 10000, CURDATE() FROM orders WHERE id = 1 AND status = 'completed';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 77: تحقق من بقاء الضمان ساري المفعول
قارن التاريخ والمسافة بالحدود.
SELECT w.id,
(CURDATE() < DATE_ADD(w.starts_on, INTERVAL w.months MONTH)) AND (48250 + 1500 <= 48250 + w.km_limit) AS still_valid
FROM warranties w WHERE w.order_id = 1;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 78: نموذج: صندوق القرض والضمان
القسط التقديري وشروط الضمان في صفحة الطلب.
<aside class="box">
<h3>Pay with a loan</h3>
<p>Example Bank · 60 months</p>
<p>Monthly payment is an estimate: <strong>฿11,800</strong></p>
<p class="status">Status: Submitted</p>
<h3>Warranty</h3>
<p>6 months or 10,000 km, whichever comes first</p>
</aside>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 12: المراجعات والبلاغات والثقة
لا يعمل السوق إلا إذا وثق الناس به. تأتي المراجعات بعد البيع المكتمل، وتتيح البلاغات للمستخدمين الإبلاغ عن المشكلات، ويجب أن تكون القواعد التي تُخفي الإعلانات واضحة وعادلة، حتى لا يستطيع مستخدم غاضب واحد إخفاء سيارة جيدة.
المراجعات بعد البيع
تُسمح المراجعة للطلب المكتمل فقط، ويمكن لكل شخص مراجعة مرة واحدة لكل طلب. تُحصر التقييمات بين 1 و5 بقاعدة CHECK. يظهر المتوسط بعد ثلاث مراجعات فقط، حتى لا تجعل مراجعة واحدة البائع يبدو مثالياً أو سيئاً للغاية.
بلاغات من عدة أشخاص
يذكر البلاغ ما هو الخطأ، مثل سعر مزيف أو عداد غير صحيح. عدّ المبلّغين المختلفين قبل إخفاء الإعلان، حتى لا يستطيع شخص واحد إخفاء سيارة بمفرده. تذهب الإعلانات المخفية إلى قائمة مراجعة لدى الموظفين.
نموذج لإجراءات المراجعة والبلاغ
يطلب نموذج المراجعة النجوم والتعليق، ورابط البلاغ مرئي دون أن يعيق. التصميم الجيد يجعل الإبلاغ سهلاً ونشر مراجعة بالخطأ صعباً.
مثال 79: التقييمات بعد البيع المكتمل
تقييم واحد لكل شخص لكل طلب، من 1 إلى 5.
CREATE TABLE reviews (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
order_id BIGINT UNSIGNED NOT NULL,
reviewer_id BIGINT UNSIGNED NOT NULL,
reviewee_id BIGINT UNSIGNED NOT NULL,
rating TINYINT UNSIGNED NOT NULL,
comment VARCHAR(1000) NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
UNIQUE KEY uq_review (order_id, reviewer_id),
FOREIGN KEY (order_id) REFERENCES orders(id),
CHECK (rating BETWEEN 1 AND 5)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 80: أضف تقييماً لطلب مكتمل
الطلب المكتمل فقط يقبل التقييم.
INSERT INTO reviews (order_id, reviewer_id, reviewee_id, rating, comment)
SELECT o.id, o.buyer_id, o.seller_id, 5, 'Car as described, smooth transfer.'
FROM orders o WHERE o.id = 1 AND o.status = 'completed';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 81: متوسط تقييم البائع
اعرض تقييم البائع فقط بعد ثلاثة تقييمات.
SELECT r.reviewee_id, ROUND(AVG(r.rating), 2) AS avg_rating, COUNT(*) AS reviews
FROM reviews r
GROUP BY r.reviewee_id
HAVING COUNT(*) >= 3;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 82: بلاغات الإعلانات
يمكن للمشتري والبائع الإبلاغ عن إعلان مشبوه.
CREATE TABLE listing_reports (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
listing_id BIGINT UNSIGNED NOT NULL,
reporter_id BIGINT UNSIGNED NOT NULL,
reason ENUM('fake_price','stolen','wrong_mileage','duplicate','other') NOT NULL,
status ENUM('open','investigating','closed') NOT NULL DEFAULT 'open',
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (listing_id) REFERENCES car_listings(id),
FOREIGN KEY (reporter_id) REFERENCES users(id)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 83: إخفاء الإعلان بعد بلاغات متعددة
عدّ المُبلِّغين المختلفين قبل اتخاذ الإجراء.
SELECT listing_id, COUNT(DISTINCT reporter_id) AS reporters
FROM listing_reports
WHERE status = 'open'
GROUP BY listing_id
HAVING COUNT(DISTINCT reporter_id) >= 3;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 84: نموذج: التقييم وزر البلاغ
تقييم بالنجوم ومربع تعليق ورابط بلاغ.
<form class="review">
<h3>Rate your purchase</h3>
<div class="stars" role="radiogroup" aria-label="Rating">
<label><input type="radio" name="rating" value="1"> 1</label>
<label><input type="radio" name="rating" value="5" checked> 5</label>
</div>
<textarea maxlength="1000" placeholder="How was the car and the transfer?"></textarea>
<button>Post review</button>
<button type="button" class="link">Report this listing</button>
</form>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 13: المعاملات في العمل: حالات السباق
يستطيع مشتريان الضغط على زر القبول في اللحظة نفسها. بدون حذر، يقرأ الطلبان أن السيارة ما زالت نشطة، فيبيعانها معاً. يوضح هذا الموضوع كيف تمنع المعاملات وأقفال الصفوف وترتيب الأقفال ومفاتيح idempotency هذه المشكلة وغيرها من أعطال النظام الحقيقي.
ما الذي يحدث بدون قفل
تقرأ الجلسة A أن السيارة نشطة. تقرأ الجلسة B الحالة نفسها. يحدّث الاثنان الحالة إلى محجوزة، ويُنشئ الاثنان طلباً. تُباع السيارة مرتين. يجعل القفل على صف السيارة، المأخوذ بـ SELECT FOR UPDATE، الجلسة الثانية تنتظر حتى تُثبَّت الأولى، ثم ترى الجلسة الثانية أنها محجوزة فتتوقف.
تحقق من عدد الصفوف المتغيرة
يغيّر UPDATE مع WHERE status = active صفاً واحداً للفائز وصفراً للخاسر. يقرأ التطبيق عدد الصفوف المتأثرة، ويعيد 409 Conflict للخاسر، فيرى المشتري رسالة واضحة بدلاً من نجاح زائف.
التعارض المتبادل، والتراجع، وإعادة المحاولة
قد تتعارض معاملتان تقفلان الصفوف نفسها بترتيب مختلف، فيحدث تعارض متبادل ويتراجع MySQL عن إحداهما. اقفل السيارة أولاً ثم العروض ثم الطلبات في كل مسار برمجي. عندما يفشل شيء في منتصف الطريق، تتراجع ROLLBACK عن كل تغيير في هذه المعاملة، ويستطيع المستدعي إعادة المحاولة بأمان بمفتاح idempotency نفسه.
مثال 85: مشتريان يقبلان في الوقت نفسه
اقفل الصف حتى تنجح معاملة واحدة فقط.
-- Session A
START TRANSACTION;
SELECT status FROM car_listings WHERE id = 1 FOR UPDATE;
-- Session B waits here until A commits, then reads 'reserved' and stops.
UPDATE car_listings SET status = 'reserved' WHERE id = 1 AND status = 'active';
COMMIT;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 86: تحقق من عدد الصفوف المحدّثة
العدد 1 يعني أن هذا الطلب نجح، والعدد 0 يعني أنه خسر.
UPDATE car_listings SET status = 'reserved'
WHERE id = 1 AND status = 'active';
-- Application code: if ROW_COUNT() = 0, return 409 Conflict to the second buyer.
SELECT ROW_COUNT() AS won;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 87: اقفل بترتيب ثابت
كل مسار يقفل السيارة قبل العروض لتفادي الجمود.
-- Every code path locks in this order: car_listings, then offers, then orders
START TRANSACTION;
SELECT id FROM car_listings WHERE id = 1 FOR UPDATE;
SELECT id FROM offers WHERE listing_id = 1 FOR UPDATE;
COMMIT;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 88: التراجع عند أخذ السيارة
ألغِ الطلب إذا فشل الحجز في منتصف العملية.
START TRANSACTION;
UPDATE car_listings SET status = 'reserved' WHERE id = 2 AND status = 'active';
-- If the car was not reserved, nothing else should happen.
-- Application code: when ROW_COUNT() = 0, run ROLLBACK instead of inserting the order.
ROLLBACK;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 89: مفاتيح منع التكرار
الطلب المكرر بالمفتاح نفسه يعيد النتيجة الأولى.
CREATE TABLE idempotency_keys (
key_value CHAR(36) PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
response JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (user_id) REFERENCES users(id)
) ENGINE=InnoDB;
INSERT IGNORE INTO idempotency_keys (key_value, user_id)
VALUES ('5f3c2a10-0000-4000-8000-000000000001', 2);
-- ROW_COUNT() = 0 means the key already exists: return the stored response.
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 90: سجل التدقيق
احفظ من غيّر ماذا، مع القيم القديمة والجديدة.
CREATE TABLE audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
actor_id BIGINT UNSIGNED NULL,
action VARCHAR(60) NOT NULL,
entity VARCHAR(40) NOT NULL,
entity_id BIGINT UNSIGNED NOT NULL,
detail JSON NULL,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_audit_entity (entity, entity_id, created_at)
) ENGINE=InnoDB;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 14: التقارير والتحليلات
يحتاج العمل إلى أرقام: المبيعات اليومية، وقيمة المخزون لكل تاجر، ومسار التحويل من الإدراج حتى البيع. تقرأ التقارير الجداول نفسها، لذلك يجب أن تستخدم الحالات الصحيحة، ولا تحسب الطلب الملغى أبداً كإيراد.
المبيعات اليومية من الطلبات المكتملة
احسب الطلبات المكتملة فقط للإيراد. جمّع حسب يوم إنشاء الطلب، أو يوم إكماله إذا احتاجت المحاسبة ذلك. اشرح هذا الاختيار في التقرير، حتى يتفق شخصان يقرآن الرقم نفسه على معناه.
عروض للتجار
يعطي العرض التجار عدد مخزونهم وقيمة المخزون ومتوسط أيام العرض في مكان واحد. يخفي العرض عمليات الربط، فيستعلم التطبيق منه كجدول، وتبقى القواعد في تعريف واحد.
قمع التحويل ولوحة المعلومات
احسب السيارات المدرجة، والسيارات التي وصلها عرض واحد على الأقل، والطلبات المكتملة. تعرض لوحة المعلومات الأرقام الرئيسية في الأعلى وجدول السيارات النشطة في الأسفل، فيرى البائع أولاً ما يحتاج إلى انتباه.
مثال 91: تقرير المبيعات اليومي
القيمة الإجمالية للطلبات المكتملة في كل يوم.
SELECT DATE(o.created_at) AS day,
COUNT(*) AS orders,
SUM(o.final_price) AS gross_value
FROM orders o
WHERE o.status = 'completed'
GROUP BY DATE(o.created_at)
ORDER BY day DESC;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 92: عرض مخزون التاجر
عدد السيارات وقيمة المخزون وأيام العرض لكل تاجر.
CREATE VIEW v_dealer_stock AS
SELECT d.id AS dealer_id,
COUNT(l.id) AS cars,
SUM(l.asking_price) AS stock_value,
AVG(DATEDIFF(NOW(), l.created_at)) AS avg_days_listed
FROM dealers d
JOIN car_listings l ON l.seller_id = d.user_id
WHERE l.status = 'active'
GROUP BY d.id;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 93: قمع التحويل
عدّ السيارات المعروضة والسيارات التي وصلتها عروض والطلبات المكتملة.
SELECT
(SELECT COUNT(*) FROM car_listings WHERE status IN ('active','reserved','sold')) AS listed,
(SELECT COUNT(DISTINCT listing_id) FROM offers) AS got_offers,
(SELECT COUNT(*) FROM orders WHERE status = 'completed') AS completed;
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 94: نموذج: لوحة البائع
الأرقام الأساسية وجدول السيارات النشطة.
<section class="dashboard">
<div class="kpi"><span>Active cars</span><strong>12</strong></div>
<div class="kpi"><span>Open offers</span><strong>3</strong></div>
<div class="kpi"><span>Sold this month</span><strong>2</strong></div>
<table>
<tr><th>Car</th><th>Price</th><th>Offers</th><th>Days listed</th></tr>
<tr><td>Camry 2019</td><td>฿765,000</td><td>2</td><td>14</td></tr>
</table>
</section>
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
الموضوع 15: الإنتاج: واجهة البرمجة والأمان والنسخ الاحتياطي والإصدار
قاعدة البيانات التي تعمل على الحاسوب المحمول ليست سوقاً بعد. يحتاج الإنتاج إلى عقد واجهة برمجية واضح، ومستخدمي قاعدة بيانات بأقل الصلاحيات، ونسخاً احتياطية يمكن استعادتها، وتغييرات آمنة في المخطط، وقائمة فحص للإصدار توقف الإصدار السيئ قبل أن يصل إلى المشترين.
عقد الواجهة البرمجية وidempotency
أعد الإعلانات بشكل JSON ثابت مع مؤشر للصفحة التالية. اطلب ترويسة Idempotency-Key للعروض والطلبات، حتى لا تُنشئ إعادة المحاولة بعد انتهاء المهلة طلباً ثانياً. وثّق قيم الحالات، لأن تطبيق الجوال وتطبيق الويب يجب أن يقرآها بالطريقة نفسها.
أقل الصلاحيات والنسخ الاحتياطي والترحيل
يستطيع مستخدم التطبيق قراءة قاعدة بيانات السيارات وإدراجها وتحديثها، لكن لا يستطيع حذف المدفوعات أو سجلات التدقيق. انسخ احتياطياً بتفريغ بمعاملة واحدة حتى لا تُحجب الكتابات، واختبر الاستعادة على جهاز آخر. غيّر المخطط بخطوات صغيرة: أضف عموداً يقبل NULL، واملأه على دفعات، ثم شدّد القاعدة في إصدار لاحق.
قائمة فحص الإصدار
قبل كل إصدار، شغّل الفحوص التجارية: لا توجد سيارة نشطة بلا فحص ناجح، ولا يوجد طلب يتجاوز فيه صافي المدفوع السعر النهائي. إذا فشل أي فحص، يتوقف سكربت الإصدار، فلا تصل قاعدة مكسورة إلى المشتري أبداً.
مثال 95: واجهة برمجية عامة للإعلانات
شكل JSON لصفحة الإعلانات مع مؤشر للصفحة التالية.
{
"method": "GET",
"path": "/api/v1/listings?province=Chonburi&max_price=900000&page_size=20",
"response": {
"items": [
{ "id": 1, "make": "Toyota", "model": "Camry", "trim": "G 2.5", "year": 2019, "mileage_km": 48250, "asking_price": 765000, "province": "Chonburi" }
],
"next_cursor": "eyJpZCI6MX0"
}
}
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 96: قدّم عرضاً عبر الواجهة البرمجية
أرسل مفتاح منع التكرار حتى تكون إعادة المحاولة آمنة.
curl -X POST https://api.example.com/api/v1/listings/1/offers \
-H "Authorization: Bearer <token>" \
-H "Idempotency-Key: 5f3c2a10-0000-4000-8000-000000000002" \
-H "Content-Type: application/json" \
-d '{"amount": 720000}'
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 97: أقل صلاحية للتطبيق
لا يستطيع مستخدم التطبيق حذف المدفوعات أو سجلات التدقيق.
CREATE USER IF NOT EXISTS 'carmarket_app'@'%' IDENTIFIED BY '<strong-password>';
GRANT SELECT, INSERT, UPDATE ON carmarket.* TO 'carmarket_app'@'%';
-- No DELETE on orders, payments or audit_log: those are kept for legal reasons.
REVOKE DELETE ON carmarket.payments FROM 'carmarket_app'@'%';
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 98: نسخ احتياطي دون حجب الكتابة
استخدم تفريغاً بمعاملة واحدة واختبر الاستعادة.
mysqldump --single-transaction --routines --triggers carmarket \
| gzip > carmarket-$(date +%F).sql.gz
# Restore test on another machine:
gunzip -c carmarket-2026-10-30.sql.gz | mysql carmarket_restore_test
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 99: تغيير آمن للمخطط
أضف عموداً يقبل NULL، ثم املأه على دفعات، ثم شدّد القيد لاحقاً.
-- Step 1: add the column as nullable (old code keeps working)
ALTER TABLE car_listings ADD COLUMN accident_free TINYINT(1) NULL;
-- Step 2: backfill in batches, not in one huge UPDATE
UPDATE car_listings SET accident_free = 1 WHERE id BETWEEN 1 AND 1000 AND accident_free IS NULL;
-- Step 3 (later release): set NOT NULL after every row has a value.
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
مثال 100: قائمة فحص الإصدار لقواعد العمل
أوقف الإصدار عندما تنكسر قاعدة مثل صافي المدفوع.
#!/usr/bin/env bash
set -euo pipefail
# 1. No active listing without a passing inspection
mysql -N carmarket -e "SELECT COUNT(*) FROM car_listings l WHERE l.status='active' AND NOT EXISTS (SELECT 1 FROM inspections i WHERE i.listing_id=l.id AND i.overall_score>=3)" | grep -q '^0$'
# 2. Net paid never exceeds the final price
mysql -N carmarket -e "SELECT COUNT(*) FROM orders o JOIN (SELECT order_id, SUM(amount) s FROM payments WHERE status='succeeded' AND kind IN ('deposit','balance') GROUP BY order_id) p ON p.order_id=o.id WHERE p.s > o.final_price" | grep -q '^0$'
echo "business checks passed"
*لم يُشغَّل هذا المثال هنا لأن MySQL لم يكن مثبتاً على الحاسوب الذي أُعدّت به هذه المقالة.*
المختبر: بناء قاعدة بيانات سوق السيارات واختبارها
اجمع المقالة على قاعدة بيانات اختبار: أنشئ الجداول بالترتيب، وأدخل سيارات ومستخدمين تجريبيين، ومرّر عرضاً عبر القبول وتدفق الطلب، وحاكِ مشترين اثنين في الوقت نفسه، ثم أنهِ بقائمة فحص الإصدار.
1. أنشئ قاعدة البيانات carmarket بترميز utf8mb4، ثم أنشئ جداول users وdealers وcar_makes وcar_models وcar_trims بهذا الترتيب.
2. أدخل المستخدمين التجريبيين وتاجراً واحداً والماركات والطرازات وإصداراً واحداً، ثم تحقق من عدد الصفوف بـ SELECT COUNT(*).
3. أنشئ car_listings وcar_images وinspections وinspection_items، ثم أدخل إعلاناً واحداً بصور وفحص ناجح.
4. انشر الإعلان فقط عبر القاعدة التي تشترط فحصاً ناجحاً، وتأكد من أن الإعلان بلا فحص يبقى قيد المراجعة.
5. أنشئ offers وorders، واقبل عرضاً واحداً داخل معاملة، وتحقق من سجل حالات الطلب.
6. افتح جلستي MySQL، واقفل السيارة نفسها في الجلستين، واعرض أن حجزاً واحداً فقط ينجح.
7. شغّل قائمة فحص الإصدار من المثال 100 على البيانات التجريبية، وأصلح أي قاعدة مكسورة قبل المتابعة.
الخطوات التالية
بعد المختبر، اقرأ أدلة تصميم قواعد البيانات لمجموعة أدواتك، وأضف شاشات تطبيق الجوال من النماذج، واختبر كل معاملة بجلستين في الوقت نفسه قبل الإطلاق. احتفظ بالسكربتات في مستودع وأعد تشغيلها بعد كل تغيير في المخطط، لأن قواعد السوق ستنمو مع نمو العمل.
الخلاصة
سوق السيارات المستعملة مجموعة من القواعد التي يجب أن تحفظها قاعدة البيانات: بيع واحد لكل سيارة، وفحص قبل النشر، وعربون محفوظ حتى يكتمل النقل، ودفتر حسابات يتوازن دائماً. تعلمت كتابة القواعد أولاً، ونمذجة المستخدمين والتجار وكتالوج السيارات، ونشر الإعلانات بالصور والفحص، والبحث بالفهارس والترقيم بالمفتاح، ومعالجة العروض والطلبات بمسارات حالات واضحة، وتسجيل المدفوعات كدفتر حسابات، وتتبع كل خطوة من نقل الملكية، وإضافة التمويل والضمانات، وبناء الثقة بالمراجعات والبلاغات، ومعالجة حالات السباق بالأقفال وidempotency، وإعداد التقارير التجارية، والإصدار بأمان.