热销排行
list.html
↓
Vue mounted()
↓
axios GET /hot/<category_id>/
↓
HotView
↓
按 sales 排序查 SKU
↓
返回 JSON
↓
Vue v-for 渲染
代码实现
后端:添加HotView
apps/goods/views.py
# ...
from django import http
from utils.response_code import RETCODE
class HotView(View):
def get(self, request, category_id):
try:
category = GoodsCategory.objects.get(id=category_id)
except GoodsCategory.DoesNotExist:
return http.JsonResponse({
'code': RETCODE.NODATAERR,
'errmsg': '暂无此分类'
})
skus = SKU.objects.filter(
category=category,
is_launched=True
).order_by('-sales')[:2]
hot_skus = []
for sku in skus:
hot_skus.append({
'id': sku.id,
'name': sku.name,
'price': sku.price,
'default_image_url': sku.default_image.url,
'url': f'/detail/{sku.id}/'
})
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok',
'hot_skus': hot_skus
})
代码讲解:
- 拿
category_id - 按
sales DESC - 取前 N 条
- 返回 JSON
路由 apps/goods/urls.py
from .views import HotView
urlpatterns = [
...
path('hot/<int:category_id>/', HotView.as_view(), name='hot'),
]
补齐前端
templates/list.html
<div class="main_wrap clearfix">
<!-- 左侧 -->
<div class="l_wrap fl clearfix">
<!-- 热销排行 -->
<div class="new_goods" v-cloak>
<h3>热销排行</h3>
<ul>
<li v-for="sku in hots">
<a :href="'/detail/' + sku.id + '/'">
<img :src="sku.default_image_url">
</a>
<h4>[[ sku.name ]]</h4>
<div class="price">¥[[ sku.price ]]</div>
</li>
</ul>
</div>
</div>
<!-- 右侧商品列表 -->
<div class="r_wrap fr clearfix">
...
</div>
</div>
// 在 list.html 底部(</body> 前)加这一段
<script>
var vm = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
hots: []
},
mounted() {
this.get_hot_skus();
},
methods: {
get_hot_skus() {
axios.get('/hot/' + category_id + '/')
.then(response => {
if (response.data.code === '0') {
this.hots = response.data.hot_skus;
}
})
.catch(error => {
console.log(error);
});
}
}
});
</script>
templates/list.html 修改后为:
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>美多商城-商品列表</title>
<!-- CSS -->
<link rel="stylesheet" href="{{ static('css/jquery.pagination.css') }}">
<link rel="stylesheet" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" href="{{ static('css/main.css') }}">
<!-- JS 库(必须在前) -->
<script src="{{ static('js/jquery-1.12.4.min.js') }}"></script>
<script src="{{ static('js/vue-2.5.16.js') }}"></script>
<script src="{{ static('js/axios-0.18.0.min.js') }}"></script>
<script src="{{ static('js/host.js') }}"></script>
<!-- ⭐ 全局变量(只能定义一次) -->
<script>
var category_id = "{{ category.id }}";
</script>
</head>
<body>
<div id="app" v-cloak>
<!-- ========== 顶部 ========== -->
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_btn fl" v-if="username">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="/logout/">退出</a>
</div>
<div class="login_btn fl" v-else>
<a href="/login/">登录</a>
<span>|</span>
<a href="/register/">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="/info/">用户中心</a>
<span>|</span>
<a href="/carts/">我的购物车</a>
<span>|</span>
<a href="/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<!-- ========== 搜索 ========== -->
<div class="search_bar clearfix">
<a href="/" 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" value="搜索">
</form>
</div>
</div>
<!-- ========== 分类导航 ========== -->
<div class="navbar_con">
<div class="navbar">
<div class="sub_menu_con fl">
<h1 class="fl">商品分类</h1>
<ul class="sub_menu">
{% for group in categories.values() %}
<li>
<div class="level1">
{% for channel in group.channels %}
<a href="{{ channel.url }}">{{ channel.name }}</a>
{% endfor %}
</div>
<div class="level2">
{% for cat2 in group.sub_cats %}
<div class="list_group">
<div class="group_name fl">{{ cat2.name }} ></div>
<div class="group_detail fl">
{% for cat3 in cat2.sub_cats %}
<a href="/list/{{ cat3.id }}/1/">{{ cat3.name }}</a>
{% endfor %}
</div>
</div>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</div>
<ul class="navlist fl">
<li><a href="/">首页</a></li>
<li class="interval">|</li>
<li><a href="#">真划算</a></li>
<li class="interval">|</li>
<li><a href="#">抽奖</a></li>
</ul>
</div>
</div>
<!-- ========== 面包屑 ========== -->
<div class="breadcrumb">
<a href="javascript:;">{{ breadcrumb.cat1.name }}</a>
<span>></span>
<a href="javascript:;">{{ breadcrumb.cat2.name }}</a>
<span>></span>
<a href="javascript:;">{{ breadcrumb.cat3.name }}</a>
</div>
<!-- ========== 商品列表 ========== -->
<div class="main_wrap clearfix">
<!-- 左侧:热销排行 -->
<div class="l_wrap fl clearfix">
<div class="new_goods">
<h3>热销排行</h3>
<ul>
<li v-for="sku in hots" :key="sku.id">
<a :href="'/detail/' + sku.id + '/'">
<img :src="sku.default_image_url">
</a>
<h4>[[ sku.name ]]</h4>
<div class="price">¥[[ sku.price ]]</div>
</li>
</ul>
</div>
</div>
<!-- 右侧商品 -->
<div class="r_wrap fr clearfix">
<div class="sort_bar">
<a href="/list/{{ category.id }}/{{ page_num }}/?sort=default"
{% if sort == 'default' %}class="active"{% endif %}>默认</a>
<a href="/list/{{ category.id }}/{{ page_num }}/?sort=price"
{% if sort == 'price' %}class="active"{% endif %}>价格</a>
<a href="/list/{{ category.id }}/{{ page_num }}/?sort=hot"
{% if sort == 'hot' %}class="active"{% endif %}>人气</a>
</div>
<ul class="goods_type_list clearfix">
{% for sku in page_skus %}
<li>
<a href="/detail/{{ sku.id }}/">
<img src="{{ sku.default_image.url }}">
</a>
<h4>{{ sku.name }}</h4>
<div class="operate">
<span class="price">¥{{ sku.price }}</span>
<span class="unit">台</span>
</div>
</li>
{% endfor %}
</ul>
<div class="pagenation">
<div id="pagination"></div>
</div>
</div>
</div>
<script src="{{ static('js/jquery.pagination.min.js') }}"></script>
<script>
$('#pagination').pagination({
currentPage: {{ page_num }},
totalPage: {{ total_page }},
callback: function (current) {
location.href = '/list/{{ category.id }}/' + current + '/?sort={{ sort }}';
}
});
</script>
<!-- ⭐ 最关键:Vue 逻辑最后加载 -->
<script src="{{ static('js/list.js') }}"></script>
</body>
</html>
在 static/js/list.js 里确认有热销逻辑
var vm = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
hots: []
},
mounted() {
this.get_hots();
},
methods: {
// 获取热销商品数据
get_hot_goods(){
var url = this.host + '/hot/' + this.category_id + '/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
this.hots = response.data.hot_skus;
for (var i = 0; i < this.hots.length; i++) {
this.hots[i].url = '/goods/' + this.hots[i].id + '.html';
}
})
.catch(error => {
console.log(error.response);
})
}
}
});
测试

