ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Django手机商城源码实战:从环境搭建到支付回调全链路解析

Django手机商城源码实战:从环境搭建到支付回调全链路解析

简介:这是一套基于Django框架的Python全栈项目源码,以手机商城为主题,适合正在学习Web全栈开发、需要完整项目练手或课程设计参考的开发者。项目采用前后端分离思路:前端为微信小程序,主要负责商品展示与页面交互;后端依托Django搭建管理后台,完成数据维护与接口支撑,能帮助读者理解从数据建模到接口联调的完整链路。资源包共102个文件,约497KB,包含16个py后端逻辑文件、13个js脚本、13个json配置、11个wxss与9个wxml小程序页面文件,以及若干png、jpg图片素材和4个html模板,另附sqlite数据库与说明文档,结构紧凑、便于快速运行与二次修改。目前已有154人学习下载。通过研读这套源码,读者可掌握Django项目目录组织、模型与视图编写、小程序页面渲染及前后端数据对接等关键技能,也可作为毕业设计或课程作业的参考方案,按模块拆解后逐层理解业务实现。

1. 从零手搓手机商城:这套 Django 全栈源码到底能跑通什么

如果你正在找一个能写进简历、能跑通支付流程、还能把 Django 全栈链路完整串起来的练手项目,这套手机商城源码值得花时间拆一遍。它不是那种只做了增删改查的玩具——商品 SKU、购物车合并、订单状态机、后台管理、用户鉴权这几块都落到了代码里。我见过太多人学完 Django 教程后卡在“不知道下一步做什么”,这个项目的价值就在于它把 MTV 模式、ORM 关联查询、Session 与 Cookie 配合、模板继承这些知识点塞进了一个有真实业务闭环的场景里。适合谁?刚学完 Python 基础、想用 Django 做第一个完整项目的人;或者已经会写视图但没处理过订单并发、库存扣减这类问题的开发者。源码包结构清晰,按 app 拆分,拿到手先跑起来再逐模块读,比对着文档空想效率高得多。

2. 环境搭建与项目初始化:把源码跑起来的第一道坎

2.1 依赖版本与虚拟环境隔离

这套项目基于 Django 框架,但 Django 版本差异会直接影响 ORM 查询语法和urls.py的路由写法。我一般会先看requirements.txt里锁定的版本,没有的话就按python -c "import django; print(django.get_version())"反查。常见做法是用venv建独立环境,避免和系统 Python 里的包打架。

# 创建虚拟环境,指定 Python 3.8 以上 python -m venv venv # 激活环境:Windows 用 venv\Scripts\activate source venv/bin/activate # 安装依赖,建议逐条装而不是一把梭 pip install django==3.2.18 pip install pillow # 商品图片上传依赖 pip install mysqlclient # 如果数据库用 MySQL

这里有个参数要留意:mysqlclient在 Windows 上编译经常翻车,报Microsoft Visual C++ 14.0 is required。解决方式要么装 Build Tools,要么换pymysql并在__init__.py里加pymysql.install_as_MySQLdb()。我一般会先确认数据库选型——源码默认可能是 SQLite,但手机商城这种有订单和库存的场景,SQLite 的写锁在并发下会拖后腿,建议切到 MySQL 或 PostgreSQL。

2.2 数据库配置与迁移命令

打开settings.py,找到DATABASES配置段。如果是 SQLite,ENGINE是django.db.backends.sqlite3,NAME指向BASE_DIR / 'db.sqlite3'。切 MySQL 的话按下面改:

# settings.py 数据库配置 DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'phone_mall', # 提前在 MySQL 里建好库 'USER': 'root', 'PASSWORD': 'your_password', 'HOST': '127.0.0.1', 'PORT': '3306', 'OPTIONS': { 'charset': 'utf8mb4', # 支持 emoji 和生僻字 }, } }

改完执行迁移。这里注意:如果源码里带了migrations文件夹,直接migrate就行;如果没有,先makemigrations再migrate。顺序反了会报Table 'django_migrations' doesn't exist。

python manage.py makemigrations python manage.py migrate # 创建后台管理员,按提示输入用户名密码 python manage.py createsuperuser # 启动开发服务器,默认 8000 端口 python manage.py runserver

启动后访问127.0.0.1:8000/admin能进后台,说明数据库和鉴权模块通了。如果报TemplateDoesNotExist,检查TEMPLATES里的DIRS是否指向了项目的templates目录——这是新手最常踩的坑,源码里模板路径写的是相对路径,换目录后必须同步改。

2.3 静态文件与媒体文件路径

手机商城离不开商品图。STATIC_URL和MEDIA_URL要分开配,MEDIA_ROOT指向用户上传目录,STATICFILES_DIRS指向开发者放的 CSS/JS。开发阶段用django.conf.urls.static提供媒体文件访问,上线后必须交给 Nginx,否则 Django 处理静态文件会拖垮性能。

# urls.py 末尾追加,仅开发环境用 from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

