订单实作思路-上篇

前言

本文是[购物车实作思路](/Users/xyy/Documents/知识专题/ruby on rails/全栈营学习/学习总结/购物车实作思路.md)的续篇,由于消费者确定好购物车中的商品和数量,接下来就是要下单,所以本文主要来描述订单实作的思路。

1.建立订单结账页

(1)设定订单结账页面的路径

resources :carts do
collection do
delete :clean
+ post :checkout
end
end

(2)修改购物车carts/index页面,为"确认结账"按钮,添加路径

- <%= link_to("确认结账","#",method: :post)  %>
+ <%= link_to("确认结账",checkout_carts_path,method: :post) %>

之所以要用collection,是因为我们要对购物车中所有的商品而不是某一件商品进行操作
(3)建立订单order的model
其中,我们要存储购物车内所有商品的总结total
订单的建立者user_id
寄件人信息billing_name,billing_address
和收件人信息shipping_name,shipping_address
终端执行:
rails g model order
在新生成的migration文件中加入:

  t.integer :total, default: 0
      t.integer :user_id
      t.string :billing_name
      t.string :billing_address
      t.string :shipping_name
      t.string :shipping_address

然后终端执行:
rails db:migrate

(4)建立订单order和用户user之间的关系
一个用户可以有很多个订单
在user.rb中加入:

has_many :orders

在order.rb中加入:

belongs_to :user

(5)在carts_controller中建立结账页的checkout action

def checkout 
@order = Order.new
end

(6)建立结账页面的carts/checkout.htm.erb
重要的代码部分:


      <% current_cart.cart_items.each do |cart_item| %>
      
      <% end %>
    
<%= link_to(cart_item.product.title,product_path(cart_item.product)) %> <%= cart_item.product.price %> <%= cart_item.quantity %>

订单资讯

<%= simple_form_for %> 订购人
<%= f.input :billing_name %>
<%= f.input :billing_address %>
收件人
<%= f.input :shipping_name %>
<%= f.input :shipping_address %>
<%= f.submit "生成订单",class: "btn btn-lg btn-danger pull-right", data: {disable_with: "Submitting"} %>
<% end %>

(7)限制必须填写寄件人和收件人信息
在order.rb中加入:

validates :billing_name, presence: true
  validates :billing_address,presence: true
  validates :shipping_name,presence: true
  validates :shipping_address,presence: true

(8)建立订单order的routes
在routes.rb中加入:

resources :orders

(9)建立生成订单的create action
前面的checkout action只是让用户填入一些参数:
寄件人和收件人的名称,地址
create action才是把资料存入到资料库中
终端执行:
rails g controller orders
在新生成的orders_controller.rb中加入代码,其中要指定order的栏位内容,即购物车商品的总计total和订单建立者user,由于checkout action中已经指定了订单的寄件人和收件人信息,因此就不用在create里面再次指定一遍了:

  before_action :authenticate_user!,only:[:create]

  def create
    @order = Order.new(order_params) #获取结账页面传入的信息(这里是从checkout页面获取,因为checkout页面的之前被我们设置成了order.new即创建order对象)传入的信息包含了收件人和寄件人的信息
    @order.user = current_user #指定订单的创建者是当前登陆用户
    @order.total = current_cart.total_price#指定订单上显示的总计是购物车的商品总价
    if @order.save
      redirect_to order_path(@order)
    else
      render 'carts/checkout'
    end

  end
  private

  def order_params
    params.require(:order).permit(:billing_name, :billing_address, :shipping_name, :shipping_address)

  end

注意:
new和build互为别名,在新建订单时我们是这样写的:

@order = Order.new(order_params)
 @order.user = current_user

也可以写成:

@order = current_user.orders.new(order_params)

同理,限制用户只能看到自己的订单,我们可以这样写

def show
  @order = Order.find(params[:id])
  if @order.user != current_user
    redirect_to root_path
  end
end

也可以写成:

def show
@order = current_user.orders.find(params[:id])
end

在使用后一种写法时,如果输入其他用户的订单地址,localhost3000是红色报错,heroku(production)是直接404,最好不让用户知道他做了什么而导致错误,这样比较安全。

2.建立购买明细

购买明细和订单明细不同的地方在于,订单明细是下单时用的,就如我们上面做的整个第1步的内容。而购买明细的作用是接下来发送给用户通知信用的,最重要的是购买明细记录了当时的购买商品的信息,以便用户追溯查看,这个购买明细,还不能直接用 在order中用product_id 的方式记录信息,然后通过order.product来获取商品信息,因为商品的价格会改变,商品也可能会下架,因此我们就要新建一个product_list这个model来存储当时的购买信息然后我们在订单详情页面用product_list中保存的资料,就可以查看当时购买商品时候的信息了,这样即便商品价格变动,商品下架都不会对订单详情中的信息产生影响。

