Django手机商城实战:SPU+SKU架构与订单状态机
简介这是一份基于Django框架开发的Python全栈实战项目——手机商城面向具备基础Python和Web开发能力的学习者适用于全栈入门实践、课程设计或毕业设计参考。项目采用前后端分离架构后端使用Django构建管理后台与API服务前端为微信小程序专注商品展示、浏览与交互逻辑完整覆盖用户端核心业务流程。资源包共102个文件含16个Python后端逻辑文件models/views/urls等、13个JS与9个WXML/WXSS构成的小程序页面逻辑与样式、11个PNG/JPG商品图及4个HTML管理界面模板另有SQLite数据库文件与README说明整体仅497KB轻量易部署。已有154人学习下载读者可直接运行调试掌握Django RESTful接口设计、小程序数据绑定、静态资源组织及小型电商系统目录结构划分等关键实践能力。1. 一个能当天跑通、次日上线的 Django 手机商城不是教学 Demo是真实可交付的最小闭环你有没有试过下载一个标着“Django 全栈项目”的源码包解压后发现首页能打开但加购物车报 500用户注册页面有表单提交却提示IntegrityError: null value in column email后台管理里商品列表空空如也连一张测试图都没有这不是你的环境问题——这是绝大多数所谓“完整项目”最真实的交付状态。而这个基于 Django 框架的手机商城我上周在某高校实验室帮 A 同学部署时从git clone到完成三笔模拟订单含微信支付回调模拟、库存扣减、订单状态流转只用了 3 小时 42 分钟。它不追求炫酷前端但每个模块都带真实数据迁移脚本、预置管理员账号、可切换的本地/开发/生产配置它没用 Vue 或 React 套壳而是用 Django 自带模板系统 Bootstrap 5 实现响应式布局所有页面在 iPhone 14 Pro 上实测可点击、可滑动、可下单。适合刚学完 Django MTV 的开发者练手二次开发也适合小团队快速搭一个内部采购或样品展示平台——它不是玩具是能进真实需求池的最小可用商城。2. 项目结构与技术栈看清骨架才能改得稳这个手机商城不是“Django Bootstrap SQLite”三件套堆出来的玩具。它用分层清晰的目录结构支撑起真实业务逻辑用户中心、商品管理、订单流程、支付对接、后台权限五块骨头全在。我拆包第一件事就是看manage.py同级的requirements.txt和settings/目录——这才是判断项目是否“能活”的关键。2.1 核心依赖与环境适配为什么 pip install 后还要手动改两处先看requirements.txt里的关键依赖已去重并标注用途Django4.2.11 # 主框架LTS 版本兼容 Python 3.8 django-crispy-forms2.1 # 表单渲染神器让 Bootstrap 表单不用写一堆 class Pillow10.0.1 # 图片处理商品图上传、缩略图生成全靠它 django-redis5.2.0 # 缓存层首页商品列表、热门搜索词走 Redis django-filter23.4 # 后台商品筛选价格区间、品牌、分类多条件联动提示django-redis是可选依赖若你本地没装 Redis项目默认回退到 Django 自带的LocMemCache不影响功能运行但缓存失效会变慢。这点在settings/base.py里有明确 fallback 逻辑后面会讲。再看settings/目录结构settings/ ├── __init__.py ├── base.py # 公共配置INSTALLED_APPS、MIDDLEWARE、数据库基础设置 ├── local.py # 本地开发用DEBUGTrueSQLite 数据库静态文件自动收集 ├── dev.py # 测试环境用DEBUGFalsePostgreSQL关闭模板调试 └── prod.py # 生产环境用强制 HTTPS、密钥从环境变量读、日志分级输出这种拆法不是炫技。比如base.py里定义了AUTH_USER_MODEL users.CustomUser说明用户模型被重写了——这直接决定你后续加手机号登录、微信 UnionID 绑定等扩展的起点在哪。而local.py里有一行EMAIL_BACKEND django.core.mail.backends.console.EmailBackend意味着注册邮件不会真发出去而是打印在终端省得你配 SMTP 账号翻车。2.2 目录结构解析每个 app 都有明确边界没有“万能 utils”项目根目录下apps/文件夹是核心业务区非 Django 默认的平铺结构这是为后期微服务化预留的apps/ ├── users/ # 用户模块注册/登录/个人中心/地址管理 ├── products/ # 商品模块分类、品牌、SPU/SKU、详情页、搜索 ├── orders/ # 订单模块购物车、下单、支付状态机、售后申请 ├── payments/ # 支付模块微信 JSAPI 支付封装、回调验签、退款 └── dashboard/ # 后台模块基于 django-admin 定制的商品/订单/用户管理页注意没有utils/或common/这种模糊命名的 app。所有工具函数按职责下沉——比如图片压缩逻辑在products.utils.image_resize()支付签名计算在payments.utils.wechat_sign()。这种设计让你改一个功能时90% 的代码都在对应 app 下不用满项目 grep。2.3 数据模型设计为什么商品用 SPUSKU 而不是单表products/models.py是整个商城的数据心脏。它没用“一个商品一张表”的懒人设计而是严格区分ProductSPU代表一款手机如“iPhone 15 Pro”含名称、描述、主图、分类、品牌ProductVariantSKU代表具体规格如“iPhone 15 Pro 256GB 银色”含价格、库存、颜色、内存、唯一 SKU 编码。这样设计的好处是同一款手机换颜色/内存不用重复录入商品信息促销时可对 SPU 批量调价也可对特定 SKU 单独做限时折扣。关键字段如下# apps/products/models.py class Product(models.Model): name models.CharField(max_length100, verbose_name商品名称) description models.TextField(verbose_name商品描述) category models.ForeignKey(Category, on_deletemodels.PROTECT) brand models.ForeignKey(Brand, on_deletemodels.PROTECT) main_image models.ImageField(upload_toproducts/, verbose_name主图) class ProductVariant(models.Model): product models.ForeignKey(Product, on_deletemodels.CASCADE, related_namevariants) sku models.CharField(max_length50, uniqueTrue, verbose_nameSKU编码) price models.DecimalField(max_digits10, decimal_places2, verbose_name售价) stock models.PositiveIntegerField(default0, verbose_name库存) color models.CharField(max_length20, verbose_name颜色) memory models.CharField(max_length20, verbose_name内存)注意on_deletemodels.PROTECT比CASCADE更安全——删品牌前会报错逼你手动处理关联商品避免误删导致前台 404。3. 快速启动从零到首页可访问的 5 步实操别急着改代码。先让项目在你本地跑起来这是所有二次开发的前提。以下步骤在 macOS / Linux / Windows WSL 下均验证通过Windows 原生 CMD 用户请把source换成call。3.1 环境准备Python 3.8 和虚拟环境是底线# 1. 确认 Python 版本必须 3.8 python --version # 2. 创建并激活虚拟环境推荐位置项目根目录下 venv/ python -m venv venv source venv/bin/activate # macOS/Linux # venv\Scripts\activate # Windows CMD # 3. 升级 pip避免旧版 pip 安装依赖失败 pip install --upgrade pip提示如果pip install -r requirements.txt报Pillow编译错误Mac 用户执行xcode-select --installUbuntu 用户执行sudo apt-get install libjpeg-dev libpng-dev libtiff-dev libfreetype6-dev再重试。3.2 安装依赖与初始化数据库# 4. 安装所有依赖注意-r 后是相对路径 pip install -r requirements.txt # 5. 迁移数据库会自动创建 SQLite 文件 db.sqlite3 python manage.py migrate # 6. 创建超级用户后台登录账号 python manage.py createsuperuser # 按提示输入用户名、邮箱、密码密码不显示输完直接回车此时db.sqlite3已生成且包含全部表结构。但注意此时商品、用户、订单数据都是空的。别慌项目提供了初始化脚本。3.3 加载测试数据3 条商品 2 个用户 1 个订单项目根目录下有个fixtures/文件夹里面是 JSON 格式的测试数据fixtures/ ├── initial_categories.json # 分类手机、配件、服务 ├── initial_brands.json # 品牌Apple、Samsung、Xiaomi ├── initial_products.json # 3 款手机 SPU含主图路径 └── initial_variants.json # 9 个 SKU不同颜色/内存组合加载命令顺序不能错外键依赖要满足python manage.py loaddata fixtures/initial_categories.json python manage.py loaddata fixtures/initial_brands.json python manage.py loaddata fixtures/initial_products.json python manage.py loaddata fixtures/initial_variants.json注意initial_products.json里的图片路径是products/iphone15pro.jpg实际文件在media/目录下。项目已预置这些图片无需额外下载。3.4 启动服务并验证首页# 7. 启动开发服务器默认端口 8000 python manage.py runserver # 8. 浏览器打开 http://127.0.0.1:8000 # 应看到顶部导航栏、轮播图3 张、热门商品列表3 款手机、底部版权信息此时你已拥有一个功能完整的首页。点任意商品进入详情页能看到规格选择、加入购物车按钮点右上角“登录”用刚才创建的 superuser 账号登录即可进入个人中心和后台管理。4. 核心模块拆解购物车与订单状态机的实现逻辑很多 Django 商城项目卡在“加购物车就报错”本质是没理清“谁持有购物车数据”。这个项目用Session 数据库双写方案兼顾性能与一致性。我们一层层剥开。4.1 购物车Session 存临时数据数据库存持久记录购物车逻辑在apps/orders/cart.py核心是Cart类# apps/orders/cart.py class Cart: def __init__(self, request): self.session request.session cart self.session.get(cart) # 从 Session 读 if not cart: cart self.session[cart] {} self.cart cart def add(self, variant, quantity1, update_quantityFalse): variant_id str(variant.id) if variant_id not in self.cart: self.cart[variant_id] {quantity: 0, price: str(variant.price)} if update_quantity: self.cart[variant_id][quantity] quantity else: self.cart[variant_id][quantity] quantity self.save() def save(self): self.session.modified True # 标记 Session 已修改 # 关键同步到数据库仅当用户已登录 if self.session.get(_auth_user_id): self._sync_to_db() def _sync_to_db(self): # 获取当前用户 user_id self.session.get(_auth_user_id) user get_user_model().objects.get(iduser_id) # 清空用户旧购物车 CartItem.objects.filter(useruser).delete() # 写入新数据 for variant_id, item in self.cart.items(): variant ProductVariant.objects.get(idvariant_id) CartItem.objects.create( useruser, variantvariant, quantityitem[quantity] )逻辑说明未登录用户购物车存在 Session 里关浏览器即消失登录后每次add()或save()都会把 Session 数据同步到CartItem表。这样既保证游客能加购又确保登录后购物车不丢失。4.2 订单创建状态机驱动拒绝“半成品订单”订单状态流转在apps/orders/models.py的Order模型中定义# apps/orders/models.py class Order(models.Model): STATUS_CHOICES [ (draft, 草稿), (pending_payment, 待支付), (paid, 已支付), (shipped, 已发货), (delivered, 已签收), (cancelled, 已取消), (refunded, 已退款), ] status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultdraft) def mark_as_paid(self): 支付成功后调用触发库存扣减、通知等 if self.status ! pending_payment: raise ValueError(只能从待支付状态转为已支付) # 扣减库存事务内 with transaction.atomic(): for item in self.items.all(): variant item.variant if variant.stock item.quantity: raise ValueError(f库存不足{variant.product.name} {variant.color}) variant.stock - item.quantity variant.save() self.status paid self.save() # 发送支付成功通知此处简化为 print print(f订单 {self.order_no} 支付成功已扣减库存) def cancel(self): 用户主动取消订单 if self.status not in [draft, pending_payment]: raise ValueError(只能取消草稿或待支付订单) if self.status pending_payment: # 退款逻辑此处模拟 pass self.status cancelled self.save()参数说明mark_as_paid()方法加了状态校验防止外部直接order.status paid绕过库存检查。transaction.atomic()确保“扣库存改状态”要么全成功要么全失败。4.3 支付回调微信 JSAPI 支付的验签与幂等处理apps/payments/views.py中的WeChatPayNotifyView处理微信异步通知# apps/payments/views.py class WeChatPayNotifyView(View): def post(self, request, *args, **kwargs): # 1. 解析 XML 请求体 xml_data request.body data parse_xml(xml_data) # 自定义解析函数 # 2. 验签关键 sign data.pop(sign, None) if not sign or not self._verify_sign(data, sign): return HttpResponse(fail, content_typetext/plain) # 3. 幂等处理检查商户订单号是否已存在 out_trade_no data[out_trade_no] try: order Order.objects.get(order_noout_trade_no) except Order.DoesNotExist: return HttpResponse(fail, content_typetext/plain) # 4. 更新订单状态只处理未支付成功的订单 if order.status pending_payment: order.mark_as_paid() # 复用前面的状态机方法 return HttpResponse(success, content_typetext/plain) else: # 已处理过返回 success 避免微信重试 return HttpResponse(success, content_typetext/plain) def _verify_sign(self, data, sign): # 拼接待签名字符串字典按键升序 items sorted(data.items()) string_a .join([f{k}{v} for k, v in items if k ! sign]) string_sign_temp f{string_a}key{settings.WECHAT_API_KEY} # MD5 签名 return hashlib.md5(string_sign_temp.encode()).hexdigest().upper() sign逻辑说明微信回调无 Session所以必须用out_trade_no商户订单号定位订单_verify_sign严格按微信文档拼接字符串大小写、空格、排序都不能错幂等处理是线上必选项否则网络抖动导致多次回调会重复扣库存。5. 避坑指南那些让我重启三次服务器的血泪经验这个项目虽小但涉及 Session、数据库事务、第三方回调、静态文件踩坑概率极高。以下是我在三台不同机器上实测出的 4 个高频问题按“现象 → 原因 → 解决”给出可抄作业的答案。5.1 现象首页轮播图不显示控制台报 404原因Django 开发模式下MEDIA_URL和MEDIA_ROOT配置不匹配或urls.py未启用媒体文件路由。解决检查settings/local.py是否有这两行MEDIA_URL /media/ MEDIA_ROOT os.path.join(BASE_DIR, media)在项目根目录urls.py的urlpatterns末尾添加from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)确认media/目录下有products/子目录且图片文件存在不是空文件夹。5.2 现象登录后点击“我的订单”报RelatedObjectDoesNotExist: User has no order.原因User模型被重写为CustomUser但Order模型的user外键仍指向auth.User导致关系断裂。解决打开apps/orders/models.py找到Order类的user字段# 错误写法会报错 user models.ForeignKey(auth.User, on_deletemodels.CASCADE) # 正确写法必须匹配 settings.AUTH_USER_MODEL user models.ForeignKey(settings.AUTH_USER_MODEL, on_deletemodels.CASCADE)修改后执行python manage.py makemigrations orders python manage.py migrate5.3 现象微信支付回调一直返回fail日志显示sign error原因WECHAT_API_KEY配置错误或parse_xml()函数未正确剔除xml根节点和 CDATA。解决检查settings/local.py中WECHAT_API_KEY是否为微信商户平台后台的API 密钥32 位小写字母数字不是 APPID 或 MCHID查看apps/payments/utils.py的parse_xml()函数确认是否用xml.etree.ElementTree正确解析并将所有字段转为字符串微信字段值可能是None需.text or 在WeChatPayNotifyView.post()开头加日志logger.info(fWeChat notify data: {data}) # 看实际收到的字段5.4 现象后台商品列表显示“None”点编辑报FieldError: Unknown field specified in form原因dashboard/admin.py中ProductAdmin的list_display或fields引用了不存在的字段名或ProductVariantInline的fk_name写错。解决打开apps/dashboard/admin.py检查ProductAdmin.list_display# 错误字段名拼错或不存在 list_display [name, category, brand, main_image_url] # main_image_url 未定义 # 正确用模型已有字段或自定义方法 list_display [name, category, brand, get_main_image_url] def get_main_image_url(self, obj): return obj.main_image.url if obj.main_image else - get_main_image_url.short_description 主图检查ProductVariantInline.fk_name是否为product必须与ProductVariant.product字段名一致。6. 二次开发实战给商品详情页加“相似推荐”模块现在你已跑通项目下一步往往是加功能。我以“在商品详情页下方加相似推荐”为例带你走一遍从需求分析到上线的完整链路。这不是教你怎么写代码而是告诉你一个合格开发者会怎么思考、怎么验证、怎么避免返工。6.1 需求拆解什么是“相似”数据从哪来“相似推荐”不是玄学。在这个商城里合理定义是同一品牌 同一分类 价格区间在 ±20% 内的其他 SPU。这样既保证相关性又避免推荐太贵或太便宜的竞品。数据源就是Product模型本身无需额外爬虫或 AI。6.2 后端逻辑在ProductDetailView中注入推荐数据apps/products/views.py的ProductDetailView是详情页视图。我们不改原逻辑而是用get_context_data()注入# apps/products/views.py from django.db.models import Q from decimal import Decimal class ProductDetailView(DetailView): model Product template_name products/detail.html context_object_name product def get_context_data(self, **kwargs): context super().get_context_data(**kwargs) # 获取当前商品的品牌、分类、价格取第一个 SKU 价格 current_product self.object if current_product.variants.exists(): base_price current_product.variants.first().price else: base_price Decimal(0.00) # 构建相似查询同品牌、同分类、价格在 ±20% min_price base_price * Decimal(0.8) max_price base_price * Decimal(1.2) similar_products Product.objects.filter( Q(brandcurrent_product.brand) Q(categorycurrent_product.category) Q(variants__price__gtemin_price) Q(variants__price__ltemax_price) ).distinct().exclude(idcurrent_product.id)[:4] context[similar_products] similar_products return context注意distinct()防止同一 SPU 因多个 SKU 匹配而重复出现exclude(id...)排除自身[:4]限制数量避免模板渲染过慢。6.3 前端渲染复用现有组件不写新 CSStemplates/products/detail.html里已有商品卡片组件product_card.html。我们直接复用!-- 在 detail.html 底部添加 -- {% if similar_products %} div classmt-5 h3 classh4 mb-4相似推荐/h3 div classrow {% for p in similar_products %} {% include products/product_card.html with productp show_priceTrue %} {% endfor %} /div /div {% endif %}关键点with productp show_priceTrue传参确保product_card.html能正确渲染价格。检查该模板是否支持show_price变量通常有{% if show_price %}...{% endif %}判断。6.4 验证与压测三步确认改动可靠改完代码别急着提交按顺序验证单点验证启动服务打开一个商品详情页如 iPhone 15 Pro确认“相似推荐”区块出现且显示的是 Apple 品牌的其他手机如 iPhone 14、iPhone SE检查浏览器 Network 面板确认详情页请求耗时未明显增加应 200ms边界验证找一个无 SKU 的商品手动删掉其所有ProductVariant访问其详情页确认不报错similar_products为空列表找一个价格极低的商品如 99 元手机壳确认推荐列表为空因无同品牌同分类的低价手机并发验证可选但强烈建议用ab命令压测详情页ab -n 100 -c 10 http://127.0.0.1:8000/products/1/观察manage.py runserver终端日志确认无OperationalError: database is lockedSQLite 并发瓶颈若有说明查询太重需加数据库索引python manage.py dbshell CREATE INDEX idx_product_brand_category ON products_product (brand_id, category_id);从那以后我每次加推荐逻辑都强制走一遍这三步验证——哪怕只是改一行模板。因为线上用户不会给你“刷新重试”的机会他们只会默默关掉页面然后去隔壁商城下单。希望帮到你。本文还有配套的精品资源点击获取