# 5.2 控制器中的方法
## 概要
本课时讲解 Controller 中的回调,权限控制,及如何实现网店的购物车和支付功能,以及使用 datatable 查看订单数据。
## 知识点
1. 回调
1. 权限设置
1. 状态变更
1. 支付
1. 带分页的数据列表
1. 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 中一样,或者参考[这里](http://guides.rubyonrails.org/action_controller_overview.html#filters)。(注:它还在用 filter 字样)
### 5.2.2 权限控制
Controller 除了对请求作出相应,另一个重要的事情是做权限控制,只有拥有权限的用户才可以触发方法。权限管理有很多 gem 可用,常用的有 [cancan](https://github.com/ryanb/cancan),[pundit](https://github.com/elabs/pundit) 等。
由于 cancan 已经两年没有维护了,所以Ruby社区推出cancan 的社区版 [cancancan](https://github.com/CanCanCommunity/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
~~~
更多文档详见 [这里](https://github.com/CanCanCommunity/cancancan/wiki/authorizing-controller-actions)。
也可以不实用回调,直接在方法上判断权限,比如判断当前用户是否可以创建商品:
~~~
class ProductsController < ApplicationController
...
def create
authorize! :create, @product
...
~~~
cancancan 更多用法,详见 [wiki](https://github.com/CanCanCommunity/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_price`。`delegate` 方法的 api 在 [这里](http://api.rubyonrails.org/classes/Module.html#method-i-delegate)。
但是,这种方法会造成过多的查询,所以在确定使用这种方法后,我们可以使用 `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
~~~
关注购物车的其他环节,我们可以查看[代码演示](https://github.com/liwei78/rails-practice-code/tree/master/chapter_5/shop),它所使用的方法,我们之前已经介绍过了。
### 5.2.4 支付
订单创建时,它的 `payment_state` 为 `confirm`,当完成支付后,它的状态改为 `paid`。这里我们使用支付宝来支付订单。
我们需要安装支付宝的 [gem](https://github.com/chloerei/alipay)。
并且增加初始配置文件 `config/initializers/alipay.rb`,这里需要填写从支付宝商家服务 [申请](https://app.alipay.com/market/index.htm) 的 PID 和 KEY。
~~~
Alipay.pid = '申请的 PID'
Alipay.key = '申请的 KEY'
~~~
支付宝常用实时到账和担保交易,如果开通了支付宝快捷登陆,在使用实时到账时,可以扫描二维码支付。
支付成功后,通常设定为跳转回订单详细页面,支付宝会通过接口自动通知 `notify` 方法,我们应该在该方法中更新订单状态,并且通知支付宝是否成功,只需 `render text: "success"` 或 `render text: "fail"`。
这里有一份非常详尽的[支付宝集成](https://ruby-china.org/topics/26354)方案,欢迎参考。
### 5.2.5 带分页的数据列表
进入到“我的订单”页面,会有多条订单记录,这里需要对订单进行分页。常用的分页 gem 是 [will_paginate](https://github.com/mislav/will_paginate)。因为我们在使用 bootstrap,所以需要安装 [will_paginate-bootstrap](https://github.com/bootstrap-ruby/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](https://github.com/amatsuda/kaminari),以及 [datatable](https://datatables.net)
### 5.2.6 datatable
datatable 是传统分页方法的一个极好的替代,当数据量较多,且需要 ajax 加载数据时,可以使用 server 端 datatable 实现,具体请参考 [示例列表](#)。
当我们的订单数量巨大的时候,我们需要使用 datatable 的 server-side,来减轻分页加载时的压力。这里有一个[演示](https://github.com/liwei78/datatable-rails4-bootstrap-on-server-side),供大家参考。
- 写在前面
- 第一章 Ruby on Rails 概述
- Ruby on Rails 开发环境介绍
- Rails 文件简介
- 用户界面(UI)设计
- 第二章 Rails 中的资源
- 应用 scaffold 命令创建资源
- REST 架构
- 深入路由(routes)
- 第三章 Rails 中的视图
- 布局和辅助方法
- 表单
- 视图中的 AJAX 交互
- 模板引擎的使用
- 第四章 Rails 中的模型
- 模型的基础操作
- 深入模型查询
- 模型中的关联关系
- 模型中的校验
- 模型中的回调
- 第五章 Rails 中的控制器
- 控制器中的方法
- 控制器中的逻辑
- 第六章 Rails 的配置及部署
- Assets 管理
- 缓存及缓存服务
- 异步任务及邮件发送
- I18n
- 生产环境部署
- 常用 Gem
- 写在后面