購物車實作思路:
[TOC]
1.建立將商品加入到購物車的action
(1)在商品頁面新建“加入購物車”按鈕
(2)設定product的routes
resources :products do
member do
post :add_to_cart
end
end
(3)在products_controller中加入add_to_cart的action
def add_to_cart
@product = Product.find(params[:id])
flash[:notice] = "已將商品加入到購物車"
redirect_to :back
end
功能:上述步驟完成后舍扰,在商品詳情頁面断医,點擊“加入購物車”會提示“商品已加入購物車”哪亿,但還沒有指定商品加入到了哪臺購物車铃芦。
2.建立商品product和購物車cart之間的關(guān)系
(1)由于一種商品可以加入到不同的購物車中美澳,一個購物車也可以加入不同種商品奢人,因此商品和購物車之間是多對多關(guān)系蓬戚,這時候我們需要引入第三方model(購物欄)來建立商品product和購物車cart之間的關(guān)系
(2)新建購物車的model cart饺著,和購物欄的model cart_item
終端執(zhí)行:
rails g model cart
rails g model cart_item
打開cart_item的migration文件亚隙,在其中記錄商品product_id磁餐,購物車cart_id和購物欄自身數(shù)量quantity默認為1(商品對應購物欄的數(shù)量就是購物車中該商品的數(shù)量,通過修改購物欄自身數(shù)量來修改購物車中同一種商品的數(shù)量阿弃,進而計算購物車總價)
(3)建立購物車cart诊霹,購物欄cart_item,商品product之間的關(guān)系
在cart.rb中加入:
has_many :cart_items
has_many :products through: :cart_items, source: :product
注意上述代碼中products是購物車已加入的商品渣淳,后面的source: :product中的product是所有商品脾还,如果怕搞混了,可以將前面的products修改為其他任意合法的變量入愧,比如goods
在cart_item.rb中加入:
belongs_to :cart
belongs_to :product
這樣就可以通過購物車cart就可以通過購物欄cart_item找到對應的商品product
這里我們不在product.rb中加入對稱的關(guān)系鄙漏,因為我們不需要通過商品good來找對應的購物車cart。我們采用rails中基于cookie的session機制來分配給用戶購物車棺蛛,并追蹤用戶的購物車泥张,以防購物車錯亂。
(4)為當前在操作的用戶分配購物車
在全局的application_controller中加入代碼
helper_method :current_cart
def current_cart
@current_cart ||= find_cart
end
也加入代碼:
private
def find_cart
cart = Cart.find_by(id: session[:cart_id])
if cart.blank?
cart = Cart.create
end
session[:cart_id] = cart.id
return cart
end
之所以要寫在application_controller中是因為我們希望無論進入哪個頁面購物車的信息都保留鞠值,比如current_cart這個方法會在購物車媚创,訂單頁面都會用到,確保用戶的購物車不會錯亂
如果用戶有購物車彤恶,就通過session檢測用戶用了哪臺購物車钞钙,以確保自己的商品不會加入到別人的購物車中
如果用戶的購物車丟失,就重新為用戶分配一個購物車声离,重新登記購物車的信息
由于接下來我們要計算購物車中有多少種商品芒炼,計算購物車商品總價,都需要捕捉到用戶的購物車术徊,就需要調(diào)用current_cart方法本刽,但在視圖view中想要controller中的方法,需要聲明該controller方法為helper_method赠涮,因此這里我們將current_cart聲明為helper_method
(5)前面已經(jīng)建立了購物車cart子寓,購物欄cart_item,商品product之間的關(guān)系笋除,這里通過新建購物欄cart_item斜友,存入對應的商品product的信息,以及指定商品的購物欄數(shù)量垃它,來將商品product存入到購物車的購物欄中鲜屏,通過獲得是哪臺購物車烹看,就可以知道商品被加入到哪個購物車了
在cart.rb中加入代碼:
def add_product_to_cart(product)
ci = cart_items.build #在購物車中新建購物欄
ci.product = product #記錄當前點擊的商品信息到購物欄中
ci.quantity = 1 #默認購物欄的數(shù)量是1(用來指代商品加入購物車后默認的數(shù)量為1,以便后面計算購物車商品總價)
ci.save
end
上述代碼中:
ci = cart_items.build #在購物車中新建購物欄
由于build和new互為別名方法(參考資料篇:build和new)洛史,因此也可以寫成:
ci = cart_items.new #在購物車中新建購物欄
但不能用
ci = CartItem.new
另外根據(jù)build和new互為別名方法惯殊,我猜測:
ci = cart_items.build
可以等價寫成
ci = CartItem.new
ci.cart = current_cart
但是model不能直接引用controller中的方法,因此這里我不知道怎么在model中告知cart_item也殖,它對應的購物車cart是那一臺靠胜,具體參見:[helper方法,model方法毕源,controller方法的運用](/Users/xyy/Documents/知識專題/ruby on rails/全棧營學習/學習總結(jié)/helper方法浪漠,model方法,controller方法的運用.md)
(6)將添加商品到購物車的方法加入product_controller中霎褐,這樣在商品頁面通過點擊“加入購物車”就可以將商品加入到購物車中址愿,并且記錄了的是加入到了當前購物車
在product_controller中加入代碼:
def add_to_cart
@product = Product.find(params[:id])
+ current_cart.add_product_to_cart(@product)
flash[:notice] = "已將商品加入到購物車"
redirect_to :back
end
這里我們也可以不用cart model中建立的add_product_to_cart方法作,而是把其中的代碼放到controller中冻璃,然后刪除掉add_product_to_cart方法响谓,需要略作修改:
def add_to_cart
@product = Product.find(params[:id])
- current_cart.add_product_to_cart(@product)
+ ci = current_cart.cart_items.build
+ ci.product = @product
+ ci.quantity =1
+ ci.save
flash[:notice] = "已將商品加入到購物車"
redirect_to :back
end
這樣也是可行的,不過為了維護方便建議還是將代碼包裝成model方法省艳,然后在controller中調(diào)用
(7)購物車圖標顯示當前購物車中有多少種商品
在導航欄視圖中加入購物車圖標娘纷,并顯示商品一共有多少種(這時候還沒有做判斷,即便是同一種商品跋炕,在多次點擊加入到購物車也會分別作為不同件商品處理)
<%= link_to "#" do %>
購物車 <i class="fa fa-shopping-cart"> </i> (<%= current_cart.products.count %>)
<% end %>
3.實作購物車詳情頁
(1)新建購物車的控制器carts_controller
終端執(zhí)行:
rails g controller carts
這里在不在carts_controller中寫index action 都可以赖晶,因為我們要撈取的資料不是購物車,而是cart_item中的資料辐烂,所以沒有通過carts_controller的index action來操作model遏插,進而操作資料庫,就可以不用寫index action.
(2)設定購物車詳情頁的路由
修改routes纠修,在其中加入代碼:
+ resources :carts
(3)新建購物車詳情頁視圖carts/index.html.erb
在carts/index.html.erb中加入關(guān)鍵代碼:
<div class="row">
<div class="col-md-12">
<h2> 購物車 </h2>
<table class="table table-bordered">
<thead>
<tr>
<th colspan="2">商品資訊</th>
<th>單價</th>
<th>數(shù)量</th>
</tr>
</thead>
<tbody>
<% current_cart.cart_items.each do |cart_item| %>
<tr>
<td>
<%= link_to product_path(cart_item.product) do %>
<% if cart_item.product.image.present? %>
<%= image_tag(cart_item.product.image.thumb.url, class: "thumbnail") %>
<% else %>
<%= image_tag("http://placehold.it/200x200&text=No Pic", class: "thumbnail") %>
<% end %>
<% end %>
</td>
<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>
<br>
<div class="total clearfix">
<span class="pull-right">
<span> 總計 xxx RMB </span>
</span>
</div>
<hr>
<div class="checkout clearfix">
<%= link_to("確認結(jié)賬", "#", method: :post, class: "btn btn-lg btn-danger pull-right") %>
</div>
</div>
</div>
如果商品有商品圖片胳嘲,則顯示對應的商品圖片,如果沒有則顯示默認的預設圖
點擊商品圖可以跳轉(zhuǎn)到商品詳情頁
顯示商品的價格扣草,和商品數(shù)量
(4)修改購物圖標對應的路徑了牛,從而可以通過點擊購物車圖標進入購物車詳情頁
- <%= link_to "#" do %>
+ <%= link_to carts_path do %>
購物車 <i class="fa fa-shopping-cart"> </i> (<%= current_cart.products.count %>)
<% end %>
4.計算購物車中的商品總價
(1)修改購物車詳情頁carts/index.html.erb中的代碼
- 總計 xxx RMB
+ <% sum= 0 %>
+ <% current_cart.cart_items.each do |cart_item| %>
+ <if cart_item.product.price.present? %>
+ <% sum += cart_item.product.price * cart_item.quantity %>
+ <% end %>
+ <% end %>
+ 總計<%= sum %> RMB
(2)上述計算總計的代碼寫在view中不易維護和閱讀,我們將其移動到carts_helper.rb中辰妙,就便于維護和閱讀了
在carts_helper中添加代碼:
def render_cart_total_price(cart)
sum = 0
cart.cart_items.each do |cart_item|
if cart_item.product.price.present?
sum += cart_item.product.price * cart_item.quantity
end
end
sum
end
修改carts/index.html.erb中計算商品總價的代碼:
- 總計 xxx RMB
- <% sum= 0 %>
- <% current_cart.cart_items.each do |cart_item| %>
- <if cart_item.product.price.present? %>
- <% sum += cart_item.product.price * cart_item.quantity %>
- <% end %>
- <% end %>
+ 總計<%= render_cart_total_price(current_cart) %> RMB
(3)其實計算商品總價的任務不應該交給helper鹰祸,而應該交給model,因此我們可以進一步重構(gòu)代碼
修改carts_helper中的render_cart_total_price為:
def render_cart_total_price(cart)
cart.total_price
end
在cart.rb中加入代碼:
def total_price
sum = 0
cart_items.each do |cart_item|
if if cart_item.product.price.present? && cart_item.quantity.present?
sum += cart_item.product.price * cart_item.quantity
end
end
sum
end
5.一鍵清空購物車
目的是清除購物車中的商品上岗,而不是刪除購物車福荸,因此要新建清空功能的clean action蕴坪,而不是使用購物車的destory action
(1)修改routes
- resources :carts
+ resources :carts do
+ collection do
+ delete :clean
+ end
+ end
(2)在carts_controller中建立清空購物車的clean action
def clean
current_cart.clean!
flash[:warning] = "已清空購物車"
redirect_to :back
end
(3)在cart.rb中加入clean!方法肴掷,把操作數(shù)據(jù)庫的任務交給model
def clean!
cart_items.destroy_all
end
你也可以使用:
def clean!
products.destroy_all
end
這里的products是購物車cart通過購物欄cart_item中保存的商品敬锐,已在文章的步驟1有所介紹
當然,你也可以不用clean!方法呆瞻,直接在carts_controller中的clean action中寫成:
def clean
current_cart.cart_items.destroy_all
flash[:warning] = "已清空購物車"
redirect_to :back
end
不過讓model方法和controller方法分別執(zhí)行各自的任務比較好
但不能寫成:
def clean!
products.destroy_all
end
def clean
current_cart.clean!
flash[:warning] = "已清空購物車"
redirect_to :back
end
這樣會提示報錯台夺,找不到clean!方法(undefined method `clean!'))
想要正常運行代碼,需要略作修改:
def clean!(cart)
cart.cart_items.destroy_all
end
def clean
clean!(current_cart)
flash[:warning] = "已清空購物車"
redirect_to :back
end
注意:
這是因為current_cart.clean!方法用的是.
方法痴脾,即從屬關(guān)系颤介,因此它可以引用model中的方法,但是不能引用controller方法赞赖,因為controller中的方法不是從屬關(guān)系滚朵。
所以,我們不能用.
來引用方法前域,而是包裝成一般的方法辕近,通過傳入?yún)?shù)來調(diào)用controller方法
(4)在購物車詳情頁添加"清空購物車"的按鈕
<%= link_to("清空購物車",clean_carts_path,method: :post) %>
6.刪除購物車內(nèi)的某一商品
由于購物車中的商品是存儲在購物欄cart_item中,因此我們需要建立cart_item的控制器匿垄,設定cart_item的路由移宅,通過刪除商品對應的cart_item來完成刪除某一商品功能
(1)建立cart_item的控制器
終端執(zhí)行:
rails g controller cart_items
(2)設定cart_items的路由(建立刪除購物車中某一商品按鈕對應的路徑)
resources :cart_items
(3)在cart_items的控制器中建立destroy action
before_action :authenticate_user!
def destroy
@cart = current_cart
@cart_item = @cart.cart_items.find_by(product_id: params[:id])
@product = @cart_item.product
@cart_item.destroy
flash[:warning] = "已將'#{@product.title}'從購物車中刪除 "
redirect_to :back
end
(4)在購物車詳情頁carts/index中加入刪除商品的按鈕
<%= link_to cart_item_path(cart_item.product_id), method: :delete do %>
<i class="fa fa-trash"></i>
<% end %>
這里也可以使用cart_item作為路徑參數(shù),不過需要將destroy action中的代碼修改為:
- @cart_item = @cart.cart_items.find_by(product_id: params[:id])
+ @cart_item = @cart.cart_items.find(params[:id])
補充:
關(guān)于使用cart_item自身id椿疗,還是cart_item對應的product_id來作為路徑參數(shù)漏峰,主要原因是routes路徑中的id和傳入的參數(shù)有關(guān),具體內(nèi)容參考我的這篇[購物車篇(5):刪除購物車中的某一件商品](/Users/xyy/Documents/知識專題/ruby on rails/全棧營學習/"購物網(wǎng)站"復習總結(jié)/購物車篇(5):刪除購物車中的某一件商品.md)
7.限制不能重復加入同種商品
前面已經(jīng)建立了購物車cart和商品product之間的關(guān)系了届榄,要想讓重復的商品不能再次加入浅乔,需要加一個判斷條件,即:檢查用戶的購物車中是否有該種商品铝条,如果沒有則加入童擎。
在products_contoller中的add_to_cart action中加入代碼:
def add_to_cart
@product = Product.find(params[:id])
+ if !current_cart.products.include?(@product)
current_cart.add_product_to_cart
- flash[:notice] = "成功加入購物車"
+ flash[:notice]="成功將 #{@product.title} 加入購物車""
else
+ flash[:warning] ="你的購物車內(nèi)已有此物品"
+ end
redirect_to :back
end
8.用戶可以更改購物車中的商品數(shù)量
(1)在cart_items的controller中加入update action,讓用戶可以修改商品數(shù)量
def update
@cart = current_cart
@cart_item = @cart.cart_items.find_by(product_id: params[:id])
@cart_item.update(cart_item_params)
redirect_to carts_path
end
private
def cart_item_params
params.require(:cart_item).permit(:quantity)
end
(2)在購物車詳情頁carts/index加入更新商品數(shù)量的按鈕
- <%= cart_item.quantity %>
+ <%= form_for cart_item, url: cart_item_path(cart_item.product_id) do |f| %>
+ <%= f.select :quantity, [1,2,3,4,5] %>
+ <%= f.submit "更新", data: { disable_with: "Submiting..." } %>
+ <% end %>
你可以將數(shù)組[1,2,3,4,5]該成range形式攻晒,如(1..5)
9.庫存為 0 的商品不能購買
要考慮兩點顾复,一是商品庫存為0,相應的商品頁面的"加入購物車"按鈕要被替換鲁捏;二是由于很多用戶通過購物車購買會對同一種商品的數(shù)量有影響芯砸,因此要在cart_item的controller種加入條件判斷:如果用戶在購物車種設定的購買商品的數(shù)量大于當前商品的庫存,則不能更新購買數(shù)量
(1)在商品詳情頁添加條件判斷给梅,如果商品庫存為0假丧,則提示"商品已售完"
+ <% if @product.quantity.present? && @product.quantity > 0 %>
<%= link_to("加入購物車", add_to_cart_product_path(@product), method: :post,
class: "btn btn-lg btn-danger") %>
+ <% else %>
+ 商品已售完
+ <% end %>
(2)修改cart_items_controller中的update action,加入條件判斷
def update
@cart = current_cart
@cart_item = @cart.cart_item.find_by(product_id: params[:id])
+ if @cart_item.product.quantity.present? && @cart_item.product.quantity >=cart_item_params[:quantity].to_i
@cart_item.update(cart_item_params)
+ flash[:notice] = "成功變更數(shù)量"
+ else
+ flash[:warning] = "數(shù)量不足以加入購物車"
+ end
redirect_to carts_path
end
private
def cart_item_params
params.require(:cart_item).permit(:quantity)
end
這里的 cart_item_params[:quantity]是從cart_item資料庫中獲取對應的數(shù)字,但它是數(shù)組型的监憎,因此我們需要用to_i將數(shù)字數(shù)組轉(zhuǎn)換成數(shù)字所计,才可以用比較運算符進行計算
10.在購物車新增數(shù)量時,不能更新超過原有庫存的數(shù)量
修改購物車詳情頁carts/index
<%= form_for cart_item, url: cart_item_path(cart_item.product_id) do |f| %>
- <%= f.select :quantity, [1,2,3,4,5] %>
+ <%= f.select :quantity, 1..cart_item.product.quantity %>
<%= f.submit "更新", data: { disable_with: "Submiting..." } %>
<% end %>
其中1..cart_item.product.quantity也可以寫成(1..cart_item.product.quantity)