(根据居然老师直播课内容整理)
一、商品详情页分析
1、.商品频道分类
- 已经提前封装在contents.utils.py文件中,直接调用方法即可
2、面包屑导航
- 已经提前封装在goods.utils.py文件中,直接调用方法即可。
3、热销排行
- 该接口已经在商品列表页中实现完毕,前端直接调用接口即可。
4、商品SKU信息(详情信息)
- 通过sku_id可以找到SKU信息,然后渲染模板即可。
- 使用Ajax实现局部刷新效果。
5、SKU规格信息
6、商品详情介绍、规格与包装、售后服务
- 通过SKU可以找到SPU信息,SPU中可以查询出商品详情介绍、规格与包装、售后服务。
7、商品评价
- 商品评价需要在生成了订单,对订单商品进行评价后再实现,商品评价信息是动态数据。
- 使用Ajax实现局部刷新效果。
二、商品详情页
1、商品列表页面完善
- 商品列表前端页面中跳转页面需要修改
2、接口设计与定义
2.1 请求方式
选项 |
方案 |
请求方法 |
GET |
请求地址 |
/detail/(?P<sku_id>\d+)/ |
2.2 请求参数 : 路径参数 和 查询参数
参数名 |
类型 |
是否必传 |
说明 |
sku_id |
string |
是 |
商品SKU编号 |
2.3 响应结果 : HTML
detail.html
2.4 接口定义
class DetailView(View):
"""商品详情页"""
def get(self, request, sku_id):
"""提供商品详情页"""
return render(request, 'detail.html')
2.5 路由定义
3、商品详情页初步渲染
3.1 校验参数、渲染商品频道分类、面包屑导航
- 校验参数
- 渲染商品频道分类、面包屑导航
- 将原先在商品列表页实现的代码拷贝到商品详情页即可。
- 面包屑导航原参数是商品分类,可通过SKU的外键category获取category对象
- html中,商品频道分类、面包屑导航不变,商品详情页面都是该商品的sku信息,将sku传到前端
def get(self,request,sku_id):
"""提供商品详情页"""
try:
sku=SKU.objects.get(id=sku_id)
except Exception as e:
return render(request,"404.html")
categories = get_categories()
breadcrumb = get_breadcrumb(sku.category)
content={
'categories': categories,
'breadcrumb': breadcrumb,
'sku':sku,
}
return render(request,"detail.html")
3.2 添加商品热销排行
- 商品热销排行是前端通过Ajax访问
- 后端实现已封装HotGoodsView()中
- 修改前端相应模块即可
- /templates/detail.html
<div class="new_goods">
<h3>热销排行</h3>
<ul>
<li v-for="sku in hot_skus">
<a href="{% url 'goods:detail' sku.id %}"><img :src="sku.default_image_url"></a>
<h4><a href="{% url 'goods:detail' sku.id %}">[[ sku.name ]]</a></h4>
<div class="price">¥[[ sku.price ]]</div>
</li>
</ul>
</div>
- /static/js/detail.js
get_hot_skus(){
if (this.category_id) {
let url = '/hot/'+ this.category_id +'/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
this.hot_skus = response.data.hot_skus;
for(let i=0; i<this.hot_skus.length; i++){
this.hot_skus[i].url = '/detail/' + this.hot_skus[i].id + '/';
}
})
.catch(error => {
console.log(error.response);
})
}
},
- 为了让前端在获取商品热销排行数据时,能够拿到商品分类ID,我们将商品分类ID从模板传入到Vue.js
3.3 修改名称、标题、单价
3.4 修改总价
- 总价通过vue计算得到
- data中定义sku_amount
- 通过监听商品数量量变化,计算总价
- 需要接收后端传递的参数,获取单价
4、查询SKU规格信息后端实现
- 不同商品的规格名称和规格信息可能不同, 即不同商品SPU的规格名称和规格选详不同
- 如衣服规格名称可能有:尺寸编码、颜色、款式等,尺寸编码信息可能有 S、M、L、XL、XXL、XXXL等
- 手机规格名称 可能有:屏幕大小、颜色等,屏幕大小信息可能:5.5 、6.3 等
- 同一商品SPU中,不同规格之间,SKU也可能不同
- 同一款手机spu_id=2 : 规格名称和信息:内存(64G、256G)、颜色(金色、深空灰色、银色)
- 选择(64G , 金色) sku_id=3 , 选择 (256G ,金色) sku_id= 4 …
- 进入详情页面时,SKU_id已明确,需要在规格选项中确定其默认值
- 当选择其它规格选项时,sku_id可能发生变化
4.1 获取当前商品的默认规格选项列表
sku_specs = SKUSpecification.objects.filter(sku__id=sku_id).order_by('spec_id')
print("sku_specs.query=",sku_specs.query)
sku_key = []
for spec in sku_specs:
sku_key.append(spec.option.id)
4.2 构建当前商品SPU下不同规格参数(选项)的sku字典
- 获取当前sku_id的商品SPU的 id :spu_id
- 查询id = sup_id 的所有SKU商品集
- 循环取出SKU商品集
- 根据商品sku 取出 SKU规格集
- 将该sku商品的所有规格id组成列表
- 将此列表转换成元组,做为spec_sku_map字典的key,把该商品sku_id作为value
- 当前sku_id的商品SPU相同的一个字典,该字典对应关系是 sku规格集 : sku_id
- 目的是选择不同的商品规格组合时,可以确定该规格对应的sku_id
spu_id = sku.spu_id
skus = SKU.objects.filter(spu_id=spu_id)
spec_sku_map = {
}
for s in skus:
s_specs = s.specs.order_by('spec_id')
key = []
for spec in s_specs:
key.append(spec.option.id)
spec_sku_map[tuple(key)] = s.id
4.3 为前端渲染提供数据
- 获取当前商品SPU规格名称集goods_specs
- 对SPU规格名称集goods_specs进行循环
- 对过enumerate()对数据集进行编号
- 将sku_key的值,赋值给key
- 取出该SPU的规格信息集spec_options
- 循环遍历规格信息集spec_options
- 将规格替换 key[当前编号]的值
- 给当格信息集spec_options中当前对象添加sku_id属性,并将获取SPU下不同规格参数(选项)的sku字典中对的sku_id
- 将 spec_options对象添加为规格名称集goods_specs的spec_options属性
goods_specs = SPUSpecification.objects.filter(spu_id=spu_id).order_by('id')
print("goods_specs=",goods_specs)
for index, spec in enumerate(goods_specs):
print("index, spec = ",index, spec)
key = sku_key[:]
spec_options = spec.options.all()
print("spec_options=",spec_options)
print("*****************spec_options.query=",spec_options.query)
for option in spec_options:
print("option=",option)
print("key=",key)
key[index] = option.id
print("key[index]=",key[index],"index=",index,"option.id=",option.id,"key=",key)
option.sku_id = spec_sku_map.get(tuple(key))
print("option.sku_id=",option.sku_id,"tuple(key=",tuple(key),"key=",key)
print("-"*10)
spec.spec_options = spec_options
print("spec.spec_options=",spec.spec_options,"spec_options=",spec_options)
for index, spec in enumerate(goods_specs):
print(spec.spec_options.query)
for option in spec.spec_options:
print("option.sku_id=",option.sku_id)
print("option.id=", option.id,"option.value=",option.value)
print("*"*20)
- 调试信息
sku_specs.query= SELECT `tb_sku_specification`.`id`, `tb_sku_specification`.`create_time`, `tb_sku_specification`.`update_time`, `tb_sku_specification`.`sku_id`, `tb_sku_specification`.`spec_id`, `tb_sku_specification`.`option_id` FROM `tb_sku_specification` WHERE `tb_sku_specification`.`sku_id` = 1 ORDER BY `tb_sku_specification`.`spec_id` ASC
sku_key= [1, 4, 7]
spec_sku_map= {
(1, 4, 7): 1, (1, 3, 7): 2}
goods_specs= <QuerySet [<SPUSpecification: Apple MacBook Pro 笔记本: 屏幕尺寸>, <SPUSpecification: Apple MacBook Pro 笔记本: 颜色>, <SPUSpecification: Apple MacBook Pro 笔记本: 版本>]>
goods_specs.query= SELECT `tb_spu_specification`.`id`, `tb_spu_specification`.`create_time`, `tb_spu_specification`.`update_time`, `tb_spu_specification`.`spu_id`, `tb_spu_specification`.`name` FROM `tb_spu_specification` WHERE `tb_spu_specification`.`spu_id` = 1 ORDER BY `tb_spu_specification`.`id` ASC
for enumerate(goods_specs) begin--------------------------------------------------
index, spec = 0 Apple MacBook Pro 笔记本: 屏幕尺寸
type(index), type(spec) = <class 'int'> <class 'goods.models.SPUSpecification'>
key= [1, 4, 7] sku_key= [1, 4, 7]
spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 13.3英寸>, <SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 15.4英寸>]>
*****************spec_options.query= SELECT `tb_specification_option`.`id`, `tb_specification_option`.`create_time`, `tb_specification_option`.`update_time`, `tb_specification_option`.`spec_id`, `tb_specification_option`.`value` FROM `tb_specification_option` WHERE `tb_specification_option`.`spec_id` = 1
option= Apple MacBook Pro 笔记本: 屏幕尺寸 - 13.3英寸
key= [1, 4, 7]
key[index]= 1 index= 0 option.id= 1 key= [1, 4, 7]
option.sku_id= 1 tuple(key= (1, 4, 7) key= [1, 4, 7]
----------
option= Apple MacBook Pro 笔记本: 屏幕尺寸 - 15.4英寸
key= [1, 4, 7]
key[index]= 2 index= 0 option.id= 2 key= [2, 4, 7]
option.sku_id= None tuple(key= (2, 4, 7) key= [2, 4, 7]
----------
spec.spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 13.3英寸>, <SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 15.4英寸>]> spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 13.3英寸>, <SpecificationOption: Apple MacBook Pro 笔记本: 屏幕尺寸 - 15.4英寸>]>
for enumerate(goods_specs) end--------------------------------------------------
for enumerate(goods_specs) begin--------------------------------------------------
index, spec = 1 Apple MacBook Pro 笔记本: 颜色
type(index), type(spec) = <class 'int'> <class 'goods.models.SPUSpecification'>
key= [1, 4, 7] sku_key= [1, 4, 7]
spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 深灰色>, <SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 银色>]>
*****************spec_options.query= SELECT `tb_specification_option`.`id`, `tb_specification_option`.`create_time`, `tb_specification_option`.`update_time`, `tb_specification_option`.`spec_id`, `tb_specification_option`.`value` FROM `tb_specification_option` WHERE `tb_specification_option`.`spec_id` = 2
option= Apple MacBook Pro 笔记本: 颜色 - 深灰色
key= [1, 4, 7]
key[index]= 3 index= 1 option.id= 3 key= [1, 3, 7]
option.sku_id= 2 tuple(key= (1, 3, 7) key= [1, 3, 7]
----------
option= Apple MacBook Pro 笔记本: 颜色 - 银色
key= [1, 3, 7]
key[index]= 4 index= 1 option.id= 4 key= [1, 4, 7]
option.sku_id= 1 tuple(key= (1, 4, 7) key= [1, 4, 7]
----------
spec.spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 深灰色>, <SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 银色>]> spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 深灰色>, <SpecificationOption: Apple MacBook Pro 笔记本: 颜色 - 银色>]>
for enumerate(goods_specs) end--------------------------------------------------
for enumerate(goods_specs) begin--------------------------------------------------
index, spec = 2 Apple MacBook Pro 笔记本: 版本
type(index), type(spec) = <class 'int'> <class 'goods.models.SPUSpecification'>
key= [1, 4, 7] sku_key= [1, 4, 7]
spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/256G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/128G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/512G存储>]>
*****************spec_options.query= SELECT `tb_specification_option`.`id`, `tb_specification_option`.`create_time`, `tb_specification_option`.`update_time`, `tb_specification_option`.`spec_id`, `tb_specification_option`.`value` FROM `tb_specification_option` WHERE `tb_specification_option`.`spec_id` = 3
option= Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/256G存储
key= [1, 4, 7]
key[index]= 5 index= 2 option.id= 5 key= [1, 4, 5]
option.sku_id= None tuple(key= (1, 4, 5) key= [1, 4, 5]
----------
option= Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/128G存储
key= [1, 4, 5]
key[index]= 6 index= 2 option.id= 6 key= [1, 4, 6]
option.sku_id= None tuple(key= (1, 4, 6) key= [1, 4, 6]
----------
option= Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/512G存储
key= [1, 4, 6]
key[index]= 7 index= 2 option.id= 7 key= [1, 4, 7]
option.sku_id= 1 tuple(key= (1, 4, 7) key= [1, 4, 7]
----------
spec.spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/256G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/128G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/512G存储>]> spec_options= <QuerySet [<SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/256G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/128G存储>, <SpecificationOption: Apple MacBook Pro 笔记本: 版本 - core i5/8G内存/512G存储>]>
for enumerate(goods_specs) end--------------------------------------------------
SELECT `tb_specification_option`.`id`, `tb_specification_option`.`create_time`, `tb_specification_option`.`update_time`, `tb_specification_option`.`spec_id`, `tb_specification_option`.`value` FROM `tb_specification_option` WHERE `tb_specification_option`.`spec_id` = 1
option.sku_id= 1
option.id= 1 option.value= 13.3英寸
option.sku_id= None
option.id= 2 option.value= 15.4英寸
********************
SELECT `tb_specification_option`.`id`, `tb_specification_option`.`create_time`, `tb_specification_option`.`update_time`, `tb_specification_option`.`spec_id`, `tb_specification_option`.`value` FROM `tb_specification_option` WHERE `tb_specification_option`.`spec_id` = 2
option.sku_id= 2
option.id= 3 option.value= 深灰色
option.sku_id= 1
option.id= 4 option.value= 银色
********************
SELECT `tb_specification_option`.`id`, `tb_specification_option`.`create_time`, `tb_specification_option`.`update_time`, `tb_specification_option`.`spec_id`, `tb_specification_option`.`value` FROM `tb_specification_option` WHERE `tb_specification_option`.`spec_id` = 3
option.sku_id= None
option.id= 5 option.value= core i5/8G内存/256G存储
option.sku_id= None
option.id= 6 option.value= core i5/8G内存/128G存储
option.sku_id= 1
option.id= 7 option.value= core i5/8G内存/512G存储
********************
5、前端实现
5.1 前端接收数据
- 后端传递的参数
- categories : 商品分类栏 展示的地方接收,并渲染
- breadcrumb:面包屑导航展示的地方接收,并渲染
- goods_specs:商品规格信息展示的地方接收,并渲染
specs = [
{
'name': '颜色',
'options': [
{
'value': '金色', 'sku_id': 'xxxx'},
{
'value': '深空灰', 'sku_id': 'xxxx'},
{
'value': '银色', 'sku_id': 'xxxx'}
]
},
{
'name': '内存',
'options': [
{
'value': '64GB', 'sku_id': 'xxxx'},
{
'value': '256GB', 'sku_id': 'xxxx'}
]
}
]
- sku: 多个地方使用,
- 商品名称、副标题、单价等地方直接使用属性
- 取sku属性赋值变量,供vue使用
5.2 商品规格渲染
- 循环遍历specs (即后端 goods_specs,SPU规格信息集,添加过属性)
- 显示 SPU规格名称
- 循环遍历spec_options
- 判断 option.sku_id(添加的属性)是否等于当前商品的sku_id
- 如果相等,商品规格信息的名称无跳转链接
- 否则判断 option.sku_id不为空,商品规格信息的名称有跳转链接:跳转到sku_id=option.sku_id的详情页面
- 否则(该商品属性组合,无sku_id) 商品规格信息的名称无跳转链接
三、统计分类商品访问量
1、定义商品访问量表
- 需要sku_id、count
- 如果按天统计访问量,需要日期,如果需求按月统计,此处可以改成月
- user 可选项,用户登录可以记,用户未登录,无法区分
class GoodsVisitCount(BaseModel):
"""统计分类商品访问量模型类"""
category = models.ForeignKey(GoodsCategory, on_delete=models.CASCADE, verbose_name='商品分类')
count = models.IntegerField(verbose_name='访问量', default=0)
date = models.DateField(auto_now_add=True, verbose_name='统计日期')
class Meta:
db_table = 'tb_goods_visit'
verbose_name = '统计分类商品访问量'
verbose_name_plural = verbose_name
2、接口设计与定义
2.1 请求方式
选项 |
方案 |
请求方法 |
POST |
请求地址 |
/detail/visit/(?P<category_id>\d+)/ |
2.2 请求参数 : 路径参数 和 查询参数
参数名 |
类型 |
是否必传 |
说明 |
category_id |
string |
是 |
商品分类ID,第三级分类 |
2.3 响应结果 : JSON
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
2.4 路由定义
3、后端view实现
- 校验参数sku_id
- 通过GoodsCategory获得id=category_id的对象category
- 获取当前的日期
- 将当前日期格式化成字符串
- 通过GoodsVisitCount得到date=当前日期字符串,category=category.id的对象counts_data
- 如果不存在,创建空的counts_data对象
- counts_data的category属性赋值 category
- counts_data的count自加1
- counts_data的日期赋值当前日期字符串
- 保存数据
- 返回前端响应结果
class DetailVisitView(View):
"""统计分类商品的访问量"""
def post(self, request, category_id):
try:
category = GoodsCategory.objects.get(id=category_id)
except Exception as e:
return http.HttpResponseForbidden('category_id不存在')
t = timezone.localtime()
today_str = '%d-%02d-%02d' % (t.year, t.month, t.day)
try:
counts_data = GoodsVisitCount.objects.get(date=today_str, category=category.id)
except GoodsVisitCount.DoesNotExist:
counts_data = GoodsVisitCount()
try:
counts_data.category = category
counts_data.count += 1
counts_data.date = today_str
counts_data.save()
except Exception as e:
return http.HttpResponseServerError('统计失败')
return http.JsonResponse({
'code': RETCODE.OK, 'errmsg': 'OK'})
4.注意事项
4.1 注意修改Django参数中的时区
4.2 objects.filter() 没有 DoesNotExist 异常,但get有
四、用户浏览记录保存与展示
- 当登录用户在浏览商品的详情页时,我们就可以把详情页这件商品信息存储起来,作为该登录用户的浏览记录。
- 用户未登录,我们不记录其商品浏览记录。
1、用户浏览记录存储方案
1.1 存储数据说明
- 虽然浏览记录界面上要展示商品的一些SKU信息,但是我们在存储时没有必要存很多SKU信息。
- 我们选择存储SKU信息的唯一编号(sku_id)来表示该件商品的浏览记录。
- 存储数据:sku_id
1.2 存储位置说明
- 用户浏览记录是临时数据,且经常变化,数据量不大,所以我们选择内存型数据库进行存储。
- 户浏览记录数据无需长久保存,并且保存失败也无关紧要
- 存储位置:Redis数据库 3号库
1.3 存储类型说明
- 由于用户浏览记录跟用户浏览商品详情的顺序有关,所以我们选择使用Redis中的list类型存储 sku_id
- 每个用户维护一条浏览记录,且浏览记录都是独立存储的,不能共用。所以我们需要对用户的浏览记录进行唯一标识。
- 我们可以使用登录用户的ID来唯一标识该用户的浏览记录。
- 存储类型:‘history_user_id’ : [sku_id_1, sku_id_2, …]
1.4 存储逻辑说明
- SKU信息不能重复。
- 最近一次浏览的商品SKU信息排在最前面,以此类推。
- 每个用户的浏览记录最多存储五个商品SKU信息。
- 存储逻辑:先去重,再存储,最后截取。
2、保存用户浏览记录
- 当用户点击商品详情页时,提交用户浏览记录(sku),后台进行保存
2.1 请求方式
选项 |
方案 |
请求方法 |
POST |
请求地址 |
/users/browse_histories/ |
2.2 请求参数 : JSON
参数名 |
类型 |
是否必传 |
说明 |
sku_id |
string |
是 |
商品SKU编号 |
2.3 响应结果 : JSON
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
2.4 接口view定义
- 接收参数,解析参数
- json参数,在request.body中,需要进行编码decode()
- 校验参数
- 读取用户信息,如果未登录,直接返回,不用保存
- 连接redis数据库
- 去重
- 插入
- 截取
- 返回前端响应结果
class UserBrowseHistory(LoginRequiredJSONMixin, View):
"""用户浏览记录"""
def post(self, request):
"""保存用户浏览记录"""
json_str=request.body.decode()
json_dict=json.loads(json_str)
sku_id = json_dict.get('sku_id')
try:
sku=SKU.Objects.get(id=sku_id)
except Exception as e:
return http.HttpResponseForbidden('sku_id不存在')
user = request.user
redis_conn = get_redis_connection('history')
redis_conn.lrem('history_%s' % user.id, 0, sku_id)
redis_conn.lpush('history_%s' % user.id, sku_id)
redis_conn.ltrim('history_%s' % user.id, 0, USER_HISTORY_COUNT_LIMIT)
return http.JsonResponse({
'code': RETCODE.OK, 'errmsg': 'OK'})
2.5 路由定义
扫描二维码关注公众号,回复:
13005568 查看本文章
2.6 代码优化
- 连续访问可有管道
2.7 运行结果查看
3、 查询用户浏览记录
3.1 请求方式
选项 |
方案 |
请求方法 |
POST |
请求地址 |
/users/browse_histories/ |
3.2 请求参数 : 无
3.3 响应结果 : JSON
响应结果 |
响应内容 |
errmsg |
错误信息 |
skus[ ] |
商品SKU列表数据 |
id |
商品SKU编号 |
name |
商品SKU名称 |
default_image_url |
商品SKU默认图片 |
price |
商品SKU单价 |
3.4 接口view定义
- 连接redis数据库
- 获取用户信息
- 查询用户浏览记录
- 生成用户浏览记录字典
- 返回前端响应结果
def get(self,request):
"""查询用户商品浏览器记录"""
redis_conn = get_redis_connection('history')
user = request.user
sku_ids = redis_conn.lrange('history_%s' % user.id, 0, -1)
skus = []
for sku_id in sku_ids:
sku = SKU.objects.get(id=sku_id)
skus.append({
'id': sku.id,
'name': sku.name,
'price': sku.price,
'default_image_url': sku.default_image.url,
})
return http.JsonResponse({
'code': RETCODE.OK, 'errmsg': 'OK', 'skus': skus})
3.4 路由已经定义过了