Rails – Xây dựng chức năng đặt hàng

3.7/5 - (18 votes)

Trong phần này chúng ta sẽ xây dựng chức năng đặt hàng đơn giản.

Chỉnh sửa model

Đầu tiên chúng ta định nghĩa một model có tên order (đơn hàng) bao gồm các trường:

  • name: tên người nhận
  • address: địa chỉ giao hàng
  • email: địa chỉ email
  • pay_type: phương thức thanh toán
C:\Projects\Rails\depot>rails generate scaffold order name:string address:text email:string pay_type:string
...

Khi một khách hàng thêm sản phẩm vào giỏ hàng thì một đối tượng LineItem sẽ được tạo ra, và chúng ta sẽ cần biết đối tượng LineItem này nằm trong giỏ hàng nào, do đó chúng ta sẽ thêm trường order_id vào model order như sau:

C:\Projects\Rails\depot>rails generate migration add_order_id_to_line_item order_id:integer
...

Cuối cùng chúng ta cập nhật lại cấu trúc CSDL:

C:\Project\Rails\depot>rake db:migrate
...

Chỉnh sửa View

Chúng ta thêm nút ‘Checkout’ vào giao diện như sau:

<h2>Your cart</h2>
<table>
    <%= render cart.line_items %>
    <tr class="total_line">
        <td colspan="2">Total</td>
        <td class="total_cell"><%= number_to_currency(cart.total_price) %></td>
    </tr>
</table>
 
<%= button_to 'Checkout', 
              new_order_path, 
              :method => :get %><br>
<%= button_to 'Empty cart', 
              cart, 
              :method => :delete,
              data: {:confirm => 'Are you sure?' } %>

Nút này sẽ gửi lên URL là '/orders/new' có trong biến new_order_path, phương thức gửi là GET, chúng ta phải chỉ rõ phương thức gửi ra vì mặc định hàm button_to gửi theo phương thức POST.

URL '/orders/new' được gọi tới phương thức action là new trong lớp OrdersController, phương thức này sẽ làm nhiệm vụ hiển thị form nhập thông tin đơn hàng, tuy nhiên chúng ta sửa lại để kiểm tra xem giỏ hàng hiện tại thật sự có hàng hay không thì mới hiển thị. Chúng ta sửa file orders_controller.rb trong thư mục app/controllers như sau:

class OrdersController < ApplicationController
    .
    .
    .
    # GET /orders/new
    def new 
        @cart = current_cart
        if @cart.line_items.empty?
            redirect_to '/', :notice => 'Your cart is empty'
            return 
        end
 
        @order = Order.new 
    end
    .
    .
    .
end

Nếu không có hàng trong giỏ hàng thì chúng ta cho quay lại trang '/', có tin nhắn thông báo là 'Your cart is empty'.

Ngược lại thì file View mặc định do Rails tạo ra là new.html.erb trong thư mục app/views/orders sẽ được hiển thị, chúng ta sửa lại như sau:

<div class="depot_form">
    <fieldset>
        <legend>Your information</legend>
        <%= render 'form' %> 
    </fieldset>
</div>

Ở đây chúng ta gọi hàm render với tham số là ‘form’, hàm này sẽ hiển thị form nhập thông tin từ file partial là _form.html.erb trong thư mục app/views/orders. File này Rails cũng tự động tạo cho chúng ta rồi, nhưng chúng ta sửa lại như sau:

<%= form_for(@order) do |f| %>
    <% if @order.errors.any? %>
        <div id="error_explanation">
            <h2>
                <%= pluralize(@order.errors.count, "error") %> prohibited this order from being saved:
            </h2>

            <ul>
                <% @order.errors.full_messages.each do |message| %>
                    <li><%= message %></li>
                <% end %>
            </ul>
        </div>
    <% end %>

    <div class="field">
        <%= f.label :name %><br>
        <%= f.text_field :name, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label :address %><br>
        <%= f.text_area :address, :rows => 3, :cols => 40 %>
    </div>
    <div class="field">
        <%= f.label :email %><br>
        <%= f.text_field :email, :size => 40 %>
    </div>
    <div class="field">
        <%= f.label :pay_type %><br>
        <%= f.select :pay_type, Order::PAYMENT_TYPES, :prompt => 'Select a payment method' %>
    </div>
 
    <div class="actions">
        <%= f.submit 'Place order' %>
    </div>
