牛骨文教育服务平台(让学习变的简单)

5.2 控制器中的方法

概要

本课时讲解 Controller 中的回调,权限控制,及如何实现网店的购物车和支付功能,以及使用 datatable 查看订单数据。

知识点

  1. 回调
  2. 权限设置
  3. 状态变更
  4. 支付
  5. 带分页的数据列表
  6. datatable

正文

5.2.1 回调

和 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 字样)

5.2.2 权限控制

Controller 除了对请求作出相应,另一个重要的事情是做权限控制,只有拥有权限的用户才可以触发方法。权限管理有很多 gem 可用,常用的有 cancanpundit 等。

由于 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

5.2.3 购物车

购物车有多种设计思路,有的会把信息保存在 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_pricedelegate 方法的 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

关注购物车的其他环节,我们可以查看代码演示,它所使用的方法,我们之前已经介绍过了。

5.2.4 支付

订单创建时,它的 payment_stateconfirm,当完成支付后,它的状态改为 paid。这里我们使用支付宝来支付订单。

我们需要安装支付宝的 gem

并且增加初始配置文件 config/initializers/alipay.rb,这里需要填写从支付宝商家服务 申请 的 PID 和 KEY。

Alipay.pid = "申请的 PID"
Alipay.key = "申请的 KEY"

支付宝常用实时到账和担保交易,如果开通了支付宝快捷登陆,在使用实时到账时,可以扫描二维码支付。

支付成功后,通常设定为跳转回订单详细页面,支付宝会通过接口自动通知 notify 方法,我们应该在该方法中更新订单状态,并且通知支付宝是否成功,只需 render text: "success"render text: "fail"

这里有一份非常详尽的支付宝集成方案,欢迎参考。

5.2.5 带分页的数据列表

进入到“我的订单”页面,会有多条订单记录,这里需要对订单进行分页。常用的分页 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

5.2.6 datatable

datatable 是传统分页方法的一个极好的替代,当数据量较多,且需要 ajax 加载数据时,可以使用 server 端 datatable 实现,具体请参考 示例列表

当我们的订单数量巨大的时候,我们需要使用 datatable 的 server-side,来减轻分页加载时的压力。这里有一个演示,供大家参考。