Skip to content

Latest commit

 

History

History
861 lines (590 loc) · 21.4 KB

08 商铺模块- 控制器相关功能开发.md

File metadata and controls

861 lines (590 loc) · 21.4 KB

使用rails6 开发纯后端 API 项目

商铺模块: 控制器


本节目标
  • 编写商铺控制器相关的行为;
  • 添加路允许通过路由控制商铺相关行为;
  • 编写商铺行为的单元测试,完成相关测试。

上一节我们已经实现了商铺模型的开发和测试。本节我们将继续开发商铺的相关功能,最终目标是基本实现用户对商铺的管理功能。

1. 功能分析

1.1 商铺模块要实现的功能
  • 查看商铺列表;
  • 注册一个商铺;
  • 查看指定商铺详情
  • 用户修改自己商铺信息
  • 用户自己删除自己的商铺;
1.2 要实现的方法
  • index: 返回商铺列表
  • create: 注册商铺
  • show: 查看指定商铺详情
  • update:用户修改自己商铺信息
  • destroy: 用户自己删除自己的商铺

2. 控制器相关功能开发

2.1 知识点预热

2.1.1 Rails 相关
  • 使用命令创建控制器

    $ rails generate controller 复数控制器名 

    该命令会生成控制器文件,以及控制器的测试文件。

2.2 创建shops控制器

$ rails generate controller api::v1::shops
Running via Spring preloader in process 2320
      create  app/controllers/api/v1/shops_controller.rb
      invoke  test_unit
      create    test/controllers/api/v1/shops_controller_test.rb

2.3 添加商铺相关方法的路由

2.3.1 修改路由文件

config/routes.rb

Rails.application.routes.draw do
  namespace :api, defaults: { format: :json } do
    namespace :v1 do
      # 其它路由
      resources :shops, only: [:index, :show, :create, :update, :destroy]
    end
  end
end

only 是限制当前只开放对这些方法的路由

2.3.2 在Rails console 中可以查看 shops控制器 的路由列表
$ rails routes -c shops
--[ Route 1 ]-------------------------------------------------
Prefix            | api_v1_shops
Verb              | GET
URI               | /api/v1/shops(.:format)
Controller#Action | api/v1/shops#index {:format=>:json}
--[ Route 2 ]-------------------------------------------------
Prefix            | 
Verb              | POST
URI               | /api/v1/shops(.:format)
Controller#Action | api/v1/shops#create {:format=>:json}
--[ Route 3 ]--------------------------------------------------
Prefix            | api_v1_shop
Verb              | GET
URI               | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#show {:format=>:json}
--[ Route 4 ]-------------------------------------------------
Prefix            | 
Verb              | PATCH
URI               | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#update {:format=>:json}
--[ Route 5 ]--------------------------------------------------
Prefix            | 
Verb              | PUT
URI               | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#update {:format=>:json}
--[ Route 6 ]-------------------------------------------------
Prefix            | 
Verb              | DELETE
URI               | /api/v1/shops/:id(.:format)
Controller#Action | api/v1/shops#destroy {:format=>:json}

2.4 商铺列表:index 方法开发


思路解析

获取商铺列表,可以直接使用Shop模型相关方法获取,既然是列表,还要注意分页和排序的情况。

路由是: api/v1/shops?page=1&per_page=10

返回值部分我们采用json数据类型的返回值,成功的http状态码:200。

至于测试部分,我们应该测试两个场景

  • 状态码应该是200
  • 返回值的商铺列表,但是有可能是空的,所以这个可以不测试

2.4.1 编写测试

test/controllers/api/v1/shops_controller_test.rb

require "test_helper"

class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
  test "index_success: should show shops" do
    get api_v1_shops_path, as: :json
    assert_response 200
  end
end

运行测试会显示一个失败,原因是找不到 index 方法, 这是当然的,我们还没有定义 index 方法。

