مقدمه
یکی از پروژههای جذاب برای توسعهدهندگان Laravel، ساخت یک Marketplace یا بازارگاه اینترنتی است. در یک فروشگاه اینترنتی معمولی، محصولات معمولاً توسط یک فروشنده یا یک کسبوکار مدیریت میشوند؛ اما در Marketplace چندین فروشنده میتوانند محصولات خود را ثبت کنند و از طریق یک پلتفرم مشترک به مشتریان بفروشند.
نمونه شناختهشده این مدل کسبوکار، بازارگاههایی هستند که فروشندگان مختلف در آنها فعالیت میکنند، هر فروشنده پنل اختصاصی دارد و مدیر اصلی پلتفرم بر فروشندگان، سفارشها، کمیسیونها و عملکرد کلی سیستم نظارت میکند.
Laravel ابزارهای مناسبی برای ساخت چنین سامانهای در اختیار توسعهدهندگان قرار میدهد. امکاناتی مانند Eloquent ORM، سیستم احراز هویت، Middleware، Policy، اعتبارسنجی، تراکنشهای دیتابیس، Queue و تستنویسی کمک میکنند اجزای مختلف بازارگاه را با ساختاری منظم پیادهسازی کنیم.
در این مقاله با Laravel 12 یک معماری اولیه برای Marketplace طراحی میکنیم. تمرکز ما روی طراحی صحیح دیتابیس، تفکیک نقشها، مدیریت محصولات فروشندگان، ثبت سفارش، محاسبه کمیسیون و رعایت اصول امنیتی است.
نکته: هدف این آموزش، ساخت هسته اولیه یک Marketplace است؛ نه پیادهسازی کامل یک پلتفرم تجاری با تمام امکانات پرداخت، ارسال، تسویهحساب و حسابداری. هر بخش میتواند در مراحل بعدی توسعه پیدا کند.
Marketplace چیست و چه تفاوتی با فروشگاه معمولی دارد؟
Marketplace یک پلتفرم واسط است که فروشندگان متعدد را به مشتریان متصل میکند. مدیر پلتفرم زیرساخت اصلی را فراهم میکند و فروشندگان از طریق پنل خود محصولات و سفارشهای مرتبط با کسبوکارشان را مدیریت میکنند.
برای درک بهتر تفاوت این دو مدل، جدول زیر را ببینید:
| ویژگی | فروشگاه معمولی | Marketplace |
|---|---|---|
| تعداد فروشندگان | معمولاً یک فروشنده | چندین فروشنده |
| مدیریت محصولات | مدیر یا مالک فروشگاه | هر فروشنده برای محصولات خودش |
| مدیریت سفارش | یک کسبوکار | سفارشها ممکن است بین چند فروشنده تقسیم شوند |
| کمیسیون | معمولاً وجود ندارد | میتواند بر اساس فروش محاسبه شود |
| تسویهحساب | مستقیم با فروشگاه | ممکن است برای هر فروشنده جداگانه باشد |
| کنترل دسترسی | نسبتاً سادهتر | نیازمند تفکیک دقیق مالکیت اطلاعات |
در یک Marketplace واقعی، مسئله فقط نمایش محصولات چند فروشنده نیست. باید مشخص باشد هر فروشنده چه اطلاعاتی را مشاهده یا تغییر میدهد، سفارشها چگونه تقسیم میشوند، کمیسیون چطور محاسبه میشود و مسئولیت بازپرداخت یا لغو سفارش بر عهده چه کسی است.
مرحله اول: تعیین نقشهای اصلی سیستم
پیش از نوشتن کد، بهتر است نقشهای سیستم را مشخص کنیم.
در یک Marketplace پایه، سه نقش اصلی داریم:
۱. مدیر سیستم
مدیر یا Admin میتواند فروشندگان را بررسی و تأیید کند، محصولات را مدیریت کند، سفارشها را نظارت کند و تنظیمات کلی بازارگاه را تغییر دهد.
۲. فروشنده
فروشنده میتواند محصولات خود را ثبت کند، قیمت و موجودی را تغییر دهد، سفارشهای مربوط به محصولات خودش را مشاهده کند و وضعیت آمادهسازی سفارش را بهروزرسانی کند.
۳. مشتری
مشتری میتواند محصولات فروشندگان مختلف را مشاهده کند، به سبد خرید اضافه کند و سفارش ثبت کند.
در پروژههای بزرگتر ممکن است نقشهای دیگری مانند پشتیبان، مدیر مالی، مدیر محتوا یا اپراتور انبار نیز اضافه شوند.
نکته مهم این است که نقش کاربر با مالکیت داده یکسان نیست. برای مثال، داشتن نقش فروشنده نباید به این معنا باشد که کاربر اجازه ویرایش محصولات تمام فروشندگان را دارد. باید مالکیت هر محصول نیز بررسی شود.
مرحله دوم: طراحی ساختار دیتابیس
ساختار دیتابیس یکی از مهمترین بخشهای طراحی Marketplace است. اگر از ابتدا ارتباط بین کاربران، فروشندگان، محصولات و سفارشها بهدرستی طراحی نشود، اضافه کردن قابلیتهایی مانند کمیسیون، تسویهحساب و گزارش مالی دشوار خواهد شد.
برای نسخه اولیه، جدولهای زیر را در نظر میگیریم:
users: کاربران سیستمsellers: اطلاعات اختصاصی فروشندگانproducts: محصولاتorders: سفارش اصلی مشتریorder_items: ردیفهای سفارشseller_payouts: در صورت نیاز، سوابق تسویهحساب فروشندگان
در این طراحی، هر فروشنده به یک حساب کاربری متصل است. در نسخههای پیشرفتهتر میتوان امکان داشتن چند کاربر مدیریتی برای یک فروشگاه را نیز اضافه کرد.
ساخت مدلها و Migrationها
ابتدا مدلها و فایلهای Migration را ایجاد میکنیم:
php artisan make:model Seller -m
php artisan make:model Product -m
php artisan make:model Order -m
php artisan make:model OrderItem -m
سپس ساختار جدولها را تعریف میکنیم.
جدول فروشندگان
در Migration مدل Seller میتوانیم فیلدهای زیر را در نظر بگیریم:
Schema::create('sellers', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->unique()
->constrained()
->cascadeOnDelete();
$table->string('store_name');
$table->string('slug')->unique();
$table->string('status')->default('pending');
$table->timestamps();
});
در این ساختار، هر فروشنده به یک کاربر متصل است. مقدار status میتواند وضعیت فروشنده را مشخص کند؛ برای مثال:
pending: در انتظار بررسیapproved: تأیید شدهrejected: رد شدهsuspended: تعلیق شده
در پروژه واقعی بهتر است وضعیتها با Enum یا یک سازوکار مشخص و قابل اعتبارسنجی مدیریت شوند تا مقدارهای نامعتبر در دیتابیس ثبت نشوند.
جدول محصولات
هر محصول باید به فروشندهای که مالک آن است متصل باشد:
Schema::create('products', function (Blueprint $table) {
$table->id();
$table->foreignId('seller_id')
->constrained()
->restrictOnDelete();
$table->string('name');
$table->string('slug')->unique();
$table->text('description')->nullable();
$table->unsignedBigInteger('price');
$table->unsignedInteger('stock')->default(0);
$table->string('status')->default('pending');
$table->timestamps();
$table->index(['seller_id', 'status']);
});
در این مثال، قیمت با یک عدد صحیح ذخیره شده است. واحد پول باید در کل برنامه مشخص و یکسان باشد؛ برای مثال، اگر قیمت را به ریال ذخیره میکنید، تمام محاسبات و نمایشها باید با همین تصمیم هماهنگ باشند.
از ذخیره قیمت بهصورت float برای محاسبات مالی خودداری کنید؛ زیرا نوعهای اعشاری شناور میتوانند خطاهای دقت ایجاد کنند.
همچنین وضعیت محصول میتواند مشخص کند محصول در انتظار تأیید است یا اجازه نمایش در فروشگاه را دارد.
جدول سفارشها
سفارش اصلی اطلاعات کلی خرید مشتری را نگهداری میکند:
Schema::create('orders', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->restrictOnDelete();
$table->unsignedBigInteger('total_amount');
$table->string('status')->default('pending');
$table->timestamps();
});
جدول ردیفهای سفارش
در یک Marketplace ممکن است مشتری از چند فروشنده محصول بخرد. به همین دلیل نباید مالکیت کل سفارش را فقط به یک فروشنده نسبت دهیم.
برای هر ردیف سفارش، فروشنده مربوط به همان محصول را ذخیره میکنیم:
Schema::create('order_items', function (Blueprint $table) {
$table->id();
$table->foreignId('order_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('seller_id')
->constrained()
->restrictOnDelete();
$table->foreignId('product_id')
->constrained()
->restrictOnDelete();
$table->unsignedInteger('quantity');
$table->unsignedBigInteger('unit_price');
$table->unsignedBigInteger('commission_amount')->default(0);
$table->timestamps();
$table->index(['seller_id', 'order_id']);
});
فیلد unit_price قیمت محصول در زمان خرید را نگهداری میکند. این موضوع اهمیت زیادی دارد؛ زیرا ممکن است قیمت محصول در آینده تغییر کند، اما سفارش قبلی باید قیمت زمان خرید را حفظ کند.
همین منطق برای کمیسیون نیز کاربرد دارد. اگر نرخ کمیسیون در آینده تغییر کند، بهتر است مبلغ محاسبهشده در زمان سفارش محفوظ بماند.
در پروژه واقعی باید برای مجموع مبلغ سفارش، مبلغ هر ردیف، تخفیف، هزینه ارسال و کمیسیون نیز قواعد دقیق حسابداری تعریف شود.
مرحله سوم: تعریف ارتباط بین مدلها
اکنون باید ارتباطهای Eloquent را مشخص کنیم تا بتوانیم اطلاعات فروشندگان، محصولات و سفارشها را بهسادگی بازیابی کنیم.
مدل Seller
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Seller extends Model
{
protected $fillable = [
'user_id',
'store_name',
'slug',
'status',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function products(): HasMany
{
return $this->hasMany(Product::class);
}
public function orderItems(): HasMany
{
return $this->hasMany(OrderItem::class);
}
}
مدل Product
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class Product extends Model
{
protected $fillable = [
'seller_id',
'name',
'slug',
'description',
'price',
'stock',
'status',
];
public function seller(): BelongsTo
{
return $this->belongsTo(Seller::class);
}
}
مدل Order
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Order extends Model
{
protected $fillable = [
'user_id',
'total_amount',
'status',
];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function items(): HasMany
{
return $this->hasMany(OrderItem::class);
}
}
مدل OrderItem
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class OrderItem extends Model
{
protected $fillable = [
'order_id',
'seller_id',
'product_id',
'quantity',
'unit_price',
'commission_amount',
];
public function order(): BelongsTo
{
return $this->belongsTo(Order::class);
}
public function seller(): BelongsTo
{
return $this->belongsTo(Seller::class);
}
public function product(): BelongsTo
{
return $this->belongsTo(Product::class);
}
}
برای ارتباط User و Seller نیز میتوانید در مدل User متد زیر را اضافه کنید:
public function seller()
{
return $this->hasOne(\App\Models\Seller::class);
}
اکنون میتوانیم از طریق مدلها به ارتباطهای مرتبط دسترسی داشته باشیم.
برای آشنایی بیشتر با ارتباطهای Eloquent، میتوانید به مستندات رسمی Laravel مراجعه کنید:
https://laravel.com/docs/12.x/eloquent-relationships
مرحله چهارم: مدیریت ثبتنام فروشندگان
در Marketplace نباید هر کاربری بلافاصله پس از ثبتنام بتواند محصول منتشر کند. معمولاً ابتدا کاربر درخواست ایجاد فروشگاه میدهد و سپس مدیر یا یک فرایند خودکار، اطلاعات او را بررسی میکند.
فرایند ساده میتواند به این صورت باشد:
- کاربر حساب کاربری ایجاد میکند.
- فرم ایجاد فروشگاه را تکمیل میکند.
- رکورد فروشنده با وضعیت
pendingثبت میشود. - مدیر اطلاعات را بررسی میکند.
- پس از تأیید، فروشنده اجازه ثبت و انتشار محصول را دریافت میکند.
برای نمونه، در یک کنترلر میتوانیم فروشنده را ایجاد کنیم:
use Illuminate\Http\Request;
use Illuminate\Support\Str;
use App\Models\Seller;
public function store(Request $request)
{
$data = $request->validate([
'store_name' => ['required', 'string', 'max:150'],
]);
$seller = Seller::create([
'user_id' => $request->user()->id,
'store_name' => $data['store_name'],
'slug' => Str::slug($data['store_name'])
. '-' . Str::random(6),
'status' => 'pending',
]);
return response()->json([
'message' => 'درخواست ایجاد فروشگاه ثبت شد.',
'seller_id' => $seller->id,
], 201);
}
این مثال یک Endpoint ساده را نشان میدهد. در برنامه واقعی باید احراز هویت فعال باشد و از ایجاد چند فروشگاه ناخواسته برای یک کاربر جلوگیری شود. همچنین باید مشخص شود که کاربران عادی، فروشندگان قبلی و مدیران چه مجوزهایی دارند.
مرحله پنجم: کنترل دسترسی فروشنده به محصولات
یکی از حساسترین بخشهای Marketplace کنترل دسترسی است.
فرض کنید فروشنده شماره ۱۰ وارد پنل شده و آدرس زیر را باز میکند:
/seller/products/25/edit
اگر محصول شماره ۲۵ متعلق به فروشنده دیگری باشد، برنامه نباید اجازه ویرایش آن را بدهد؛ حتی اگر کاربر بتواند شناسه محصول را حدس بزند.
یک روش ساده این است که محصول را از ارتباط فروشنده فعلی بازیابی کنیم:
$seller = $request->user()->seller;
abort_unless(
$seller && $seller->status === 'approved',
403
);
$product = $seller->products()->findOrFail($productId);
در این روش، محصول فقط از بین محصولات فروشنده فعلی جستوجو میشود. اگر محصول به فروشنده دیگری تعلق داشته باشد، پیدا نخواهد شد.
برای پروژههای بزرگتر، بهتر است قوانین دسترسی را در Laravel Policy پیادهسازی کنید تا منطق مجوزها در کنترلرهای مختلف تکرار نشود.
همچنین باید در نظر بگیرید که مدیر سیستم ممکن است مجوزهای متفاوتی داشته باشد. این تفاوت را میتوان با Policy و نقشهای مشخص مدیریت کرد.
نکته امنیتی: مخفی کردن دکمه ویرایش در رابط کاربری کافی نیست. بررسی مالکیت باید در سمت سرور و در هر درخواست حساس انجام شود.
مرحله ششم: ثبت محصول توسط فروشنده
پس از تأیید فروشنده، میتوانیم امکان ثبت محصول را فعال کنیم.
نمونهای از منطق ثبت محصول:
use Illuminate\Http\Request;
public function store(Request $request)
{
$seller = $request->user()->seller;
abort_unless(
$seller && $seller->status === 'approved',
403
);
$data = $request->validate([
'name' => ['required', 'string', 'max:200'],
'description' => ['nullable', 'string'],
'price' => ['required', 'integer', 'min:1'],
'stock' => ['required', 'integer', 'min:0'],
]);
$product = $seller->products()->create([
...$data,
'slug' => \Illuminate\Support\Str::slug($data['name'])
. '-' . \Illuminate\Support\Str::random(6),
'status' => 'pending',
]);
return response()->json([
'message' => 'محصول ثبت شد و در انتظار بررسی است.',
'product_id' => $product->id,
], 201);
}
در این نمونه، محصول با وضعیت pending ذخیره میشود؛ بنابراین تا زمان تأیید مدیر در فهرست عمومی محصولات نمایش داده نخواهد شد.
این تصمیم برای Marketplaceهایی مفید است که کیفیت محصولات، قوانین انتشار یا دستهبندیها باید پیش از انتشار بررسی شوند.
در بخشهای بعدی پروژه باید آپلود تصویر محصول، اعتبارسنجی فایل، دستهبندیها، ویژگیهای محصول و مدیریت موجودی نیز پیادهسازی شوند.
مرحله هفتم: نمایش محصولات تأییدشده
در صفحه عمومی Marketplace، نباید همه محصولات دیتابیس را نمایش دهیم. فقط محصولاتی باید در دسترس مشتریان باشند که وضعیت انتشار آنها مجاز است و فروشنده نیز شرایط فعالیت را دارد.
برای نمونه:
use App\Models\Product;
$products = Product::query()
->where('status', 'approved')
->whereHas('seller', function ($query) {
$query->where('status', 'approved');
})
->with('seller:id,store_name,slug')
->latest()
->paginate(20);
در این کوئری، فقط محصولات تأییدشده فروشندگان تأییدشده دریافت میشوند. همچنین اطلاعات موردنیاز فروشنده با with() بارگذاری میشود تا از اجرای Query جداگانه برای هر محصول جلوگیری شود.
در پروژه واقعی، بهتر است فیلترهای جستوجو، دستهبندی، محدوده قیمت و مرتبسازی نیز بهصورت کنترلشده به Query اضافه شوند.
استفاده از Pagination نیز کمک میکند تعداد محدودی محصول در هر صفحه بارگذاری شود.
مرحله هشتم: طراحی فرایند ثبت سفارش
ثبت سفارش یکی از مهمترین بخشهای Marketplace است؛ زیرا باید قیمت، موجودی، مالکیت محصول و اطلاعات مالی در لحظه خرید بهدرستی بررسی شوند.
یک فرایند پایه شامل مراحل زیر است:
- دریافت محصولات و تعداد درخواستی مشتری
- بررسی موجود بودن محصولات
- محاسبه مبلغ از روی قیمتهای معتبر دیتابیس
- ثبت سفارش و ردیفهای آن
- کاهش موجودی با رعایت همزمانی
- آغاز فرایند پرداخت
- ثبت نتیجه پرداخت و بهروزرسانی وضعیت سفارش
هرگز قیمت نهایی را مستقیماً از مرورگر مشتری قبول نکنید. مشتری میتواند درخواست HTTP را تغییر دهد؛ بنابراین سرور باید قیمت محصولات را از دیتابیس بخواند و مبلغ را دوباره محاسبه کند.
استفاده از تراکنش دیتابیس
برای جلوگیری از ثبت ناقص سفارش، میتوانیم از تراکنش استفاده کنیم. در مثال زیر، منطق پایه ثبت سفارش نشان داده شده است:
use App\Models\Order;
use App\Models\Product;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;
$order = DB::transaction(function () use ($request) {
$items = $request->validate([
'items' => ['required', 'array', 'min:1'],
'items.*.product_id' => ['required', 'integer'],
'items.*.quantity' => ['required', 'integer', 'min:1'],
])['items'];
$order = Order::create([
'user_id' => $request->user()->id,
'total_amount' => 0,
'status' => 'pending',
]);
$total = 0;
foreach ($items as $item) {
$product = Product::query()
->whereKey($item['product_id'])
->where('status', 'approved')
->lockForUpdate()
->firstOrFail();
if (
$product->seller->status !== 'approved' ||
$product->stock < $item['quantity']
) {
throw ValidationException::withMessages([
'items' => 'محصول ناموجود است یا امکان خرید آن وجود ندارد.',
]);
}
$quantity = $item['quantity'];
$unitPrice = $product->price;
$order->items()->create([
'seller_id' => $product->seller_id,
'product_id' => $product->id,
'quantity' => $quantity,
'unit_price' => $unitPrice,
'commission_amount' => 0,
]);
$product->decrement('stock', $quantity);
$total += $unitPrice * $quantity;
}
$order->update([
'total_amount' => $total,
]);
return $order;
});
این کد برای توضیح ایده اصلی است و پیش از استفاده در Production باید کاملتر شود. برای نمونه، بهتر است ارتباط فروشنده محصول نیز از قبل بارگذاری شود یا وضعیت فروشنده با Query مناسب بررسی شود. همچنین باید سقف تعداد، محدودیتهای مبلغ، موجودی، تخفیف و سیاستهای لغو سفارش تعریف شوند.
اگر چند درخواست همزمان برای خرید آخرین واحد یک محصول ارسال شوند، کنترل همزمانی اهمیت ویژهای پیدا میکند. استفاده از تراکنش و قفل ردیف میتواند به جلوگیری از فروش بیش از موجودی کمک کند؛ البته رفتار دقیق به دیتابیس و ساختار تراکنش وابسته است.
همچنین در این مثال موجودی هنگام ایجاد سفارش کاهش مییابد. در یک سیستم واقعی باید مشخص شود موجودی در چه مرحلهای رزرو یا قطعی میشود و در صورت پرداخت ناموفق یا انقضای سفارش چگونه آزاد خواهد شد.
برای سفارشهای چندفروشندگی، بهتر است در مراحل بعدی مدل جداگانهای برای سفارش هر فروشنده طراحی شود تا ارسال، لغو و تسویه هر فروشنده مستقل مدیریت شود.
مرحله نهم: محاسبه کمیسیون Marketplace
یکی از تفاوتهای مهم Marketplace با فروشگاه معمولی، محاسبه درآمد پلتفرم و سهم فروشنده است.
برای مثال، فرض کنید مبلغ یک محصول ۱,۰۰۰,۰۰۰ واحد پولی و نرخ کمیسیون پلتفرم ۱۰ درصد باشد.
در این صورت:
- مبلغ فروش: ۱,۰۰۰,۰۰۰
- کمیسیون پلتفرم: ۱۰۰,۰۰۰
- سهم اولیه فروشنده: ۹۰۰,۰۰۰
این مثال فرض میکند تخفیف، مالیات، هزینه ارسال و سایر هزینهها وجود ندارند. در یک سیستم واقعی، مبنای محاسبه کمیسیون باید بهطور دقیق تعریف شود.
محاسبه کمیسیون در Laravel
میتوانیم نرخ کمیسیون را در تنظیمات یا جدول اختصاصی نگهداری کنیم. برای نمونه:
$commissionRate = 10;
$lineTotal = $unitPrice * $quantity;
$commissionAmount = intdiv(
$lineTotal * $commissionRate,
100
);
در این مثال، کمیسیون از مبلغ ناخالص ردیف محاسبه شده است. اگر سیاست تجاری شما کمیسیون را پس از کسر تخفیف محاسبه میکند، فرمول باید متناسب با آن تغییر کند.
در پروژههای مالی، نرخ کمیسیون، روش گرد کردن، واحد پول و نحوه برخورد با بازپرداخت باید از ابتدا مشخص باشند. همچنین بهتر است مبلغ کمیسیون محاسبهشده در زمان خرید ذخیره شود تا تغییر نرخ آینده روی سفارشهای قدیمی اثر نگذارد.
برای حسابداری دقیقتر، بهتر است بهجای محاسبه درآمد از روی وضعیت فعلی سفارشها، یک دفتر ثبت مالی یا Ledger طراحی کنید که رویدادهای مالی مانند فروش، کمیسیون، بازپرداخت و تسویه را ثبت کند.
مرحله دهم: مدیریت سفارشهای چندفروشندگی
فرض کنید مشتری سه محصول از دو فروشنده مختلف خریداری کرده است. در ظاهر، مشتری یک سفارش ثبت میکند؛ اما از دید عملیاتی، هر فروشنده باید فقط سفارشهای مربوط به محصولات خودش را مدیریت کند.
برای این منظور میتوان دو سطح از سفارشها را در نظر گرفت:
- سفارش اصلی: نمای کلی خرید مشتری
- سفارش فروشنده: بخشی از خرید که باید توسط یک فروشنده خاص آماده و ارسال شود
در نسخه ساده، ارتباط seller_id در جدول order_items مشخص میکند هر ردیف متعلق به کدام فروشنده است.
در نسخه پیشرفتهتر، میتوان جدول مستقلی مانند seller_orders ایجاد کرد که اطلاعات ارسال، وضعیت پردازش، هزینه ارسال و مبلغ تسویه هر فروشنده را نگهداری کند.
این جداسازی به شما اجازه میدهد یک سفارش اصلی شامل چند سفارش فروشنده باشد، بدون اینکه وضعیت همه فروشندگان به یکدیگر وابسته شود.
برای مثال، اگر فروشنده اول سفارش را ارسال کرده باشد اما فروشنده دوم هنوز در حال آمادهسازی باشد، سیستم میتواند وضعیت هر بخش را جداگانه نشان دهد.
مرحله یازدهم: طراحی پنل فروشنده و پنل مدیریت
پس از آماده شدن هسته دیتابیس و منطق اصلی، باید رابطهای مدیریتی سیستم را طراحی کنیم.
پنل فروشنده
امکانات اولیه پنل فروشنده میتواند شامل موارد زیر باشد:
- مشاهده آمار فروش
- ثبت و ویرایش محصولات
- مدیریت موجودی
- مشاهده سفارشهای مربوط به فروشگاه
- تغییر وضعیت آمادهسازی سفارش
- مشاهده کمیسیونها
- مشاهده سوابق تسویهحساب
- مدیریت اطلاعات فروشگاه
هر صفحه باید فقط دادههایی را نمایش دهد که فروشنده مجاز به مشاهده آنها است. برای مثال، گزارش سفارشها باید بر اساس فروشنده احراز هویتشده فیلتر شود.
پنل مدیریت
مدیر سیستم به امکانات گستردهتری نیاز دارد:
- بررسی و تأیید فروشندگان
- مدیریت محصولات و دستهبندیها
- بررسی سفارشها و پرداختها
- تنظیم نرخ کمیسیون
- گزارش فروش و درآمد
- مدیریت بازپرداختها
- مدیریت شکایتها و درخواستهای پشتیبانی
- ثبت سوابق اقدامات مدیریتی
برای پروژههای بزرگ، میتوان از ابزارهایی مانند Filament برای ساخت پنل مدیریت Laravel استفاده کرد. با این حال، مجوزهای امنیتی باید در منطق سمت سرور نیز رعایت شوند و نباید صرفاً به مخفی کردن گزینهها در پنل تکیه کرد.
مرحله دوازدهم: استفاده از Queue برای عملیات زمانبر
در Marketplace، بعضی عملیات نباید پاسخ درخواست کاربر را طولانی کنند. برای مثال:
- ارسال ایمیل تأیید سفارش
- ارسال پیامک تغییر وضعیت
- تولید فاکتور PDF
- اطلاعرسانی به فروشنده
- همگامسازی موجودی با سرویس خارجی
- ساخت گزارشهای فروش
این عملیات را میتوان به Job تبدیل کرد و در Queue قرار داد.
برای نمونه، بعد از ثبت موفق سفارش میتوانید Job ارسال اعلان را Dispatch کنید:
SendOrderNotification::dispatch($order->id)
->afterCommit();
استفاده از afterCommit() کمک میکند Job تا بعد از Commit شدن تراکنش فعال به صف فرستاده نشود.
این موضوع اهمیت دارد؛ زیرا Worker ممکن است خیلی سریع اجرا شود و اگر تراکنش هنوز Commit نشده باشد، اطلاعات موردنیاز را پیدا نکند.
در عملیات مالی یا سفارشهای حساس، باید رفتار تلاش مجدد و پردازش تکراری نیز طراحی شود. ارسال دوباره یک اعلان ممکن است قابلقبول باشد، اما ثبت دوباره یک تراکنش مالی میتواند مشکل جدی ایجاد کند.
مرحله سیزدهم: امنیت Marketplace
امنیت در Marketplace اهمیت زیادی دارد؛ زیرا چندین فروشنده و مشتری به یک دیتابیس مشترک دسترسی دارند.
مهمترین مواردی که باید رعایت شوند عبارتاند از:
کنترل مالکیت داده
هر فروشنده فقط باید بتواند محصولات، سفارشها و اطلاعات مالی مربوط به خودش را مشاهده یا ویرایش کند.
اعتبارسنجی سمت سرور
تمام دادههای دریافتی از کاربر باید اعتبارسنجی شوند. قیمت، مبلغ کمیسیون، شناسه فروشنده و وضعیت پرداخت نباید صرفاً از ورودی مرورگر پذیرفته شوند.
کنترل وضعیت فروشنده
فروشنده تعلیقشده نباید بتواند محصولات جدید منتشر کند یا عملیات ممنوعشده را انجام دهد. بررسی وضعیت باید در مسیرهای حساس سمت سرور انجام شود.
حفاظت از فایلها
اگر فروشندگان میتوانند تصویر یا فایل بارگذاری کنند، باید نوع فایل، حجم، دسترسی و نام ذخیرهسازی کنترل شود. فایلهای خصوصی نباید صرفاً با یک URL عمومی قابلدسترسی باشند.
امنیت پرداخت
وضعیت پرداخت باید با تأیید معتبر سمت سرور و مطابق مستندات درگاه پرداخت بهروزرسانی شود. بازگشت کاربر به صفحه موفقیت بهتنهایی اثبات نمیکند که پرداخت انجام شده است.
ثبت سوابق
برای عملیات حساس مانند تغییر کمیسیون، تعلیق فروشنده، بازپرداخت و تغییر وضعیت مالی، بهتر است سابقه اقدام، کاربر انجامدهنده و زمان عملیات ثبت شود.
برای پیادهسازی مجوزها و محافظت از مسیرها میتوانید از امکانات رسمی Laravel استفاده کنید:
- احراز هویت: https://laravel.com/docs/12.x/authentication
- مجوزها و Policyها: https://laravel.com/docs/12.x/authorization
مرحله چهاردهم: تستنویسی برای Marketplace
به دلیل تعدد نقشها و عملیات مالی، تستنویسی در Marketplace ضروری است.
حداقل سناریوهای زیر باید بررسی شوند:
- فروشنده تأییدنشده نتواند محصول منتشر کند.
- فروشنده نتواند محصول فروشنده دیگر را ویرایش کند.
- مشتری نتواند قیمت محصول را با تغییر درخواست HTTP دستکاری کند.
- سفارش با موجودی ناکافی ثبت نشود.
- مبلغ سفارش با مجموع ردیفهای معتبر هماهنگ باشد.
- کمیسیون طبق سیاست تعریفشده محاسبه شود.
- پرداخت ناموفق باعث ثبت سفارش بهعنوان پرداختشده نشود.
- بازپرداخت باعث ثبت مجدد یا دوبارهکاری مالی نشود.
- Jobهای اعلان پس از ثبت موفق سفارش به صف ارسال شوند.
برای سناریوهای مالی، علاوه بر تستهای معمول، باید وضعیتهای همزمانی و تلاش مجدد نیز بررسی شوند. تست کردن صرفاً مسیر موفق کافی نیست؛ بسیاری از خطاهای مهم در زمان لغو سفارش، قطع سرویس خارجی یا اجرای مجدد عملیات رخ میدهند.
مرحله پانزدهم: توسعهپذیر کردن معماری
هنگامی که تعداد فروشندگان و سفارشها افزایش پیدا میکند، ممکن است به امکانات بیشتری نیاز داشته باشید؛ از جمله:
- جستوجوی پیشرفته محصولات
- دستهبندی و ویژگیهای قابلتنظیم
- تخفیف و کدهای کوپن
- چند روش ارسال
- تسویهحساب دورهای فروشندگان
- گزارشهای مالی
- سیستم امتیازدهی و نظرات
- سیستم تیکت و پشتیبانی
- API برای اپلیکیشن موبایل
- Cache و Queue برای کاهش بار سیستم
بهتر است از ابتدا منطق محاسبه قیمت، کمیسیون، پرداخت و موجودی را در بخشهای مشخصی از برنامه قرار دهید تا وابستگی آنها به کنترلرها و رابط کاربری کاهش پیدا کند.
برای مثال، میتوانید محاسبه مبلغ سفارش را در یک سرویس اختصاصی قرار دهید و از همان منطق در ثبت سفارش، تستها و گزارشها استفاده کنید.
همچنین اگر تعداد Queryها افزایش پیدا کرد، باید به Indexها، Pagination، بارگذاری ارتباطها با with() و جلوگیری از مشکل N+1 توجه کنید.
جمعبندی
ساخت Marketplace با Laravel فقط به ایجاد جدول محصولات و اضافه کردن یک فیلد فروشنده محدود نمیشود. یک بازارگاه چندفروشندگی باید مالکیت دادهها، نقشها، سفارشهای چندفروشندهای، کمیسیون، موجودی، پرداخت و تسویهحساب را بهصورت منظم مدیریت کند.
در این آموزش، هسته اولیه معماری را طراحی کردیم: مدلهای فروشنده و محصول، ارتباطهای Eloquent، کنترل مالکیت، ثبت سفارش، محاسبه اولیه کمیسیون و استفاده از Queue برای عملیات پسزمینه.
برای تبدیل این ساختار به یک محصول تجاری، باید فرایند پرداخت، بازپرداخت، دفتر ثبت مالی، تسویهحساب فروشندگان، ارسال سفارشها، گزارشگیری و تستهای امنیتی را با جزئیات بیشتری پیادهسازی کنید.
پیشنهاد عملی: اگر قصد ساخت یک Marketplace واقعی را دارید، ابتدا چرخه «ثبتنام فروشنده، تأیید فروشگاه، ثبت محصول و ثبت سفارش» را بهصورت کامل و تستشده پیادهسازی کنید. سپس سراغ کمیسیون، پرداخت و تسویهحساب بروید؛ زیرا این بخشها به مدل داده و قوانین تجاری دقیقتری نیاز دارند.
سؤالات متداول
آیا میتوان با Laravel یک Marketplace چندفروشندگی ساخت؟
بله. امکانات Laravel برای مدیریت کاربران، ارتباطهای دیتابیس، احراز هویت، مجوزها، صفها و تستنویسی، زیرساخت مناسبی برای ساخت یک Marketplace اختصاصی فراهم میکنند.
تفاوت Marketplace با فروشگاه اینترنتی چیست؟
در فروشگاه معمولی، محصولات غالباً متعلق به یک کسبوکار هستند؛ اما در Marketplace چند فروشنده مستقل از طریق یک پلتفرم محصولات خود را عرضه میکنند.
چگونه دسترسی فروشندگان را از یکدیگر جدا کنیم؟
باید مالکیت دادهها در سمت سرور بررسی شود. برای نمونه، فروشنده باید محصولات خود را از طریق ارتباط seller بازیابی کند و برای عملیات حساس از Policy یا کنترل دسترسی مناسب استفاده شود.
کمیسیون فروشندگان در Laravel چگونه محاسبه میشود؟
ابتدا باید سیاست کمیسیون مشخص شود؛ سپس مبلغ بر اساس قیمت معتبر سفارش و قواعد مالی تعریفشده محاسبه و در زمان خرید ذخیره شود. در سیستمهای مالی حرفهای، ثبت رویدادهای مالی و بازپرداختها نیز باید در نظر گرفته شوند.
آیا برای ساخت Marketplace حتماً به Redis نیاز داریم؟
خیر. Redis برای بسیاری از صفها و سناریوهای پرترافیک مفید است، اما انتخاب آن به نیاز پروژه بستگی دارد. میتوان هسته اولیه را با امکانات پایه Laravel ساخت و پس از اندازهگیری عملکرد، زیرساخت را توسعه داد.
منابع رسمی
- مستندات Laravel 12: https://laravel.com/docs/12.x
- Eloquent ORM: https://laravel.com/docs/12.x/eloquent
- ارتباطهای Eloquent: https://laravel.com/docs/12.x/eloquent-relationships
- احراز هویت: https://laravel.com/docs/12.x/authentication
- مجوزها و Policyها: https://laravel.com/docs/12.x/authorization
- تراکنشهای دیتابیس: https://laravel.com/docs/12.x/database
- Queueها: https://laravel.com/docs/12.x/queues