提示:DEBUG = True时 Django 才会自动处理静态文件,上线前改成False并配ALLOWED_HOSTS,否则页面样式全丢。

3. 商品与购物车模块:ORM 关联查询和 Session 合并逻辑

3.1 商品模型设计与 SKU 拆分

手机商城和普通商城最大的区别在 SKU 维度——同一款手机有颜色、内存、版本三个属性组合。源码里常见做法是Category、Goods、GoodsSKU三层模型,用外键串起来。

# models.py 商品模型核心字段 class Category(models.Model): name = models.CharField(max_length=50) parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE) class Goods(models.Model): name = models.CharField(max_length=100) category = models.ForeignKey(Category, on_delete=models.CASCADE) price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.IntegerField(default=0) image = models.ImageField(upload_to='goods/%Y/%m/') is_on_sale = models.BooleanField(default=True) # 上下架开关 class GoodsSKU(models.Model): goods = models.ForeignKey(Goods, on_delete=models.CASCADE) color = models.CharField(max_length=20) version = models.CharField(max_length=20) price = models.DecimalField(max_digits=10, decimal_places=2)

DecimalField而不是FloatField是血泪经验——浮点数算钱会出现0.1 + 0.2 = 0.30000000000000004这种玄学结果,订单金额对不上账。upload_to带日期路径是为了避免单目录文件过多导致ls卡死。

查询时用select_related减少数据库往返:

# 查商品详情时一次性把分类和 SKU 拉出来 goods = Goods.objects.select_related('category').prefetch_related('goodssku_set').get(id=goods_id)

select_related走 SQL JOIN,适合外键;prefetch_related走两次查询再在 Python 里拼,适合多对多和反向外键。用反了不会报错,但查询次数会从 2 次涨到 N+1 次。

3.2 购物车 Session 存储与登录合并

未登录用户的购物车存 Session,登录后要合并到数据库。源码里一般用cart_key做标识,结构是{sku_id: count}。

# views.py 购物车添加逻辑 def add_to_cart(request): sku_id = request.POST.get('sku_id') count = int(request.POST.get('count', 1)) cart = request.session.get('cart', {}) # 已存在则累加,不存在则初始化 cart[sku_id] = cart.get(sku_id, 0) + count request.session['cart'] = cart return JsonResponse({'code': 0, 'cart_count': len(cart)})

登录合并时遍历 Session 里的cart,逐条写入Cart模型,已存在的记录做数量累加而不是覆盖。这里有个坑:Session 默认序列化用 JSON,key 必须是字符串,sku_id是 int 的话取出来会变 str,比较时要统一类型,否则cart.get(sku_id)永远返回None。

3.3 库存扣减与并发处理

下单时扣库存不能先查再改,两个请求同时进来会超卖。正确做法是用F表达式做原子更新:

from django.db.models import F # 原子扣减,stock 小于 count 时更新 0 行 updated = GoodsSKU.objects.filter(id=sku_id, stock__gte=count).update(stock=F('stock') - count) if not updated: return JsonResponse({'code': 1, 'msg': '库存不足'})

filter里的stock__gte=count是条件锁,update返回受影响行数,为 0 说明库存不够。这比select_for_update轻量,适合中小规模并发。如果项目要扛更高并发,得引入 Redis 做库存预扣,但那是另一个量级的改造了。

4. 订单与支付链路:状态机、超时取消与回调验签

4.1 订单状态流转设计

订单不是一张表存到底,状态字段要能表达完整生命周期。源码里常见状态值:待支付、已支付、待发货、已发货、已完成、已取消。用choices约束取值范围,避免脏数据。

class Order(models.Model): STATUS_CHOICES = ( (1, '待支付'), (2, '已支付'), (3, '待发货'), (4, '已发货'), (5, '已完成'), (6, '已取消'), ) status = models.SmallIntegerField(choices=STATUS_CHOICES, default=1) order_sn = models.CharField(max_length=30, unique=True) # 订单号 total_amount = models.DecimalField(max_digits=10, decimal_places=2) create_time = models.DateTimeField(auto_now_add=True)

状态流转必须单向,不能从「已完成」跳回「待支付」。我一般会在视图层加判断:if order.status != 1: return error,只允许待支付订单发起支付。

4.2 订单号生成与超时取消

订单号用「时间戳 + 用户 ID 后四位 + 随机数」拼,保证唯一且可读。超时取消用 Celery 延时任务或定时脚本扫create_time超过 30 分钟且状态为待支付的订单。

import time, random def generate_order_sn(user_id): # 格式:20250101120000 + 用户后四位 + 6位随机 timestamp = time.strftime('%Y%m%d%H%M%S') rand = str(random.randint(100000, 999999)) return f'{timestamp}{str(user_id)[-4:]}{rand}'

超时取消的脚本逻辑:

from datetime import datetime, timedelta def cancel_expired_orders(): deadline = datetime.now() - timedelta(minutes=30) expired = Order.objects.filter(status=1, create_time__lt=deadline) for order in expired: order.status = 6 order.save() # 回滚库存,用 F 表达式加回去 for item in order.orderitem_set.all(): GoodsSKU.objects.filter(id=item.sku_id).update(stock=F('stock') + item.count)