$ rails test 
......
Error:
Api::V1::ShopsControllerTest#test_index_success:_should_show_shops:
DRb::DRbRemoteError: The action 'index' could not be found for Api::V1::ShopsController
2.4.2 定义shops#index方法

app/controllers/api/v1/shops_controller.rb

class Api::V1::ShopsController < ApplicationController
  before_action :set_per_page, only: [:index]
  before_action :set_page, only: [:index]

  def index
    @shops  = Shop.offset(@page).limit(@per_page)
    render json: {error_code:0, data:@shops, message:'ok'}, status: 200
  end

  private
    def _to_i(param, default_no = 1)
      param && param&.to_i > 0 ? param&.to_i : default_no.to_i
    end
	
    def set_page
      @page      = _to_i(params[:page], 1)
      @page      = set_per_page * (@page - 1)
    end
	
    def set_per_page
      @per_page  = _to_i(params[:per_page], 10)
    end
end

再次运行测试,这次测试通过了。

$ rails test
......
Finished in 2.230317s, 11.2092 runs/s, 13.4510 assertions/s.
25 runs, 30 assertions, 0 failures, 0 errors, 0 skips

我们先来提交我们的代码到git.

2.4.3 代码变更提交
$ git add .
$ git commit -m "add shops_controller index action"
2.4.4 代码优化

思路分析

实现了index方法,我们需要先暂停以下开发。因为同为 index 方法,用户的index方法和shop的index方法中关于分页有着完全一致的代码,我们可以预计,后面如果遇到列表分页也应该会用到一样的代码。遇到这样的情况,我们需要把公共代码提取出来维护。

首先由于这是在控制器内使用的代码,所以我们遵循通常的约定:把控制器的通用代码放到 控制器所在目录的 concerns 目录下面,我们可以创建一个和分页相关名称的文件存放代码。

然后在 application_controller.rb 文件中引入该文件供所有的控制器使用。

最后修改并删除掉分散在其它控制器内的重复代码,当然改代码,一定要重新测试。


  • 创建公共方法文件:

    $ touch app/controllers/concerns/paginable.rb
  • 编辑文件,把公共代码移植到文件内。

    app/controllers/concerns/paginable.rb

    module Paginable
      protected
        def _to_i(param, default_no = 1)
          param && param&.to_i > 0 ? param&.to_i : default_no.to_i
        end
    
        def set_page
          @page      = _to_i(params[:page], 1)
          @page      = set_per_page * (@page - 1)
        end
    
        def set_per_page
          @per_page  = _to_i(params[:per_page], 10)
        end
    end

    当然这里的代码使用权限使用了 protected 来约束,因为这些代码不仅仅当前类私用,而是要与子类共用。

  • application_controller 中引入 Paginable

    app/controllers/application_controller.rb

    class ApplicationController < ActionController::API
      # 其它代码
      include Paginable
      # 其它代码
    end
  • 删除到原来文件中的重复代码

    • app/controllers/api/v1/shops_controller.rb 删除重复代码后的文件内容

      class Api::V1::ShopsController < ApplicationController
        before_action :set_per_page, only: [:index]
        before_action :set_page, only: [:index]
      
        def index
          @shops  = Shop.offset(@page).limit(@per_page)
          render json: {error_code:0, data:@shops, message:'ok'}, status: 200
        end
      end
    • app/controllers/api/v1/users_controller.rb 删除重复代码后的文件内容

      class Api::V1::UsersController < ApplicationController
        before_action :set_per_page, only: [:index]
        before_action :set_page, only: [:index]
        before_action :set_user, only: [:show, :update, :destroy]
        before_action :check_admin, only: [:index, :destroy]
        before_action :check_admin_or_owner, only: [:update]
          
      
        def index
          @users  = User.offset(@page).limit(@per_page)
          render json: {error_code:0, data:@users, message:'ok'}, status: 200
        end
      
        def show
          render json: {error_code:0, data:@user, message:'ok'}, status: 200
        end
      
        def create
          @user = User.new(user_params)
          if @user.save
            render json: {error_code:0, data:@user, message:'ok'}, status: 201
          else
            render json: {error_code:500, message:@user.errors}, status: 201
          end
        end
      
        def update
          if @user.update(user_params)
            render json: {error_code:0, data:@user, message:'ok'}, status: 202
          else
            render json: {error_code:500, message:@user.errors}, status: 202
          end
        end
      
        def destroy
          @user.destroy
          render json: {error_code:0, message:'ok'}, status: 204
        end
      
        private
          def set_user
            @user = User.find_by_id params[:id].to_i
            @user = @user || {} 
          end
      
          def user_params
            params.require(:user).permit(:name, :email, :password)
          end
      
          def is_admin?
            current_user&.role == 0
          end
          
          def check_admin
            head 403 unless is_admin?
          end
      
          def is_owner?
            @user.id == current_user&.id
          end
      
          def check_admin_or_owner
            head 403 unless is_admin? || is_owner?
          end
      end
  • 再次测试代码

    $ rails test                                       
    
    Finished in 2.283189s, 10.9496 runs/s, 13.1395 assertions/s.
    25 runs, 30 assertions, 0 failures, 0 errors, 0 skips

    这样就保证了我们没有因为抽离代码引发错误。

  • 再次提交代码到版本管理

    $ git add .
    $ git commit -m "add paginable file"