商品搜索
简介
Elasticsearch(ES) 是一个 基于 Lucene 的分布式搜索与分析引擎
Elasticsearch = 专门用来“搜东西”的数据库,在海量数据中,极快地完成“全文搜索 + 多条件过滤 + 排序 + 统计聚合”
核心优势
1、搜索特别快(比 MySQL 快很多)
- MySQL 用
LIKE %关键词%,慢 - ES 用搜索引擎级别索引,秒出结果
2、支持“人说的话”搜索(中文分词)
比如用户输入:“索尼 微单 相机”
ES 能自动拆成:
- 索尼
- 微单
- 相机
3、搜索结果更“聪明”
谁更相关,谁排前面
可以按下面字段排序:
- 销量
- 价格
- 新品
4、搜索 + 筛选一起做
一次搜索就能完成:
- 关键词搜索
- 分类筛选
- 品牌筛选
- 价格区间
- 规格筛选
5、数据多了也不怕
- 商品 1 万、10 万、100 万
- ES 都能扛住
- 天生支持分布式
Django 商城中,ES 的典型用法
1️⃣ 用户输入关键词
2️⃣ Django 调 Elasticsearch 搜索
3️⃣ ES 返回商品 ID 列表
4️⃣ Django 再查 MySQL 拿完整商品数据
例如:用户搜索:“iphone 15”
ES 做的事:
- 搜商品名、描述
- 排序(销量高的在前)
- 返回 SKU id:
[101, 205, 330]
MySQL 做的事:
- 根据这些 id
- 查价格、库存、图片
- 返回给前端
用户
↓
Django View
↓
Elasticsearch(搜索)
↓
返回 SKU ID 列表
↓
MySQL(补充价格、库存)
Django 项目如何引入 Elasticsearch?
- 第 1 步:Docker 启动 Elasticsearch
- 第 2 步:Django 接入 ES
- 第 3 步:为
goods.SKU建搜索索引 - 第 4 步:写一个最简单搜索接口
- 第 5 步:Celery 同步 ES/商品上下架自动更新索引/搜索排序 / 筛选
全文检索 Elasticsearch 接入
第 1 步:用 Docker 跑 Elasticsearch
在 docker-compose/ 下新增文件:docker-compose.es.yml
version: "3.8"
services:
elasticsearch:
image: docker.elastic.co/elasticsearch/elasticsearch:8.15.3
container_name: meiduo-es
environment:
- discovery.type=single-node
- xpack.security.enabled=false
- xpack.security.http.ssl.enabled=false
- ES_JAVA_OPTS=-Xms512m -Xmx512m
ports:
- "9200:9200"
volumes:
- esdata:/usr/share/elasticsearch/data
volumes:
esdata:
启动:
cd docker-compose
docker compose -f docker-compose.es.yml up -d
验证:
curl http://127.0.0.1:9200

