Building a Shopping Cart in Ruby on Rails

Bài hướng dẫn này sẽ giúp bạn làm thế nào để xây dựng một giỏ hàng đúng cách trong các hệ thống mua sắm online bằng Ruby on rails.

Introduction

Một câu hỏi được đặt ra khi cần xây dựng các hệ thống mua sắm online đó là xây dựng giỏ hàng. Giỏ hàng ở đây được hiểu là nơi lưu lại tạm thời các sản phẩm được người dùng chọn trước khi được thanh toán. Trong bài viết này, chúng ta sẽ xây dựng một giỏ hàng đơn giản có thể dễ dàng mở rộng với những yêu cần thêm của các dự án khác nhau.

Rails application setup.

Model

Điều đầu tiên chúng ta cần làm đó là tạo models. Đối với bài toán dạng này chúng ta sẽ cần tạo ra 4 models cơ bản sau.

  1. Product: Chứa thông tin sản phẩm được bán.
  2. Order Status: Chứa trạng thái của một đơn đặt hàng. Ví dụ như là: “Inprogress” thể hiện giỏ hàng đang trong quá trình mua sắm chưa được thanh toán, “Completed” thể hiện giỏ hàng đã được thanh toán và kết thúc phiên mua hàng.
  3. Order: Chứa thông tin của một phiên mua hàng. Những việc như là tính toán, lưu trữ sẽ được sử lý tại đây.
  4. OrderItem: Chứa thông tin chi tiết các sản phẩm đã được mua.

Thực hiện các dòng lệnh như dưới đây để sinh ra các models cần thiết.

rails g model Product name 'price:decimal{12,3}' active:boolean
rails g model OrderStatus name:string
rails g model Order 'subtotal:decimal{12,3}' 'tax:decimal{12,3}' 'shipping:decimal{12,3}' 'total:decimal{12,3}' order_status:references
rails g model OrderItem product:references order:references 'unit_price:decimal{12,3}' quantity:integer 'total_price:decimal{12,3}'
rake db:migrate

Tiếp theo chúng ta cần tạo ra một số dữ liệu dùng để test. Thay đổi nội dung file db/seeds như sau.

Product.delete_all
Product.create! id: 1, name: "Banana", price: 0.49, active: true
Product.create! id: 2, name: "Apple", price: 0.29, active: true
Product.create! id: 3, name: "Strawberries", price: 1.99, active: true

OrderStatus.delete_all
OrderStatus.create! id: 1, name: "In Progress"
OrderStatus.create! id: 2, name: "Placed"
OrderStatus.create! id: 3, name: "Shipped"
OrderStatus.create! id: 4, name: "Cancelled"

Bây giờ hãy chạy lệnh rake db:seed để những dữ liệu trên được sinh ra trong database.

rake db:seed

Tiếp theo chúng ta cần thêm 1 số dòng code trong các file model. Tôi sẽ giải thích cho bạn mục đích của việc thay đổi dưới đây. app/models/order.rb:

class Order < ActiveRecord::Base
  belongs_to :order_status
  has_many :order_items
  before_create :set_order_status
  before_save :update_subtotal

  def subtotal
    order_items.collect { |oi| oi.valid? ? (oi.quantity * oi.unit_price) : 0 }.sum
  end
private
  def set_order_status
    self.order_status_id = 1
  end

  def update_subtotal
    self[:subtotal] = subtotal
  end
end

Trong model order, 2 dòng đầu tiên chúng ta đã thiết lập các quan hệ 1 order thuộc về 1 order_status và 1 order sẽ có nhiều order_items. 2 dòng tiếp theo chúng ta thành lập 2 cơ chế callbacks. (kiểm soát sự hợp lệ của dữ liệu) Hàm set_order_status thực hiện việc set order_status_id của đối tượng order khi vừa được khởi tạo = 1. Thể hiện phiên mua hàng đang trong quá trình thêm hàng vào giỏ. Hàm update subtotal thực hiện việc lưu tổng giá trị của đơn hàng. (Giá trị này sẽ được lưu trữ trong trường subtotal của đối tượng order). Hàm subtotal trả về tổng giá trị của đơn hàng và là một phương thức public.

Tiếp theo chúng ta sẽ thêm code cho OrderItem model. app/models/order_item.rb:

class OrderItem < ActiveRecord::Base
  belongs_to :product
  belongs_to :order

  validates :quantity, presence: true, numericality: { only_integer: true, greater_than: 0 }
  validate :product_present
  validate :order_present

  before_save :finalize

  def unit_price
    if persisted?
      self[:unit_price]
    else
      product.price
    end
  end

  def total_price
    unit_price * quantity
  end

