简介:这是一套基于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 是"该型号的 8+256 黑色"。常见做法是建三张表:SPU 主表、规格项表、SKU 表。下面是最小可用版本。
# models.py from django.db import models class SPU(models.Model): """商品主表:一款手机""" name = models.CharField(max_length=120, verbose_name="商品名") brand = models.CharField(max_length=60, verbose_name="品牌") description = models.TextField(blank=True, verbose_name="描述") is_on_sale = models.BooleanField(default=True, verbose_name="是否上架") created_at = models.DateTimeField(auto_now_add=True) class Meta: db_table = "mall_spu" indexes = [models.Index(fields=["brand", "is_on_sale"])] class SpecOption(models.Model): """规格项:颜色/内存,用于前台筛选""" spu = models.ForeignKey(SPU, on_delete=models.CASCADE, related_name="specs") spec_name = models.CharField(max_length=30) # 如 "颜色" spec_value = models.CharField(max_length=30) # 如 "黑色" class SKU(models.Model): """具体可售单元,库存和价格挂在这里""" spu = models.ForeignKey(SPU, on_delete=models.CASCADE, related_name="skus") spec_desc = models.CharField(max_length=120) # "8+256 黑色" price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField(default=0) sku_code = models.CharField(max_length=40, unique=True) 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_length=32, unique=True) user_id = models.BigIntegerField(db_index=True) total_amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.CharField(max_length=16, choices=STATUS, default="pending") created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name="items") sku_id = models.BigIntegerField() # 以下为下单时的快照字段 spu_name = models.CharField(max_length=120) spec_desc = models.CharField(max_length=120) price = models.DecimalField(max_digits=10, decimal_places=2) 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_index=True,比如Order.status、Order.user_id。第三,金额字段统一DecimalField(max_digits=10, decimal_places=2),全项目不要出现第二种精度。
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(many=True, read_only=True) 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会触发 N+1 查询,需要在视图里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_sale=True).prefetch_related("skus") serializer_class = SPUListSerializer filter_backends = [DjangoFilterBackend] filterset_fields = ["brand"]prefetch_related("skus")是这里的关键,它把 N+1 压成两次查询。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": "购物车为空"}, status=400) order = Order.objects.create( order_no=self.gen_order_no(request.user.id), user_id=request.user.id, total_amount=0, ) total = 0 for it in items: # select_for_update 锁行,防止并发超卖 sku = SKU.objects.select_for_update().get(id=it["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( order=order, sku_id=sku.id, spu_name=sku.spu.name, spec_desc=sku.spec_desc, price=sku.price, quantity=it["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"), id=spu_id) return render(request, "mall/detail.html", {"spu": spu})<!-- templates/mall/detail.html --> <h1>{{ spu.name }}</h1> <p>品牌:{{ spu.brand }}</p> <select id="sku"> {% 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。
现象二:商品列表页越翻越慢。原因是序列化器里访问了关联对象,触发 N+1。解决:视图 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,再跑一遍压测脚本,最后才提交。这个顺序帮我挡掉过好几次"本地没事、线上超卖"的事故。手机商城这类项目,数据正确性永远比页面好看重要。希望帮到你。
本文还有配套的精品资源,点击获取