Skip to content

Latest commit

 

History

History
1086 lines (819 loc) · 28.6 KB

09 返回值与分页.md

File metadata and controls

1086 lines (819 loc) · 28.6 KB

使用rails6 开发纯后端 API 项目

返回值与分页


本节目标
  • 返回值
    • 引入返回值定制方案并编写公共方法;
    • 优化之前模块的返回值信息的定制;
    • 修改之前的测试并通过测试。
  • 分页
    • 引入分页功能并编写公共方法;
    • 优化之前模块的代码;
    • 编写分页测试并完成测试。

1. 文件版本管理

进入新的一章,最重要的事儿就是切换新分支开发。

$ git checkout -b chapter07

我们已经完成了用户和商铺模块的开发,完成了吗?确切地说是已经完成了一部分。

前面的开发过程中至少还有两个问题需要我们认真研究,第一个是返回值的字段和格式,第二个是分页。

首先我们来聊聊返回值的相关问题。

2. 返回值

对于前后端分离开发而言,API的返回值十分发重要,因为返回值不仅仅携带了数据这么简单,好的返回值的数据结构应该是 统一风格的结构清晰的字段安全的节省资源的。Restful风格的API通常返回的是JSON类型的数据结构,我们也不例外,所以我们的目标就是要创建 统一风格的结构清晰的字段安全的节省资源的 JSON 数据结构。在前面的章节中,我们使用了自定义的转换方法,在具体的实现中我们“笨拙”的手动对数据格式进行了拼装,例如下面的代码:

class Api::V1::ShopsController < ApplicationController
  def show
    @data = set_response_data(@shop)
    render json: {error_code:0, data:@data, message:'ok'}, status: 200
  end

  private
	#......
    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

以上代码是上一章商铺模块商铺详情展示的主体代码和返回值data部分拼装数据的方法。其实这种手动拼装的方法从灵活度上个人感觉还是不错的,也能统一风格,结构也很清晰,而且字段都是经过我们筛选的,也能保证安全。然而,最重要的问题是我们需要编写大量的这种侵入在控制器内的代码,当然我们可以使用编程技巧尝试解决这个问题,但是这其实是个公共需求,实际上是有解决方案的。

不过在引出解决方案前,我们先就上面我们提到的 “好的返回值的数据结构” 简单分析下原因:

  • 统一风格

    所有返回值的数据结构基本分为两类:请求成功的返回值,请求失败的返回值。无论是成功的还是失败的返回值,无论是哪一个API的返回值其基本结构风格都应该是统一的,包括参数名,参数数量,参数位置。统一的风格能够让项目前后端配合的更和谐。

  • 结构清晰

    返回值的数据应该是结构化的,每一部分的含义不能有歧义和混淆。从数据结构上就能明确体现数据之间的关系,是包含还是属于,是单数还是列表,都应该从结构上明确体现。和上面一样,结构清晰的返回值能够让项目前后端配合的更和谐。

  • 字段安全

    首先我们要明确返回值通常是数据库中存储的数据,然而不是所有的数据库字段都适合被返回给客户端,尤其是一些敏感字段,比如用户表中的存储密码信息的字段和其值,我们一定不能查询出来返回给客户端。还有一些字段通常情况下是没有必要返回的字段,比如创建时间字段。只返回必要的、没有安全隐患的字段是保证我们数据安全的必要条件!

  • 节省资源

    返回值是要通过网络传输给客户端的,所以我们应该尽量让返回值的体积变小,在前面三个约定的前提下,这部分实际上是可操作性最强的!也是我们引入现有方法比较重要的原因。

好了,下面就让我们引出本章的 “返回值担当“ -- jsonapi-serializer

2.1 jsonapi-serializer

2.1.1 简介

​ 首先我们先简单了解下 jsonapi-serializer 是什么。

jsonapi-serializer 是一个符合 JSON:API 规范的 Ruby 对象编译器,实际上就是能够很方便的把 Ruby 对象构造成特定格式的 json 数据。这里的特定格式类似于下面例子:

{
  "data": {
    "type": "user",
    "id": "1",
    "attributes": {
      "email": "[email protected]"
    },
    "relationships": {
      "products": {
        "data": [
          { "type": "product", "id": "1" },
        ]
      }
    }
  },
  "include": [{
    "type": "product",
    "id": "1",
    "attributes": {
      "name": "iphone",
    }
  }],
  "links": {
    "self": "http://example.com/products/1",
    "next": "http://example.com/products/2",
    "last": "http://example.com/products/10"
  }
}
2.1.2 特点
  • 支持通过定义序列化文件来自定义返回字段
  • 支持 互相 引用
  • 支持belongs_tohas_manyhas_one
  • 支持缓存