private
  def product_present
    if product.nil?
      errors.add(:product, "is not valid or is not active.")
    end
  end

  def order_present
    if order.nil?
      errors.add(:order, "is not a valid order.")
    end
  end

  def finalize
    self[:unit_price] = unit_price
    self[:total_price] = quantity * self[:unit_price]
  end
end

Trong model order_item , 2 dòng đầu tiên chúng ta cũng thiết lập các quan hệ. 1 order_item thuộc về 1 product và 1 order. Tiếp theo chúng ta sẽ thêm các điều kiện hợp lệ của các thuộc tính. Đảm bảo rằng quantity phải là một số nguyên lớn hơn 0. 2 điều kiện tiếp theo để đảm bảo rằng sản phẩm hiện tại và phiên mua hàng hiện tại là hợp lệ. Hàm unit_price sẽ trả về giá tiền của sản phẩm (Order_item). Việc lưu lại unit_price rất có ý nghĩa trong logic sử lý việc thanh toán giỏ hàng. Tại thời điểm người dùng thêm hàng vào giỏ, giá tiền của sản phẩm khi đó trong bảng product sẽ được lưu lại trong trường unit_price của đối tượng order_item. Điều này có nghĩa là sau khi sản phẩm được thêm vào giỏ nếu sản phẩm thay đổi giá thì người dùng vẫn có thể mua sản phẩm ở mức giá trước đó (tại thời điểm thêm vào giỏ hàng). Điều này là cần thiết bởi vì nếu giá cả thay đổi trong khi người dùng đang sử dụng thì có thể có sự không khớp giữa giá sản phẩm bên trong giỏ hàng và giá sản phẩm trên trang thanh toán. Bạn luôn có thể sửa đổi mã này để phù hợp với nhu cầu riêng của bạn. Hàm finalize sẽ được gọi đến mỗi khi lưu và update thông tin trong 2 trường unit_price và total_price với mục đích là tính toán lại 2 gía trị này. Với unit_price sẽ được lấy theo giá tiền của product tại thời điểm lưu.

Tiếp theo chúng ta cần xét quan hệ trong OrderStatus, và Product app/models/order_status.rb:

class OrderStatus < ActiveRecord::Base
  has_many :orders
end

app/models/product.rb:

class Product < ActiveRecord::Base
  has_many :order_items

  default_scope { where(active: true) }
end

Scope default_scope sẽ trả lại những sản phẩm đang đượcc set trạng thái active là true. Điều này giúp đảm bảo rằng những sản phẩm đang bị deactive hoặc bị đánh dấu là xoá sẽ chắc chắn không được hiển thị.

Ok. Bây giờ chúng ta đã xong đối với các model. Tiếp theo chúng ta sẽ viết thêm code cho các controller.

Controller

Trong ví dụ này chúng ta sẽ tạo ra 3 controller. Products controller giúp hiển thị list các sản phẩm đang bán. Carts controller sẽ hiển thị nội dung của giỏ hàng. Cuối cùng à OrderItems controller sẻ quản lý các công việc liên quan đến các item trong giỏ hàng (xoá, thay đổi số lượng hàng hoá muốn mua).

Chạy các dòng lệnh sau trên terminal để sinh ra các controller cần thiết.

rails g controller Products index
rails g controller Carts show
rails g controller OrderItems create update destroy

Tiếp theo chúng ta sẽ thay đổi lại một chút trong router. config/routes.rb:

Rails.application.routes.draw do
  resources :products, only: [:index]
  resource :cart, only: [:show]
  resources :order_items, only: [:create, :update, :destroy]
  root to: "products#index"
end

Bây giờ chúng ta sẽ thêm code cho ApplicationController. Trong ApplicationController chúng ta sẽ thêm 1 phương thức là current_order. Phương thức này sẽ trả về đối tượng order hiện tại hoặc tạo mới 1 order nếu nó không tồn tại. Chúng ta sẽ làm điều này vì mục đích của chúng ta sẽ lưu lại 1 session trên trình duyệt của người dùng cho phép họ có thể thay đổi giỏ hàng ở nhiều thời điểm khác nhau. Chúng ta sẽ xoá session này sau khi người dùng thanh toán giỏ hàng. Bạn có thể tuỳ chỉnh lại chức năng này cho phù hợp với nhu cầu của mình. app/controllers/application_controller.rb:

class ApplicationController < ActionController::Base
  protect_from_forgery with: :exception
  helper_method :current_order

  def current_order
    if !session[:order_id].nil?
      Order.find(session[:order_id])
    else
      Order.new
    end
  end
end

Tiếp theo chúng ta sẽ thêm code cho CardsController. Trong màn hình show cart chúng ta sẽ hiển thị tất cả thông tin sản phẩm người dùng đã thêm vào giỏ. app/controllers/carts_controller.rb:

class CartsController < ApplicationController
  def show
    @order_items = current_order.order_items
  end
end