<% end %>

Hàm form_for là một hàm helper giúp tạo form một cách nhanh chóng. Cú pháp có dạng:

<%= form_for <đối tượng> ... do |f| %>
    <%= f.label ... %>
    <%= f.text_area ... %>
<% end %>

Hàm form_for nhận vào một đối tượng, và tạo một đối tượng thuộc lớp ActionView::Helpers::FormaBuilder, ở đây là 'f', chúng ta có thể gọi các phương thức của đối tượng này để tạo các thẻ trong <form>, chẳng hạn như label, select, text_area, submit…v.v bạn có thể xem danh sách tại đây.

Ở phương thức f.select dùng để tạo combobox, chúng ta cho danh sách các phần tử là hằng số PAYMENT_TYPES trong lớp Order (app/models/order.rb), do đó chúng ta định nghĩa thêm hằng số này như sau:

class Order < ActiveRecord::Base
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
end

Cuối cùng chúng ta định nghĩa thêm một số lớp CSS như sau:

...
/* Order form style */
.depot_form fieldset {
    background: #efe;
}

.depot_form legend {
    color: #dfd;
    background: #141;
    font-family: sans-serif;
    padding: 0.2em 1em;
}

.depot_form label {
    width: 5em;
    float: left;
    text-align: right;
    padding-top: 0.2em;
    margin-right: 0.1em;
    display: block;
}

.depot_form select, .depot_form textarea, .depot_forma input {
    margin-left: 0.5em;
}

.depot_form .submit {
    margin-left: 4em;
}

.depot_form div {
    margin: 0.5em 0;
}

Đến đây giao diện sẽ như thế này:

capture

Định nghĩa validation

Chúng ta muốn đơn hàng được gửi lên phải thỏa mãn một số điều kiện, đó là không có trường nào được để trống. Chúng ta sửa lại file app/models/order.rb như sau:

class Order < ActiveRecord::Base
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
 
    validates :name, :address, :email, :pay_type, :presence => true
    validates :pay_type, :inclusion => PAYMENT_TYPES
end

Định nghĩa mỗi quan hệ

Một đối tượng LineItem sẽ thuộc về một đối tượng Order. Chúng ta sửa lại file model line_item.rb như sau:

class LineItem < ActiveRecord::Base
    belongs_to :order
    belongs_to :product
    belongs_to :cart
 
    def total_price
        product.price * quantity
    end
end

Ngược lại, một đối tượng Order sẽ có nhiều đối tượng LineItem trong đó, ngoài ra khi một đối tượng Order bị hủy thì các đối tượng LineItem “thuộc về” Order đó cũng sẽ bị hủy theo. Chúng ta sửa file model order.rb như sau:

class Order < ActiveRecord::Base
    has_many :line_items, :dependent => :destroy
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
 
    validates :name, :address, :email, :pay_type, :presence => true
    validates :pay_type, :inclusion => PAYMENT_TYPES
end

Tạo đơn hàng

Khi người dùng điền đầy đủ thông tin và click nút ‘Place order’, nút này sẽ gửi các thông tin đó đến phương thức create trong lớp OrdersController. Chúng ta sửa lại một tí như sau:

class OrdersController < ApplicationController
    .
    .
    .
    # POST /orders
    # POST /orders.json
    def create
        @order = Order.new(order_params)
        @order.add_line_items_from_cart(current_cart)
 
        respond_to do |format|
            if @order.save
                Cart.destroy(session[:cart_id])
                session[:cart_id] = nil
 
                format.html { redirect_to '/', notice: 'Thank you for your order' }
                format.json { render :show, status: :created, location: @order }
            else
                format.html { render :new }
                format.json { render json: @order.errors, status: :unprocessable_entity }
            end
        end
    end
    .
    .
    .
end

