購物車
提示
- 使用redis數據庫存儲購物車數據
- 購物車需要完成增、刪、改、查的邏輯
- 查詢的結果,需要由服務器響應界面給客戶端展示出來
- 增刪改的操作,是客戶端發數據給服務器,兩者之間的交互是局部刷新的效果,需要用ajax交互
- 添加購物車的請求方法:post
- 服務器和客戶端傳輸數據格式:json
- 服務器接收的數據
- 用戶id:user_id
- 商品id:sku_id
- 商品數量:count
定義添加購物車視圖
# 項目的urls url(r'^cart/', include('cart.urls', namespace='cart')),
# 應用的urls urlpatterns = [ url(r'^add$', views.AddCartView.as_view(), name='add') ]
class AddCartView(View): """添加到購物車""" def post(self, request): # 判斷用戶是否登陸 # 接收數據:user_id,sku_id,count # 校驗參數all() # 判斷商品是否存在 # 判斷count是否是整數 # 判斷庫存 # 操作redis數據庫存儲商品到購物車 # json方式響應添加購物車結果 pass
添加到購物車視圖和JS
添加到購物車視圖
- 判斷用戶是否登陸
- 此時不需要使用裝飾器
LoginRequiredMixin
- 用戶訪問到
AddCartView
時,需要是登陸狀態 - 因為購物車中使用json在前后端交互,是否登陸的結果也要以json格式交給客戶端
- 而
LoginRequiredMixin
驗證之后的結果是以重定向的方式告訴客戶端的
- 此時不需要使用裝飾器
- 獲取請求參數:用戶id:user_id。商品id:sku_id。商品數量:count
- 校驗參數:all(),判斷參數是否完整
- 判斷商品是否存在,如果不存在,異常為:
GoodsSKU.DoesNotExist
- 判斷count是否是整數,不是整數,異常為:
Exception
- 判斷庫存
- 以上判斷沒錯,才能操作redis,存儲商品到購物車
- 如果加入的商品不在購物車中,直接新增到購物車
- 如果加入的商品存在購物車中,直接累加計數即可
- 為了方便前端展示購物車數量,所以查詢一下購物車總數
- 提示:
origin_count = redis_conn.hget("cart_%s" %user_id, sku_id)
origin_count
為bytes
類型的,如果做加減操作需要轉成int(origin_count)
- 字典遍歷出來的值,也是
bytes
類型的,如果做加減操作需要轉成整數
class AddCartView(View): """添加購物車""" def post(self, request): # 判斷用戶是否登陸 if not request.user.is_authenticated(): return JsonResponse({'code': 1, 'message':'用戶未登錄'}) # 接收數據:user_id,sku_id,count user_id = request.user.id sku_id = request.POST.get('sku_id') count = request.POST.get('count') # 校驗參數 if not all([sku_id,count]): return JsonResponse({'code': 2, 'message': '參數不完整'}) # 判斷商品是否存在 try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: return JsonResponse({'code': 3, 'message': '商品不存在'}) # 判斷count是否是整數 try: count = int(count) except Exception: return JsonResponse({'code': 4, 'message': '數量錯誤'}) # 判斷庫存 # if count > sku.stock: # return JsonResponse({'code': 5, 'message': '庫存不足'}) # 操作redis數據庫存儲商品到購物車 redis_conn = get_redis_connection('default') # 需要先獲取要添加到購物車的商品是否存在 origin_count = redis_conn.hget('cart_%s'%user_id, sku_id) # 如果商品在購物車中存在,就直接累加商品數量;反之,把新的商品和數量添加到購物車 if origin_count is not None: count += int(origin_count) # 判斷庫存:計算最終的count與庫存比較 if count > sku.stock: return JsonResponse({'code': 5, 'message': '庫存不足'}) # 存儲到redis redis_conn.hset('cart_%s'%user_id, sku_id, count) # 為了配合模板中js交互並展示購物車的數量,在這里需要查詢一下購物車的總數 cart_num = 0 cart_dict = redis_conn.hgetall('cart_%s'%user_id) for val in cart_dict.values(): cart_num += int(val) # json方式響應添加購物車結果 return JsonResponse({'code': 0, 'message': '添加購物車成功', 'cart_num':cart_num})
添加到購物車JS
- ajax請求方法:post
- 請求地址:/cart/add
- 請求參數:商品id+商品數量+csrftoken
添加購物車ajax請求
$('#add_cart').click(function(){ // 將商品的id和數量發送給后端視圖,后端進行購物車數據的記錄 // 獲取商品的id和數量 var request_data = { sku_id: $('#add_cart').attr('sku_id'), count: $('#num_show').val(), csrfmiddlewaretoken: "{{ csrf_token }}" }; // 使用ajax向后端發送數據 $.post('/cart/add', request_data, function (response_data) { // 根據后端響應的數據,決定處理效果 if (1 == response_data.code){ location.href = '/users/login'; // 如果未登錄,跳轉到登錄頁面 } else if (0 == response_data.code) { $(".add_jump").stop().animate({ 'left': $to_y+7, 'top': $to_x+7}, "fast", function() { $(".add_jump").fadeOut('fast',function(){ // 展示購物車總數量 $('#show_count').html(response_data.cart_num); }); }); } else { // 其他錯誤信息,簡單彈出來 alert(response_data.message); } }); });
未登錄添加購物車介紹
思考
- 當用戶已登錄時,將購物車數據存儲到服務器的redis中
- 當用戶未登錄時,將購物車數據存儲到瀏覽器的cookie中
- 當用戶進行登錄時,將cookie中的購物車數據合並到redis中
- 購物車的增刪改查都要區分用戶是否登陸
設計思想
- 使用json字符串將購物車數據保存到瀏覽器的cookie中
- 提示:每個人的瀏覽器cookie存儲的都是個人的購物車數據,所以key不用唯一標示
'cart':'{'sku_1':10, 'sku_2':20}'
操作cookie的相關方法
# 向瀏覽器中寫入購物車cookie信息 response.set_cookie('cart', cart_str)
。。。 # 讀取cookie中的購物車信息 cart_json = request.COOKIES.get('cart')
json模塊
- 在操作cookie保存購物車數據時
- 我們需要將json字符串格式的購物車數據轉成python對象來增刪改查
- 需求:將json字符串轉成python字典
-
實現:json模塊
未登錄添加購物車視圖和JS
提示
- 如果用戶未登錄,就保存購物車數據到cookie中
- 即使用戶未登錄,客戶端在添加購物車時,也會向服務器傳遞商品(sku_id)和商品數量(count)
- 所以,也是需要校驗和判斷
- 直到,需要存儲購物車數據時,才來判斷是否是登陸狀態
核心邏輯
1.先從cookie中,獲取當前商品的購物車記錄 (cart_json) 2.判斷購物車(cart_json)數據是否存在,有可能用戶從來沒有操作過購物車 2.1.如果(cart_json)存在就把它轉成字典(cart_dict) 2.2.如果(cart_json)不存在就定義空字典(cart_dict) 3.判斷要添加的商品在購物車中是否存在 3.1.如果存在就取出源有值,並進行累加 3.2.如果不存在就直接保存商品數量 4.將(cart_dict)重新生成json字符串,方便寫入到cookie 5.創建JsonResponse對象,該對象就是要響應的對象 6.在響應前,設置cookie信息 7.計算購物車數量總和,方便前端展示
核心代碼
if not request.user.is_authenticated(): # 如果用戶未登錄,就保存購物車數據到cookie中 # 先從cookie的購物車信息中,獲取當前商品的購物車記錄,即json字符串購物車數據 cart_json = request.COOKIES.get('cart') # 判斷購物車cookie數據是否存在,有可能用戶從來沒有操作過購物車 if cart_json is not None: # 將json字符串轉成json字典 cart_dict = json.loads(cart_json) else: # 如果用戶沒有操作購物車,就給個空字典 cart_dict = {} if sku_id in cart_dict: # 如果cookie中有這個商品記錄,則直接進行求和;如果cookie中沒有這個商品記錄,則將記錄設置到購物車cookie中 origin_count = cart_dict[sku_id] # json模塊,存進去的是數字,取出來的也是數字 count += origin_count # 判斷庫存:計算最終的count與庫存比較 if count > sku.stock: return JsonResponse({'code': 6, 'message': '庫存不足'}) # 設置最終的商品數量到購物車 cart_dict[sku_id] = count # 計算購物車總數 cart_num = 0 for val in cart_dict.values(): cart_num += int(val) # 將json字典轉成json字符串 cart_str = json.dumps(cart_dict) # 將購物車數據寫入到cookie中 response = JsonResponse({"code": 0, "message": "添加購物車成功", 'cart_num': cart_num}) response.set_cookie('cart', cart_str) return response
整體實現
class AddCartView(View): """添加到購物車: sku_id, count, user_id""" def post(self, request): # 判斷用戶是否登錄 # if not request.user.is_authenticated(): # # 提示用戶未登錄 # return JsonResponse({"code": 1, "message": "用戶未登錄"}) # 商品id sku_id = request.POST.get("sku_id") # 商品數量 count = request.POST.get("count") # 檢驗參數 if not all([sku_id, count]): return JsonResponse({"code": 2, "message": "參數不完整"}) # 判斷商品是否存在 try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 表示商品不存在 return JsonResponse({"code": 3, "message": "商品不存在"}) # 判斷count是整數 try: count = int(count) except Exception: return JsonResponse({"code": 4, "message": "參數錯誤"}) # 判斷庫存 # if count > sku.stock: # return JsonResponse({"code": 5, "message": "庫存不足"}) # 提示:無論是否登陸狀態,都需要獲取suk_id,count,校驗參數。。。 # 所以等待參數校驗結束后,再來判斷用戶是否登陸 # 如果用戶已登錄,就保存購物車數據到redis中 if request.user.is_authenticated(): # 用戶id user_id = request.user.id # "cart_用戶id": {"sku_1": 10, "sku_2": 11} # 先嘗試從用戶的購物車中獲取這個商品的數量,如果購物車中不存在這個商品,則直接添加購物車記錄 # 否則,需要進行數量的累計,在添加到購物車記錄中 redis_conn = get_redis_connection("default") origin_count = redis_conn.hget("cart_%s" % user_id, sku_id) # 原有數量 if origin_count is not None: count += int(origin_count) # 判斷庫存:計算最終的count與庫存比較 if count > sku.stock: return JsonResponse({'code': 5, 'message': '庫存不足'}) # 存儲到redis redis_conn.hset("cart_%s" % user_id, sku_id, count) # 為了方便前端展示購物車數量,所以查詢一下購物車總數 cart_num = 0 cart = redis_conn.hgetall("cart_%s" % user_id) for val in cart.values(): cart_num += int(val) # 采用json返回給前端 return JsonResponse({"code": 0, "message": "添加購物車成功", "cart_num": cart_num}) else: # 如果用戶未登錄,就保存購物車數據到cookie中 # 先從cookie的購物車信息中,獲取當前商品的記錄,json字符串購物車數據 cart_json = request.COOKIES.get('cart') # 判斷購物車cookie數據是否存在,有可能用戶從來沒有操作過購物車 if cart_json is not None: # 將json字符串轉成json字典 cart_dict = json.loads(cart_json) else: # 如果用戶沒有操作購物車,就給個空字典 cart_dict = {} if sku_id in cart_dict: # 如果cookie中有這個商品記錄,則直接進行求和;如果cookie中沒有這個商品記錄,則將記錄設置到購物車cookie中 origin_count = cart_dict[sku_id] count += origin_count # 判斷庫存:計算最終的count與庫存比較 if count > sku.stock: return JsonResponse({'code': 6, 'message': '庫存不足'}) # 設置最終的商品數量到購物車 cart_dict[sku_id] = count # 計算購物車總數 cart_num = 0 for val in cart_dict.values(): cart_num += val # 將json字典轉成json字符串 cart_str = json.dumps(cart_dict) # 將購物車數據寫入到cookie中 response = JsonResponse({"code": 0, "message": "添加購物車成功", 'cart_num': cart_num}) response.set_cookie('cart', cart_str) return response
前端ajax請求代碼
-
不需要再判斷是否是登陸用戶
$('#add_cart').click(function(){ // 將商品的id和數量發送給后端視圖,后端進行購物車數據的記錄 // 獲取商品的id和數量 var request_data = { sku_id: $(this).attr('sku_id'), count: $('#num_show').val(), csrfmiddlewaretoken: "" }; // 使用ajax向后端發送數據 $.post('/cart/add', request_data, function (response_data) { // 根據后端響應的數據,決定處理效果 if (0 == response_data.code) { $(".add_jump").stop().animate({ 'left': $to_y+7, 'top': $to_x+7}, "fast", function() { $(".add_jump").fadeOut('fast',function(){ // 展示購物車總數量 $('#show_count').html(response_data.cart_num); }); }); } else { // 其他錯誤信息,簡單彈出來 alert(response_data.message); } }); });
未登錄時添加購物車測試
商品模塊購物車數量展示
-
提示:商品模塊的購物車包括,
主頁
、詳情頁
、列表頁
-
由於
主頁
、詳情頁
、列表頁
中都涉及到購物車數據的展示 -
所以,將購物車邏輯封裝到基類
BaseCartView
中
基類BaseCartView
class BaseCartView(View): """提供購物車數據統計功能""" def get_cart_num(self, request): cart_num = 0 # 如果用戶登錄,就從redis中獲取購物車數據 if request.user.is_authenticated(): # 創建redis_conn對象 redis_conn = get_redis_connection('default') # 獲取用戶id user_id = request.user.id # 從redis中獲取購物車數據,返回字典,如果沒有數據,返回None,所以不需要異常判斷 cart = redis_conn.hgetall('cart_%s' %user_id) # 遍歷購物車字典,累加購物車的值 for value in cart.values(): cart_num += int(value) else: # 如果用戶未登錄,就從cookie中獲取購物車數據 cart_json = request.COOKIES.get('cart') # json字符串 # 判斷購物車數據是否存在 if cart_json is not None: # 將json字符串購物車數據轉成json字典 cart_dict = json.loads(cart_json) else: cart_dict = {} # 遍歷購物車字典,計算商品數量 for val in cart_dict.values(): cart_num += val return cart_num
修改base.html模板
-
修改了
base.html
后,主頁,詳情頁,列表頁 都具備相同的數據
購物車頁面展示
分析
- 如果用戶未登錄,從cookie中獲取購物車數據
# cookie 'cart':'{'sku_1':10, 'sku_2':20}'
如果用戶已登錄,從redis中獲取購物車數據
cart_userid:{sku_3, 30}
- 展示購物車數據時,不需要客戶端傳遞數據到服務器端,因為需要的數據可以從cookie或者redis中獲取
- 注意:從cookie中獲取的count是整數,從redis中獲取的count是字節類型,需要統一類型
定義視圖
url(r'^cart/', include('cart.urls', namespace='cart'))
url(r'^cart/', include('cart.urls', namespace='cart'))
class CartInfoView(View): """獲取購物車數據""" def get(self, request): """提供購物車頁面:不需要請求參數""" pass
展示購物車數據視圖
class CartInfoView(View): """獲取購物車數據""" def get(self, request): """提供購物車頁面:不需要請求參數""" # 查詢購物車數據 # 如果用戶登陸從redis中獲取數據 if request.user.is_authenticated(): # 創建redis連接對象 redis_conn = get_redis_connection('default') user_id = request.user.id # 獲取所有數據 cart_dict = redis_conn.hgetall('cart_%s'%user_id) else: # 如果用戶未登陸從cookie中獲取數據 cart_json = request.COOKIES.get('cart') # 判斷用戶是否操作過購物車cookie if cart_json is not None: cart_dict = json.loads(cart_json) else: cart_dict = {} # 保存遍歷出來的sku skus = [] # 總金額 total_amount = 0 # 總數量 total_count = 0 # 遍歷cart_dict,形成模板所需要的數據 for sku_id, count in cart_dict.items(): # 查詢商品sku try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 商品不存在,跳過這個商品,繼續遍歷 continue # 將count轉成整數,因為redis中取出的count不是整數類型的 count = int(count) # 計算總價 amount = sku.price * count # 將需要展示的數據保存到對象中 sku.amount = amount sku.count = count # 生成模型列表 skus.append(sku) # 計算總金額 total_amount += amount # 計算總數量 total_count += count # 構造上下文 context = { 'skus':skus, 'total_amount':total_amount, 'total_count':total_count } return render(request, 'cart.html', context)
展示購物車數據模板
{% extends 'base.html' %} {% block title %}天天生鮮-購物車{% endblock %} {% load staticfiles %} {% block search_bar %} <div class="search_bar clearfix"> <a href="{% url 'goods:index' %}" class="logo fl"><img src="{% static 'images/logo.png' %}"></a> <div class="sub_page_name fl">| 購物車</div> <div class="search_con fr"> <form action="/search/" method="get"> <input type="text" class="input_text fl" name="q" placeholder="搜索商品"> <input type="submit" class="input_btn fr" value="搜索"> </form> </div> </div> {% endblock %} {% block body %} <div class="total_count">全部商品<em>{{total_count}}</em>件</div> <ul class="cart_list_th clearfix"> <li class="col01">商品名稱</li> <li class="col02">商品單位</li> <li class="col03">商品價格</li> <li class="col04">數量</li> <li class="col05">小計</li> <li class="col06">操作</li> </ul> <form method="post" action="#"> {% csrf_token %} {% for sku in skus %} <ul class="cart_list_td clearfix" sku_id="{{ sku.id }}"> <li class="col01"><input type="checkbox" name="sku_ids" value="{{ sku.id }}" checked></li> <li class="col02"><img src="{{ sku.default_image.url }}"></li> <li class="col03">{{ sku.name }}<br><em>{{ sku.price }}/{{ sku.unit}}</em></li> <li class="col04">{{ sku.unit }}</li> <li class="col05"><span>{{sku.price}}</span>元</li> <li class="col06"> <div class="num_add"> <a href="javascript:;" class="add fl">+</a> <input type="text" class="num_show fl" sku_id="{{ sku.id }}" value="{{sku.count}}"> <a href="javascript:;" class="minus fl">-</a> </div> </li> <li class="col07"><span>{{sku.amount}}</span>元</li> <li class="col08"><a href="javascript:;" class="del_btn">刪除</a></li> </ul> {% endfor %} <ul class="settlements"> <li class="col01"><input type="checkbox" checked></li> <li class="col02">全選</li> <li class="col03">合計(不含運費):<span>¥</span><em id="total_amount">{{total_amount}}</em><br>共計<b id="total_count">{{total_count}}</b>件商品</li> <li class="col04"><a href="place_order.html">去結算</a></li> </ul> </form> {% endblock %} {% block bottom_files %} <script type="text/javascript" src="{% static 'js/jquery-1.12.2.js' %}"></script> <script type="text/javascript"> // 更新頁面合計信息 function freshOrderCommitInfo() { var total_amount = 0; //總金額 var total_count = 0; // 總數量 $('.cart_list_td').find(':checked').parents('ul').each(function () { var sku_amount = $(this).children('li.col07').text(); // 商品的金額 var sku_count = $(this).find('.num_show').val(); // 商品的數量 total_count += parseInt(sku_count); total_amount += parseFloat(sku_amount); }); // 設置商品的總數和總價 $("#total_amount").text(total_amount.toFixed(2)); $("#total_count").text(total_count); } // 更新頁面頂端全部商品數量 function freshTotalGoodsCount() { var total_count = 0; $('.cart_list_td').find(':checkbox').parents('ul').each(function () { var sku_count = $(this).find('.num_show').val(); total_count += parseInt(sku_count); }); $(".total_count>em").text(total_count); } // 更新后端購物車信息 function updateRemoteCartInfo(sku_id, sku_count, num_dom) { // 發送給后端的數據 var req = { sku_id: sku_id, count: sku_count, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post("/cart/update", req, function(data){ if (0 == data.code) { // 更新商品數量 $(num_dom).val(sku_count); // 更新商品金額信息 var sku_price = $(".cart_list_td[sku_id="+sku_id+"]").children('li.col05').children().text(); var sku_amount = parseFloat(sku_price) * sku_count; $(".cart_list_td[sku_id="+sku_id+"]").children('li.col07').children().text(sku_amount.toFixed(2)); // 更新頂部商品總數 freshTotalGoodsCount(); // 更新底部合計信息 freshOrderCommitInfo(); } else { alert(data.message); } }); } // 增加 $(".add").click(function(){ // 獲取操作的商品id var sku_id = $(this).next().attr("sku_id"); // 獲取加操作前的的數量 var sku_num = $(this).next().val(); // 進行數量加1 sku_num = parseInt(sku_num); sku_num += 1; // 顯示商品數目的dom var num_dom = $(this).next(); // 更新購物車數量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 減少 $(".minus").click(function(){ // 獲取操作的商品id var sku_id = $(this).prev().attr("sku_id"); // 獲取加操作前的的數量 var sku_num = $(this).prev().val(); // 進行數量加1 sku_num = parseInt(sku_num); sku_num -= 1; if (sku_num < 1) sku_num = 1; // 更新頁面顯示數量 var num_dom = $(this).prev(); // 更新購物車數量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); var pre_sku_count = 0; $('.num_show').focus(function () { // 記錄用戶手動輸入之前商品數目 pre_sku_count = $(this).val(); }); // 手動輸入 $(".num_show").blur(function(){ var sku_id = $(this).attr("sku_id"); var sku_num = $(this).val(); // 如果輸入的數據不合理,則將輸入值設置為在手動輸入前記錄的商品數目 if (isNaN(sku_num) || sku_num.trim().length<=0 || parseInt(sku_num)<=0) { $(this).val(pre_sku_count); return; } sku_num = parseInt(sku_num); var num_dom = $(this); updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 刪除 $(".del_btn").click(function(){ var sku_id = $(this).parents("ul").attr("sku_id"); var req = { sku_id: sku_id, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post('/cart/delete', req, function(data){ // window.reload() location.href="/cart/"; // 刪除后,刷新頁面 }); }); // 商品對應checkbox發生改變時,全選checkbox發生改變 $('.cart_list_td').find(':checkbox').change(function () { // 獲取商品所有checkbox的數目 var all_len = $('.cart_list_td').find(':checkbox').length; // 獲取選中商品的checkbox的數目 var checked_len = $('.cart_list_td').find(':checked').length; if (checked_len < all_len){ // 有商品沒有被選中 $('.settlements').find(':checkbox').prop('checked', false) } else{ // 所有商品都被選中 $('.settlements').find(':checkbox').prop('checked', true) } freshOrderCommitInfo(); }); // 全選和全不選 $('.settlements').find(':checkbox').change(function () { // 1.獲取當前checkbox的選中狀態 var is_checked = $(this).prop('checked'); // 2.遍歷並設置商品ul中checkbox的選中狀態 $('.cart_list_td').find(':checkbox').each(function () { // 設置每一個goods ul中checkbox的值 $(this).prop('checked', is_checked) }); freshOrderCommitInfo(); }); </script> {% endblock %}
登陸時購物車合並cookie和redis
- 需求:在登陸頁面跳轉前,將cookie和redis中的購物車數據合並到redis中
分析
- 獲取cookie中的購物車數據
- 獲取redis中的購物車數據
- 合並購物車上商品數量信息
- 如果cookie中存在的,redis中也有,則進行數量累加
- 如果cookie中存在的,redis中沒有,則生成新的購物車數據
- 將cookie中的購物車數據合並到redis中
- 清除瀏覽器購物車cookie
實現
# 在頁面跳轉之前,將cookie中和redis中的購物車數據合並 # 從cookie中獲取購物車數據 cart_json = request.COOKIES.get('cart') if cart_json is not None: cart_dict_cookie = json.loads(cart_json) else: cart_dict_cookie = {} # 從redis中獲取購物車數據 redis_conn = get_redis_connection('default') cart_dict_redis = redis_conn.hgetall('cart_%s'%user.id) # 進行購物車商品數量合並:將cookie中購物車數量合並到redis中 for sku_id, count in cart_dict_cookie.items(): # 提示:由於redis中的鍵與值都是bytes類型,cookie中的sku_id是字符串類型 # 需要將cookie中的sku_id字符串轉成bytes sku_id = sku_id.encode() if sku_id in cart_dict_redis: # 如果cookie中的購物車商品在redis中也有,就取出來累加到redis中 # 提示:redis中的count是bytes,cookie中的count是整數,無法求和,所以,轉完數據類型在求和 origin_count = cart_dict_redis[sku_id] count += int(origin_count) # 如果cookie中的商品在redis中有,就累加count賦值。反之,直接賦值cookie中的count cart_dict_redis[sku_id] = count # 將合並后的redis數據,設置到redis中:redis_conn.hmset()不能傳入空字典 if cart_dict_redis: redis_conn.hmset('cart_%s'%user.id, cart_dict_redis) # 獲取next參數,用於判斷登陸界面是從哪里來的 next = request.GET.get('next') if next is None: # 跳轉到首頁 response = redirect(reverse('goods:index')) else: # 從哪兒來,回哪兒去 response = redirect(next) # 清除cookie response.delete_cookie('cart') return response
注意
- 由於redis中的鍵與值都是bytes類型,cookie中的sku_id是字符串類型,兩者要統一類型再比較
- redis中的count是bytes,cookie中的count是整數,無法求和,所以,轉完數據類型在求和
- redis_conn.hmset()不能傳入空字典
購物車更新接口設計
冪等
- 非冪等
- /cart/update?sku_id=1 & num=1
- 對於同一種行為,如果最終的結果與執行的次數有關,每次執行后結果都不相同,就稱這種行為為非冪等
- 缺點:當用戶多次點擊
+
,增加購物車商品數量時,如果其中有一次數據傳輸失敗,則商品數量出錯
- 冪等
- /cart/update?sku_id=1&finally_num=18
- 對於同一種行為,如果執行不論多少次,最終的結果都是一致相同的,就稱這種行為是冪等的
- 結論:
- 購物車中,無論是增加還是減少商品數量,都對應相同接口,都傳遞最終商品的數量即可
- 優點:每次向服務器發送的是最終商品的數量,如果某一次傳輸失敗,可以展示上一次的商品最終數量
購物車前端代碼編寫
主要邏輯
- 更新頁面合計信息
- 更新頁面頂端全部商品數量
- 更新后端購物車信息
- 增加商品數量
- 減少商品數量
- 手動輸入商品數量
- 商品對應checkbox發生改變時,全選checkbox發生改變
- 全選和全不選
{% extends 'base.html' %} {% block title %}天天生鮮-購物車{% endblock %} {% load staticfiles %} {% block search_bar %} <div class="search_bar clearfix"> <a href="{% url 'goods:index' %}" class="logo fl"><img src="{% static 'images/logo.png' %}"></a> <div class="sub_page_name fl">| 購物車</div> <div class="search_con fr"> <form action="/search/" method="get"> <input type="text" class="input_text fl" name="q" placeholder="搜索商品"> <input type="submit" class="input_btn fr" value="搜索"> </form> </div> </div> {% endblock %} {% block body %} <div class="total_count">全部商品<em>{{total_count}}</em>件</div> <ul class="cart_list_th clearfix"> <li class="col01">商品名稱</li> <li class="col02">商品單位</li> <li class="col03">商品價格</li> <li class="col04">數量</li> <li class="col05">小計</li> <li class="col06">操作</li> </ul> <form method="post" action="#"> {% csrf_token %} {% for sku in skus %} <ul class="cart_list_td clearfix" sku_id="{{ sku.id }}"> <li class="col01"><input type="checkbox" name="sku_ids" value="{{ sku.id }}" checked></li> <li class="col02"><img src="{{ sku.default_image.url }}"></li> <li class="col03">{{ sku.name }}<br><em>{{ sku.price }}/{{ sku.unit}}</em></li> <li class="col04">{{ sku.unit }}</li> <li class="col05"><span>{{sku.price}}</span>元</li> <li class="col06"> <div class="num_add"> <a href="javascript:;" class="add fl">+</a> <input type="text" class="num_show fl" sku_id="{{ sku.id }}" value="{{sku.count}}"> <a href="javascript:;" class="minus fl">-</a> </div> </li> <li class="col07"><span>{{sku.amount}}</span>元</li> <li class="col08"><a href="javascript:;" class="del_btn">刪除</a></li> </ul> {% endfor %} <ul class="settlements"> <li class="col01"><input type="checkbox" checked></li> <li class="col02">全選</li> <li class="col03">合計(不含運費):<span>¥</span><em id="total_amount">{{total_amount}}</em><br>共計<b id="total_count">{{total_count}}</b>件商品</li> <li class="col04"><a href="place_order.html">去結算</a></li> </ul> </form> {% endblock %} {% block bottom_files %} <script type="text/javascript" src="{% static 'js/jquery-1.12.2.js' %}"></script> <script type="text/javascript"> // 更新頁面合計信息 function freshOrderCommitInfo() { var total_amount = 0; //總金額 var total_count = 0; // 總數量 $('.cart_list_td').find(':checked').parents('ul').each(function () { var sku_amount = $(this).children('li.col07').text(); // 商品的金額 var sku_count = $(this).find('.num_show').val(); // 商品的數量 total_count += parseInt(sku_count); total_amount += parseFloat(sku_amount); }); // 設置商品的總數和總價 $("#total_amount").text(total_amount.toFixed(2)); $("#total_count").text(total_count); } // 更新頁面頂端全部商品數量 function freshTotalGoodsCount() { var total_count = 0; $('.cart_list_td').find(':checkbox').parents('ul').each(function () { var sku_count = $(this).find('.num_show').val(); total_count += parseInt(sku_count); }); $(".total_count>em").text(total_count); } // 更新后端購物車信息 function updateRemoteCartInfo(sku_id, sku_count, num_dom) { // 發送給后端的數據 var req = { sku_id: sku_id, count: sku_count, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post("/cart/update", req, function(data){ if (0 == data.code) { // 更新商品數量 $(num_dom).val(sku_count); // 更新商品金額信息 var sku_price = $(".cart_list_td[sku_id="+sku_id+"]").children('li.col05').children().text(); var sku_amount = parseFloat(sku_price) * sku_count; $(".cart_list_td[sku_id="+sku_id+"]").children('li.col07').children().text(sku_amount.toFixed(2)); // 更新頂部商品總數 freshTotalGoodsCount(); // 更新底部合計信息 freshOrderCommitInfo(); } else { alert(data.message); } }); } // 增加 $(".add").click(function(){ // 獲取操作的商品id var sku_id = $(this).next().attr("sku_id"); // 獲取加操作前的的數量 var sku_num = $(this).next().val(); // 進行數量加1 sku_num = parseInt(sku_num); sku_num += 1; // 顯示商品數目的dom var num_dom = $(this).next(); // 更新購物車數量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 減少 $(".minus").click(function(){ // 獲取操作的商品id var sku_id = $(this).prev().attr("sku_id"); // 獲取加操作前的的數量 var sku_num = $(this).prev().val(); // 進行數量加1 sku_num = parseInt(sku_num); sku_num -= 1; if (sku_num < 1) sku_num = 1; // 更新頁面顯示數量 var num_dom = $(this).prev(); // 更新購物車數量 updateRemoteCartInfo(sku_id, sku_num, num_dom); }); var pre_sku_count = 0; $('.num_show').focus(function () { // 記錄用戶手動輸入之前商品數目 pre_sku_count = $(this).val(); }); // 手動輸入 $(".num_show").blur(function(){ var sku_id = $(this).attr("sku_id"); var sku_num = $(this).val(); // 如果輸入的數據不合理,則將輸入值設置為在手動輸入前記錄的商品數目 if (isNaN(sku_num) || sku_num.trim().length<=0 || parseInt(sku_num)<=0) { $(this).val(pre_sku_count); return; } sku_num = parseInt(sku_num); var num_dom = $(this); updateRemoteCartInfo(sku_id, sku_num, num_dom); }); // 刪除 $(".del_btn").click(function(){ var sku_id = $(this).parents("ul").attr("sku_id"); var req = { sku_id: sku_id, csrfmiddlewaretoken: "{{ csrf_token }}" }; $.post('/cart/delete', req, function(data){ // window.reload() location.href="/cart/"; // 刪除后,刷新頁面 }); }); // 商品對應checkbox發生改變時,全選checkbox發生改變 $('.cart_list_td').find(':checkbox').change(function () { // 獲取商品所有checkbox的數目 var all_len = $('.cart_list_td').find(':checkbox').length; // 獲取選中商品的checkbox的數目 var checked_len = $('.cart_list_td').find(':checked').length; if (checked_len < all_len){ // 有商品沒有被選中 $('.settlements').find(':checkbox').prop('checked', false) } else{ // 所有商品都被選中 $('.settlements').find(':checkbox').prop('checked', true) } freshOrderCommitInfo(); }); // 全選和全不選 $('.settlements').find(':checkbox').change(function () { // 1.獲取當前checkbox的選中狀態 var is_checked = $(this).prop('checked'); // 2.遍歷並設置商品ul中checkbox的選中狀態 $('.cart_list_td').find(':checkbox').each(function () { // 設置每一個goods ul中checkbox的值 $(this).prop('checked', is_checked) }); freshOrderCommitInfo(); }); </script> {% endblock %}
更新購物車數量視圖
主要邏輯:
- 更新哪條購物車記錄,商品數量更新為多少
- 如果用戶登陸,更新redis中的購物車記錄
- 如果用戶未登陸,更新cookie中的購物車記錄
准備工作
# 更新購物車數據 url(r'^update$', views.UpdateCartView.as_view(), name='update'),
class UpdateCartView(View): """更新購物車數據:+ -""" def post(self,request): # 獲取參數:sku_id, count # 校驗參數all() # 判斷商品是否存在 # 判斷count是否是整數 # 判斷庫存 # 判斷用戶是否登陸 # 如果用戶登陸,將修改的購物車數據存儲到redis中 # 如果用戶未登陸,將修改的購物車數據存儲到cookie中 # 響應結果 pass
更新購物車數量實現
class UpdateCartView(View): """更新購物車數據:+ - 編輯""" def post(self, request): # 獲取參數:sku_id, count sku_id = request.POST.get('sku_di') count = request.POST.get('count') # 校驗參數all() if not all([sku_id, count]): return JsonResponse({'code': 1, 'message': '參數不完整'}) # 判斷商品是否存在 try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: return JsonResponse({'code': 2, 'message': '商品不存在'}) # 判斷count是否是整數 try: count = int(count) except Exception: return JsonResponse({'code': 3, 'message': '數量有誤'}) # 判斷庫存 if count > sku.stock: return JsonResponse({'code': 4, 'message': '庫存不足'}) # 判斷用戶是否登陸 if request.user.is_authenticated(): # 如果用戶登陸,將修改的購物車數據存儲到redis中 redis_conn = get_redis_connection('default') user_id = request.user.id # 如果設計成冪等的,count就是最終要保存的商品的數量,不需要累加 redis_conn.hset('cart_%s'%user_id, sku_id, count) return JsonResponse({'code': 0, 'message': '添加購物車成功'}) else: # 如果用戶未登陸,將修改的購物車數據存儲到cookie中 # 獲取cookie中的購物車的json字符串 cart_json = request.COOKIES.get('cart') # 如果json字符串存在,將json字符串轉成字典,因為用戶可能從來沒有添加過購物車 if cart_json is not None: cart_dict = json.loads(cart_json) else: cart_dict = {} # 如果設計成冪等的,count就是最終要保存的商品的數量,不需要累加 cart_dict[sku_id] = count # 將購物車字典轉成json字符串格式 new_cart_json = json.dumps(cart_dict) # 響應結果 response = JsonResponse({'code': 0, 'message': '添加購物車成功'}) # 寫入cookie response.set_cookie('cart', new_cart_json) return response
刪除購物車記錄視圖
主要邏輯
- 刪除哪條購物車記錄
- 如果用戶登陸,刪除redis中的購物車記錄
- 如果用戶未登陸,刪除cookie中的購物車記錄
准備工作
# 刪除購物車數據 url(r'^delete$', views.DeleteCartView.as_view(), name='delete')
class DeleteCartView(View): """刪除購物車數據""" def post(self, request): # 接收參數:sku_id # 校驗參數:not,判斷是否為空 # 判斷用戶是否登錄 # 如果用戶登陸,刪除redis中購物車數據 # 如果用戶未登陸,刪除cookie中購物車數據 pass
刪除購物車記錄實現
class DeleteCartView(View): """刪除購物車數據""" def post(self, request): # 接收參數:sku_id sku_id = request.POST.get('sku_id') # 校驗參數:not,判斷是否為空 if not sku_id: return JsonResponse({'code': 1, 'message': '參數錯誤'}) # 判斷用戶是否登錄 if request.user.is_authenticated(): # 如果用戶登陸,刪除redis中購物車數據 redis_conn= get_redis_connection('default') user_id = request.user.id # 商品不存在會直接忽略 redis_conn.hdel('cart_%s'%user_id, sku_id) else: # 如果用戶未登陸,刪除cookie中購物車數據 cart_json = request.COOKIES.get('cart') if cart_json is not None: cart_dict = json.loads(cart_json) # 判斷要刪除的商品是否存在 if sku_id in cart_dict: # 字典刪除key對應的value del cart_dict[sku_id] # 響應中重新寫入cookie response = JsonResponse({'code': 0, 'message': '刪除成功'}) response.set_cookie('cart', json.dumps(cart_dict)) return response # 當刪除成功或者沒有要刪除的都提示用戶成功 return JsonResponse({'code': 0, 'message': '刪除成功'})
訂單
訂單生成流程界面
頁面入口
- 點擊《詳情頁》的《立即購買》可以進入該《訂單確認頁面》
- 點擊《購物車》的《去結算》可以進入該《訂單確認頁面》
- 點擊《訂單確認頁面》的《提交訂單》可以進入《全部訂單》
- 點擊《全部訂單》的《去付款》可以進入《支付寶》
訂單確認頁面分析
頁面入口
- 點擊《詳情頁》的《立即購買》可以進入該《訂單確認頁面》
- 點擊《購物車》的《去結算》可以進入該《訂單確認頁面》
說明
- 訂單確認頁面的生成是由用戶發送商品數據給服務器,服務器渲染后再響應給客戶端而生成的
- 請求方法:POST
- 只有當用戶登陸后,才能訪問該《訂單確認頁面》,
LoginRequiredMixin
- 提示:進入訂單確認頁面,沒有使用ajax交互
- 如果前后端是通過json交互的,不能使用
LoginRequiredMixin
- 因為
LoginRequiredMixin
不響應json數據,而是響應302的重定向
- 參數說明:
- 地址:通過
request.user
獲取關聯的Address - 支付方式:頁面內部選擇
- 商品id:請求參數,傳入sku_id
- 一次傳入的sku_id有可能有多個,所以設計成
sku_ids=sku_1&sku_ids=sku_2
- 如果從《購物車》進入《訂單確認頁面》,sku_ids設計成一鍵多值的情況
- 如果從《詳情頁》進入《訂單確認頁面》,sku_ids設計成一鍵一值的情況
- 獲取方式:
sku_ids = request.POST.getlist('sku_ids')
- 一次傳入的sku_id有可能有多個,所以設計成
- 商品數量:count
- 如果從《購物車》進入《訂單確認頁面》,count在redis數據庫中,不需要傳遞
- 如果從《詳情頁》進入《訂單確認頁面》,count在POST請求參數中,需要傳遞
- 可以通過count是否存在,判斷用戶是從《購物車》進入《訂單確認頁面》還是從《詳情頁》進入的
- 地址:通過
准備工作
# 確認訂單 url(r'^place$', views.PlaceOrdereView.as_view(), name='place')
class PlaceOrdereView(LoginRequiredMixin, View): """訂單確認頁面""" def post(self, request): # 判斷用戶是否登陸:LoginRequiredMixin # 獲取參數:sku_ids, count # 校驗sku_ids參數:not # 校驗count參數:用於區分用戶從哪兒進入訂單確認頁面 # 如果是從購物車頁面過來 # 查詢商品數據 # 商品的數量從redis中獲取 # 如果是從詳情頁面過來 # 查詢商品數據 # 商品的數量從request中獲取,並try校驗 # 判斷庫存:立即購買沒有判斷庫存 # 查詢用戶地址信息 # 構造上下文 # 響應結果:html頁面 pass
模板調整:去結算
和立即購買
去結算模板調整
立即購買模板調整
訂單確認頁面后端視圖編寫
接收參數
class PlaceOrdereView(LoginRequiredMixin, View): """訂單確認頁面""" def post(self, request): # 判斷用戶是否登陸:LoginRequiredMixin # 獲取參數:sku_ids, count sku_ids = request.POST.getlist('sku_ids') # 用戶從詳情過來時,才有count count = request.POST.get('count') pass
校驗參數
class PlaceOrdereView(LoginRequiredMixin, View): """訂單確認頁面""" def post(self, request): # 判斷用戶是否登陸:LoginRequiredMixin # 獲取參數:sku_ids, count sku_ids = request.POST.getlist('sku_ids') # 用戶從詳情過來時,才有count count = request.POST.get('count') # 校驗sku_ids參數 if not sku_ids: # 如果sku_ids沒有,就重定向購物車,重選 return redirect(reverse('cart:info')) # 查詢商品數據 if count is None: # 如果是從購物車頁面過來,商品的數量從redis中獲取 # 遍歷商品sku_ids else: # 如果是從詳情頁面過來,商品的數量從request中獲取 # 遍歷商品sku_ids pass
查詢商品和計算金額
- 提示:hgetall()取得的redis中的key是字節類型的
class PlaceOrdereView(LoginRequiredMixin, View): """訂單確認頁面""" def post(self, request): # 判斷用戶是否登陸:LoginRequiredMixin # 獲取參數:sku_ids, count sku_ids = request.POST.getlist('sku_ids') # 用戶從詳情過來時,才有count count = request.POST.get('count') # 校驗參數 if not sku_ids: # 如果sku_ids沒有,就重定向到購物車,重選 return redirect(reverse('cart:info')) # 定義臨時容器 skus = [] total_count = 0 total_sku_amount = 0 trans_cost = 10 total_amount = 0 # 實付款 # 查詢商品數據 if count is None: # 如果是從購物車頁面過來,商品的數量從redis中獲取 redis_conn = get_redis_connection('default') user_id = request.user.id cart_dict = redis_conn.hgetall('cart_%s'%user_id) # 遍歷商品sku_ids for sku_id in sku_ids: try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 重定向到購物車 return redirect(reverse('cart:info')) # 取出每個sku_id對應的商品數量 sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) # 計算商品總金額 amount = sku.price * sku_count # 將商品數量和金額封裝到sku對象 sku.count = sku_count sku.amount = amount skus.append(sku) # 金額和數量求和 total_count += sku_count total_sku_amount += amount else: # 如果是從詳情頁面過來,商品的數量從request中獲取 # 遍歷商品sku_ids:如果是從詳情過來,sku_ids只有一個sku_id for sku_id in sku_ids: try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 重定向到購物車 return redirect(reverse('cart:info')) # 獲取request中得到的count try: sku_count = int(count) except Exception: return redirect(reverse('goods:detail', args=sku_id)) # 判斷庫存:立即購買沒有判斷庫存 if sku_count > sku.stock: return redirect(reverse('goods:detail', args=sku_id)) # 計算商品總金額 amount = sku.price * sku_count # 將商品數量和金額封裝到sku對象 sku.count = sku_count sku.amount = amount skus.append(sku) # 金額和數量求和 total_count += sku_count total_sku_amount += amount # 實付款 total_amount = total_sku_amount + trans_cost # 用戶地址信息 try: address = Address.objects.filter(user=request.user).latest('create_time') except Address.DoesNotExist: address = None # 模板會做判斷,然后跳轉到地址編輯頁面 # 構造上下文 context = { 'skus':skus, 'total_count':total_count, 'total_sku_amount':total_sku_amount, 'trans_cost':trans_cost, 'total_amount':total_amount, 'address':address } # 響應結果:html頁面 return render(request, 'place_order.html', context)
注意
- 當用戶是未登錄時,嘗試訪問這個視圖時,會被
LoginRequiredMixin
引導到登陸界面 - 但是,登陸結束后,該裝飾器使用的是重定向
next參數
,將用戶引導回來 - 問題:
PlaceOrdereView
支持POST
請求,但是next參數
重定向回來的是GET
請求- 錯誤碼
405
- 解決:
- 如果是訪問訂單確認頁面,登陸成功后重定向到購物車頁面
if next is None: response = redirect(reverse('goods:index')) else: if next == '/orders/place': response = redirect('/cart') else: response = redirect(next)
提交訂單分析
- 請求方法:POST
- 訂單提交時,可能成功,可能失敗
- 在訂單提交頁面,我們沒有設計過渡頁面,我們設計的是前后端通過ajax的json溝通的
- 提交訂單頁面需要登錄用戶才能訪問
- 所以我們又需要驗證用戶是否登陸
- 由於使用ajax的json在前后端交互,所以LoginRequiredMixin,無法滿足需求
- 而且類似ajax的json實現前后端交互,且需要用戶驗證的需求很多
- 我們可以自定義一個裝飾器,實現用戶驗證並能與ajax的json交互
- 后端的
CommitOrderView
視圖,主要邏輯是保存訂單信息,並把成功、錯誤通過json傳給前端頁面
准備工作
# 訂單提交 url(r'^commit$', views.CommitOrderView.as_view(), name='commit')
class CommitOrderView(View): """訂單提交""" def post(self, request): pass
自定義返回json的登陸驗證裝飾器
自定義裝飾器
from functools import wraps def login_required_json(view_func): # 恢復view_func的名字和文檔 @wraps(view_func) def wrapper(request, *args, **kwargs): # 如果用戶未登錄,返回json數據 if not request.user.is_authenticated(): return JsonResponse({'code': 1, 'message': '用戶未登錄'}) else: # 如果用戶登陸,進入到view_func中 return view_func(request, *args, **kwargs) return wrapper
封裝自定義裝飾器的拓展類
class LoginRequiredJSONMixin(object): @classmethod def as_view(cls, **initkwargs): view = super().as_view(**initkwargs) return login_required_json(view)
使用
class CommitOrderView(LoginRequiredJSONMixin, View): """訂單提交""" def post(self, request): pass
提交訂單后端視圖編寫
主要邏輯
- 獲取訂單確認頁面傳入的數據,查詢商品信息
- 在下訂單以前,要明確訂單相關的兩張表:商品訂單表和訂單商品表
- 商品訂單表和訂單商品表是一對多的關系
- 一條商品訂單記錄可以對應多條訂單商品記錄
參數說明
- 可以根據界面需要的數據分析
- 也可以根據訂單相關數據庫需要保存的數據分析(參考訂單的模型類)
-
參數:
- 用戶信息:user
- 地址信息:address_id(確認訂單時的地址的id)
- 支付方式:pay_method
- 商品id:sku_ids (sku_ids = '1,2,3'),不是表單,無法做成一鍵多值
- 商品數量:count
-
商品數量參數的思考
- 點擊立即購買將商品加入到購物車
- 當從《詳情頁》進入《訂單確認頁面》時,為了提交訂單時,不用把商品數量當做請求參數
-
優化邏輯:點擊立即購買將商品加入到購物車
需要處理的邏輯
class CommitOrderView(View): """訂單提交""" def post(self, request): # 獲取參數:user,address_id,pay_method,sku_ids,count # 校驗參數:all([address_id, pay_method, sku_ids]) # 判斷地址 # 判斷支付方式 # 截取出sku_ids列表 # 遍歷sku_ids # 循環取出sku,判斷商品是否存在 # 獲取商品數量,判斷庫存 (redis) # 減少sku庫存 # 增加sku銷量 # 保存訂單商品數據OrderGoods(能執行到這里說明無異常) # 先創建商品訂單信息 # 計算總數和總金額 # 修改訂單信息里面的總數和總金額(OrderInfo) # 訂單生成后刪除購物車(hdel) # 響應結果 pass
timezone
# django提供的時間格式化工具 from django.utils import timezone # python提供的時間格式化工具 datetime 和 time # 相關方法 strftime : 將時間轉字符串 strptime : 將字符串轉時間 # 使用:20171222031955 timezone.now().strftime('%Y%m%d%H%M%S')
需要處理的邏輯實現
class CommitOrderView(LoginRequiredJSONMixin, View): """提交訂單""" def post(self, request): # 獲取參數:user,address_id,pay_method,sku_ids,count user = request.user address_id = request.POST.get('address_id') pay_method = request.POST.get('pay_method') sku_ids = request.POST.get('sku_ids') # 校驗參數:all([address_id, sku_ids, pay_method]) if not all([address_id, sku_ids, pay_method]): return JsonResponse({'code': 2, 'message': '缺少參數'}) # 判斷地址 try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'code': 3, 'message': '地址不存在'}) # 判斷支付方式 if pay_method not in OrderInfo.PAY_METHOD: return JsonResponse({'code': 4, 'message': '支付方式錯誤'}) # 創建redis鏈接對象,取出字典 redis_conn = get_redis_connection('default') cart_dict = redis_conn.hgetall('cart_%s'%user.id) # 判斷商品是否存在:跟前端約定,sku_ids='1,2,3' sku_ids = sku_ids.split(',') # 定義臨時容器 total_count = 0 total_amount = 0 # 手動生成order_id order_id = timezone.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 在創建訂單商品信息前,創建商品訂單信息,(商品訂單和訂單商品時一對多的關系) order = OrderInfo.objects.create( order_id = order_id, user = user, address = address, total_amount = 0, trans_cost = 10, pay_method = pay_method ) # 遍歷sku_ids, for sku_id in sku_ids: # 循環取出sku try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: return JsonResponse({'code': 5, 'message': '商品不存在'}) # 獲取商品數量,判斷庫存 (redis) sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: return JsonResponse({'code': 6, 'message': '庫存不足'}) # 減少sku庫存 sku.stock -= sku_count # 增加sku銷量 sku.sales += sku_count sku.save() # 保存訂單商品數據OrderGoods(能執行到這里說明無異常) OrderGoods.objects.create( order = order, sku = sku, count = sku_count, price = sku.price ) # 計算總數和總金額 total_count += sku_count total_amount += (sku_count * sku.price) # 修改訂單信息里面的總數和總金額(OrderInfo) order.total_count = total_count order.total_amount = total_amount + 10 order.save() # 訂單生成后刪除購物車(hdel) redis_conn.hdel('cart_%s'%user.id, *sku_ids) # 響應結果 return JsonResponse({'code': 0, 'message': '下單成功'})
提交訂單之事務支持
提交訂單需求
- Django中的數據庫,默認是自動提交的
- 當
OrderInfo
和OrderGoods
保存數據時,如果出現異常,需要執行回滾,不要自動提交 - 保存數據時,只有當沒有任何錯誤時,才能完成數據的保存
- 要么一起成功,要么一起失敗
Django數據庫事務
atomic裝飾器
from django.db import transaction class TransactionAtomicMixin(object): """提供數據庫事務功能""" @classmethod def as_view(cls, **initkwargs): view = super(TransactionAtomicMixin, cls).as_view(**initkwargs) return transaction.atomic(view)
事務實現
- 在操作數據庫前創建事務保存點
- 出現異常的地方都回滾到事務保存點
- 暴力回滾,將大范圍的數據庫操作整體捕獲異常
-
當數據庫操作結束,還沒有異常時,才能提交事務
class CommitOrderView(LoginRequiredJSONMixin, TransactionAtomicMixin, View): """訂單提交""" def post(self, request): # 獲取參數:user,address_id,pay_method,sku_ids,count user = request.user address_id = request.POST.get('address_id') sku_ids = request.POST.get('sku_ids') # '1,2,3' pay_method = request.POST.get('pay_method') # 校驗參數 if not all([address_id, sku_ids, pay_method]): return JsonResponse({'code': 2, 'message': '缺少參數'}) # 判斷地址 try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'code': 3, 'message': '地址不存在'}) # 判斷支付方式 if pay_method not in OrderInfo.PAY_METHOD: return JsonResponse({'code': 4, 'message': '支付方式錯誤'}) # 創建redis連接對象 redis_conn = get_redis_connection('default') cart_dict = redis_conn.hgetall('cart_%s' % user.id) # 創建訂單id:時間+user_id order_id = timezone.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 在操作數據庫前創建事務保存點 save_point = transaction.savepoint() try: # 先創建商品訂單信息 order = OrderInfo.objects.create( order_id = order_id, user = user, address = address, total_amount = 0, trans_cost = 10, pay_method = pay_method, ) # 判斷商品是否存在 sku_ids = sku_ids.split(',') # 定義臨時容器 total_count = 0 total_amount = 0 # 遍歷sku_ids,循環取出sku for sku_id in sku_ids: try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 5, 'message': '商品不存在'}) # 獲取商品數量,判斷庫存 sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 6, 'message': '庫存不足'}) # 減少庫存 sku.stock -= sku_count # 增加銷量 sku.sales += sku_count sku.save() # 保存訂單商品數據 OrderGoods.objects.create( order = order, sku = sku, count = sku_count, price = sku.price, ) # 計算總數和總金額 total_count += sku_count total_amount += (sku.price * sku_count) # 修改訂單信息里面的總數和總金額 order.total_count = total_count order.total_amount = total_amount + 10 order.save() except Exception: # 出現任何異常都回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 7, 'message': '下單失敗'}) # 沒有異常,就手動提交 transaction.savepoint_commit(save_point) # 訂單生成后刪除購物車 redis_conn.hdel('cart_%s' % user.id, *sku_ids) # 響應結果 return JsonResponse({'code': 0, 'message': '訂單創建成功'})
提交訂單之並發和鎖
提示
- 問題:多線程和多進程訪問共享資源時,容易出現資源搶奪的問題
- 解決:加鎖 (悲觀鎖+樂觀鎖)
- 悲觀鎖:
- 當要操作某條記錄時,立即將該條記錄鎖起來,誰也無法操作,直到它操作完
- select * from table where id=1 for update;
- 樂觀鎖:
- 在查詢數據的時候不加鎖,在更新時進行判斷
- 判斷更新時的庫存和之前,查出的庫存是否一致
- update table set stock=2 where id=1 and stock=7;
樂觀鎖控制提交訂單
-
沒有使用鎖
# 減少sku庫存 sku.stock -= sku_count # 增加sku銷量 sku.sales += sku_count sku.save()
使用樂觀鎖
# 減少庫存,增加銷量 origin_stock = sku.stock new_stock = origin_stock - sku_count new_sales = sku.sales + sku_count # 更新庫存和銷量 result = GoodsSKU.objects.filter(id=sku_id,stock=origin_stock).update(stock=new_stock,sales=new_sales) if 0 == result: # 異常,回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 8, 'message': '下單失敗'})
樂觀鎖控制提交訂單整體代碼
# 每個訂單三次下單機會 for i in range(3): pass
class CommitOrderView(LoginRequiredJSONMixin, TransactionAtomicMixin, View): """訂單提交""" def post(self, request): # 獲取參數:user,address_id,pay_method,sku_ids,count user = request.user address_id = request.POST.get('address_id') sku_ids = request.POST.get('sku_ids') # '1,2,3' pay_method = request.POST.get('pay_method') # 校驗參數 if not all([address_id, sku_ids, pay_method]): return JsonResponse({'code': 2, 'message': '缺少參數'}) # 判斷地址 try: address = Address.objects.get(id=address_id) except Address.DoesNotExist: return JsonResponse({'code': 3, 'message': '地址不存在'}) # 判斷支付方式 if pay_method not in OrderInfo.PAY_METHOD: return JsonResponse({'code': 4, 'message': '支付方式錯誤'}) # 創建redis連接對象 redis_conn = get_redis_connection('default') cart_dict = redis_conn.hgetall('cart_%s' % user.id) # 創建訂單id:時間+user_id order_id = timezone.now().strftime('%Y%m%d%H%M%S')+str(user.id) # 在操作數據庫前創建事務保存點 save_point = transaction.savepoint() try: # 先創建商品訂單信息 order = OrderInfo.objects.create( order_id = order_id, user = user, address = address, total_amount = 0, trans_cost = 10, pay_method = pay_method, ) # 判斷商品是否存在 sku_ids = sku_ids.split(',') # 定義臨時容器 total_count = 0 total_amount = 0 # 遍歷sku_ids,循環取出sku for sku_id in sku_ids: for i in range(3): try: sku = GoodsSKU.objects.get(id=sku_id) except GoodsSKU.DoesNotExist: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 5, 'message': '商品不存在'}) # 獲取商品數量,判斷庫存 sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 6, 'message': '庫存不足'}) # 減少庫存,增加銷量 origin_stock = sku.stock new_stock = origin_stock - sku_count new_sales = sku.sales + sku_count # 更新庫存和銷量 result = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock,sales=new_sales) if 0 == result and i < 2 : continue # 還有機會,繼續重新下單 elif 0 == result and i == 2: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 8, 'message': '下單失敗'}) # 保存訂單商品數據 OrderGoods.objects.create( order = order, sku = sku, count = sku_count, price = sku.price, ) # 計算總數和總金額 total_count += sku_count total_amount += (sku.price * sku_count) # 下單成功,跳出循環 break # 修改訂單信息里面的總數和總金額 order.total_count = total_count order.total_amount = total_amount + 10 order.save() except Exception: # 出現任何異常都回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 7, 'message': '下單失敗'}) # 沒有異常,就手動提交 transaction.savepoint_commit(save_point) # 訂單生成后刪除購物車 redis_conn.hdel('cart_%s' % user.id, *sku_ids) # 響應結果 return JsonResponse({'code': 0, 'message': '訂單創建成功'})
提交訂單之測試事務並發和鎖
首先處理提交訂單的ajax請求
- 處理提交訂單的ajax請求,前端的代碼在
place_order.html
當中 - post請求地址:
'/orders/commit'
補充sku_ids到模板中
- 提交訂單的post請求,需要一個sku_ids數據
- 我們可以在渲染這個
place_order.html
模板時,在上下文中傳入sku_ids - 傳入的sku_ids,設計成以
,
隔開的字符串
# 構造上下文 context = { 'skus':skus, 'total_count':total_count, 'total_sku_amount':total_sku_amount, 'trans_cost':trans_cost, 'total_amount':total_amount, 'address':address, 'sku_ids':','.join(sku_ids) }
提交訂單之事務和並發和鎖的測試
- 在更新銷量和庫存前增加時間延遲
- 在兩個瀏覽器中輸入兩個賬號,分別前后下單,觀察后下單的用戶是否下單成功
# 獲取商品數量,判斷庫存 (redis) sku_count = cart_dict.get(sku_id.encode()) sku_count = int(sku_count) if sku_count > sku.stock: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 6, 'message': '庫存不足'}) import time time.sleep(10) origin_stock = sku.stock new_stock = origin_stock - sku_count new_sales = sku.sales + sku_count result = GoodsSKU.objects.filter(id=sku_id, stock=origin_stock).update(stock=new_stock, sales=new_sales) if 0 == result and i < 2: continue elif 0 == result and i == 2: # 回滾 transaction.savepoint_rollback(save_point) return JsonResponse({'code': 8, 'message': '庫存不足'}) # 保存訂單商品數據OrderGoods(能執行到這里說明無異常) OrderGoods.objects.create( order=order, sku=sku, count=sku_count, price=sku.price )
補充
- 如果事務和並發和鎖的測試失敗
- 嘗試修改事務隔離級別
- 修改之后重啟mysql數據庫
- 事務隔離級別相關文檔
我的訂單代碼說明
- 主要邏輯
- 訂單數據的查詢
- 訂單數據渲染到模板
視圖
# 訂單信息頁面 url(r'^(?P<page>\d+)$', views.UserOrdersView.as_view(), name='info')
class UserOrdersView(LoginRequiredMixin, View): """用戶訂單頁面""" def get(self, request, page): """提供訂單信息頁面""" user = request.user # 查詢所有訂單 orders = user.orderinfo_set.all().order_by("-create_time") # 遍歷所有訂單 for order in orders: # 給訂單動態綁定:訂單狀態 order.status_name = OrderInfo.ORDER_STATUS[order.status] # 給訂單動態綁定:支付方式 order.pay_method_name = OrderInfo.PAY_METHODS[order.pay_method] order.skus = [] # 查詢訂單中所有商品 order_skus = order.ordergoods_set.all() # 遍歷訂單中所有商品 for order_sku in order_skus: sku = order_sku.sku sku.count = order_sku.count sku.amount = sku.price * sku.count order.skus.append(sku) # 分頁 page = int(page) try: paginator = Paginator(orders, 2) page_orders = paginator.page(page) except EmptyPage: # 如果傳入的頁數不存在,就默認給第1頁 page_orders = paginator.page(1) page = 1 # 頁數 page_list = paginator.page_range context = { "orders": page_orders, "page": page, "page_list": page_list, } return render(request, "user_center_order.html", context)