(1)新建购买明细的model product_list
终端执行:
rails g model product_list
在新生成的migration文件中加入:

      t.integer :order_id
      t.string :product_name
      t.integer :product_price
      t.integer :quantity

分别用来记录购买明细对应的订单,商品名称,商品价格和商品购买数量
终端执行:
rake db:migrate

(2)建立订单order和购买明细product_list之间的关系
一个订单可以有很多个购买明细(因为我们在购物车中可以放入很多中商品一起买,一种商品实际上就对应了一个购买明细)
在order.rb中加入:

has_many :product_lists

在product_list.rb中加入:

belongs_to :order

(3)在订单建立的时候同时建立购买明细
这就需要在order的create action中加入代码:

def create
if @order.save
+ current_cart.cart_items.each do |cart_item|
      +  product_list = ProductList.new
       +  product_list.order = @order
        + product_list.product_price = cart_item.product.price
        + product_list.product_name = cart_item.product.title
        + product_list.quantity = cart_item.quantity
        + product_list.save
+ end
redirect_to order_path(@order)
else
render 'carts/checkout'
end
end

(4)建立订单详情页的action
在orders_controller中加入show action

def show
@order = Order.find(params[:id])
@product_lists = @order.product_lists
end

这里我们要找到对应的订单,并且需要用到订单对应的购买明细product_list,等下会把它们在订单详情页面中进行渲染

(5)建立订单详情页orders/show.html.erb
在其中加入代码:

订单明细

<% @product_lists.each do |product_list| %> <% end %>
商品明细 单价
<%= product_list.product_name %> <%= product_list.product_price %>
总计 <%= @order.total %> CNY

寄送资讯

订购人
<%= @order.billing_name %> - <%= @order.billing_address %>
收件人
<%= @order.shipping_name %> - <%= @order.shipping_address %>

3.将网址改为乱码序号

之前我们订单在打开时,显示的网址是其id,这样隐私性比较差,别人很容易知道你的订单成交量,或者找到规律。因此,我们要将订单的网址改成乱码序号,提高隐私性。
(1)在order上新建栏位token,用来保存乱码序号
终端执行:
rails g migration add_token_to_order
在新生成的migration文件中加入:

  + add_column :orders, :token,:string

终端执行:
rails db:migrate

(2)在order.rb中新建产生乱码序号的generate_token方法,并将该方法挂在before_create上

  before_create :generate_token

  def generate_token
    self.token = SecureRandom.uuid
  end

注意:
before_create 是 Rails model 内建的 callbacks,目的是让资料生成储存前先执行某某动作。model其实就是一个ActiveRecord类,每笔资料(或者说物件,对象)都是model的实例,model是用来操作资料库的,因此在将乱码数字存入到资料库中,就需要在存入前进行操作,所以要将before_create写在model中,然后会继续去执行controller中的create方法,最终将资料存入资料库

(3)进入orders_controller的create action和show action中修改代码,完成重导网址
由于我们把乱码序号作为网址,因此需要显性指定order_path中传入的参数是order.token,否则路径仍然会默认调取order的id,因此要对按钮或者重导路径做如下修改:

def create
....
- redirect_to order_path(@order)
+ redirect_to order_path(@order.token)
...
end

同时也要把获得订单的方式改成用token方式获得,因此要对show action做如下修改:

def show
- @order = Order.find(params[:id])
+ @order = Order.find_by_token(params[:id])
end

4.用户可以看到自己的所有订单

(1)新建路由

namespace :account do
resources :orders
end

用account/orders是为了和之前的order做区别,你也将可以将order定义成其他名称

