购物车实作思路

96
稻草人_b788
0.1 2018.02.04 20:02 字数 2828

购物车实作思路:

[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之间的关系

(1)由于一种商品可以加入到不同的购物车中,一个购物车也可以加入不同种商品,因此商品和购物车之间是多对多关系,这时候我们需要引入第三方model(购物栏)来建立商品product和购物车cart之间的关系
(2)新建购物车的model cart,和购物栏的model cart_item
终端执行:
rails g model cart
rails g model cart_item
打开cart_item的migration文件,在其中记录商品product_id,购物车cart_id和购物栏自身数量quantity默认为1(商品对应购物栏的数量就是购物车中该商品的数量,通过修改购物栏自身数量来修改购物车中同一种商品的数量,进而计算购物车总价)
(3)建立购物车cart,购物栏cart_item,商品product之间的关系
在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中加入对称的关系,因为我们不需要通过商品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检测用户用了哪台购物车,以确保自己的商品不会加入到别人的购物车中
如果用户的购物车丢失,就重新为用户分配一个购物车,重新登记购物车的信息
由于接下来我们要计算购物车中有多少种商品,计算购物车商品总价,都需要捕捉到用户的购物车,就需要调用current_cart方法,但在视图view中想要controller中的方法,需要声明该controller方法为helper_method,因此这里我们将current_cart声明为helper_method

(5)前面已经建立了购物车cart,购物栏cart_item,商品product之间的关系,这里通过新建购物栏cart_item,存入对应的商品product的信息,以及指定商品的购物栏数量,来将商品product存入到购物车的购物栏中,通过获得是哪台购物车,就可以知道商品被加入到哪个购物车了
在cart.rb中加入代码:

def add_product_to_cart(product)
ci = cart_items.build #在购物车中新建购物栏
ci.product = product #记录当前点击的商品信息到购物栏中
ci.quantity = 1           #默认购物栏的数量是1(用来指代商品加入购物车后默认的数量为1,以便后面计算购物车商品总价)
ci.save
end

上述代码中:

ci = cart_items.build #在购物车中新建购物栏

由于build和new互为别名方法(参考资料篇:build和new),因此也可以写成:

ci = cart_items.new #在购物车中新建购物栏

但不能用

ci = CartItem.new

另外根据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/全栈营学习/学习总结/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中调用
(7)购物车图标显示当前购物车中有多少种商品
在导航栏视图中加入购物车图标,并显示商品一共有多少种(这时候还没有做判断,即便是同一种商品,在多次点击加入到购物车也会分别作为不同件商品处理)

<%= link_to "#" do %>
购物车 <i class="fa fa-shopping-cart"> </i> (<%= current_cart.products.count %>)
<% end %>

3.实作购物车详情页

(1)新建购物车的控制器carts_controller
终端执行:
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中加入关键代码:

<div class="row">
  <div class="col-md-12">

    <h2> 购物车 </h2>

    <table class="table table-bordered">
      <thead>
        <tr>
          <th colspan="2">商品资讯</th>
          <th>单价</th>
          <th>数量</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("确认结账", "#", method: :post, class: "btn btn-lg btn-danger pull-right") %>
    </div>
  </div>
</div>

如果商品有商品图片,则显示对应的商品图片,如果没有则显示默认的预设图
点击商品图可以跳转到商品详情页
显示商品的价格,和商品数量

(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,因此我们可以进一步重构代码
修改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!方法,把操作数据库的任务交给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方法分别执行各自的任务比较好
但不能写成:

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!方法用的是.方法,即从属关系,因此它可以引用model中的方法,但是不能引用controller方法,因为controller中的方法不是从属关系。
所以,我们不能用.来引用方法,而是包装成一般的方法,通过传入参数来调用controller方法
(4)在购物车详情页添加"清空购物车"的按钮

<%= link_to("清空购物车",clean_carts_path,method: :post) %>

6.删除购物车内的某一商品

由于购物车中的商品是存储在购物栏cart_item中,因此我们需要建立cart_item的控制器,设定cart_item的路由,通过删除商品对应的cart_item来完成删除某一商品功能
(1)建立cart_item的控制器
终端执行:
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作为路径参数,不过需要将destroy action中的代码修改为:

- @cart_item = @cart.cart_items.find_by(product_id: params[:id])
+ @cart_item = @cart.cart_items.find(params[:id])

补充:
关于使用cart_item自身id,还是cart_item对应的product_id来作为路径参数,主要原因是routes路径中的id和传入的参数有关,具体内容参考我的这篇[购物车篇(5):删除购物车中的某一件商品](/Users/xyy/Documents/知识专题/ruby on rails/全栈营学习/"购物网站"复习总结/购物车篇(5):删除购物车中的某一件商品.md)

7.限制不能重复加入同种商品

前面已经建立了购物车cart和商品product之间的关系了,要想让重复的商品不能再次加入,需要加一个判断条件,即:检查用户的购物车中是否有该种商品,如果没有则加入。
在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] ="你的购物车内已有此物品"
+ end
redirect_to :back
end

8.用户可以更改购物车中的商品数量

(1)在cart_items的controller中加入update action,让用户可以修改商品数量

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加入更新商品数量的按钮

- <%= 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 %>

你可以将数组[1,2,3,4,5]该成range形式,如(1..5)

9.库存为 0 的商品不能购买

要考虑两点,一是商品库存为0,相应的商品页面的"加入购物车"按钮要被替换;二是由于很多用户通过购物车购买会对同一种商品的数量有影响,因此要在cart_item的controller种加入条件判断:如果用户在购物车种设定的购买商品的数量大于当前商品的库存,则不能更新购买数量
(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] = "成功变更数量"
+ else
+ flash[:warning] = "数量不足以加入购物车"
+ end

redirect_to carts_path
end

  private
  
  def cart_item_params
    params.require(:cart_item).permit(:quantity)
  end

这里的 cart_item_params[:quantity]是从cart_item资料库中获取对应的数字,但它是数组型的,因此我们需要用to_i将数字数组转换成数字,才可以用比较运算符进行计算

10.在购物车新增数量时,不能更新超过原有库存的数量

修改购物车详情页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)

ruby on rails知识专题