سی ام اس نویسسی ام اس نویسیاد بگیر، سیستم خودتو بنویس
مجلهتماس با ما
سی ام اس نویسسی ام اس نویس

CMS نویس مرجعی برای آموزش برنامه نویسی وب و PHP، توسعه افزونه‌های اختصاصی وردپرس، طراحی ورک‌فلو و اتوماسیون با n8n و توسعه CMS های اختصاصی است. هدف ما ارائه آموزش‌های کاربردی و راهکارهای حرفه‌ای برای توسعه‌دهندگان، کسب‌وکارها و علاقه‌مندان به دنیای برنامه‌نویسی است.

پیوندهای سریع
محصولات فیزیکیمحصولات دیجیتالدوره‌های آموزشی
سی ام اس نویس
مجله سی ام اس نویستماس با ماپنل کاربری
سی ام اس نویس — تمامی حقوق برای سی ام اس نویس محفوظ است. © 1406 تماس با ما09307711941
آموزش ساخت سیستم کیف پول با Laravel؛ شارژ، برداشت و مدیریت تراکنش‌ها

آموزش ساخت سیستم کیف پول با Laravel؛ شارژ، برداشت و مدیریت تراکنش‌ها

ممدیر کل سیستم ۲۲ مهر ۱۴۰۵ ۱۷ دقیقه مطالعه ۲ بازدید
آموزش ساخت سیستم کیف پول با Laravel؛ شارژ، برداشت و مدیریت تراکنش‌ها

سیستم کیف پول یکی از قابلیت‌های کاربردی در بسیاری از پروژه‌های تحت وب است. فروشگاه‌های اینترنتی، سامانه‌های اشتراکی، مارکت‌پلیس‌ها، پلتفرم‌های خدماتی و نرم‌افزارهای SaaS می‌توانند از کیف پول برای نگهداری اعتبار کاربران، پرداخت هزینه خدمات، بازپرداخت وجه و مدیریت تراکنش‌های مالی استفاده کنند.

در این مقاله یاد می‌گیریم چگونه با Laravel یک سیستم کیف پول طراحی کنیم که قابلیت شارژ، برداشت، مشاهده تراکنش‌ها و مدیریت موجودی را داشته باشد. همچنین با روش‌هایی آشنا می‌شویم که از برداشت هم‌زمان، ثبت تراکنش تکراری و منفی‌شدن موجودی جلوگیری می‌کنند.

سیستم کیف پول چیست و چگونه کار می‌کند؟

کیف پول در یک نرم‌افزار، حسابی است که موجودی اعتباری یک کاربر را نگهداری می‌کند. کاربر می‌تواند کیف پول خود را شارژ کند و سپس از موجودی آن برای پرداخت هزینه محصولات یا خدمات استفاده کند.

برای مثال، در یک سامانه فروش خدمات، کاربر ابتدا مبلغی به کیف پول خود اضافه می‌کند. هنگام خرید سرویس، مبلغ موردنیاز از موجودی کیف پول کسر می‌شود و اطلاعات این عملیات در تاریخچه تراکنش‌ها ثبت خواهد شد.

یک سیستم کیف پول استاندارد باید این قابلیت‌ها را داشته باشد:

  • ایجاد کیف پول برای کاربران
  • شارژ موجودی
  • برداشت یا پرداخت از موجودی
  • نمایش موجودی فعلی
  • ثبت تاریخچه تراکنش‌ها
  • جلوگیری از برداشت بیشتر از موجودی
  • جلوگیری از ثبت چندباره یک تراکنش
  • امکان پیگیری و بازپرداخت تراکنش‌ها
  • کنترل دسترسی کاربران به اطلاعات مالی خود

نکته مهم این است که موجودی کیف پول و تاریخچه تراکنش‌ها دو مفهوم جداگانه هستند. موجودی برای دسترسی سریع به اعتبار فعلی نگهداری می‌شود؛ اما تاریخچه تراکنش‌ها باید نشان دهد هر تغییری در موجودی، چه زمانی و به چه دلیلی اتفاق افتاده است.

۱. طراحی دیتابیس کیف پول در Laravel

برای پیاده‌سازی اولیه، دو جدول اصلی در نظر می‌گیریم:

جدولکاربرد
walletsنگهداری موجودی فعلی کیف پول
wallet_transactionsثبت تاریخچه شارژ، برداشت و سایر تغییرات موجودی