注意回滚库存和改状态要放在同一个事务里,否则中途报错会出现「订单取消了但库存没回来」的黑匣子情况。

4.3 支付回调验签与幂等处理

支付平台回调时,先验签再改状态。验签用平台提供的公钥和签名算法,源码里一般封装在utils/pay.py。幂等处理是关键——同一笔回调可能重复到达,必须判断订单当前状态,已支付就直接返回成功,不重复发货。

def pay_callback(request): data = request.POST.dict() if not verify_sign(data): # 验签失败直接拒绝 return HttpResponse('fail') order_sn = data.get('out_trade_no') order = Order.objects.get(order_sn=order_sn) if order.status != 1: # 幂等:非待支付状态直接返回 return HttpResponse('success') order.status = 2 order.save() return HttpResponse('success')

注意:回调地址必须是公网可访问的,本地开发用内网穿透工具临时映射,别把127.0.0.1填进去,否则永远收不到回调。

5. 避坑与排查:那些让项目跑不起来的常见问题

5.1 迁移文件冲突导致 migrate 失败

现象:执行python manage.py migrate报InconsistentMigrationHistory或Table already exists。原因:手动改过模型但没重新生成迁移,或者多人协作时迁移文件版本对不上。解决:删掉对应 app 下migrations里除__init__.py外的文件,删掉数据库里的django_migrations表,重新makemigrations+migrate。数据重要的话先备份。

5.2 模板中静态文件路径 404

现象:页面能打开但 CSS/JS 全丢,控制台报 404。原因:模板里写的是{% static 'css/style.css' %},但STATICFILES_DIRS没配或路径不对。解决:确认settings.py里STATICFILES_DIRS = [BASE_DIR / 'static'],且static目录和manage.py同级。DEBUG=False时还要配 Nginx 的location /static/。

5.3 图片上传后无法访问

现象:后台上传商品图成功,前端<img>标签裂开。原因:MEDIA_URL配了但urls.py没加static()路由,或者MEDIA_ROOT目录没有写权限。解决:开发环境在urls.py追加static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT);Linux 下chmod 755媒体目录。

5.4 购物车数量显示为字符串拼接

现象:购物车角标显示11而不是2。原因:Session 里存的 count 是字符串,+变成了拼接。解决:存之前int(count)强转,取出来也转一次。Django Session 序列化不会保留 int 类型,这是设计如此。

5.5 订单提交后库存没变

现象:下单成功但商品库存还是原值。原因:扣库存代码写在order.save()之后且没加事务,或者用了Goods而不是GoodsSKU扣减。解决:用transaction.atomic()包住下单逻辑,确认扣的是 SKU 表的 stock 字段。

6. 进阶技巧:用 Django Debug Toolbar 定位 N+1 查询与缓存优化

项目跑通之后,真正拉开差距的是性能排查。我习惯在settings.py里挂上 Django Debug Toolbar,它能把每个请求的 SQL 条数、耗时、重复查询全列出来。安装很简单:

pip install django-debug-toolbar
# settings.py INSTALLED_APPS += ['debug_toolbar'] MIDDLEWARE.insert(0, 'debug_toolbar.middleware.DebugToolbarMiddleware') INTERNAL_IPS = ['127.0.0.1'] # 只有本机访问才显示面板
# urls.py 开发环境追加 if settings.DEBUG: import debug_toolbar urlpatterns += [path('__debug__/', include(debug_toolbar.urls))]

刷新商品列表页,看 SQL 面板。如果发现同一个SELECT执行了 20 次,那就是 N+1 问题。比如循环里取sku.goods.name,每次循环都查一次Goods表。改成prefetch_related('goodssku_set__goods')后 SQL 条数会从 21 降到 2。

另一个高频优化点是首页商品列表。手机商城首页通常要展示分类、热销、新品三块数据,如果每块都单独查库,一个请求打十几次 SQL。我一般用 Django 的cache框架把结果缓存 60 秒:

from django.core.cache import cache def index(request): goods_list = cache.get('index_goods') if not goods_list: goods_list = list(Goods.objects.filter(is_on_sale=True)[:10]) cache.set('index_goods', goods_list, 60) # 缓存 60 秒 return render(request, 'index.html', {'goods_list': goods_list})

缓存时间别设太长,商品上下架后要等缓存过期才生效。如果对实时性要求高,就在后台改商品时主动cache.delete('index_goods')。

还有个小技巧:DEBUG=False后静态文件交给 Nginx,但 Django 的runserver不再处理媒体文件,本地测试支付回调时图片会 404。我一般会在开发阶段用whitenoise中间件顶一下,上线再摘掉。

从那以后我每次拿到新源码,都强制先跑python manage.py check --deploy过一遍安全检查,再挂 Debug Toolbar 看首页 SQL 条数,最后才读业务代码。这个顺序能省掉大量「跑不起来」的无效折腾。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进