2.1.3 API
  • 序列化对象到hash

    MovieSerializer.new(模型对象[, 可选参数]).serializable_hash
  • 序列化对象到 Json

    MovieSerializer.new(模型对象[, 可选参数]).serializable_hash.to_json

2.2 在项目中引入 jsonapi-serializer

2.2.1 首先编辑Gemfile,添加 jsonapi-serializer

Gemfile

gem 'jsonapi-serializer'
2.2.2 执行命令安装
$ bundle

通过以上命令我们就可以使用 jsonapi-serializer 了。

2.3 项目改造

2.3.1 用户模块

思路分析

首先要使用 jsonapi-serializer 提供的命令生成用户序列化的定义文件;

然后就可以改造用户控制器中的代码;

最后我们需要修改并通过测试。


  • 生成用户序列化文件: 用户信息只返回email即可

    $ rails generate serializer User email
    Running via Spring preloader in process 4089
          create  app/serializers/user_serializer.rb

    我们可以查看 app/serializers/user_serializer.rb 文件

    class UserSerializer
      include JSONAPI::Serializer
      attributes :email
    end

    其中的 attributes 就是标示我们要返回当前对象的那些字段, 这里我们需要也返回 role字段,所以我们手动添加上:

    # app/serializers/user_serializer.rb
    class UserSerializer
      include JSONAPI::Serializer
      attributes :email, :role
    end
  • 修改用户控制器代码

    app/controllers/api/v1/users_controller.rb

    class Api::V1::UsersController < ApplicationController
    
      def index
        @users  = User.offset(@page).limit(@per_page)
        render json:serializer_user(@users), status: 200
      end
    
      def show
        render json: serializer_user(@user), status: 200
      end
    
      def create
        @user = User.new(user_params)
        if @user.save
          render json: serializer_user(@user), status: 201
        else
          render json: {error_code:500, message:@user.errors}, status: 201
        end
      end
    
      def update
        if @user.update(user_params)
          render json: serializer_user(@user), status: 202
        else
          render json: {error_code:500, message:@user.errors}, status: 202
        end
      end
    
      private
      	# ......
    
        def serializer_user(user, error_code=0, message='ok')
          user_hash =  UserSerializer.new(user).serializable_hash
          user_hash['error_code'] = error_code
          user_hash['message'] = message
          return user_hash
        end
    end

    以上代码主要是定义了私有方法serializer_user, 然后在每个处理方法中调用该方法处理用户实例模型。

    现在如果访问 /api/v1/users 可以获得下列数据信息:

    {
        "error_code": 0,
        "data": [
            {
                "id": "1",
                "type": "user",
                "attributes": {
                    "email": "[email protected]",
                    "role": 1
                }
            },
            {
                "id": "2",
                "type": "user",
                "attributes": {
                    "email": "[email protected]",
                    "role": 1
                }
            }
        ],
        "message": "ok"
    }
  • 修改测试

    如果现在直接运行测试,会报错,那是因为我们的返回值的格式变化了,我们可以修改我们的测试:

    require "test_helper"
    
    class Api::V1::UsersControllerTest < ActionDispatch::IntegrationTest
      test "show_success: should show user" do
        get api_v1_user_path(@user), as: :json
        json_response = JSON.parse(self.response.body)
        # 验证状态码
        assert_response 200
        # 验证返回数据
        assert_equal @user.email, json_response['data']['attributes']['email']
      end
    end

    这个容易修改,因为我们的返回值出现了变动,所以只修改测试中的 show_success: should show user 即可。

    现在测试

    $ rails test
    
    Finished in 2.116465s, 15.1196 runs/s, 18.8994 assertions/s.
    32 runs, 40 assertions, 0 failures, 0 errors, 0 skips
2.3.2 商铺模块修改

思路分析

首先要使用 jsonapi-serializer 提供的命令生成商铺序列化的定义文件;

然后就可以改造商户控制器中的代码;