در این طراحی، هر کاربر یک کیف پول دارد. اگر قرار است کیف پول متعلق به شرکت، فروشنده یا مستأجر یک سامانه SaaS باشد، می‌توان به‌جای ارتباط مستقیم با کاربر، ساختار را بر اساس حساب یا tenant طراحی کرد.

ساخت مدل‌ها و مایگریشن‌ها

در ترمینال پروژه Laravel دستورات زیر را اجرا کنید:


php artisan make:model Wallet -m

php artisan make:model WalletTransaction -m

ساخت جدول کیف پول‌ها

فایل مایگریشن مربوط به wallets را باز کنید و ساختار زیر را در متد up() قرار دهید:


Schema::create('wallets', function (Blueprint $table) {

    $table->id();



    $table->foreignId('user_id')

        ->unique()

        ->constrained()

        ->cascadeOnDelete();



    $table->unsignedBigInteger('balance')->default(0);



    $table->timestamps();

});

در این مثال، موجودی با عدد صحیح ذخیره می‌شود؛ بنابراین از خطاهای محاسباتی مربوط به اعداد اعشاری جلوگیری می‌کنیم.

واحد پول باید در کل سیستم مشخص و یکسان باشد. برای مثال، اگر موجودی بر اساس ریال ذخیره می‌شود، تمام مقادیر ورودی و خروجی باید مطابق همین واحد باشند. اگر رابط کاربری مبلغ را به تومان دریافت می‌کند، تبدیل تومان به ریال باید در یک نقطه مشخص و با کنترل سرریز عددی انجام شود.

همچنین استفاده از unsignedBigInteger به معنی مجازبودن اعداد منفی نیست؛ کنترل موجودی و جلوگیری از برداشت غیرمجاز همچنان باید در منطق برنامه انجام شود.

ساخت جدول تراکنش‌ها

فایل مایگریشن wallet_transactions را به شکل زیر تکمیل کنید:


Schema::create('wallet_transactions', function (Blueprint $table) {

    $table->id();



    $table->foreignId('wallet_id')

        ->constrained()

        ->restrictOnDelete();



    $table->string('type', 20);

    $table->unsignedBigInteger('amount');



    $table->unsignedBigInteger('balance_before');

    $table->unsignedBigInteger('balance_after');



    $table->string('reference')->nullable();

    $table->string('idempotency_key')->nullable()->unique();



    $table->string('description')->nullable();

    $table->json('metadata')->nullable();



    $table->timestamps();



    $table->index(['wallet_id', 'created_at']);

});

در این ساختار:

  • type نوع عملیات را مشخص می‌کند؛ مانند credit برای افزایش موجودی و debit برای کاهش آن.
  • amount مبلغ تراکنش است و باید همیشه مثبت باشد.
  • balance_before موجودی پیش از انجام عملیات است.
  • balance_after موجودی پس از انجام عملیات است.
  • reference برای نگهداری شناسه مرتبط با سفارش یا عملیات خارجی استفاده می‌شود.
  • idempotency_key شناسه یکتایی است که به جلوگیری از اجرای تکراری یک درخواست کمک می‌کند.
  • metadata می‌تواند اطلاعات تکمیلی مرتبط با تراکنش را نگهداری کند.

مبالغ را در این جدول به‌صورت مثبت ذخیره می‌کنیم و نوع تراکنش تعیین می‌کند که موجودی افزایش یافته یا کاهش پیدا کرده است.

> نکته مالی مهم: تراکنش‌های ثبت‌شده نباید برای اصلاح موجودی قبلی حذف یا بازنویسی شوند. برای اصلاح یک عملیات مالی، تراکنش جبرانی یا بازپرداخت جدید ثبت کنید تا سابقه مالی قابل پیگیری باقی بماند.

پس از تکمیل مایگریشن‌ها، اجرا کنید:


php artisan migrate

۲. تعریف ارتباط مدل‌ها

در مدل Wallet ارتباط کیف پول با کاربر و تراکنش‌های آن را تعریف می‌کنیم:


<?php



namespace App\Models;



use Illuminate\Database\Eloquent\Model;

use Illuminate\Database\Eloquent\Relations\BelongsTo;

use Illuminate\Database\Eloquent\Relations\HasMany;



class Wallet extends Model

{

    protected $fillable = [

        'user_id',

        'balance',

    ];



    protected function casts(): array