(2)修改导航栏加入"我的订单"选项

  • <%= link_to("我的订单",account_orders_path) %>
  • (3)新建account/orders_controller.rb,在其中建立index action
    终端执行:
    rails g controller account::orders
    然后在心生成的account/orders_controller.rb中加入:

    before_action :authenticate_user!
    def index
    @order = current_user.orders.order"id DESC"
    end
    

    其中DESC是按照从新到旧的顺序排列订单

    (4)新建"我的订单"页面app/views/account/orders/index.html.erb
    在其中加入代码:

    订单列表

    <% @orders.each do |order| %> <% end %>
    # 生成时间
    <%= link_to(order.id,order_path(order.token)) %> <%= order.created_at.to_s(:long) %>

    其中to_s(:long)可以把created_at和updated_at的时间格式进行修改,具体可参考:[修改时间格式的方法](/Users/xyy/Documents/知识专题/ruby on rails/12 WebApps in 12 Weeks/修改时间格式的方法.md)

    (5)这时候进入"我的订单"页面会出现报错,我们需要解决报错


    006tKfTcly1fnrw33nybuj31d60hmdjw.jpg

    报错信息提示orders_controller中的show action缺少[:id],不能正常重导至show网页
    在看看orders_controller中的show action的代码:

      def show
        @order = Order.find_by_token(params[:id])
        @product_lists = @order.product_lists
      end
    

    说明无法通过params[:id]找到对应的订单
    这是由于,订单的乱码序号token是我们后面加入的,之前的订单都没有token,因此那些没有乱码序号的订单就无法通过token传入参数到params[:id],也就无法找到对应的订单了。
    解决办法:
    进入rails console,输入指令Order.where(token: nil).destroy_all将没有token的订单全部删除掉,再进入"我的订单"就正常了

    补充:

    1.关于关系的建立:has_many和belongs_to
    根据需要我们有时候需要同时设定has_many和belongs_to,从而建立完整的成对映射关系;但有时候如果只需要其中一个,也可以建立不成对的映射关系。
    例如本例子中建立的order和user之间的映射关系
    user.rb中:

    has_many :orders
    

    order.rb中:

    belongs_to :user
    

    由于我们在orders_controller的create action中用到了:

    @order.user = current_user
    

    并且用户可以看到自己的orders,在account/orders_controller中

    def index
    @orders = current_user.orders
    end
    

    其实都是为了记录或者找到订单order对应的用户user。
    总而言之:如果order需要调用user,那么order.rb中一定要有belongs_to :user
    如果user要调用order,那么user.rb中一定要有has_many: orders
    比如,如果order想要调用user,但却没有belongs_to :user,则会出现下面的问题:

    从图中可以看出,在没有order.rb中没有belongs_to :user的情况下,是不能调用user的,虽然能找到user_id是谁,但这个user_id其实是order自身的栏位内容,其实还是没有调取到user。

    在[订单实作思路](/Users/xyy/Documents/知识专题/ruby on rails/全栈营学习/学习总结/订单实作思路.md)中我们也提到过,在建立了cart和cart_item,以及product之间的联系时,我们在cart.rb中这样写:

    has_many :cart_items
      has_many :products, through: :cart_items, source: :product
    

    在cart_item.rb中这样写:

      belongs_to :cart
      belongs_to :product
    end
    

    但却没有在product中设置

    has_many :cart_items
    has_many :carts, through: :cart_items, source: :cart
    

    这是因为我们只需要通过cart_item来调用product,从而知道每个购物栏放了什么商品,但却不需要通过product来获取它对应的购物栏cart_item是什么,
    同样的,我们想通过购物车cart来调取products来获取购物车中放了什么种类的商品product,但却不用product来寻找对应的购物车cart,而是把分配和寻找购物车的任务交给了session,因此我们并没有在product中建立has_many :cart_itemshas_many :carts, through: :cart_items, source: :cart

    2.关于路径中的id

    006tKfTcgy1fnrlmmplu7j313q07igng.jpg

    (1)默认情况下路径参数调用的是id
    (2)图中的id指的是参数params[:id]
    本例中要将乱码序号作为网址,需要将乱码序号token栏位中的内容作为参数,传入到params[:id]中,从而才能作为网址

    @order = Order.find_by_token(params[:id])
    

    并且需要在按钮的路径上或者跳转到的页面上调用token作为路径参数。
    因此创建后订单后,要跳转到订单详情页面,因此我们在order的create action中指定的路径是:

    redirect_to order_path(@order.token)
    

    其中:

    @order = Order.find_by_token(params[:id])
    

    等价写法是:

    @order = Order.find_by(token: params[:id])
    

    这样就可以token作为参数id传入到路径中。
    因此当使用乱数序号时,需要显性指定传入的参数,例如本例子中的token。
    同理,在[订单实作思路](/Users/xyy/Documents/知识专题/ruby on rails/全栈营学习/学习总结/订单实作思路.md)中,我们写过:

    @cart_item = current_cart.cart_items.find_by(product_id: params[:id])
    

    在购物车详情页的删除某一商品的路径写成:

    <%= link_to cart_item_path(cart_item.product_id), method: :delete do %>
     
     <% end %>
    

    也是由于我们是通过product_id寻找cart_item,因此路径参数要传入的是product_id
    而如果使用cart_item自身的id来寻找cart_item,即:

    @cart_item = current_cart.cart_items.find(id: params[:id])
    

    那么在购物车详情页的删除某一商品的路径要对应写成:

    <%= link_to cart_item_path(cart_item.id), method: :delete do %>
     
     <% end %>
    

    其中路径中的cart_item.id也可以换成用cart_item,这是因为路径参数默认传入的就是id,即便我们写的cart_item,它也会捞出其中的cart_item.id传给路径
    总结:如果不是model对象自身的id捞出对象,那么都需要显性指定捞取的方式是什么,并且将这种捞取方式对应的参数传入到路径中

    3.关于结账页checkout action的请求动作
    new action在routes中默认是的请求动作是GET,checkout action虽然和new action名称不一样,但是方法中的代码都是用new方法来新建对象,为什么这里checkout的请求动作用的POST,是因为需要发送新表单的原因,还是因为可以用GET动作,只是考虑到骇客原因,改用POST动作了呢?
    答:
    经过测试验证,checkout action是可以用GET动作的,使用POST动作的原因应该就是为了防骇客,这一点还需要后面继续验证。

    4.关于新建结账页的checkout action是否一定要用在carts_controller中,或者是否一定要用checkout action,可以用new action吗?
    答:
    我认为这里checkout的路径不一定要建立在resources :carts中,也可以新建别的controller,将checkout 放入对应的resources中也是可行的。甚至是将checkout 改成new,使用orders_controller来完成订单详情页也是可行的。不过这些都是我的猜测,接下来通过实作验证下。
    答:经过验证得出新建订单的checkout action可以不用放在resources :carts,也可以新建其他controller,放在其对应的resources中,或者放在resources :orders中也是可以的,当然可以直接用new action而不用checkout action也是可行的,action名称其实是可以新建或修改的。但是需要注意的是如果不是resources默认的7个action,那么需要在routes中resources设定默认7个action外的其他action的路由。比如这里如果将checkout action放在resources :orders中需要指定collection do,并且要指定action 为checkout,并且请求方式为get 或者post;而如果使用的是new action则不用显性指定 new action和请求动作。

    结论:
    从上述carts_controller.rb中的checkout action

    def checkout 
    @order = Order.new
    end
    

    可以看出,用model建立的新对象(物件)不用和controller名称对应,只要有需要任何model建立的对象都可以用在其他controller中,比如这里在carts_controller中可以建立订单Order的新对象。
    5.为什么用product_list建立购买明细就可以不随着商品信息变化而发生变化呢?
    答:这是因为product_list的资料写入过程用的是赋值运算符"=",当用赋值运算符进行赋值运算时,integer,float等Fixnum类型,和string类型,symbol类型,range类型的以及hash类型值有如下特点:

    006tNc79ly1fnsmvnkvd8j30yk0hqgmw.jpg

    即如果变量a通过赋值运算给其他变量b进行赋值后,再次改变变量a,变量b的值不受影响
    而数组类型则不同,它是指向同一个地址,当地址对应的数值发生变化,所有指向该地址的值都将改变,如图:


    006tNc79ly1fnsn0dk20wj30jc09ewf4.jpg

    可以看出数组arr将值赋值给数组br后,再次改变数组arr的值,那么数组br的值也会发生同样的改变
    因此,在使用product_list时我们用的也是赋值运算,对其中的product_name用的是string,quantity和product_price,order_id用的都是integer。这样通过赋值运算后,即便原来的商品名称,价格都发生了变化,也不会影响到product_list中的数值。
    而如果直接在order中通过.调用product的话,如果商品的信息发生变化,商品明细也会发生变化,这样我们就无法记录当初买下商品那一刻的商品价格,名称等信息。
    值得注意的是新建栏位或新建migration时sqlite3不支持数组类型,需要使用Json来转载,例如:

    add_column :products, :photos, :string
    

    此时的photo是并不是数组类型,仍然是string类型,要想把photos作为数组类型,需要到product model中加上:

    serialize :photos, JSON
    

    那么这个photos就同JSON转载成数组类型

    6.为什么只需要在create action中记录用户,总价,购买明细等信息,其他action 就不用再显性指明了呢?
    答:通过例子来解释,首先看order的create action,代码如下:

    def create
        @order = Order.new(order_params)
        @order.user = current_user
        @order.total = current_cart.total_price
    
        if @order.save
          current_cart.cart_items.each do |cart_item|
            product_list = ProductList.new
            product_list.order = @order
            product_list.product_price = cart_item.product.price
            product_list.product_name = cart_item.product.title
            product_list.quantity = cart_item.quantity
            product_list.save
          end
          redirect_to order_path(@order.token)
        else
          render 'carts/checkout'
        end
    
      end
    

    其中在create action中用了new(order_params),将新建订单页面传入的寄件人和收件人信息传入,又指定了订单建立者,购物车中的商品总价,并且记录了订单中的购买信息product_lists,最后一并写入到资料库中。这样其实就将order的栏位内容和其相关的model(这里是product_list)内容一一填写上了。
    由于在create action中已经将order和其相关的product_list栏位内容已经填入到资料库,其他action其实都是对资料库进行操作,因此不用重新在指定栏位对应的内容,需要调用什么内容直接从资料库中取出即可。
    例如order的show action:

    def show
        @order = Order.find_by_token(params[:id])
        @product_lists = @order.product_lists
      end
    

    就是从资料库中找到对应的订单@order,并且捞出该订单相关的购买明细@product_lists,然后在html中调用绘制出页面即可。

    你可能感兴趣的:(订单实作思路-上篇)