2.5 商铺详情:show 方法开发


思路解析

获取指定商铺,路由是:get:api/v1/shops/:id

我们首先要获取参数中的商铺id,然后根据商铺id找到具体的商铺,返回商铺信息,成功状态码是200。

至于测试部分,我们应该测试两部分

  • 状态码应该是200

  • 返回值的商铺就是我们指定的商铺信息。返回值数据结构:

    {
        id:1, 
        name: "shop01", 
        products_count: 10, 
        orders_count: 10, 
        created_at: "2021-05-14 04:08:24.249180000 +0000", 
        owner:{
           id:1, 
           email:'[email protected]'
        }
        #......
    }

2.5.1 编写测试

test/controllers/api/v1/shops_controller_test.rb

class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @shop = shops(:one)
  end
  
  # ... ...
  
  test "show_success: should show shop" do
    get api_v1_shop_path(@shop), as: :json
    json_response = JSON.parse(self.response.body)
    # 验证状态码
    assert_response 200
    # 验证返回数据
    assert_equal @shop.name, json_response['data']['name']
  end
end

运行测试会显示一个失败,原因是找不到 show 方法, 这是当然的,我们还没有定义 show 方法。

$ rails test
Error:
Api::V1::ShopsControllerTest#test_show_success:_should_show_shop:
DRb::DRbRemoteError: The action 'show' could not be found for Api::V1::ShopsController
2.5.2 定义shops#show方法

app/controllers/api/v1/shops_controller.rb

class Api::V1::ShopsController < ApplicationController
  before_action :set_shop, only: [:show]

  def show
    @data = set_response_data(@shop)
    render json: {error_code:0, data:@data, message:'ok'}, status: 200
  end

  private
	#......
    def set_shop
      @shop = Shop.includes(:user).find_by_id params[:id]
      @shop = @shop || {} 
    end
    
    def set_response_data shop
      retrun {} unless shop.present?
      {
        id: shop.id, 
        name: shop.name, 
        products_count: shop.products_count, 
        orders_count: shop.orders_count, 
        created_at: shop.created_at, 
        owner:{
           id: shop.user.id, 
           email: shop.user.email
        }
      }
    end
end

再次运行测试,这次测试通过了。

$ rails test
Finished in 1.923310s, 13.5184 runs/s, 16.6380 assertions/s.
26 runs, 32 assertions, 0 failures, 0 errors, 0 skips
2.5.3 代码变更提交
$ git add .
$ git commit -m "add shops_controller show action"