    {

        return [

            'balance' => 'integer',

        ];

    }



    public function user(): BelongsTo

    {

        return $this->belongsTo(User::class);

    }



    public function transactions(): HasMany

    {

        return $this->hasMany(WalletTransaction::class);

    }

}

سپس مدل WalletTransaction را تعریف کنید:


<?php



namespace App\Models;



use Illuminate\Database\Eloquent\Model;

use Illuminate\Database\Eloquent\Relations\BelongsTo;



class WalletTransaction extends Model

{

    protected $fillable = [

        'wallet_id',

        'type',

        'amount',

        'balance_before',

        'balance_after',

        'reference',

        'idempotency_key',

        'description',

        'metadata',

    ];



    protected function casts(): array

    {

        return [

            'amount' => 'integer',

            'balance_before' => 'integer',

            'balance_after' => 'integer',

            'metadata' => 'array',

        ];

    }



    public function wallet(): BelongsTo

    {

        return $this->belongsTo(Wallet::class);

    }

}

در مدل User نیز رابطه کیف پول را اضافه کنید:


use Illuminate\Database\Eloquent\Relations\HasOne;



public function wallet(): HasOne

{

    return $this->hasOne(Wallet::class);

}

اکنون می‌توانیم با استفاده از رابطه‌هایی مانند $user->wallet به کیف پول کاربر و با $wallet->transactions به تراکنش‌های آن دسترسی داشته باشیم.

۳. ایجاد کیف پول برای کاربر

می‌توان هنگام ثبت‌نام کاربر، کیف پول او را ایجاد کرد. برای این کار، در بخش مناسب از فرایند ثبت‌نام یا سرویس ساخت کاربر از کد زیر استفاده کنید:


$user->wallet()->firstOrCreate(

    [],

    ['balance' => 0]

);

استفاده از قید unique روی user_id در دیتابیس مهم است؛ زیرا در شرایطی که دو درخواست هم‌زمان برای ایجاد کیف پول ارسال شود، دیتابیس اجازه ثبت دو کیف پول برای یک کاربر را نمی‌دهد.

اگر ایجاد کیف پول در یک عملیات حساس انجام می‌شود، خطای ناشی از رقابت هم‌زمان درخواست‌ها را نیز مدیریت کنید یا از یک فرایند ساخت اتمیک استفاده کنید.

۴. ساخت سرویس مدیریت کیف پول

بهتر است منطق مالی را داخل کنترلرها پراکنده نکنیم. یک سرویس مستقل ایجاد می‌کنیم تا شارژ و برداشت از یک منطق مشترک و قابل آزمایش استفاده کنند.

ابتدا فایل سرویس را ایجاد کنید:


app/Services/WalletService.php

کد زیر را داخل آن قرار دهید:


<?php



namespace App\Services;



use App\Models\Wallet;

use App\Models\WalletTransaction;

use Illuminate\Support\Facades\DB;

use Illuminate\Validation\ValidationException;



class WalletService

{

    public function credit(

        int $walletId,

        int $amount,

        string $idempotencyKey,

        ?string $description = null

    ): WalletTransaction {

        return $this->applyTransaction(

            $walletId,

            $amount,

            'credit',

            $idempotencyKey,

            $description

        );

    }



    public function debit(

        int $walletId,

        int $amount,

        string $idempotencyKey,

        ?string $description = null

    ): WalletTransaction {

        return $this->applyTransaction(

            $walletId,

            $amount,

            'debit',

            $idempotencyKey,

            $description

        );

    }



