前言
本文是[購物車實作思路](/Users/xyy/Documents/知識專題/ruby on rails/全棧營學習/學習總結(jié)/購物車實作思路.md)的續(xù)篇利凑,由于消費者確定好購物車中的商品和數(shù)量浆劲,接下來就是要下單,所以本文主要來描述訂單實作的思路哀澈。
1.建立訂單結(jié)賬頁
(1)設(shè)定訂單結(jié)賬頁面的路徑
resources :carts do
collection do
delete :clean
+ post :checkout
end
end
(2)修改購物車carts/index頁面牌借,為"確認結(jié)賬"按鈕,添加路徑
- <%= link_to("確認結(jié)賬","#",method: :post) %>
+ <%= link_to("確認結(jié)賬",checkout_carts_path,method: :post) %>
之所以要用collection割按,是因為我們要對購物車中所有的商品而不是某一件商品進行操作
(3)建立訂單order的model
其中走哺,我們要存儲購物車內(nèi)所有商品的總結(jié)total
訂單的建立者user_id
寄件人信息billing_name,billing_address
和收件人信息shipping_name,shipping_address
終端執(zhí)行:
rails g model order
在新生成的migration文件中加入:
t.integer :total, default: 0
t.integer :user_id
t.string :billing_name
t.string :billing_address
t.string :shipping_name
t.string :shipping_address
然后終端執(zhí)行:
rails db:migrate
(4)建立訂單order和用戶user之間的關(guān)系
一個用戶可以有很多個訂單
在user.rb中加入:
has_many :orders
在order.rb中加入:
belongs_to :user
(5)在carts_controller中建立結(jié)賬頁的checkout action
def checkout
@order = Order.new
end
(6)建立結(jié)賬頁面的carts/checkout.htm.erb
重要的代碼部分:
<table>
<tbody>
<% current_cart.cart_items.each do |cart_item| %>
<tr>
<td>
<%= link_to(cart_item.product.title,product_path(cart_item.product)) %>
</td>
<td>
<%= cart_item.product.price %>
</td>
<td>
<%= cart_item.quantity %>
</td>
</tr>
<% end %>
</tbody>
</table>
<h2>訂單資訊</h2>
<div class="order-form">
<%= simple_form_for %>
<legend>訂購人</legend>
<div class="form-group col-lg-6">
<%= f.input :billing_name %>
</div>
<div class="form-group col-lg-6">
<%= f.input :billing_address %>
</div>
<legend>收件人</legend>
<div class="form-group col-lg-6">
<%= f.input :shipping_name %>
</div>
<div class="form-group col-lg-6">
<%= f.input :shipping_address %>
</div>
<div class="checkout">
<%= f.submit "生成訂單",class: "btn btn-lg btn-danger pull-right",
data: {disable_with: "Submitting"} %>
</div>
<% end %>
</div>
(7)限制必須填寫寄件人和收件人信息
在order.rb中加入:
validates :billing_name, presence: true
validates :billing_address,presence: true
validates :shipping_name,presence: true
validates :shipping_address,presence: true
(8)建立訂單order的routes
在routes.rb中加入:
resources :orders
(9)建立生成訂單的create action
前面的checkout action只是讓用戶填入一些參數(shù):
寄件人和收件人的名稱丙躏,地址
create action才是把資料存入到資料庫中
終端執(zhí)行:
rails g controller orders
在新生成的orders_controller.rb中加入代碼择示,其中要指定order的欄位內(nèi)容,即購物車商品的總計total和訂單建立者user晒旅,由于checkout action中已經(jīng)指定了訂單的寄件人和收件人信息栅盲,因此就不用在create里面再次指定一遍了:
before_action :authenticate_user!,only:[:create]
def create
@order = Order.new(order_params) #獲取結(jié)賬頁面?zhèn)魅氲男畔ⅲㄟ@里是從checkout頁面獲取,因為checkout頁面的之前被我們設(shè)置成了order.new即創(chuàng)建order對象)傳入的信息包含了收件人和寄件人的信息
@order.user = current_user #指定訂單的創(chuàng)建者是當前登陸用戶
@order.total = current_cart.total_price#指定訂單上顯示的總計是購物車的商品總價
if @order.save
redirect_to order_path(@order)
else
render 'carts/checkout'
end
end
private
def order_params
params.require(:order).permit(:billing_name, :billing_address, :shipping_name, :shipping_address)
end
注意:
new和build互為別名废恋,在新建訂單時我們是這樣寫的:
@order = Order.new(order_params)
@order.user = current_user
也可以寫成:
@order = current_user.orders.new(order_params)
同理谈秫,限制用戶只能看到自己的訂單,我們可以這樣寫
def show
@order = Order.find(params[:id])
if @order.user != current_user
redirect_to root_path
end
end
也可以寫成:
def show
@order = current_user.orders.find(params[:id])
end
在使用后一種寫法時鱼鼓,如果輸入其他用戶的訂單地址拟烫,localhost3000是紅色報錯,heroku(production)是直接404迄本,最好不讓用戶知道他做了什么而導致錯誤硕淑,這樣比較安全。
2.建立購買明細
購買明細和訂單明細不同的地方在于嘉赎,訂單明細是下單時用的置媳,就如我們上面做的整個第1步的內(nèi)容。而購買明細的作用是接下來發(fā)送給用戶通知信用的公条,最重要的是購買明細記錄了當時的購買商品的信息拇囊,以便用戶追溯查看,這個購買明細靶橱,還不能直接用 在order中用product_id 的方式記錄信息寥袭,然后通過order.product來獲取商品信息,因為商品的價格會改變关霸,商品也可能會下架传黄,因此我們就要新建一個product_list這個model來存儲當時的購買信息然后我們在訂單詳情頁面用product_list中保存的資料,就可以查看當時購買商品時候的信息了谒拴,這樣即便商品價格變動,商品下架都不會對訂單詳情中的信息產(chǎn)生影響涉波。
(1)新建購買明細的model product_list
終端執(zhí)行:
rails g model product_list
在新生成的migration文件中加入:
t.integer :order_id
t.string :product_name
t.integer :product_price
t.integer :quantity
分別用來記錄購買明細對應(yīng)的訂單英上,商品名稱,商品價格和商品購買數(shù)量
終端執(zhí)行:
rake db:migrate
(2)建立訂單order和購買明細product_list之間的關(guān)系
一個訂單可以有很多個購買明細(因為我們在購物車中可以放入很多中商品一起買啤覆,一種商品實際上就對應(yīng)了一個購買明細)
在order.rb中加入:
has_many :product_lists
在product_list.rb中加入:
belongs_to :order
(3)在訂單建立的時候同時建立購買明細
這就需要在order的create action中加入代碼:
def create
if @order.save
+ current_cart.cart_items.each do |cart_item|
+ product_list = ProductList.new
+ product_list.order = @order
+ product_list.product_price = cart_item.product.price
+ product_list.product_name = cart_item.product.title
+ product_list.quantity = cart_item.quantity
+ product_list.save
+ end
redirect_to order_path(@order)
else
render 'carts/checkout'
end
end
(4)建立訂單詳情頁的action
在orders_controller中加入show action
def show
@order = Order.find(params[:id])
@product_lists = @order.product_lists
end
這里我們要找到對應(yīng)的訂單苍日,并且需要用到訂單對應(yīng)的購買明細product_list,等下會把它們在訂單詳情頁面中進行渲染
(5)建立訂單詳情頁orders/show.html.erb
在其中加入代碼:
<div class="row">
<div class="col-md-12">
<h2>訂單明細</h2>
<table class="table table-bordered">
<thead>
<tr>
<th width= "80%">商品明細</th>
<th>單價</th>
</tr>
</thead>
<tbody>
<% @product_lists.each do |product_list| %>
<tr>
<td>
<%= product_list.product_name %>
</td>
<td>
<%= product_list.product_price %>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="total clearfix">
<span class="pull-right">
總計 <%= @order.total %> CNY
</span>
</div>
<hr>
<h2>寄送資訊</h2>
<table class="table table-striped table-bordered">
<tbody>
<tr>
<td>訂購人</td>
</tr>
<tr>
<td>
<%= @order.billing_name %> - <%= @order.billing_address %>
</td>
</tr>
<tr>
<td>收件人</td>
</tr>
<tr>
<td>
<%= @order.shipping_name %> - <%= @order.shipping_address %>
</td>
</tr>
</tbody>
</table>
</div>
</div>
3.將網(wǎng)址改為亂碼序號
之前我們訂單在打開時窗声,顯示的網(wǎng)址是其id相恃,這樣隱私性比較差,別人很容易知道你的訂單成交量笨觅,或者找到規(guī)律拦耐。因此耕腾,我們要將訂單的網(wǎng)址改成亂碼序號,提高隱私性杀糯。
(1)在order上新建欄位token扫俺,用來保存亂碼序號
終端執(zhí)行:
rails g migration add_token_to_order
在新生成的migration文件中加入:
+ add_column :orders, :token,:string
終端執(zhí)行:
rails db:migrate
(2)在order.rb中新建產(chǎn)生亂碼序號的generate_token方法,并將該方法掛在before_create上
before_create :generate_token
def generate_token
self.token = SecureRandom.uuid
end
注意:
before_create 是 Rails model 內(nèi)建的 callbacks固翰,目的是讓資料生成儲存前先執(zhí)行某某動作狼纬。model其實就是一個ActiveRecord類,每筆資料(或者說物件骂际,對象)都是model的實例疗琉,model是用來操作資料庫的,因此在將亂碼數(shù)字存入到資料庫中歉铝,就需要在存入前進行操作盈简,所以要將before_create寫在model中,然后會繼續(xù)去執(zhí)行controller中的create方法犯戏,最終將資料存入資料庫
(3)進入orders_controller的create action和show action中修改代碼送火,完成重導網(wǎng)址
由于我們把亂碼序號作為網(wǎng)址,因此需要顯性指定order_path中傳入的參數(shù)是order.token先匪,否則路徑仍然會默認調(diào)取order的id种吸,因此要對按鈕或者重導路徑做如下修改:
def create
....
- redirect_to order_path(@order)
+ redirect_to order_path(@order.token)
...
end
同時也要把獲得訂單的方式改成用token方式獲得,因此要對show action做如下修改:
def show
- @order = Order.find(params[:id])
+ @order = Order.find_by_token(params[:id])
end
4.用戶可以看到自己的所有訂單
(1)新建路由
namespace :account do
resources :orders
end
用account/orders是為了和之前的order做區(qū)別呀非,你也將可以將order定義成其他名稱
(2)修改導航欄加入"我的訂單"選項
<li><%= link_to("我的訂單",account_orders_path) %></li>
(3)新建account/orders_controller.rb坚俗,在其中建立index action
終端執(zhí)行:
rails g controller account::orders
然后在心生成的account/orders_controller.rb中加入:
before_action :authenticate_user!
def index
@order = current_user.orders.order"id DESC"
end
其中DESC是按照從新到舊的順序排列訂單
(4)新建"我的訂單"頁面app/views/account/orders/index.html.erb
在其中加入代碼:
<h2>訂單列表</h2>
<table class="table table-bordered">
<thead>
<tr>
<th>#</th>
<th>生成時間</th>
</tr>
</thead>
<tbody>
<% @orders.each do |order| %>
<tr>
<td>
<%= link_to(order.id,order_path(order.token)) %>
</td>
<td>
<%= order.created_at.to_s(:long) %>
</td>
</tr>
<% end %>
</tbody>
</table>
其中to_s(:long)可以把created_at和updated_at的時間格式進行修改,具體可參考:[修改時間格式的方法](/Users/xyy/Documents/知識專題/ruby on rails/12 WebApps in 12 Weeks/修改時間格式的方法.md)
(5)這時候進入"我的訂單"頁面會出現(xiàn)報錯岸裙,我們需要解決報錯
報錯信息提示orders_controller中的show action缺少[:id]猖败,不能正常重導至show網(wǎng)頁
在看看orders_controller中的show action的代碼:
def show
@order = Order.find_by_token(params[:id])
@product_lists = @order.product_lists
end
說明無法通過params[:id]找到對應(yīng)的訂單
這是由于巡球,訂單的亂碼序號token是我們后面加入的赐写,之前的訂單都沒有token,因此那些沒有亂碼序號的訂單就無法通過token傳入?yún)?shù)到params[:id]幢妄,也就無法找到對應(yīng)的訂單了剧董。
解決辦法:
進入rails console幢尚,輸入指令Order.where(token: nil).destroy_all
將沒有token的訂單全部刪除掉,再進入"我的訂單"就正常了
補充:
1.關(guān)于關(guān)系的建立:has_many和belongs_to
根據(jù)需要我們有時候需要同時設(shè)定has_many和belongs_to翅楼,從而建立完整的成對映射關(guān)系尉剩;但有時候如果只需要其中一個,也可以建立不成對的映射關(guān)系毅臊。
例如本例子中建立的order和user之間的映射關(guān)系
user.rb中:
has_many :orders
order.rb中:
belongs_to :user
由于我們在orders_controller的create action中用到了:
@order.user = current_user
并且用戶可以看到自己的orders理茎,在account/orders_controller中
def index
@orders = current_user.orders
end
其實都是為了記錄或者找到訂單order對應(yīng)的用戶user。
總而言之:如果order需要調(diào)用user,那么order.rb中一定要有belongs_to :user
如果user要調(diào)用order皂林,那么user.rb中一定要有has_many: orders
比如朗鸠,如果order想要調(diào)用user,但卻沒有belongs_to :user式撼,則會出現(xiàn)下面的問題:
從圖中可以看出童社,在沒有order.rb中沒有belongs_to :user的情況下,是不能調(diào)用user的著隆,雖然能找到user_id是誰扰楼,但這個user_id其實是order自身的欄位內(nèi)容,其實還是沒有調(diào)取到user美浦。
在[訂單實作思路](/Users/xyy/Documents/知識專題/ruby on rails/全棧營學習/學習總結(jié)/訂單實作思路.md)中我們也提到過弦赖,在建立了cart和cart_item,以及product之間的聯(lián)系時浦辨,我們在cart.rb中這樣寫:
has_many :cart_items
has_many :products, through: :cart_items, source: :product
在cart_item.rb中這樣寫:
belongs_to :cart
belongs_to :product
end
但卻沒有在product中設(shè)置
has_many :cart_items
has_many :carts, through: :cart_items, source: :cart
這是因為我們只需要通過cart_item來調(diào)用product蹬竖,從而知道每個購物欄放了什么商品,但卻不需要通過product來獲取它對應(yīng)的購物欄cart_item是什么流酬,
同樣的币厕,我們想通過購物車cart來調(diào)取products來獲取購物車中放了什么種類的商品product,但卻不用product來尋找對應(yīng)的購物車cart芽腾,而是把分配和尋找購物車的任務(wù)交給了session旦装,因此我們并沒有在product中建立has_many :cart_items
和has_many :carts, through: :cart_items, source: :cart
2.關(guān)于路徑中的id
(1)默認情況下路徑參數(shù)調(diào)用的是id
(2)圖中的id指的是參數(shù)params[:id]
本例中要將亂碼序號作為網(wǎng)址,需要將亂碼序號token欄位中的內(nèi)容作為參數(shù)摊滔,傳入到params[:id]中阴绢,從而才能作為網(wǎng)址
@order = Order.find_by_token(params[:id])
并且需要在按鈕的路徑上或者跳轉(zhuǎn)到的頁面上調(diào)用token作為路徑參數(shù)。
因此創(chuàng)建后訂單后艰躺,要跳轉(zhuǎn)到訂單詳情頁面呻袭,因此我們在order的create action中指定的路徑是:
redirect_to order_path(@order.token)
其中:
@order = Order.find_by_token(params[:id])
等價寫法是:
@order = Order.find_by(token: params[:id])
這樣就可以token作為參數(shù)id傳入到路徑中。
因此當使用亂數(shù)序號時腺兴,需要顯性指定傳入的參數(shù)左电,例如本例子中的token。
同理页响,在[訂單實作思路](/Users/xyy/Documents/知識專題/ruby on rails/全棧營學習/學習總結(jié)/訂單實作思路.md)中篓足,我們寫過:
@cart_item = current_cart.cart_items.find_by(product_id: params[:id])
在購物車詳情頁的刪除某一商品的路徑寫成:
<%= link_to cart_item_path(cart_item.product_id), method: :delete do %>
<i class="fa fa-trash"></I>
<% end %>
也是由于我們是通過product_id尋找cart_item,因此路徑參數(shù)要傳入的是product_id
而如果使用cart_item自身的id來尋找cart_item拘泞,即:
@cart_item = current_cart.cart_items.find(id: params[:id])
那么在購物車詳情頁的刪除某一商品的路徑要對應(yīng)寫成:
<%= link_to cart_item_path(cart_item.id), method: :delete do %>
<i class="fa fa-trash"></I>
<% end %>
其中路徑中的cart_item.id也可以換成用cart_item纷纫,這是因為路徑參數(shù)默認傳入的就是id枕扫,即便我們寫的cart_item陪腌,它也會撈出其中的cart_item.id傳給路徑
總結(jié):如果不是model對象自身的id撈出對象,那么都需要顯性指定撈取的方式是什么,并且將這種撈取方式對應(yīng)的參數(shù)傳入到路徑中
3.關(guān)于結(jié)賬頁checkout action的請求動作
new action在routes中默認是的請求動作是GET诗鸭,checkout action雖然和new action名稱不一樣染簇,但是方法中的代碼都是用new方法來新建對象,為什么這里checkout的請求動作用的POST强岸,是因為需要發(fā)送新表單的原因锻弓,還是因為可以用GET動作,只是考慮到駭客原因蝌箍,改用POST動作了呢青灼?
答:
經(jīng)過測試驗證,checkout action是可以用GET動作的妓盲,使用POST動作的原因應(yīng)該就是為了防駭客杂拨,這一點還需要后面繼續(xù)驗證。
4.關(guān)于新建結(jié)賬頁的checkout action是否一定要用在carts_controller中悯衬,或者是否一定要用checkout action弹沽,可以用new action嗎?
答:
我認為這里checkout的路徑不一定要建立在resources :carts中筋粗,也可以新建別的controller策橘,將checkout 放入對應(yīng)的resources中也是可行的。甚至是將checkout 改成new娜亿,使用orders_controller來完成訂單詳情頁也是可行的丽已。不過這些都是我的猜測,接下來通過實作驗證下暇唾。
答:經(jīng)過驗證得出新建訂單的checkout action可以不用放在resources :carts促脉,也可以新建其他controller,放在其對應(yīng)的resources中策州,或者放在resources :orders中也是可以的瘸味,當然可以直接用new action而不用checkout action也是可行的,action名稱其實是可以新建或修改的够挂。但是需要注意的是如果不是resources默認的7個action旁仿,那么需要在routes中resources設(shè)定默認7個action外的其他action的路由。比如這里如果將checkout action放在resources :orders中需要指定collection do孽糖,并且要指定action 為checkout枯冈,并且請求方式為get 或者post;而如果使用的是new action則不用顯性指定 new action和請求動作办悟。
結(jié)論:
從上述carts_controller.rb中的checkout action
def checkout
@order = Order.new
end
可以看出尘奏,用model建立的新對象(物件)不用和controller名稱對應(yīng),只要有需要任何model建立的對象都可以用在其他controller中病蛉,比如這里在carts_controller中可以建立訂單Order的新對象炫加。
5.為什么用product_list建立購買明細就可以不隨著商品信息變化而發(fā)生變化呢瑰煎?
答:這是因為product_list的資料寫入過程用的是賦值運算符"=",當用賦值運算符進行賦值運算時俗孝,integer酒甸,float等Fixnum類型,和string類型赋铝,symbol類型插勤,range類型的以及hash類型值有如下特點:
即如果變量a通過賦值運算給其他變量b進行賦值后,再次改變變量a革骨,變量b的值不受影響
而數(shù)組類型則不同农尖,它是指向同一個地址,當?shù)刂穼?yīng)的數(shù)值發(fā)生變化良哲,所有指向該地址的值都將改變卤橄,如圖:
可以看出數(shù)組arr將值賦值給數(shù)組br后,再次改變數(shù)組arr的值臂外,那么數(shù)組br的值也會發(fā)生同樣的改變
因此窟扑,在使用product_list時我們用的也是賦值運算,對其中的product_name用的是string漏健,quantity和product_price嚎货,order_id用的都是integer。這樣通過賦值運算后蔫浆,即便原來的商品名稱殖属,價格都發(fā)生了變化,也不會影響到product_list中的數(shù)值瓦盛。
而如果直接在order中通過.
調(diào)用product的話洗显,如果商品的信息發(fā)生變化,商品明細也會發(fā)生變化原环,這樣我們就無法記錄當初買下商品那一刻的商品價格挠唆,名稱等信息。
值得注意的是新建欄位或新建migration時sqlite3不支持數(shù)組類型嘱吗,需要使用Json來轉(zhuǎn)載玄组,例如:
add_column :products, :photos, :string
此時的photo是并不是數(shù)組類型,仍然是string類型谒麦,要想把photos作為數(shù)組類型俄讹,需要到product model中加上:
serialize :photos, JSON
那么這個photos就同JSON轉(zhuǎn)載成數(shù)組類型
6.為什么只需要在create action中記錄用戶,總價绕德,購買明細等信息患膛,其他action 就不用再顯性指明了呢?
答:通過例子來解釋耻蛇,首先看order的create action踪蹬,代碼如下:
def create
@order = Order.new(order_params)
@order.user = current_user
@order.total = current_cart.total_price
if @order.save
current_cart.cart_items.each do |cart_item|
product_list = ProductList.new
product_list.order = @order
product_list.product_price = cart_item.product.price
product_list.product_name = cart_item.product.title
product_list.quantity = cart_item.quantity
product_list.save
end
redirect_to order_path(@order.token)
else
render 'carts/checkout'
end
end
其中在create action中用了new(order_params)驹溃,將新建訂單頁面?zhèn)魅氲募募撕褪占诵畔魅耄种付擞唵谓⒄哐邮铮徫镘囍械纳唐房們r,并且記錄了訂單中的購買信息product_lists亡哄,最后一并寫入到資料庫中枝缔。這樣其實就將order的欄位內(nèi)容和其相關(guān)的model(這里是product_list)內(nèi)容一一填寫上了。
由于在create action中已經(jīng)將order和其相關(guān)的product_list欄位內(nèi)容已經(jīng)填入到資料庫蚊惯,其他action其實都是對資料庫進行操作愿卸,因此不用重新在指定欄位對應(yīng)的內(nèi)容,需要調(diào)用什么內(nèi)容直接從資料庫中取出即可截型。
例如order的show action:
def show
@order = Order.find_by_token(params[:id])
@product_lists = @order.product_lists
end
就是從資料庫中找到對應(yīng)的訂單@order趴荸,并且撈出該訂單相關(guān)的購買明細@product_lists,然后在html中調(diào)用繪制出頁面即可宦焦。