Skip to content

Latest commit

 

History

History
779 lines (536 loc) · 18.6 KB

11 商品模块- 控制器相关功能开发.md

File metadata and controls

779 lines (536 loc) · 18.6 KB

使用rails6 开发纯后端 API 项目

商品模块: 控制器


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

上一节我们已经实现了商品模型的验证并实现密码加密。本节我们将继续开发商品相关功能,最终目标是基本实现商品的管理功能。

1. 功能分析

1.1 商品模块要实现的功能
  • 查看商品列表;
  • 增加一个商品;
    • 从excel导入商品:上传文件到服务器,解析把excel数据生成数组,生成过程中验证数据完整性,客户端最好可以完成该操作
  • 查看指定商品详情
  • 商铺修改某个商品信息
  • 商铺删除某个商品;
1.2 要实现的方法
  • index: 返回商品列表
  • create: 新增商品
  • show: 返回指定的商品信息
  • update:修改指定商品
  • destroy: 删除指定商品

2. 控制器相关功能开发

2.1 知识点预热

2.1.1 Rails 相关

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

2.2 创建products控制器

$ rails generate controller api::v1::products
Running via Spring preloader in process 2280
      create  app/controllers/api/v1/products_controller.rb
      invoke  test_unit
      create    test/controllers/api/v1/products_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 :products, only: [:index, :show, :create, :update, :destroy]
    end
  end
end

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

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

2.4 商品列表:index 方法开发


思路解析

获取商品列表,可以直接使用product模型相关方法获取,这里要注意分页和排序的情况。

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

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

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

  • 状态码应该是200
  • 返回值的商品列表,但是有可能是空的,所以这个可以不测试
  • 返回值包含分页数据。

2.4.1 编写测试

test/controllers/api/v1/products_controller_test.rb

require "test_helper"

class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
  test "index_success: should show products" do
    get api_v1_products_path, as: :json
    assert_response 200

    # 测试分页
    json_response = JSON.parse(response.body, symbolize_names:true)
		assert_not_nil json_response.dig(:links, :first)
		assert_not_nil json_response.dig(:links, :last)
		assert_not_nil json_response.dig(:links, :prev)
		assert_not_nil json_response.dig(:links, :next)
  end
end

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

$ rails test                 

.........E

