前言
本文是[购物车实作思路](/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| %>
<%= link_to(cart_item.product.title,product_path(cart_item.product)) %>
<%= cart_item.product.price %>
<%= cart_item.quantity %>
<% end %>
订单资讯
<%= 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| %>
<%= product_list.product_name %>
<%= product_list.product_price %>
<% end %>
总计 <%= @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| %>
<%= link_to(order.id,order_path(order.token)) %>
<%= order.created_at.to_s(:long) %>
<% end %>
其中to_s(:long)可以把created_at和updated_at的时间格式进行修改,具体可参考:[修改时间格式的方法](/Users/xyy/Documents/知识专题/ruby on rails/12 WebApps in 12 Weeks/修改时间格式的方法.md)
(5)这时候进入"我的订单"页面会出现报错,我们需要解决报错
报错信息提示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_items
和has_many :carts, through: :cart_items, source: :cart
2.关于路径中的id
(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类型值有如下特点:
即如果变量a通过赋值运算给其他变量b进行赋值后,再次改变变量a,变量b的值不受影响
而数组类型则不同,它是指向同一个地址,当地址对应的数值发生变化,所有指向该地址的值都将改变,如图:
可以看出数组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中调用绘制出页面即可。