在开始购物车模块之前,我们先把商品模块最后一个知识点实现。打开我们的商品详情页,这个页面的改变商品数量,获取总价的功能我们还没有实现。商品详情页的页面显示如下:
计算商品总价
思考什么时候计算商品总价?
当商品数量改变的时候需要重新计算商品总价。因为我们详情页这里可以添加商品数量,减少商品数量,修改商品数量以及刚加载页面的时候都需要计算商品总价,所以我们把计算商品总价这个业务封装成一个函数。
定义计算商品总价函数
function UpdateGoodsPrice() {}
获取商品数量和单价
由视图代码可以找到,商品单价标签是class=show_pirze的子标签,所以获取单价的代码为:
price = $('.show_pirze').children('em').text()
商品的数量标签是class=num_show的标签,则获取数量的代码为:
count = $('.num_show').val()
计算总价
在计算总价之前需要注意,我们这时候获取的price和count都是字符串类型,需要做类型转换之后才能做运算。代码如下:
//计算商品总价
price = parseFloat(price)
count = parseInt(count)
amount = price * count
设置总价
显示总价的标签为class=total的子标签,同时我们设置总价的时候,价格有两位小数点,设置总价代码如下:
$('.total').children('em').text(amount.toFixed(2)+'元')
全部函数代码如下:
//计算商品总价
function UpdateGoodsPrice() {
//获取单价和数量
price = $('.show_pirze').children('em').text()
count = $('.num_show').val()
//计算商品总价
price = parseFloat(price)
count = parseInt(count)
amount = price * count
//设置总价
$('.total').children('em').text(amount.toFixed(2)+'元')
}
注意:当我们加载页面的时候就要计算总价,所以要在外面提前调用一下这个函数
添加商品数量
当点击+
超链接的时候显示栏的数量就添加1,这里我们通过js控制显示数量的增加
获取到按钮的点击事件
通过代码发现+
这个超链接的class为add
,所以按钮的点击事件为:
$('.add').click(function () {})
获取原来显示框的数量
count = $('.num_show').val()
原来的数量加一
count = parseInt(count) + 1
直接获取的count类型为string类型,需要先转为Int类型,然后再进行加法运算
在显示框中显示
$('.num_show').val(count)
更新总价
//更新总价
UpdateGoodsPrice()
全部代码:
$('.add').click(function () {
//获取原来的数量,加一
count = $('.num_show').val()
count = parseInt(count) + 1
//重新设置数目
$('.num_show').val(count)
//更新总价
UpdateGoodsPrice()
})
减少商品数量
逻辑和增加商品数量一样,但是需要做一个简单的判断,代码如下:
$('.minus').click(function () {
//获取原来的数量,加一
count = $('.num_show').val()
count = parseInt(count) - 1
//对数据大小进行判断
if (count < 1){
count = 1
}
//重新设置数目
$('.num_show').val(count)
//更新总价
UpdateGoodsPrice()
})
手动设置商品数量
除了点击按钮外,我们还能够直接在input框中输入商品数量,然后获取总价。那我们什么时候获取这个手动的商品数量呢?一般是当这个input标签失去焦点的时候,我们开始获取手动设置的商品数量。
失去焦点的时候获取商品数量,相应函数如下:
$('.num_show').blur(function () {}
获取输入的商品数量
count = $(this).val()
当响应函数内部获取某个标签的时候,如果这个标签和出发这个事件的标签一致,可以用this代替
对输入的数据进行校验
如果用户输入的数据是字母,一堆空格,或者小于1,这时候我们认为用户输入数据非法,需要手动设置一个正确的数值。代码如下:
if(isNaN(count) || count.trim().length == 0||parseInt(count)<1){
count = 1
}
然后再次设置数量
$(this).val(count)
更新商品总价
UpdateGoodsPrice()
全部代码如下:
$('.num_show').blur(function () {
count = $(this).val()
if(isNaN(count) || count.trim().length == 0||parseInt(count)<1){
count = 1
}
$(this).val(count)
UpdateGoodsPrice()
})
我们这时候再回到商品详情页,这个页面有个添加购物车按钮。那我们就在这里实现这个功能。这时候需要思考一个问题,我们向数据库中添加购物车数据的时候,页面要全部刷新吗?
一般情况下,我们向数据库中添加购物车数据的时候,页面只是进行局部刷新。局部刷新我们一般用ajax来实现这个功能,那这里我们就用ajax发送请求。
请求
添加购物车,需要传递数据,我们一般是用post请求,函数名是$.post()
,有三个参数,第一个参数是请求路径,第二个参数是传递的数据(json格式的数据),第三个参数是成功后所执行的函数
所以在发送请求之前,我们需要先确定请求路径,添加购物车数据需要登陆的状态下才能进行,所以我们设置请求路径为/user/addCart。
接着我们要去构造传递的数据,添加购物车,需要把商品id
和对应的数量
传递给后台,这里我们设计数据格式为{“skuid”:id,“count”:count}。
最后是我们的函数。代码如下:
封装要传递的数据
我们要传递的数据为商品的id,所以我们需要获取这个id,但是这时候你发现,我们页面里面没有显示商品id的地方。那这里需要我们手动在页面里面添加这个内容,有两种解决方法:
第一种:隐藏域传值,这个方法我们前面用过,不做详细介绍。
第二种:给某一个标签添加一个自定义属性,获取这个属性的值,这里我们给加入购物车
按钮添加一个商品ID属性,代码如下:
获取自定义属性的值,使用attr()方法。具体代码如下:
skuid = $('#add_cart').attr("skuid")
获取商品数量,直接获取相应标签值即可,代码如下:
count = $('.num_show').val()
封装成一个json格式的数据包,代码如下:
params = {'skuid':skuid,'goodsCount':count}
发送请求
$.post('/user/addCart',params,function (data) {})
路由
ajax发送了请求之后,我们要在router.go中添加相对应的控制器和方法。
beego.Router("/user/addCart",&controllers.CartController{},"post:HandleAddCart")
控制器
在路由指定了控制器和方法之后,我们就创建控制器并且实现HandleAddCart方法。
获取数据
skuid,err1 := this.GetInt("skuid")
count,err2 := this.GetInt("goodsCount")
校验数据
校验数据传输是否正确
//返回数据
resp := make(map[string]interface{})
defer this.ServeJSON()
//数据校验
if err1 != nil || err2 != nil{
resp["res"]=1
resp["errmsg"] = "获取数据信息错误"
this.Data["json"] = resp
return
}
注意,ajax传递过来的数据,我们回复的时候不再指定视图,而是回复给视图json格式的数据。那这里我们怎么给视图中传递json数据呢?
首先定义一个map[string]interface类型,用来存储返回的数据,然后指定传递的数据代码为:
this.Data["json"] = resp
接着调用传递数据函数:
this.ServeJSON()
不管能不能执行成功我们都要给ajax请求返回数据,所以这里我们可以直接defer调用函数的代码。
校验传过来的商品id是否有对应的商品数据
o := orm.NewOrm()
var goods models.GoodsSKU
goods.Id = skuid
err := o.Read(&goods)
if err != nil{
resp["res"]=2
resp["errmsg"] = "商品不存在"
}
校验添加商品的数量是否超出我们的库存
校验登陆状态
userName := this.GetSession("userName")
if userName == nil{
resp["res"]=4
resp["errmsg"] = "用户未登录,请提前登录"
this.Data["json"] = resp
return
}
思考:我们这个请求都是在登陆状态下才能发的,那我这个校验还有意义没有?
处理数据
添加购物车数据其实就是向数据库中添加数据。那这时候我们思考以下几个问题:
第一:添加哪些数据?
第二:用哪种数据库?用哪种类型?
第一个问题的答案跟明确,我们要添加当前用户对应的商品,和商品数量。
根据第一个问题的答案我们分析出来,我们数据库中要存储用户信息,商品信息,商品数量,并且这三者还是一一对应的。那我们分析之后可以得出,用redis中的hash存储这个数据最合适。
那我们就给redis中设置一个hash值。
但是在添加这个记录之前,如果redis中该用户已经添加过该商品数据,那么我们做的就是累加操作。具体代码如下:
conn,_:=redis.Dial("tcp",":6379")
preCount,_:=redis.Int(conn.Do("hget","cart_"+strconv.Itoa(user.Id),skuid))
conn.Do("hset","cart_"+strconv.Itoa(user.Id),skuid,count+preCount)
查询购物车中商品数量
//获取购物车商品个数
cartcount,_ :=redis.Int(conn.Do("hlen","cart_"+strconv.Itoa(user.Id)))
返回数据
resp["res"] = 5
resp["cartcount"]=cartcoun
this.Data["json"] = resp
视图
这时候ajax能够拿到数据,我们就需要在回调函数里面做一个处理。具体处理如下(是一个执行动画操作,我们不需要详细了解,课堂上带你们看一下,知道即可):
$.post('goods/cartAdd',params,function (data) {
if(data.res == 5){
//添加成功
$(".add_jump").css({'left':$add_y+80,'top':$add_x+10,'display':'block'})
$(".add_jump").stop().animate({
'left': $to_y+7,
'top': $to_x+7},
"fast", function() {
$(".add_jump").fadeOut('fast',function(){
$('#show_count').html(data.cartcount);
});
});
}else{
//添加失败
alert(data.errmsg)
}
})
到这里添加购物车内容基本实现,接着需要把项目中很多地方的添加购物车按钮都实现相应的超链接。
添加完购物车内容,我们就需要去获取相关数据,我们发现在很多页面都需要展示购物车条目数,如图:
那么我们可以在后台封装一个获取购物车数据条目数的函数,加载各个页面的时候调用,代码如下:
func GetCartCount(this*beego.Controller)int{
userName := this.GetSession("userName")
if userName == nil{
return 0
}
o := orm.NewOrm()
var user models.User
user.Name = userName.(string)
o.Read(&user,"Name")
conn,_ := redis.Dial("tcp","192.168.110.81:6379")
rep,err :=conn.Do("hlen","cart_"+strconv.Itoa(user.Id))
cartCount ,_ :=redis.Int(rep,err)
return cartCount
}
请求
我们每个页面头部有一个导航栏,叫我的购物车,这个超链接就是发起购物车页面请求的。查看购物车数据需要在登陆的状态下才能够查看,所以这里我们设计访问购物车页面的请求为:/user/cart
路由
有了请求,需要到router.go中指定相应的控制器和方法。代码如下:
beego.Router("/user/cart",&controllers.CartController{},"get:ShowCart")
控制器
接着我们在控制器中实现ShowCart函数。
从redis中获取数据
因为我们前面设计的redis中存储购物车数据的key
值是cart_userId
,所以我们要先获取用户Id,代码如下:
userName := this.GetSession("userName")
o := orm.NewOrm()
var user models.User
user.Name = userName.(string)
o.Read(&user,"Name")
有了key值,我们就可以获取相应数据。这里注意我们要获取购物车存储数据的类型为hash
,获取所有数据的命令为hgetall
。这个命令返回的结果是一个map[string]int
切片。所以这里我们获取所有购物车数据的代码如下:
conn,_:=redis.Dial("tcp","192.168.110.81:6379")
defer conn.Close()
//以map[string]int的形式获取购物车数据
reply,_:=redis.IntMap(conn.Do("hgetall","cart_"+strconv.Itoa(user.Id)))
获取相应的商品数据
这时候我们从redis数据库中获取到的是购物车中所有商品的ID和数量,视图中我们要获取的是确定的商品信息和数量 ,所以需要我们从数据库中获取到商品信息并和数量一起存储。这里要注意,我们这里又需要一个容器,存储商品信息和数量两个不同的类型,参考我们首页讲过的内容,这里我们还用[]map[string]interface{}
,我们的切片的长度就是从redis数据库中获取了几条数据,获取所有商品的代码如下:
计算总的价格和总数量
总价和总数量应该在循环获取商品的时候获取,这时候可以给总价个总数量做一个叠加。代码如下:
//循环遍历,获取购物车商品数据
totalCount := 0
totalPrice := 0
i := 0
for index,count := range reply{
temp := make(map[string]interface{})
var goods models.GoodsSKU
id,_ := strconv.Atoi(index)
goods.Id = id
o.Read(&goods)
temp["goods"] = goods
temp["count"] = count
cartGoods[i] = temp
totalCount += count
totalPrice += goods.Price * count
i += 1
}
把获取到的总结,总数量,所有商品传递给视图
this.Data["totalCount"] = totalCount
this.Data["totalPrice"] = totalPrice
this.Data["goods"] = cartGoods
视图
视图中获取到数据之后,需要在视图中循环显示,代码如下:
{{range .goods}}
<ul class="cart_list_td clearfix">
<li class="col01"><input type="checkbox" name="skuids" value="{{.goods.Id}}" checked>li>
<li class="col02"><img src="http://192.168.110.81:8888/{{.goods.Image}}">li>
<li class="col03">{{.goods.Name}}<br><em>{{.goods.Price}}元/{{.goods.Unite}}em>li>
<li class="col04">{{.goods.Unite}}li>
<li class="col05">{{.goods.Price}}元li>
<li class="col06">
<div class="num_add">
<a href="javascript:;" class="add fl">+a>
<input type="text" class="num_show fl" skuid = {{.goods.Id}} value="{{.count}}">
<a href="javascript:;" class="minus fl">-a>
div>
li>
<li class="col07">{{.amount}}元li>
<li class="col08"><a href="javascript:;" class="deleteCart">删除a>li>
ul>
{{end}}
<ul class="settlements">
<li class="col01"><input type="checkbox" name="" checked="">li>
<li class="col02">全选li>
<li class="col03">合计(不含运费):<span>¥span><em>{{.totalPrice}}em><br>共计<b>{{.totalCount}}b>件商品li>
<li class="col04"><input type="submit" value="去结算">li>
ul>
购物车页面如下:
当很多地方数据改变的时候都需要计算商品的总件数和总价格,所以这里我们也要把总件数和总价格封装成一个函数。
定义一个初始的总件数和总价格变量
//定义一个初始变量
totalCount = 0
totalPrice = 0
循环遍历所有选中的商品数据
我们在jquery中学过基础的选择器,那这里我们来学习一些高级的选择器:http://www.w3school.com.cn/jquery/jquery_ref_selectors.asp。了解了选择器的用法之后,我们来看一下如何获取所有的选中状态并遍历。首先找到上一级标签cart_list_td
,然后查找选中状态并遍历,代码如下:
$('.cart_list_td').find(':checked').parents('ul').each(function () {})
获取选中状态的建树和总价格进行叠加
//获取选中商品的数量和总价
count = $(this).find('.num_show').val()
amount = $(this).children('.col07').text()
//累计计算选中商品的总数量和总价格
totalCount += parseInt(count)
totalPrice += parseFloat(amount)
设置总件数和总价格
//设置被选中的商品的总件数和总价格
$('.settlements').find('em').text(totalPrice.toFixed(2))
$('.settlements').find('b').text(totalCount)
获取当前全选按钮的状态
//获取全选的checkbox的选种状态
is_checked = $(this).prop('checked')
获取标签原有属性用prop
获取所有的checkedbox标签,并设置选中状态
//遍历商品对应的checkbox,设置这些checkbox的选中状态和全选的checkbox保持一致
$('.cart_list_td').find(':checkbox').each(function () {
$(this).prop('checked',is_checked)
})
更新总件数和总价格的值
//更新页面信息
update_page_info()
当单独取消某个商品的选中状态时,全选状态也要能够跟着变化:
获取所有checkbox的个数
all_lenth = $('.cart_list_td').length
获取选中状态的checkbox的个数
checkedLenth = $('.cart_list_td').find(':checked').length
比较两个之间的大小
如果两个个数相同,则设置全选的checkbox为选中状态,反之设置为未选中,代码如下:
if(checkedLenth < all_lenth){
$('.settlements').find(':checkbox').prop('checked',false)
}else {
$('.settlements').find(':checkbox').prop('checked',true)
}
更新总件数和总价格
update_page_info()
当点击+
按钮的时候,显示的商品数量要添加,后台也要相应的更新购物车数据,同时总价和总件数也应该相应的跟着变化。
获取商品的ID和数量
//获取数量和商品id
count = $(this).next().val()
skuid = $(this).next().attr('skuid')
给后台发送ajax请求
//发送请求
count = parseInt(count) + 1
params = {'skuid':skuid,'goodsCount':count}
totalCount = 0
$.post('/goods/cartUpdate',params,function (data) {
if(data.res ==5){
err_update = false
totalCount = data.totalCount
}else{
err_update = true
alert(data.errmsg)
}
})
根据返回的值确定总价格和总件数
if (!err_update){
//重新设置商品数目
$(this).next().val(count)
//计算商品的小计
price = $(this).parents('ul').children('.col05').text()
amount = count * parseFloat(price)
$(this).parents('ul').children('.col07').text(amount.toFixed(2)+'元')
//获取商品对应的checkbox选中状态,计算总价
is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
if(is_checked){
update_page_info()
}
//更新总件数
$('.total_count').children('em').text(totalCount)
}
但是这时候发现页面显示不正常,是因为ajax默认为异步。不能及时的刷新页面数据,所以需要暂时的关闭ajax的异步状态,然后在刷新页面数据的时候开启。具体代码如下:
$('.add').click(function () {
//获取数量和商品id
count = $(this).next().val()
skuid = $(this).next().attr('skuid')
//发送请求
count = parseInt(count) + 1
params = {'skuid':skuid,'goodsCount':count}
//设置ajax请求为同步
$.ajaxSettings.async = false
totalCount = 0
$.post('/goods/cartUpdate',params,function (data) {
if(data.res ==5){
err_update = false
totalCount = data.totalCount
}else{
err_update = true
alert(data.errmsg)
}
})
$.ajaxSettings.async = true
if (!err_update){
//重新设置商品数目
$(this).next().val(count)
//计算商品的小计
price = $(this).parents('ul').children('.col05').text()
amount = count * parseFloat(price)
$(this).parents('ul').children('.col07').text(amount.toFixed(2)+'元')
//获取商品对应的checkbox选中状态,计算总价
is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
if(is_checked){
update_page_info()
}
//更新总件数
$('.total_count').children('em').text(totalCount)
}
})
减少和添加的业务逻辑一样只是在减少的时候需要对减少后的数量做一个判断。代码如下:
//购物车商品数量的减少
$('.minus').click(function () {
//获取数量和商品id
count = $(this).prev().val()
skuid = $(this).prev().attr('skuid')
//发送请求
count = parseInt(count)-1
if (count <= 0){
return
}
$.ajaxSettings.async = false
params = {'skuid':skuid,'goodsCount':count}
$.post('/goods/cartUpdate',params,function (data) {
if(data.res ==5){
err_update = false
totalCount = data.totalCount
}else{
err_update = true
alert(data.errmsg)
}
})
$.ajaxSettings.async = true
if (!err_update){
//重新设置商品数目
$(this).prev().val(count)
//计算商品的小计
price = $(this).parents('ul').children('.col05').text()
amount = count * parseFloat(price)
$(this).parents('ul').children('.col07').text(amount.toFixed(2)+'元')
//获取商品对应的checkbox选中状态,计算总价
is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
if(is_checked){
update_page_info()
}
//更新总件数
$('.total_count').children('em').text(totalCount)
}
})
手动修改购物车数量业务逻辑也一样,只是需要对输入的数据做一个校验。代码如下:
//手动输入购物车中的商品数量
$('.num_show').blur(function () {
//获取数量和商品id
count = $(this).val()
skuid = $(this).attr('skuid')
//发送请求
if (isNaN(count) || count.trim() || parseInt(count) <= 0){
$(this).val(preCount)
return
}
$.ajaxSettings.async = false
params = {'skuid':skuid,'goodsCount':count}
$.post('/goods/cartUpdate',params,function (data) {
if(data.res ==5){
err_update = false
totalCount = data.totalCount
}else{
err_update = true
alert(data.errmsg)
}
})
$.ajaxSettings.async = true
if (!err_update){
//重新设置商品数目
$(this).val(count)
//计算商品的小计
price = $(this).parents('ul').children('.col05').text()
amount = count * parseFloat(price)
$(this).parents('ul').children('.col07').text(amount.toFixed(2)+'元')
//获取商品对应的checkbox选中状态,计算总价
is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
if(is_checked){
update_page_info()
}
//更新总件数
$('.total_count').children('em').text(totalCount)
}else{
$(this).val(preCount)
}
})
当点击删除按钮的时候出发删除事件
$('.deleteCart').click(function () {})
发送删除的ajax请求
发送ajax请求的时候需要发送要删除的商品id,所以需要先获取商品的id。
sku_ul = $(this).parents('ul')
skuid = sku_ul.find('.num_show').attr('skuid')
发送请求
$.post('/user/deleteCart',{"skuid":skuid},function (data) {})
在router.go文件中添加相对应的控制器和方法
beego.Router("/user/deleteCart",&controllers.CartController{},"post:DeleteCart")
然后我们开始实现DeleteCart
方法
获取数据
获取ajax传递过来的商品id
skuid,err := this.GetInt("skuid")
校验数据
判断获取数据是否有错,如果出错的话返回 错误信息给ajax请求
resp := make(map[string]interface{})
resp["res"] = 5
resp["errmsg"] = "OK"
defer this.ServeJSON()
if err != nil{
resp["res"] = 1
resp["errmsg"] = "无效商品id"
this.Data["json"] = resp
return
}
处理数据
如果能够获取到数据的话,就删除数据,因为我们的购物车数据是存在redis中的,key值是cart_userId
,所以需要先获取当前用户数据。
userName := this.GetSession("userName")
var user models.User
user.Name = userName.(string)
o.Read(&user,"Name")
然后连接redis数据库,删除数据:
conn,_:=redis.Dial("tcp",":6379")
_,err = conn.Do("hdel","cart_"+strconv.Itoa(user.Id),goods.Id)
if err != nil{
resp["res"] = 3
resp["errmsg"] = "删除商品失败"
this.Data["json"] = resp
return
}
返回数据
如果删除数据成功,返回数据:
resp["res"] = 5
resp["errmsg"] = "ok"
this.Data["json"] = resp
视图处理
拿到后天传递过来的数据,我们需要做一个判断,当删除成功的时候,移除掉删除按钮所在的那一行ul。并且判断一下,当前ul的单选框是否选中,如果选中还需要更新总件数和总价格。删除失败弹框错误信息:
$.post('/goods/deleteCart',{"skuid":skuid},function (data) {
if(data.res == 5){
//删除成功,移除页面上商品所在的ul元素
sku_ul.remove()
//获取sku_ul中商品的状态
is_checked = sku_ul.find(':checkbox').prop('checked')
if(is_checked){
//更新页面信息
update_page_info()
}
//重新设置页面上购物车商品的总件数
$('.total_count').children('em').text(data.totalCount)
}else{
alert(data.errmsg)
}
})
请求
当我们在购物车页面点击去结算按钮的时候,发起展示订单页的请求,这时候需要把购物车中选中的所有商品都传递过来。我们这里可以利用checkbox的一个特性,只有选中的状态下才会给后台传递value值,把所有的商品都用form表单包起来,然后给其中的checkbox设置name属性和value属性。我们后台要获取商品对应,这里我们可以传递给后台商品的ID。代码如下:
<form method="post" action="/goods/showOrder">
{{range .goods}}
<ul class="cart_list_td clearfix">
<li class="col01"><input type="checkbox" name="skuids" value="{{.goods.Id}}" checked>li>
<li class="col02"><img src="http://192.168.110.81:8888/{{.goods.Image}}">li>
<li class="col03">{{.goods.Name}}<br><em>{{.goods.Price}}元/{{.goods.Unite}}em>li>
<li class="col04">{{.goods.Unite}}li>
<li class="col05">{{.goods.Price}}元li>
<li class="col06">
<div class="num_add">
<a href="javascript:;" class="add fl">+a>
<input type="text" class="num_show fl" skuid = {{.goods.Id}} value="{{.count}}">
<a href="javascript:;" class="minus fl">-a>
div>
li>
<li class="col07">{{.amount}}元li>
<li class="col08"><a href="javascript:;" class="deleteCart">删除a>li>
ul>
{{end}}
> 去结算按钮,设置为submit属性。
设置路由
接着我们在router.go中添加请求对应的控制器和方法,这里我们要处理的是订单业务,所以添加一个订单的控制器和显示订单页面的方法.
//展示订单页面
beego.Router("/user/showOrder",&controllers.OrderController{},"post:ShowOrder")
控制器
接着我们去控制器中实现ShowOrder
函数
获取数据
这里我们拿到的不是一个数据,而是一个int型数组,所以这里获取数据的方法为getStrings。代码如下:
//获取skuids
skuids := this.GetStrings("skuids")
beego中没有获取整型数组的方法,只有获取string数组的方法
校验数据
//检验数据
if len(skuids) == 0{
//如果没有选中商品,跳转到购物车界面重新选择商品
this.Redirect("/goods/cart",302)
return
}
处理数据
这里我们获取的是商品的id,我们可以根据拿到的id从redis中获取相应的数量,但是我们视图需要的是商品数据,所以我们还是需要定义一个容器,存储商品和商品数量。定义容器代码如下:
goodsBuffer := make([]map[string]interface{},len(skuids))
然后连接redis数据库
conn,_ := redis.Dial("tcp","192.168.110.81:6379")
这里需要注意,我们存到购物车的数据是hash类型,key值是cart_userid
,所以我们需要获取用户ID,那我们要先查找用户数据
userName := this.GetSession("userName")
var user models.User
user.Name = userName.(string)
o.Read(&user,"Name")
接着我们就需要循环获取商品数据,然后把商品,小计和数量存储到我们的容器中,代码如下:
for index, val := range skuids{
temp := make(map[string]interface{})
//获取商品
var goods models.GoodsSKU
id,_ := strconv.Atoi(val)
goods.Id = id
o.Read(&goods)
temp["goods"] = goods
//获取数量
count,_:=redis.Int(conn.Do("hget","cart_"+strconv.Itoa(user.Id),id))
temp["count"] = count
//获取小计
amount:=count * goods.Price
temp["amount"] = amount
goodsBuffer[index] = temp
}
通过观察页面可知,我们还需要获取所有的地址信息。那我们还需要查询地址表,代码如下:
//获取地址信息
var addrs []models.Address
o.QueryTable("Address").All(&addrs)
然后把查询到的数据传递给视图,并指定视图。
this.Data["goods"] = goodsBuffer
this.Data["addrs"] = addrs
GetUser(&this.Controller)
this.TplName = "place_order.html"
视图处理
<h3 class="common_title">确认收货地址h3>
<div class="common_list_con clearfix">
<dl>
<dt>寄送到:dt>
{{range .addrs}}
<dd><input type="radio" name="addrId" value="{{.Id}}" {{if .Is_default}}checked{{end}}>{{.Addr}} ({{.Receiver}} 收) {{.Phone}}dd>
{{end}}
dl>
<a href="/goods/UserCenterSite" class="edit_site">编辑收货地址a>
div>
<h3 class="common_title">支付方式h3>
<div class="common_list_con clearfix">
<div class="pay_style_con clearfix">
<input type="radio" name="pay_style" checked value="1">
<label class="cash">货到付款label>
<input type="radio" name="pay_style" value="2">
<label class="weixin">微信支付label>
<input type="radio" name="pay_style" value="3">
<label class="zhifubao">label>
<input type="radio" name="pay_style" value="4">
<label class="bank">银行卡支付label>
div>
div>
<h3 class="common_title">商品列表h3>
<div class="common_list_con clearfix">
<ul class="goods_list_th clearfix">
<li class="col01">商品名称li>
<li class="col02">商品单位li>
<li class="col03">商品价格li>
<li class="col04">数量li>
<li class="col05">小计li>
ul>
{{range $index,$val := .goods}}
<ul class="goods_list_td clearfix">
<li class="col01">{{$index}}li>
<li class="col02"><img src="http://192.168.110.81:8888/{{$val.goods.Image}}">li>
<li class="col03">{{$val.goods.Name}}li>
<li class="col04">{{$val.goods.Unite}}li>
<li class="col05">{{$val.goods.Price}}元li>
<li class="col06">{{$val.count}}li>
<li class="col07">{{$val.amount}}元li>
ul>
{{end}}
div>
<h3 class="common_title">总金额结算h3>
<div class="common_list_con clearfix">
<div class="settle_con">
<div class="total_goods_count">共<em>{{.totalCount}}em>件商品,总金额<b>{{.totalAmount}}元b>div>
<div class="transit">运费:<b>10元b>div>
<div class="total_pay">实付款:<b>100元b>div>
div>
div>
<div class="order_submit clearfix">
<a href="javascript:;" id="order_btn" skuid="{{.skuid}}">提交订单a>
div>
提交订单其实就是创建订单,本质上是把我们订单页面的商品信息,数量信息再次从视图传递给后台,然后做一次插入。首先我们需要发送请求。
发送请求
发送请求这里我们用ajax给后台发送,根据页面可知,我们需要把下面四块内容发送到后台。
地址我们可以传递地址ID,支付方式我们可以传递个数字,用来标识是哪种支付,商品的话就需要从后台传递过来所有的商品和对应的数量,总价也可以获取之后传递给后台。那我们怎么把商品的信息传递给后台并通过js获取呢?
商品信息和数量其实就是从购物车页面传递到订单页面的内容,再次把那个数组传递给视图即可。但是我们发现页面没有显示商品ID的地方,可以把这个数组作为提交订单超链接的一个自定义属性。获取相应信息如下:
//获取用户选择的地址id,支付方式,要购买的商品id字符串,总件数,总价格,运输费
addrId = $('input[name="addrId"]:checked').val()
payId = $('input[name="pay_style"]:checked').val()
skuid = $(this).attr("skuids")
totalCount = $('.total_goods_count').children('em').text()
transferPrice = $('.transit').children('b').text()
totalPrice = $('.total_pay').children('b').text()
然后发送请求:
params = {"addrId":addrId,"payId":payId,"skuid":skuid,"totalCount":totalCount,"transferPrice":parseFloat(transferPrice),"transfer":parseFloat(transferPrice)}
//发送请求
$.post("/user/addOrder",params,function (data) {})
在router.go中指定相应控制器和方法
beego.Router("/user/addOrder",&controllers.OrderController{},"post:AddOrder")
接着我们去控制器中实现AddOrder
方法
获取数据
addrid,_ :=this.GetInt("addrId")
payId,_ := this.GetInt("payId")
skuids := this.GetStrings("skuid")
totalCount ,err1 :=this.GetInt("totalCount")
totalPrice,err2 :=this.GetInt("totalPrice")
transfer,err3 :=this.GetInt("transferPrice")
校验数据
if addrid ==0 || payId==0 || len(skuids) == 0 || err1 != nil || err2 != nil || err3 != nil{
resp["res"] = 1
resp["errmsg"] = "无效数据"
this.Data["json"] = resp
this.ServeJSON()
return
}
处理数据
向订单表插入数据:
var order models.OrderInfo
orderid := time.Now().Format("20060102150405")+strconv.Itoa(user.Id)
order.OrderId = orderid
order.User = &user
order.Pay_Method = payId
//获取快递地址
var addr models.Address
addr.Id = addrid
o.Read(&addr)
order.Address = &addr
//获取商品总数量,运费和总价格
order.Transit_Price = transfer
order.Total_Count = totalCount
order.Total_Price = totalPrice
o.Insert(&order)
向订单商品表插入数据
//向订单商品表插入数据
conn,_ := redis.Dial("tcp","192.168.110.81:6379")
var orderGoods models.OrderGoods
for _,val := range skuids{
id,_ := strconv.Atoi(val)
count,_ := redis.Int(conn.Do("hegt","cart_"+strconv.Itoa(user.Id),id))
var goods models.GoodsSKU
goods.Id = id
o.Read(&goods)
orderGoods.Price = goods.Price
orderGoods.GoodsSKU = &goods
orderGoods.OrderInfo = &order
orderGoods.Count = count
goods.Stock -= count
goods.Sales += count
//插入数据库
o.Insert(&orderGoods)
//更新商品数据
}
返回数据
resp["res"] = 5
this.Data["json"] = resp
this.ServeJSON()
视图处理
这时候我们页面拿到了后台返回过来的数据,需要做如下处理。
$.post("/goods/addOrder",params,function (data) {
if(data.res == 5){
localStorage.setItem('order_finish',2);
$('.popup_con').fadeIn('fast', function() {
setTimeout(function(){
$('.popup_con').fadeOut('fast',function(){
window.location.href = '/user/userCenterOrder';
});
},3000)
});
}else{
alert(data.errmsg)
}
})
这串js代码就是先弹框,显示创建订单成功,然后跳转到用户中心页面显示。
基本业务实现了,我们来看看刚才的代码还有些什么问题。
当我们向订单商品表中插入数据,应该提前判断一下,商品的库存是否充足,如果不充足的话要直接返回。代码如下:
if goods.Stock < count{
resp["res"] = 3
resp["errmsg"] = "商品库存不足"
this.Data["json"] = resp
this.ServeJSON()
o.Rollback()
return
}
但是这时候我们发现一个问题,当添加商品订单失败的时候,订单表仍然会创建,这样和我们的逻辑就有些冲突,所以这里我们需要让他们同时执行成功,或者同时执行失败,这里就用到了我们事务的概念
我们在插入数据库的时候
事务的概念:**一组mysql语句,要么执行,要么全不不执行。 **
事务的特点:
事务的隔离级别:
SQL标准定义了4类隔离级别,包括了一些具体规则,用来限定事务内外的哪些改变是可见的,哪些是不可见的。低级别的隔离级一般支持更高的并发处理,并拥有更低的系统开销。
Read Uncommitted(读取未提交内容) :在该隔离级别,所有事务都可以看到其他未提交事务的执行结果。本隔离级别很少用于实际应用,因为它的性能也不比其他级别好多少。读取未提交的数据,也被称之为脏读(Dirty Read)。
Read Committed(读取提交内容):这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
**Repeatable Read(可重读) **:这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
**Serializable(可串行化) **:这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 /body>
这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
**脏读(Drity Read):**某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read): 在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read): 在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的 。
设置MySQL的隔离级别:
当我们在一个网站购物时,可能会遇到这样一种情况,两个用户同时向网站中提交了购买请求。业务逻辑如下:
可能会出现如下错误。当用户A判断完商品1库存之后,系统的执行时间可能会跳转到B的进程,这时候用户B把商品买完了,也把商品数据更新了一边,然后A已经判断过库存了,认为商品是充足的,所以仍然会去更新数据库,这就造成了一件商品可能被卖了两次。那怎么解决这个问题呢?
我们需要先设定一个原始的库存数据,然后在更新的时候,先去判断现在的库存数据是否和原始的数据一致,如果不一致,认为库存数据已经改变了,判断提交订单失败,如果一致,认为可以继续下订单。
用户中心订单页显示
tted(读取提交内容)**:这是大多数数据库系统的默认隔离级别(但不是MySQL默认的)。它满足了隔离的简单定义:一个事务只能看见已经提交事务所做的改变。这种隔离级别 也支持所谓的不可重复读(Nonrepeatable Read),因为同一事务的其他实例在该实例处理其间可能会有新的commit,所以同一select可能返回不同结果。
**Repeatable Read(可重读) **:这是MySQL的默认事务隔离级别,它确保同一事务的多个实例在并发读取数据时,会看到同样的数据行。不过理论上,这会导致另一个棘手的问题:幻读 (Phantom Read)。简单的说,幻读指当用户读取某一范围的数据行时,另一个事务又在该范围内插入了新行,当用户再读取该范围的数据行时,会发现有新的“幻影” 行。InnoDB和Falcon存储引擎通过多版本并发控制(MVCC,Multiversion Concurrency Control)机制解决了该问题。
**Serializable(可串行化) **:这是最高的隔离级别,它通过强制事务排序,使之不可能相互冲突,从而解决幻读问题。简言之,它是在每个读的数据行上加上共享锁。在这个级别,可能导致大量的超时现象和锁竞争。 /body>
这四种隔离级别采取不同的锁类型来实现,若读取的是同一个数据的话,就容易发生问题。例如:
**脏读(Drity Read):**某个事务已更新一份数据,另一个事务在此时读取了同一份数据,由于某些原因,前一个RollBack了操作,则后一个事务所读取的数据就会是不正确的。
不可重复读(Non-repeatable read): 在一个事务的两次查询之中数据不一致,这可能是两次查询过程中间插入了一个事务更新的原有的数据。
幻读(Phantom Read): 在一个事务的两次查询中数据笔数不一致,例如有一个事务查询了几列(Row)数据,而另一个事务却在此时插入了新的几列数据,先前的事务在接下来的查询中,就会发现有几列数据是它先前所没有的 。
[外链图片转存中…(img-exMXToWf-1650597397250)]
设置MySQL的隔离级别:
[外链图片转存中…(img-2XlYHloB-1650597397252)]
当我们在一个网站购物时,可能会遇到这样一种情况,两个用户同时向网站中提交了购买请求。业务逻辑如下:
[外链图片转存中…(img-lsjIxEjl-1650597397254)]
可能会出现如下错误。当用户A判断完商品1库存之后,系统的执行时间可能会跳转到B的进程,这时候用户B把商品买完了,也把商品数据更新了一边,然后A已经判断过库存了,认为商品是充足的,所以仍然会去更新数据库,这就造成了一件商品可能被卖了两次。那怎么解决这个问题呢?
我们需要先设定一个原始的库存数据,然后在更新的时候,先去判断现在的库存数据是否和原始的数据一致,如果不一致,认为库存数据已经改变了,判断提交订单失败,如果一致,认为可以继续下订单。
用户中心订单页显示