تقریباً هر نرمافزار تحت وب که به مشتریان خدمات ارائه میدهد، به راهی برای دریافت و پیگیری درخواستهای پشتیبانی نیاز دارد. کاربران ممکن است درباره سفارش، پرداخت، مشکلات فنی یا نحوه استفاده از خدمات سؤال داشته باشند. اگر این درخواستها فقط از طریق تماس تلفنی یا پیامرسان مدیریت شوند، پیگیری و اولویتبندی آنها دشوار خواهد شد.
سیستم تیکت یکی از راهکارهای متداول برای مدیریت این فرایند است. با استفاده از Laravel میتوان سامانهای طراحی کرد که کاربران در آن درخواست پشتیبانی ثبت کنند، کارشناسان پاسخ دهند و وضعیت هر درخواست تا زمان حل مشکل قابل پیگیری باشد.
در این مقاله، مراحل طراحی دیتابیس، ساخت مدلها، ثبت تیکت، ارسال پاسخ، مدیریت وضعیت و کنترل دسترسی را بررسی میکنیم.
۱. سیستم تیکت چیست و چه قابلیتهایی دارد؟
سیستم تیکتینگ نرمافزاری است که درخواستهای پشتیبانی را بهصورت ساختاریافته ثبت و مدیریت میکند. هر درخواست یک شناسه مستقل دارد و میتواند شامل عنوان، توضیحات، اولویت، وضعیت و مجموعهای از پیامها باشد.
قابلیتهای متداول سیستم تیکت عبارتاند از:
- ثبت درخواست پشتیبانی توسط کاربر
- مشاهده فهرست تیکتهای ثبتشده
- ارسال پاسخ جدید به تیکت
- تعیین وضعیت و اولویت درخواست
- اختصاص تیکت به کارشناس
- ثبت تاریخچه پاسخها
- اطلاعرسانی هنگام دریافت پاسخ جدید
- محدودکردن دسترسی به تیکتهای خصوصی
- جستوجو و فیلترکردن درخواستها
در نسخه اولیه میتوان با سه بخش اصلی شروع کرد: تیکتها، پیامهای تیکت و کاربران. در مراحل بعدی، قابلیتهایی مانند فایل پیوست، دپارتمان، SLA، گزارشگیری و امتیازدهی به پاسخ پشتیبانی نیز قابل اضافهشدن هستند.
۲. طراحی دیتابیس سیستم تیکت
برای پیادهسازی اولیه، دو جدول اصلی ایجاد میکنیم:
| جدول | کاربرد |
|---|---|
tickets | نگهداری اطلاعات اصلی هر درخواست |
ticket_messages | نگهداری پیامهای کاربر و کارشناسان |
هر تیکت متعلق به یک کاربر است و میتواند چند پیام داشته باشد. هر پیام نیز به تیکت و نویسنده آن مرتبط میشود.
ایجاد مدلها و مایگریشنها
در ترمینال پروژه اجرا کنید:
php artisan make:model Ticket -m
php artisan make:model TicketMessage -m
مایگریشن جدول تیکتها
در فایل مایگریشن tickets کد زیر را قرار دهید:
Schema::create('tickets', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')
->constrained()
->restrictOnDelete();
$table->foreignId('assigned_to')
->nullable()
->constrained('users')
->nullOnDelete();
$table->string('subject', 200);
$table->string('department', 50)->default('general');
$table->string('priority', 20)->default('normal');
$table->string('status', 30)->default('open');
$table->timestamp('last_message_at')->nullable();
$table->timestamps();
$table->index(['status', 'priority']);
$table->index(['user_id', 'created_at']);
});
فیلدهای اصلی این جدول عبارتاند از:
user_id: کاربری که تیکت را ثبت کرده است.assigned_to: کارشناس مسئول تیکت؛ این فیلد اختیاری است.subject: عنوان درخواست.department: بخش پشتیبانی مربوط به درخواست.priority: اولویت رسیدگی.status: وضعیت فعلی تیکت.last_message_at: زمان آخرین پیام برای مرتبسازی درخواستها.
مقادیر status و priority در این مثال بهصورت رشته ذخیره میشوند. در پروژه واقعی بهتر است مقادیر مجاز آنها را در یک محل مشخص، مانند PHP Enum یا کلاس ثابت، تعریف کنید تا مقادیر نامعتبر وارد دیتابیس نشوند.
مایگریشن جدول پیامها
اکنون جدول پیامهای تیکت را ایجاد میکنیم:
Schema::create('ticket_messages', function (Blueprint $table) {
$table->id();
$table->foreignId('ticket_id')
->constrained()
->cascadeOnDelete();
$table->foreignId('user_id')
->constrained()
->restrictOnDelete();
$table->text('message');
$table->timestamps();
$table->index(['ticket_id', 'created_at']);
});
در این ساختار، پیامها در جدولی جداگانه ذخیره میشوند. این کار باعث میشود هر تیکت بتواند تعداد زیادی پیام داشته باشد، بدون آنکه ساختار جدول اصلی پیچیده شود.
در پروژههای عملیاتی، معمولاً بهتر است حذف فیزیکی تیکتهای دارای سابقه پشتیبانی محدود شود. اگر به نگهداری سوابق نیاز دارید، میتوانید بهجای حذف اطلاعات از سیاست بایگانی یا Soft Deletes استفاده کنید.
پس از تکمیل مایگریشنها، اجرا کنید:
php artisan migrate
۳. تعریف روابط مدلها
در مدل Ticket رابطههای مربوط به مالک، کارشناس و پیامها را تعریف میکنیم:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
class Ticket extends Model
{
protected $fillable = [
'user_id',
'assigned_to',
'subject',
'department',
'priority',
'status',
'last_message_at',
];
protected function casts(): array
{
return [
'last_message_at' => 'datetime',
];
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function assignee(): BelongsTo
{
return $this->belongsTo(
User::class,
'assigned_to'
);
}
public function messages(): HasMany
{
return $this->hasMany(TicketMessage::class);
}
}
سپس مدل TicketMessage را تکمیل میکنیم:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class TicketMessage extends Model
{
protected $fillable = [
'ticket_id',
'user_id',
'message',
];
public function ticket(): BelongsTo
{
return $this->belongsTo(Ticket::class);
}
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
}
اکنون میتوانیم با استفاده از روابط Eloquent، پیامهای هر تیکت و اطلاعات نویسنده هر پیام را دریافت کنیم.
۴. ایجاد مسیرهای سیستم تیکت
برای شروع، چند مسیر اصلی تعریف میکنیم:
use App\Http\Controllers\TicketController;
use App\Http\Controllers\TicketMessageController;
use Illuminate\Support\Facades\Route;
Route::middleware('auth')->group(function () {
Route::get(
'/tickets',
[TicketController::class, 'index']
)->name('tickets.index');
Route::get(
'/tickets/create',
[TicketController::class, 'create']
)->name('tickets.create');
Route::post(
'/tickets',
[TicketController::class, 'store']
)->name('tickets.store');
Route::get(
'/tickets/{ticket}',
[TicketController::class, 'show']
)->name('tickets.show');
Route::post(
'/tickets/{ticket}/messages',
[TicketMessageController::class, 'store']
)->name('tickets.messages.store');
});
این مسیرها برای کاربران واردشده در نظر گرفته شدهاند. بااینحال، قرارگرفتن مسیرها پشت Middleware احراز هویت بهتنهایی کافی نیست؛ در هر عملیات باید مالکیت تیکت و مجازبودن کاربر نیز بررسی شود.
برای نمونه، کاربر نباید بتواند با تغییر شناسه در URL، تیکت خصوصی شخص دیگری را مشاهده کند.
۵. ساخت فرم ثبت تیکت
فرم ثبت تیکت میتواند شامل عنوان، بخش پشتیبانی، اولویت و توضیحات اولیه باشد.
یک فرم ساده Blade به شکل زیر است:
<form method="POST" action="{{ route('tickets.store') }}">
@csrf
<div>
<label for="subject">عنوان تیکت</label>
<input
id="subject"
name="subject"
value="{{ old('subject') }}"
required
maxlength="200"
>
@error('subject')
<p>{{ $message }}</p>
@enderror
</div>
<div>
<label for="department">بخش پشتیبانی</label>
<select id="department" name="department">
<option value="general">عمومی</option>
<option value="technical">فنی</option>
<option value="billing">مالی</option>
</select>
</div>
<div>
<label for="priority">اولویت</label>
<select id="priority" name="priority">
<option value="low">کم</option>
<option value="normal" selected>عادی</option>
<option value="high">زیاد</option>
</select>
</div>
<div>
<label for="message">شرح درخواست</label>
<textarea
id="message"
name="message"
required
maxlength="10000"
>{{ old('message') }}</textarea>
@error('message')
<p>{{ $message }}</p>
@enderror
</div>
<button type="submit">ثبت تیکت</button>
</form>
در این فرم از @csrf استفاده شده است تا درخواست با محافظت CSRF لاراول ارسال شود.
گزینههای فرم نیز باید در سمت سرور اعتبارسنجی شوند؛ زیرا کاربر میتواند درخواست HTTP را بدون استفاده از رابط کاربری ارسال کند.
۶. ذخیره تیکت و پیام اولیه
اکنون کنترلر ثبت تیکت را میسازیم:
php artisan make:controller TicketController
در متد store، دادهها را اعتبارسنجی میکنیم و تیکت و پیام اولیه را در یک تراکنش دیتابیس ثبت میکنیم:
<?php
namespace App\Http\Controllers;
use App\Models\Ticket;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TicketController extends Controller
{
public function store(Request $request)
{
$data = $request->validate([
'subject' => ['required', 'string', 'max:200'],
'department' => [
'required',
'in:general,technical,billing',
],
'priority' => [
'required',
'in:low,normal,high',
],
'message' => ['required', 'string', 'max:10000'],
]);
$ticket = DB::transaction(function () use (
$request,
$data
) {
$ticket = Ticket::create([
'user_id' => $request->user()->id,
'subject' => $data['subject'],
'department' => $data['department'],
'priority' => $data['priority'],
'status' => 'open',
'last_message_at' => now(),
]);
$ticket->messages()->create([
'user_id' => $request->user()->id,
'message' => $data['message'],
]);
return $ticket;
});
return redirect()
->route('tickets.show', $ticket)
->with('success', 'تیکت شما با موفقیت ثبت شد.');
}
}
نکته مهم این است که user_id از کاربر احراز هویتشده دریافت میشود، نه از داده ارسالی فرم. به این ترتیب، کاربر نمیتواند هنگام ثبت تیکت، مالک آن را به شخص دیگری تغییر دهد.
استفاده از تراکنش دیتابیس نیز باعث میشود اگر ذخیره پیام اولیه شکست بخورد، تیکت ناقص بدون پیام اولیه باقی نماند.
۷. نمایش فهرست تیکتهای کاربر
کاربر باید بتواند درخواستهای ثبتشده خودش را مشاهده کند.
در کنترلر، متد index را اضافه کنید:
public function index(Request $request)
{
$tickets = Ticket::query()
->where('user_id', $request->user()->id)
->latest()
->paginate(15);
return view('tickets.index', compact('tickets'));
}
استفاده از شرط where('user_id', ...) باعث میشود فهرست فقط شامل تیکتهای متعلق به همان کاربر باشد.
در صورت نیاز میتوانید فیلترهایی برای وضعیت، بخش پشتیبانی و تاریخ ایجاد نیز اضافه کنید. ورودی فیلترها باید اعتبارسنجی شوند و مقادیر نامعتبر نباید مستقیماً وارد Query شوند.
۸. نمایش جزئیات تیکت و پیامها
در صفحه جزئیات تیکت باید عنوان، وضعیت، اولویت و مکالمه میان کاربر و کارشناسان نمایش داده شود.
نمونه متد show:
public function show(Request $request, Ticket $ticket)
{
abort_unless(
$ticket->user_id === $request->user()->id,
403
);
$ticket->load([
'messages.user',
'assignee',
]);
return view('tickets.show', compact('ticket'));
}
این نمونه برای بخش کاربری است. اگر پنل پشتیبانی دارید، باید مجوز دسترسی کارشناسان و مدیران را نیز بهصورت جداگانه تعریف کنید.
برای نمایش پیامها در Blade:
@foreach ($ticket->messages as $message)
<article>
<strong>{{ $message->user->name }}</strong>
<p>{{ $message->message }}</p>
<small>
{{ $message->created_at->format('Y/m/d H:i') }}
</small>
</article>
@endforeach
در Blade، نمایش عادی {{ ... }} محتوای متنی را Escape میکند. اگر اجازه ارسال HTML به کاربران را میدهید، باید آن را با یک روش معتبر پاکسازی کنید؛ از نمایش مستقیم HTML کنترلنشده خودداری کنید.
۹. ارسال پاسخ جدید به تیکت
کاربر یا کارشناس مجاز باید بتواند به تیکت پاسخ دهد. ابتدا کنترلر پیام را بسازید:
php artisan make:controller TicketMessageController
نمونه متد ذخیره پیام:
<?php
namespace App\Http\Controllers;
use App\Models\Ticket;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\DB;
class TicketMessageController extends Controller
{
public function store(Request $request, Ticket $ticket)
{
abort_unless(
$ticket->user_id === $request->user()->id,
403
);
$data = $request->validate([
'message' => [
'required',
'string',
'max:10000',
],
]);
DB::transaction(function () use (
$request,
$ticket,
$data
) {
$ticket->messages()->create([
'user_id' => $request->user()->id,
'message' => $data['message'],
]);
$ticket->last_message_at = now();
if ($ticket->status === 'answered') {
$ticket->status = 'open';
}
$ticket->save();
});
return redirect()
->route('tickets.show', $ticket)
->with('success', 'پاسخ شما ثبت شد.');
}
}
این مثال مخصوص پاسخ کاربر به تیکت خودش است. در یک سیستم واقعی، پاسخ کارشناس باید از مسیر مجزا و با مجوز مناسب انجام شود.
همچنین منطق تغییر وضعیت باید متناسب با سیاست پشتیبانی طراحی شود. برای مثال، ممکن است ارسال پاسخ کاربر، تیکت را دوباره باز کند؛ اما ارسال پاسخ کارشناس، وضعیت را به answered تغییر دهد.
۱۰. مدیریت وضعیت تیکت
در یک سیستم تیکت معمولاً چند وضعیت اصلی وجود دارد:
| وضعیت | مفهوم |
|---|---|
open | درخواست جدید یا باز |
in_progress | در حال بررسی |
answered | کارشناس پاسخ داده است |
waiting_for_user | منتظر پاسخ کاربر |
closed | درخواست بسته شده است |
این وضعیتها نمونه هستند و باید متناسب با گردش کار کسبوکار انتخاب شوند.
بهتر است تغییر وضعیت فقط از طریق یک متد یا سرویس مشخص انجام شود. برای نمونه، کاربر نباید بتواند با ارسال status=closed در فرم پاسخ، وضعیت تیکت را بدون بررسی تغییر دهد.
در سیستمهای بزرگتر، میتوان از Policy و سرویس مدیریت گردش کار استفاده کرد تا مشخص شود چه کسی، در چه وضعیتی و تحت چه شرایطی مجاز به تغییر تیکت است.
۱۱. اختصاص تیکت به کارشناس
در پنل مدیریت، میتوان امکان اختصاص تیکت به کارشناس را فراهم کرد. فیلد assigned_to در جدول تیکتها برای همین منظور استفاده میشود.
بااینحال، هنگام اختصاص تیکت باید بررسی کنید که کاربر انتخابشده واقعاً کارشناس یا عضو تیم پشتیبانی است. معتبر بودن شناسه یک کاربر برای انتساب او به تیکت کافی نیست.
اگر از پکیجهایی مانند spatie/laravel-permission استفاده میکنید، میتوانید مجوزهای مربوط به نقش کارشناس و مدیر پشتیبانی را تعریف کنید. کنترل دسترسی باید در سمت سرور اجرا شود، نه صرفاً با مخفیکردن دکمههای پنل.
۱۲. ارسال اعلان برای پاسخ جدید
یکی از قابلیتهای مفید سیستم تیکت، اطلاعرسانی هنگام ثبت پاسخ جدید است. Laravel سیستم Notification دارد و میتواند اعلانها را از طریق کانالهایی مانند ایمیل و دیتابیس ارسال کند.
برای ایجاد Notification:
php artisan make:notification TicketReplyNotification
در متد مربوط به ارسال پاسخ، میتوانید پس از ثبت موفق پیام، اعلان را برای مخاطب مناسب ارسال کنید.
برای نمونه:
$ticket->user->notify(
new TicketReplyNotification($ticket)
);
این مثال زمانی مناسب است که پاسخ جدید باید به کاربر صاحب تیکت اطلاع داده شود. هنگام پاسخ خود کاربر، معمولاً باید کارشناس مسئول یا اعضای تیم پشتیبانی را مطلع کنید.
در پروژههای پرترافیک، بهتر است اعلانها از صف پردازش شوند تا پاسخ HTTP به تأخیر نیفتد. همچنین باید از ارسال اعلان تکراری در اثر پردازش دوباره یک رویداد جلوگیری کنید.
۱۳. کنترل دسترسی و امنیت سیستم تیکت
تیکتها ممکن است شامل اطلاعات سفارش، مشخصات حساب یا جزئیات خصوصی کاربران باشند. بنابراین امنیت این بخش اهمیت زیادی دارد.
کنترل مالکیت تیکت
کاربر باید فقط به تیکتهای خودش دسترسی داشته باشد. این کنترل باید در نمایش، پاسخدادن، دانلود فایلها و سایر عملیات نیز اعمال شود.
مجوزهای کارشناسان
کارشناس پشتیبانی باید فقط به تیکتهایی دسترسی داشته باشد که بر اساس سیاست سیستم مجاز به مشاهده یا مدیریت آنهاست.
اعتبارسنجی پیامها
طول پیام، فرمت دادهها و محدودیتهای ارسال باید سمت سرور کنترل شوند. در صورت پشتیبانی از فایل پیوست، نوع واقعی فایل، حجم و روش ذخیرهسازی نیز باید بررسی شود.
محدودیت نرخ درخواست
برای جلوگیری از ارسال انبوه تیکت یا پیام، میتوان از Rate Limiting لاراول استفاده کرد. محدودیت باید متناسب با نیاز کاربران و نوع سامانه تنظیم شود.
ثبت رویدادهای مدیریتی
تغییر وضعیت، انتساب به کارشناس و سایر عملیات حساس را میتوان در تاریخچه رویدادها ثبت کرد تا در صورت بروز اختلاف، قابل پیگیری باشند.
۱۴. توسعه سیستم تیکت در پروژههای واقعی
پس از پیادهسازی قابلیتهای اصلی، میتوان امکانات دیگری نیز اضافه کرد:
- فایل پیوست با ذخیرهسازی امن
- دستهبندی و دپارتمانهای پشتیبانی
- جستوجو بر اساس شناسه و عنوان
- فیلتر بر اساس وضعیت و اولویت
- زمان پاسخگویی و SLA
- گزارش تعداد تیکتهای باز و بسته
- امتیازدهی کاربران به پاسخ پشتیبانی
- اعلان ایمیلی یا پیامکی
- API برای اپلیکیشن موبایل
- ارتباط تیکت با سفارش یا تراکنش مالی
برای فایلهای پیوست، از نام اصلی فایل بهعنوان مسیر ذخیرهسازی قابل اعتماد استفاده نکنید. فایل را در فضای ذخیرهسازی مناسب قرار دهید، دسترسی را کنترل کنید و لینک دانلود را فقط پس از بررسی مجوز کاربر ارائه دهید.
۱۵. تست سیستم تیکت در Laravel
پیش از انتشار، سناریوهای زیر را آزمایش کنید:
- [ ] ثبت تیکت توسط کاربر واردشده
- [ ] رد عنوان یا پیام خالی
- [ ] نمایش فقط تیکتهای متعلق به کاربر
- [ ] جلوگیری از مشاهده تیکت کاربر دیگر
- [ ] ثبت پیام و بهروزرسانی زمان آخرین پاسخ
- [ ] جلوگیری از تغییر غیرمجاز وضعیت
- [ ] کنترل مجوز کارشناس
- [ ] ارسال اعلان برای مخاطب درست
- [ ] جلوگیری از ارسال پیام به تیکت بسته، در صورت ممنوعبودن آن
- [ ] مدیریت فایلهای پیوست و دسترسی به آنها
برای این سناریوها میتوانید از Feature Testهای Laravel استفاده کنید و رفتار سیستم را با کاربران دارای نقشهای متفاوت بررسی کنید.
سوالات متداول
چگونه در Laravel سیستم تیکت بسازیم؟
برای شروع، دو جدول تیکتها و پیامها ایجاد کنید، روابط Eloquent را تعریف کنید و قابلیتهای ثبت درخواست، نمایش مکالمه و ارسال پاسخ را پیادهسازی کنید. سپس کنترل دسترسی و اعلانها را اضافه کنید.
چرا پیامهای تیکت در جدول جداگانه ذخیره میشوند؟
چون هر تیکت میتواند تعداد زیادی پیام داشته باشد. جدول جداگانه، ذخیره و نمایش مکالمه را سادهتر میکند و امکان افزودن اطلاعاتی مانند نویسنده و زمان هر پیام را فراهم میسازد.
چگونه جلوی دسترسی کاربران به تیکت دیگران را بگیریم؟
باید مالکیت تیکت را در سمت سرور بررسی کنید و برای عملیات مدیریتی از Policy یا مجوزهای مشخص استفاده کنید. احراز هویت بهتنهایی برای کنترل دسترسی کافی نیست.
چگونه پس از پاسخ جدید اعلان ارسال کنیم؟
میتوانید از Notification لاراول استفاده کنید و در صورت نیاز، ارسال اعلان را به Queue بسپارید. بهتر است اعلان فقط پس از ثبت موفق پیام ایجاد شود.
آیا میتوان سیستم تیکت را به پنل مدیریت متصل کرد؟
بله. میتوانید بخش کارشناسان را برای مشاهده، دستهبندی، اولویتبندی و پاسخگویی به تیکتها بسازید و با استفاده از نقشها و مجوزها دسترسی هر گروه را کنترل کنید.
جمعبندی
ساخت سیستم تیکت با Laravel فرصتی مناسب برای استفاده عملی از قابلیتهایی مانند Eloquent، Migration، Validation، Authentication، کنترل دسترسی و Notification است.
با جداکردن تیکتها از پیامها و پیادهسازی کنترل مالکیت و وضعیتها، میتوان یک هسته ساده و قابل توسعه ساخت. سپس با افزودن صفها، اعلانها، فایلهای پیوست و گزارشگیری، این سیستم را برای استفاده در پروژههای واقعی آماده کرد.





