iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

1
Modern Web

Laravel從入門到放棄…………原生PHP (疑?系列 第 45

[Day 44] Laravel實作 - 商品購買與交易記錄(三)

昨天忙了一整天忘記發文了,
不過今天開始還是繼續發文吧.

今天繼續購買商品的主題.

資料交易處理

在電子商務網站中,為了避免同時間的交易發生,導致商品發生超賣,讓交易資料對不起來的情況,所以當交易進行中,會將商品資料做鎖定的狀態,禁止其他消費者存取交易中的資料,直到交易結束,才將資料鎖定解除,解除後其他消費者才可以繼續存取此商品的資料。

很容易發生的狀況是,當商品只剩1個時,但有A跟B兩個使用者同時要購買此商品,當A取得此商品資料的優先存取權限時,會將此商品的資料鎖定不讓B存取,所以B的交易畫面就會一直出現載入中的情況,這個載入中的狀況是在等商品資料被解除鎖定所造成的延遲,當A成功購買了此商品後,會將商品剩餘數量設為0,並將此商品資料做解除鎖定的動作,但在交易過程中,會發現已經沒有可以購買的商品了,所以整個交易會丟出例外,造成購買失敗的狀況,並將資料的異動恢復為交易前的狀態。

當要開始鎖定資料庫資料時,Laravel提供DB::beginTransaction(); 方法,讓我們可以在對之後存取的資料進行鎖定,像是商品、點數餘額之類的資料,在所有的交易資料異動都成功後,像是商品有足夠的數量可供購買,使用者的餘額有正確的扣款,我們會使用DB::commit(); 告訴資料庫在之前的異動都是正確的可以生效,等生效後就可以解除資料的鎖定了,但當發生錯誤例外時,像是商品可購買數量不足,使用者餘額不足扣款失敗時,會使用DB::rollBack(); 恢復交易之前的資料狀態,並解除鎖定。

整個需要確保資料一致性的程式邏輯會長的像下面這樣

app/Http/Controllers/MerchandiseController.php

<?PHP
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Shop\Entity\Merchandise;
use App\Shop\Entity\Transaction;
use App\Shop\Entity\User;
use Illuminate\Support\Facades\Log;
use DB;
use Exception;
use Validator; //驗證器
use Image;

class MerchandiseController extends Controller
{
    //商品購買處理
    public function merchandiseItemBuyProcess($merchandise_id)
    {
        //接收輸入資料
        $input = request()->all();

        //...中間省略...

        try
        {
            //交易開始
            DB::beginTransaction();

            //...處理交易...
            
            //交易結束
            DB::commit();
        }
        catch(Exception $exception)
        {
            //恢復原先交易狀態
            DB::rollBack();

            //...處理交易錯誤...
        }
    }
}
?>

交易資料鎖定

當開始處理購買資料時,會在交易開始做DB::beginTransaction(); 取得此次交易需要的資料,當有做資料取得的動作,資料庫就會將此筆資料做鎖定,禁止其他的請求去存取這些資料,程式邏輯會像下面這樣

app/Http/Controllers/MerchandiseController.php

<?PHP
namespace App\Http\Controllers;

use App\Http\Controllers\Controller;
use App\Shop\Entity\Merchandise;
use App\Shop\Entity\Transaction;
use App\Shop\Entity\User;
use Illuminate\Support\Facades\Log;
use DB;
use Exception;
use Validator; //驗證器
use Image;

class MerchandiseController extends Controller
{
    //商品購買處理
    public function merchandiseItemBuyProcess($merchandise_id)
    {
        //接收輸入資料
        $input = request()->all();

        //...中間省略...

        try
        {
            //取得登入會員資料
            $user_id = session()->get('user_id');
            $User = User::findOrFail($user_id);

            //交易開始
            DB::beginTransaction();

            //取得商品資料
            $Merchandise = Merchandise::findOrFail($merchandise_id);

            //購買數量
            $buy_count = $input['buy_count'];
            //購買後剩餘數量
            $remain_count_after_buy = $Merchandise->remain_count - $buy_count;
            if($remain_count_after_buy < 0)
            {
                //購買後剩餘數量小於0,不足以賣給使用者
                throw new Exception('商品數量不足,無法購買');
            }
            //記錄購買後剩餘數量
            $Merchandise->remain_count = $remain_count_after_buy;
            $Merchandise->save();

            //總金額:總購買數量 * 商品價格
            $total_price = $buy_count * $Merchandise->price;

            $transaction_data = [
                'user_id' => $User->id,
                'merchandise_id' => $Merchandise->id,
                'price' => $Merchandise->price,
                'buy_count' => $buy_count,
                'total_price' => $total_price,
            ];

            //建立交易資料
            Transaction::create($transaction_data);
            //交易結束
            DB::commit();

            //回傳購物成功訊息
            $message = [
                'msg' => [
                    '購買成功',
                ],
            ];

            return redirect()
                ->to('/merchandise/'.$Merchandise->id)
                ->withErrors($message);
        }
        catch(Exception $exception)
        {
            //恢復原先交易狀態
            DB::rollBack();
            
            //回傳錯誤訊息
            $error_message = [
                'msg' => [
                    $exception->getMessage(),
                ],
            ];

            return redirect()
                ->back()
                ->withErrors($error_message)
                ->withInput();
        }
    }
}
?>

因為這次的交易進行中,我們要確保商品資料不會被其他請求做異動,所以在交易開始DB::beginTransaction(); 後,我們會取得商品的資料,將商品做鎖定的動作,這樣在交易結束DB::commit(); 之前,此筆商品資料都不會被其他的請求做任何的異動,只有此次的交易可以做商品資料異動。

try
{
    //交易開始
    DB::beginTransaction();

    //...處理交易...
    
    //交易結束
    DB::commit();
}
catch(Exception $exception)
{
    //恢復原先交易狀態
    DB::rollBack();

    //...處理交易錯誤...
}

購買完成畫面
http://localhost:6943/merchandise/1
https://ithelp.ithome.com.tw/upload/images/20191015/20105694pVBGPheiDU.png


上一篇
[Day 43] Laravel實作 - 商品購買與交易記錄(二)
下一篇
[Day 45] Laravel實作 - 商品購買與交易記錄(四)
系列文
Laravel從入門到放棄…………原生PHP (疑?48

尚未有邦友留言

立即登入留言