    private function applyTransaction(

        int $walletId,

        int $amount,

        string $type,

        string $idempotencyKey,

        ?string $description

    ): WalletTransaction {

        if ($amount <= 0) {

            throw ValidationException::withMessages([

                'amount' => 'مبلغ تراکنش باید بیشتر از صفر باشد.',

            ]);

        }



        if (! in_array($type, ['credit', 'debit'], true)) {

            throw new \InvalidArgumentException(

                'نوع تراکنش معتبر نیست.'

            );

        }



        if ($idempotencyKey === '') {

            throw new \InvalidArgumentException(

                'شناسه یکتای تراکنش الزامی است.'

            );

        }



        return DB::transaction(function () use (

            $walletId,

            $amount,

            $type,

            $idempotencyKey,

            $description

        ) {

            $wallet = Wallet::query()

                ->whereKey($walletId)

                ->lockForUpdate()

                ->firstOrFail();



            $existing = WalletTransaction::query()

                ->where('idempotency_key', $idempotencyKey)

                ->first();



            if ($existing) {

                if (

                    $existing->wallet_id !== $wallet->id ||

                    $existing->amount !== $amount ||

                    $existing->type !== $type

                ) {

                    throw ValidationException::withMessages([

                        'transaction' =>

                            'شناسه تراکنش قبلاً با اطلاعات دیگری استفاده شده است.',

                    ]);

                }



                return $existing;

            }



            $before = (int) $wallet->balance;



            if ($type === 'debit' && $before < $amount) {

                throw ValidationException::withMessages([

                    'amount' => 'موجودی کیف پول کافی نیست.',

                ]);

            }



            $after = $type === 'credit'

                ? $before + $amount

                : $before - $amount;



            $wallet->balance = $after;

            $wallet->save();



            return $wallet->transactions()->create([

                'type' => $type,

                'amount' => $amount,

                'balance_before' => $before,

                'balance_after' => $after,

                'idempotency_key' => $idempotencyKey,

                'description' => $description,

            ]);

        });

    }

}

این نمونه، هسته اولیه کیف پول را پیاده‌سازی می‌کند. در نسخه عملیاتی باید محدودیت‌های مبلغ، سقف موجودی، نوع واحد پول و سیاست‌های کسب‌وکار نیز کنترل شوند.

چرا از تراکنش دیتابیس استفاده کردیم؟

فرض کنید موجودی کاربر یک میلیون ریال است و دو درخواست هم‌زمان برای برداشت هفتصد هزار ریال ارسال می‌شود.

بدون کنترل هم‌زمانی، ممکن است هر دو درخواست موجودی اولیه را بخوانند و هر دو برداشت را موفق بدانند. نتیجه می‌تواند نادرست باشد.

در کد بالا از دو قابلیت استفاده کردیم:

  • DB::transaction() باعث می‌شود تغییر موجودی و ثبت تاریخچه تراکنش با هم انجام شوند؛ در صورت شکست عملیات، تغییرات نیز بازگردانده می‌شوند.
  • lockForUpdate() ردیف کیف پول را هنگام اجرای تراکنش قفل می‌کند تا عملیات مالی هم‌زمان روی همان کیف پول، موجودی را به‌صورت ناسازگار تغییر ندهند.

این قفل باید داخل تراکنش دیتابیس استفاده شود. رفتار و پشتیبانی دقیق آن به موتور دیتابیس نیز وابسته است؛ برای نمونه، در MySQL باید از موتور تراکنشی مانند InnoDB استفاده شود.

شناسه یکتای تراکنش چه فایده‌ای دارد؟

گاهی یک درخواست به دلیل قطع ارتباط یا خطای شبکه دوباره ارسال می‌شود. در چنین شرایطی، اگر سیستم بدون بررسی دوباره عملیات را انجام دهد، ممکن است مبلغ دو بار کسر یا به کیف پول اضافه شود.

با استفاده از idempotency_key می‌توان یک درخواست را چند بار دریافت کرد، اما نتیجه همان عملیات قبلی را بازگرداند؛ مشروط بر اینکه کلید با همان اطلاعات مالی قبلی استفاده شده باشد.

این کلید باید برای هر عملیات منطقی یکتا و پایدار باشد. در عملیات حساس مانند شارژ کیف پول، بهتر است از شناسه سفارش یا تراکنش داخلی که قبلاً در دیتابیس ثبت شده است استفاده کنید؛ نه اینکه در هر تلاش مجدد یک شناسه جدید بسازید.

همچنین قید یکتای دیتابیس آخرین لایه دفاع در برابر ثبت تکراری است. در سامانه پرترافیک، باید برخورد احتمالی با قید یکتا را مدیریت کنید و در صورت لزوم تراکنش را مجدداً بررسی کنید.

۵. شارژ کیف پول کاربر

شارژ کیف پول می‌تواند از طریق پنل مدیریت، کد تخفیف، انتقال اعتبار یا درگاه پرداخت انجام شود. اما روش شارژ در امنیت عملیات تأثیر زیادی دارد.

برای شارژ دستی یا داخلی، می‌توان سرویس را به شکل زیر فراخوانی کرد:


$transaction = app(\App\Services\WalletService::class)

    ->credit(

        walletId: $wallet->id,

        amount: 500000,

        idempotencyKey: 'manual-credit-ORDER-1001',

        description: 'شارژ کیف پول'

    );

