QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

提示

  • 使用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_countbytes类型的,如果做加减操作需要转成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

QQ_990814268 摘抄Django项目购物车、订单(三)

添加购物车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}'

QQ_990814268 摘抄Django项目购物车、订单(三)

操作cookie的相关方法

# 向浏览器中写入购物车cookie信息
response.set_cookie('cart', cart_str)
。。。 # 读取cookie中的购物车信息 cart_json = request.COOKIES.get('cart')

json模块

  • 在操作cookie保存购物车数据时
  • 我们需要将json字符串格式的购物车数据转成python对象来增删改查
  • 需求:将json字符串转成python字典
  • 实现:json模块

QQ_990814268 摘抄Django项目购物车、订单(三)

未登录添加购物车视图和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);
      }
  });
});

 

未登录时添加购物车测试

QQ_990814268 摘抄Django项目购物车、订单(三)

商品模块购物车数量展示

  • 提示:商品模块的购物车包括,主页详情页列表页

  • 由于主页详情页列表页中都涉及到购物车数据的展示

  • 所以,将购物车逻辑封装到基类BaseCartView

QQ_990814268 摘抄Django项目购物车、订单(三)

基类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后,主页,详情页,列表页 都具备相同的数据

QQ_990814268 摘抄Django项目购物车、订单(三)

购物车页面展示

QQ_990814268 摘抄Django项目购物车、订单(三)

分析

  • 如果用户未登录,从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">|&nbsp;&nbsp;&nbsp;&nbsp;购物车</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()不能传入空字典

购物车更新接口设计

QQ_990814268 摘抄Django项目购物车、订单(三)

幂等

  • 非幂等
    • /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">|&nbsp;&nbsp;&nbsp;&nbsp;购物车</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': '删除成功'})

订单

订单生成流程界面

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

页面入口

  • 点击《详情页》的《立即购买》可以进入该《订单确认页面》
  • 点击《购物车》的《去结算》可以进入该《订单确认页面》
  • 点击《订单确认页面》的《提交订单》可以进入《全部订单》
  • 点击《全部订单》的《去付款》可以进入《支付宝》

  


 

订单确认页面分析

QQ_990814268 摘抄Django项目购物车、订单(三)

页面入口

  • 点击《详情页》的《立即购买》可以进入该《订单确认页面》
  • 点击《购物车》的《去结算》可以进入该《订单确认页面》

说明

  • 订单确认页面的生成是由用户发送商品数据给服务器,服务器渲染后再响应给客户端而生成的
  • 请求方法: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')
    • 商品数量: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

 

 

 

立即购买

去结算模板调整

QQ_990814268 摘抄Django项目购物车、订单(三)

立即购买模板调整

QQ_990814268 摘抄Django项目购物车、订单(三)

订单确认页面后端视图编写

接收参数

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)

 

提交订单分析

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

  • 请求方法: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
    • 商品数量参数的思考

      • 点击立即购买将商品加入到购物车
      • 当从《详情页》进入《订单确认页面》时,为了提交订单时,不用把商品数量当做请求参数
      • 优化逻辑:点击立即购买将商品加入到购物车

QQ_990814268 摘抄Django项目购物车、订单(三)

需要处理的逻辑

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中的数据库,默认是自动提交的
  • OrderInfoOrderGoods保存数据时,如果出现异常,需要执行回滚,不要自动提交
  • 保存数据时,只有当没有任何错误时,才能完成数据的保存
  • 要么一起成功,要么一起失败

Django数据库事务

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄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' 

QQ_990814268 摘抄Django项目购物车、订单(三)

补充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
)

补充

 QQ_990814268 摘抄Django项目购物车、订单(三)

QQ_990814268 摘抄Django项目购物车、订单(三)

我的订单代码说明

  • 主要逻辑
    • 订单数据的查询
    • 订单数据渲染到模板

视图

 

# 订单信息页面
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)