2.6 注册商铺:create方法开发


思路解析

新建商铺,我们的路由是:post:api/v1/shops

我们的参数应该类似: {shop:{name:'shop_name', products_count:10, orders_count:10}}

要创建商铺,首先要求用户是登录的状态!我们可以获取到当前登录的用户信息。

然后要创建一个商铺对象来接受参数,然后保存!

如果保存成功,则返回成功的信息;否则,返回失败的信息。

至于测试部分,我们应该测试两个场景

  • 成功:使用有效的token,合法的参数
    • 状态码应该是 201
    • 添加成功后商铺的总数增加 1。
  • 失败:不登录
    • 状态码是 401

2.5.1 编写测试

test/controllers/api/v1/shops_controller_test.rb

class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @shop = shops(:one)
    @user_5 = users(:five)
  end
    
  test "create_success: should create shop with token" do
    # 验证某个值变化了
    assert_difference('Shop.count', 1) do
      post api_v1_shops_path, 
        headers: { Authorization: JsonWebToken.encode(user_id: @user_5.id) },
        params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
        as: :json
    end

    assert_response 201
  end
    
  test "create_forbiden: should not create shop without token" do
    post api_v1_shops_path, 
      params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
      as: :json
    assert_response 401
  end
end

运行测试会显示失败,原因是找不到 create 方法, 这是当然的,我们还没有定义 create 方法。

$ rails test
Error:
Api::V1::ShopsControllerTest#test_create_forbiden:_should_not_create_shop_without_token:
DRb::DRbRemoteError: The action 'create' could not be found for Api::V1::ShopsController
2.5.2 定义shops#create方法

app/controllers/api/v1/shops_controller.rb

class Api::V1::ShopsController < ApplicationController
  before_action :check_login, only: [:create]
    
  def create
    @user = current_user
    @shop = Shop.new(shop_params)
    @shop.user = @user
    @shop.transaction do
      @user.role = 2
      if @shop.save! && @user.save!
        @data = set_response_data(@shop)
        render json: {error_code:0, data:@data, message:'ok'}, status: 201
      else
        render json: {error_code:500, message:@shop.errors}, status: 201
      end
    end
  end

  private
    def shop_params
      params.require(:shop).permit(:name, :products_count, :orders_count)
    end

    def check_login
      head 401 unless current_user
    end
end

再次运行测试,这次测试通过了。

$ rails test
......
Finished in 2.027363s, 13.8110 runs/s, 17.2638 assertions/s.
28 runs, 35 assertions, 0 failures, 0 errors, 0 skips

2.7 修改商铺信息:update方法


思路解析

修改用户,路由是:put:api/v1/shops/:id

我们的参数应该类似: {shop:{name:'shop_name', products_count:10, orders_count:10}}

首先要利用商铺id找到对应商铺,然后要验证当前用户是否是店铺的主人,验证通过,然后把接受到的商铺信息保存,完成修改!

如果保存成功,则返回成功的信息;否则,返回失败的信息。

至于测试部分,我们应该测试两种场景:

  • 成功:当前用户是店铺的拥有者
    • 状态码: 202
  • 失败: 当用户不是店铺拥有者
    • 状态码: 403

2.7.1 编写测试

test/controllers/api/v1/shops_controller_test.rb

require "test_helper"

class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
  test "update_success: should update shop with owner" do
    put api_v1_shop_path(@shop), 
      headers: { Authorization: JsonWebToken.encode(user_id: @shop.user.id) },
      params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
      as: :json
  
    assert_response 201
  end

  test "update_forbidden: forbidden update shop without owner" do
    put api_v1_shop_path(@shop), 
      headers: { Authorization: JsonWebToken.encode(user_id: @user_5.id) },
      params: {shop:{name:'shop_name', products_count:10, orders_count:10}},
      as: :json
  
    assert_response 403
  end
