1. Mô tả đề tài
Mục tiêu: Xây dựng một trang thương mại điện tử có tính năng hiện đại, dễ sử dụng và an toàn, nhằm cung cấp một nền tảng trực tuyến để người dùng có thể mua bán các sản phẩm đa dạng.
2. Công nghệ sử dụng
- Laravel: Framework PHP mạnh mẽ và linh hoạt, giúp xây dựng các ứng dụng web nhanh chóng với cấu trúc MVC (Model-View-Controller) rõ ràng.
- MySQL: Hệ quản trị cơ sở dữ liệu quan hệ phổ biến, dễ sử dụng và có hiệu suất cao.
- HTML/CSS/JavaScript: Các công nghệ front-end để xây dựng giao diện người dùng.
- Blade: Template engine của Laravel để tạo giao diện người dùng động.
3. Chức năng chính
- Đăng ký và đăng nhập người dùng
- Đăng ký tài khoản với email xác thực.
- Đăng nhập bằng email và mật khẩu.
- Quản lý sản phẩm
- Thêm, sửa, xóa sản phẩm (dành cho admin).
- Hiển thị danh sách sản phẩm theo danh mục.
- Tìm kiếm và lọc sản phẩm.
- Giỏ hàng và thanh toán
- Thêm sản phẩm vào giỏ hàng.
- Cập nhật số lượng hoặc xóa sản phẩm khỏi giỏ hàng.
- Thanh toán qua các phương thức thanh toán phổ biến.
- Quản lý đơn hàng
- Xem lịch sử đơn hàng của người dùng.
- Quản lý trạng thái đơn hàng (admin).
- Quản lý danh mục sản phẩm
- Thêm, sửa, xóa danh mục sản phẩm (admin).
- Tích hợp API
- Tích hợp API để cập nhật giá và tình trạng hàng từ các nhà cung cấp.
4. Thiết kế cơ sở dữ liệu
- Bảng users: Lưu thông tin người dùng (id, name, email, password, role, created_at, updated_at).
- Bảng products: Lưu thông tin sản phẩm (id, name, description, price, category_id, created_at, updated_at).
- Bảng categories: Lưu thông tin danh mục sản phẩm (id, name, created_at, updated_at).
- Bảng orders: Lưu thông tin đơn hàng (id, user_id, code, status, created_at, updated_at).
- Bảng order_details: Lưu thông tin chi tiết đơn hàng (id, order_id, product_id, quantity, price, created_at, updated_at).
5. Mô hình dữ liệu Database
7.Model
Customer :
<?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class Customer extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', 'phone', 'address', 'gender' ]; public function carts() { return $this->hasMany(Cart::class, 'customer_id', 'id'); } public function orders() { return $this->hasMany(Order::class, 'customer_id', 'id')->orderBy('id','DESC'); } /** * The attributes that should be hidden for serialization. * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast. * * @var array<string, string> */ protected $casts = [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; public function favorites() { return $this->hasMany(Favorite::class, 'customer_id', 'id'); } }
User:
<?php namespace App\Models; // use Illuminate\Contracts\Auth\MustVerifyEmail; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Notifications\Notifiable; use Laravel\Sanctum\HasApiTokens; class User extends Authenticatable { use HasApiTokens, HasFactory, Notifiable; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'password', ]; /** * The attributes that should be hidden for serialization. * * @var array<int, string> */ protected $hidden = [ 'password', 'remember_token', ]; /** * The attributes that should be cast. * * @var array<string, string> */ protected $casts = [ 'email_verified_at' => 'datetime', 'password' => 'hashed', ]; }
Banner:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Banner extends Model { use HasFactory; protected $fillable = ['name', 'link', 'image', 'position', 'description', 'prioty', 'status']; public function scopeGetBanner($q, $pos = 'top-banner') { $q = $q->where('position', $pos) ->where('status', 1)->orderBy('prioty', 'ASC'); return $q; } }
Cart:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Cart extends Model { use HasFactory; public $timestamps = false; protected $fillable = ['customer_id', 'product_id', 'price', 'quantity']; public function prod() { return $this->hasOne(Product::class, 'id', 'product_id'); } }
Category:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Category extends Model { use HasFactory; protected $fillable = ['name', 'status']; protected $hidden = ['created_at','updated_at']; // 1 - n public function products() { return $this->hasMany(Product::class, 'category_id','id')->orderBy('created_at','DESC'); } }
Order:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Order extends Model { use HasFactory; protected $appends = ['totalPrice']; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'name', 'email', 'phone', 'address', 'token', 'customer_id', 'status' ]; public function customer() { return $this->hasOne(Customer::class, 'id', 'customer_id'); } public function details() { return $this->hasMany(OrderDetail::class, 'order_id', 'id'); } public function getTotalPriceAttribute() { $t = 0; foreach($this->details as $item) { $t += $item->price * $item->quantity; } return $t; } }
Product:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class Product extends Model { use HasFactory; protected $appends = ['favorited']; protected $fillable = ['name', 'price','sale_price', 'image', 'category_id', 'description', 'status']; // 1-1 public function cat() { return $this->hasOne(Category::class, 'id','category_id'); } // 1-n public function images() { return $this->hasMany(ProductImage::class, 'product_id','id'); } public function getFavoritedAttribute() { $favorited = Favorite::where(['product_id' => $this->id, 'customer_id' => auth('cus')->id()])->first(); return $favorited ? true : false; } }
Order_detail:
<?php namespace App\Models; use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Model; class OrderDetail extends Model { use HasFactory; public $timestamps = false; /** * The attributes that are mass assignable. * * @var array<int, string> */ protected $fillable = [ 'order_id', 'product_id', 'price', 'quantity' ]; public function product() { return $this->hasOne(Product::class, 'id', 'product_id'); } }
8.Controller
Admin:
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use Illuminate\Http\Request; class AdminController extends Controller { public function index() { return view('admin.index'); } public function login() { return view('admin.login'); } public function check_login(Request $req) { $req->validate([ 'email' => 'required|email|exists:users', 'password' => 'required' ]); $data = $req->only('email','password'); $check = auth()->attempt($data); if ($check) { return redirect()->route('admin.index')->with('ok','Welcom Back'); } return redirect()->back()->with('no','Your email Or Password is not match'); } public function logout() { auth()->logout(); return redirect()->route('admin.login')->with('no','Logouted'); } }
CategoryController:
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Category; use Illuminate\Http\Request; class CategoryController extends Controller { /** * Display a listing of the resource. */ public function index() { return view('admin.category.index'); } /** * Show the form for creating a new resource. */ public function create() { return view('admin.category.create'); } /** * Store a newly created resource in storage. */ public function store(Request $request) { // } /** * Display the specified resource. */ public function show(Category $category) { // } /** * Show the form for editing the specified resource. */ public function edit(Category $category) { return view('admin.category.edit'); } /** * Update the specified resource in storage. */ public function update(Request $request, Category $category) { // } /** * Remove the specified resource from storage. */ public function destroy(Category $category) { // } }
ProductController:
<?php namespace App\Http\Controllers\Admin; use App\Http\Controllers\Controller; use App\Models\Category; use App\Models\Product; use App\Models\ProductImage; use Illuminate\Http\Request; class ProductController extends Controller { /** * Display a listing of the resource. */ public function index() { $data = Product::orderBy('id','DESC')->paginate(20); return view('admin.product.index', compact('data')); } /** * Show the form for creating a new resource. */ public function create() { $cats = Category::orderBy('name','ASC')->select('id','name')->get(); return view('admin.product.create', compact('cats')); } /** * Store a newly created resource in storage. */ public function store(Request $request) { $request->validate([ 'name' => 'required|min:4|max:150|unique:products', 'description' => 'required|min:4', 'price' => 'required|numeric', 'sale_price' => 'numeric|lte:price', 'img' => 'required|file|mimes:jpg,jpeg,png,gif', 'category_id' => 'required|exists:categories,id' ]); $data = $request->only('name','price','sale_price','status','description','category_id'); $imag_name = $request->img->hashName(); $request->img->move(public_path('uploads/product'), $imag_name); $data['image'] = $imag_name; if ($product = Product::create($data)) { if($request->has('other_img')) { foreach($request->other_img as $img){ $other_name = $img->hashName(); $img->move(public_path('uploads/product'), $other_name); ProductImage::create([ 'image' => $other_name, 'product_id' => $product->id ]); } } return redirect()->route('product.index')->with('ok','Create new product successffuly'); } return redirect()->back()->with('no','Something error, Please try again'); } /** * Display the specified resource. */ public function show(Product $product) { // } /** * Show the form for editing the specified resource. */ public function edit(Product $product) { $cats = Category::orderBy('name','ASC')->select('id','name')->get(); return view('admin.product.edit', compact('cats','product')); } /** * Update the specified resource in storage. */ public function update(Request $request, Product $product) { $request->validate([ 'name' => 'required|min:4|max:150|unique:products,name,'.$product->id, 'description' => 'required|min:4', 'price' => 'required|numeric', 'sale_price' => 'numeric|lte:price', 'img' => 'file|mimes:jpg,jpeg,png,gif', 'category_id' => 'required|exists:categories,id' ]); $data = $request->only('name','price','sale_price','status','description','category_id'); if ($request->has('img')) { $img_name = $product->image; $image_path = public_path('uploads/product').'/'.$img_name; if (file_exists($image_path)) { unlink($image_path); } $imag_name = $request->img->hashName(); $request->img->move(public_path('uploads/product'), $imag_name); $data['image'] = $imag_name; } if ($product->update($data)) { if($request->has('other_img')) { if ($product->images->count() > 0) { foreach($product->images as $img) { $othe_image = $img->image; $other_path = public_path('uploads/product').'/'.$othe_image; if (file_exists($other_path)) { unlink($other_path); } } ProductImage::where('product_id', $product->id)->delete(); } foreach($request->other_img as $img){ $other_name = $img->hashName(); $img->move(public_path('uploads/product'), $other_name); ProductImage::create([ 'image' => $other_name, 'product_id' => $product->id ]); } } return redirect()->route('product.index')->with('ok','UIpdate the product successffuly'); } return redirect()->back()->with('no','Something error, Please try again'); } /** * Remove the specified resource from storage. */ public function destroy(Product $product) { $img_name = $product->image; $image_path = public_path('uploads/product').'/'.$img_name; if ($product->images->count() > 0) { foreach($product->images as $img) { $othe_image = $img->image; $other_path = public_path('uploads/product').'/'.$othe_image; if (file_exists($other_path)) { unlink($other_path); } } ProductImage::where('product_id', $product->id)->delete(); if ($product->delete()) { if (file_exists($image_path)) { unlink($image_path); } return redirect()->route('product.index')->with('ok','Delete product successffuly'); } } else { if ($product->delete()) { if (file_exists($image_path)) { unlink($image_path); } return redirect()->route('product.index')->with('ok','Delete product successffuly'); } } return redirect()->back()->with('no','Something error, Please try again'); } public function destroyImage(ProductImage $image) { $img_name = $image->image; if ($image->delete()) { $image_path = public_path('uploads/product').'/'.$img_name; if (file_exists($image_path)) { unlink($image_path); } return redirect()->back()->with('ok','Delete Image successffuly'); } return redirect()->back()->with('no','Something error, Please try again'); } }
ApiCategoryController:
<?php namespace App\Http\Controllers\Api; use App\Http\Controllers\Controller; use Illuminate\Http\Request; use App\Models\Category; use Str; class ApiCategoryController extends Controller { public function index() { $cats = Category::all(); return response()->json($cats); } public function destroy(Category $category) { return [ 'success' => $category->delete() ]; } public function store() { $data = request()->all('name','status'); return Category::create($data); } public function show(Category $category) { return $category; } public function update(Category $category) { $data = request()->all('name','status'); return [ 'success' => $category->update($data) ]; } public function login(Request $req) { $data = $req->only('email','password'); $check = auth()->attempt($data); if ($check) { // tạo mã token gửi về cho client $user = auth()->user(); $token = $user->createToken(Str::slug($user->name)); return [ 'token' => $token->plainTextToken ]; } return ['success' => false]; } }
AccountController:
<?php namespace App\Http\Controllers; use App\Mail\ForgotPassword; use App\Mail\VerifyAccount; use App\Models\Customer; use App\Models\CustomerResetToken; use Illuminate\Auth\Notifications\VerifyEmail; use Illuminate\Http\Request; use Mail; use Hash; class AccountController extends Controller { public function login() { return view('account.login'); } public function favorite() { $favorites = auth('cus')->user()->favorites ? auth('cus')->user()->favorites : []; return view('account.favorite', compact('favorites')); } public function logout() { auth('cus')->logout(); return redirect()->route('account.login')->with('ok','Your logouted'); } public function check_login(Request $req) { $req->validate([ 'email' => 'required|exists:customers', 'password' => 'required' ]); $data = $req->only('email','password'); $check = auth('cus')->attempt($data); if ($check) { if (auth('cus')->user()->email_verified_at == '') { auth('cus')->logout(); return redirect()->back()->with('no','You account is not verify, please check email again'); } return redirect()->route('home.index')->with('ok','Welcome back'); } return redirect()->back()->with('no','Your account or password invalid'); } public function register() { return view('account.register'); } public function check_register(Request $req) { $req->validate([ 'name' => 'required|min:6|max:100', 'email' => 'required|email|min:6|max:100|unique:customers', 'password' => 'required|min:4', 'confirm_password' => 'required|same:password', ], [ 'name.required' => 'Họ tên không được để tróng', 'name.min' => 'Họ ten tối thiểu là 6 ký tự' ]); $data = $req->only('name','email','phone','address','gender'); $data['password'] = bcrypt($req->password); if ($acc = Customer::create($data) ) { Mail::to($acc->email)->send(new VerifyAccount($acc)); return redirect()->route('account.login')->with('ok','Register successfully, please check your email to verify account'); } return redirect()->back()->with('no','Smething error, please try again'); } public function veryfy($email) { $acc = Customer::where('email', $email)->whereNUll('email_verified_at')->firstOrFail(); Customer::where('email', $email)->update(['email_verified_at' => date('Y-m-d')]); return redirect()->route('account.login')->with('ok','erify account successfully, Now you can login'); } public function change_password() { return view('account.change_password'); } public function check_change_password(Request $req) { $auth = auth('cus')->user(); $req->validate([ 'old_password' => [ 'required', function($attr, $value, $fail) use($auth) { if (!Hash::check($value, $auth->password) ) { $fail('Your password is not match'); } }], 'password' => 'required|min:4', 'confirm_password' => 'required|same:password' ]); $data['password'] = bcrypt($req->password); $check = $auth->update($data); if ($check) { auth('cus')->logout(); return redirect()->route('account.login')->with('ok','Update your password successfuly'); } return redirect()->back()->with('no','Something error, please check agian'); } public function forgot_password() { return view('account.forgot_password'); } public function check_forgot_password(Request $req) { $req->validate([ 'email' => 'required|exists:customers' ]); $customer = Customer::where('email', $req->email)->first(); $token = \Str::random(50); $tokenData = [ 'email' => $req->email, 'token' => $token ]; if (CustomerResetToken::create($tokenData)) { Mail::to($req->email)->send(new ForgotPassword($customer, $token)); return redirect()->back()->with('ok','Send email successfully, please check email to continue'); } return redirect()->back()->with('no','Something error, please check agian'); } public function profile() { $auth = auth('cus')->user(); return view('account.profile', compact('auth')); } public function check_profile(Request $req) { $auth = auth('cus')->user(); $req->validate([ 'name' => 'required|min:6|max:100', 'email' => 'required|email|min:6|max:100|unique:customers,email,'.$auth->id, 'password' => ['required', function($attr, $value, $fail) use($auth) { if (!Hash::check($value, $auth->password)) { return $fail('Your password í not mutch'); } }], ], [ 'name.required' => 'Họ tên không được để tróng', 'name.min' => 'Họ ten tối thiểu là 6 ký tự' ]); $data = $req->only('name','email','phone','address','gender'); $check = $auth->update($data); if ($check) { return redirect()->back()->with('ok','Update your profile successfuly'); } return redirect()->back()->with('no','Something error, please check agian'); } public function reset_password($token) { $tokenData = CustomerResetToken::checkToken($token); return view('account.reset_password'); } public function check_reset_password($token) { request()->validate([ 'password' => 'required|min:4', 'confirm_password' => 'required|same:password' ]); $tokenData = CustomerResetToken::checkToken($token); $customer = $tokenData->customer; $data = [ 'password' => bcrypt(request(('password'))) ]; $check = $customer->update($data); if ($check) { return redirect()->back()->with('ok','Update your password successfuly'); } return redirect()->back()->with('no','Something error, please check agian'); } }
CartController:
<?php namespace App\Http\Controllers; use App\Models\Product; use App\Models\Cart; use Illuminate\Http\Request; class CartController extends Controller { public function index() { return view('home.cart'); } public function add(Product $product, Request $req) { $quantity = $req->quantity ? floor($req->quantity) : 1; $cus_id = auth('cus')->id(); $cartExist = Cart::where([ 'customer_id' => $cus_id, 'product_id' => $product->id ])->first(); // dd ($cartExist); if ($cartExist) { Cart::where([ 'customer_id' => $cus_id, 'product_id' => $product->id ])->increment('quantity', $quantity); // $cartExist->update([ // 'quantity' => $cartExist->quantity + $quantity // ]); return redirect()->route('cart.index')->with('ok','Update product quantity in cart successfully');; } else { $data = [ 'customer_id' => auth('cus')->id(), 'product_id' => $product->id, 'price' => $product->sale_price ? $product->sale_price : $product->price, 'quantity' => $quantity ]; if (Cart::create($data)) { return redirect()->route('cart.index')->with('ok','Add product to cart successfully');; } } return redirect()->back()->with('no','Something error, please try again'); } public function update(Product $product, Request $req) { $quantity = $req->quantity ? floor($req->quantity) : 1; $cus_id = auth('cus')->id(); $cartExist = Cart::where([ 'customer_id' => $cus_id, 'product_id' => $product->id ])->first(); if ($cartExist) { Cart::where([ 'customer_id' => $cus_id, 'product_id' => $product->id ])->update([ 'quantity' => $quantity ]); return redirect()->route('cart.index')->with('ok','Update product quantity in cart successfully');; } return redirect()->back()->with('no','Something error, please try again'); } public function delete($product_id) { $cus_id = auth('cus')->id(); Cart::where([ 'customer_id' => $cus_id, 'product_id' => $product_id ])->delete(); return redirect()->back()->with('ok','Deleted product in shopping cart'); } public function clear() { $cus_id = auth('cus')->id(); Cart::where([ 'customer_id' => $cus_id ])->delete(); return redirect()->back()->with('ok','Deleted all product in shopping cart'); } }
CheckoutController:
<?php namespace App\Http\Controllers; use App\Mail\OrderMail; use App\Models\Order; use App\Models\OrderDetail; use Illuminate\Http\Request; use Mail; class CheckoutController extends Controller { public function checkout() { $auth = auth('cus')->user(); return view('home.checkout', compact('auth')); } public function history() { $auth = auth('cus')->user(); return view('home.history', compact('auth')); } public function detail(Order $order) { $auth = auth('cus')->user(); return view('home.detail', compact('auth','order')); } public function post_checkout(Request $req) { $auth = auth('cus')->user(); $req->validate([ 'name' => 'required', 'email' => 'required|email', 'phone' => 'required', 'address' => 'required' ]); $data = $req->only('name','email','phone','address'); $data['customer_id'] = $auth->id; if($order = Order::create($data)) { $token = \Str::random(40); foreach($auth->carts as $cart) { $data1 = [ 'order_id' => $order->id, 'product_id' => $cart->product_id, 'price' => $cart->price, 'quantity' => $cart->quantity ]; OrderDetail::create($data1); } // $auth->carts()->delete(); $order->token = $token; $order->save(); Mail::to($auth->email)->send(new OrderMail($order, $token)); // guwir email xac nhan return redirect()->route('home.index')->with('ok','Order checkout successfully'); } return redirect()->route('home.index')->with('no','Something orror, please try again'); } public function verify($token) { $order = Order::where('token', $token)->first(); if ($order) { $order->token = null; $order->status = 1; $order->save(); return redirect()->route('home.index')->with('ok','Order verify successfully'); } return abort(404); } }
HomeController:
<?php namespace App\Http\Controllers; use App\Models\Banner; use App\Models\Favorite; use App\Models\Category; use App\Models\Product; use Illuminate\Http\Request; class HomeController extends Controller { public function index() { $topBanner = Banner::getBanner()->first(); $gallerys = Banner::getBanner('gallery')->get(); $news_products = Product::orderBy('created_at', 'DESC')->limit(2)->get(); $sale_products = Product::orderBy('created_at', 'DESC')->where('sale_price','>', 0)->limit(3)->get(); $feature_products = Product::inRandomOrder()->limit(4)->get(); // dd ($news_products); return view('home.index', compact('topBanner','gallerys','news_products','sale_products','feature_products')); } public function about() { return view('home.about'); } public function category (Category $cat) { // $products = Product::where('category_id', $cat->id)->get(); $products = $cat->products()->paginate(9); $news_products = Product::orderBy('created_at', 'DESC')->limit(3)->get(); return view('home.category', compact('cat','products','news_products')); } public function product (Product $product) { $products = Product::where('category_id', $product->category_id)->limit(12)->get(); return view('home.product', compact('product','products')); } public function favorite ($product_id) { $data = [ 'product_id' => $product_id, 'customer_id' => auth('cus')->id() ]; $favorited = Favorite::where(['product_id' => $product_id, 'customer_id' => auth('cus')->id()])->first(); if($favorited) { $favorited->delete(); return redirect()->back()-> with('ok','Bạn đã bỏ yêu thích sản phẩm'); } else { Favorite::create($data); return redirect()->back()-> with('ok','Bạn đã yêu thích sản phẩm'); } } }
OrderController:
<?php namespace App\Http\Controllers; use App\Models\Order; use Illuminate\Http\Request; class OrderController extends Controller { public function index() { $status = request('status', 1); $orders = Order::orderBy('id','DESC')->where('status', $status)->paginate(); return view('admin.order.index', compact('orders')); } public function show(Order $order) { $auth = $order->customer; return view('admin.order.detail', compact('auth','order')); } public function update(Order $order) { $status = request('status', 1); if ($order->status != 2) { $order->update(['status' => $status]); return redirect()->route('order.index')->with('ok', 'Cập nhật trạng thái thanh công'); } return redirect()->route('order.index')->with('no', 'Không thể cập nhật đơn hàng đã giao'); } }
Controller:
<?php namespace App\Http\Controllers; use Illuminate\Foundation\Auth\Access\AuthorizesRequests; use Illuminate\Foundation\Validation\ValidatesRequests; use Illuminate\Routing\Controller as BaseController; class Controller extends BaseController { use AuthorizesRequests, ValidatesRequests; }
9.View
Thành Viên Nhóm:
1.Hoàng Đình Hiến
2.Trần Minh Dũng
3.Trần Nguyễn Đăng Kha
Link github: https://github.com/hoangdinhhien/doanlaravel