能返回集群信息就 OK。
第 2 步:安装 Django 侧依赖
虚拟环境里执行:
pip install "elasticsearch>=8,<9" "django-elasticsearch-dsl>=8,<9"
(django-elasticsearch-dsl 会依赖 elasticsearch-dsl)
第 3 步:改 settings.py(只加几行)
INSTALLED_APPS 加两项
INSTALLED_APPS = [
# ...
"django_elasticsearch_dsl",
"django_elasticsearch_dsl.registries",
# ...
]
增加 ES 连接配置(建议放 settings.py 底部)
ELASTICSEARCH_DSL = {
"default": {"hosts": "http://127.0.0.1:9200"}
}
# 索引自动刷新频率(开发环境)
ELASTICSEARCH_DSL_AUTOSYNC = False # 先关掉,后面我们用命令重建/手动触发更可控
先把 autosync 关掉:避免你调试阶段改一个 SKU 就触发信号导致“莫名其妙 ES 报错”。
第 4 步:在 goods 里新增 documents.py(索引定义)
新建:apps/goods/documents.py
from django_elasticsearch_dsl import Document, Index, fields
from django_elasticsearch_dsl.registries import registry
from .models import SKU
# 索引名(建议带环境前缀)
sku_index = Index("meiduo_skus")
# 可选:索引级设置(先极简)
sku_index.settings(number_of_shards=1, number_of_replicas=0)
@registry.register_document
class SKUDocument(Document):
# ======================
# 关联字段(用于搜索 / 过滤 / 展示)
# ======================
# SPU 名称(用于搜索)
spu_name = fields.TextField(attr="spu.name")
# 品牌
brand_id = fields.IntegerField(attr="spu.brand_id")
brand_name = fields.TextField(attr="spu.brand.name")
# 分类
category_id = fields.IntegerField(attr="category_id")
# 图片(⚠️ 不能直接用 attr="default_image",要转成字符串)
default_image = fields.KeywordField()
class Index:
name = "meiduo_skus"
class Django:
model = SKU
# 直接从 SKU 模型中读取的字段(基础类型,ES 可序列化)
fields = [
"id",
"name",
"caption",
"price",
"sales",
"is_launched",
"create_time",
]
# ======================
# 只索引“已上架”的商品
# ======================
def get_queryset(self):
return super().get_queryset().filter(is_launched=True)
# ======================
# 自定义字段序列化
# ======================
def prepare_default_image(self, instance):
"""
Elasticsearch 里只能存 JSON 基础类型
ImageFieldFile 必须转成字符串路径
"""
if instance.default_image:
return str(instance.default_image)
return ""
第 5 步:生成索引 + 灌数据(最关键的跑通点)
django-elasticsearch-dsl库已安装,调用库执行:
python manage.py search_index --rebuild
如果你 MySQL 里有 SKU 数据,这一步会:
- 找到你项目中注册的 apps/goods/documents.py
- 删除旧索引
- 按 Document 定义重新创建索引
- 从 Django ORM 批量导入数据,从 MySQL 读 SKU,序列化成 dict,把上架 SKU 全部写入 ES

