当前位置: 首页 > news >正文

Django 从 0 到 1 打造完整电商平台:商品排序与浏览量统计

IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我也会在其它平台发布最新文章助你少走弯路。前面几篇我们让用户能按分类浏览商品、用关键词搜索商品商品发现的能力已经基本齐了。但还有一个关键体验没做排序。用户在列表页不能只按“最新发布”看还需要能按“价格从低到高”、“销量最高”、“最多人在看”来筛选这样才像一个成熟的电商平台。今天我们就一口气完成两件事商品排序功能价格升降、销量、浏览量、综合排序浏览量统计每次进入商品详情页该 SKU 的浏览量自动 1。这两块结合既能让用户灵活筛选也能为“热门商品”提供数据基础。全程用 Django ORM 和 F 表达式零第三方依赖。一、需求分析1.1 排序需求在商品列表页增加一个排序下拉菜单选项包括排序可以和分类、搜索叠加分页链接也要保留排序参数。1.2 浏览量统计需求用户每次访问 SKU 详情页通过 SPU 详情页切换到某个 SKU 暂不算我们统计的是进入 SPU 详情页即算 SPU 下所有 SKU通常统计 SPU 详情页的浏览量或 SKU 维度。我们选择在 SPU 详情页视图被调用时给该 SPU 下的所有活动 SKU 的浏览量 1。这样能反映产品的热度又不至于太细粒度。或者更合理的是SPU 详情页进入时默认展示的 SKU第一个浏览量 1。但我们可以在访问时给当前 spu 的所有 skus 的 views 都加 1简单粗暴后续可改成针对具体被展示的 SKU。考虑到产品级热度我们给 SPU 下所有 is_active 的 SKU 都加 1便于排序时按 SPU 热度排。使用F(views) 1原子更新避免竞态条件。二、模型回顾views 字段已就位在第 11 篇我们已经给SKU模型添加了views字段class SKU(models.Model):# ...viewsmodels.PositiveIntegerField(default0,verbose_name浏览量)确保该字段存在且迁移已完成。如果没有请执行makemigrations和migrate。三、改造商品列表视图支持排序打开apps/products/views.py找到sku_list视图。在已有的搜索和分类过滤之后、分页之前插入排序逻辑。def sku_list(request): skusSKU.objects.filter(is_activeTrue).select_related(spu__category).prefetch_related(images)# 1. 搜索过滤保持不变queryrequest.GET.get(q,).strip()ifquery: skusskus.filter(Q(name__icontainsquery)|Q(spu__name__icontainsquery)|Q(spu__brand__icontainsquery)|Q(spu__desc__icontainsquery))# 2. 分类过滤保持不变category_idrequest.GET.get(category_id)current_categoryNoneifcategory_id: try: categoryCategory.objects.get(pkcategory_id,is_activeTrue)category_ids[category.id]childrenCategory.objects.filter(parentcategory,is_activeTrue)category_ids.extend(children.values_list(id,flatTrue))skusskus.filter(spu__category_id__incategory_ids)current_categorycategory except Category.DoesNotExist: skusskus.none()# 3. 排序新增sort_optionrequest.GET.get(sort,-create_time)# 默认按发布时间降序valid_sorts{price_asc:price,price_desc:-price,sales_desc:-sales,views_desc:-views,-create_time:-create_time,# 默认}ifsort_optioninvalid_sorts: skusskus.order_by(valid_sorts[sort_option])else: sort_option-create_timeskusskus.order_by(-create_time)# 4. 分页保持不变from django.core.paginatorimportPaginator, EmptyPage, PageNotAnInteger paginatorPaginator(skus,12)page_numberrequest.GET.get(page,1)try: page_objpaginator.page(page_number)except PageNotAnInteger: page_objpaginator.page(1)except EmptyPage: page_objpaginator.page(paginator.num_pages)# 5. 顶级分类top_categoriesCategory.objects.filter(parent__isnullTrue,is_activeTrue).order_by(sort)context{page_obj:page_obj,current_category:current_category,top_categories:top_categories,query:query,sort_option:sort_option,# 传递给模板}returnrender(request,products/sku_list.html, context)四、改造商品列表模板加入排序选择编辑apps/products/templates/products/sku_list.html。在“共 X 件商品”的右侧或下方添加排序下拉菜单。修改顶部标题栏部分divclassd-flex justify-content-between align-items-center mb-3h3{%ifquery %} 搜索 “{{query}}”{%elifcurrent_category %}{{current_category.name}}{%else%}️ 全部商品{% endif %}/h3divclassd-flex align-items-centerspanclasstext-muted me-3共{{page_obj.paginator.count}}件/span!-- 排序下拉 --divclassdropdownbuttonclassbtn btn-outline-secondary btn-sm dropdown-toggletypebuttonidsortDropdown>dropdown排序{%ifsort_optionprice_asc%}价格 ↑{%elifsort_optionprice_desc%}价格 ↓{%elifsort_optionsales_desc%}销量 ↓{%elifsort_optionviews_desc%}浏览量 ↓{%else%}最新{% endif %}/buttonulclassdropdown-menuliaclassdropdown-item {% if sort_option -create_time %}active{% endif %}href?sort-create_time{% if query %}q{{ query }}{% endif %}{% if current_category %}category_id{{ current_category.id }}{% endif %}最新/a/liliaclassdropdown-item {% if sort_option price_asc %}active{% endif %}href?sortprice_asc{% if query %}q{{ query }}{% endif %}{% if current_category %}category_id{{ current_category.id }}{% endif %}价格从低到高/a/liliaclassdropdown-item {% if sort_option price_desc %}active{% endif %}href?sortprice_desc{% if query %}q{{ query }}{% endif %}{% if current_category %}category_id{{ current_category.id }}{% endif %}价格从高到低/a/liliaclassdropdown-item {% if sort_option sales_desc %}active{% endif %}href?sortsales_desc{% if query %}q{{ query }}{% endif %}{% if current_category %}category_id{{ current_category.id }}{% endif %}销量从高到低/a/liliaclassdropdown-item {% if sort_option views_desc %}active{% endif %}href?sortviews_desc{% if query %}q{{ query }}{% endif %}{% if current_category %}category_id{{ current_category.id }}{% endif %}浏览量从高到低/a/li/ul/div/div/div同时分页链接也需要保留sort参数。将分页中所有的href修改为携带当前sort、query和category_id。例如aclasspage-linkhref?page{{ page_obj.previous_page_number }}{% if sort_option %}sort{{ sort_option }}{% endif %}{% if query %}q{{ query }}{% endif %}{% if current_category %}category_id{{ current_category.id }}{% endif %}« 上一页/a其他页码链接同理。五、实现浏览量统计打开apps/products/views.py找到spu_detail视图。进入该页时将该 SPU 下所有上架 SKU 的views字段原子加 1。from django.db.modelsimportF def spu_detail(request, spu_id): spuget_object_or_404(SPU.objects.prefetch_related(skus__images),pkspu_id)skusspu.skus.filter(is_activeTrue)# 浏览量统计所有上架的 SKU 浏览量 1skus.update(viewsF(views)1)# 聚合所有规格维度specs_data{}forskuinskus:forkey, valueinsku.specs.items():ifkey notinspecs_data: specs_data[key]set()specs_data[key].add(value)specs_list{k: list(v)fork,vinspecs_data.items()}default_skuskus.first()returnrender(request,products/spu_detail.html,{spu:spu,skus:skus,specs:specs_list,default_sku:default_sku,})skus.update(viewsF(views) 1)会生成一条 SQLUPDATE tb_sku SET views views 1 WHERE ...原子操作不会产生竞态。注意在update之后skus的 QuerySet 内部缓存已经过期但我们后面仍遍历skus因此需要重新获取实际上update执行后之前的skus查询集的_result_cache会失效Django 在遍历时会重新查询所以没问题。但为了保险可以在update后重新取一次比如skus spu.skus.filter(is_activeTrue)然后在重新取后再构建 specs 数据。调整如下def spu_detail(request, spu_id): spuget_object_or_404(SPU.objects.prefetch_related(skus__images),pkspu_id)# 浏览量 1SPU.objects.filter(pkspu_id).update(viewsF(views))# 不views在SKU上# 正确写法skus_querysetspu.skus.filter(is_activeTrue)skus_queryset.update(viewsF(views)1)# 重新获取因为 update 后之前加载的 skus 对象不带更新后的 viewsskuslist(spu.skus.filter(is_activeTrue).prefetch_related(images))# ... 后续 specs 聚合和默认 sku 使用 skus为避免混淆我们直接在update后重新查询 skus然后进行 specs 聚合def spu_detail(request, spu_id): spuget_object_or_404(SPU.objects.prefetch_related(skus__images),pkspu_id)# 浏览量 1对所有上架 SKUactive_skusspu.skus.filter(is_activeTrue)active_skus.update(viewsF(views)1)# 重新获取更新后的 SKU 列表skusspu.skus.filter(is_activeTrue).prefetch_related(images)specs_data{}forskuinskus:forkey, valueinsku.specs.items():ifkey notinspecs_data: specs_data[key]set()specs_data[key].add(value)specs_list{k: list(v)fork,vinspecs_data.items()}default_skuskus.first()# ... render这样能保证模板中显示的浏览量是最新的。六、测试流程与输出启动服务器python manage.py runserver6.1 排序功能测试访问http://127.0.0.1:8000/products/list/默认按最新排序。点击排序下拉选择“价格从低到高”URL 变为/products/list/?sortprice_asc页面重新加载商品按价格升序排列。终端输出[25/May/202610:05:00]GET /products/list/?sortprice_asc HTTP/1.12009123再选“销量从高到低”URL 变为?sortsales_desc列表顺序变化。测试组合搜索与排序搜索“Phone”然后选“价格从高到低”URL 类似/products/list/?qPhonesortprice_desc。结果正确。测试分页携带排序参数点击第2页URL 包含sortprice_descpage2排序保持。6.2 浏览量统计测试进入某个 SPU 详情页例如 iPhone 15/products/spu/1/在浏览器中查看该页记录当前显示的销量和库存不变。通过命令行查看数据库确认浏览量增加python manage.py dbshell sqliteSELECT id, name, views FROM tb_sku WHERE spu_id1;每次刷新详情页views字段递增 1。刷新 3 次后结果示例1|iPhone15128GB 午夜色|32|iPhone15256GB 午夜色|35|iPhone15128GB 星光色|36|iPhone15256GB 星光色|3现在返回商品列表页选择“浏览量从高到低”排序?sortviews_desc多次刷新的商品会排在前面。终端输出多次访问[25/May/202610:10:12]GET /products/spu/1/ HTTP/1.120014256[25/May/202610:10:18]GET /products/spu/1/ HTTP/1.120014256[25/May/202610:10:22]GET /products/spu/1/ HTTP/1.120014256每次请求都成功数据库更新无异常。6.3 高并发下的原子性验证可选可以通过ab或简单多线程脚本模拟多个并发请求但这里不展开。F表达式保证了原子性不会有计数丢失。七、细节优化与注意事项去重统计当前简单地对每次进入详情页都 1同一个用户反复刷会虚增浏览量。如果需要更精确的统计可以记录用户 cookie 或 session 去重但会引入额外复杂度。本系列作为入门项目先采用简单方案后续优化篇第 24 篇可结合 Redis 做去重计数。排序性能所有排序字段price, sales, views, create_time都有索引吗sales和views我们没有建索引但数据量小的时候没问题。第 26 篇会讲解如何为经常排序的字段加索引。默认排序我们保留了-create_time作为缺省排序这是电商常用做法。八、总结与下集预告今天我们为商品列表加上了灵活动态的排序功能并实现了浏览量统计让商品数据维度更加丰富。主要收获使用 Django ORM 的order_by结合 URL 参数实现多维排序使用F表达式原子更新浏览量避免竞态排序参数通过模板标签在各链接间正确传递。现在用户不仅能搜、能分类还能按价格、销量、热度排序商品模块的功能已经基本完善。下一步我们将迎来电商的核心——购物车。下一篇第 16 篇我会带大家分析购物车的实现方式、设计模型并在第 17 篇实现完整的购物车增删改查。想了解更多也可以在其它平台搜索「IT策士」一起升级 IT 思维 本文为《Django 从 0 到 1 打造完整电商平台》系列第 15 篇。
http://www.rkmt.cn/news/1372418.html