最后我们需要修改并通过测试。


  • 生成商铺序列化文件:

    $ rails generate serializer Shop
    Running via Spring preloader in process 6384
          create  app/serializers/shop_serializer.rb

    我们可以查看 app/serializers/shop_serializer.rb 文件

    class ShopSerializer
      include JSONAPI::Serializer
      attributes
    end

    这里我们需要设定需要返回的字段:

    # app/serializers/shop_serializer.rb
    class ShopSerializer
      include JSONAPI::Serializer
      attributes :name, :products_count, :orders_count
      belongs_to :user
    end

    现在就体现出了 jsonapi-serializer 的优势

  • 修改商铺控制器代码

    app/controllers/api/v1/shops_controller.rb

    class Api::V1::ShopsController < ApplicationController
      # ... ...
    
      def index
        @shops  = Shop.offset(@page).limit(@per_page)
        render json: serializer_shop(@shops), status: 200
      end
    
      def show
        render json: serializer_shop(@shop), status: 200
      end
    
      def create
        @user = current_user
        @shop = Shop.new(shop_params)
        @shop.user = @user
        @shop.transaction do
          @user.role = 2
          if @shop.save! && @user.save!
            render json: serializer_shop(@shop), status: 201
          else
            render json: {error_code:500, message:@shop.errors}, status: 201
          end
        end
      end
    
      def update
        @user = current_user
        if @user.shop.update(shop_params)
          render json: serializer_shop(@shop), status: 201
        else
          render json: {error_code:500, message:@shop.errors}, status: 201
        end
      end
    
    
      private
        def serializer_shop(shop, error_code=0, message='ok')
          options = { include: [:user] }
          shop_hash =  ShopSerializer.new(shop, options).serializable_hash
          shop_hash['error_code'] = error_code
          shop_hash['message'] = message
          return shop_hash
        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
    

    以上代码主要是定义了私有方法serializer_shop, 然后在每个处理方法中调用该方法处理商铺实例模型,此时我们可以删除掉方法set_response_data

    需要注意的是:在 方法中我们添加了options 参数,并把它当做 ShopSerializer.new的第二个参数,用以实现返回值包含具体的商户信息。

    现在如果访问 /api/v1/shops 可以获得下列数据信息:

    {
        "data": [
            {
                "id": "1",
                "type": "shop",
                "attributes": {
                    "name": "test1",
                    "products_count": 0,
                    "orders_count": 0
                },
                "relationships": {
                    "user": {
                        "data": {
                            "id": "1",
                            "type": "user"
                        }
                    }
                }
            }
        ],
        "included": [
            {
                "id": "1",
                "type": "user",
                "attributes": {
                    "email": "[email protected]",
                    "role": 1
                }
            }
        ],
        "error_code": 0,
        "message": "ok"
    }
  • 修改测试

    如果现在直接运行测试,会报错,那是因为我们的返回值的格式变化了,我们可以修改我们的测试:

    require "test_helper"
    
    class Api::V1::ShopsControllerTest < ActionDispatch::IntegrationTest
    
      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']['attributes']['name']
      end
    
    end

    这个容易修改,因为我们的返回值出现了变动,所以只修改测试中的 show_success: should show shop 即可。

    现在测试

    $ rails test
    
    Finished in 2.134784s, 14.9898 runs/s, 18.7373 assertions/s.
    32 runs, 40 assertions, 0 failures, 0 errors, 0 skips

2.4 代码版本管理

$ git add .
$ git commit -m "add jsonapi-serializer"

3. 分页

我们在已经开发的用户和商铺模块中使用了简单的自己写的分页,但是分页实际上还需要有很多其它的需求,比如总数,上页,下页,最后一页等。我们本节将引入一个分页工具 will_paginate 来帮助我们实现更多复杂的操作。

3.1 will_paginate

3.1.1 简介

will_paginate是一个强大的与Rails深度集成的分页库。当然还有很多其它的实现方案,也可以使用。

3.1.2 特点
  • 使用简单
  • 功能强大
  • 与Rails深度集成
3.1.3 API
  • paginate(page: 第几页, per_page: 每一页显示数量)
  • page(第几页)
  • per_page(每一页显示数)
  • total_pages  # 总页数
  • previous_page # 上一页
  • next_page # 下一页

更多api请查阅官方文档

3.1.4 配置
  • 模型内配置

    # Post 模型 app/models/post.rb
    class Post
      self.per_page = 10
    end
  • 全局配置

    # app/models/application_record.rb
    WillPaginate.per_page = 10

3.2 项目中使用 will_paginate

3.2.1 首先编辑Gemfile,添加 will_paginate

Gemfile

gem 'will_paginate'
3.2.2 执行命令安装
$ bundle

通过以上命令我们就可以使用 will_paginate 了。

will_paginate的使用很简单,可以在模型对象上像使用模型的类方法一样直接使用即可,例如:

