# 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
- 寫在后面