本课时讲解 Controller 中的回调,权限控制,及如何实现网店的购物车和支付功能,以及使用 datatable 查看订单数据。
和 Model 中的回调一样,Controller 中也有回调。Rails 4 之前,它称作过滤器,Filter,现在一些文档也在使用 filter 字样。
回调它之前的名字是 xxx_filter
,但是这种称呼很是歧义,于是在 Rails 4 中改成了 xxx_action
。
Controller 中的回调有三个,before,after,around。并且可以通过 :only
和 :except
指定在哪些方法上应用该回调。
在我们的项目里,为了使登录用户才能访问,我们在 application_controller.rb
中已经使用了一个前置回调:
class ApplicationController < ActionController::Base
...
before_action :authenticate_user!
...
end
因为其他的 Controller 都继承自它,所以这个前置回调会在所有 Controller 中生效。也就是说,访问所有页面,都需要登录状态。
但是对于首页,展示页等,可以公开访问的页面,我们需要跳过这个登录校验,Controller 中还可以使用 skip_before_action :xxx
跳过回调。
class ProductsController < ApplicationController
skip_before_action :authenticate_user!, only: [:index, :show, :top]
end
回调也可以使用 block 和单独的回调类,方法和 Model 中一样,或者参考这里。(注:它还在用 filter 字样)
Controller 除了对请求作出相应,另一个重要的事情是做权限控制,只有拥有权限的用户才可以触发方法。权限管理有很多 gem 可用,常用的有 cancan,pundit 等。
由于 cancan 已经两年没有维护了,所以Ruby社区推出cancan 的社区版 cancancan。
% rails g cancan:ability
create app/models/ability.rb
编辑 ability.rb,我们的权限是:当一个 user(已登录)字段 role 是 admin 时,可以管理所有资源,否则,只能管理它自己的资源。
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
else
can :read, :all [1]
can :manage, Address, :user_id => user.id [2]
end
[1] 非管理员可读所有
[2] 用户管理自己的收货地址
我们给 users
表添加 role 字段:
rails g migration addRoleToUsers role:string
在视图中判断权限:
<%= link_to "Edit", edit_product_path(product) if can? :update, product %>
这里有四个动作可以判断::read
,:create
,:update
,:destroy
。
我们在 Controller 中增加 load_and_authorize_resource
回调,这个回调将自动加载一个资源,并且进行权限校验,这适合资源管理中的方法:
class ProductsController < ApplicationController
load_and_authorize_resource
也可以将这个回调分成两个回调,这样方便覆写其中的方法:
class ProductsController < ApplicationController
load_resource
authorize_resource
更多文档详见 这里。
也可以不实用回调,直接在方法上判断权限,比如判断当前用户是否可以创建商品:
class ProductsController < ApplicationController
...
def create
authorize! :create, @product
...
cancancan 更多用法,详见 wiki。
购物车有多种设计思路,有的会把信息保存在 cookie 中,有的保存在数据库中。
我们将它保存到数据库中,使用 CartItem 这个 Model。当向购物车增加商品时,我们将商品的商品类型(Variant)以及数量保存到购物车中。如果再次购买,会增加该商品类型的数量。
我们将订单的创建过程分为三步,第一步:确认购物车,第二步:填写收货地址,第三部:形成订单,第四部:支付,第五步:支付成功后通知订单。
为了方便管理购物和支付流程,我把这个逻辑单独的放置在 checkout_controller.rb
。
当我们计算购物车和商品类型价格的时候,经常的出现 line_item.variant.price
,这种查询可以通过 Model 中的 delegate
进行改进:
class LineItem < ActiveRecord::Base
...
delegate :price, to: :variant, prefix: true
这样,刚才的查询可以改为 line_item.variant_price
。delegate
方法的 api 在 这里。
但是,这种方法会造成过多的查询,所以在确定使用这种方法后,我们可以使用 has_many
中的 includes
选项:
class Order < ActiveRecord::Base
has_many :line_items, -> { includes :variant }
end
当我们再次查询 line_items 时,会自动的检索关联的 variant,避免多余的 sql 查询。
我们编写代码的时候,有一些代码可能需要优化,有一些功能还待完成,这时可以在代码中增加特殊的注释:
def checkout
# OPTIMIZE
# TODO
# FIXME
使用 rake 命令可以查看代码中的注解
rake notes:optimize/fixme/todo
关注购物车的其他环节,我们可以查看代码演示,它所使用的方法,我们之前已经介绍过了。
订单创建时,它的 payment_state
为 confirm
,当完成支付后,它的状态改为 paid
。这里我们使用支付宝来支付订单。
我们需要安装支付宝的 gem。
并且增加初始配置文件 config/initializers/alipay.rb
,这里需要填写从支付宝商家服务 申请 的 PID 和 KEY。
Alipay.pid = '申请的 PID'
Alipay.key = '申请的 KEY'
支付宝常用实时到账和担保交易,如果开通了支付宝快捷登陆,在使用实时到账时,可以扫描二维码支付。
支付成功后,通常设定为跳转回订单详细页面,支付宝会通过接口自动通知 notify
方法,我们应该在该方法中更新订单状态,并且通知支付宝是否成功,只需 render text: "success"
或 render text: "fail"
。
这里有一份非常详尽的支付宝集成方案,欢迎参考。
进入到“我的订单”页面,会有多条订单记录,这里需要对订单进行分页。常用的分页 gem 是 will_paginate。因为我们在使用 bootstrap,所以需要安装 will_paginate-bootstrap。
分页的代码非常简单:
class OrdersController < ApplicationController
...
def index
@orders = Order.paginate(:page => params[:page], :per_page => 20)
页面上:
<div class="well">
<%= page_entries_info @orders %>
</div>
<%= will_paginate @orders, renderer: BootstrapPagination::Rails %>
为了让 page_entries_info
方法和分页按钮显示中文,我们增加一个新的语言包:
config/locales/will_paginate/zh-CN.yml
除了 will_paginate,还有 kaminari,以及 datatable
datatable 是传统分页方法的一个极好的替代,当数据量较多,且需要 ajax 加载数据时,可以使用 server 端 datatable 实现,具体请参考 示例列表。
当我们的订单数量巨大的时候,我们需要使用 datatable 的 server-side,来减轻分页加载时的压力。这里有一个演示,供大家参考。