User.page(current_page).per_page(per_page)
3.3.3 全局配置默认每页数量

app/models/application_record.rb

class ApplicationRecord < ActiveRecord::Base
  # ......
  WillPaginate.per_page = 20
end

我们这里设置默认值为20,这就相当于以后调用分页会自动把每页设定为显示20条。

3.3.4 编写全局方法

分页肯定是多出用到,而且我们这里一般是在控制器中,其实我们在前面已经自己定义了 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

    def current_page
      _to_i(params[:page], 1)
    end

    def per_page
      _to_i(params[:per_page], 10)
    end

    def get_links_serializer_options links_paths, collection 
      {
        links: {
          first: send(links_paths, page: 1),
          last: send(links_paths, page: collection.total_pages),
          prev: send(links_paths, page: collection.previous_page),
          next: send(links_paths, page: collection.next_page),
        }
      }
    end
end

重点说明下 get_links_serializer_options 方法,它的出现是为了在返回值中添加相关的分页信息,这个需要作为我们在上面引入的分页方法的第二个参数。

3.3 项目改造

3.3.1 控制器全局引入分页

分页基本上要在所有的控制器含有列表的地方使用,所以我们在控制器的全局引入分页的公共方法。

app/controllers/application_controller.rb

class ApplicationController < ActionController::API
  # ... ...
  include Paginable
  # ... ...
end

其实我们之前已经引入过了,如果没有引入,需要在这里引入一下。

3.3.2 用户模块
3.3.2.1 控制器

在用户模块中,我们在用户列表的地方使用到了分页,所以我们需要的就是修改 users#index方法:

app/controllers/api/v1/users_controller.rb

def index
    @users  = User.page(current_page).per_page(per_page)
    options = get_links_serializer_options 'api_v1_users_path', @users
    render json: serializer_user(@users,0,'ok',options), status: 200
end

上面的代码,我们传入了options参数,是因为我们也要将分页信息加入到返回的信息中,所以我们需要修改 users#serializer_user:

def serializer_user(user, error_code=0, message='ok', options={})
    user_hash =  UserSerializer.new(user, options).serializable_hash
    user_hash['error_code'] = error_code
    user_hash['message'] = message
    return user_hash
end

现在,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_login?, expect: [:destroy]
  before_action :check_admin, only: [:index, :destroy]
  before_action :check_admin_or_owner, only: [:update]
    

  def index
    @users  = User.page(current_page).per_page(per_page)
    options = get_links_serializer_options 'api_v1_users_path', @users
    render json: serializer_user(@users,0,'ok',options), status: 200
  end

  def show
    render json: serializer_user(@user), status: 200
  end

  def create
    @user = User.new(user_params)
    if @user.save
      render json: serializer_user(@user), status: 201
    else
      render json: {error_code:500, message:@user.errors}, status: 201
    end
  end

  def update
    if @user.update(user_params)
      render json: serializer_user(@user), 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

    def serializer_user(user, error_code=0, message='ok', options={})
      user_hash =  UserSerializer.new(user, options).serializable_hash
      user_hash['error_code'] = error_code
      user_hash['message'] = message
      return user_hash
    end
end

你会发现,有两行代码被我注释掉了:

  # before_action :set_per_page, only: [:index]
  # before_action :set_page, only: [:index]

这是之前我们自己实现分页所用的代码!现在可以删除掉。

现在访问 api/v1/users 获取的数据格式是这样的:

{
    "data": [
        {
            "id": "1",
            "type": "user",
            "attributes": {
                "email": "[email protected]",
                "role": 1
            }
        },
        {
            "id": "2",
            "type": "user",
            "attributes": {
                "email": "[email protected]",
                "role": 1
            }
        },
        {
            "id": "3",
            "type": "user",
            "attributes": {
                "email": "[email protected]",
                "role": 1
            }
        },
        {
            "id": "4",
            "type": "user",
            "attributes": {
                "email": "[email protected]",
                "role": 0
            }
        }
    ],
    "links": {
        "first": "/api/v1/users?page=1",
        "last": "/api/v1/users?page=1",
        "prev": "/api/v1/users",
        "next": "/api/v1/users"
    },
    "error_code": 0,
    "message": "ok"
}
3.3.2.2 测试

我们可以为返回值的分页标签添加测试,由于分页实在用户列表中,所以测试可以在用户列表的测试中完成:

test "index_success: should show users" do
    get api_v1_users_path, 
    # 新增
    headers: { Authorization: JsonWebToken.encode(user_id: @user.id) },
    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