Phương thức mặc định của Rails là tạo một đối tượng Order với thông tin lấy từ các tham số do người dùng nhập vào. Tuy nhiên chưa có thông tin về các đối tượng LineItem, chúng ta thêm vào bằng phương thức add_line_items_from_cart, phương thức này sẽ được định nghĩa sau. Sau khi đã tạo đối tượng Order xong và lưu vào giỏ hàng thì chúng ta xóa giỏ hàng trong session đi.

Bây giờ chúng ta định nghĩa phương thức add_line_items_from_cart như sau:

class Order < ActiveRecord::Base
    has_many :line_items, :dependent => :destroy
    PAYMENT_TYPES = [ "Cash on delivery", "Ngân lượng", "Bảo Kim", "Bank Card" ]
 
    validates :name, :address, :email, :pay_type, :presence => true
    validates :pay_type, :inclusion => PAYMENT_TYPES
 
    def add_line_items_from_cart(cart) 
        cart.line_items.each do |item|
            item.cart_id = nil
            line_items << item
        end
    end
end

Phương thức này nhận vào tham số là một đối tượng Cart, chúng ta duyệt danh sách các đối tượng LineItem có trong Cart này, các đối tượng LineItem sẽ được gán giá trị cho thuộc tính cart_idnil để tránh báo lỗi khi xóa giỏ hàng, tiếp theo chúng ta đưa đối tượng LineItem đó vào mảng line_items.

Vậy là xong, bây giờ chúng ta có thể sử dụng chức năng đặt hàng được rồi.

capture

Và khi đặt hàng xong thì chúng ta được trỏ về trang ‘/’ với thông báo đặt hàng thành công (Thanh you for your order).

capture

Tuy nhiên nếu tiếp tục thêm hàng vào giỏ thì tin nhắn này vẫn tồn tại ở đó, chẳng qua là vì trình duyệt gửi lệnh AJAX để cập nhật giỏ hàng chứ không gửi lệnh HTTP để tải lại trang. Để ẩn dòng thông báo này thì chúng ta sửa lại file create.js.erb như sau:

var content = "<%= escape_javascript(render(current_cart)) %>";
document.getElementById("cart").innerHTML = content;

$("#current_item").css({'background-color':'#88ff88'});
$("#current_item").animate({backgroundColor:'#114411'}, 1000 );

var p_notice = document.getElementById("notice");
if(p_notice !== null)
    p_notice.remove();

Chỉ đơn giản là kiểm tra xem trang web có thẻ nào có idnotice hay không và xóa nó đi. Vậy là dòng thông báo sẽ biến mất.

5 1 vote
Article Rating
Subscribe
Thông báo cho tôi qua email khi
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

11 Comments
Inline Feedbacks
View all comments
Đài
Đài
6 năm trước

ad cho hỏi sau khi thực hiện xong các bước xây dựng chưc năng đặt hàng thì khi Add to cart không hiện cart mới tại giao diện bên trái

quanbh
quanbh
6 năm trước

Mình đúng theo hướng dẫn,không biết tại sao lúc add to cart nó cứ bị redirect qua http://localhost:3000/line_items?product_id=1 và báo lỗi “1 error prohibited this line_item from being saved: Order must exist”.Mấy hôm nay mò mẫm không ra mong được bạn hỗ trợ.

hao
hao
6 năm trước

khi check out xong nhấn nút add to cart k dc nữa ad ??

tung
tung
3 năm trước
Reply to  Phở Code

bài nào vậy anh? e tìm mãi không thấy . Em giải thích cho e với.

Le Tuan
Le Tuan
2 năm trước
Reply to  Phở Code

bạn sửa lại dòng
belongs_to :order, optional: true
trong model line_item

Le Tuan
Le Tuan
2 năm trước
Reply to  hao

bạn sửa lại dòng
belongs_to :order, optional: true
trong model line_item

Le Tuan
Le Tuan
2 năm trước

Admin cho mình hỏi, sau khi show notice ví dụ hiên ra dòng Your cart is empty, sau đó mình nhay qua trang khác, và quay trở lại bằng button back cua brosern thì tái sao notice vaxn hiên ra. mình đã thử flash.clear(), flash[:notice] = nil nhưng không đc.
Cảm ơn bạn trước nhé.

Bildschirmfoto 2021-10-30 um 19.30.56.png