相关文章:

  • 【无人机三维路径规划】基于circle序列和正余弦策略的APO和CO算法无人机集群路径规划附Matlab代码
  • DeepSeek配额策略失效的7个静默信号(第5个90%工程师都忽略),立即执行这1次curl诊断脚本保生产稳定
  • ChatGPT路演PPT背后的资本语言学:用BERT模型分析217份AI融资材料,发现高过会率PPT共有的8个动词密度阈值
  • 2026GEO公司哪家好:全球AI搜索流量迁移与大模型认知主权争夺战 - GEO优化
  • Solid.js信号驱动架构深度解析:告别虚拟DOM的真正实践
  • 如何用GHelper实现华硕笔记本性能与静音的完美平衡
  • 后端架构技术01-「10万并发压垮线程池?Project Loom虚拟线程:一个线程几KB,轻松扛住流量洪峰」
  • Taotoken的API Key管理与审计日志功能实践体验
  • 昇腾NPU的算子公共平台,实现M×N算子复用
  • 火盾声学材料:安庆地区防火吸音板综合解决方案,玻纤吸音板/演播厅空间吸声体/布艺软包吸音板,防火吸音板源头厂家有哪些 - 品牌推荐师
  • 论文初稿被批太水?青年教师力荐这几个AI论文写作软件
  • JavaScript 比较
  • 今天不用就过期:Gemini深度研究模式2024Q3权限变更预警——3类高价值功能即将对免费用户关闭
  • 洛谷 P11398
  • 5月20号
  • 如何解锁索尼相机的隐藏功能:OpenMemories-Tweak完整指南
  • 日志爆炸时代如何不被淹没?DeepSeek智能分析方案全链路实操,含Prometheus+Loki+DeepSeek三端联调手册
  • Java学习笔记:多态
  • ChatGPT记忆功能安全风险预警,3大数据泄露漏洞已验证(附GDPR/等保2.0合规配置清单)
  • C++的STL
  • DLSS Swapper深度解析:如何实现跨平台游戏DLSS版本智能管理
  • 【优化调度】基于改进遗传算法求解带时间窗约束多卫星任务规划附Matlab代码
  • 2026年5月有实力的一体化污水提升泵站/一体化泵站厂家推荐河北铄康环保设备有限公司,水质适应性广各类浑浊污水均可稳定输送处理 - 品牌鉴赏师
  • 溧阳沙发翻新换皮换布面靠谱商家优选推荐|匠阁沙发翻新、御匠沙发翻新、锦修沙发翻新三大品牌、全品类沙发翻新换皮换布一站式服务 - 卓信营销
  • 建立在不同的基线模型上,GAT,GCN,和GIN
  • 2026年5月优秀的EPS外墙装饰/EPS装饰线条厂家推荐丰县建鑫泡沫制品有限公司,雕花构件定制打造建筑独特标识 - 品牌鉴赏师
  • 2026长岛民宿排名指南,长岛海东渔家民宿没白来! - 资讯纵览
  • 英语 听力 重读软件app
  • AI写作辅助平台8款AI写作辅助软件梯队榜,毕业护航!
  • 权威测评!2026年顶尖AI论文写作软件榜单,高质初稿轻松写