در این مثال، مبلغ ۵۰۰٬۰۰۰ واحد پولی به موجودی اضافه می‌شود. واحد مبلغ باید همان واحد تعریف‌شده برای کیف پول باشد.

این کد به‌تنهایی جایگزین تأیید پرداخت بانکی نیست. برای شارژ از طریق درگاه پرداخت، ابتدا باید درخواست پرداخت و سفارش داخلی را ایجاد کنید. سپس پس از بازگشت کاربر، پرداخت را از سمت سرور و با API رسمی درگاه تأیید کنید. فقط بعد از تأیید معتبر و موفقیت‌آمیز باید عملیات شارژ انجام شود.

جریان پیشنهادی پرداخت به این شکل است:

  1. ثبت سفارش شارژ با وضعیت pending.
  2. انتقال کاربر به درگاه پرداخت.
  3. دریافت callback از درگاه.
  4. اعتبارسنجی و تأیید پرداخت با سرور درگاه.
  5. ثبت نتیجه پرداخت به‌صورت اتمیک و جلوگیری از تأیید تکراری.
  6. شارژ کیف پول با شناسه یکتای مبتنی بر سفارش.

نباید صرفاً با دریافت پارامترهایی مانند status=success از مرورگر کاربر، کیف پول را شارژ کنید.

۶. برداشت از کیف پول

برای برداشت از کیف پول یا پرداخت هزینه یک سرویس، از متد debit() استفاده می‌کنیم:


$transaction = app(\App\Services\WalletService::class)

    ->debit(

        walletId: $wallet->id,

        amount: 150000,

        idempotencyKey: 'service-payment-ORDER-2001',

        description: 'پرداخت هزینه سرویس'

    );

در این نمونه، مبلغ ۱۵۰٬۰۰۰ واحد پولی از موجودی کیف پول کم می‌شود.

اگر موجودی کافی نباشد، سرویس خطا می‌دهد و به دلیل استفاده از تراکنش دیتابیس، تغییری در موجودی ثبت نخواهد شد.

در برنامه واقعی، قبل از اجرای برداشت باید بررسی کنید که کاربر مجاز به پرداخت این سفارش است و سفارش نیز هنوز پرداخت نشده است. بررسی مالکیت سفارش و وضعیت آن باید سمت سرور انجام شود.

۷. نمایش موجودی و تاریخچه تراکنش‌ها

برای نمایش موجودی فعلی کیف پول:


$wallet = $request->user()

    ->wallet()

    ->firstOrFail();



$balance = $wallet->balance;

برای نمایش تاریخچه تراکنش‌ها:


$transactions = $wallet->transactions()

    ->latest()

    ->paginate(20);

در کنترلر، باید مطمئن شوید که کیف پول از کاربر احراز هویت‌شده گرفته شده است؛ نه از یک شناسه دلخواه در درخواست. در غیر این صورت، کاربر ممکن است بتواند اطلاعات مالی کاربران دیگر را مشاهده کند.

در یک API، می‌توانید اطلاعات تراکنش‌ها را با یک API Resource ارائه دهید تا فقط فیلدهای مجاز مانند مبلغ، نوع تراکنش، توضیحات، تاریخ و شناسه مرجع نمایش داده شوند. اطلاعات داخلی یا حساس نباید بدون نیاز در پاسخ API قرار گیرند.

۸. بازپرداخت و اصلاح تراکنش‌ها

فرض کنید کاربر هزینه یک سفارش را از کیف پول پرداخت کرده، اما سفارش بعداً لغو شده است. در این حالت نباید تراکنش برداشت قبلی را حذف کنیم.

روش بهتر، ثبت یک تراکنش جدید با نوع credit و شناسه یکتای بازپرداخت است.

برای نمونه:


app(\App\Services\WalletService::class)->credit(

    walletId: $wallet->id,

    amount: 150000,

    idempotencyKey: 'refund-ORDER-2001',

    description: 'بازپرداخت سفارش لغوشده'

);

این کد صرفاً بخش افزایش اعتبار را نشان می‌دهد. در سیستم واقعی، قبل از اجرای بازپرداخت باید بررسی شود که سفارش واقعاً واجد شرایط بازپرداخت است و این بازپرداخت قبلاً انجام نشده است. بهتر است بازپرداخت به شناسه تراکنش اصلی مرتبط باشد و مبلغ کل بازپرداخت از مبلغ پرداخت‌شده بیشتر نشود.

