开源web框架django知识总结(十九)
阿尔法商城(购物车)
购物车存储方案
新建apps->carts
- 必须是用户登录状态下,才可以保存购物车数据。
- 用户对购物车数据的操作包括:增、删、改、查、全选等等
- 每个用户的购物车数据都要做唯一性的标识。
1. 购物车存储方案
1.存储数据说明
- 如何描述一条完整的购物车记录?
- 用户,选择了两个 iPhone8 添加到了购物车中,状态为勾选
- 一条完整的购物车记录包括:
用户
、商品
、数量
、勾选状态
。 - 存储数据:user_id、sku_id、count、selected
2.存储位置说明
- 购物车数据量小,结构简单,更新频繁,所以我们选择内存型数据库Redis进行存储。
- 存储位置:dev.py文件中Redis数据库 5号库
"carts": { # 购物车
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://192.168.42.128:6379/5",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
3.存储类型说明
- 提示:我们很难将用户、商品、数量、勾选状态存放到一条Redis记录中。所以我们要把购物车数据合理的分开存储。
- 用户、商品、数量:
hash
carts_user_id: {sku_id1: count, sku_id3: count, sku_id5: count, ...}
- 勾选状态:
set
- 只将已勾选商品的sku_id存储到set中,比如,1号和3号商品是被勾选的。
-
selected_user_id: [sku_id1, sku_id3, ...]
注释:Redis Smembers 命令返回集合中的所有的成员。 不存在的集合 key 被视为空集合。
4.存储逻辑说明
- 当要添加到购物车的商品已存在时,对商品数量进行累加计算。
- 当要添加到购物车的商品不存在时,向hash中新增field和value即可。
==============================================
购物车管理
添加购物车
提示:在商品详情页添加购物车使用局部刷新的效果。
1. 添加购物车接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | post |
请求地址 | /carts/ |
2.请求参数:JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
sku_id | int | 是 | 商品SKU编号 |
count | int | 是 | 商品数量 |
selected | bool | 否 | 是否勾选 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
2. 添加购物车后端逻辑实现
1.接收和校验参数 carts.views.py
import json
from django.http import JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from django_redis import get_redis_connection
from utils.views import login_required # 注意修改导包路径
from goods.models import SKU
# Create your views here.
class CartsView(View):
# 把sku商品加入购物车
@method_decorator(login_required)
def post(self, request):
# 1、提取参数
data = json.loads(request.body.decode())
sku_id = data.get('sku_id')
count = data.get('count')
selected = data.get('selected', True)
# 2、校验参数
if not all([sku_id, count]):
return JsonResponse({'code': 400, 'errmsg': '缺少参数!'})
if not isinstance(selected, bool):
return JsonResponse({'code': 400, 'errmsg': '参数有误!'})
# 3、判断是否登陆
user = request.user
if user.is_authenticated:
# 4、登陆写入redis
conn = get_redis_connection('carts')
# 4.1 记录sku商品数量——carts_<user_id> : {sku_id: count}
# conn.hmset('carts_%s'%user.id, {sku_id:count}) # 我们不能使用该函数,因为他会覆盖原有数据
conn.hincrby('carts_%s'%user.id, sku_id, amount=count) # 把sku商品数量增加count,如果不存在则新建
# 4.2 记录选中状态——selected_<user_id> : [sku_id]
if selected:
conn.sadd('selected_%s'%user.id, sku_id)
return JsonResponse({'code': 0, 'errmsg': 'ok'})
=========================================
展示购物车
1. 展示购物车接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /carts/ |
**2.请求参数:**无
3.响应结果:HTML cart.html
4.后端接口定义 carts.views.py
class CartsView(View):
def get(self, request):
# 0、初始化一个空字典,用于保存sku购物车数据,其格式和cookie购物车格式一样
cart_dict = {} # {1: {count:xx, selected:xx}}
user = request.user
if user.is_authenticated:
# 1、用户登陆,则从redis中读取sku商品信息
pass
2. 展示购物车后端逻辑实现
1.查询Redis购物车
2.查询购物车SKU信息
3.返回响应数据
# 展示购物车
@method_decorator(login_required)
def get(self, request):
# 0、初始化一个空字典,用于保存sku购物车数据,其格式和cookie购物车格式一样
cart_dict = {} # {1: {count:xx, selected:xx}}
user = request.user
if user.is_authenticated:
# 1、用户登陆,则从redis中读取sku商品信息
conn = get_redis_connection('carts')
# 2、sku商品数量、是否选中
# cart_redis_dict = {b'1': b'8'}
cart_redis_dict = conn.hgetall('carts_%s' % user.id)
# cart_redis_selected = [b'1']
cart_redis_selected = conn.smembers('selected_%s' % user.id) #smembers返回集合中的所有的成员。 不存在的集合 key 被视为空集合
#将cart_redis_dict与cart_redis_selected合并
for k, v in cart_redis_dict.items(): #字典(Dictionary) items() 函数以列表返回可遍历的(键, 值) 元组数组。
# k: b'1'; v: b'8'
cart_dict[int(k)] = {
'count': int(v),
'selected': k in cart_redis_selected # b'1' in [b'1']返回True
}
cart_skus = []
# 3、构建响应返回
for k, v in cart_dict.items():
# k: sku_id; v: {count:xx, selected: xx}
sku = SKU.objects.get(pk=k)
cart_skus.append({
'id': sku.id,
'name': sku.name,
'count': v['count'],
'selected': v['selected'],
'price': sku.price,
'default_image_url': sku.default_image_url.url,
'amount': sku.price * v['count']
})
return JsonResponse({'code': 0, 'errmsg': 'ok', 'cart_skus': cart_skus})
==============================================
修改购物车
提示:在购物车页面修改购物车使用局部刷新的效果。
1. 修改购物车接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | PUT |
请求地址 | /carts/ |
2.请求参数:JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
sku_id | int | 是 | 商品SKU编号 |
count | int | 是 | 商品数量 |
selected | bool | 否 | 是否勾选 |
3.响应结果:JSON
字段 | 说明 |
---|---|
sku_id | 商品SKU编号 |
count | 商品数量 |
selected | 是否勾选 |
4.后端接口定义
# 修改购物车
def put(self, request):
user = request.user
if user.is_authenticated:
# 登陆,修改redis
pass
2. 修改购物车后端逻辑实现
1.接收和校验参数
2.修改Redis购物车
# 修改购物车
@method_decorator(login_required)
def put(self, request):
data = json.loads(request.body.decode())
sku_id = data.get('sku_id')
count = data.get('count')
selected = data.get('selected', True)
user = request.user
if user.is_authenticated:
# 登陆,修改redis
conn = get_redis_connection('carts')
conn.hmset('carts_%s' % user.id, {sku_id: count}) # 覆盖写入
if selected:
conn.sadd('selected_%s' % user.id, sku_id) #选中,sadd增加
else:
conn.srem('selected_%s' % user.id, sku_id) #未选中,srem删除
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'cart_sku': {
'id': sku_id,
'count': count,
'selected': selected
}
})
=======================================
删除购物车
提示:在购物车页面删除购物车使用局部刷新的效果。
1. 删除购物车接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | DELETE |
请求地址 | /carts/ |
2.请求参数:JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
sku_id | int | 是 | 商品SKU编号 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
4.后端接口定义
# 删除购物车
def delete(self, request):
user = request.user
# 1、已登陆
if user.is_authenticated:
pass
2. 删除购物车后端逻辑实现
1.接收和校验参数
2.删除Redis购物车
# 删除购物车
@method_decorator(login_required)
def delete(self, request):
# {"sku_id": xxx}}
data = json.loads(
request.body.decode()
)
sku_id = data.get('sku_id')
user = request.user
# 已登陆
if user.is_authenticated:
conn = get_redis_connection('carts')
# 1.1 删除购物车哈希数据——carts_user_id : {sku_id : count}
# Hdel 命令用于删除哈希表 key 中的一个或多个指定字段,不存在的字段将被忽略。
conn.hdel('carts_%s' % user.id, sku_id)
# 1.2 删除集合中的sku_id
# Srem 命令用于移除集合中的一个或多个成员元素,不存在的成员元素会被忽略。
#https://www.runoob.com/redis/redis-commands.html
conn.srem('selected_%s' % user.id, sku_id)
return JsonResponse({'code': 0, 'errmsg': 'ok'})
===========================================
全选购物车
提示:在购物车页面修改购物车使用局部刷新的效果。
1. 全选购物车接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | PUT |
请求地址 | /carts/selection/ |
2.请求参数:JSON
参数名 | 类型 | 是否必传 | 说明 |
---|---|---|---|
selected | bool | 是 | 是否全选 |
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
4.后端接口定义
class CartSelectAllView(View):
# 设置全选/全取消
def put(self, request):
# 1、获取参数
if user.is_authenticated:
# 已登陆:把所有的sku_id加入/删除到selected_user_id的集合中
pass
2. 全选购物车后端逻辑实现
1.接收和校验参数
2.全选Redis购物车
class CartSelectAllView(View):
# 设置全选/全取消
@method_decorator(login_required)
def put(self, request):
# 1、获取参数
data = json.loads(request.body.decode())
selected = data.get("selected") # True or False
user = request.user
if user.is_authenticated:
# 已登陆:把所有的sku_id加入/删除到selected_user_id的集合中
conn = get_redis_connection('carts')
# 1、获取用户的购物车数据
# {b'1': b'5'}
cart_dict = conn.hgetall('carts_%s'%user.id)
# [b'1', b'2']
sku_ids = cart_dict.keys()
# 2、设置全/全取消
if selected:
conn.sadd('selected_%s'%user.id, *sku_ids)
else:
conn.srem('selected_%s'%user.id, *sku_ids)
return JsonResponse({'code': 0, 'errmsg': 'ok'})
=============================================
展示商品页面简单购物车
需求:用户鼠标悬停在商品页面右上角购物车标签上,以下拉框形式展示当前购物车数据。
1. 简单购物车数据接口设计和定义
1.请求方式
选项 | 方案 |
---|---|
请求方法 | GET |
请求地址 | /carts/simple/ |
**2.请求参数:**无
3.响应结果:JSON
字段 | 说明 |
---|---|
code | 状态码 |
errmsg | 错误信息 |
cart_skus[ ] | 简单购物车SKU列表 |
id | 购物车SKU编号 |
name | 购物车SKU名称 |
count | 购物车SKU数量 |
default_image_url | 购物车SKU图片 |
4.后端接口定义
class CartsSimpleView(View):
"""商品页面右上角购物车"""
def get(self, request):
# 判断用户是否登录
user = request.user
if user.is_authenticated:
# 用户已登录,查询Redis购物车
pass
# 构造简单购物车JSON数据
pass
2. 简单购物车数据后端逻辑实现
1.查询Redis购物车
2.构造简单购物车JSON数据
class CartsSimpleView(View):
def get(self, request):
# 读取购物车数据,把具体的sku商品信息返回
user = request.user
# {1: {"count":xx, "selected":xxx}}
cart_dict = {} # 购物车数据——格式和cookie格式是一样的
if user.is_authenticated:
# 已登陆,从redis中读商品数据
conn = get_redis_connection('carts')
# {b'1': b'4'}
carts_redis = conn.hgetall('carts_%s'%user.id)
# [b'1']
carts_selected = conn.smembers('selected_%s'%user.id)
for k,v in carts_redis.items():
# k: b'1'; v: b'4'
cart_dict[int(k)] = {
'count': int(v),
'selected': k in carts_selected
}
# 构造响应数据
cart_skus = []
for k,v in cart_dict.items():
# k: sku_id; v: {"count":xx, "selected":xxx}
sku = SKU.objects.get(pk=k)
cart_skus.append({
'id': sku.id,
'name': sku.name,
'count': v['count'],
'default_image_url': sku.default_image_url.url
})
return JsonResponse({
'code': 0,
'errmsg': 'ok',
'cart_skus': cart_skus
})
carts.urls.py
from django.urls import re_path
from .views import *
urlpatterns = [
re_path(r'^carts/$', CartsView.as_view()),
re_path(r'^carts/selection/$', CartSelectAllView.as_view()),
re_path(r'^carts/simple/$', CartsSimpleView.as_view()),
]
修改前端文件
1、1.html 修改动态加载“简单购物车”显示
<!-- <div class="guest_cart fr">
<a href="#" class="cart_name fl">我的购物车</a>
<div class="goods_count fl" id="show_count">15</div>
<ul class="cart_goods_show">
<li>
<img src="#" alt="商品图片">
<h4>商品名称手机</h4>
<div>4</div>
</li>
<li>
<img src="#" alt="商品图片">
<h4>商品名称手机</h4>
<div>5</div>
</li>
<li>
<img src="#" alt="商品图片">
<h4>商品名称手机</h4>
<div>6</div>
</li>
<li>
<img src="#" alt="商品图片">
<h4>商品名称手机</h4>
<div>6</div>
</li>
</ul>
</div> -->
<div class="guest_cart fr">
<a href="#" class="cart_name fl">我的购物车</a>
<div class="goods_count fl" id="show_count">[[ cart_total_count ]]</div>
<ul class="cart_goods_show">
<li v-for="sku in carts">
<img :src="sku.default_image_url" alt="商品图片">
<h4>[[ sku.name ]]</h4>
<div>[[ sku.count ]]</div>
</li>
</ul>
</div>
修改
- 为
-
2、修改detail.js
改19行
mounted: function()中添加:this.get_hot_goods();
把注释部分打开
var vm = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { host, username: '', //改 user_id: sessionStorage.user_id || localStorage.user_id, token: sessionStorage.token || localStorage.token, tab_content: { detail: true, pack: false, comment: false, service: false }, sku_id: '', sku_count: 1, sku_price: price, cart_total_count: 0, // 购物车总数量 carts: [], // 购物车数据 hot_skus: [], // 热销商品 cat: cat, // 商品类别 comments: [], // 评论信息 score_classes: { 1: 'stars_one', 2: 'stars_two', 3: 'stars_three', 4: 'stars_four', 5: 'stars_five', } }, computed: { sku_amount: function(){ return (this.sku_price * this.sku_count).toFixed(2); } }, mounted: function(){ // 获取cookie中的用户名 (增) this.username = getCookie('username'); // 添加用户浏览历史记录 this.get_sku_id(); axios.post(this.host+'/browse_histories/', { sku_id: this.sku_id },{ responseType: 'json', withCredentials:true, }) .then(response=>{ console.log(response) }) .catch(error=>{ console.log(error) }) this.get_cart(); this.get_hot_goods(); this.get_comments(); }, methods: { // 退出登录按钮 logoutfunc: function () { var url = this.host + '/logout/'; axios.delete(url, { responseType: 'json', withCredentials:true, }) .then(response => { location.href = 'http://192.168.42.128/login.html';//改退出页面404错误 }) .catch(error => { console.log(error.response); }) }, // 控制页面标签页展示 on_tab_content: function(name){ this.tab_content = { detail: false, pack: false, comment: false, service: false }; this.tab_content[name] = true; }, // 从路径中提取sku_id get_sku_id: function(){ var re = /^\/goods\/(\d+).html$/; this.sku_id = document.location.pathname.match(re)[1]; }, // 减小数值 on_minus: function(){ if (this.sku_count > 1) { this.sku_count--; } }, // 获取用户所有的资料 (增:校验用户是否登陆状态) get_person_info: function () { var url = 'http://192.168.42.128:8000'+ '/info/'; console.log(url) axios.get(url, { responseType: 'json', withCredentials: true }) .then(response => { if (response.data.code == 400) { window.location.href = 'http://192.168.42.128/login.html' return } this.username = response.data.info_data.username; }) .catch(error => { location.href = 'http://192.168.42.128/login.html' }) }, // 添加购物车 add_cart: function(){ this.get_person_info() var url = this.host + '/carts/' axios.post(url, { sku_id: parseInt(this.sku_id), count: this.sku_count }, { responseType: 'json', withCredentials: true }) .then(response => { alert('添加购物车成功'); this.cart_total_count += response.data.count; window.location.reload() //新增刷新页面,解决购物车数据变更显示bug }) .catch(error => { console.log(error); }) }, get_cart(){ let url = this.host + '/carts/simple/'; axios.get(url, { responseType: 'json', withCredentials:true, }) .then(response => { this.carts = response.data.cart_skus; this.cart_total_count = 0; for(let i=0;i<this.carts.length;i++){ if (this.carts[i].name.length>25){ this.carts[i].name = this.carts[i].name.substring(0, 25) + '...'; } this.cart_total_count += this.carts[i].count; } }) .catch(error => { console.log(error); }) }, // 获取热销商品数据(补全) get_hot_goods: function(){ // 请求获取热销商品数据 var url = this.host+'/hot/'+this.cat + '/' axios.get(url, { responseType: 'json', withCredentials: true }) .then(response => { this.hot_skus = response.data.hot_skus for(let i=0; i<this.hot_skus.length; i++){ this.hot_skus[i].url = '/goods/' + this.skus[i].id + ".html"; } }) .catch(error => { console.log(error); }) } , // 获取商品评价信息 get_comments: function(){ } } });
3、修改cart.js
var vm = new Vue({ el: '#app', delimiters: ['[[', ']]'], data: { host, username: '', user_id: sessionStorage.user_id || localStorage.user_id, token: sessionStorage.token || localStorage.token, cart: [], total_selected_count: 0, origin_input: 0 // 用于记录手动输入前的值 }, computed: { total_count: function(){ var total = 0; for(var i=0; i<this.cart.length; i++){ total += (this.cart[i].count); this.cart[i].amount = ((this.cart[i].price) * (this.cart[i].count)).toFixed(2); } return total; }, total_selected_amount: function(){ var total = 0; this.total_selected_count = 0; for(var i=0; i<this.cart.length; i++){ if(this.cart[i].selected) { total += ((this.cart[i].price) * (this.cart[i].count)); this.total_selected_count += (this.cart[i].count); } } return total.toFixed(2); }, selected_all: function(){ var selected=true; for(var i=0; i<this.cart.length; i++){ if(this.cart[i].selected==false){ selected=false; break; } } return selected; } }, mounted: function(){ this.username = getCookie('username') // 获取个人信息: (新增) this.get_person_info() // 获取购物车数据 axios.get(this.host+'/carts/', { responseType: 'json', withCredentials: true }) .then(response => { this.cart = response.data.cart_skus; for(var i=0; i<this.cart.length; i++){ this.cart[i].amount = ((this.cart[i].price) * this.cart[i].count).toFixed(2); } }) .catch(error => { console.log(error.response.data); }) }, methods: { // 退出 logoutfunc: function(){ sessionStorage.clear(); localStorage.clear(); location.href = '/login.html'; }, // 获取用户所有的资料(新增) get_person_info: function () { var url = this.host + '/info/'; axios.get(url, { responseType: 'json', withCredentials: true }) .then(response => { if (response.data.code == 400) { location.href = 'login.html' return } this.username = response.data.info_data.username; this.mobile = response.data.info_data.mobile; this.email = response.data.info_data.email; this.email_active = response.data.info_data.email_active; }) .catch(error => { this.set_email = false location.href = 'login.html' }) }, // 减少操作 on_minus: function(index){ if (this.cart[index].count > 1) { var count = this.cart[index].count - 1; this.update_count(index, count); } }, on_add: function(index){ var count = this.cart[index].count + 1; this.update_count(index, count); }, // 删除购物车数据 on_delete: function(index){ axios.delete(this.host+'/carts/', { data: { sku_id: this.cart[index].id }, responseType: 'json', withCredentials: true }) .then(response => { if (response.data.code == 0) { this.cart.splice(index, 1); } }) .catch(error => { console.log(error); }) }, on_input: function(index){ var val = parseInt(this.cart[index].count); if (isNaN(val) || val <= 0) { this.cart[index].count = this.origin_input; } else { // 更新购物车数据 axios.put(this.host+'/carts/', { sku_id: this.cart[index].id, count: val, selected: this.cart[index].selected }, { responseType: 'json', withCredentials: true }) .then(response => { this.cart[index].count = response.data.count; }) .catch(error => { console.log(error) }) } }, // 更新购物车数据 update_count: function(index, count){ axios.put(this.host+'/carts/', { sku_id: this.cart[index].id, count, selected: this.cart[index].selected }, { responseType: 'json', withCredentials: true }) .then(response => { this.cart[index].count = response.data.cart_sku.count; }) .catch(error => { console.log(error) }) }, // 更新购物车数据 update_selected: function(index) { axios.put(this.host+'/carts/', { sku_id: this.cart[index].id, count: this.cart[index].count, selected: this.cart[index].selected }, { responseType: 'json', withCredentials: true }) .then(response => { this.cart[index].selected = response.data.cart_sku.selected; }) .catch(error => { console.log(error); }) }, // 购物车全选 on_selected_all: function(){ var selected = !this.selected_all; axios.put(this.host + '/carts/selection/', { selected }, { responseType: 'json', withCredentials: true }) .then(response => { for (var i=0; i<this.cart.length;i++){ this.cart[i].selected = selected; } }) .catch(error => { console.log(error); }) }, } });
查看redis
(aerf_mall) pyvip@VIP:~/df17/aerf_mall/aerf_mall/aerf_mall/apps$ redis-cli 127.0.0.1:6379> select 5 OK 127.0.0.1:6379[5]> keys * 1) "carts_1" 2) "selected_1" 127.0.0.1:6379[5]> hgetall carts_1 1) "1" 2) "2" 127.0.0.1:6379[5]> smembers selected_1 1) "1" 127.0.0.1:6379[5]> keys * 1) "carts_1" 2) "selected_1" 127.0.0.1:6379[5]> smembers selected_1 1) "1" 2) "3" 127.0.0.1:6379[5]> hgetall carts_1 1) "1" 2) "2" 3) "3" 4) "1" 127.0.0.1:6379[5]>
祝大家学习python顺利!