# laravel 專案建立+ 後台產生+ 雜七雜八套件
###### tags: `Laravel`
---
# 時間管理 php套件 - carbon
https://carbon.nesbot.com/docs/
---
# Route
列出所有Route列表
```
php artisan route:list
```
---
# Middleware (中介層)
## 使用方式
1. 下在route中
在單一一個Route後面加上middleware
```
Route::get('admin/profile', function () {
//
})->middleware('auth');
```
Route群組起來(https://laravel.com/docs/master/routing#route-groups),加上middleware
```
Route::middleware(['auth'])->group(function () {
Route::get('/', function () {
// 使用 auth 中间件
});
});
```
更進階的做法
prefix(路由前綴) + group(群組) + Auth middleware(中介層)
```
Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function(){
Route::get('/', function () {
// 使用 auth 中间件
});
});
```
---
e.g.
```
Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function(){
Route::get('/','AdminController@index');
Route::get('product', 'ProductController@index');
Route::get('product/create', 'ProductController@create');
Route::post('product/store', 'ProductController@store');
Route::get('product/edit/{id}', 'ProductController@edit');
Route::post('product/update/{id}', 'ProductController@update');
Route::post('product/destroy/{id}', 'ProductController@destroy');
Route::get('news', 'NewsController@index');
Route::get('news/create', 'NewsController@create');
Route::post('news/store', 'NewsController@store');
Route::get('news/edit/{id}', 'NewsController@edit');
Route::post('news/update/{id}', 'NewsController@update');
Route::post('news/destroy/{id}', 'NewsController@destroy');
});
```
---
# 建立後臺步驟
## 準備步驟
1. Run Authentication(https://laravel.com/docs/master/authentication)
2. 由於Laravel 更新7版,UI更新到2.0 執行下面第二句就能安裝UI
```
composer require laravel/ui --dev
composer require laravel/ui "^1.0" --dev -vvv
php artisan ui vue --auth
npm run dev
```
<!-- composer require laravel/ui:^2.4 -->
https://laravel.com/docs/7.x/frontend
建立使用者資料表
```
php artisan migrate
```
2. 修改`$redirectTo`
修改以下路徑的php檔案,要將其中的$redirectTo從 '/home' => '/admin'.
* app\Http\Controllers\Auth底下的
* ConfirmPasswordController.php
* LoginController.php
* RegisterController.php
* ResetPasswordController.php
* VerificationController.php
* app\Http\Middleware底下的
* RedirectIfAuthenticated.php
共有六處要做修改。
* app\Http\Providers底下的
* RouteServiceProvider.php
```public const admin = '/admin';```
可改可不改 改了 註冊後不會錯誤 重新導向admin的route
3. 建立後臺要使用的Route列表
e.g. 以產品管理為例,可以使用以下指令來快速產生ProductController,加上參數`--resource`可以再把Controller中的程式碼預先長出來。
```
php artisan make:controller ProductController --resource
```
通常來說,在後台的特定管理動作,會包含新增、編輯、刪除三個動作。
統計共會使用到以下Route步驟
```
Route::get('product', 'ProductController@index');
Route::get('product/create', 'ProductController@create');
Route::post('product/store', 'ProductController@store');
Route::get('product/edit/{id}', 'ProductController@edit');
Route::post('product/update/{id}', 'ProductController@update');
Route::post('product/destroy/{id}', 'ProductController@destroy');
```
如果要再仔細說明一下其中每個Route的作用
```
Route::get('product', 'ProductController@index'); //----------------> 列出所有產品的頁面
Route::get('product/create', 'ProductController@create'); //---------> 到建立產品的頁面
Route::post('product/store', 'ProductController@store'); //----------> "儲存"產品資料
Route::get('product/edit/{id}', 'ProductController@edit'); //--------> 到特定產品的頁面
Route::post('product/update/{id}', 'ProductController@update'); //---> "更新"產品資料
Route::post('product/destroy/{id}', 'ProductController@destroy'); //---> "刪除"產品資料
```
4. 修改app layout (resources\views\layouts\app.blade.php)
原始檔案
```
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}" defer></script>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
<ul class="navbar-nav mr-auto">
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@guest
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
@if (Route::has('register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
</li>
@endif
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }} <span class="caret"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
</div>
</li>
@endguest
</ul>
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
</body>
</html>
```
需修改以下步驟
* 補上yield css跟yield js
`@yield('css')` `@yield('js')`
* 把app.js 移到 </body>前
```
<script src="{{ asset('js/app.js') }}"></script>
```
最終檔案如下
```
<!doctype html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- CSRF Token -->
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<!-- Fonts -->
<link rel="dns-prefetch" href="//fonts.gstatic.com">
<link href="https://fonts.googleapis.com/css?family=Nunito" rel="stylesheet">
<!-- Styles -->
<link href="{{ asset('css/app.css') }}" rel="stylesheet">
@yield('css')
</head>
<body>
<div id="app">
<nav class="navbar navbar-expand-md navbar-light bg-white shadow-sm">
<div class="container">
<a class="navbar-brand" href="{{ url('/') }}">
{{ config('app.name', 'Laravel') }}
</a>
<button class="navbar-toggler" type="button" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="{{ __('Toggle navigation') }}">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<!-- Left Side Of Navbar -->
<ul class="navbar-nav mr-auto">
<li class="nav-item">
<a class="nav-link" href="/admin/news">最新消息管理</a>
</li>
</ul>
<!-- Right Side Of Navbar -->
<ul class="navbar-nav ml-auto">
<!-- Authentication Links -->
@guest
<li class="nav-item">
<a class="nav-link" href="{{ route('login') }}">{{ __('Login') }}</a>
</li>
@if (Route::has('register'))
<li class="nav-item">
<a class="nav-link" href="{{ route('register') }}">{{ __('Register') }}</a>
</li>
@endif
@else
<li class="nav-item dropdown">
<a id="navbarDropdown" class="nav-link dropdown-toggle" href="#" role="button" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false" v-pre>
{{ Auth::user()->name }} <span class="caret"></span>
</a>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="navbarDropdown">
<a class="dropdown-item" href="{{ route('logout') }}"
onclick="event.preventDefault();
document.getElementById('logout-form').submit();">
{{ __('Logout') }}
</a>
<form id="logout-form" action="{{ route('logout') }}" method="POST" style="display: none;">
@csrf
</form>
</div>
</li>
@endguest
</ul>
</div>
</div>
</nav>
<main class="py-4">
@yield('content')
</main>
</div>
<!-- Scripts -->
<script src="{{ asset('js/app.js') }}"></script>
@yield('js')
</body>
</html>
```
5.建立index.create.edit 三個頁面
* resources\views\admin\news\index.blade.php
* resources\views\admin\news\create.blade.php
* resources\views\admin\news\edit.blade.php
---
## Index
基本上要有以下功能
1. 新增項目的按鈕。
2. 要列出所有的項目,提供編輯及刪除功能。
--
在列表的部分可以使用Datatable,
Datatable中有一個Bootstrap Styling(https://datatables.net/examples/styling/bootstrap4),可以套用該套件,就可以快速有分頁/查詢的功能列表。
要小心再分頁中如果有要用到按鈕事件綁定請參考以下連結
(https://www.gyrocode.com/articles/jquery-datatables-why-click-event-handler-does-not-work/)
JQuery On Click Function not working after appending HTML
(https://www.codewall.co.uk/jquery-on-click-function-not-working-after-appending-html/)
### DataTable使用注意事項
* 記得去引用以下檔案
```
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css">
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js"></script>
```
* Datatable要記得初始化,可以再去做其他參數設定,如依照特定欄位去排序(https://datatables.net/reference/option/order)
```
$(document).ready(function() {
$('#example').DataTable({
"order": [1,"desc"]
});
});
```
---
最終`index.blade.php`檔案如下
```
@extends('layouts.app')
@section('css')
<link rel="stylesheet" href="https://cdn.datatables.net/1.10.20/css/dataTables.bootstrap4.min.css">
@endsection
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header">最新消息管理 - Index</div>
<div class="card-body">
<a class="btn btn-success" href="/admin/news/create">新增最新消息</a>
<hr>
<table id="example" class="table table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>標題(title)</th>
<th>排序(sort)</th>
<th width="120">功能</th>
</tr>
</thead>
<tbody>
@foreach ($items as $item)
<tr>
<td>{{ $item->title}}</td>
<td>{{ $item->sort}}</td>
<td>
<a class="btn btn-success btn-sm" href="/admin/news/edit/{{ $item->id}}">編輯</a>
<a class="btn btn-danger btn-sm" href="#" data-itemid="{{$item->id}}">刪除</a>
<form class="destroy-form" data-itemid="{{$item->id}}"
action="/admin/news/destroy/{{$item->id}}" method="POST" style="display: none;">
@csrf
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('js')
<script src="https://cdn.datatables.net/1.10.20/js/jquery.dataTables.min.js"></script>
<script src="https://cdn.datatables.net/1.10.20/js/dataTables.bootstrap4.min.js"></script>
<script>
$(document).ready(function() {
$('#example').DataTable({
"order": [1,"desc"]
});
$('#example').on('click','.btn-danger',function(){
event.preventDefault();
var r = confirm("你確定要刪除此項目嗎?");
if (r == true) {
var itemid = $(this).data("itemid");
$(`.destroy-form[data-itemid="${itemid}"]`).submit();
}
});
});
</script>
@endsection
```
---
## Create
在Create頁面中,主要是做到新增資料的動作,會使用到html中的Form Post(表單發送)。
--
Laravel的 From Post的範例如下,通常放於create.blade.php
```
<form method="post" action="/admin/news/store" enctype="multipart/form-data">
@csrf
<div class="form-group row">
<label for="title" class="col-sm-2 col-form-label">最新消息標題</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" name="title">
</div>
</div>
<div class="form-group row">
<label for="sort" class="col-sm-2 col-form-label">sort</label>
<div class="col-sm-10">
<input type="number" class="form-control" id="inputEmail3" value="" name="sort">
</div>
</div>
<div class="form-group row">
<div class="col-sm-10">
<button type="submit" class="btn btn-primary">SEND</button>
</div>
</div>
</form>
```
在做Form的時候要注意以下內容
* **Form的屬性**
`<form></form>`標籤中的三個屬性,分別是`method`、`action`跟`enctype`
* method: form表單發送大多是使用post,因為get會將參數顯示在網址列上,且長度有限制。
* action: 表單要發送到的路徑位置,請參考Route接收的路徑
* enctype: 表單的編碼方式,如要上傳檔案,一定要加上。
* **CSRF Token**
表單發送時一定要加上 `@csrf`
* **input的name屬性**
未來在contrller接收資料時,是看input的name,一定要加上。
--
接收Form資料時,Controller的寫法(NewsController.php)
```
public function store(Request $request)
{
/*
*抓出所有表單發送的資料
*$request->all();
*/
$title = $request->title;
$sort = $request->sort;
DB::table('news')->insert(
['title' => $title, 'sort' => $sort]
);
return redirect('/admin/news');
}
```
可以於一開始測試時先使用`$request->all();`
把資料使用`dd($request->all());` 將其所有表單內容抓出來確認。
確認資料欄位皆正確之後,在塞進資料庫中。
--
新增資料進資料庫的動作通常稱為Inserts,在Laravel中有兩種比較常見Inserts的作法。
1.使用DB
`DB::table('users')->insert(
['email' => 'john@example.com', 'votes' => 0]
);`
2.使用ORM
方法1
```
public function store(Request $request)
{
// Validate the request...
$flight = new Flight;
$flight->name = $request->name;
$flight->save();
}
```
方法2
```
$flight = App\Flight::create(['name' => 'Flight 10']);
```
方法3
```
$flight = App\Flight::create($request->all());
```
---
# ORM - 使用Model連結至資料庫
建立Model
```
php artisan make:model News
```
範例Model
```
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class News extends Model
{
protected $table = 'news';
protected $fillable = ['title','sort'];
}
```
---
## 在Controller中使用ORM
首先要記得引用class
e.g. 使用News的Modal => `use App\News;`
新增
```
//方法1
$news = new News;
$news->title = $request->title;
$news->sort = $request->sort;
$news->save();
```
```
//方法2
News::create($request->all());
```
修改
```
//方法1
$news = News::find($id);
$news->title = $request->title;
$news->sort = $request->sort;
$news->save();
```
```
//方法2
$news = News::find($id);
$news->update($request->all());
```
---
# 檔案上傳
## Laravel - filesystem方法
(https://nicole929chan.wordpress.com/2018/05/04/laravel5-file-storage/)
(https://learnku.com/articles/5615/some-thoughts-about-the-storagelink-artisan-command)
```
php artisan storage:link
```
跑過storage:link之後,類似建立一個捷徑的資料夾
public/storage → storage/app/public
```
//Controller
public function store(Request $request)
{
$requsetData = $request->all();
//上傳檔案
$file_name = $request->file('img')->store('','public');
$requsetData['img'] = $file_name;
Product::create($requsetData);
return redirect('/admin/product');
}
//view
<img src="{{asset('/storage/'.$item->img)}}" alt="">
```
## 暴力直接move檔案到/public中
```
$imageName = time().'.'.$request->image->getClientOriginalExtension();
$request->image->move(public_path('/uploaded_images'), $imageName);
```
刪除時會用到File::delete
要記得use Illuminate\Support\Facades\File;
```
public function store(Request $request)
{
$requsetData = $request->all();
if($request->hasFile('img')) {
$file = $request->file('img');
$path = $this->fileUpload($file,'product');
$requsetData['img'] = $path;
}
Product::create($requsetData);
return redirect('/admin/product');
}
public function update(Request $request, $id)
{
$item = Product::find($id);
$requsetData = $request->all();
if($request->hasFile('img')) {
$old_image = $item->img;
$file = $request->file('img');
$path = $this->fileUpload($file,'product');
$requsetData['img'] = $path;
File::delete(public_path().$old_image);
}
$item->update($requsetData);
return redirect('/admin/product');
}
public function destroy($id)
{
$item = Product::find($id);
$old_image = $item->img;
if(file_exists(public_path().$old_image)){
File::delete(public_path().$old_image);
}
$item->delete();
return redirect('/admin/product');
}
private function fileUpload($file,$dir){
//防呆:資料夾不存在時將會自動建立資料夾,避免錯誤
if( ! is_dir('upload/')){
mkdir('upload/');
}
//防呆:資料夾不存在時將會自動建立資料夾,避免錯誤
if ( ! is_dir('upload/'.$dir)) {
mkdir('upload/'.$dir);
}
//取得檔案的副檔名
$extension = $file->getClientOriginalExtension();
//檔案名稱會被重新命名
$filename = strval(time().md5(rand(100, 200))).'.'.$extension;
//移動到指定路徑
move_uploaded_file($file, public_path().'/upload/'.$dir.'/'.$filename);
//回傳 資料庫儲存用的路徑格式
return '/upload/'.$dir.'/'.$filename;
}
```
---
**Summernoe 上傳圖片方法**
1. 使用原本的base64
2. 另外上傳圖片(沒有刪除機制,會占用硬碟空間)
```
前端
<script>
$(document).ready(function() {
$('#description').summernote({
height: 150,
lang: 'zh-TW',
callbacks: {
onImageUpload: function(files) {
for(let i=0; i < files.length; i++) {
$.upload(files[i]);
}
},
onMediaDelete : function(target) {
$.delete(target[0].getAttribute("src"));
}
},
});
$.upload = function (file) {
let out = new FormData();
out.append('file', file, file.name);
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_upload_img',
contentType: false,
cache: false,
processData: false,
data: out,
success: function (img) {
$('#description').summernote('insertImage', img);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
};
$.delete = function (file_link) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_delete_img',
data: {file_link:file_link},
success: function (img) {
console.log("delete:",img);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
}
});
</script>
後端
//Route Web.php
Route::post('/ajax_upload_img','AdminController@ajax_upload_img');
Route::post('/ajax_delete_img','AdminController@ajax_delete_img');
//Controller AdminController
public function ajax_upload_img()
{
// A list of permitted file extensions
$allowed = array('png', 'jpg', 'gif','zip');
if(isset($_FILES['file']) && $_FILES['file']['error'] == 0){
$extension = pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION);
if(!in_array(strtolower($extension), $allowed)){
echo '{"status":"error"}';
exit;
}
$name = strval(time().md5(rand(100, 200)));
$ext = explode('.', $_FILES['file']['name']);
$filename = $name . '.' . $ext[1];
//防呆:資料夾不存在時將會自動建立資料夾,避免錯誤
if( ! is_dir('upload/')){
mkdir('upload/');
}
//防呆:資料夾不存在時將會自動建立資料夾,避免錯誤
if ( ! is_dir('upload/img')) {
mkdir('upload/img');
}
$destination = public_path().'/upload/img/'. $filename; //change this directory
$location = $_FILES["file"]["tmp_name"];
move_uploaded_file($location, $destination);
echo "/upload/img/".$filename;//change this URL
}
exit;
}
public function ajax_delete_img(Request $request){
if(file_exists(public_path().$request->file_link)){
File::delete(public_path().$request->file_link);
}
}
```
---
# **Auth in web.php**
Auth::routes();
```
// Laravel 5.7, Laravel 5.8, and Laravel 6.0
// Authentication Routes...
Route::get('login', 'Auth\LoginController@showLoginForm')->name('login');
Route::post('login', 'Auth\LoginController@login');
Route::post('logout', 'Auth\LoginController@logout')->name('logout');
// Registration Routes...
Route::get('register', 'Auth\RegisterController@showRegistrationForm')->name('register');
Route::post('register', 'Auth\RegisterController@register');
// Password Reset Routes...
Route::get('password/reset', 'Auth\ForgotPasswordController@showLinkRequestForm')->name('password.request');
Route::post('password/email', 'Auth\ForgotPasswordController@sendResetLinkEmail')->name('password.email');
Route::get('password/reset/{token}', 'Auth\ResetPasswordController@showResetForm')->name('password.reset');
Route::post('password/reset', 'Auth\ResetPasswordController@reset')->name('password.update');
// Email Verification Routes...
Route::get('email/verify', 'Auth\VerificationController@show')->name('verification.notice');
Route::get('email/verify/{id}/{hash}', 'Auth\VerificationController@verify')->name('verification.verify'); // v6.x
/* Route::get('email/verify/{id}', 'Auth\VerificationController@verify')->name('verification.verify'); // v5.x */
Route::get('email/resend', 'Auth\VerificationController@resend')->name('verification.resend');
```
```
Auth::routes(['register' => false,'reset' => false,'verify' => false]);
```
---
# ORM - Relationships
一對多 (三層)
## Model
```
//產品類別
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ProductType extends Model
{
protected $table = 'product_type';
protected $fillable = ['type_name'];
public function products()
{
return $this->hasMany('App\Product','type_id')->orderBy('sort', 'desc');
}
}
//產品
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class Product extends Model
{
protected $table = 'products';
protected $fillable = ['title','description','img','sort','type_id'];
public function productType()
{
return $this->belongsTo('App\ProductType','type_id');
}
public function product_imgs()
{
return $this->hasMany('App\ProductImg','type_id');
}
}
//產品圖片
<?php
namespace App;
use Illuminate\Database\Eloquent\Model;
class ProductImg extends Model
{
protected $table = 'product_imgs';
protected $fillable = ['product_id','img'];
}
```
## Controller
```
產品類別 再加上 產品(多個)
$ProductTypes = ProductType::orderBy('sort', 'desc')->with('products')->get();
//view
@foreach ($ProductTypes as $ProductType)
//$ProductType 為單一類別
@foreach ($ProductType->products as $product)
//$product 為單一產品
@endforeach
@endforeach
```
---
# 多張圖片上傳
## KeyWords
* laravel file upload multiple
(https://stackoverflow.com/questions/39846148/laravel-5-3-multiple-file-uploads)
* laravel create get id
(https://stackoverflow.com/questions/37075148/laravel-get-the-id-of-usercreate-and-insert-new-row-using-that-id)
(https://laravelcode.com/post/laravel-55-get-last-inserted-id-with-example)
* js refresh div
(https://stackoverflow.com/questions/33801650/how-do-i-refresh-a-div-content)
* js input onchange ajax
(https://stackoverflow.com/questions/23712799/post-input-onchange-with-ajax)
```
//HTML的部分
//iuput的name後面要有方括弧
//要有屬性 multiple
<input type="file" id="imgs" name="imgs[]" multiple required>
//Controller
$requsetData = $request->all();
//單一檔案
if($request->hasFile('img')) {
$file = $request->file('img');
$path = $this->fileUpload($file,'product');
$requsetData['img'] = $path;
}
$new_product = Product::create($requsetData);
$new_product_id = $new_product->id;
//多個檔案
$files = $request->file('imgs');
if($request->hasFile('imgs'))
{
foreach ($files as $file) {
//上傳圖片
$path = $this->fileUpload($file,'product');
//新增資料進DB
$product_img = new ProductImg;
$product_img->product_id = $new_product_id;
$product_img->img = $path;
$product_img->save();
}
}
```
## 完整的Controller
```
<?php
namespace App\Http\Controllers;
use App\Product;
use App\ProductImg;
use App\ProductType;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Storage;
class ProductController extends Controller
{
public function index()
{
$items = Product::with('productType')->get();
return view('admin.product.index',compact("items"));
}
public function create()
{
$productTypes = ProductType::all();
return view('admin.product.create',compact('productTypes'));
}
public function store(Request $request)
{
$requsetData = $request->all();
//單一檔案
if($request->hasFile('img')) {
$file = $request->file('img');
$path = $this->fileUpload($file,'product');
$requsetData['img'] = $path;
}
$new_product = Product::create($requsetData);
$new_product_id = $new_product->id;
//多個檔案
if($request->hasFile('imgs'))
{
$files = $request->file('imgs');
foreach ($files as $file) {
//上傳圖片
$path = $this->fileUpload($file,'product_imgs');
//新增資料進DB
$product_img = new ProductImg;
$product_img->product_id = $new_product_id;
$product_img->img = $path;
$product_img->save();
}
}
return redirect('/admin/product');
}
public function edit($id)
{
$productTypes = ProductType::all();
$item = Product::where('id',$id)->with('product_imgs')->first();
return view('admin.product.edit',compact('item','productTypes'));
}
public function update(Request $request, $id)
{
$item = Product::find($id);
$requsetData = $request->all();
if($request->hasFile('img')) { //如果使用者有重新上傳圖片
$old_image = $item->img; //抓取舊圖片路徑
File::delete(public_path().$old_image); //把舊圖片刪除
//上傳圖片
$file = $request->file('img');
$path = $this->fileUpload($file,'product');
$requsetData['img'] = $path;
}
//多個檔案
if($request->hasFile('imgs'))
{
$files = $request->file('imgs');
foreach ($files as $file) {
//上傳圖片
$path = $this->fileUpload($file,'product_imgs');
//新增資料進DB
$product_img = new ProductImg;
$product_img->product_id = $id;
$product_img->img = $path;
$product_img->save();
}
}
$item->update($requsetData);
return redirect('/admin/product');
}
public function destroy($id)
{
$item = Product::find($id);
//單一圖片的刪除
$old_image = $item->img;
if(file_exists(public_path().$old_image)){
File::delete(public_path().$old_image);
}
$item->delete();
//多張圖片的刪除
$product_imgs = ProductImg::where('product_id',$id)->get();
foreach($product_imgs as $product_img){
$old_product_img = $product_img->img;
if(file_exists(public_path().$old_product_img)){
File::delete(public_path().$old_product_img);
}
$product_img->delete();
}
return redirect('/admin/product');
}
private function fileUpload($file,$dir){
//防呆:資料夾不存在時將會自動建立資料夾,避免錯誤
if( ! is_dir('upload/')){
mkdir('upload/');
}
//防呆:資料夾不存在時將會自動建立資料夾,避免錯誤
if ( ! is_dir('upload/'.$dir)) {
mkdir('upload/'.$dir);
}
//取得檔案的副檔名
$extension = $file->getClientOriginalExtension();
//檔案名稱會被重新命名
$filename = strval(time().md5(rand(100, 200))).'.'.$extension;
//移動到指定路徑
move_uploaded_file($file, public_path().'/upload/'.$dir.'/'.$filename);
//回傳 資料庫儲存用的路徑格式
return '/upload/'.$dir.'/'.$filename;
}
}
```
## Edit Page
```
@extends('layouts.app')
@section('css')
<style>
.product_imgs{
position: relative;
}
.product_imgs .btn-danger{
border-radius: 50%;
position: absolute;
right: 20px;
top: 5px;
}
.product_imgs .sort{
display: flex;
margin-top: 5px;
}
.product_imgs label{
margin: 0 5px;
line-height: 37px;
}
.product_imgs input{
width: 100%;
}
</style>
@endsection
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header">產品管理 - Edit</div>
<div class="card-body">
<form method="post" action="/admin/product/update/{{$item->id}}" enctype="multipart/form-data">
@csrf
<div class="form-group row">
<label for="type_id" class="col-sm-2 col-form-label">產品類別</label>
<div class="col-sm-10">
<select class="form-control" name="type_id" id="type_id">
@foreach ($productTypes as $productType)
<option value="{{ $productType->id }}" @if($item->type_id === $productType->id)
selected @endif>{{ $productType->type_name }}</option>
@endforeach
</select>
</div>
</div>
<hr>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">現有產品圖片</label>
<div class="col-sm-10">
<img class="img-fluid" src="{{$item->img}}" alt="">
</div>
</div>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">重新上傳產品圖片 <br><small
class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label>
<div class="col-sm-10">
<input type="file" class="form-control" id="img" value="" name="img">
</div>
</div>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">現有產品組圖片</label>
@foreach ($item->product_imgs as $product_img)
<div class="col-sm-2 product_imgs" data-productimgid="{{$product_img->id}}">
<img class="img-fluid" src="{{$product_img->img}}" alt="">
<button class="btn btn-danger btn-sm" data-productimgid="{{$product_img->id}}" type="button">X</button>
<div class="sort">
<label for="imgs">Sort</label>
<input class="form-control" type="text">
</div>
</div>
@endforeach
</div>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">重新上傳產品組圖片 <br><small
class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label>
<div class="col-sm-10">
<input type="file" class="form-control" id="imgs" name="imgs[]" multiple>
</div>
</div>
<hr>
<div class="form-group row">
<label for="title" class="col-sm-2 col-form-label">標題</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" value="{{$item->title}}" name="title"
required>
</div>
</div>
<div class="form-group row">
<label for="description" class="col-sm-2 col-form-label">敘述</label>
<div class="col-sm-10">
<textarea class="form-control" name="description" id="description"
rows="5">{{$item->description}}</textarea>
</div>
</div>
<div class="form-group row">
<label for="sort" class="col-sm-2 col-form-label">排序(sort)</label>
<div class="col-sm-10">
<input type="number" class="form-control" id="sort" value="{{$item->sort}}" name="sort"
value="1" required>
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-sm-12 text-center">
<button type="submit" class="btn btn-primary">SEND</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('js')
<script>
$(document).ready(function() {
$('#description').summernote({
height: 150,
lang: 'zh-TW',
callbacks: {
onImageUpload: function(files) {
for(let i=0; i < files.length; i++) {
$.upload(files[i]);
}
},
onMediaDelete : function(target) {
$.delete(target[0].getAttribute("src"));
}
},
});
$.upload = function (file) {
let out = new FormData();
out.append('file', file, file.name);
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_upload_img',
contentType: false,
cache: false,
processData: false,
data: out,
success: function (img) {
$('#description').summernote('insertImage', img);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
};
$.delete = function (file_link) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_delete_img',
data: {file_link:file_link},
success: function (img) {
console.log("delete:",img);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
}
$('.product_imgs .btn-danger').click(function () {
var product_imgs_id = $(this).data('productimgid');
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_delete_product_imgs',
data: {product_imgs_id: product_imgs_id},
success: function (res) {
$( `.product_imgs[data-productimgid='${product_imgs_id}']` ).remove();
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
});
});
</script>
@endsection
```
## web ajax Route and controller
```
//Web.php
Route::post('/ajax_delete_product_imgs','AdminController@ajax_delete_product_imgs');
//AdminController
public function ajax_delete_product_imgs(Request $request)
{
$product_imgs_id = $request->product_imgs_id;
//多張圖片組的單一圖片刪除
$product_img = ProductImg::where('id',$product_imgs_id)->first();
$old_product_img = $product_img->img;
if(file_exists(public_path().$old_product_img)){
File::delete(public_path().$old_product_img);
}
$product_img->delete();
echo '{"status":"success","message":"delete file success"}';
}
```
---
# 各個類別限定三個產品的作法
```
$ProductTypes = ProductType::orderBy('sort', 'desc')->with('products')->get();
foreach( $ProductTypes as $type){
$type->products = $type->products->take(3);
}
```
---
# 多張圖片排序 - 使用Muuri(未完成) or 手動輸入排序數字
Edit-Page
```
@extends('layouts.app')
@section('css')
<style>
.product_imgs {
position: relative;
}
.product_imgs .btn-danger {
border-radius: 50%;
position: absolute;
right: 5px;
top: 5px;
}
.product_imgs .sort {
display: flex;
margin-top: 5px;
}
.product_imgs label {
margin: 0 5px;
line-height: 37px;
}
.product_imgs input {
width: 100%;
}
.grid {
position: relative;
min-height: 120px;
}
.item {
display: block;
position: absolute;
width: 100px;
height: 100px;
margin: 5px;
z-index: 1;
}
.item.muuri-item-dragging {
z-index: 3;
}
.item.muuri-item-releasing {
z-index: 2;
}
.item.muuri-item-hidden {
z-index: 0;
}
.item-content {
position: relative;
width: 100%;
height: 100%;
}
</style>
@endsection
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header">產品管理 - Edit</div>
<div class="card-body">
<form method="post" action="/admin/product/update/{{$item->id}}" enctype="multipart/form-data">
@csrf
<input type="text" id="new_sort" name="new_sort" value="" hidden>
<div class="form-group row">
<label for="type_id" class="col-sm-2 col-form-label">產品類別</label>
<div class="col-sm-10">
<select class="form-control" name="type_id" id="type_id">
@foreach ($productTypes as $productType)
<option value="{{ $productType->id }}" @if($item->type_id === $productType->id)
selected @endif>{{ $productType->type_name }}</option>
@endforeach
</select>
</div>
</div>
<hr>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">現有產品圖片</label>
<div class="col-sm-10">
<img class="img-fluid" src="{{$item->img}}" alt="">
</div>
</div>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">重新上傳產品圖片 <br><small
class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label>
<div class="col-sm-10">
<input type="file" class="form-control" id="img" value="" name="img">
</div>
</div>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">現有產品組圖片</label>
<div class="col-sm-10">
<div class="grid">
@foreach ($item->product_imgs as $product_img)
<div class="item" data-productimgid="{{$product_img->id}}">
<div class="item-content">
<div class="product_imgs" data-productimgid="{{$product_img->id}}">
<img class="img-fluid" src="{{$product_img->img}}" alt="">
<button class="btn btn-danger btn-sm"
data-productimgid="{{$product_img->id}}" type="button">X</button>
<div class="sort">
<label for="imgs">Sort</label>
<input class="form-control"
onchange="post_ajax_sort(this,{{$product_img->id}})"
type="text" value="{{$product_img->sort}}">
</div>
</div>
</div>
</div>
@endforeach
</div>
</div>
</div>
<div class="form-group row">
<label for="img" class="col-sm-2 col-form-label">重新上傳產品組圖片 <br><small
class="text-danger">*建議圖片尺寸500px(寬)*700px(高)</small></label>
<div class="col-sm-10">
<input type="file" class="form-control" id="imgs" name="imgs[]" multiple>
</div>
</div>
<hr>
<div class="form-group row">
<label for="title" class="col-sm-2 col-form-label">標題</label>
<div class="col-sm-10">
<input type="text" class="form-control" id="title" value="{{$item->title}}" name="title"
required>
</div>
</div>
<div class="form-group row">
<label for="description" class="col-sm-2 col-form-label">敘述</label>
<div class="col-sm-10">
<textarea class="form-control" name="description" id="description"
rows="5">{{$item->description}}</textarea>
</div>
</div>
<div class="form-group row">
<label for="sort" class="col-sm-2 col-form-label">排序(sort)</label>
<div class="col-sm-10">
<input type="number" class="form-control" id="sort" value="{{$item->sort}}" name="sort"
value="1" required>
</div>
</div>
<hr>
<div class="form-group row">
<div class="col-sm-12 text-center">
<button type="submit" class="btn btn-primary">SEND</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('js')
<script src="https://unpkg.com/web-animations-js@2.3.2/web-animations.min.js"></script>
<script src="https://unpkg.com/muuri@0.8.0/dist/muuri.min.js"></script>
<script>
$(document).ready(function() {
var grid = new Muuri('.grid',{
dragEnabled: true,
}).on('move', function () {
getOrder(grid);
});
function getOrder(grid) {
var currentItems = grid.getItems();
var currentItemIds = currentItems.map(function (item) {
return item.getElement().getAttribute('data-productimgid')
});
$('#new_sort').val(currentItemIds.join());
}
$('#description').summernote({
height: 150,
lang: 'zh-TW',
callbacks: {
onImageUpload: function(files) {
for(let i=0; i < files.length; i++) {
$.upload(files[i]);
}
},
onMediaDelete : function(target) {
$.delete(target[0].getAttribute("src"));
}
},
});
$.upload = function (file) {
let out = new FormData();
out.append('file', file, file.name);
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_upload_img',
contentType: false,
cache: false,
processData: false,
data: out,
success: function (img) {
$('#description').summernote('insertImage', img);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
};
$.delete = function (file_link) {
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_delete_img',
data: {file_link:file_link},
success: function (img) {
console.log("delete:",img);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
}
$('.product_imgs .btn-danger').click(function () {
var product_imgs_id = $(this).data('productimgid');
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_delete_product_imgs',
data: {product_imgs_id: product_imgs_id},
success: function (res) {
$( `.product_imgs[data-productimgid='${product_imgs_id}']` ).remove();
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
});
});
function post_ajax_sort(element,product_imgs_id) {
var product_imgs_id;
var sort_value = element.value;
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/admin/ajax_sort_product_imgs',
data: {product_imgs_id: product_imgs_id,sort_value: sort_value},
success: function (res) {
// $( `.product_imgs[data-productimgid='${product_imgs_id}']` ).remove();
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
}
</script>
@endsection
```
後端送JS
Route
```
Route::post('/ajax_sort_product_imgs','AdminController@ajax_sort_product_imgs');
```
Controller
```
public function ajax_sort_product_imgs(Request $request)
{
$product_imgs_id = $request->product_imgs_id;
$sort_value = $request->sort_value;
$ProductImg = ProductImg::find($product_imgs_id);
$ProductImg->sort = $sort_value;
$ProductImg->save();
echo '{"status":"success","message":"sort img success"}';
}
```
---
# 自動產生Model
由建立好的DB後,可自行產生其Model
https://github.com/krlove/eloquent-model-generator
範例:
```
php artisan krlove:generate:model OrderStatus --table-name=order_status
php artisan krlove:generate:model DonateCashData --table-name=donate_cash_data
```
> 要記得在Migration或DB設定好關聯,即可在Model中自動附帶上relationship
---
# 購物車
在選擇使用Laravel shopping cart購物車套件的時候,要注意支援的Laravel版本
這次選擇的是 darryldecode/laravelshoppingcart
(https://github.com/darryldecode/laravelshoppingcart)
依照不同的版本,支援Laravel 5.1~ ,Laravel 5.5, 5.6 or 5.7~,6
For Laravel 5.1~
```
composer require "darryldecode/cart:~2.0"
```
For Laravel 5.5, 5.6 or 5.7~,6
```
composer require "darryldecode/cart:~4.0"
or
composer require "darryldecode/cart"
```
---
購物車有分是否要指定User or 不分User的版本
下面為不分User的版本範例
可以看到`\Cart::`之後直接接著methods
```
<?php
namespace App\Http\Controllers;
use Darryldecode\Cart\Cart;
use Illuminate\Http\Request;
class CartController extends Controller
{
public function addProductToCar(){
\Cart::add(455, 'Sample Item', 100.99, 2, array());
}
public function getContent()
{
$content = \Cart::getContent();
}
public function TotalCart()
{
$total = \Cart::getTotal();
}
}
```
---
## 加入購物車
下面為指定User,並新增加入購物車的範例程式
在View部分主要是以Ajax方式,將產品編號(productID)傳至後端
由後端Controller進行加入購物車的動作,並統計所有購物車內的產品數量
將統計後的數字傳至前端,更新Navbar上的購物車數字
以下為製作時須注意事項
* 於前端要送出ajax要注意crsf token,要於ajax補上相關設定
* 加入購物車需要填入相關參數,除屬性(attributess)之外,其餘皆不能為空值
* 使用購物車時要確定有沒有要指定使用者,如果有要加上`\Cart::session($userID)`
* 上述的`userID`為使用者的ID,要再透過以下方式取得`$userId = auth()->user()->id;`
完整範例程式碼如下
```
$userId = auth()->user()->id; // or any string represents user identifier
\Cart::session($userId)->add('456', 'Product Name', '200', 1, array());
```
### WEB
```
// testcart
Route::post('/addcart', 'cartcontroller@addcart');
Route::get('/getcontent', 'cartcontroller@getcontent');
Route::get('/totalcart', 'cartcontroller@totalcart');
```
### Controller
```
public function addcart(Request $request)
{
$product_id = $request->product_id;
$product = Product::find($product_id);
$userId = auth()->user()->id; // or any string represents user identifier
\Cart::session($userId)->add($product_id, $product->title, $product->price, 1, array());
$cartTotalQuantity = \Cart::session($userId)->getTotalQuantity();
return $cartTotalQuantity;
}
```
### Product
Html按鈕 加入購物車按鈕
```
<button class="btn btn-danger addcart" data-productid="{{$product->id}}">加入購物車</button>
```
JS按鍵事件綁定 + Ajax發送
```
$('.addcart').click(function () {
var product_id = $(this).data('productid');
console.log(product_id);
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/addcart',
data: {product_id:product_id},
success: function (res) {
$('#cartTotalQuantity').text(res);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
});
```
### front layout
在Navbar上要顯示出購物車的數量
```
<i class="icon-shopping-cart">
<span id="cartTotalQuantity">
{{-- {{ \Cart::getTotalQuantity() }} 沒指定人的寫法 --}}
{{-- 指定對象的PHP原生寫法 --}}
@guest
0
@else
<?php
$userId = auth()->user()->id;
$cartTotalQuantity = \Cart::session($userId)->getTotalQuantity();
echo $cartTotalQuantity;
?>
@endguest
</span>
</i>
```
---
## 結帳頁 + 修改購物車上的產品數量
### Web
```
Route::get('/cart', 'CartController@cart'); //結帳頁
Route::post('/changeProductQty','CartController@changeProductQty'); //修改產品數量ajax
```
### Controller
```
public function cart()
{
$content = \Cart::getContent()->sort(); //取得購物車產品後排序
$total = \Cart::getTotal();
return view("front.cart",compact('content','total'));
}
public function changeProductQty(Request $request)
{
$product_id = $request->product_id;
$new_qty = $request->new_qty;
\Cart::update($product_id , array(
'quantity' => array(
'relative' => false,
'value' => $new_qty
),
));
return "suceess";
}
```
### View
```
@extends('layouts.front_layout')
@section('css')
@endsection
@section('content')
<div class="container">
<h1>Cart結帳頁</h1>
<div>
<table class="table table-dark">
<thead>
<tr>
<th scope="col">#</th>
<th scope="col">ProductName</th>
<th scope="col">Price</th>
<th scope="col">Qty</th>
<th scope="col">SubTotal</th>
<th width="50">Delete</th>
</tr>
</thead>
<tbody>
{{$content}}
@foreach ($content as $item)
<tr>
<th scope="row">1</th>
<td>{{$item->name}}</td>
<td>{{$item->price}}</td>
<td><input type="text" value="{{$item->quantity}}" class="product_qty"
data-productid="{{$item->id}}"></td>
<td>{{$item->price * $item->quantity}}</td>
<td><button class="btn btn-danger btn-sm">X</button></td>
</tr>
@endforeach
</tbody>
</table>
</div>
<div>
<h2>總計:$ {{$total}}</h2>
</div>
<button class="text-center btn btn-success">確定結帳</button>
</div>
@endsection
@section('js')
<script>
$('.product_qty').on('change', function() {
// console.log("onchangeValue:",this.value);
// console.log("onchangeProductID:",this.getAttribute("data-productid"));
var new_qty = this.value;
var product_id = this.getAttribute("data-productid");
$.ajaxSetup({
headers: {
'X-CSRF-TOKEN': $('meta[name="csrf-token"]').attr('content')
}
});
$.ajax({
method: 'POST',
url: '/changeProductQty',
data: {
product_id:product_id,
new_qty:new_qty
},
success: function (res) {
document.location.reload(true);
},
error: function (jqXHR, textStatus, errorThrown) {
console.error(textStatus + " " + errorThrown);
}
});
});
</script>
@endsection
```
## Order - 訂單管理
### Migration
資料表`Order` 的Migration
主要紀錄訂單的編號、寄件人相關資訊、付費方式及寄送時間
```
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOrderTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('order', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('order_no');
$table->string('receive_name');
$table->string('receive_phone');
$table->string('receive_mobile');
$table->string('receive_address');
$table->string('receive_email');
$table->string('receipt');
$table->string('time_to_send');
$table->string('status')->default('新訂單');
$table->integer('total_price');
$table->string('remark','2000');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order');
}
}
```
資料表`OrderItems` 的Migration
主要紀錄訂單中的購買品項(product_id)及其數量(Qty)
與Order關聯,為一對多關係
一個訂單中會有多個產品,每個產品會買的數量都可能會不同
```
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateOrderItemsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('order_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('order_id');
$table->foreign('order_id')->references('id')->on('order');
$table->unsignedBigInteger('product_id');
$table->foreign('product_id')->references('id')->on('products');
$table->integer('qty');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('order_items');
}
}
```
### Controller
負責將訂單資料及購買的產品細項存進資料庫,確定訂單建立成功後,才會跑第三方支付
--
先將request->all()取出,放至變數$request_data中
再添加order_no的部分進陣列中,最後再以ORM - Create的方式建立訂單
```
public function send_check_out(Request $request)
{
$request_data = $request->all();
$request_data["order_no"] = "201912061";
$request_data["total_price"] = \Cart::getTotal();
$new_order = Order::create($request_data);
$cat_contents = \Cart::getContent()->sort();
foreach ($cat_contents as $item) {
$OrderItem = new OrderItems();
$OrderItem->order_id = $new_order->id;
$OrderItem->product_id = $item->id;
$OrderItem->qty = $item->quantity;
$OrderItem->price = $item->price;
$OrderItem->save();
}
}
```
---
# 訂單建立+金流串接
* 綠界金流API串接文件(https://www.ecpay.com.tw/Content/files/ecpay_011.pdf)
* Laravel 串接綠界非官方套件
(https://github.com/tsaiyihua/laravel-ecpay)
## Laravel 串接綠界套件安裝
Composer安裝套件
```
composer require tsaiyihua/laravel-ecpay
```
Config設定
```
php artisan vendor:publish --tag=ecpay
```
.env設定
```
ECPAY_MERCHANT_ID=2000132
ECPAY_HASH_KEY=5294y06JbISpM5x9
ECPAY_HASH_IV=v77hoKGq4kWxNNIS
```
---
**web.php**
```
Route::prefix('cart_ecpay')->group(function(){
//當消費者付款完成後,綠界會將付款結果參數以幕後(Server POST)回傳到該網址。
Route::post('notify', 'CartController@notifyUrl')->name('notify');
//付款完成後,綠界會將付款結果參數以幕前(Client POST)回傳到該網址
Route::post('return', 'CartController@returnUrl')->name('return');
});
```
**middleware > VerifyCsrfToken.php**
```
<?php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
/**
* Indicates whether the XSRF-TOKEN cookie should be set on the response.
*
* @var bool
*/
protected $addHttpCookie = true;
/**
* The URIs that should be excluded from CSRF verification.
*
* @var array
*/
protected $except = [
'cart_ecpay/return','cart_ecpay/notify'
];
}
```
**controller**
```
<?php
namespace App\Http\Controllers;
use App\Order;
use App\Product;
use Carbon\Carbon;
use App\OrderItems;
use Darryldecode\Cart\Cart;
use Illuminate\Http\Request;
use TsaiYiHua\ECPay\Checkout;
use TsaiYiHua\ECPay\Services\StringService;
use TsaiYiHua\ECPay\Collections\CheckoutResponseCollection;
class CartController extends Controller
{
public function __construct(Checkout $checkout,CheckoutResponseCollection $checkoutResponse)
{
$this->checkout = $checkout;
$this->checkoutResponse = $checkoutResponse;
}
public function addProductToCar(Request $request){
$product_id = $request->product_id;
$product = Product::find($product_id);
\Cart::add($product_id, $product->title, $product->price, 1, array());
$cartTotalQuantity = \Cart::getTotalQuantity();
return $cartTotalQuantity;
}
public function cart()
{
$content = \Cart::getContent()->sort();
$total = \Cart::getTotal();
return view("front.cart",compact('content','total'));
}
public function changeProductQty(Request $request)
{
$product_id = $request->product_id;
$new_qty = $request->new_qty;
\Cart::update($product_id , array(
'quantity' => array(
'relative' => false,
'value' => $new_qty
),
));
return "suceess";
}
public function deleteProductInCart(Request $request)
{
$product_id = $request->product_id;
\Cart::remove($product_id);
return "suceess";
}
public function cart_check_out()
{
$content = \Cart::getContent()->sort();
$total = \Cart::getTotal();
return view("front.cart_check_out",compact('content','total'));
}
public function send_check_out(Request $request)
{
//建立訂單
$request_data = $request->all();
$request_data["order_no"] = Carbon::now()->format('Ymd');
$request_data["total_price"] = \Cart::getTotal();
$new_order = Order::create($request_data);
$new_order->order_no = 'hk'.Carbon::now()->format('Ymd').$new_order->id;
$new_order->save();
$cat_contents = \Cart::getContent()->sort();
$items=[];
foreach ($cat_contents as $item) {
$OrderItem = new OrderItems();
$OrderItem->order_id = $new_order->id;
$OrderItem->product_id = $item->id;
$OrderItem->qty = $item->quantity;
$OrderItem->price = $item->price;
$OrderItem->save();
$product = Product::find($item->id);
$product_name = $product->title;
$new_ary = [
'name' => $product_name,
'qty' => $item->quantity,
'price' => $item->price,
'unit' => '個'
];
array_push($items, $new_ary);
}
//第三方支付
$formData = [
'UserId' => 1, // 用戶ID , Optional
'ItemDescription' => '產品簡介',
'Items' => $items,
'OrderId' => 'hk'.Carbon::now()->format('Ymd').$new_order->id,
// 'ItemName' => 'Product Name',
// 'TotalAmount' => \Cart::getTotal(),
'PaymentMethod' => 'Credit', // ALL, Credit, ATM, WebATM
];
//清空購物車
\Cart::clear();
return $this->checkout->setNotifyUrl(route('notify'))->setReturnUrl(route('return'))->setPostData($formData)->send();
}
public function notifyUrl(Request $request){
$serverPost = $request->post();
$checkMacValue = $request->post('CheckMacValue');
unset($serverPost['CheckMacValue']);
$checkCode = StringService::checkMacValueGenerator($serverPost);
if ($checkMacValue == $checkCode) {
return '1|OK';
} else {
return '0|FAIL';
}
}
public function returnUrl(Request $request){
$serverPost = $request->post();
$checkMacValue = $request->post('CheckMacValue');
unset($serverPost['CheckMacValue']);
$checkCode = StringService::checkMacValueGenerator($serverPost);
if ($checkMacValue == $checkCode) {
if (!empty($request->input('redirect'))) {
return redirect($request->input('redirect'));
} else {
//付款完成,下面接下來要將購物車訂單狀態改為已付款
//目前是顯示所有資料將其DD出來
dd($this->checkoutResponse->collectResponse($serverPost));
}
}
}
}
```
---
# 權限管理
Step1.新增role欄位
於User Table新增一個新的欄位`role`之後用於管理身分
分為`user`、`admin`、`super_admin`三個身分
可於CreateUser的migration中直接新增以下程式,新增欄位role並將預設值為user
```
$table->string('role')->default('user');
```
Step2.新增Middleware
Step2.1 新增IsAdmin與IsSuperAdmin 兩個MiddleWare
```
php artisan make:middleware IsAdmin
php artisan make:middleware IsSuperAdmin
```
Step2.2 設定app/Http/Kernel.php
在`$routeMiddleware`陣列中,新增
```
'admin' => \App\Http\Middleware\IsAdmin::class,
'super_admin' => \App\Http\Middleware\IsSuperAdmin::class,
```
修改後的陣列如下
```
protected $routeMiddleware = [
'auth' => \App\Http\Middleware\Authenticate::class,
'auth.basic' => \Illuminate\Auth\Middleware\AuthenticateWithBasicAuth::class,
'bindings' => \Illuminate\Routing\Middleware\SubstituteBindings::class,
'cache.headers' => \Illuminate\Http\Middleware\SetCacheHeaders::class,
'can' => \Illuminate\Auth\Middleware\Authorize::class,
'guest' => \App\Http\Middleware\RedirectIfAuthenticated::class,
'password.confirm' => \Illuminate\Auth\Middleware\RequirePassword::class,
'signed' => \Illuminate\Routing\Middleware\ValidateSignature::class,
'throttle' => \Illuminate\Routing\Middleware\ThrottleRequests::class,
'verified' => \Illuminate\Auth\Middleware\EnsureEmailIsVerified::class,
'admin' => \App\Http\Middleware\IsAdmin::class,
'super_admin' => \App\Http\Middleware\IsSuperAdmin::class,
];
```
Step2.3 修改IsAdmin與IsSuperAdmin MiddleWare檔案
IsAdmin MiddleWare
```
public function handle($request, Closure $next)
{
if (Auth::user()->role == "admin" || Auth::user()->role == "super_admin") {
return $next($request);
}
return abort(403, 'Unauthorized action.');
}
```
IsSuperAdmin MiddleWare
```
public function handle($request, Closure $next)
{
if (Auth::user()->role == "super_admin") {
return $next($request);
}
return abort(403, 'Unauthorized action.');
}
```
Step2.3 調整web中 在admin群組中的middlewire
原本的,只要有登入就可以進入到以下頁面
```
Route::group(['prefix' => 'admin', 'middleware' => ['auth']], function(){
...
});
```
--
要修改為 除了登入之外,還是要role為admin或者是super_admin身分才可以
```
//admin
Route::group(['prefix' => 'admin', 'middleware' => ['auth','admin']], function(){
...
});
//super_admin
Route::group(['prefix' => 'admin', 'middleware' => ['auth','super_admin']], function(){
...
});
```
Step3. 新增有權限的帳號
主要是要在後台新增一個帳號管理之頁面
於帳號管理的頁面中顯示所有目前的帳號、刪除帳號、新增帳號
新增帳號要可以選擇權限為`admin`、`super_admin`
Route
```
Route::group(['prefix' => 'admin', 'middleware' => ['auth','super_admin']], function(){
//帳號管理
Route::get('account', 'AccountController@index');
Route::get('account/create', 'AccountController@create');
Route::post('account/store', 'AccountController@store');
Route::post('account/destroy/{id}', 'AccountController@destroy');
...
});
```
Controller
```
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Hash;
class AccountController extends Controller
{
public function index()
{
$items = User::all();
return view('admin.account.index',compact('items'));
}
public function create()
{
return view('admin.account.create');
}
public function store(Request $request)
{
$this->validator($request->all())->validate();
$this->create_account($request->all());
return redirect('/admin/account');
}
public function destroy(Request $request,$id)
{
}
protected function validator(array $data)
{
return Validator::make($data, [
'name' => ['required', 'string', 'max:255'],
'email' => ['required', 'string', 'email', 'max:255', 'unique:users'],
'password' => ['required', 'string', 'min:8', 'confirmed'],
'role' => ['required', 'string'],
]);
}
/**
* Create a new user instance after a valid registration.
*
* @param array $data
* @return \App\User
*/
protected function create_account(array $data)
{
return User::create([
'name' => $data['name'],
'email' => $data['email'],
'role' => $data['role'],
'password' => Hash::make($data['password']),
]);
}
}
```
View - Index
```
@extends('layouts.app')
@section('css')
@endsection
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-12">
<div class="card">
<div class="card-header">帳號管理 - Index</div>
<div class="card-body">
<a class="btn btn-success" href="/admin/account/create">新增帳號</a>
<hr>
<table id="example" class="table table-striped table-bordered" style="width:100%">
<thead>
<tr>
<th>使用者名稱(name)</th>
<th>Email</th>
<th>權限(role)</th>
<th width="120">功能</th>
</tr>
</thead>
<tbody>
@foreach ($items as $item)
<tr>
<td>{{ $item->name}}</td>
<td>{{ $item->email}}</td>
<td>{{ $item->role}}</td>
<td>
<a class="btn btn-danger btn-sm" href="#" data-itemid="{{$item->id}}">刪除</a>
<form class="destroy-form" data-itemid="{{$item->id}}"
action="/admin/news/destroy/{{$item->id}}" method="POST" style="display: none;">
@csrf
</form>
</td>
</tr>
@endforeach
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
@endsection
@section('js')
<script>
$(document).ready(function() {
$('#example').DataTable({
"order": [1,"desc"]
});
$('#example').on('click','.btn-danger',function(){
event.preventDefault();
var r = confirm("你確定要刪除此項目嗎?");
if (r == true) {
var itemid = $(this).data("itemid");
$(`.destroy-form[data-itemid="${itemid}"]`).submit();
}
});
});
</script>
@endsection
```
View - Create
```
@extends('layouts.app')
@section('content')
<div class="container">
<div class="row justify-content-center">
<div class="col-md-8">
<div class="card">
<div class="card-header">Create Account</div>
<div class="card-body">
<form method="POST" action="/admin/account/store">
@csrf
<div class="form-group row">
<label for="name" class="col-md-4 col-form-label text-md-right">{{ __('Name') }}</label>
<div class="col-md-6">
<input id="name" type="text" class="form-control @error('name') is-invalid @enderror" name="name" value="{{ old('name') }}" required autocomplete="name" autofocus>
@error('name')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="email" class="col-md-4 col-form-label text-md-right">{{ __('E-Mail Address') }}</label>
<div class="col-md-6">
<input id="email" type="email" class="form-control @error('email') is-invalid @enderror" name="email" value="{{ old('email') }}" required autocomplete="email">
@error('email')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="role" class="col-md-4 col-form-label text-md-right">Role</label>
<div class="col-md-6">
<select class="form-control" name="role" id="role">
<option value="admin">Admin</option>
<option value="super_admin">SuperAdmin</option>
</select>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-md-4 col-form-label text-md-right">{{ __('Password') }}</label>
<div class="col-md-6">
<input id="password" type="password" class="form-control @error('password') is-invalid @enderror" name="password" required autocomplete="new-password">
@error('password')
<span class="invalid-feedback" role="alert">
<strong>{{ $message }}</strong>
</span>
@enderror
</div>
</div>
<div class="form-group row">
<label for="password-confirm" class="col-md-4 col-form-label text-md-right">{{ __('Confirm Password') }}</label>
<div class="col-md-6">
<input id="password-confirm" type="password" class="form-control" name="password_confirmation" required autocomplete="new-password">
</div>
</div>
<div class="form-group row mb-0">
<div class="col-md-6 offset-md-4">
<button type="submit" class="btn btn-primary">
{{ __('Register') }}
</button>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
</div>
@endsection
```
# 表單機器人驗證
biscolab/laravel-recaptcha
(https://github.com/biscolab/laravel-recaptcha)
(https://laravel-recaptcha-docs.biscolab.com/docs/intro)
---
Installation
```
$ composer require biscolab/laravel-recaptcha
```
Configuration
```
$ php artisan vendor:publish --provider="Biscolab\ReCaptcha\ReCaptchaServiceProvider"
```
Add API Keys to .env file
```
# in your .env file
RECAPTCHA_SITE_KEY=YOUR_API_SITE_KEY
RECAPTCHA_SECRET_KEY=YOUR_API_SECRET_KEY
```
Complete configuration
Open config/recaptcha.php configuration file and set version:
```
return [
'api_site_key' => env('RECAPTCHA_SITE_KEY', ''),
'api_secret_key' => env('RECAPTCHA_SECRET_KEY', ''),
// changed in v4.0.0
'version' => 'v2', // supported: "v3"|"v2"|"invisible"
// @since v3.4.3 changed in v4.0.0
'curl_timeout' => 10,
'skip_ip' => [], // array of IP addresses - String: dotted quad format e.g.: "127.0.0.1"
// @since v3.2.0 changed in v4.0.0
'default_validation_route' => 'biscolab-recaptcha/validate',
// @since v3.2.0 changed in v4.0.0
'default_token_parameter_name' => 'token',
// @since v3.6.0 changed in v4.0.0
'default_language' => null,
// @since v4.0.0
'default_form_id' => 'biscolab-recaptcha-invisible-form', // Only for "invisible" reCAPTCHA
// @since v4.0.0
'explicit' => false, // true|false
// @since v4.0.0
'tag_attributes' => [
'theme' => 'light', // "light"|"dark"
'size' => 'normal', // "normal"|"compact"
'tabindex' => 0,
'callback' => null, // DO NOT SET "biscolabOnloadCallback"
'expired-callback' => null, // DO NOT SET "biscolabOnloadCallback"
'error-callback' => null, // DO NOT SET "biscolabOnloadCallback"
]
];
```
# mail發送
### 安裝
```
composer require guzzlehttp/guzzle
```
.envfile
```
MAIL_DRIVER=smtp
MAIL_HOST=smtp.gmail.com
MAIL_PORT=465
MAIL_USERNAME=teacherTest0929@gmail.com
MAIL_PASSWORD=QWEasdzxc0929
MAIL_ENCRYPTION=SSL
```
MAIL_DRIVER是使用的服務 預設smtp
google stmp服務的細項 ->Gmail SMTP 伺服器需求:https://support.google.com/a/answer/176600?hl=zh-Hant
MAIL_USERNAME:要發送郵件的帳號
MAIL_PASSWORD:郵件的密碼
記得開安全性權限:https://blog.user.today/gmail-smtp-authentication-required/
```
php artisan make:mail OrderShipped --markdown=emails.orders.shipped
```
.config/mail.php
```
'from' => [
'address' => env('MAIL_FROM_ADDRESS', 'teacherTest0929@gmail.com'),
'name' => env('MAIL_FROM_NAME', '測試用'),
],
```
!!!!記得import class
在要送mail的地方下
```
public function store(Request $request)
{
Mail::to($content->email)->send(new OrderShipped($content));
//to(放收件人email)
}
```
### 更改信件樣式
改信件的header footer
Ctrl+p +p 搜尋 footer.blade.php header.blade.php 就可以修改標題及頁尾
將表單傳送過來的資料塞進信箱中
Mail.OrderShipped.php
```
public function __construct(ContentUs $content)
{
//ContentUs 是model名稱 記得import
$this->content = $content;
//這邊變數要與下面的with變數來源相同
}
/**
* Build the message.
*
* @return $this
*/
public function build()
{
return $this->subject('感謝你的來信')->markdown('emails.orders.shipped')->with('content',$this->content);
//$message->subject($subject); (定義信件標題)
//$message->attach($pathToFile, array $options = []); (寄送附件)
//$message->with(變數名稱, 變數來源) (上方__construct預先定義的資料庫內容,引入何種資料庫在constuct定義,並無限制)
}
```
# 購物車前置作業->訂單產生 (要有產品頁)
流程:進入產品頁->按下購買->購買產品頁(選擇數量以及規格)
->加進購物車後->到結帳頁面填寫收件人資料->在建立訂單。
### 建立說明
此流程只教學建立訂單的頁面。
建立兩張資料表 一張order(主要訂單)
一張order_detial(訂單明細)
```
php artisan make:migration create_orders
php artisan make:migration create_orders_detial
```
order資料表:
```
public function up()
{
Schema::create('orders', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('user_id')->nullable();
$table->string('Recipient_name');
$table->string('Recipient_phone');
$table->string('Recipient_address');
$table->string('shipment_time')->default('不指定');
$table->string('totalPrice');
$table->string('ship_status')->default('未結帳');
$table->string('Purchase_status')->default('未送達');
$table->timestamps();
});
}
```
user_id:當有登入時 存入userID
Recipient_name:收件人名子
Recipient_phone:收件人電話
Recipient_address:收件人地址
shipment_time:送達時間
totalPrice:購物車產品總金額
ship_status:預設未結帳
Purchase_status:預設未送達
order_detial
```
public function up()
{
Schema::create('orders_detail', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('order_id');
$table->string('product_id');
$table->string('qty');
$table->string('price');
$table->timestamps();
});
}
```
order_id:訂單的ID
product_id:購買產品的ID (一對多)一張訂單可能有好幾個產品
qty:產品數量
price:產品價格
示意圖

!!記得設定好按鈕的action 以及對應的web.php 裡的EX: route:get/post('/cart_checkout',Frontcontroller@cart_check)
Frontcontroller裡面的
### 建立主要與次要訂單流程
```
public function post_cart_check(Request $request)
{
// $sessionKey = Auth::id();
// $items = \Cart::session($sessionKey)->getContent();
// dd($items);
$Recipient_name = $request->Recipient_name;
$Recipient_phone = $request->Recipient_phone;
$Recipient_address = $request->Recipient_address;
$shipment_time = $request->shipment_time;
$order = new Order();
$sessionKey = Auth::id();
//主要訂單建立
$order->user_id= $sessionKey;
$order->Recipient_name = $Recipient_name;
$order->Recipient_phone= $Recipient_phone;
$order->Recipient_address = $Recipient_address;
$order->shipment_time = $shipment_time;
$order->totalPrice = \Cart::session($sessionKey)->getTotal();
$order->save();
//訂單詳細建立
$items = \Cart::session($sessionKey)->getContent();
foreach($items as $row) {
$order_detial= new Order_detail();
$order_detial->order_id = $order->id;
$order_detial->product_id= $row->id;
$order_detial->qty = $row->quantity;
$order_detial->price =$row->price;
$order_detial->save();
}
}
```
```$Recipient_name = $request->Recipient_name;```
//將from表單傳送過來的收件人姓名用變數Recipient_name存起來
```$Recipient_phone = $request->Recipient_phone;```
//將from表單傳送過來的收件人電話用變數Recipient_phone存起來
```$Recipient_address = $request->Recipient_address;```
//將from表單傳送過來的收件人地址用變數Recipient_address存起來
```$shipment_time = $request->shipment_time;```
//將from表單傳送過來的收貨時間用變數shipment_time存起來
```$order = new Order();```//使用order這個模型 用$order變數
```$sessionKey = Auth::id();```//如果有登入的話可以直接使用使用者ID
$order->(根據資料庫欄位填寫)= 相對應的值
!!!記得save 在結尾的地方!!!
$order->save();
```$items = \Cart::session($sessionKey)->getContent();```這段是上面購物車的內容
將購物車每一筆的產品存入items
若選擇不用登入的方式 改寫下面寫法
$items = \Cart::getContent();
使用foreach將items裡面的每個產品變成$row的變數代入
Ex兩樣產品做兩次
//```$order_detial= new Order_detail();``` 使用Order_detail的Model存入$order_detial
注意 $order_detial->order_id = $order->id; 這裡的$order_id 是從上面訂單建立後才有的訂單編號注意Create順序
在model中要關聯兩個資料表 由於orders可以有許多orders_detail的產品細項 所以是一對多的概念
在order的MOdel中要使用下列語法關聯
```
public function order_detail()
{
return $this->hasMany('App\Order_detail');
}
```
預設是不用更改forgin_key local_key
訂單在資料庫的示意圖 連結:https://i.imgur.com/tYztdpA.png
