iT邦幫忙

2022 iThome 鐵人賽

DAY 25
0
Modern Web

跳脫MVC,Laravel + React 建立電商網站系列 第 25

Day 25 Laravel + React 實戰之路 -7電商轉換最後一哩路

  • 分享至 

  • xImage
  •  

開電商的目的是什麼?賺錢!!
怎麼賺錢? 接外包!! 賣商品!!

所以 我們的基本畫面之中 一定會有下訂單購買的地方!今天就來完成基本的結帳畫面。
就用結帳來當作 小電商專案的最後一步,首先 因為我一開始故意忘記規劃購物車,所以其實這個結帳頁面只能夠有一個商品。
但是後來我想到一個問題:如果沒有做購物車,直接點結帳進到結帳頁,變成我要把商品跟數量的資訊都放到網址,這樣才能順利get結帳頁面
但我覺得這樣不太好,所以結帳邏輯我會把它改成:在商品詳細頁點選 結帳去 的時候,發axios post給後端 新增一條訂單紀錄,然後把訂單id回傳之後再把使用者導到下單的地方。

因為要這樣做,所以我發現一開始order這張表規劃的type不太夠用,要多新增幾個狀態:未下單(預開訂單)、已下單(客戶端完成送單)
然後order_detail 好像還少一個紀錄數量的欄位XD。
看來一開始的規畫不夠完善,還是需要再多練習。

然後這裡可以在navicate改,這樣不用再跑一次migrate的應用,migration也要改,不過因為我們是一人團隊!!就不用應用了(要有信心自己改的是對的)
好處是:這樣推上git 之後 其他協作的夥伴才可以用到最新的資料表結構。

後端 先寫一個接收資料預開order的api:

api.php
我先定義了一個post的group (雖然我覺得後面不會再多了
然後把路徑的註冊寫進去,寫法跟在web.php的時候一樣。

Route::group(['prefix' => 'post'], function () { 
    Route::post('order/create' , [\App\Http\Controllers\APIController::class,'createOrder'])
    ->name('create-order'); 
});

然後在Http/Controllers 底下新建立一個APIController.php
把基本的function寫起來就好,接下來要做一個 "表單驗證的檔案"

namespace App\Http\Controllers; 
use Illuminate\Support\Facades\Auth; 
use Inertia\Inertia; 
class APIController extends Controller 
{ 
    public function __construct() 
    { 
    } 
    public function createOrder()
    { 
    } 
}

驗證

檔案路徑在:Http/Requests 底下創建一個:OrderCreateRequest.php
這個檔案,主要是用來幫我們驗證丟進這隻api的資料,有沒有符合我們設定的規則,如果有符合我們才讓程式繼續往下跑,不符合就直接擋掉。
Laravel 官方文件說明

OrderCreateRequest.php

<?php 
namespace App\Http\Requests; 
use Illuminate\Foundation\Http\FormRequest; 
class OrderCreateRequest extends FormRequest 
{ 
    public function authorize() 
    { 
        return true; 
    } 
    public function rules() 
    { 
        return [ 
            'customer_id' => 'required', 
            'product' => 'required', 
            'amount' => 'required', 
        ]; 
    } 
}

我們回到APIController,要在createOrder這個function之中使用驗證

public function createOrder(OrderCreateRequest $request) 
{

}

改成這樣即可,因為我使用phpStorm 上面的use會自動幫我帶入,如果沒有use的話記得要use 歐~

接下來我們把request dd出來:dd($requset->validated())
validated()這個方法,會回傳一個已驗證的資料給我們。

然後我們用postman這個軟體 來發送一組資料來看看:

注意:我們現在的這個網址是:http://{你的ip}/api/post/order/create
中間的api是laravel 幫我們加上去的:路由由 RouteServiceProvider 在一個組內加載,該組被分配了“api”中間件組。

https://ithelp.ithome.com.tw/upload/images/20220929/20145703K0szFc1aCQ.png

Ok,這組api是可以正常運行的,接下來就是要把接收的資料塞回資料庫

在開始塞資料之前,因為我們要處理的資料表示訂單,訂單就有它的狀態。
為了定義明確,我這裡在Http底下建立一個Constant的direct然後 再建立一個 OrderConst.php 來明確規範每個狀態,如此可以避免其他地方打錯字而造成錯誤。
OrderConst.php

<?php 
namespace App\Constant; 
class OrderConst 
{ 
    const UNCHECK = 'UNCHECK' ;//未成立訂單 
    const CHECKOUT = 'CHECKOUT';//已成立訂單 
    const PREPARE = 'PREPARE';//訂單準備中 
    const DELIVERY = 'DELIVERY';//訂單運送中 
    const COMPLETE = 'COMPLETE';//完成 
    const RETURN = 'RETURN';//退貨 
     
}

接下來 先塞資料 為了開發的方便,身分驗證等之後再補:

public function createOrder(OrderCreateRequest $request) 
{ 
    $post_data = $request->validated(); 
    $product_model = Product::with('prices')->where('id' , $post_data['product'])->first(); 
    $tmp_price = $product_model->prices->first()->price; 
    $create_order = Order::create([ 
        'number' => (string)Str::uuid(), 
        'user_id' => $post_data['customer_id'], 
        'type' => OrderConst::UNCHECK, 
        'phone_number' => '尚未成立', 
        'address' => '尚未成立', 
        'price' => (int)$tmp_price * $post_data['amount'] 
    ]); 
    OrderDetail::create([ 
        'order_id' => $create_order->id, 
        'product_id' => $post_data['product'], 
        'amount' => $post_data['amount'], 
        'price' => $tmp_price 
    ]);
    return $create_order->number;
}

ps.這裡可以依據不同的情形判斷是不是要使用 beginTransaction,不過因為預開訂單的邏輯並沒有很難,所以我就不用了

最後再發送一次postman,資料有成功新增!!

接著我們回到商品詳細頁button的地方,要來發送axios~

前端,結帳按鈕修改

function goToCheckOut(e) 
{ 
    e.preventDefault(); 
     
    axios.post(route('create-order') , { 
        "customer_id": 1, 
        "product": 1, 
        "amount": amount, 
    }).then( response => { 
        console.log(response.data); 
    }).catch( error => { 
        console.error(error); 
    }); 
}

注意:為了防止重複點擊而發送,可以加入sweetalert2來轉圈圈、也可以用點擊後加個 disabled 之類的方法,我這裡就用比較基礎的方式:

<button className="inline-flex items-center px-3 py-2 border border-transparent text-sm leading-4 font-medium rounded-md text-gray-500 bg-indigo-50 hover:text-gray-700  focus:outline-none transition ease-in-out duration-150" 
onClick={duplicateSend === 0 ? goToCheckOut : '' } > 
    結帳去 
</button>
const [duplicateSend , setDuplicateSend] = useState(0);

function goToCheckOut(e) 
{ 
    setDuplicateSend(1); 
    axios.post(route('create-order') , { 
        "customer_id": 1, 
        "product": 1, 
        "amount": amount, 
    }).then( response => { 
        console.log(response.data); 
    }).catch( error => { 
        setDuplicateSend(0); 
        console.error(error); 
    }); 
}

接著我們發送一次axios然後到資料庫看,確實有新增成功order。
接下來就要做auth的限制了~

我這邊先修改 HomeConmntroller
多丟一個資料,把user一起丟到畫面上~

public function product($product_id) 
    { 
        $product = $this->product_service->getSingleProduct('id' , $product_id)->first(); 
        $new_product = new ProductVariable($product); 
        $new_product->setProductPrice($product->prices
        ->where('user_type',$this->user_type)->first()->price); 
        return Inertia::render('ProductDetail' , [ 
            'product' => $new_product, 
            'user' => $this->user ?? null 
        ]); 
    }

其實如果前端直接用props接收資料,不用解構的話,依據 解讀breeze的code,好像也是可以拿到使用者的(不確定,要實際測試看看)

然後 Prodetail.jsx 改成以下:
如此一來,在有登錄跟沒登陸的情況下就會看到不同的按鈕,各別去做不同事情了!

export default function Home({product , user}){ 
    const [amount , setAmount] = useState(1); 
    const [duplicateSend , setDuplicateSend] = useState(0); 
    function amountChange(e) 
    { 
        if(!isNaN(Math.round(e.target.value))) 
            setAmount(Math.round(e.target.value)); 
    } 
    function goToCheckOut(e) 
    { 
        setDuplicateSend(1); 
        axios.post(route('create-order') , { 
            "customer_id": user.id, 
            "product": product.id, 
            "amount": amount, 
        }).then( response => { 
            document.location.href = route('send-order') + `/${response.data}`; 
        }).catch( error => { 
            setDuplicateSend(0); 
            console.error(error); 
        }); 
    }
    function goToLogin(e) 
    { 
        e.preventDefault(); 
        document.location.href = route('login'); 
    } 
    return( 
        <div className="flex justify-center"> 
            <div className="pt-8 flex-col max-w-6xl"> 
                <div className="max-w-6xl flex justify-center"> 
                    <div className="w-6/12"> 
                        <img src={product.product_image} alt="商品圖片" /> 
                    </div> 
                    <div className="w-6/12 flex flex-col justify-around pl-3"> 
                        <p>商品名稱:{product.product_title}</p> 
                        <p className="font-bold text-lg text-red-600">
                            價格:{product.product_price}
                        </p> 
                    </div> 
                </div> 
                <div className="max-w-6xl pt-2"> 
                    <h3>商品敘述</h3> 
                    <div> 
                        <p>{product.product_description}</p> 
                    </div> 
                </div> 
                <div className="pt-2 max-w-6xl"> 
                    <label>數量:</label> 
                    <input type="number" value={amount} onChange={ amountChange }/> 
                    <p className="text-red-600">總金額:{ product.product_price * amount}</p> 
                </div> 
                <div className="pt-2 max-w-6xl text-center"> 
                    <button className="inline-flex items-center px-3 py-2 border 
                    border-transparent text-sm leading-4 font-medium rounded-md 
                    text-gray-500 bg-indigo-50 hover:text-gray-700 focus:outline-none 
                    transition ease-in-out duration-150" 
                            onClick={
                            duplicateSend === 0 && user !== null ? goToCheckOut : goToLogin 
                            } 
                    > 
                        { 
                            `${user !== null ? '去結帳' : '去登錄'}` 
                        } 
                    </button> 
                </div> 
            </div> 
        </div> 
    ); 
}

明天,我們也把api的地方加個登入驗證,以及製作 送出訂單的畫面~

那就明天見啦!


上一篇
Day 24 番外篇 - 試試看刻一個元件
下一篇
Day 26 Laravel + React 實戰之路 -7電商轉換最後一哩路
系列文
跳脫MVC,Laravel + React 建立電商網站30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言