Error:
Api::V1::ProductsControllerTest#test_index_success:_should_show_products:
DRb::DRbRemoteError: The action 'index' could not be found for Api::V1::ProductsController
Did you mean?  current_user (AbstractController::ActionNotFound)
    test/controllers/api/v1/products_controller_test.rb:5:in `block in <class:ProductsControllerTest>'
......

Finished in 2.637243s, 14.4090 runs/s, 20.0967 assertions/s.
38 runs, 53 assertions, 0 failures, 1 errors, 0 skips
2.4.2 定义序列化文件

因为要json序列化,所以先要创建 product 的序列化文件

$ rails generate serializer Product title price published shop_id
Running via Spring preloader in process 3784
      create  app/serializers/product_serializer.rb

修改 app/serializers/product_serializer.rb 文件:

class ProductSerializer
  include JSONAPI::Serializer
  attributes :title, :price, :published, :shop_id
  # 添加模型关系
  belongs_to :shop
end

同时也要修改 Shop 的序列化文件,添加模型关系:

class ShopSerializer
  include JSONAPI::Serializer
  attributes :name, :products_count, :orders_count
  belongs_to :user
  has_many :products
end
2.4.3 定义products#index方法

app/controllers/api/v1/products_controller.rb

class Api::V1::ProductsController < ApplicationController
  def index
    @products  = Product.page(current_page).per_page(per_page)
    option = get_links_serializer_options 'api_v1_products_path', @products
    render json: serializer_product(@products, 0, 'ok', option), status: 200
  end

  private
    def serializer_product(product, error_code=0, message='ok', option = {})
      product_hash =  ProductSerializer.new(product, option).serializable_hash
      product_hash['error_code'] = error_code
      product_hash['message'] = message
      return product_hash
    end
end

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

$ rails test
# Running:
........
Finished in 2.228403s, 17.0526 runs/s, 26.0276 assertions/s.
38 runs, 58 assertions, 0 failures, 0 errors, 0 skips

请求 api/v1/products得到的数据:

{
    "data": [
        {
            "id": "1",
            "type": "product",
            "attributes": {
                "title": "123",
                "price": 1,
                "published": 1,
                "shop_id": 1
            },
            "relationships": {
                "shop": {
                    "data": {
                        "id": "1",
                        "type": "shop"
                    }
                }
            }
        }
    ],
    "links": {
        "first": "/api/v1/products?page=1",
        "last": "/api/v1/products?page=1",
        "prev": "/api/v1/products",
        "next": "/api/v1/products"
    },
    "error_code": 0,
    "message": "ok"
}
2.4.3 代码变更提交
$ git add .
$ git commit -m "add products_controller index action"

2.5 商品详情:show 方法开发


思路解析

获取指定商品,路由是:get:api/v1/products/:id

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

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

  • 状态码应该是200

  • 返回值的商品就是我们指定的商品信息。返回值数据类型:

    {
        "id": "1",
        "type": "product",
        "attributes": {
            "title": "123",
            "price": 1,
            "published": 1,
            "shop_id": 1
        },
        "relationships": {
            "shop": {
                "data": {
                    "id": "1",
                    "type": "shop"
                }
            }
        }
    }

2.5.1 编写测试

test/controllers/api/v1/products_controller_test.rb

class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @product = products(:one)
  end
  
  # ... ...
  
  test "show_success: should show product" do
    get api_v1_product_path(@product), as: :json
    json_response = JSON.parse(self.response.body, symbolize_names:true)
    # 验证状态码
    assert_response 200
    # 验证返回数据
    assert_equal @product.title, json_response.dig(:data, :attributes, :title)
  end
end

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

$ rails test

Error:
Api::V1::ProductsControllerTest#test_show_success:_should_show_product:
DRb::DRbRemoteError: The action 'show' could not be found for Api::V1::ProductsController
2.5.2 定义products#show方法

app/controllers/api/v1/products_controller.rb

class Api::V1::productsController < ApplicationController
  before_action :set_product, only: [:show]

  def show
    render json: serializer_product(@product), status:200
  end

  private
	#......
    def set_product
      @product = Product.find_by_id params[:id].to_i
      @product = @product || {} 
    end
end

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

$ rails test
Finished in 0.207461s, 43.3816 runs/s, 48.2018 assertions/s.
9 runs, 10 assertions, 0 failures, 0 errors, 0 skips

请求api/v1/products/1获取数据:

{
    "data": {
        "id": "1",
        "type": "product",
        "attributes": {
            "title": "123",
            "price": 1,
            "published": 1,
            "shop_id": 1
        },
        "relationships": {
            "shop": {
                "data": {
                    "id": "1",
                    "type": "shop"
                }
            }
        }
    },
    "error_code": 0,
    "message": "ok"
}
2.5.3 代码变更提交
$ git add .
$ git commit -m "add products_controller show action"

2.6 新建商品:create方法开发


思路解析

新建商品,我们的路由是:post:api/v1/products

我们的参数应该类似: {product:{title:'first test', price:1, published:1}}

这个接口应该是要求用户登录状态下操作的,所以一些信息我们需要通过用户信息自动获取,比如 shop_id

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

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

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

  • 状态码应该是 201
  • 添加成功后商品的总数增加 1。

2.6.1 编写测试

test/controllers/api/v1/products_controller_test.rb

require "test_helper"

class Api::V1::ProductsControllerTest < ActionDispatch::IntegrationTest
  setup do
    @product = products(:one)
    @user = users(:two)
  end
  # ... ... 
  test "create_success: should create product" do
    # 验证某个值变化了
    assert_difference('Product.count', 1) do
      post api_v1_products_path, 
        headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
        params: {product:{title:'first test', price:1, published:1}},
        as: :json
    end

    assert_response 201
  end
end

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

$ rails test

Error:
Api::V1::ProductsControllerTest#test_create_success:_should_create_product:
DRb::DRbRemoteError: The action 'create' could not be found for Api::V1::ProductsController
2.6.2 定义products#create方法

app/controllers/api/v1/products_controller.rb

class Api::V1::productsController < ApplicationController
  def create
    @product = current_user&.shop&.products&.build(product_params)
    if @product.save
      render json: serializer_product(@product), status: 201
    else
      render json: {error_code:500, message:@product.errors}, status: 201
    end
  end

  private
    def product_params
      params.require(:product).permit(:title, :price, :published)
    end
end

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

$ rails test
Finished in 2.333086s, 17.1447 runs/s, 26.5742 assertions/s.
40 runs, 62 assertions, 0 failures, 0 errors, 0 skips
2.6.3 代码变更提交
$ git add .
$ git commit -m "add products_controller create action"

2.7 修改商品:update方法


思路解析

修改商品,路由是:put:api/v1/products/:id

我们的参数应该类似: {product:{title:'first test', price:1, published:1}}

首先要利用商品id找到对应商品,然后把接受到的商品信息赋值给该商品并保存,完成修改!

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

但是要注意,该操作需要用户登录并且要修改的商品必须是当前用户的商铺内的商品才可以。

至于测试部分,我们应该测试状态码即可

  • 状态码应该是 202
  • 错误的用户返回403

2.7.1 编写测试

test/controllers/api/v1/products_controller_test.rb

class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
  test "update_success: should update product" do
    put api_v1_product_path(@product), 
      headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
      params: {product:{title:'first test', price:1, published:1}},
      as: :json
  
    assert_response 202
  end
    
  test "update_forbidden: forbidden update product cause unonwer" do
    put api_v1_product_path(@product), 
      headers: { Authorization: JsonWebToken.encode(user_id: users(:one).id) },
      params: {product:{title:'first test', price:1, published:1}},
      as: :json
  
    assert_response 403
  end
end

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

$ rails test

Error:
Api::V1::ProductsControllerTest#test_update_success:_should_update_product:
DRb::DRbRemoteError: The action 'update' could not be found for Api::V1::ProductsController
2.7.2 定义products#update方法

app/controllers/api/v1/products_controller.rb

class Api::V1::ProductsController < ApplicationController
  before_action :set_product, only: [:update, :show]
  before_action :check_owner, only: [:update]

  # ... ...

  def update
    if @product.update(product_params)
      render json: serializer_product(@product), status: 202
    else
      render json: {error_code:500, message:@product.errors}, status: 202
    end
  end

  private
	# ... ...
    
    def check_owner
      head 403 unless current_user&.shop&.products&.exists?(id:params[:id].to_i)
    end
end

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

$ rails test

Finished in 2.222468s, 18.8979 runs/s, 28.7968 assertions/s.
42 runs, 64 assertions, 0 failures, 0 errors, 0 skips
2.7.3 代码变更提交
$ git add .
$ git commit -m "add products_controller update action"

2.8 删除商品:destroy方法


思路解析

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

首先要利用商品id找到对应商品,然后直接删除即可!

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

当然本操作也要用户登录并且商品是用户店铺内商品

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

  • 成功
    • 状态码应该是 204
    • 删除成功后商品的总数减少1。
  • 失败
    • 错误用户返回403

2.8.1 编写测试

test/controllers/api/v1/products_controller_test.rb

class Api::V1::productsControllerTest < ActionDispatch::IntegrationTest
  test "destroy_success: should destroy product" do
    # 验证某个值变化了
    assert_difference('Product.count', -1) do
      delete api_v1_product_path(@product), 
        headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
        as: :json
    end

    assert_response 204
  end

  test "destroy_forbidden: forbidden destroy product cause unowner" do
    delete api_v1_product_path(@product), 
      headers: { Authorization: JsonWebToken.encode(user_id: users(:one).id) },
      as: :json
    
    assert_response 403
  end
end

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

$ rails test

Error:
Api::V1::ProductsControllerTest#test_destroy_forbidden:_forbidden_destroy_product_cause_unowner:
DRb::DRbRemoteError: The action 'destroy' could not be found for Api::V1::ProductsController
2.8.2 定义products#destroy方法

app/controllers/api/v1/products_controller.rb

class Api::V1::productsController < ApplicationController
  before_action :check_owner, only: [:update, :destroy]
  before_action :set_product, only: [:show, :update, :destroy]
  
  def destroy
    @product.destroy
    head 204
  end
end

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

$ rails test

Finished in 2.122942s, 20.7260 runs/s, 31.5600 assertions/s.
44 runs, 67 assertions, 0 failures, 0 errors, 0 skips
2.8.3 代码变更提交
$ git add .
$ git commit -m "add products_controller destroy action"

2.9 文件版本管理

2.9.1 合并版本到主分支
$ git checkout master
$ git merge chapter08

3. 总结

我们本节完成了商品控制器的相关方法,并设置了对应路由提供商品在外部直接访问。 现在有了商铺,有了商品,下一节我们就可以让我们的用户购物了,下一节我们一起开发订单模块!