-
{% for item in page %}
-
{{ item.object.name }}
{% endfor %}
关键:全文检索框架、搜索引擎、分词包、Ajax请求的使用...
仅作为个人笔记!
目录
1.搜索
1.1. haystack框架和whoosh引擎安装
1.2.索引文件的生成
1.3.全文检索的使用
1.4.更改分词方式
2.购物车
2.3.购物车前端Ajax请求
在首页、详情页和列表页中,都有搜索框,如何实现搜索呢?
关键:全文检索框架、搜索引擎、分词包的使用、
全文检索不同于特定字段的模糊查询,使用全文检索的效率更高,并且能够对于中文进行分词处理。
搜索引擎:可以对表中的某些字段进行关键词分析,建立关键词对应的索引数据;
全文检索框架:帮助用户使用搜索引擎;
本处使用 haystack框架 和 whoosh引擎 。
- haystack:全文检索的框架,支持whoosh、solr、Xapian、Elasticsearc四种全文检索引擎,点击查看官方网站。
- whoosh:纯Python编写的全文搜索引擎,虽然性能比不上sphinx、xapian、Elasticsearc等,但是无二进制包,程序不会莫名其妙的崩溃,对于小型的站点,whoosh已经足够使用,点击查看whoosh文档。
- jieba:一款免费的中文分词包,如果觉得不好用可以使用一些收费产品。
(1)在虚拟环境中依次安装需要的包
pip install django-haystack
pip install whoosh
pip install jieba
(2)在settings.py文件中注册应用haystack,并做配置
# 注册
INSTALLED_APPS = (
...
'haystack',
)
...
# 全文检索框架配置
HAYSTACK_CONNECTIONS = {
'default': {
# 使用whoosh引擎(配置路径)
'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
# 'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine', # 配置好jieba中文分词包后用这个
# 设置索引文件生成的路径
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
}
}
# 当添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
# 指定搜索结果每页显示的条数
HAYSTACK_SEARCH_RESULTS_PER_PAGE = 2
要搜索商品表中的数据,就需要搜索引擎根据表的某些字段来建立关键词对应的索引数据。然后才能让搜索引擎来搜索对应的数据。
根据模型类(如:GoodsSKU)的表的数据生成索引的数据。
from haystack import indexes
# 导入你的模型类
from goods.models import GoodsSKU
# 指定对于某个类的某些数据建立索引
# 建议的索引类名格式:模型类名+Index
class GoodsSKUIndex(indexes.SearchIndex, indexes.Indexable):
# 索引字段 use_template=True:指定根据表中的哪些字段建立索引文件,这个'指定说明'放在一个文件中
text = indexes.CharField(document=True, use_template=True)
def get_model(self):
# 返回你的模型类
return GoodsSKU
# 建立索引的数据
def index_queryset(self, using=None):
return self.get_model().objects.all()
# 指定根据表中的哪些字段建立索引数据
{{ object.name }} # 根据商品的名称建立索引
{{ object.desc }} # 根据商品的简介建立索引
{{ object.goods.detail }} # 根据商品的详情建立索引
python manage.py rebuild_index
就会按照配置,生成目录whoosh_index,并在目录下生成索引数据。
(1)搜索框的前端,点击进行提交时,会通过 haystack 搜索数据。
(2)action提交后的处理应该让搜索引擎来完成,会通过 haystack 搜索数据,故在项目的url中配置。注意提交的地址应该和配置的url一致。
urlpatterns = [
...
path('search', include('haystack.urls')), # 全文检索框架
]
(3)全文检索结果。
搜索出结果后,haystack会把搜索出的结果传递给templates/search目录下的search.html(没有这个文件可以先自己建),传递的上下文包括:
通过HAYSTACK_SEARCH_RESULTS_PER_PAGE 可以控制每页显示数量。
以下可以快速的查看搜索结果:
......
搜索的关键字:{{ query }}
当前页的Page对象:{{ page }}
{% for item in page %}
- {{ item.object }}
{% endfor %}
分页paginator对象:{{ paginator }}
...
浏览器显示结果如下:
搜索的关键字:草莓
当前页的Page对象:
草莓 500g
分页paginator对象:
项目中templates/search目录下的search.html关键代码:
{% for item in page %}
-
{{ item.object.name }}
{% endfor %}
{% if page.has_previous %}
<上一页
{% endif %}
{% for pindex in paginater.page_range %}
{% if pindex == page.number %}
{{ pindex }}
{% else %}
{{ pindex }}
{% endif %}
{% endfor %}
{% if page.has_next %}
下一页>
{% endif %}
默认的引擎在对中文进行关键词分析的时候,可能支持的不是很好。可以使用jieba这个分词包,对中文的处理更好。
(1)安装jieba分词模块:
pip install jieba
(2)更改whoosh引擎的默认词语分析类
import jieba
from whoosh.analysis import Tokenizer, Token
class ChineseTokenizer(Tokenizer):
def __call__(self, value, positions=False, chars=False,
keeporiginal=False, removestops=True,
start_pos=0, start_char=0, mode='', **kwargs):
t = Token(positions, chars, removestops=removestops, mode=mode, **kwargs)
seglist = jieba.cut(value, cut_all=True) # 关键是这句
for w in seglist:
t.original = t.text = w
t.boost = 1.0
if positions:
t.pos = start_pos + value.find(w)
if chars:
t.startchar = start_char + value.find(w)
t.endchar = start_char + value.find(w) + len(w)
yield t
def ChineseAnalyzer():
return ChineseTokenizer()
...
from .ChineseAnalyzer import ChineseAnalyzer
...
# 查找词语分析类:analyzer=StemmingAnalyzer()
# 并改为: analyzer=ChineseAnalyzer()
# 如下:
...
schema_fields[field_class.index_fieldname] = TEXT(stored=True, analyzer=ChineseAnalyzer(), field_boost=field_class.b oost, sortable=True)
...
# 全文检索框架配置
HAYSTACK_CONNECTIONS = {
'default': {
# 使用whoosh引擎(配置路径)
# 'ENGINE': 'haystack.backends.whoosh_backend.WhooshEngine',
'ENGINE': 'haystack.backends.whoosh_cn_backend.WhooshEngine', # 配置好jieba中文分词包后用这个
# 设置索引文件生成的路径
'PATH': os.path.join(BASE_DIR, 'whoosh_index'),
}
}
# 当添加、修改、删除数据时,自动生成索引
HAYSTACK_SIGNAL_PROCESSOR = 'haystack.signals.RealtimeSignalProcessor'
python manage.py rebuild_index
这样,使用新的分词类后,即使是商品详情中包含的词语,也能搜索到结果。
要完成购物车功能,要确定(尤其是前后端分开开发时):
在商品的详情页,包含购买商品的数量、加入购物车等功能。
在js中,绑定相关按钮等的点击事件进行处理。比如增减商品数、计算总价格等
// 更新总价
update_goods_amount()
// 计算商品的总价
function update_goods_amount() {
// 获取商品的单价和数量
price = $('.show_pirze').children('em').text()
count = $('.num_show').val()
// 计算商品的总价
price = parseFloat(price)
count = parseInt(count)
amount = price*count
// 设置商品的总价,设置两位小数
$('.total').children('em').text(amount.toFixed(2)+'元')
}
//增商品的数量(减同理
$('.add').click(function () {
// 获取商品数目并+1
count = $('.num_show').val()
count = parseInt(count)+parseInt('1')
// 重新设置商品的数目
$('.num_show').val(count)
update_goods_amount()
})
...
// 手动输入商品的数量
$('.num_show').blur(function () {
count = $(this).val()
// 校验count是否合法(能否转为数字、去除空格...)
if (isNaN(count) || count.trim().length==0 || parseInt(count) <=0){
count = 1
}
$(this).val(parseInt(count))
update_goods_amount()
})
注:点击事件中,若加1的操作结果并不对,检查代码也没问题。使用alert输出后发现被多次执行了。原来是出现了累加绑定的问题。解决办法可以参考这个博文试试:https://blog.csdn.net/GSCurry/article/details/71857127。
- 使用前先解除绑定:$("#id").unbind("click")
- 使用jQuery的one()方法。该方法为元素绑定一个一次性的事件处理函数,这个事件处理函数只会被执行一次。
- 配合off()方法解除绑定。该方法为元素绑定一个的事件处理函数,再次给改元素添加相同事件时不会累加绑定。
购物车设计
是否传递数据、什么格式什么数据?
访问方式get? post?
返回给前端什么格式什么数据?
redis存储购物车记录:
- 用户点击加入购物车时需要添加购物车记录(添加);
- 使用购物车中数据和访问购物车页面时需要获取购物车记录(获取);
- 存储购物车记录的格式:一个用户的购物车记录用一条数据保存,用hash类型(属性:值,'cart_用户id':{'sku_id1':商品数目, 'sku_id2':商品数目, ...})记录skuid和数量;
在前端点击加入购物车之后,通常页面的整体是不进行刷新操作的。所以采用的方式如下
附:传参的几种方式
- get传参:cart/add?sku_id=1?count=3 ;值涉及获取,可采用get;
- post传参:参数会放在一个字典 {'sku_id':1, 'count':3 } ;若涉及到数据的修改(增删改),采用post ;
- url传参:url配置时捕获参数;
cart/view.py中相关视图
...
# /cart/add
class CartAddView(View):
"""购物车记录添加"""
def post(self, request):
user = request.user
if not user.is_authenticated:
return JsonResponse({'res': 0, 'errmsg': '请先登录'})
# 接收数据
sku_id = request.POST.get('sku_id')
count = request.POST.get('count')
if not all([sku_id, count]):
return JsonResponse({'res': 1, 'errmsg': '数据不完整'})
# 校验添加的商品数量
try:
count = int(count)
except Exception as e:
return JsonResponse({'res': 2, 'errmsg': '商品数目出错'})
try:
sku = GoodsSKU.objects.get(id=sku_id)
except GoodsSKU.DoesNotExist:
return JsonResponse({'res': 3, 'errmsg': '商品不存在'})
# 业务处理:添加购物车记录(已经有的累加、没有的添加)
conn = get_redis_connection('default')
cart_key = 'cart_%d' % user.id
# 先尝试获取sku_id的值 : hget cart_key 属性
# 如果sku_id在hash中不存在,hget会返回None
cart_count = conn.hget(cart_key, sku_id)
if cart_count:
# 累加购物车中商品的数目
count += int(cart_count)
# 校验商品的库存
if count > sku.stock:
return JsonResponse({'res': 4, 'errmsg': '商品库存不足'})
# 设置hash中sku_id对应的值
# hset:如果sku_id已经存在,更新数据, 如果sku_id不存在,添加数据
conn.hset(cart_key, sku_id, count)
# 计算用户购物车商品的条目数
total_count = conn.hlen(cart_key)
# 返回应答
return JsonResponse({'res':5, 'total_count':total_count, 'message':'添加成功'})
购物车记录的添加在Ajax中提交,在刚刚的视图中进行处理后返回。
注意,如果是表单post提交,csrf验证比较好处理,加上{% csrf_token %}即可。Ajax提交怎么处理csrf验证呢?
(1)在前端同加上{% csrf_token %};
(2)访问页面,然后查看源代码,发现对应的隐藏域,类似这样 ;
(3)在Ajax的js中,获取它的值,并加入到要传递的参数中
csrf = $('input[name="csrfmiddlewaretoken"]').val() params = {'sku_id':sku_id, 'count':count, 'csrfmiddlewaretoken':csrf}
这样,就可以通过验证。
// 获取add_cart div元素左上角的坐标
var $add_x = $('#add_cart').offset().top;
var $add_y = $('#add_cart').offset().left;
// 获取show_count div元素左上角的坐标
var $to_x = $('#show_count').offset().top;
var $to_y = $('#show_count').offset().left;
$('#add_cart').click(function(){
// 获取商品的id和数量
sku_id = $(this).attr('sku_id') // 获取自定义属性用attr
count = $('.num_show').val()
// 获取csrf隐藏域
csrf = $('input[name="csrfmiddlewaretoken"]').val()
params = {'sku_id':sku_id, 'count':count, 'csrfmiddlewaretoken':csrf}
// 发起ajax post请求,访问/cart/add, 传递参数
$.post('/cart/add', params, function (data) {
if (data.res == 5){
// 添加成功,并显示动画
$(".add_jump").css({'left':$add_y+80,'top':$add_x+10,'display':'block'})
$(".add_jump").stop().animate({
'left': $to_y+7,
'top': $to_x+7},
"fast", function() {
$(".add_jump").fadeOut('fast',function(){
// 设置用户购物车中商品的条目数
$('#show_count').html(data.total_count);
});
});
}
else {
// 添加失败
alert(data.errmsg)
}
})
})
用一个独立的页面来显示购物车。
相关基本视图如下:
# /cart/
class CartInfoView(LoginRequiredMixin, View):
"""购物车页面显示"""
def get(self, request):
"""显示"""
# 获取登录的用户
user = request.user
# 获取用户购物车中商品的信息(保存在redis)
conn = get_redis_connection('default')
cart_key = 'cart_%d' % user.id
# 记录的格式:{'商品id':商品数量, ...}
cart_dict = conn.hgetall(cart_key)
skus = []
# 保存用户购物车中商品的总数目和总价格
total_count = 0
total_price = 0
# 遍历获取商品的信息
for sku_id, count in cart_dict.items():
# 根据商品的id获取商品的信息
sku = GoodsSKU.objects.get(id=sku_id)
# 计算商品的小计
amount = sku.price * int(count)
# 动态给sku对象增加一个属性amount, 保存商品的小计
sku.amount = amount
# 动态给sku对象增加一个属性count, 保存购物车中对应商品的数量
sku.count = int(count) # 注意转为int
# 添加
skus.append(sku)
# 累加计算商品的总数目和总价格
total_count += int(count)
total_price += amount
# 组织上下文
context = {'total_count': total_count,
'total_price': total_price,
'skus': skus}
# 使用模板
return render(request, 'cart.html', context)
使用js实现购物车页面显示的全选、选部分商品时相应的处理。在cart.html中添加js,改变相应的checkbox的事件。
JQuery选择器参考:https://www.w3school.com.cn/jquery/jquery_ref_selectors.asp、:checkbox、:checked、
购物车页面中,可以对数量进行增减等操作。采用Ajax post请求提交给后台,传递的参数:商品的id(sku_id)。在后台的视图中进行处理。
相关视图跟基本视图差不多(校验登录、接收数据、检验数据、业务处理、返回json...),注意Ajax返回值。前端购物车商品数量的增加的Ajax如下,减少的类似:
(注意:默认发起的ajax请求都是异步的,不会等回调函数执行,需要时要先设置ajax请求为同步,再设置回异步)
// 计算商品的小计
function update_goods_amount(sku_ul) {
count = sku_ul.find('.num_show').val()
price = sku_ul.children('.col05').text()
amount = parseInt(count)*parseFloat(price)
// 设置商品的小计
sku_ul.children('.col07').text(amount.toFixed(2)+'元')
}
...
// 更新购物车中商品的数量
error_update = false
total = 0
function update_remote_cart_info(sku_id, count){
csrf = $('input[name="csrfmiddlewaretoken"]').val()
//组织参数
params = {'sku_id': sku_id, 'count': count, 'csrfmiddlewaretoken': csrf}
// 设置ajax请求为同步
$.ajaxSettings.async = false
// 发起Ajax post请求,访问/cart/update, 传递参数:sku_id count
// 注意:默认发起的ajax请求都是异步的,不会等回调函数执行,需要时要先设置ajax请求为同步,再设置回异步
$.post('/cart/update', params, function (data) {
if(data.res == 5){
// 更新成功
error_update = false
total = data.total_count
}
else{
error_update = true
alert(data.errmsg)
}
})
// 回调函数执行结束,设置ajax请求为异步
$.ajaxSettings.async = true
}
// 购物车商品数量的增加
$('.add').click(function () {
// 获取商品的id和商品的数量
sku_id = $(this).next().attr('sku_id')
count = $(this).next().val()
count = parseInt(count)+1
// 更新购物车记录
update_remote_cart_info(sku_id, count)
// 判断更新是否成功
if(error_update == false){
// 重新设置商品的数目
$(this).next().val(count)
// 计算商品的小计
update_goods_amount($(this).parents('ul'))
// 获取商品对应的checkbox的选中状态,如果被选中,更新页面信息
is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
if (is_checked){
// 更新页面信息
update_page_info()
}
// 更新页面上购物车商品的总件数
$('.total_count').children('em').text(total)
}
})
// 购物车商品数量的减少
......
// 记录用户输入之前商品的数量
pre_count = 0
$('.num_show').focus(function () {
pre_count = $(this).val()
})
// 手动输入购物车中的商品数量
$('.num_show').blur(function () {
sku_id = $(this).attr('sku_id')
count = $(this).val()
if (isNaN(count) || count.trim().length==0 || parseInt(count)<=0){
// 设置商品的数目为用户输入之前的数目
$(this).val(pre_count)
return
}
update_remote_cart_info(sku_id, count)
if(error_update == false){
$(this).val(count)
update_goods_amount($(this).parents('ul'))
// 获取商品对应的checkbox的选中状态,如果被选中,更新页面信息
is_checked = $(this).parents('ul').find(':checkbox').prop('checked')
if (is_checked){
update_page_info()
}
$('.total_count').children('em').text(total)
}
else {
// 设置商品的数目为用户输入之前的数目
$(this).val(pre_count)
}
})
点击删除将购物车商品删除。
同样采用ajax post请求、 前端需要传递的参数:商品的id(sku_id)。
view.py视图关键code:
...
# 业务处理:删除购物车记录
conn = get_redis_connection('default')
cart_key = 'cart_%d' % user.id
# 删除 hdel
conn.hdel(cart_key, sku_id)
。。。
cart.html提交的相关Ajax请求,关键是回调成功后,执行.remove():
// 删除购物车中的记录
$('.cart_list_td').children('.col08').children('a').click(function () {
// 获取对应商品的id
sku_id = $(this).parents('ul').find('.num_show').attr('sku_id')
csrf = $('input[name="csrfmiddlewaretoken"]').val()
params = {'sku_id':sku_id, 'csrfmiddlewaretoken':csrf}
// 获取商品所在的ul元素
sku_ul = $(this).parents('ul')
// 发起ajax post请求, 访问/cart/delete, 传递参数:sku_id
$.post('/cart/delete', params, function (data) {
if (data.res == 3){
// 删除成功,移除页面上商品所在的ul元素
sku_ul.remove()
// 获取sku_ul中商品的选中状态
is_checked = sku_ul.find(':checkbox').prop('checked')
if (is_checked){
update_page_info()
}
// 重新设置页面上购物车中商品的总件数
$('.total_count').children('em').text(data.total_count)
}
else{
alert(data.errmsg)
}
})
})
01 框架、数据表设计、项目框架笔记
02 注册、登录、用户中心 (itsdangerous模块加密、celery异步、 Django 的验证系统、redis作为缓存等)
03 FastDFS文件存储-首页-详情页-列表页
04 搜索(搜索引擎、分词包的使用)、购物车
05 订单(Mysql事务、并发处理、支付宝支付、评论)
06 项目部署(uwsgi服务器、Nginx服务器)
仅作为个人笔记 !
-----end-----