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

Django 从 0 到 1 打造完整电商平台:商品分类与 SPU/SKU 设计

IT策士 10余年一线大厂经验专注 IT 思维、架构、职场进阶。我会在公众号、今日头条持续发布最新文章助你少走弯路。从本篇开始我们告别用户模块正式进入电商的核心——商品模块。前 10 篇我们打好了地基用户注册登录、个人中心、地址管理、权限体系都已就位。现在是时候让“货”登场了。今天的内容偏“设计思路 代码落地”我会先帮你彻底搞懂 SPU 和 SKU 的概念区别然后基于第 2 篇已经建好的模型实现商品分类的树形展示和SPU 详情页的 SKU 规格切换。数据库里早已有数据我们要做的就是把它们搬上页面。一、搞懂 SPU 与 SKU——电商新手最容易混淆的概念很多初学者会把“商品”当成一个东西但在电商系统中至少要拆成两层一句话总结SPU 是“同款”SKU 是“同款的不同配置”。为什么要这样分因为价格、库存、图片都是跟着 SKU 走的。128GB 和 256GB 的 iPhone 15 价格不同库存不同甚至主图都可能不同。如果把它们都放在一张表里那表结构会非常混乱。在我们的模型设计中第 2 篇SPU表存产品名称、品牌、描述、所属分类SKU表存具体规格JSON 字段、价格、成本价、库存、销量、上下架状态SKU通过外键关联SPU一个 SPU 可以有多个 SKU。二、模型回顾与优化在第 2、3 篇中我们已经完成了模型定义和索引优化。为了今天的开发先回顾一下关键字段。apps/products/models.py核心结构class Category(models.Model): namemodels.CharField(max_length50)parentmodels.ForeignKey(self,on_deletemodels.CASCADE,nullTrue,blankTrue,related_namechildren)levelmodels.PositiveSmallIntegerField(default1)sortmodels.PositiveIntegerField(default0)is_activemodels.BooleanField(defaultTrue)class SPU(models.Model): namemodels.CharField(max_length100)brandmodels.CharField(max_length50,nullTrue,blankTrue)descmodels.TextField(nullTrue,blankTrue)categorymodels.ForeignKey(Category,on_deletemodels.PROTECT,related_namespus)class SKU(models.Model): spumodels.ForeignKey(SPU,on_deletemodels.CASCADE,related_nameskus)namemodels.CharField(max_length500,default)specsmodels.JSONField(defaultdict)pricemodels.DecimalField(max_digits10,decimal_places2)cost_pricemodels.DecimalField(max_digits10,decimal_places2,nullTrue,blankTrue)stockmodels.PositiveIntegerField(default0)salesmodels.PositiveIntegerField(default0)is_activemodels.BooleanField(defaultTrue)class ProductImage(models.Model): skumodels.ForeignKey(SKU,on_deletemodels.CASCADE,related_nameimages)imagemodels.ImageField(upload_toproducts/%Y/%m/)is_mainmodels.BooleanField(defaultFalse)sortmodels.PositiveIntegerField(default0)今天我们还会增加一个浏览量字段为后面的排序和热度统计做准备。三、给 SKU 增加浏览量字段在SKU模型中增加viewsmodels.PositiveIntegerField(default0,verbose_name浏览量)然后生成并执行迁移python manage.py makemigrations products python manage.py migrate控制台输出Migrationsforproducts:apps/products/migrations/0003_sku_views.py - Add field views to sku Running migrations: Applying products.0003_sku_views... OK这个字段将用于后续的商品排序按热度和详情页浏览统计。四、商品分类——树形结构的获取与展示4.1 需求分析电商首页通常有分类导航形如电子产品 ├── 手机 └── 电脑 服装 ├── 男装 └── 女装我们的Category模型通过parent字段实现了无限级分类。要在页面上展示树形结构有两种方式一次性查出所有分类在 Python 里构建树适合分类数量不多的情况。逐层查询适合层级很深的情况。电商分类一般两级就够了我们用第一种方式。4.2 编写视图编辑apps/products/views.py新创建from django.shortcutsimportrender from .modelsimportCategory def category_tree(request): 获取所有顶级分类并在模板中通过 children 属性递归渲染 top_categoriesCategory.objects.filter(parent__isnullTrue,is_activeTrue).prefetch_related(children__children).order_by(sort)returnrender(request,products/category_tree.html,{categories:top_categories})prefetch_related(children__children)一次性把两层子分类都查出来避免模板中多次查询N1 问题。如果你的分类只有一级子类prefetch_related(children)就够。4.3 配置 URL在apps/products/下创建urls.pyfrom django.urlsimportpath from.importviews app_nameproductsurlpatterns[path(categories/, views.category_tree,namecategory_tree),]然后在项目django_ecommerce/urls.py中 includeurlpatterns[# ... 其他路由 ...path(products/, include(apps.products.urls)),]4.4 模板递归渲染分类树创建apps/products/templates/products/category_tree.html{% extendsbase.html%}{% block title %}商品分类{% endblock %}{% block content %}h3classmb-4 商品分类/h3{%forcatincategories %}divclasscard mb-3 shadow-smdivclasscard-header bg-lighth5classmb-0ahref#classtext-decoration-none{{cat.name}}/a/h5/div{%ifcat.children.all %}divclasscard-bodydivclassrow{%forchildincat.children.all %}divclasscol-md-3 col-sm-6 mb-2ahref#classbtn btn-outline-primary btn-sm w-100{{child.name}}/a/div{% endfor %}/div/div{% endif %}/div{% empty %}pclasstext-muted暂无分类数据。/p{% endfor %}{% endblock %}虽然用prefetch_related预取了子分类模板里仍然写cat.children.allDjango 会优先从缓存中读取不会产生额外查询。测试访问http://127.0.0.1:8000/products/categories/你应该看到两个顶级分类电子产品、服装其下各自有子分类按钮。终端输出[23/May/2026 09:10:22]GET /products/categories/ HTTP/1.12003256五、SPU 详情页——SKU 规格切换这是电商产品页的核心体验。用户进入一个 SPU 页面能看到该产品的所有 SKU如不同颜色、不同内存点击规格按钮切换价格、库存和主图。5.1 需求拆解页面顶部展示 SPU 的基本信息名称、品牌、描述。规格选择区将同一 SPU 下的所有 SKU 的specs数据聚合展示可选的规格维度如颜色、内存。默认选中第一个有效 SKU展示其价格、库存。用户点击规格组合后通过 JavaScript 查找匹配的 SKU 并更新显示。图片区域显示当前选中 SKU 的主图后续第 13 篇扩展。5.2 视图继续在apps/products/views.py中添加from django.shortcutsimportrender, get_object_or_404 from .modelsimportSPU, SKU def spu_detail(request, spu_id): spuget_object_or_404(SPU.objects.prefetch_related(skus__images),pkspu_id)skusspu.skus.filter(is_activeTrue)# 聚合所有规格维度颜色、内存等及其可选值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()}# 默认选中第一个 SKUdefault_skuskus.first()returnrender(request,products/spu_detail.html,{spu:spu,skus:skus,specs:specs_list,default_sku:default_sku,})5.3 URL在apps/products/urls.py中添加path(spu/int:spu_id/, views.spu_detail,namespu_detail),5.4 模板创建apps/products/templates/products/spu_detail.html{% extendsbase.html%}{% load static %}{% block title %}{{spu.name}}{% endblock %}{% block content %}divclassrow!-- 图片区暂时占位第13篇完善 --divclasscol-md-5divclasscard shadow-smdivclasscard-body text-center{%ifdefault_sku.images.first %}imgsrc{{ default_sku.images.first.image.url }}classimg-fluidalt{{ default_sku.name }}{%else%}imgsrc{% static images/placeholder.png %}classimg-fluidalt暂无图片{% endif %}/div/div/div!-- 商品信息区 --divclasscol-md-7h2classfw-bold{{spu.name}}/h2{%ifspu.brand %}pclasstext-muted品牌{{spu.brand}}/p{% endif %}pclasstext-muted{{spu.desc}}/p!-- 价格区 --divclassmy-3spanclassfs-3 text-danger fw-boldidsku-price¥{{default_sku.price}}/span{%ifdefault_sku.cost_price %}spanclasstext-muted text-decoration-line-through ms-2¥{{default_sku.cost_price}}/span{% endif %}/div!-- 库存 --pclassmb-1库存spanidsku-stock{{default_sku.stock}}/span件/ppclassmb-3销量{{default_sku.sales}}件/p!-- 规格选择区 --dividspecs-area{%forkey, valuesinspecs.items %}divclassmb-3labelclassform-label fw-bold{{key}}/labeldivclassbtn-group>{{ key }}{%forvalinvalues %}buttontypebuttonclassbtn btn-outline-secondary spec-btn {% if forloop.first %}active{% endif %}>{{ val }}{{val}}/button{% endfor %}/div/div{% endfor %}/div!-- 数量与购买按钮后续购物车用 --divclassmt-4buttonclassbtn btn-primary btn-lgidadd-to-cart-btndisabled加入购物车/buttonbuttonclassbtn btn-danger btn-lg ms-2disabled立即购买/button/div/div/div{% endblock %}{% block extra_js %}script// 将所有 SKU 数据传给前端 const skusData[{%forskuinskus %}{id:{{sku.id}}, specs:{{sku.specs|safe}}, price:{{ sku.price }}, stock:{{sku.stock}}, sales:{{sku.sales}}}{%ifnot forloop.last %},{% endif %}{% endfor %}];// 当前选中的规格letselectedSpecs{};// 初始化默认选中的规格 document.querySelectorAll(.spec-btn.active).forEach(btn{const keybtn.closest([data-spec-key]).dataset.specKey;selectedSpecs[key]btn.dataset.specValue;});// 根据选中规格匹配 SKUfunctionfindMatchingSku(){returnskusData.find(sku{returnObject.keys(selectedSpecs).every(key{returnsku.specs[key]selectedSpecs[key];});});}// 更新页面显示functionupdateDisplay(sku){if(sku){document.getElementById(sku-price).textContent¥ sku.price;document.getElementById(sku-stock).textContentsku.stock;}}// 绑定规格按钮点击事件 document.querySelectorAll(.spec-btn).forEach(btn{btn.addEventListener(click,function(){const groupthis.closest([data-spec-key]);const keygroup.dataset.specKey;const valuethis.dataset.specValue;// 切换同组按钮的 active 状态 group.querySelectorAll(.spec-btn).forEach(bb.classList.remove(active));this.classList.add(active);// 更新当前选中规格 selectedSpecs[key]value;// 匹配 SKU 并更新 const matchedfindMatchingSku();updateDisplay(matched);});});/script{% endblock %}前端逻辑解读后端把当前 SPU 下所有 SKU 以 JSON 数组的形式渲染到skusData变量中。用户点击规格按钮时selectedSpecs对象更新然后遍历skusData找到规格完全匹配的 SKU。匹配成功后实时更新价格和库存。加入购物车按钮暂时 disabled后续第 16、17 篇会激活。六、测试完整流程6.1 准备测试数据如果第 4 篇的初始化数据还在iPhone 15、MacBook Pro、纯棉 T 恤及其 SKU可以直接测试。如果没有重新执行python manage.py init_product_data6.2 测试分类页访问http://127.0.0.1:8000/products/categories/看到分类树。终端[23/May/2026 09:15:30]GET /products/categories/ HTTP/1.120032566.3 测试 SPU 详情页在 Admin 中找一个 SPU 的 ID比如 iPhone 15 的 ID 是 1。访问http://127.0.0.1:8000/products/spu/1/。页面展示标题“iPhone 15”品牌 Apple描述“Apple 最新款智能手机”价格 ¥5999.00默认第一个 SKU规格按钮组颜色午夜色、存储128GB / 256GB点击不同规格点击 256GB 按钮价格变为 ¥6999.00库存变为 50。再切回 128GB价格恢复 ¥5999.00。终端[23/May/2026 09:20:05]GET /products/spu/1/ HTTP/1.120048726.4 验证只有上架 SKU 参与展示进入 Admin 将某个 SKU如 iPhone 15 128GB 午夜色的is_active取消勾选并保存。重新访问 SPU 详情页默认 SKU 变为 256GB 版本且规格区 128GB 不再出现。控制台 SQL 验证dbshellSELECT id, name, is_active FROM tb_sku WHERE spu_id1;只显示is_active1的记录被视图查询到。七、总结与下集预告今天我们迈出了商品模块坚实的一步彻底理清了 SPU 与 SKU 的业务含义实现了商品分类的树形展示用prefetch_related优化查询创建了 SPU 详情页完成了规格切换的核心前端逻辑给 SKU 增加了浏览量字段为后续统计做准备。有了详情页下一步就是让更多商品能被用户看到。第 12 篇我们将实现商品列表页带分页包括按分类筛选、价格区间、分页导航让商城初具雏形。别走开明天继续想了解更多还可以去公众号、今日头条搜索「IT策士」一起升级 IT 思维 本文为《Django 从 0 到 1 打造完整电商平台》系列第 11 篇作者IT策士
http://www.zskr.cn/news/1361387.html

相关文章:

  • 终极指南:RDPWrap如何免费解锁Windows多用户远程桌面功能
  • QMCDecode:Mac用户专属的QQ音乐加密文件终极解密方案
  • API管理:五款平台的核心能力与关键指标
  • AI项目GPU选型策略:任务匹配、显存计算与TCO优化指南
  • 碳化硅衬底与器件:怎么分辨有真产能的原厂和贸易商
  • 【AI入门知识点】Harness 是什么?为什么 DeepSeek 要组建 Harness 团队?
  • C++虚函数与多态机制
  • 社交AI Agent不是Chatbot!5个被99%团队忽略的协议层设计陷阱(附LinkedIn/小红书级SDK接口规范)
  • Unity WebGL文本输入解决方案:DOM桥接与IME兼容架构
  • 2026年北京餐饮外卖打包盒厂家推荐:瀚隆包装为什么适合单店与连锁餐饮共同选择? - 企业深度横评dyy6420
  • Docker 日常操作笔记(开发最常用命令)
  • Docker 入门笔记(后端开发必学)
  • WzComparerR2完整指南:冒险岛游戏数据提取与可视化分析工具
  • 线路板清洁度萃取+分析全套设备实力厂家推荐,西恩士工业 - 工业设备研究社
  • 这次终于选对了!高效论文写作全流程AI论文网站推荐(2026 最新)
  • Python爬虫实战:爬取论文期刊 文献整理+管理表生成
  • MoE稀疏激活原理与工程落地实战
  • SSH安全加固:禁用弱加密算法的实操指南
  • 文件上传漏洞深度解析:从getshell到六维纵深防御
  • Linux服务器入侵排查实战:三层切片应急响应流程
  • LSTM为何在工业时序建模中不可替代?梯度消失与门控机制的工程真相
  • 5分钟搞定Windows 11安卓应用安装:WSA Toolbox完全指南
  • [Python实战] 路径、编码、解释器老出问题时,怎样把脚本环境一次性理顺?
  • 无监督跌倒检测:不依赖标注数据的实时异常建模方法
  • Mumu模拟器ADB连接Unity Profiler全攻略
  • 一天干完一百万字,谷歌 agy 这个工具简直是头不要命的洪水猛兽
  • DeepSeek总结的从 DuckDB 迁移到 chDB基准测试
  • OpenSSH PKCS#11双重释放漏洞深度解析与实战防护
  • SQL报错注入实战:MySQL/PostgreSQL/Oracle三库绕过与数据提取
  • CVE-2025-68493深度解析:OGNL沙箱坍塌与Java Web内网横向移动