Django全栈手机商城实战:SPU+SKU模型与订单快照设计
简介这是一套基于Django框架的Python全栈项目源码以手机商城为主题适合正在学习Web全栈开发、需要完整项目练手的在校学生与初级开发者。项目采用前后端分离思路前端为微信小程序主要负责商品展示与数据呈现后端依托Django搭建管理后台完成数据维护与接口支撑可帮助读者理解从数据建模到接口联调再到小程序渲染的完整链路。资源包共102个文件约497KB包含16个py后端逻辑文件、13个js脚本、13个json配置、11个wxss与9个wxml小程序页面文件以及若干png、jpg图片素材和html模板另附sqlite数据库与说明文档目录结构清晰便于按模块阅读与二次开发。目前已有154人学习下载。通过研读这套源码读者可掌握Django项目组织方式、小程序页面与样式编写、前后端数据交互流程并可直接在此基础上扩展商品管理、订单等功能是较为实用的全栈入门参考案例。1. 手机商城全栈项目为什么 Django 依然是快速交付的稳妥选择做过几个电商类外包的同行大概都有体会客户要的不是炫技是两周内能跑起来、能下单、能看订单、后台能改价格的完整闭环。手机商城这类需求尤其典型——SKU 少、规格多颜色、内存、版本、下单链路短但后台运营动作频繁。用 Django 做全栈最大的价值不是框架先进而是它把用户体系、权限、ORM、Admin 后台这四块电商刚需一次性给全了你只需要专注在商品模型和订单状态机上。这篇文章面向的是能写 Python、想独立交付一个手机商城全栈项目的开发者。我会按模型怎么设计 → 接口怎么串 → 前端怎么接 → 坑在哪的顺序讲中间给出可直接抄的代码和参数。不追求高并发架构追求的是一个人能维护、能上线、能改需求的工程结构。如果你正卡在表结构反复改、订单状态乱、后台不好用这几个点上下面的内容应该能省你不少返工时间。2. 商品与订单模型手机商城表结构怎么设计才不返工手机商城的模型设计有个反直觉的地方真正难的不是商品表而是同一款手机不同规格和订单快照这两件事。前者决定你前台筛选好不好做后者决定你退款和售后时数据对不对得上。我见过太多项目把规格塞进商品表的 JSON 字段前期爽后期做库存扣减时全是玄学 bug。2.1 用 SPU SKU 拆开商品别把规格塞进 JSON手机这类商品天然是两层结构SPU 是某品牌某型号SKU 是该型号的 8256 黑色。常见做法是建三张表SPU 主表、规格项表、SKU 表。下面是最小可用版本。# models.py from django.db import models class SPU(models.Model): 商品主表一款手机 name models.CharField(max_length120, verbose_name商品名) brand models.CharField(max_length60, verbose_name品牌) description models.TextField(blankTrue, verbose_name描述) is_on_sale models.BooleanField(defaultTrue, verbose_name是否上架) created_at models.DateTimeField(auto_now_addTrue) class Meta: db_table mall_spu indexes [models.Index(fields[brand, is_on_sale])] class SpecOption(models.Model): 规格项颜色/内存用于前台筛选 spu models.ForeignKey(SPU, on_deletemodels.CASCADE, related_namespecs) spec_name models.CharField(max_length30) # 如 颜色 spec_value models.CharField(max_length30) # 如 黑色 class SKU(models.Model): 具体可售单元库存和价格挂在这里 spu models.ForeignKey(SPU, on_deletemodels.CASCADE, related_nameskus) spec_desc models.CharField(max_length120) # 8256 黑色 price models.DecimalField(max_digits10, decimal_places2) stock models.PositiveIntegerField(default0) sku_code models.CharField(max_length40, uniqueTrue) class Meta: db_table mall_sku逻辑说明SPU 负责展示和搜索SKU 负责交易。价格和库存只存在 SKU 上避免同一商品多个价格来源。spec_desc用冗余字符串存规格组合是为了订单快照和列表展示时不用再联表拼字符串这是用空间换查询效率的常见取舍。参数说明price用DecimalField而不是FloatField金额计算绝不能用浮点stock用PositiveIntegerField从数据库层挡住负数sku_code加唯一约束方便对接外部 ERP 时做幂等。2.2 订单快照为什么必须在下单时冻结商品信息订单表如果只存sku_id商品改价或下架后历史订单显示的价格就跟着变了售后对账直接翻车。正确做法是下单瞬间把商品关键字段复制进订单明细。class Order(models.Model): STATUS [ (pending, 待支付), (paid, 已支付), (shipped, 已发货), (done, 已完成), (closed, 已关闭), ] order_no models.CharField(max_length32, uniqueTrue) user_id models.BigIntegerField(db_indexTrue) total_amount models.DecimalField(max_digits10, decimal_places2) status models.CharField(max_length16, choicesSTATUS, defaultpending) created_at models.DateTimeField(auto_now_addTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) sku_id models.BigIntegerField() # 以下为下单时的快照字段 spu_name models.CharField(max_length120) spec_desc models.CharField(max_length120) price models.DecimalField(max_digits10, decimal_places2) quantity models.PositiveIntegerField()逻辑说明OrderItem里冗余了spu_name、spec_desc、price这些是快照商品后续怎么改都不影响历史订单。sku_id只作为关联线索保留不参与金额计算。参数说明order_no建议用时间戳 用户 ID 后四位 随机数生成长度控制在 32 以内方便索引user_id用BigIntegerField加索引是因为订单查询几乎永远按用户维度过滤。2.3 迁移与索引三个必调参数模型写完别急着migrate先确认三件事。第一db_table显式命名避免 Django 默认的app_model命名在后期多环境同步时对不上。第二高频查询字段加db_indexTrue比如Order.status、Order.user_id。第三金额字段统一DecimalField(max_digits10, decimal_places2)全项目不要出现第二种精度。python manage.py makemigrations mall python manage.py migrate # 检查生成的 SQL确认索引是否落地 python manage.py sqlmigrate mall 0001sqlmigrate这一步很多人跳过但它是排查索引没生效最快的办法。如果输出里看不到CREATE INDEX说明你的Meta.indexes写错了位置。3. 接口层用 DRF 把商品列表和下单链路串起来模型是地基接口是门面。手机商城的接口有两个性能敏感点商品列表带筛选和分页和下单要保证库存扣减不超卖。这一章给出可直接跑的 DRF 写法以及下单时绕不开的并发处理。3.1 商品列表接口分页、筛选、序列化器怎么写先装依赖pip install djangorestframework django-filter然后在settings.py注册。# settings.py INSTALLED_APPS [rest_framework, django_filters] REST_FRAMEWORK { DEFAULT_PAGINATION_CLASS: rest_framework.pagination.PageNumberPagination, PAGE_SIZE: 20, DEFAULT_FILTER_BACKENDS: [django_filters.rest_framework.DjangoFilterBackend], }序列化器只暴露必要字段别用__all__否则商品成本价这类敏感字段容易漏出去。# serializers.py from rest_framework import serializers from .models import SPU, SKU class SKUSerializer(serializers.ModelSerializer): class Meta: model SKU fields [id, spec_desc, price, stock] class SPUListSerializer(serializers.ModelSerializer): skus SKUSerializer(manyTrue, read_onlyTrue) min_price serializers.SerializerMethodField() class Meta: model SPU fields [id, name, brand, skus, min_price] def get_min_price(self, obj): prices [s.price for s in obj.skus.all()] return min(prices) if prices else None逻辑说明min_price用于列表页显示¥2999 起避免前端自己遍历 SKU。skus嵌套返回前端拿到就能渲染规格选择。参数说明PAGE_SIZE设 20 是移动端列表的常见值太大首屏慢太小翻页频繁。如果 SKU 数量大get_min_price会触发 N1 查询需要在视图里prefetch_related(skus)。# views.py from rest_framework import viewsets from django_filters.rest_framework import DjangoFilterBackend from .models import SPU from .serializers import SPUListSerializer class SPUViewSet(viewsets.ReadOnlyModelViewSet): queryset SPU.objects.filter(is_on_saleTrue).prefetch_related(skus) serializer_class SPUListSerializer filter_backends [DjangoFilterBackend] filterset_fields [brand]prefetch_related(skus)是这里的关键它把 N1 压成两次查询。filterset_fields让/api/spu/?brand某品牌直接可用不用自己写过滤逻辑。3.2 下单接口库存扣减与事务边界下单是唯一必须上事务的地方。核心原则扣库存和建订单要么全成功要么全回滚。# views.py from django.db import transaction from rest_framework.views import APIView from rest_framework.response import Response from .models import SKU, Order, OrderItem class CreateOrderView(APIView): transaction.atomic def post(self, request): items request.data.get(items, []) if not items: return Response({msg: 购物车为空}, status400) order Order.objects.create( order_noself.gen_order_no(request.user.id), user_idrequest.user.id, total_amount0, ) total 0 for it in items: # select_for_update 锁行防止并发超卖 sku SKU.objects.select_for_update().get(idit[sku_id]) if sku.stock it[quantity]: raise ValueError(f{sku.spec_desc} 库存不足) sku.stock - it[quantity] sku.save(update_fields[stock]) OrderItem.objects.create( orderorder, sku_idsku.id, spu_namesku.spu.name, spec_descsku.spec_desc, pricesku.price, quantityit[quantity], ) total sku.price * it[quantity] order.total_amount total order.save(update_fields[total_amount]) return Response({order_no: order.order_no})逻辑说明select_for_update()在事务内对 SKU 行加锁保证同一 SKU 的并发下单串行执行这是防超卖最直接的手段。transaction.atomic保证任何一步抛异常前面的扣减全部回滚。参数说明update_fields显式指定更新列减少写放大raise ValueError会被事务捕获并回滚但生产环境建议换成自定义异常并统一处理返回格式。注意select_for_update必须在事务内使用否则报错。3.3 订单状态流转用状态机约束别用 if-else 堆订单状态如果散落在各个视图里用if status pending判断后期加部分退款就会失控。常见做法是集中定义允许的流转。ALLOWED_TRANSITIONS { pending: [paid, closed], paid: [shipped, closed], shipped: [done], done: [], closed: [], } def transit(order, target): if target not in ALLOWED_TRANSITIONS.get(order.status, []): raise ValueError(f非法流转 {order.status} - {target}) order.status target order.save(update_fields[status])这张表就是订单的后悔药边界哪些状态能跳、哪些不能一眼看清。加新状态时只改这一处比满项目搜if靠谱得多。4. 前端与后台模板渲染还是前后端分离怎么选不后悔到这一步很多人会纠结Django 模板直接渲染还是 DRF 前端框架分离我的经验是看交付节奏。如果后台运营需求重、前台页面不多模板 Admin 最快如果前台交互复杂规格联动、购物车动画分离更省心。下面两条路都给最小实现。4.1 模板渲染路线商品详情页与购物车模板路线适合快速上线。视图传上下文模板负责展示。# views.py from django.shortcuts import render, get_object_or_404 from .models import SPU def spu_detail(request, spu_id): spu get_object_or_404(SPU.objects.prefetch_related(skus), idspu_id) return render(request, mall/detail.html, {spu: spu})!-- templates/mall/detail.html -- h1{{ spu.name }}/h1 p品牌{{ spu.brand }}/p select idsku {% for sku in spu.skus.all %} option value{{ sku.id }}># settings.py INSTALLED_APPS [corsheaders, rest_framework_simplejwt] MIDDLEWARE.insert(0, corsheaders.middleware.CorsMiddleware) CORS_ALLOWED_ORIGINS [http://localhost:5173] # 前端开发地址 REST_FRAMEWORK[DEFAULT_AUTHENTICATION_CLASSES] [ rest_framework_simplejwt.authentication.JWTAuthentication, ]# 获取 token curl -X POST http://127.0.0.1:8000/api/token/ \ -H Content-Type: application/json \ -d {username:test,password:test1234}逻辑说明CorsMiddleware必须放在中间件最前面否则预检请求会被拦。JWT 的 access token 有效期默认 5 分钟前端要用 refresh token 续期。参数说明CORS_ALLOWED_ORIGINS生产环境要写真实域名别用CORS_ALLOW_ALL_ORIGINS True那是给自己挖坑。Token 有效期在SIMPLE_JWT里配移动端可以适当延长 access 到 30 分钟。4.3 Admin 后台让运营能改价、改库存、看订单Django Admin 是手机商城项目最容易被低估的部分。配好它运营需求能省掉一半开发。# admin.py from django.contrib import admin from .models import SPU, SKU, Order, OrderItem class SKUInline(admin.TabularInline): model SKU extra 1 admin.register(SPU) class SPUAdmin(admin.ModelAdmin): list_display [name, brand, is_on_sale, created_at] list_filter [brand, is_on_sale] search_fields [name] inlines [SKUInline] admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display [order_no, user_id, total_amount, status, created_at] list_filter [status] search_fields [order_no] readonly_fields [order_no, created_at]逻辑说明SKUInline让运营在商品页直接改规格和库存不用跳转。readonly_fields保护订单号和创建时间不被误改。参数说明list_filter和search_fields是运营效率的关键订单量上千后没有搜索会很难受。extra 1控制新增行数别设太大页面会卡。5. 避坑与排查手机商城项目里最容易翻车的五件事这一章是我自己踩过、也帮别人排查过的真实问题按现象 → 原因 → 解决写遇到对应症状直接对号入座。现象一下单后库存变成负数。原因是没有用select_for_update两个请求同时读到相同库存。解决把扣减逻辑放进transaction.atomic并对 SKU 行加锁如果数据库是 SQLite锁行为有限生产务必换 PostgreSQL 或 MySQL。现象二商品列表页越翻越慢。原因是序列化器里访问了关联对象触发 N1。解决视图 queryset 加prefetch_related(skus)并用django-debug-toolbar确认查询次数稳定在个位数。现象三订单金额和商品现价对不上。原因是订单明细没做快照直接读 SKU 现价。解决下单时把price、spu_name、spec_desc复制进OrderItem历史订单只读快照。现象四Admin 后台改价后前台不更新。原因是加了缓存但没失效。解决改价时用信号或重写save清缓存如果用了cache_page注意它按 URL 缓存筛选参数不同会命中不同缓存。现象五JWT 登录后接口仍返回 401。原因是请求头格式不对或 token 过期。解决确认是Authorization: Bearer token中间有空格access 过期用 refresh 换新别让前端硬编码长有效期。提示这五条里库存和快照是数据正确性问题必须优先解决列表性能和缓存是体验问题可以上线后迭代。6. 上线前的压测与验收三个我必做的检查动作项目能跑通不等于能交付。上线前我会固定做三件事这里把具体做法和判断标准写清楚你可以直接照做。第一件是并发下单压测。用locust或ab对下单接口发 50 并发观察库存是否精确等于初始值减去成功订单数。命令示例ab -n 200 -c 50 -p order.json -T application/json \ -H Authorization: Bearer token \ http://127.0.0.1:8000/api/order/create/判断标准成功订单数 × 每单数量 库存减少量误差为 0。如果出现负数或对不上回到第 5 章第一条排查。第二件是慢查询检查。打开数据库慢日志阈值设 200ms跑一遍商品列表、详情、下单三条主链路。常见慢查询集中在没有索引的brand筛选和订单按用户查询上补索引即可。第三件是订单状态全链路走查。手动从待支付走到已完成再走一遍关闭流程确认每一步 Admin 显示和接口返回一致。这一步能抓出状态机配置漏掉的分支。最后说个我自己的习惯每次改完模型先跑sqlmigrate看 SQL再跑一遍压测脚本最后才提交。这个顺序帮我挡掉过好几次本地没事、线上超卖的事故。手机商城这类项目数据正确性永远比页面好看重要。希望帮到你。本文还有配套的精品资源点击获取