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. 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 ...
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.
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.
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.
- Product: Chứa thông tin sản phẩm được bán.
- 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.
- 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.
- 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 !@order_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