Tiếp theo chúng ta sẽ thêm code cho OrderItemsController. Chúng ta muốn lần đầu tiên người dùng thêm một order_item vào giỏ hàng của mình, lúc đó order mới được lưu vào cơ sở dữ liệu. Chúng ta cũng cho phép người dùng update số lượng hàng muốn mua hoặc xoá món hàng đó. app/controllers/order_items_controller.rb:

class OrderItemsController < ApplicationController
  def create
    @order = current_order
    @order_item = @order.order_items.new(order_item_params)
    @order.save
    session[:order_id] = @order.id
  end

  def update
    @order = current_order
    @order_item = @order.order_items.find(params[:id])
    @order_item.update_attributes(order_item_params)
    @order_items = @order.order_items
  end

  def destroy
    @order = current_order
    @order_item = @order.order_items.find(params[:id])
    @order_item.destroy
    @order_items = @order.order_items
  end
private
  def order_item_params
    params.require(:order_item).permit(:quantity, :product_id)
  end
end

Bây giờ chúng ta sẽ thêm code cho ProductController. Điều duy nhất chúng ta cần ở đây là hiển thị tất cả các mặt hàng đang kinh doanh. Tuy nhiên cũng cần tạo mới một đối tượng order_item để chuẩn bị cho hành động thêm một sản phẩm vào giỏ hàng. (Sử dụng cho các form ở phần view) app/controllers/products_controller.rb:

class ProductsController < ApplicationController
  def index
    @products = Product.all
    @order_item = current_order.order_items.new
  end
end

View

Công việc tiếp theo đó là xây dựng giao diện cho trang web của chúng ta. Một trang web thương mại đơn giản sẽ bao gồm việc hiển thị sản phẩm ở trung tâm và một biểu tượng như là giỏ hàng ở góc trên bên phải. Giỏ hàng này sẽ hiển thị một vài thông tin đại diện như là đã có bao nhiêu sản phẩm trong giỏ hàng, tổng số tiền cần thanh toán là bao nhiêu... Chúng ta sẽ từng bước xây dựng giao diện mong muốn trên.

Đầu tiên là Application layout. Trong layout này chúng ta sẽ thêm vào bootstrap và tạo ra một layout cơ bản cho ứng dụng. Ở đây tôi muốn giỏ hàng lúc nào cũng hiển thị phía bên trên tay phải trong tất cả các màn hình. Các bạn có thể thay đổi để phù hợp với nhu cầu. app/views/layouts/application.html.erb:

<!DOCTYPE html>
<html>
<head>
  <title>ShoppingCartExample</title>
  <%= javascript_include_tag 'application', 'data-turbolinks-track' => true %>
  <% if request.ssl? %>
    <%= stylesheet_link_tag 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' %>
    <%= javascript_include_tag 'https://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js' %>
  <% else %>
    <%= stylesheet_link_tag 'http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' %>
    <%= javascript_include_tag 'http://maxcdn.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js' %>
  <% end %>
  <%= stylesheet_link_tag    'application', media: 'all', 'data-turbolinks-track' => true %>
  <%= csrf_meta_tags %>
</head>
<body>
  <div class="container">
    <div class="row">
      <div class="col-xs-6">
        <h1><%= link_to "My Store", root_path %></h1>
      </div>
      <div class="col-xs-6 text-right">
        <h1 class="cart-text"><%= render 'layouts/cart_text' %></h1>
      </div>
    </div>
    <hr>
    <%= yield %>
  </div>
</body>
</html>

Tiếp theo chúng ta sẽ tạo ra phần giao diện đại diện cho giỏ hàng được gọi là cart_text. Phần cart_text sẽ được sử dụng để sinh ra một giỏ hàng ở góc trên bên phải. Tạo ra phần cart_text bằng cách thêm đoạn code như dưới đây. app/views/layouts/_cart_text.html.erb:

<%= link_to "#{current_order.order_items.size} Items in Cart ( #{number_to_currency current_order.subtotal} )", cart_path, class: "btn btn-link" %>

**Xây dựng màn hình thứ 1: Màn hình hiển thị tất cả các sản phẩm đang bán. **app/views/products/index.html.erb:

<h3 class="text-center">Products for Sale</h3>

<div class="row">
  <div class="col-xs-6 col-xs-offset-3">
    <% @products.each do |product| %>
      <%= render "product_row", product: product, order_item: @order_item %>
    <% end %>
  </div>
</div>

Với mỗi một ô hiển thị sản phẩm chúng ta mong muốn thêm vào 1 form để người dùng có thể nhập số lượng và thêm nó vào giỏ hàng của mình. app/views/products/_product_row.html.erb:

<div class="well">

  <div class="row">
    <div class="col-xs-8">
      <h4><%= product.name %></small></h4>
    </div>
    <div class="col-xs-4">

      <%= form_for order_item, remote: true do |f| %>
      <h4 class="text-right">Unit Price: <span style="color: green"><%= number_to_currency product.price %></span></h4>
        <div class="input-group">
          <%= f.number_field :quantity, value: 1, class: "form-control", min: 1 %>
          <div class="input-group-btn">
            <%= f.hidden_field :product_id, value: product.id %>
            <%= f.submit "Add to Cart", class: "btn btn-primary" %>
          </div>
        </div>
      <% end %>
    </div>

  </div>
</div>

Ở đây để không phải load lại trang mỗi khi người dùng thêm 1 món hàng vào giỏ. Tôi sẽ sử dụng AJAX. Để chỉ cập nhật lại cart_text sau mỗi lần thêm. app/views/order_items/create.js.erb:

<% if @order.errors.any? || @order_item.errors.any? %>
alert("not valid.")
<% else %>
  $(".cart-text").html("<%= escape_javascript(render 'layouts/cart_text') %>")
<% end %>

**Xây dựng màn hình chính thứ 2: Hiển thị các món hàng trong giỏ. **Giờ chúng ta sẽ chỉnh sửa phần giao diện show trong CartsController. Màn hình show sẽ hiển thị tất cả các item đã được thêm vào giỏ hàng. Ngoài ra trong màn hình này chúng ta cũng cho phép người dùng thay đổi số lượng hàng muốn mua cũng như xoá món hàng đó khỏi giỏ hàng. app/views/carts/show.html.erb:

<div class="shopping-cart">
  <%= render "shopping_cart" %>
</div>

app/views/carts/_shopping_cart.html.erb:

<% if [email protected]_item.nil? && @order_item.errors.any? %>
  <div class="alert alert-danger">
    <ul>
    <% @order_item.errors.full_messages.each do |msg| %>
      <li><%= msg %></li>
    <% end %>
    </ul>
  </div>
<% end %>
<% if @order_items.size == 0 %>
  <p class="text-center">
    There are no items in your shopping cart.  Please <%= link_to "go back", root_path %> and add some items to your cart.
  </p>
<% else %>
  <% @order_items.each do |order_item| %>
    <%= render 'carts/cart_row', product: order_item.product, order_item: order_item, show_total: true %>
  <% end %>
<% end %>

Bây giờ chúng ta sẽ tạo ra form cart_row. Hiển thị mỗi một mặt hàng hàng đang có trong giỏ bao gồm cả các hành động thay đổi số lượng sản phẩm muốn mua hoặc xoá sản phẩm đó khỏi giỏ hàng. Ở đây chúng ta sẽ sử dụng AJAX cho 2 hành động trên giúp không cần load lại toàn bộ trang. app/views/carts/_cart_row.html.erb:

<div class="well">

  <div class="row">
    <div class="col-xs-8">
      <h4><%= product.name %></h4>
    </div>
    <div class="col-xs-4">

      <%= form_for order_item, remote: true do |f| %>
        <h4 class="text-right">Unit Price: <span style="color: green"><%= number_to_currency order_item.unit_price %></span></h4>
        <div class="row">
          <div class="col-xs-4">
            <%= f.number_field :quantity, value: order_item.quantity.to_i, class: "form-control", min: 1 %>
            <%= f.hidden_field :product_id, value: product.id %>
          </div>
          <div class="col-xs-8 text-right">
            <div class="btn-group">
              <%= f.submit "Update Quantity", class: "btn btn-primary" %>
              <%= link_to "Delete", order_item, { data: { confirm: "Are you sure you wish to delete the product '#{order_item.product.name}' from your cart?" }, method: :delete, remote: true, class: "btn btn-danger" } %>
            </div>
          </div>
        </div>
        <h4 class="text-right">Total Price: <span style="color: green"><%= number_to_currency order_item.total_price %></span></h4>
      <% end %>
    </div>

  </div>
</div>

code js cho hành động xoá app/views/order_items/destroy.js.erb:

$(".cart-text").html("<%= escape_javascript(render 'layouts/cart_text') %>")
$(".shopping-cart").html("<%= escape_javascript(render 'carts/shopping_cart') %>")

Code js cho hành động thay đổi số lượng. app/views/order_items/update.js.erb:

$(".cart-text").html("<%= escape_javascript(render 'layouts/cart_text') %>")
$(".shopping-cart").html("<%= escape_javascript(render 'carts/shopping_cart') %>")

Xong! Chúng ta đã hoàn thành. Bạn có thể khởi động rails server và truy cập vào địa chỉ:  localhost:3000 bằng bất kì trình duyệt nào để chạy thử ứng dụng của mình.

Nếu chương trình của bạn vẫn không hoạt động được, bạn có thể tham khảo code mẫu tại đây: Code Demo

Bài viết có tham khảo từ nguồn: richonrails.com

All Rights Reserved