验证一下 ES 有数据:
curl "http://127.0.0.1:9200/meiduo_skus/_count?pretty"

简单搜索测试
http://127.0.0.1:9200/meiduo_skus/_search?q=Apple&pretty

第 6 步:写一个最简单的搜索接口
在 apps/goods/views.py 末尾加
from django.views import View
from django.shortcuts import render
from elasticsearch_dsl import Search
from .documents import SKUDocument
class SKUSimpleVO:
"""
SKU 展示用 View Object(不依赖 ORM,不依赖 ES)
"""
def __init__(self, *, id, name, price, default_image):
self.id = id
self.name = name
self.price = price
self.default_image = default_image
from django.conf import settings
class SearchView(View):
"""
商品搜索页
GET /search/?q=xxx&page=1
"""
def get(self, request):
keyword = request.GET.get("q", "").strip()
page = int(request.GET.get("page", 1))
page_size = 6
skus = []
total = 0
if keyword:
s = Search(index=SKUDocument.Index.name)
s = s.query(
"multi_match",
query=keyword,
fields=["name^3", "caption^2", "spu_name", "brand_name"],
)
start = (page - 1) * page_size
s = s[start:start + page_size]
response = s.execute()
total = response.hits.total.value
for hit in response:
skus.append(
SKUSimpleVO(
id=hit.id,
name=hit.name,
price=hit.price,
default_image=settings.MEDIA_URL + str(hit.default_image),
)
)
context = {
"keyword": keyword,
"skus": skus,
"total": total,
"page": page,
}
return render(request, "search.html", context)
在 apps/goods/urls.py 加路由
from django.urls import path
from .views import ListView, SearchView
urlpatterns = [
path('list/<int:category_id>/<int:page_num>/', views.ListView.as_view(), name='list'),
...
path("search/", SearchView.as_view(), name='search'), # 新增
]
第 7 步:让 templates/search.html 页面真正用上搜索
templates/search.htmltemplates/search.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/jquery.pagination.css">
<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/host.js"></script>
<script type="text/javascript" src="../static/js/jquery-1.12.4.min.js"></script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
{% if request.user.is_authenticated %}
<div class="login_btn fl">
欢迎您:<em>{{ request.user.username }}</em>
<span>|</span>
<a href="/logout/" class="quit">退出</a>
</div>
{% else %}
<div class="login_btn fl">
<a href="../login.html">登录</a>
<span>|</span>
<a href="../register.html">注册</a>
</div>
{% endif %}
<div class="user_link fl">
<span>|</span>
<a href="/info/">用户中心</a>
<span>|</span>
<a href="/carts/">我的购物车</a>
<span>|</span>
<a href="/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="/static" 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" value="{{ keyword }}" placeholder="搜索商品">
<input type="submit" class="input_btn fr" 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="main_wrap clearfix">
<div class="clearfix">
<ul class="goods_type_list clearfix">
{% for sku in skus %}
<li>
<a href="/detail/{{ sku.id }}/">
<!-- 关键:SearchView 已经把 sku.default_image 拼成完整 URL,这里直接用 -->
<img src="{{ sku.default_image }}">
</a>
<h4>
<a href="/detail/{{ sku.id }}/">{{ sku.name }}</a>
</h4>
<div class="operate">
<span class="price">¥{{ sku.price }}</span>
<span class="unit">台</span>
<a href="#" class="add_goods" title="加入购物车"></a>
</div>
</li>
{% endfor %}
{% if not skus %}
<li style="padding:20px;">没有搜索到相关商品</li>
{% endif %}
</ul>
<div class="pagenation">
<div id="pagination" class="page"></div>
</div>
</div>
</div>
<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 © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
</div>
<script type="text/javascript" src="../static/js/common.js"></script>
<script type="text/javascript" src="../static/js/jquery.pagination.min.js"></script>
<script>
$(function () {
// ✅ 用 Jinja2 更安全的兜底写法(即使 total/page 没传也不会炸)
var total = {{ total if total is not none else 0 }};
var pageSize = 6;
var totalPage = Math.ceil(total / pageSize);
var currentPage = {{ page if page is not none else 1 }};
var keyword = "{{ keyword }}";
if (totalPage > 1) {
$('#pagination').pagination({
currentPage: currentPage,
totalPage: totalPage,
callback: function (current) {
location.href = '/search/?q=' + encodeURIComponent(keyword) + '&page=' + current;
}
});
}
});
</script>
</body>
</html>
测试:
浏览器直接访问:
http://127.0.0.1:8000/search/?q=Apple