end

运行测试会显示一个失败,原因是找不到 create 方法, 这是当然的,我们还没有定义 show 方法。

$ rails test
Error:
Api::V1::ShopsControllerTest#test_update_forbidden:_forbidden_update_shop_without_owner:
DRb::DRbRemoteError: The action 'update' could not be found for Api::V1::ShopsController
2.7.2 定义shops#update方法

app/controllers/api/v1/shops_controller.rb

class Api::V1::ShopsController < ApplicationController
  before_action :set_shop, only: [:show, :update]
  before_action :check_login, only: [:create, :update]
  before_action :check_owner, only: [:update]

  def update
    @user = current_user
    if @user.shop.update(shop_params)
      @data = set_response_data(@shop)
      render json: {error_code:0, data:@data, message:'ok'}, status: 201
    else
      render json: {error_code:500, message:@shop.errors}, status: 201
    end
  end

  private
    def check_owner
      head 403 unless current_user.id == @shop.user.id
    end
end

再次运行测试,这次测试通过了。

$ rails test
......
Finished in 2.022039s, 14.8365 runs/s, 18.2984 assertions/s.
30 runs, 37 assertions, 0 failures, 0 errors, 0 skips

2.8 删除商铺:destroy方法


思路解析

删除商铺,我们的路由是:delete:api/v1/shops/:id

首先要利用商铺id找到对应商铺,判断当前用户是否是店铺拥有者,然后直接删除即可!

如果删除成功,则返回成功的信息;否则,返回失败的信息。

至于测试部分,我们应该测试两种场景

  • 成功:拥有者删除
    • 状态码应该是 204
    • 删除成功后用户的总数减少1。
  • 失败:使用非商铺拥有着登录
    • 状态码:403

2.8.1 编写测试

test/controllers/api/v1/shops_controller_test.rb

require "test_helper"

class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest

  test "delete_success: should delete shop with owner" do
    assert_difference('Shop.count', -1) do
      delete api_v1_shop_path(@shop), 
        headers: { Authorization: JsonWebToken.encode(user_id: @shop.user.id) },
        as: :json
    end
  
    assert_response 204
  end

  test "delete_forbidden: forbidden delete shop without owner" do
    delete api_v1_shop_path(@shop), 
      headers: { Authorization: JsonWebToken.encode(user_id: @user_5.id) },
      as: :json
  
    assert_response 403
  end
end

运行测试会显示一个失败,原因是找不到 create 方法, 这是当然的,我们还没有定义 show 方法。

$ rails test
Error:
Api::V1::ShopsControllerTest#test_destroy_success:_should_destroy_shop:
DRb::DRbRemoteError: The action 'destroy' could not be found for Api::V1::ShopsController
2.7.2 定义shops#destroy方法

app/controllers/api/v1/shops_controller.rb

class Api::V1::ShopsController < ApplicationController
  before_action :set_shop, only: [:show, :update, :destroy]
  before_action :check_login, only: [:create, :update, :destroy]
  before_action :check_owner, only: [:update, :destroy]
  def destroy
    @shop.destroy
    head 204
  end
end

再次运行测试,这次测试通过了。

$ rails test
Finished in 2.117606s, 15.1114 runs/s, 18.4170 assertions/s.
32 runs, 39 assertions, 0 failures, 0 errors, 0 skip

2.8 文件版本管理

2.8.1 添加所有变动文件到git管理
$ git add .
2.8.2 提交所有变动文件
$ git commit -m "set shops controller and routes"
2.8.3 合并版本到主分支
$ git checkout master
$ git merge chapter06

3. 总结

我们本节完成了商铺控制器的相关方法,并设置了对应路由提供用户在外部直接访问。但就我们今天实现的功能,实际上还是有很多不方便的地方,比如分页,比如json返回值,下一节我们就这两个问题展开探讨和解决。

同志们继续加油!一定要跟上脚步,认真练习!