۹. نکات امنیتی مهم در طراحی کیف پول

کیف پول مستقیماً با اعتبار مالی کاربر سروکار دارد؛ بنابراین چند نکته باید از ابتدای طراحی رعایت شود.

جلوگیری از تغییر موجودی از سمت کاربر

کاربر نباید بتواند با ارسال یک درخواست HTTP مقدار موجودی کیف پول را تغییر دهد. مقدار شارژ و برداشت باید از سفارش معتبر و منطق سمت سرور به دست بیاید.

جلوگیری از برداشت تکراری

برای هر عملیات مالی یک شناسه یکتا در نظر بگیرید و وضعیت سفارش یا پرداخت را نیز کنترل کنید. بررسی شناسه یکتا باید همراه با قید دیتابیس و منطق اتمیک انجام شود.

نگهداری تاریخچه قابل پیگیری

تراکنش‌های مالی را حذف یا دستکاری نکنید. برای اصلاح عملیات قبلی، تراکنش جبرانی ایجاد کنید و شناسه عملیات اصلی را در اطلاعات مرجع نگه دارید.

کنترل دسترسی

هر کاربر باید فقط به کیف پول و تراکنش‌های مجاز خود دسترسی داشته باشد. مخفی‌کردن دکمه‌ها در رابط کاربری کافی نیست؛ کنترل مالکیت باید در سمت سرور هم اجرا شود.

تعیین سیاست واحد پول

اگر سیستم با تومان و ریال کار می‌کند، تبدیل واحد باید مشخص و یکسان باشد. از مخلوط‌کردن واحدها در دیتابیس، سرویس مالی و درگاه پرداخت جلوگیری کنید.

کنترل سقف تراکنش‌ها

حداقل و حداکثر مبلغ شارژ و برداشت، محدودیت تعداد درخواست‌ها، بررسی وضعیت حساب و کنترل‌های ضدتقلب باید متناسب با نوع کسب‌وکار پیاده‌سازی شوند.

توجه به خطاهای مالی

اگر درخواست درگاه پرداخت موفق شود ولی شارژ کیف پول به دلیل خطای دیتابیس انجام نشود، باید امکان بازیابی و پردازش مجدد امن وجود داشته باشد. ذخیره وضعیت سفارش و پرداخت، ثبت گزارش مناسب و استفاده از عملیات تکرارپذیر به حل این مسئله کمک می‌کنند.

۱۰. آیا ذخیره موجودی در جدول کیف پول کافی است؟

برای یک پروژه ساده، نگهداری موجودی فعلی در جدول wallets همراه با تاریخچه تراکنش‌ها نقطه شروع مناسبی است. اما در یک سامانه مالی بزرگ‌تر، بهتر است طراحی به سمت دفتر کل مالی (Ledger) حرکت کند.

در دفتر کل، هر رویداد مالی به‌صورت ثبت‌های قابل پیگیری نگهداری می‌شود. در سامانه‌های چندحسابی، ثبت دوطرفه بدهکار و بستانکار نیز می‌تواند برای کنترل صحت عملیات استفاده شود.

برای نمونه، هنگام پرداخت هزینه یک سرویس، ممکن است لازم باشد کاهش اعتبار کیف پول کاربر و افزایش حساب درآمد یا حساب تسویه در دو ثبت مرتبط انجام شوند.

در چنین سیستم‌هایی، موجودی می‌تواند از روی ثبت‌های مالی محاسبه یا با یک مقدار تجمیعی کنترل‌شده نگهداری شود. طراحی نهایی به الزامات حسابداری، حجم تراکنش‌ها و نیازهای گزارش‌گیری بستگی دارد.

۱۱. تست سیستم کیف پول در Laravel

پس از پیاده‌سازی، فقط موفقیت یک شارژ یا برداشت را آزمایش نکنید. سناریوهای خطا و هم‌زمانی نیز باید بررسی شوند.

