商品详情页分析和准备
1.商品分类
2.面包屑
3.热销排行
4.商品名字、价格、数量、规格(颜色,内存,) 总价(和数量有关系)
5.商品详情 规格与包装 售后服务
6. 商品评价(完成下单后)
商品详情页组成结构分析
1.商品频道分类
• 已经提前封装在contents.utils.py文件中,直接调用方法即可。
2.面包屑导航
• 已经提前封装在goods.utils.py文件中,直接调用方法即可。
3.热销排行
• 该接口已经在商品列表页中实现完毕,前端直接调用接口即可。
4.商品SKU信息(详情信息)
• 通过sku_id可以找到SKU信息,然后渲染模板即可。
• 使用Ajax实现局部刷新效果。
5.SKU规格信息
• 通过SKU可以找到SPU规格和SKU规格信息。
6.商品详情介绍、规格与包装、售后服务
• 通过SKU可以找到SPU信息,SPU中可以查询出商品详情介绍、规格与包装、售后服务。
7.商品评价
• 商品评价需要在生成了订单,对订单商品进行评价后再实现,商品评价信息是动态数据。
• 使用Ajax实现局部刷新效果。
商品详情页接口设计和定义
1.请求方式
选项 方案
请求方法 GET
请求地址 /detail/(?P<sku_id>\d+)/
2.请求参数:路径参数
参数名 类型 是否必传 说明
sku_id string 是 商品SKU编号
3.响应结果:HTML
detail.html
4.接口定义
商品详情页
goods/views.py
class DetailDoodsView(View):
"""商品详情页"""
def get(self, request, sku_id):
# 验证
try:
sku = SKU.objects.get(id=sku_id)
except Exception as e:
# return http.HttpResponseForbidden('参数sku_id不存在')
return render(request, '404.html')
# 查询商品分类
categories = get_categories()
# 查询面包屑
breadcrumb = get_breadcrumb(sku.category)
# 以下代码为整个项目最难理解的内容
# 构建当前商品的规格键
sku_specs = SKUSpecification.objects.filter(sku__id=sku_id).order_by('spec_id')
sku_key = []
for spec in sku_specs:
sku_key.append(spec.option.id)
# [1, 4, 7] [8, 11]
# print(sku_key)
# 获取当前商品的所有SKU
spu_id = sku.spu_id
skus = SKU.objects.filter(spu_id=spu_id)
# 构建不同规格参数(选项)的sku字典
spec_sku_map = {
}
for s in skus:
# 获取sku的规格参数
s_specs = s.specs.order_by('spec_id')
# print(s_specs.query)
# 用于形成规格参数-sku字典的键
key = []
for spec in s_specs:
key.append(spec.option.id)
# 向规格参数-sku字典添加记录
spec_sku_map[tuple(key)] = s.id
# print(spec_sku_map)
# 获取当前商品的规格信息
goods_specs = SPUSpecification.objects.filter(spu_id=spu_id).order_by('id')
# print(goods_specs)
# 若当前sku的规格信息不完整,则不再继续
# if len(sku_key) < len(goods_specs):
# return
for index, spec in enumerate(goods_specs):
# print(index, spec)
# 复制当前sku的规格键
key = sku_key[:]
# 该规格的选项
spec_options = spec.options.all()
for option in spec_options:
# 在规格参数sku字典中查找符合当前规格的sku
key[index] = option.id
option.sku_id = spec_sku_map.get(tuple(key))
spec.spec_options = spec_options
context = {
"sku": sku,
"categories": categories,
"breadcrumb": breadcrumb,
"specs": goods_specs
}
return render(request, 'detail.html', context=context)
detail.html
渲染SKU详情信息
<div class="goods_detail_con clearfix">
<div class="goods_detail_pic fl"><img src="{
{ sku.default_image.url }}"></div>
<div class="goods_detail_list fr">
<h3>{
{
sku.name }}</h3>
<p>{
{
sku.caption }}</p>
<div class="price_bar">
<span class="show_pirce">¥<em>{
{
sku.price }}</em></span>
<a href="javascript:;" class="goods_judge">18人评价</a>
</div>
<div class="goods_num clearfix">
<div class="num_name fl">数 量:</div>
<div class="num_add fl">
<input v-model="sku_count" @blur="check_sku_count" type="text" class="num_show fl">
<a @click="on_addition" class="add fr">+</a>
<a @click="on_minus" class="minus fr">-</a>
</div>
</div>
{
#...商品规格...#}
<div class="total" v-cloak>总价:<em>[[ sku_amount ]]元</em></div>
<div class="operate_btn">
<a href="javascript:;" class="add_cart" id="add_cart">加入购物车</a>
</div>
</div>
</div>
为了实现用户选择商品数量的局部刷新效果,我们将商品单价从模板传入到Vue.js
<script type="text/javascript">
let category_id = {
{
sku.category.id }};
let sku_price = {
{
sku.price }};
let sku_id = {
{
sku.id }};
</script>
统计分类商品的访问量
goodes/views.py
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()
# print(t) # 2021-03-28 14:43:34.388257+08:00
# 获取当前的时间字符串
today_str = '%d-%02d-%02d' % (t.year, t.month, t.day)
# print(today_str)
# 保存
# 需要在models.py中创建一张与访问量有关系的表
# 分析字段应该包含:count(访问量) category_id(商品) time (某一天) user(可选)
try:
# 存在记录 修改记录 count
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'})
models.py
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
浏览记录 browse_histories/
当登录用户在浏览商品的详情页时,可以把详情页这件商品信息存储起来,作为该登录用户的浏览记录
存储数据说明:
浏览记录界面上要展示商品的一些SKU信息,但是在存储时没有必要存很多SKU信息。
选择存储SKU的唯一编号(sku_id)来表示该件商品浏览记录
存储数据:sku_id
存储位置说明
用户浏览记录时临时数据,且经常变化,数据量不大,所以选择内存型数据库进行存储
存储位置:Redis数据库 3号数据库
CACHES = {
"history": {
# 用户浏览记录
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/3",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
}
选择数据类型分析:
最新浏览的排在前面,如果保存浏览的个数是固定的,访问相同的商品前需删除先访问浏览过的商品,保留最近浏览品
Redis数据类型有5种:String(字符串) Hash(哈希表) List列表 Set集合 SortedSet(有序集合)
要保留数据类型一定是key value(多个),排除用String(字符串),Hash(哈希表) 特点也是key value,只是value是一个字典,比如key是某一个用户,value如何设计?还要有序,添加,删除。List列表 :如同空心的竹子,从右边进是LPUSH,从左边进是RPUSH, 本项目存储可选择列表来处理:将sku_id往里面添加,遵守LPUSH和RPUSH就可以了,
存储类型说明
• 由于用户浏览记录跟用户浏览商品详情的顺序有关,所以我们选择使用Redis中的list类型存储 sku_id
• 每个用户维护一条浏览记录,且浏览记录都是独立存储的,不能共用。所以我们需要对用户的浏览记录进行唯一标识。
• 我们可以使用登录用户的ID来唯一标识该用户的浏览记录。
• 存储类型:‘history_user_id’ : [sku_id_1, sku_id_2, …]
Redis 命令参考
http://doc.redisfans.com/
LREM
LREM key count value
count 的值可以是以下几种:
count > 0 : 从表头开始向表尾搜索,移除与 value 相等的元素,数量为 count 。
count < 0 : 从表尾开始向表头搜索,移除与 value 相等的元素,数量为 count 的绝对值。
count = 0 : 移除表中所有与 value 相等的值
根据参数 count 的值,移除列表中与参数 value 相等的元素
用第三种 count = 0 : 移除表中所有与 value 相等的值
LTRIM
LTRIM key start stop
对一个列表进行修剪(trim),就是说,让列表只保留指定区间内的元素,不在指定区间之内的元素都将被删除。
举个例子,执行命令 LTRIM list 0 2 ,表示只保留列表 list 的前三个元素,其余元素全部删除。
下标(index)参数 start 和 stop 都以 0 为底,也就是说,以 0 表示列表的第一个元素,以 1 表示列表的第二个元素,以此类推。
你也可以使用负数下标,以 -1 表示列表的最后一个元素, -2 表示列表的倒数第二个元素,以此类推。
当 key 不是列表类型时,返回一个错误。
总结存储逻辑说明:
• SKU信息不能重复。
• 最近一次浏览的商品SKU信息排在最前面,以此类推。
• 每个用户的浏览记录最多存储五个商品SKU信息。
• 存储逻辑:先去重,再存储,最后截取。
保存和查询浏览记录
users/views.py
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.objects.get(id=sku_id)
except SKU.DoesNotExist:
return http.HttpResponseForbidden('sku_id不存在')
redis_conn = get_redis_connection('history')
user = request.user
pl = redis_conn.pipeline()
# 去重复
pl.lrem('history_%s' % user.id, 0, sku_id)
# 保存
pl.lpush('history_%s' % user.id, sku_id)
# 截取 需求是保存5个商品 0, 4
pl.ltrim('history_%s' % user.id, 0, 4)
# 执行
pl.execute()
# 响应结果
return http.JsonResponse({
'code': RETCODE.OK, 'errmsg': 'OK'})
def get(self, request):
"""查询用户商品浏览记录"""
redis_conn = get_redis_connection('history')
user = request.user
sku_ids = redis_conn.lrange('history_%s' % user.id, 0, -1)
# print(sku_ids)
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})
user_conter_info.html
<h3 class="common_title2">最近浏览</h3>
<div class="has_view_list" v-cloak>
<ul class="goods_type_list clearfix">
<li v-for="sku in histories">
<a :href="sku.url"><img :src="sku.default_image_url"></a>
<h4><a :href="sku.url">[[ sku.name ]]</a></h4>
<div class="operate">
<span class="price">¥[[ sku.price ]]</span>
<span class="unit">台</span>
<a href="javascript:;" class="add_goods" title="加入购物车"></a>
</div>
user_conter_info.js
// 请求浏览历史记录
browse_histories(){
let url = '/users/browse_histories/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
this.histories = response.data.skus;
for(let i=0; i<this.histories.length; i++){
this.histories[i].url = '/detail/' + this.histories[i].id + '/';
}
})
.catch(error => {
console.log(error.response);
})
},