搜索框测试,打开 http://127.0.0.1:8000/search/ ,输入关键字比如:MacBook

无结果测试:

分页测试

高亮优化(可选)
现在的 SearchView 是这样的结构
s = Search(index=SKUDocument.Index.name)
s = s.query("multi_match", ...)
response = s.execute()
for hit in response:
skus.append(
SKUSimpleVO(
id=hit.id,
name=hit.name,
...
)
)
目前:
hit.name是 普通字符串- ES 并没有返回高亮字段
第一步:在 ES 查询中开启
apps/goods/views.py 的 SearchView 中,在 s = s.query(...) 后面,加上高亮配置
s = Search(index=SKUDocument.Index.name)
s = s.query(
"multi_match",
query=keyword,
fields=["name^3", "caption^2", "spu_name", "brand_name"],
)
# 高亮配置(新增)
s = s.highlight(
'name',
'caption',
pre_tags=['<span style="color:red">'],
post_tags=['</span>']
).highlight_options(require_field_match=False)
第二步:优先使用 ES 返回的高亮字段
修改 SearchView 中构造 SKUSimpleVO 的地方
skus.append(
SKUSimpleVO(
id=hit.id,
name=hit.name,
price=hit.price,
default_image=settings.MEDIA_URL + str(hit.default_image),
)
)
改成:
# 优先用高亮结果
if hasattr(hit.meta, "highlight") and "name" in hit.meta.highlight:
name = hit.meta.highlight.name[0]
else:
name = hit.name
skus.append(
SKUSimpleVO(
id=hit.id,
name=name,
price=hit.price,
default_image=settings.MEDIA_URL + str(hit.default_image),
)
)
第三步:模板中允许 HTML 生效
现在模板里是:
<a href="/detail/{{ sku.id }}/">{{ sku.name }}</a>
<span> 会被当成普通文本,改成
<a href="/detail/{{ sku.id }}/">{{ sku.name | safe }}</a>
加到 static/css/main.css 最底部
/* ===============================
搜索关键词高亮(Elasticsearch)
=============================== */
.hl {
color: red;
font-weight: bold;
}
测试:

统计分类商品访问量
触发“访问统计”
访问“分类列表页”就算一次访问
apps/goods/views.py
在 ListView.get() 里,开头将分类、面包屑与新增的访问统计结合,改为这一段
from django.utils import timezone
from django.db.models import F
# 一、分类 & 访问统计 & 面包屑
try:
category = GoodsCategory.objects.get(id=category_id)
except GoodsCategory.DoesNotExist:
return render(request, "list.html", {"errmsg": "分类不存在"})
today = timezone.localdate()
obj, created = GoodsVisitCount.objects.get_or_create(
category=category,
date=today,
defaults={'count': 1} # 首次插入只能是确定值
)
if not created:
GoodsVisitCount.objects.filter(
category=category,
date=today
).update(count=F('count') + 1)
breadcrumb = get_breadcrumb(category)
打开一个分类列表页:http://127.0.0.1:8000/list/115/1/

数据库验证
SELECT * FROM tb_goods_visit ORDER BY date DESC;
刷新页面 → count +1

发布者:LJH,转发请注明出处:https://www.ljh.cool/44616.html