提示
- 使用redis数据库存储购物车数据
- 购物车需要完成增、删、改、查的逻辑
- 查询的结果,需要由服务器响应界面给客户端展示出来
- 增删改的操作,是客户端发数据给服务器,两者之间的交互是局部刷新的效果,需要用ajax交互
- 添加购物车的请求方法:post
- 服务器和客户端传输数据格式:json
- 服务器接收的数据
- 用户id:user_id
- 商品id:sku_id
- 商品数量:count
定义添加购物车视图
# 项目的urls url(r'^cart/', include('cart.urls', namespace='cart')),
# 应用的urls urlpatterns = [ url(r'^add$', views.AddCartView.as_view(), name='add') ]
class AddCartView(View): """添加到购物车""" def post(self, request): # 判断用户是否登陆 # 接收数据:user_id,sku_id,count # 校验参数all() # 判断商品是否存在 # 判断count是否是整数 # 判断库存 # 操作redis数据库存储商品到购物车 # json方式响应添加购物车结果 pass
添加到购物车视图和JS
添加到购物车视图
- 判断用户是否登陆
- 此时不需要使用装饰器
LoginRequiredMixin
- 用户访问到
AddCartView
时,需要是登陆状态 - 因为购物车中使用json在前后端交互,是否登陆的结果也要以json格式交给客户端
- 而
LoginRequiredMixin
验证之后的结果是以重定向的方式告诉客户端的
- 此时不需要使用装饰器
- 获取请求参数:用户id:user_id。商品id:sku_id。商品数量:count
- 校验参数:all(),判断参数是否完整
- 判断商品是否存在,如果不存在,异常为:
GoodsSKU.DoesNotExist
- 判断count是否是整数,不是整数,异常为:
Exception
- 判断库存
- 以上判断没错,才能操作redis,存储商品到购物车
- 如果加入的商品不在购物车中,直接新增到购物车
- 如果加入的商品存在购物车中,直接累加计数即可
- 为了方便前端展示购物车数量,所以查询一下购物车总数
- 提示:
origin_count = redis_conn.hget("cart_%s" %user_id, sku_id)
-
origin_count
为bytes
类型的,如果做加减操作需要转成int(origin_count)
- 字典遍历出来的值,也是
bytes
类型的,如果做加减操作需要转成整数
class AddCartView(View): """添加购物车""" def post(self, request): # 判断用户是否登陆 if not request.user.is_authenticated(): return JsonResponse({'code': 1, 'message':'用户未登录'}) # 接收数据:user_id,sku_id,count user_id = request.user.id sku_id = request.POST.get('sku_id') count = request.POST.get('count') # 校验参数 if not all([sku_id,count]): return JsonResponse({'code': 2, 'message': '参数不完整'}) # 判断商品是否存在 try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: return JsonResponse({'code': 3, 'message': '商品不存在'}) # 判断count是否是整数 try: count = int(count) except Exception: return JsonResponse({'code': 4, 'message': '数量错误'}) # 判断库存 # if count > sku.stock: # return JsonResponse({'code': 5, 'message': '库存不足'}) # 操作redis数据库存储商品到购物车 redis_conn = get_redis_connection('default') # 需要先获取要添加到购物车的商品是否存在 origin_count = redis_conn.hget('cart_%s'%user_id, sku_id) # 如果商品在购物车中存在,就直接累加商品数量;反之,把新的商品和数量添加到购物车 if origin_count is not None: count += int(origin_count) # 判断库存:计算最终的count与库存比较 if count > sku.stock: return JsonResponse({'code': 5, 'message': '库存不足'}) # 存储到redis redis_conn.hset('cart_%s'%user_id, sku_id, count) # 为了配合模板中js交互并展示购物车的数量,在这里需要查询一下购物车的总数 cart_num = 0 cart_dict = redis_conn.hgetall('cart_%s'%user_id) for val in cart_dict.values(): cart_num += int(val) # json方式响应添加购物车结果 return JsonResponse({'code': 0, 'message': '添加购物车成功', 'cart_num':cart_num})
添加到购物车JS
- ajax请求方法:post
- 请求地址:/cart/add
- 请求参数:商品id+商品数量+csrftoken
添加购物车ajax请求
$('#add_cart').click(function(){ // 将商品的id和数量发送给后端视图,后端进行购物车数据的记录 // 获取商品的id和数量 var request_data = { sku_id: $('#add_cart').attr('sku_id'), count: $('#num_show').val(), csrfmiddlewaretoken: "{{ csrf_token }}" }; // 使用ajax向后端发送数据 $.post('/cart/add', request_data, function (response_data) { // 根据后端响应的数据,决定处理效果 if (1 == response_data.code){ location.href = '/users/login'; // 如果未登录,跳转到登录页面 } else if (0 == response_data.code) { $(".add_jump").stop().animate({ 'left': $to_y+7, 'top': $to_x+7}, "fast", function() { $(".add_jump").fadeOut('fast',function(){ // 展示购物车总数量 $('#show_count').html(response_data.cart_num); }); }); } else { // 其他错误信息,简单弹出来 alert(response_data.message); } }); });
未登录添加购物车介绍
思考
- 当用户已登录时,将购物车数据存储到服务器的redis中
- 当用户未登录时,将购物车数据存储到浏览器的cookie中
- 当用户进行登录时,将cookie中的购物车数据合并到redis中
- 购物车的增删改查都要区分用户是否登陆
设计思想
- 使用json字符串将购物车数据保存到浏览器的cookie中
- 提示:每个人的浏览器cookie存储的都是个人的购物车数据,所以key不用唯一标示
'cart':'{'sku_1':10, 'sku_2':20}'
操作cookie的相关方法
# 向浏览器中写入购物车cookie信息 response.set_cookie('cart', cart_str)
。。。 # 读取cookie中的购物车信息 cart_json = request.COOKIES.get('cart')
json模块
- 在操作cookie保存购物车数据时
- 我们需要将json字符串格式的购物车数据转成python对象来增删改查
- 需求:将json字符串转成python字典
-
实现:json模块
未登录添加购物车视图和JS
提示
- 如果用户未登录,就保存购物车数据到cookie中
- 即使用户未登录,客户端在添加购物车时,也会向服务器传递商品(sku_id)和商品数量(count)
- 所以,也是需要校验和判断
- 直到,需要存储购物车数据时,才来判断是否是登陆状态
核心逻辑
1.先从cookie中,获取当前商品的购物车记录 (cart_json) 2.判断购物车(cart_json)数据是否存在,有可能用户从来没有操作过购物车 2.1.如果(cart_json)存在就把它转成字典(cart_dict) 2.2.如果(cart_json)不存在就定义空字典(cart_dict) 3.判断要添加的商品在购物车中是否存在 3.1.如果存在就取出源有值,并进行累加 3.2.如果不存在就直接保存商品数量 4.将(cart_dict)重新生成json字符串,方便写入到cookie 5.创建JsonResponse对象,该对象就是要响应的对象 6.在响应前,设置cookie信息 7.计算购物车数量总和,方便前端展示
核心代码
if not request.user.is_authenticated(): # 如果用户未登录,就保存购物车数据到cookie中 # 先从cookie的购物车信息中,获取当前商品的购物车记录,即json字符串购物车数据 cart_json = request.COOKIES.get('cart') # 判断购物车cookie数据是否存在,有可能用户从来没有操作过购物车 if cart_json is not None: # 将json字符串转成json字典 cart_dict = json.loads(cart_json) else: # 如果用户没有操作购物车,就给个空字典 cart_dict = {} if sku_id in cart_dict: # 如果cookie中有这个商品记录,则直接进行求和;如果cookie中没有这个商品记录,则将记录设置到购物车cookie中 origin_count = cart_dict[sku_id] # json模块,存进去的是数字,取出来的也是数字 count += origin_count # 判断库存:计算最终的count与库存比较 if count > sku.stock: return JsonResponse({'code': 6, 'message': '库存不足'}) # 设置最终的商品数量到购物车 cart_dict[sku_id] = count # 计算购物车总数 cart_num = 0 for val in cart_dict.values(): cart_num += int(val) # 将json字典转成json字符串 cart_str = json.dumps(cart_dict) # 将购物车数据写入到cookie中 response = JsonResponse({"code": 0, "message": "添加购物车成功", 'cart_num': cart_num}) response.set_cookie('cart', cart_str) return response
整体实现
class AddCartView(View): """添加到购物车: sku_id, count, user_id""" def post(self, request): # 判断用户是否登录 # if not request.user.is_authenticated(): # # 提示用户未登录 # return JsonResponse({"code": 1, "message": "用户未登录"}) # 商品id sku_id = request.POST.get("sku_id") # 商品数量 count = request.POST.get("count") # 检验参数 if not all([sku_id, count]): return JsonResponse({"code": 2, "message": "参数不完整"}) # 判断商品是否存在 try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 表示商品不存在 return JsonResponse({"code": 3, "message": "商品不存在"}) # 判断count是整数 try: count = int(count) except Exception: return JsonResponse({"code": 4, "message": "参数错误"}) # 判断库存 # if count > sku.stock: # return JsonResponse({"code": 5, "message": "库存不足"}) # 提示:无论是否登陆状态,都需要获取suk_id,count,校验参数。。。 # 所以等待参数校验结束后,再来判断用户是否登陆 # 如果用户已登录,就保存购物车数据到redis中 if request.user.is_authenticated(): # 用户id user_id = request.user.id # "cart_用户id": {"sku_1": 10, "sku_2": 11} # 先尝试从用户的购物车中获取这个商品的数量,如果购物车中不存在这个商品,则直接添加购物车记录 # 否则,需要进行数量的累计,在添加到购物车记录中 redis_conn = get_redis_connection("default") origin_count = redis_conn.hget("cart_%s" % user_id, sku_id) # 原有数量 if origin_count is not None: count += int(origin_count) # 判断库存:计算最终的count与库存比较 if count > sku.stock: return JsonResponse({'code': 5, 'message': '库存不足'}) # 存储到redis redis_conn.hset("cart_%s" % user_id, sku_id, count) # 为了方便前端展示购物车数量,所以查询一下购物车总数 cart_num = 0 cart = redis_conn.hgetall("cart_%s" % user_id) for val in cart.values(): cart_num += int(val) # 采用json返回给前端 return JsonResponse({"code": 0, "message": "添加购物车成功", "cart_num": cart_num}) else: # 如果用户未登录,就保存购物车数据到cookie中 # 先从cookie的购物车信息中,获取当前商品的记录,json字符串购物车数据 cart_json = request.COOKIES.get('cart') # 判断购物车cookie数据是否存在,有可能用户从来没有操作过购物车 if cart_json is not None: # 将json字符串转成json字典 cart_dict = json.loads(cart_json) else: # 如果用户没有操作购物车,就给个空字典 cart_dict = {} if sku_id in cart_dict: # 如果cookie中有这个商品记录,则直接进行求和;如果cookie中没有这个商品记录,则将记录设置到购物车cookie中 origin_count = cart_dict[sku_id] count += origin_count # 判断库存:计算最终的count与库存比较 if count > sku.stock: return JsonResponse({'code': 6, 'message': '库存不足'}) # 设置最终的商品数量到购物车 cart_dict[sku_id] = count # 计算购物车总数 cart_num = 0 for val in cart_dict.values(): cart_num += val # 将json字典转成json字符串 cart_str = json.dumps(cart_dict) # 将购物车数据写入到cookie中 response = JsonResponse({"code": 0, "message": "添加购物车成功", 'cart_num': cart_num}) response.set_cookie('cart', cart_str) return response
前端ajax请求代码
-
不需要再判断是否是登陆用户
$('#add_cart').click(function(){ // 将商品的id和数量发送给后端视图,后端进行购物车数据的记录 // 获取商品的id和数量 var request_data = { sku_id: $(this).attr('sku_id'), count: $('#num_show').val(), csrfmiddlewaretoken: "" }; // 使用ajax向后端发送数据 $.post('/cart/add', request_data, function (response_data) { // 根据后端响应的数据,决定处理效果 if (0 == response_data.code) { $(".add_jump").stop().animate({ 'left': $to_y+7, 'top': $to_x+7}, "fast", function() { $(".add_jump").fadeOut('fast',function(){ // 展示购物车总数量 $('#show_count').html(response_data.cart_num); }); }); } else { // 其他错误信息,简单弹出来 alert(response_data.message); } }); });
未登录时添加购物车测试
商品模块购物车数量展示
-
提示:商品模块的购物车包括,
主页
、详情页
、列表页
-
由于
主页
、详情页
、列表页
中都涉及到购物车数据的展示 -
所以,将购物车逻辑封装到基类
BaseCartView
中
基类BaseCartView
class BaseCartView(View): """提供购物车数据统计功能""" def get_cart_num(self, request): cart_num = 0 # 如果用户登录,就从redis中获取购物车数据 if request.user.is_authenticated(): # 创建redis_conn对象 redis_conn = get_redis_connection('default') # 获取用户id user_id = request.user.id # 从redis中获取购物车数据,返回字典,如果没有数据,返回None,所以不需要异常判断 cart = redis_conn.hgetall('cart_%s' %user_id) # 遍历购物车字典,累加购物车的值 for value in cart.values(): cart_num += int(value) else: # 如果用户未登录,就从cookie中获取购物车数据 cart_json = request.COOKIES.get('cart') # json字符串 # 判断购物车数据是否存在 if cart_json is not None: # 将json字符串购物车数据转成json字典 cart_dict = json.loads(cart_json) else: cart_dict = {} # 遍历购物车字典,计算商品数量 for val in cart_dict.values(): cart_num += val return cart_num
修改base.html模板
-
修改了
base.html
后,主页,详情页,列表页 都具备相同的数据
购物车页面展示
分析
- 如果用户未登录,从cookie中获取购物车数据
# cookie 'cart':'{'sku_1':10, 'sku_2':20}'
如果用户已登录,从redis中获取购物车数据
cart_userid:{sku_3, 30}
- 展示购物车数据时,不需要客户端传递数据到服务器端,因为需要的数据可以从cookie或者redis中获取
- 注意:从cookie中获取的count是整数,从redis中获取的count是字节类型,需要统一类型
定义视图
url(r'^cart/', include('cart.urls', namespace='cart'))
url(r'^cart/', include('cart.urls', namespace='cart'))
class CartInfoView(View): """获取购物车数据""" def get(self, request): """提供购物车页面:不需要请求参数""" pass
展示购物车数据视图
class CartInfoView(View): """获取购物车数据""" def get(self, request): """提供购物车页面:不需要请求参数""" # 查询购物车数据 # 如果用户登陆从redis中获取数据 if request.user.is_authenticated(): # 创建redis连接对象 redis_conn = get_redis_connection('default') user_id = request.user.id # 获取所有数据 cart_dict = redis_conn.hgetall('cart_%s'%user_id) else: # 如果用户未登陆从cookie中获取数据 cart_json = request.COOKIES.get('cart') # 判断用户是否操作过购物车cookie if cart_json is not None: cart_dict = json.loads(cart_json) else: cart_dict = {} # 保存遍历出来的sku skus = [] # 总金额 total_amount = 0 # 总数量 total_count = 0 # 遍历cart_dict,形成模板所需要的数据 for sku_id, count in cart_dict.items(): # 查询商品sku try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 商品不存在,跳过这个商品,继续遍历 continue # 将count转成整数,因为redis中取出的count不是整数类型的 count = int(count) # 计算总价 amount = sku.price * count # 将需要展示的数据保存到对象中 sku.amount = amount sku.count = count # 生成模型列表 skus.append(sku) # 计算总金额 total_amount += amount # 计算总数量 total_count += count # 构造上下文 context = { 'skus':skus, 'total_amount':total_amount, 'total_count':total_count } return render(request, 'cart.html', context)
展示购物车数据模板
{% extends 'base.html' %} {% block title %}天天生鲜-购物车{% endblock %} {% load staticfiles %} {% block search_bar %} <div class="search_bar clearfix"> <a href="{% url 'goods:index' %}" class="logo fl"><img src="{% static 'images/logo.png' %}"></a> <div class="sub_page_name fl">| 购物车</div> <div class="search_con fr"> <form action="/search/" method="get"> <input type="text" class="input_text fl" name="q" placeholder="搜索商品"> <input type="submit" class="input_btn fr" value="搜索"> </form> </div> </div> {% endblock %} {% block body %} <div class="total_count">全部商品<em>{{total_count}}</em>件</div> <ul class="cart_list_th clearfix"> <li class="col01">商品名称</li> <li class="col02">商品单位</li> <li class="col03">商品价格</li> <li class="col04">数量</li> <li class="col05">小计</li> <li class="col06">操作</li> </ul> <form method="post" action="#"> {% csrf_token %} {% for sku in skus %} <ul class="cart_list_td clearfix" sku_id="{{ sku.id }}"> <li class="col01"><input type="checkbox" name="sku_ids" value="{{ sku.id }}" checked></li> <li class="col02"><img src="{{ sku.default_image.url }}"></li> <li class="col03">{{ sku.name }}<br><em>{{ sku.price }}/{{ sku.unit}}</em></li> <li class="col04">{{ sku.unit }}</li> <li class="col05"><span>{{sku.price}}</span>元</li> <li class="col06"> <div class="num_add"> <a href="javascript:;" class="add fl">+</a> <input type="text" class="num_show fl" sku_id="{{ sku.id }}" value="{{sku.count}}"> <a href="javascript:;" class="minus fl">-</a> </div> </li> <li class="col07"><span>{{sku.amount}}</span>元</li> <li class="col08"><a href="javascript:;" class="del_btn">删除</a></li> </ul> {% endfor %} <ul class="settlements"> <li class="col01"><input type="checkbox" checked></li> <li class="col02">全选</li> <li class="col03">合计(不含运费):<span>¥</span><em id="total_amount">{{total_amount}}</em><br>共计<b id="total_count">{{total_count}}</b>件商品</li> <li class="col04"><a href="place_order.html">去结算</a></li> </ul> </form> {% endblock %} {% block bottom_files %} <script type="text/javascript" src="{% static 'js/jquery-1.12.2.js' %}"></script> <script type="text/javascript"> // 更新页面合计信息 function freshOrderCommitInfo() { var total_amount = 0; //总金额 var total_count = 0; // 总数量 $('.cart_list_td').find(':checked').parents('ul').each(function () { var sku_amount = $(this).children('li.col07').text(); // 商品的金额 var sku_count = $(this).find('.num_show').val(); // 商品的数量 total_count += parseInt(sku_count); total_amount += parseFloat(sku_amount); }); // 设置商品的总数和总价 $("#total_amount").text(total_amount.toFixed(2)); $("#total_count").text(total_count); } // 更新页面顶端全部商品数量 function freshTotalGoodsCount() { var total_count = 0; $('.cart_list_td').find(':checkbox').parents('ul').each(function () { var sku_count = $(this).find('.num_show').val(); total_count += parseInt(sku_count); }); $(".total_count>em").text(total_count); } // 更新后端购物车信息 function updateRemoteCartInfo(sku_id, sku_count, num_dom) { // 发送给后端的数据 var req = { sku_id: sku_id, count: sku_count, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post("/cart/update", req, function(data){ if (0 == data.code) { // 更新商品数量 $(num_dom).val(sku_count); // 更新商品金额信息 var sku_price = $(".cart_list_td[sku_id="+sku_id+"]").children('li.col05').children().text(); var sku_amount = parseFloat(sku_price) * sku_count; $(".cart_list_td[sku_id="+sku_id+"]").children('li.col07').children().text(sku_amount.toFixed(2)); // 更新顶部商品总数 freshTotalGoodsCount(); // 更新底部合计信息 freshOrderCommitInfo(); } else { alert(data.message); } }); } // 增加 $(".add").click(function(){ // 获取操作的商品id var sku_id = $(this).next().attr("sku_id"); // 获取加操作前的的数量 var sku_num = $(this).next().val(); // 进行数量加1 sku_num = parseInt(sku_num); sku_num += 1; // 显示商品数目的dom var num_dom = $(this).next(); // 更新购物车数量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 减少 $(".minus").click(function(){ // 获取操作的商品id var sku_id = $(this).prev().attr("sku_id"); // 获取加操作前的的数量 var sku_num = $(this).prev().val(); // 进行数量加1 sku_num = parseInt(sku_num); sku_num -= 1; if (sku_num < 1) sku_num = 1; // 更新页面显示数量 var num_dom = $(this).prev(); // 更新购物车数量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); var pre_sku_count = 0; $('.num_show').focus(function () { // 记录用户手动输入之前商品数目 pre_sku_count = $(this).val(); }); // 手动输入 $(".num_show").blur(function(){ var sku_id = $(this).attr("sku_id"); var sku_num = $(this).val(); // 如果输入的数据不合理,则将输入值设置为在手动输入前记录的商品数目 if (isNaN(sku_num) || sku_num.trim().length<=0 || parseInt(sku_num)<=0) { $(this).val(pre_sku_count); return; } sku_num = parseInt(sku_num); var num_dom = $(this); updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 删除 $(".del_btn").click(function(){ var sku_id = $(this).parents("ul").attr("sku_id"); var req = { sku_id: sku_id, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post('/cart/delete', req, function(data){ // window.reload() location.href="/cart/"; // 删除后,刷新页面 }); }); // 商品对应checkbox发生改变时,全选checkbox发生改变 $('.cart_list_td').find(':checkbox').change(function () { // 获取商品所有checkbox的数目 var all_len = $('.cart_list_td').find(':checkbox').length; // 获取选中商品的checkbox的数目 var checked_len = $('.cart_list_td').find(':checked').length; if (checked_len < all_len){ // 有商品没有被选中 $('.settlements').find(':checkbox').prop('checked', false) } else{ // 所有商品都被选中 $('.settlements').find(':checkbox').prop('checked', true) } freshOrderCommitInfo(); }); // 全选和全不选 $('.settlements').find(':checkbox').change(function () { // 1.获取当前checkbox的选中状态 var is_checked = $(this).prop('checked'); // 2.遍历并设置商品ul中checkbox的选中状态 $('.cart_list_td').find(':checkbox').each(function () { // 设置每一个goods ul中checkbox的值 $(this).prop('checked', is_checked) }); freshOrderCommitInfo(); }); </script> {% endblock %}
登陆时购物车合并cookie和redis
- 需求:在登陆页面跳转前,将cookie和redis中的购物车数据合并到redis中
分析
- 获取cookie中的购物车数据
- 获取redis中的购物车数据
- 合并购物车上商品数量信息
- 如果cookie中存在的,redis中也有,则进行数量累加
- 如果cookie中存在的,redis中没有,则生成新的购物车数据
- 将cookie中的购物车数据合并到redis中
- 清除浏览器购物车cookie
实现
# 在页面跳转之前,将cookie中和redis中的购物车数据合并 # 从cookie中获取购物车数据 cart_json = request.COOKIES.get('cart') if cart_json is not None: cart_dict_cookie = json.loads(cart_json) else: cart_dict_cookie = {} # 从redis中获取购物车数据 redis_conn = get_redis_connection('default') cart_dict_redis = redis_conn.hgetall('cart_%s'%user.id) # 进行购物车商品数量合并:将cookie中购物车数量合并到redis中 for sku_id, count in cart_dict_cookie.items(): # 提示:由于redis中的键与值都是bytes类型,cookie中的sku_id是字符串类型 # 需要将cookie中的sku_id字符串转成bytes sku_id = sku_id.encode() if sku_id in cart_dict_redis: # 如果cookie中的购物车商品在redis中也有,就取出来累加到redis中 # 提示:redis中的count是bytes,cookie中的count是整数,无法求和,所以,转完数据类型在求和 origin_count = cart_dict_redis[sku_id] count += int(origin_count) # 如果cookie中的商品在redis中有,就累加count赋值。反之,直接赋值cookie中的count cart_dict_redis[sku_id] = count # 将合并后的redis数据,设置到redis中:redis_conn.hmset()不能传入空字典 if cart_dict_redis: redis_conn.hmset('cart_%s'%user.id, cart_dict_redis) # 获取next参数,用于判断登陆界面是从哪里来的 next = request.GET.get('next') if next is None: # 跳转到首页 response = redirect(reverse('goods:index')) else: # 从哪儿来,回哪儿去 response = redirect(next) # 清除cookie response.delete_cookie('cart') return response
注意
- 由于redis中的键与值都是bytes类型,cookie中的sku_id是字符串类型,两者要统一类型再比较
- redis中的count是bytes,cookie中的count是整数,无法求和,所以,转完数据类型在求和
- redis_conn.hmset()不能传入空字典
购物车更新接口设计
幂等
- 非幂等
- /cart/update?sku_id=1 & num=1
- 对于同一种行为,如果最终的结果与执行的次数有关,每次执行后结果都不相同,就称这种行为为非幂等
- 缺点:当用户多次点击
+
,增加购物车商品数量时,如果其中有一次数据传输失败,则商品数量出错
- 幂等
- /cart/update?sku_id=1&finally_num=18
- 对于同一种行为,如果执行不论多少次,最终的结果都是一致相同的,就称这种行为是幂等的
- 结论:
- 购物车中,无论是增加还是减少商品数量,都对应相同接口,都传递最终商品的数量即可
- 优点:每次向服务器发送的是最终商品的数量,如果某一次传输失败,可以展示上一次的商品最终数量
购物车前端代码编写
主要逻辑
- 更新页面合计信息
- 更新页面顶端全部商品数量
- 更新后端购物车信息
- 增加商品数量
- 减少商品数量
- 手动输入商品数量
- 商品对应checkbox发生改变时,全选checkbox发生改变
- 全选和全不选
{% extends 'base.html' %} {% block title %}天天生鲜-购物车{% endblock %} {% load staticfiles %} {% block search_bar %} <div class="search_bar clearfix"> <a href="{% url 'goods:index' %}" class="logo fl"><img src="{% static 'images/logo.png' %}"></a> <div class="sub_page_name fl">| 购物车</div> <div class="search_con fr"> <form action="/search/" method="get"> <input type="text" class="input_text fl" name="q" placeholder="搜索商品"> <input type="submit" class="input_btn fr" value="搜索"> </form> </div> </div> {% endblock %} {% block body %} <div class="total_count">全部商品<em>{{total_count}}</em>件</div> <ul class="cart_list_th clearfix"> <li class="col01">商品名称</li> <li class="col02">商品单位</li> <li class="col03">商品价格</li> <li class="col04">数量</li> <li class="col05">小计</li> <li class="col06">操作</li> </ul> <form method="post" action="#"> {% csrf_token %} {% for sku in skus %} <ul class="cart_list_td clearfix" sku_id="{{ sku.id }}"> <li class="col01"><input type="checkbox" name="sku_ids" value="{{ sku.id }}" checked></li> <li class="col02"><img src="{{ sku.default_image.url }}"></li> <li class="col03">{{ sku.name }}<br><em>{{ sku.price }}/{{ sku.unit}}</em></li> <li class="col04">{{ sku.unit }}</li> <li class="col05"><span>{{sku.price}}</span>元</li> <li class="col06"> <div class="num_add"> <a href="javascript:;" class="add fl">+</a> <input type="text" class="num_show fl" sku_id="{{ sku.id }}" value="{{sku.count}}"> <a href="javascript:;" class="minus fl">-</a> </div> </li> <li class="col07"><span>{{sku.amount}}</span>元</li> <li class="col08"><a href="javascript:;" class="del_btn">删除</a></li> </ul> {% endfor %} <ul class="settlements"> <li class="col01"><input type="checkbox" checked></li> <li class="col02">全选</li> <li class="col03">合计(不含运费):<span>¥</span><em id="total_amount">{{total_amount}}</em><br>共计<b id="total_count">{{total_count}}</b>件商品</li> <li class="col04"><a href="place_order.html">去结算</a></li> </ul> </form> {% endblock %} {% block bottom_files %} <script type="text/javascript" src="{% static 'js/jquery-1.12.2.js' %}"></script> <script type="text/javascript"> // 更新页面合计信息 function freshOrderCommitInfo() { var total_amount = 0; //总金额 var total_count = 0; // 总数量 $('.cart_list_td').find(':checked').parents('ul').each(function () { var sku_amount = $(this).children('li.col07').text(); // 商品的金额 var sku_count = $(this).find('.num_show').val(); // 商品的数量 total_count += parseInt(sku_count); total_amount += parseFloat(sku_amount); }); // 设置商品的总数和总价 $("#total_amount").text(total_amount.toFixed(2)); $("#total_count").text(total_count); } // 更新页面顶端全部商品数量 function freshTotalGoodsCount() { var total_count = 0; $('.cart_list_td').find(':checkbox').parents('ul').each(function () { var sku_count = $(this).find('.num_show').val(); total_count += parseInt(sku_count); }); $(".total_count>em").text(total_count); } // 更新后端购物车信息 function updateRemoteCartInfo(sku_id, sku_count, num_dom) { // 发送给后端的数据 var req = { sku_id: sku_id, count: sku_count, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post("/cart/update", req, function(data){ if (0 == data.code) { // 更新商品数量 $(num_dom).val(sku_count); // 更新商品金额信息 var sku_price = $(".cart_list_td[sku_id="+sku_id+"]").children('li.col05').children().text(); var sku_amount = parseFloat(sku_price) * sku_count; $(".cart_list_td[sku_id="+sku_id+"]").children('li.col07').children().text(sku_amount.toFixed(2)); // 更新顶部商品总数 freshTotalGoodsCount(); // 更新底部合计信息 freshOrderCommitInfo(); } else { alert(data.message); } }); } // 增加 $(".add").click(function(){ // 获取操作的商品id var sku_id = $(this).next().attr("sku_id"); // 获取加操作前的的数量 var sku_num = $(this).next().val(); // 进行数量加1 sku_num = parseInt(sku_num); sku_num += 1; // 显示商品数目的dom var num_dom = $(this).next(); // 更新购物车数量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 减少 $(".minus").click(function(){ // 获取操作的商品id var sku_id = $(this).prev().attr("sku_id"); // 获取加操作前的的数量 var sku_num = $(this).prev().val(); // 进行数量加1 sku_num = parseInt(sku_num); sku_num -= 1; if (sku_num < 1) sku_num = 1; // 更新页面显示数量 var num_dom = $(this).prev(); // 更新购物车数量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); var pre_sku_count = 0; $('.num_show').focus(function () { // 记录用户手动输入之前商品数目 pre_sku_count = $(this).val(); }); // 手动输入 $(".num_show").blur(function(){ var sku_id = $(this).attr("sku_id"); var sku_num = $(this).val(); // 如果输入的数据不合理,则将输入值设置为在手动输入前记录的商品数目 if (isNaN(sku_num) || sku_num.trim().length<=0 || parseInt(sku_num)<=0) { $(this).val(pre_sku_count); return; } sku_num = parseInt(sku_num); var num_dom = $(this); updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 删除 $(".del_btn").click(function(){ var sku_id = $(this).parents("ul").attr("sku_id"); var req = { sku_id: sku_id, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post('/cart/delete', req, function(data){ // window.reload() location.href="/cart/"; // 删除后,刷新页面 }); }); // 商品对应checkbox发生改变时,全选checkbox发生改变 $('.cart_list_td').find(':checkbox').change(function () { // 获取商品所有checkbox的数目 var all_len = $('.cart_list_td').find(':checkbox').length; // 获取选中商品的checkbox的数目 var checked_len = $('.cart_list_td').find(':checked').length; if (checked_len < all_len){ // 有商品没有被选中 $('.settlements').find(':checkbox').prop('checked', false) } else{ // 所有商品都被选中 $('.settlements').find(':checkbox').prop('checked', true) } freshOrderCommitInfo(); }); // 全选和全不选 $('.settlements').find(':checkbox').change(function () { // 1.获取当前checkbox的选中状态 var is_checked = $(this).prop('checked'); // 2.遍历并设置商品ul中checkbox的选中状态 $('.cart_list_td').find(':checkbox').each(function () { // 设置每一个goods ul中checkbox的值 $(this).prop('checked', is_checked) }); freshOrderCommitInfo(); }); </script> {% endblock %}
更新购物车数量视图
主要逻辑:
- 更新哪条购物车记录,商品数量更新为多少
- 如果用户登陆,更新redis中的购物车记录
- 如果用户未登陆,更新cookie中的购物车记录
准备工作
# 更新购物车数据 url(r'^update$', views.UpdateCartView.as_view(), name='update'),
class UpdateCartView(View): """更新购物车数据:+ -""" def post(self,request): # 获取参数:sku_id, count # 校验参数all() # 判断商品是否存在 # 判断count是否是整数 # 判断库存 # 判断用户是否登陆 # 如果用户登陆,将修改的购物车数据存储到redis中 # 如果用户未登陆,将修改的购物车数据存储到cookie中 # 响应结果 pass
更新购物车数量实现
class UpdateCartView(View): """更新购物车数据:+ - 编辑""" def post(self, request): # 获取参数:sku_id, count sku_id = request.POST.get('sku_di') count = request.POST.get('count') # 校验参数all() if not all([sku_id, count]): return JsonResponse({'code': 1, 'message': '参数不完整'}) # 判断商品是否存在 try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: return JsonResponse({'code': 2, 'message': '商品不存在'}) # 判断count是否是整数 try: count = int(count) except Exception: return JsonResponse({'code': 3, 'message': '数量有误'}) # 判断库存 if count > sku.stock: return JsonResponse({'code': 4, 'message': '库存不足'}) # 判断用户是否登陆 if request.user.is_authenticated(): # 如果用户登陆,将修改的购物车数据存储到redis中 redis_conn = get_redis_connection('default') user_id = request.user.id # 如果设计成幂等的,count就是最终要保存的商品的数量,不需要累加 redis_conn.hset('cart_%s'%user_id, sku_id, count) return JsonResponse({'code': 0, 'message': '添加购物车成功'}) else: # 如果用户未登陆,将修改的购物车数据存储到cookie中 # 获取cookie中的购物车的json字符串 cart_json = request.COOKIES.get('cart') # 如果json字符串存在,将json字符串转成字典,因为用户可能从来没有添加过购物车 if cart_json is not None: cart_dict = json.loads(cart_json) else: cart_dict = {} # 如果设计成幂等的,count就是最终要保存的商品的数量,不需要累加 cart_dict[sku_id] = count # 将购物车字典转成json字符串格式 new_cart_json = json.dumps(cart_dict) # 响应结果 response = JsonResponse({'code': 0, 'message': '添加购物车成功'}) # 写入cookie response.set_cookie('cart', new_cart_json) return response
删除购物车记录视图
主要逻辑
- 删除哪条购物车记录
- 如果用户登陆,删除redis中的购物车记录
- 如果用户未登陆,删除cookie中的购物车记录
准备工作
# 删除购物车数据 url(r'^delete$', views.DeleteCartView.as_view(), name='delete')
class DeleteCartView(View): """删除购物车数据""" def post(self, request): # 接收参数:sku_id # 校验参数:not,判断是否为空 # 判断用户是否登录 # 如果用户登陆,删除redis中购物车数据 # 如果用户未登陆,删除cookie中购物车数据 pass
删除购物车记录实现
class DeleteCartView(View): """删除购物车数据""" def post(self, request): # 接收参数:sku_id sku_id = request.POST.get('sku_id') # 校验参数:not,判断是否为空 if not sku_id: return JsonResponse({'code': 1, 'message': '参数错误'}) # 判断用户是否登录 if request.user.is_authenticated(): # 如果用户登陆,删除redis中购物车数据 redis_conn= get_redis_connection('default') user_id = request.user.id # 商品不存在会直接忽略 redis_conn.hdel('cart_%s'%user_id, sku_id) else: # 如果用户未登陆,删除cookie中购物车数据 cart_json = request.COOKIES.get('cart') if cart_json is not None: cart_dict = json.loads(cart_json) # 判断要删除的商品是否存在 if sku_id in cart_dict: # 字典删除key对应的value del cart_dict[sku_id] # 响应中重新写入cookie response = JsonResponse({'code': 0, 'message': '删除成功'}) response.set_cookie('cart', json.dumps(cart_dict)) return response # 当删除成功或者没有要删除的都提示用户成功 return JsonResponse({'code': 0, 'message': '删除成功'})
订单
订单生成流程界面
页面入口
- 点击《详情页》的《立即购买》可以进入该《订单确认页面》
- 点击《购物车》的《去结算》可以进入该《订单确认页面》
- 点击《订单确认页面》的《提交订单》可以进入《全部订单》
- 点击《全部订单》的《去付款》可以进入《支付宝》
订单确认页面分析
页面入口
- 点击《详情页》的《立即购买》可以进入该《订单确认页面》
- 点击《购物车》的《去结算》可以进入该《订单确认页面》
说明
- 订单确认页面的生成是由用户发送商品数据给服务器,服务器渲染后再响应给客户端而生成的
- 请求方法:POST
- 只有当用户登陆后,才能访问该《订单确认页面》,
LoginRequiredMixin
- 提示:进入订单确认页面,没有使用ajax交互
- 如果前后端是通过json交互的,不能使用
LoginRequiredMixin
- 因为
LoginRequiredMixin
不响应json数据,而是响应302的重定向
- 参数说明:
- 地址:通过
request.user
获取关联的Address - 支付方式:页面内部选择
- 商品id:请求参数,传入sku_id
- 一次传入的sku_id有可能有多个,所以设计成
sku_ids=sku_1&sku_ids=sku_2
- 如果从《购物车》进入《订单确认页面》,sku_ids设计成一键多值的情况
- 如果从《详情页》进入《订单确认页面》,sku_ids设计成一键一值的情况
- 获取方式:
sku_ids = request.POST.getlist('sku_ids')
- 一次传入的sku_id有可能有多个,所以设计成
- 商品数量:count
- 如果从《购物车》进入《订单确认页面》,count在redis数据库中,不需要传递
- 如果从《详情页》进入《订单确认页面》,count在POST请求参数中,需要传递
- 可以通过count是否存在,判断用户是从《购物车》进入《订单确认页面》还是从《详情页》进入的
- 地址:通过
准备工作
# 确认订单 url(r'^place$', views.PlaceOrdereView.as_view(), name='place')
class PlaceOrdereView(LoginRequiredMixin, View): """订单确认页面""" def post(self, request): # 判断用户是否登陆:LoginRequiredMixin # 获取参数:sku_ids, count # 校验sku_ids参数:not # 校验count参数:用于区分用户从哪儿进入订单确认页面 # 如果是从购物车页面过来 # 查询商品数据 # 商品的数量从redis中获取 # 如果是从详情页面过来 # 查询商品数据 # 商品的数量从request中获取,并try校验 # 判断库存:立即购买没有判断库存 # 查询用户地址信息 # 构造上下文 # 响应结果:html页面 pass
立即购买
去结算模板调整
立即购买模板调整
订单确认页面后端视图编写
接收参数
class PlaceOrdereView(LoginRequiredMixin, View): """订单确认页面""" def post(self, request): # 判断用户是否登陆:LoginRequiredMixin # 获取参数:sku_ids, count sku_ids = request.POST.getlist('sku_ids') # 用户从详情过来时,才有count count = request.POST.get('count') pass
校验参数
class PlaceOrdereView(LoginRequiredMixin, View): """订单确认页面""" def post(self, request): # 判断用户是否登陆:LoginRequiredMixin # 获取参数:sku_ids, count sku_ids = request.POST.getlist('sku_ids') # 用户从详情过来时,才有count count = request.POST.get('count') # 校验sku_ids参数 if not sku_ids: # 如果sku_ids没有,就重定向购物车,重选 return redirect(reverse('cart:info')) # 查询商品数据 if count is None: # 如果是从购物车页面过来,商品的数量从redis中获取 # 遍历商品sku_ids else: # 如果是从详情页面过来,商品的数量从request中获取 # 遍历商品sku_ids pass
查询商品和计算金额
- 提示:hgetall()取得的redis中的key是字节类型的
class PlaceOrdereView(LoginRequiredMixin, View): """订单确认页面""" def post(self, request): # 判断用户是否登陆:LoginRequiredMixin # 获取参数:sku_ids, count sku_ids = request.POST.getlist('sku_ids') # 用户从详情过来时,才有count count = request.POST.get('count') # 校验参数 if not sku_ids: # 如果sku_ids没有,就重定向到购物车,重选 return redirect(reverse('cart:info')) # 定义临时容器 skus = [] total_count = 0 total_sku_amount = 0 trans_cost = 10 total_amount = 0 # 实付款 # 查询商品数据 if count is None: # 如果是从购物车页面过来,商品的数量从redis中获取 redis_conn = get_redis_connection('default') user_id = request.user.id cart_dict = redis_conn.hgetall('cart_%s'%user_id) # 遍历商品sku_ids for sku_id in sku_ids: try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 重定向到购物车 return redirect(reverse('cart:info')) # 取出每个sku_id对应的商品数量 sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) # 计算商品总金额 amount = sku.price * sku_count # 将商品数量和金额封装到sku对象 sku.count = sku_count sku.amount = amount skus.append(sku) # 金额和数量求和 total_count += sku_count total_sku_amount += amount else: # 如果是从详情页面过来,商品的数量从request中获取 # 遍历商品sku_ids:如果是从详情过来,sku_ids只有一个sku_id for sku_id in sku_ids: try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 重定向到购物车 return redirect(reverse('cart:info')) # 获取request中得到的count try: sku_count = int(count) except Exception: return redirect(reverse('goods:detail', args=sku_id)) # 判断库存:立即购买没有判断库存 if sku_count > sku.stock: return redirect(reverse('goods:detail', args=sku_id)) # 计算商品总金额 amount = sku.price * sku_count # 将商品数量和金额封装到sku对象 sku.count = sku_count sku.amount = amount skus.append(sku) # 金额和数量求和 total_count += sku_count total_sku_amount += amount # 实付款 total_amount = total_sku_amount + trans_cost # 用户地址信息 try: address = Address.objects.filter(user=request.user).latest('create_time') except Address.DoesNotExist: address = None # 模板会做判断,然后跳转到地址编辑页面 # 构造上下文 context = { 'skus':skus, 'total_count':total_count, 'total_sku_amount':total_sku_amount, 'trans_cost':trans_cost, 'total_amount':total_amount, 'address':address } # 响应结果:html页面 return render(request, 'place_order.html', context)
注意
- 当用户是未登录时,尝试访问这个视图时,会被
LoginRequiredMixin
引导到登陆界面 - 但是,登陆结束后,该装饰器使用的是重定向
next参数
,将用户引导回来 - 问题:
-
PlaceOrdereView
支持POST
请求,但是next参数
重定向回来的是GET
请求 - 错误码
405
-
- 解决:
- 如果是访问订单确认页面,登陆成功后重定向到购物车页面
if next is None: response = redirect(reverse('goods:index')) else: if next == '/orders/place': response = redirect('/cart') else: response = redirect(next)
提交订单分析
- 请求方法:POST
- 订单提交时,可能成功,可能失败
- 在订单提交页面,我们没有设计过渡页面,我们设计的是前后端通过ajax的json沟通的
- 提交订单页面需要登录用户才能访问
- 所以我们又需要验证用户是否登陆
- 由于使用ajax的json在前后端交互,所以LoginRequiredMixin,无法满足需求
- 而且类似ajax的json实现前后端交互,且需要用户验证的需求很多
- 我们可以自定义一个装饰器,实现用户验证并能与ajax的json交互
- 后端的
CommitOrderView
视图,主要逻辑是保存订单信息,并把成功、错误通过json传给前端页面
准备工作
# 订单提交 url(r'^commit$', views.CommitOrderView.as_view(), name='commit')
class CommitOrderView(View): """订单提交""" def post(self, request): pass
自定义返回json的登陆验证装饰器
自定义装饰器
from functools import wraps def login_required_json(view_func): # 恢复view_func的名字和文档 @wraps(view_func) def wrapper(request, *args, **kwargs): # 如果用户未登录,返回json数据 if not request.user.is_authenticated(): return JsonResponse({'code': 1, 'message': '用户未登录'}) else: # 如果用户登陆,进入到view_func中 return view_func(request, *args, **kwargs) return wrapper
封装自定义装饰器的拓展类
class LoginRequiredJSONMixin(object): @classmethod def as_view(cls, **initkwargs): view = super().as_view(**initkwargs) return login_required_json(view)
使用
class CommitOrderView(LoginRequiredJSONMixin, View): """订单提交""" def post(self, request): pass
提交订单后端视图编写
主要逻辑
- 获取订单确认页面传入的数据,查询商品信息
- 在下订单以前,要明确订单相关的两张表:商品订单表和订单商品表
- 商品订单表和订单商品表是一对多的关系
- 一条商品订单记录可以对应多条订单商品记录
参数说明
- 可以根据界面需要的数据分析
- 也可以根据订单相关数据库需要保存的数据分析(参考订单的模型类)
-
参数:
- 用户信息:user
- 地址信息:address_id(确认订单时的地址的id)
- 支付方式:pay_method
- 商品id:sku_ids (sku_ids = '1,2,3'),不是表单,无法做成一键多值
- 商品数量:count
-
商品数量参数的思考
- 点击立即购买将商品加入到购物车
- 当从《详情页》进入《订单确认页面》时,为了提交订单时,不用把商品数量当做请求参数
-
优化逻辑:点击立即购买将商品加入到购物车
需要处理的逻辑
class CommitOrderView(View): """订单提交""" def post(self, request): # 获取参数:user,address_id,pay_method,sku_ids,count # 校验参数:all([address_id, pay_method, sku_ids]) # 判断地址 # 判断支付方式 # 截取出sku_ids列表 # 遍历sku_ids # 循环取出sku,判断商品是否存在 # 获取商品数量,判断库存 (redis) # 减少sku库存 # 增加sku销量 # 保存订单商品数据OrderGoods(能执行到这里说明无异常) # 先创建商品订单信息 # 计算总数和总金额 # 修改订单信息里面的总数和总金额(OrderInfo) # 订单生成后删除购物车(hdel) # 响应结果 pass
timezone
# django提供的时间格式化工具 from django.utils import timezone # python提供的时间格式化工具 datetime 和 time # 相关方法 strftime : 将时间转字符串 strptime : 将字符串转时间 # 使用:20171222031955 timezone.now().strftime('%Y%m%d%H%M%S')
需要处理的逻辑实现
class CommitOrderView(LoginRequiredJSONMixin, View): """提交订单""" def post(self, request): # 获取参数:user,address_id,pay_method,sku_ids,count user = request.user address_id = request.POST.get('address_id') pay_method = request.POST.get('pay_method') sku_ids = request.POST.get('sku_ids') # 校验参数:all([address_id, sku_ids, pay_method]) if not all([address_id, sku_ids, pay_method]): return JsonResponse({'code': 2, 'message': '缺少参数'}) # 判断地址 try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'code': 3, 'message': '地址不存在'}) # 判断支付方式 if pay_method not in OrderInfo.PAY_METHOD: return JsonResponse({'code': 4, 'message': '支付方式错误'}) # 创建redis链接对象,取出字典 redis_conn = get_redis_connection('default') cart_dict = redis_conn.hgetall('cart_%s'%user.id) # 判断商品是否存在:跟前端约定,sku_ids='1,2,3' sku_ids = sku_ids.split(',') # 定义临时容器 total_count = 0 total_amount = 0 # 手动生成order_id order_id = timezone.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 在创建订单商品信息前,创建商品订单信息,(商品订单和订单商品时一对多的关系) order = OrderInfo.objects.create( order_id = order_id, user = user, address = address, total_amount = 0, trans_cost = 10, pay_method = pay_method ) # 遍历sku_ids, for sku_id in sku_ids: # 循环取出sku try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: return JsonResponse({'code': 5, 'message': '商品不存在'}) # 获取商品数量,判断库存 (redis) sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: return JsonResponse({'code': 6, 'message': '库存不足'}) # 减少sku库存 sku.stock -= sku_count # 增加sku销量 sku.sales += sku_count sku.save() # 保存订单商品数据OrderGoods(能执行到这里说明无异常) OrderGoods.objects.create( order = order, sku = sku, count = sku_count, price = sku.price ) # 计算总数和总金额 total_count += sku_count total_amount += (sku_count * sku.price) # 修改订单信息里面的总数和总金额(OrderInfo) order.total_count = total_count order.total_amount = total_amount + 10 order.save() # 订单生成后删除购物车(hdel) redis_conn.hdel('cart_%s'%user.id, *sku_ids) # 响应结果 return JsonResponse({'code': 0, 'message': '下单成功'})
提交订单之事务支持
提交订单需求
- Django中的数据库,默认是自动提交的
- 当
OrderInfo
和OrderGoods
保存数据时,如果出现异常,需要执行回滚,不要自动提交 - 保存数据时,只有当没有任何错误时,才能完成数据的保存
- 要么一起成功,要么一起失败
Django数据库事务
atomic装饰器
from django.db import transaction class TransactionAtomicMixin(object): """提供数据库事务功能""" @classmethod def as_view(cls, **initkwargs): view = super(TransactionAtomicMixin, cls).as_view(**initkwargs) return transaction.atomic(view)
事务实现
- 在操作数据库前创建事务保存点
- 出现异常的地方都回滚到事务保存点
- 暴力回滚,将大范围的数据库操作整体捕获异常
-
当数据库操作结束,还没有异常时,才能提交事务
class CommitOrderView(LoginRequiredJSONMixin, TransactionAtomicMixin, View): """订单提交""" def post(self, request): # 获取参数:user,address_id,pay_method,sku_ids,count user = request.user address_id = request.POST.get('address_id') sku_ids = request.POST.get('sku_ids') # '1,2,3' pay_method = request.POST.get('pay_method') # 校验参数 if not all([address_id, sku_ids, pay_method]): return JsonResponse({'code': 2, 'message': '缺少参数'}) # 判断地址 try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'code': 3, 'message': '地址不存在'}) # 判断支付方式 if pay_method not in OrderInfo.PAY_METHOD: return JsonResponse({'code': 4, 'message': '支付方式错误'}) # 创建redis连接对象 redis_conn = get_redis_connection('default') cart_dict = redis_conn.hgetall('cart_%s' % user.id) # 创建订单id:时间+user_id order_id = timezone.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 在操作数据库前创建事务保存点 save_point = transaction.savepoint() try: # 先创建商品订单信息 order = OrderInfo.objects.create( order_id = order_id, user = user, address = address, total_amount = 0, trans_cost = 10, pay_method = pay_method, ) # 判断商品是否存在 sku_ids = sku_ids.split(',') # 定义临时容器 total_count = 0 total_amount = 0 # 遍历sku_ids,循环取出sku for sku_id in sku_ids: try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 5, 'message': '商品不存在'}) # 获取商品数量,判断库存 sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 6, 'message': '库存不足'}) # 减少库存 sku.stock -= sku_count # 增加销量 sku.sales += sku_count sku.save() # 保存订单商品数据 OrderGoods.objects.create( order = order, sku = sku, count = sku_count, price = sku.price, ) # 计算总数和总金额 total_count += sku_count total_amount += (sku.price * sku_count) # 修改订单信息里面的总数和总金额 order.total_count = total_count order.total_amount = total_amount + 10 order.save() except Exception: # 出现任何异常都回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 7, 'message': '下单失败'}) # 没有异常,就手动提交 transaction.savepoint_commit(save_point) # 订单生成后删除购物车 redis_conn.hdel('cart_%s' % user.id, *sku_ids) # 响应结果 return JsonResponse({'code': 0, 'message': '订单创建成功'})
提交订单之并发和锁
提示
- 问题:多线程和多进程访问共享资源时,容易出现资源抢夺的问题
- 解决:加锁 (悲观锁+乐观锁)
- 悲观锁:
- 当要操作某条记录时,立即将该条记录锁起来,谁也无法操作,直到它操作完
- select * from table where id=1 for update;
- 乐观锁:
- 在查询数据的时候不加锁,在更新时进行判断
- 判断更新时的库存和之前,查出的库存是否一致
- update table set stock=2 where id=1 and stock=7;
乐观锁控制提交订单
-
没有使用锁
# 减少sku库存 sku.stock -= sku_count # 增加sku销量 sku.sales += sku_count sku.save()
使用乐观锁
# 减少库存,增加销量 origin_stock = sku.stock new_stock = origin_stock - sku_count new_sales = sku.sales + sku_count # 更新库存和销量 result = GoodsSKU.objects.filter(id=sku_id,stock=origin_stock).update(stock=new_stock,sales=new_sales) if 0 == result: # 异常,回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 8, 'message': '下单失败'})
乐观锁控制提交订单整体代码
# 每个订单三次下单机会 for i in range(3): pass
class CommitOrderView(LoginRequiredJSONMixin, TransactionAtomicMixin, View): """订单提交""" def post(self, request): # 获取参数:user,address_id,pay_method,sku_ids,count user = request.user address_id = request.POST.get('address_id') sku_ids = request.POST.get('sku_ids') # '1,2,3' pay_method = request.POST.get('pay_method') # 校验参数 if not all([address_id, sku_ids, pay_method]): return JsonResponse({'code': 2, 'message': '缺少参数'}) # 判断地址 try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'code': 3, 'message': '地址不存在'}) # 判断支付方式 if pay_method not in OrderInfo.PAY_METHOD: return JsonResponse({'code': 4, 'message': '支付方式错误'}) # 创建redis连接对象 redis_conn = get_redis_connection('default') cart_dict = redis_conn.hgetall('cart_%s' % user.id) # 创建订单id:时间+user_id order_id = timezone.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 在操作数据库前创建事务保存点 save_point = transaction.savepoint() try: # 先创建商品订单信息 order = OrderInfo.objects.create( order_id = order_id, user = user, address = address, total_amount = 0, trans_cost = 10, pay_method = pay_method, ) # 判断商品是否存在 sku_ids = sku_ids.split(',') # 定义临时容器 total_count = 0 total_amount = 0 # 遍历sku_ids,循环取出sku for sku_id in sku_ids: for i in range(3): try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 5, 'message': '商品不存在'}) # 获取商品数量,判断库存 sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 6, 'message': '库存不足'}) # 减少库存,增加销量 origin_stock = sku.stock new_stock = origin_stock - sku_count new_sales = sku.sales + sku_count # 更新库存和销量 result = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock,sales=new_sales) if 0 == result and i < 2 : continue # 还有机会,继续重新下单 elif 0 == result and i == 2: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 8, 'message': '下单失败'}) # 保存订单商品数据 OrderGoods.objects.create( order = order, sku = sku, count = sku_count, price = sku.price, ) # 计算总数和总金额 total_count += sku_count total_amount += (sku.price * sku_count) # 下单成功,跳出循环 break # 修改订单信息里面的总数和总金额 order.total_count = total_count order.total_amount = total_amount + 10 order.save() except Exception: # 出现任何异常都回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 7, 'message': '下单失败'}) # 没有异常,就手动提交 transaction.savepoint_commit(save_point) # 订单生成后删除购物车 redis_conn.hdel('cart_%s' % user.id, *sku_ids) # 响应结果 return JsonResponse({'code': 0, 'message': '订单创建成功'})
提交订单之测试事务并发和锁
首先处理提交订单的ajax请求
- 处理提交订单的ajax请求,前端的代码在
place_order.html
当中 - post请求地址:
'/orders/commit'
补充sku_ids到模板中
- 提交订单的post请求,需要一个sku_ids数据
- 我们可以在渲染这个
place_order.html
模板时,在上下文中传入sku_ids - 传入的sku_ids,设计成以
,
隔开的字符串
# 构造上下文 context = { 'skus':skus, 'total_count':total_count, 'total_sku_amount':total_sku_amount, 'trans_cost':trans_cost, 'total_amount':total_amount, 'address':address, 'sku_ids':','.join(sku_ids) }
提交订单之事务和并发和锁的测试
- 在更新销量和库存前增加时间延迟
- 在两个浏览器中输入两个账号,分别前后下单,观察后下单的用户是否下单成功
# 获取商品数量,判断库存 (redis) sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 6, 'message': '库存不足'}) import time time.sleep(10) origin_stock = sku.stock new_stock = origin_stock - sku_count new_sales = sku.sales + sku_count result = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales) if 0 == result and i < 2: continue elif 0 == result and i == 2: # 回滚 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 8, 'message': '库存不足'}) # 保存订单商品数据OrderGoods(能执行到这里说明无异常) OrderGoods.objects.create( order=order, sku=sku, count=sku_count, price=sku.price )
补充
- 如果事务和并发和锁的测试失败
- 尝试修改事务隔离级别
- 修改之后重启mysql数据库
- 事务隔离级别相关文档
我的订单代码说明
- 主要逻辑
- 订单数据的查询
- 订单数据渲染到模板
视图
# 订单信息页面 url(r'^(?P<page>\d+)$', views.UserOrdersView.as_view(), name='info')
class UserOrdersView(LoginRequiredMixin, View): """用户订单页面""" def get(self, request, page): """提供订单信息页面""" user = request.user # 查询所有订单 orders = user.orderinfo_set.all().order_by("-create_time") # 遍历所有订单 for order in orders: # 给订单动态绑定:订单状态 order.status_name = OrderInfo.ORDER_STATUS[order.status] # 给订单动态绑定:支付方式 order.pay_method_name = OrderInfo.PAY_METHODS[order.pay_method] order.skus = [] # 查询订单中所有商品 order_skus = order.ordergoods_set.all() # 遍历订单中所有商品 for order_sku in order_skus: sku = order_sku.sku sku.count = order_sku.count sku.amount = sku.price * sku.count order.skus.append(sku) # 分页 page = int(page) try: paginator = Paginator(orders, 2) page_orders = paginator.page(page) except EmptyPage: # 如果传入的页数不存在,就默认给第1页 page_orders = paginator.page(1) page = 1 # 页数 page_list = paginator.page_range context = { "orders": page_orders, "page": page, "page_list": page_list, } return render(request, "user_center_order.html", context)
本站文章如无特殊说明,均为本站原创,如若转载,请注明出处:QQ_990814268 摘抄Django项目购物车、订单(三) - Python技术站