在这里,我们引入了一个新的语法 json_response.dig , 这是使用Hash#dig方法。它是一种Ruby方法,允许您在嵌套Hash中检索元素,避免在元素不存在时出现错误。

现在运行测试:

$ rails test

Finished in 2.329378s, 13.7376 runs/s, 18.8892 assertions/s.
32 runs, 44 assertions, 0 failures, 0 errors, 0 skips
3.3.3 商铺模块
3.3.3.1 控制器

在商铺模块中,我们在商铺列表的地方使用到了分页,所以我们需要的就是修改 shops#index方法:

app/controllers/api/v1/shops_controller.rb

def index
    @shops  = Shop.page(current_page).per_page(per_page)
    option = get_links_serializer_options 'api_v1_shops_path', @shops
    render json: serializer_shop(@shops, 0, 'ok', option), status: 200
end

上面的代码,我们传入了options参数,是因为我们也要将分页信息加入到返回的信息中,所以我们需要修改 shops#serializer_shop:

def serializer_shop(shop, error_code=0, message='ok', option = {})
    options = { include: [:user] }
    options = options.merge(option)
    shop_hash =  ShopSerializer.new(shop, options).serializable_hash
    shop_hash['error_code'] = error_code
    shop_hash['message'] = message
    return shop_hash
end

现在,shops_controller.rb 整体文件变为:

class Api::V1::ShopsController < ApplicationController
  # before_action :set_per_page, only: [:index]
  # before_action :set_page, only: [:index]
  before_action :set_shop, only: [:show, :update, :destroy]
  before_action :check_login, only: [:create, :update, :destroy]
  before_action :check_owner, only: [:update, :destroy]

  def index
    @shops  = Shop.page(current_page).per_page(per_page)
    option = get_links_serializer_options 'api_v1_shops_path', @shops
    render json: serializer_shop(@shops, 0, 'ok', option), status: 200
  end

  def show
    render json: serializer_shop(@shop), status: 200
  end

  def create
    @user = current_user
    @shop = Shop.new(shop_params)
    @shop.user = @user
    @shop.transaction do
      @user.role = 2
      if @shop.save! && @user.save!
        render json: serializer_shop(@shop), status: 201
      else
        render json: {error_code:500, message:@shop.errors}, status: 201
      end
    end
  end

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

  def destroy
    @shop.destroy
    head 204
  end

  private
    def set_shop
      @shop = Shop.includes(:user).find_by_id params[:id]
      @shop = @shop || {} 
    end

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

    def check_login
      head 401 unless current_user
    end

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

    def serializer_shop(shop, error_code=0, message='ok', option = {})
      options = { include: [:user] }
      options = options.merge(option)
      shop_hash =  ShopSerializer.new(shop, options).serializable_hash
      shop_hash['error_code'] = error_code
      shop_hash['message'] = message
      return shop_hash
    end
end

现在访问 api/v1/shops, 返回值:

{
    "data": [
        {
            "id": "1",
            "type": "shop",
            "attributes": {
                "name": "test1",
                "products_count": 0,
                "orders_count": 0
            },
            "relationships": {
                "user": {
                    "data": {
                        "id": "1",
                        "type": "user"
                    }
                }
            }
        }
    ],
    "included": [
        {
            "id": "1",
            "type": "user",
            "attributes": {
                "email": "[email protected]",
                "role": 1
            }
        }
    ],
    "links": {
        "first": "/api/v1/shops?page=1",
        "last": "/api/v1/shops?page=1",
        "prev": "/api/v1/shops",
        "next": "/api/v1/shops"
    },
    "error_code": 0,
    "message": "ok"
}
3.3.3.2 测试

我们可以为返回值的分页标签添加测试,由于分页实在商铺列表中,所以测试可以在商铺列表的测试中完成:

test "index_success: should show shops" do
    get api_v1_shops_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

现在运行测试:

$ rails test

Finished in 2.218713s, 14.4228 runs/s, 21.6342 assertions/s.
32 runs, 48 assertions, 0 failures, 0 errors, 0 skips

3.4 文件版本管理

$ git add .
$ git commit -m "git add will_paginate"

4. 文件版本管理

4.1 将本章代码合并到主干
$ git checkout master
$ git merge chapter07

5. 总结

我们本章引入了返回值定制和分页系统,可以使我们的工作更顺畅!下一节,我们将继续开发我们的简约商城系统的一个很重要的模块:商品模块!大家加油!