چک‌لیست پیشنهادی:

  • [ ] ایجاد کیف پول برای کاربر جدید
  • [ ] افزایش موجودی با شارژ موفق
  • [ ] کاهش موجودی با برداشت موفق
  • [ ] جلوگیری از برداشت بیشتر از موجودی
  • [ ] جلوگیری از اجرای دوباره یک عملیات با کلید یکسان
  • [ ] رد استفاده از کلید قبلی با مبلغ یا نوع متفاوت
  • [ ] بازگرداندن تغییرات در صورت شکست ثبت تراکنش
  • [ ] جلوگیری از دسترسی کاربر به کیف پول دیگران
  • [ ] جلوگیری از شارژ کیف پول با callback جعلی
  • [ ] بررسی رفتار دو برداشت هم‌زمان از یک کیف پول
  • [ ] جلوگیری از بازپرداخت تکراری یک سفارش

برای آزمایش این موارد می‌توانید از تست‌های Feature لاراول و دیتابیس آزمایشی استفاده کنید. تست هم‌زمانی بهتر است جداگانه و روی موتور دیتابیسی نزدیک به محیط عملیاتی انجام شود؛ زیرا رفتار قفل‌ها در همه دیتابیس‌ها یکسان نیست.

سوالات متداول

چگونه در Laravel برای هر کاربر کیف پول بسازیم؟

می‌توانید یک جدول wallets با ارتباط یک‌به‌یک با کاربران ایجاد کنید و هنگام ثبت‌نام یا اولین استفاده، کیف پول کاربر را بسازید. قید یکتای user_id از ایجاد بیش از یک کیف پول برای هر کاربر جلوگیری می‌کند.

چگونه از منفی‌شدن موجودی کیف پول جلوگیری کنیم؟

موجودی باید داخل یک تراکنش دیتابیس و پس از قفل‌کردن ردیف کیف پول بررسی شود. سپس برداشت و ثبت تراکنش در همان تراکنش دیتابیس انجام شود.

آیا می‌توان موجودی کیف پول را مستقیماً از کنترلر تغییر داد؟

بهتر است خیر. منطق مالی را در یک سرویس مستقل قرار دهید تا همه مسیرهای شارژ و برداشت از قوانین یکسانی پیروی کنند.

برای شارژ کیف پول با درگاه پرداخت چه کاری لازم است؟

ابتدا درخواست پرداخت را ثبت کنید، سپس پرداخت را از سمت سرور تأیید کنید و بعد از تأیید معتبر، کیف پول را شارژ کنید. callback به‌تنهایی مدرک کافی برای موفقیت پرداخت نیست.

آیا باید تراکنش‌های قبلی کیف پول را حذف کنیم؟

خیر. برای بازپرداخت یا اصلاح، تراکنش جدیدی ثبت کنید تا سابقه مالی قابل پیگیری بماند.

جمع‌بندی

برای ساخت یک سیستم کیف پول با Laravel، فقط به یک ستون موجودی نیاز نداریم. یک پیاده‌سازی قابل اعتماد باید ساختار دیتابیس مناسب، تاریخچه تراکنش‌ها، کنترل هم‌زمانی، شناسه یکتای عملیات، کنترل دسترسی و فرایند امن تأیید پرداخت داشته باشد.

در این مقاله، یک هسته اولیه برای شارژ و برداشت ساختیم. برای استفاده در محیط عملیاتی، باید آن را با مدیریت سفارش و پرداخت، بازپرداخت کنترل‌شده، تست‌های مالی و در صورت نیاز دفتر کل حسابداری تکمیل کنیم.

اگر در حال ساخت یک فروشگاه اینترنتی، سامانه اشتراکی یا نرم‌افزار SaaS هستید، سیستم کیف پول می‌تواند پرداخت‌های داخلی را ساده‌تر کند؛ به شرط آنکه تمام تغییرات اعتبار از مسیرهای امن و قابل پیگیری انجام شوند.

نظرات (0)

نظرات پس از تأیید مدیر نمایش داده می‌شوند

هنوز نظری ثبت نشده است؛ اولین نفر باشید!

مقالات مرتبط

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

ساخت CMS با Laravel 12؛ آموزش ساخت سیستم مدیریت محتوا از صفر

۲۴ دقیقه
ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

ساخت فروشگاه اینترنتی با Laravel 12؛ آموزش پروژه‌محور از صفر تا طراحی هسته فروشگاه

۲۲ دقیقه
ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

ساخت Marketplace با Laravel؛ آموزش طراحی فروشگاه چندفروشندگی

۲۲ دقیقه
مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

مدیریت Queueهای سنگین در Laravel؛ آموزش پردازش کارهای طولانی

۲۲ دقیقه