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

添加購物車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模塊javascript

未登陸添加購物車視圖和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請求代碼

  • 不須要再判斷是不是登錄用戶html

$('#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);
      }
  });
});

 

未登陸時添加購物車測試

商品模塊購物車數量展現

  • 提示:商品模塊的購物車包括,主頁詳情頁列表頁前端

  • 因爲主頁詳情頁列表頁中都涉及到購物車數據的展現java

  • 因此,將購物車邏輯封裝到基類BaseCartViewpython

基類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後,主頁,詳情頁,列表頁 都具有相同的數據mysql

購物車頁面展現

分析

  • 若是用戶未登陸,從cookie中獲取購物車數據
# cookie
'cart':'{'sku_1':10, 'sku_2':20}'

若是用戶已登陸,從redis中獲取購物車數據jquery

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()不能傳入空字典

購物車更新接口設計

冪等

  • 非冪等
    • /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': '刪除成功'})

訂單

訂單生成流程界面

頁面入口

  • 點擊《詳情頁》的《當即購買》能夠進入該《訂單確認頁面》
  • 點擊《購物車》的《去結算》能夠進入該《訂單確認頁面》
  • 點擊《訂單確認頁面》的《提交訂單》能夠進入《所有訂單》
  • 點擊《所有訂單》的《去付款》能夠進入《支付寶》

  


 

訂單確認頁面分析

頁面入口

  • 點擊《詳情頁》的《當即購買》能夠進入該《訂單確認頁面》
  • 點擊《購物車》的《去結算》能夠進入該《訂單確認頁面》

說明

  • 訂單確認頁面的生成是由用戶發送商品數據給服務器,服務器渲染後再響應給客戶端而生成的
  • 請求方法: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

 

 

 

模板調整:去結算當即購買

去結算模板調整

當即購買模板調整

訂單確認頁面後端視圖編寫

接收參數

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

 

 

提交訂單後端視圖編寫

主要邏輯

  • 獲取訂單確認頁面傳入的數據,查詢商品信息
  • 在下訂單之前,要明確訂單相關的兩張表:商品訂單表和訂單商品表
  • 商品訂單表和訂單商品表是一對多的關係
  • 一條商品訂單記錄能夠對應多條訂單商品記錄

參數說明

  • 能夠根據界面須要的數據分析
  • 也能夠根據訂單相關數據庫須要保存的數據分析(參考訂單的模型類)
  • 參數:web

    • 用戶信息:user
    • 地址信息:address_id(確認訂單時的地址的id)
    • 支付方式:pay_method
    • 商品id:sku_ids (sku_ids = '1,2,3'),不是表單,沒法作成一鍵多值
    • 商品數量:count
    • 商品數量參數的思考ajax

      • 點擊當即購買將商品加入到購物車
      • 當從《詳情頁》進入《訂單確認頁面》時,爲了提交訂單時,不用把商品數量當作請求參數
      • 優化邏輯:點擊當即購買將商品加入到購物車redis

須要處理的邏輯

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數據庫事務

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
)

補充

 

個人訂單代碼說明

  • 主要邏輯
    • 訂單數據的查詢
    • 訂單數據渲染到模板

視圖

 

# 訂單信息頁面
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)
相關文章
相關標籤/搜索