设计
购物网站应提供用户已登录或未登录状态下都能添加商品到购物车的功能,所以两种状态要采用两种不同的实现方式。
-
登录状态:可根据用户id保存用户勾选的商品,考虑到购物车数据量小,且更新频繁,所以采用redis来存储。
redis提供的数据类型有:string hash list
语法格式为:
carts_user_id:{sku_id:count1,sku_id2:count,...}
selected_user_id:[sku_id1,sku_id2,...]
- 未登录状态:无法获取用户id,所以将用户勾选商品存放在浏览器的Cookie中。购物数据为隐私数据,存放时要对数据加密,用pickle库可以实现数据的序列化,base64可对序列化后的数据进行编码(加密)。
cart_dict = {
'sku_id1': {'count': 5, 'selected': 'True'},
'sku_id2': {'count': 3, 'selected': 'False'}
...
}
实现
购物车视图类Carts.py,post请求用于提交购物车数据,get请求用于查询购物车数据。
import base64
import json
import pickle
from django.conf import settings
from django.http import HttpResponseForbidden, JsonResponse
from django.shortcuts import render
from django.views import View
from django_redis import get_redis_connection
from goods.models import SKU
from xiaoyu_mall_new.utils.response_code import RETCODE
class CartsView(View):
def get(self, request):
# 判断用户是否登录
user = request.user
if user.is_authenticated:
# 创建连接到redis的对象
redis_conn = get_redis_connection('carts')
# 查询user_id、count与sku_id构成的购物车记录
redis_cart = redis_conn.hgetall('carts_%s' % user.id)
# 查询勾选的商品smembers 命令返回集合中的所有的成员
redis_selected = redis_conn.smembers('selected_%s' % user.id)
cart_dict = {}
for sku_id, count in redis_cart.items():
cart_dict[int(sku_id)] = {
"count": int(count),
"selected": sku_id in redis_selected
}
else:
# 用户未登录,查询cookies购物车
cart_str = request.COOKIES.get('carts')
if cart_str:
# 对 cart_str进行编码,获取字节类型的数据
cart_str_bytes = cart_str.encode()
# 对cart_str_bytes进行解码,获取明文数据
cart_dict_bytes = base64.b64decode(cart_str_bytes)
# 对cart_dict_bytes反序列化,转换成Python能识别的字典类型的数据
cart_dict = pickle.loads(cart_dict_bytes)
else:
cart_dict = {}
# 构造响应数据
sku_ids = cart_dict.keys()
# 一次性查询出所有的skus
skus = SKU.objects.filter(id__in=sku_ids)
cart_skus = []
for sku in skus:
cart_skus.append({
'id': sku.id,
'count': cart_dict.get(sku.id).get('count'),
# 将True,转'True',方便json解析
'selected': str(cart_dict.get(sku.id).get('selected')),
'name': sku.name,
'default_image_url': settings.STATIC_URL +
'images/goods/' + sku.default_image.url + '.jpg',
'price': str(sku.price),
'amount': str(sku.price * cart_dict.get(sku.id).get('count')),
'stock': sku.stock
})
context = {
'cart_skus': cart_skus
}
# 渲染购物车页面
return render(request, 'cart.html', context)
def post(self, request):
# 将JSON格式的字符串反序列化为Python对象
json_dict = json.loads(request.body.decode())
# 接收参数
sku_id = json_dict.get('sku_id')
count = json_dict.get('count')
selected = json_dict.get('selected', True)
# 校验参数
if not all([sku_id, count]):
return HttpResponseForbidden('缺少必要参数')
# 校验sku_id参数
try:
SKU.objects.get(id=sku_id)
except SKU.DoesNotExist:
return HttpResponseForbidden('参数sku_id错误')
# 校验count参数
try:
count = int(count)
except Exception:
return HttpResponseForbidden('参数count错误')
# 校验selected参数
if selected:
if not isinstance(selected, bool):
return HttpResponseForbidden('参数selected错误')
# 判断用户是否登录
user = request.user
# 已登录,数据格式为:
# carts_user_id:{sku_id1:count1,sku_id2:count2,...}
# selected_user_id:{sku_id1,sku_id2,...}
if user.is_authenticated:
redis_conn = get_redis_connection('carts')
# 创建管道,用于执行多个命令
pl = redis_conn.pipeline()
# 以增量形式保存商品数据
# - 'carts_%s' % user.id :生成以用户ID为后缀的购物车键名(如 carts_123 )
# - sku_id :商品SKU的唯一标识,作为哈希表的字段名
# - count :要增加的数量(可为正数或负数)
pl.hincrby('carts_%s' % user.id, sku_id, count)
# 保存商品的勾选状态
if selected:
# 若selected为True,将sku_id添加到selected集合中
pl.sadd('selected_%s' % user.id, sku_id)
# 执行管道命令
pl.execute()
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
else: # 未登录,购物车数据存储在cookie中,数据结构如下
# cart_dict = {
# 'sku_id1': {'count': 5, 'selected': 'True'},
# 'sku_id2': {'count': 3, 'selected': 'False'}
# ...
# }
cart_str = request.COOKIES.get('carts')
# 若购物车数据存在,将其反序列化为字典
if cart_str:
# 对cart_str进行编码,获取字节类型数据
cart_str_bytes = cart_str.encode()
# 对密文类型数据cart_str_bytes进行base64解码,获取明文数据
cart_dict_bytes = base64.b64decode(cart_str_bytes)
# 对明文类型数据cart_dict_bytes进行反序列化,获取字典类型数据
cart_dict = pickle.loads(cart_dict_bytes)
# 若没有数据,创建空字典
else:
cart_dict = {}
# 若购物车数据中已存在该商品,累加数量
if sku_id in cart_dict:
origin_count = cart_dict[sku_id]['count']
count += origin_count
cart_dict[sku_id] = {'count': count, 'selected': selected}
# 对字典类型数据cart_dict进行序列化,获取字节类型数据
cart_dict_bytes = pickle.dumps(cart_dict)
# 对字节类型数据cart_dict_bytes进行base64编码,获取密文类型数据
cart_str_bytes = base64.b64encode(cart_dict_bytes)
# 对密文类型数据cart_str_bytes进行解码,获取明文类型数据
cookie_cart_str = cart_str_bytes.decode()
response = JsonResponse({'code': RETCODE.OK, 'errmsg': 'OK'})
response.set_cookie('carts', cookie_cart_str)
# 响应结果
return response
前端页面cart.html
{#<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">#}
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>小鱼商城-购物车</title>
<link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
<script type="text/javascript" src="{{ static('js/vue-2.5.16.js') }}"></script>
<script type="text/javascript" src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div id="app">
<div class="header_con">
<div class="header" v-cloak>
<div class="welcome fl">欢迎来到小鱼商城!</div>
<div class="fr">
<div v-if="username" class="login_btn fl">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="{{ url('users:logout') }}">退出</a>
</div>
<div v-else class="login_btn fl">
<a href="{{ url('users:login') }}">登录</a>
<span>|</span>
<a href="{{ url('users:register') }}">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:info') }}">用户中心</a>
<span>|</span>
<a href="{{ url('carts:info') }}">我的购物车</a>
<span>|</span>
{# <a href="{{ url('users:myorderinfo',args=(1,)) }}">我的订单</a>#}
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="{{ url('contents:index') }}" class="logo fl"><img src="{{ static('images/logo.png') }}"></a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" name="" value="搜索">
</form>
<ul class="search_suggest fl">
<li><a href="#">索尼微单</a></li>
<li><a href="#">优惠15元</a></li>
<li><a href="#">美妆个护</a></li>
<li><a href="#">买2免1</a></li>
</ul>
</div>
</div>
<div class="total_count">全部商品<em>[[ total_count ]]</em>件</div>
<ul class="cart_list_th clearfix">
<li class="col01">商品名称</li>
<li class="col03">商品价格</li>
<li class="col04">数量</li>
<li class="col05">小计</li>
<li class="col06">操作</li>
</ul>
<ul class="cart_list_td clearfix" v-for="(cart_sku,index) in carts" v-cloak>
<li class="col01"><input type="checkbox" name="" v-model="cart_sku.selected" @change="update_selected(index)"></li>
<li class="col02"><img :src="cart_sku.default_image_url"></li>
<li class="col03">[[ cart_sku.name ]]</li>
<li class="col05">[[ cart_sku.price ]]元</li>
<li class="col06">
<div class="num_add">
<a @click="on_minus(index)" class="minus fl">-</a>
<input v-model="cart_sku.count" @blur="on_input(index)" type="text" class="num_show fl">
<a @click="on_add(index)" class="add fl">+</a>
</div>
</li>
<li class="col07">[[ cart_sku.amount ]]元</li>
<li class="col08"><a @click="on_delete(index)">删除</a></li>
</ul>
<ul class="settlements" v-cloak>
<li class="col01"><input type="checkbox" name="" @change="on_selected_all" v-model="selected_all"></li>
<li class="col02">全选</li>
<li class="col03">合计(不含运费):<span>¥</span><em>[[ total_selected_amount ]]</em><br>共计<b>[[ total_selected_count ]]</b>件商品</li>
{# <li class="col04"><a href="{{ url('orders:settlement') }}">去结算</a></li>#}
</ul>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2024 北京小鱼商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script type="text/javascript">
let carts = {{ cart_skus | safe }};
</script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/cart.js') }}"></script>
</body>
</html>
配置路由carts应用下urls.py
from django.urls import path
from . import views
# 设置应用程序命名空间
app_name = 'carts'
urlpatterns = [
path("carts/", views.CartsView.as_view(), name='info'),
]
配置根路由,项目下urls.py下增加如下配置模式
path('', include('carts.urls'), name='carts')
购物车添加与查询功能设计与实现
1871

被折叠的 条评论
为什么被折叠?



