news 2026/9/8 23:58:18

Django电商网站实战:数据模型、库存事务到支付部署全解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Django电商网站实战:数据模型、库存事务到支付部署全解析

简介:一份基于Django框架的电子商务网站完整项目,面向希望系统学习Web开发、掌握Django MVT架构与HTML前端结合的开发者,可帮助理解从商品展示、购物车到订单结算的完整电商闭环。压缩包共101个文件,以Python源码(py)、HTML模板、JPG/WebP图片资源为主,另含pyc编译文件与sqlite3数据库文件,整体仅3.89MB,轻量便于快速部署与二次开发。项目覆盖用户注册登录、商品分类展示、搜索、购物车管理、订单生成及支付接口预留等核心模块,运用Django模板语言(DTL)动态渲染页面、ORM模型建表以及CSRF等安全防护机制。具体包含Product、Category、Order等数据模型定义,以及基于会话和Cookie的购物车状态保持、支付网关对接预留等功能;同时提供多个HTML页面模板与URL路由配置,展示了静态资源加载、分页搜索、表单处理等常见Web开发技巧。项目结构清晰,模型、视图、模板分层明确,并附有静态图片资源与SQLite数据库文件,下载后即可对照学习。已有90人学习浏览,适合作为课程设计、Django入门实战或电商项目参考。 Django能不能做电商网站?这个问题我被问过很多次。我的答案一直是:能做,而且比你想的更合适。但前提是你别照着网上那种“商品列表+购物车+下单页面”的教程来——那些Demo跑通没问题,真要上线,会在订单、库存、支付回调、部署这些地方连环踩坑。我花了小半年把一个基于Django的电子商务网站从0写到上线,项目名字就叫EcommerceWebsite,商品管理、购物车、下单扣库存、支付回调、后台管理都完整过了一遍。下面这些内容,都是我那个项目里真实踩过、调过的记录。适合已经会用Django做基础CRUD、准备做完整项目的开发者,也适合准备电商方向Django面试的人。核心不是教你抄代码,而是讲清楚每个设计选择背后的为什么。

1. 电商项目不是CRUD:先盘清楚模块边界和数据模型

1.1 模块划分:几个App不是越多越好

拿到项目别急着写代码。我先用django-admin startapp把项目拆成了五个核心App,边界一开始就定清楚,后面写业务才不会被绕晕。

App核心职责
users用户注册/登录/收货地址管理
goods商品SPU/SKU/分类/品牌
carts购物车存取
orders订单、订单项、支付流水
payment支付渠道对接与回调处理

这个拆分不是拍脑袋。电商的核心数据链路是“用户→商品→购物车→订单→支付”,每个环节一个独立App,职责边界很清晰。很多人搜“django创建app”,但创建App本身很简单,难的是想清楚哪些内容放哪个App。比如支付状态这个字段,我见过有人放在orders里,也有人放在payment里,我的建议是:订单模型只保留面向业务的status(待支付/已支付/已发货等),支付渠道的原始返回和流水记录统一放payment,两个App通过order_no关联。这样换支付渠道的时候,订单模块不用动。

1.2 数据模型设计的三个关键点

第一,一定要区分SPU和SKU。SPU是“商品”,SKU是“具体可卖的规格”。比如一台手机是SPU,“蓝色256G版”才是SKU。库存、价格、购物车、订单项挂在SKU上,不设计SKU的电商网站,后面的库存和订单逻辑就是空中楼阁。新手容易把颜色、容量全塞进商品详情字段里,看起来简单,后面想加规格就哭。

class Product(models.Model): title = models.CharField(max_length=200) category = models.ForeignKey(Category, on_delete=models.PROTECT) detail = models.TextField() # 富文本内容 is_active = models.BooleanField(default=True) class ProductSKU(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE) spec = models.CharField(max_length=100) # 如 "蓝色 256G" price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.PositiveIntegerField(default=0) sales = models.PositiveIntegerField(default=0) is_active = models.BooleanField(default=True)

商品详情我集成过wangeditor这类富文本编辑器,用来填图文介绍没问题,但要注意:不要把所有商品信息都塞进富文本里。筛选、排序、SEO需要的是结构化的分类、价格、规格字段,富文本只应该承担“图文详情展示”这一件事。

第二,商品不能物理删除。用户下单后订单里引用了商品,你直接把商品记录delete()掉,订单详情、历史记录就全部断链了。Django里执行删除对象很简单,但电商场景我强烈建议一律不用objects.delete(),而是用软删除(下架):

# 软删除:下架而不是物理删除 ProductSKU.objects.filter(id=sku_id).update(is_active=False) # 外键用 PROTECT,防止误删连锁把订单历史冲掉 order_item = models.ForeignKey(Order, on_delete=models.PROTECT)

第三,订单必须做价格快照。商品价格随时可能调,订单创建后不能再去商品表实时取价。所以订单项上要冗余存下商品名称、SKU规格、单价、促销价、图片这些信息。原因很简单:给用户开的“单据”必须和下单那一刻的成交条件一致,等商品改价或者被下架了,订单数据不能被带跑。

2. 库存与订单状态:最容易翻车的地方

2.1 超卖是怎么发生的

电商最典型的并发问题就是超卖。假设某个SKU库存只剩1件,两个用户同时下单,最常见的错误写法长这样:

sku = ProductSKU.objects.get(id=sku_id) if sku.stock > 0: sku.stock -= 1 sku.save()

两个请求同时读到stock=1,都判断大于0,都做了减1,最后库存变成0,但订单创建了两个——多卖出去一件。问题根源在于“读库存”和“写库存”不是一个原子操作。解决办法是让数据库来完成判断和扣减:

from django.db.models import F updated = ProductSKU.objects.filter(id=sku_id, stock__gt=0).update(stock=F("stock") - 1) if updated == 0: raise InsufficientStock("库存不足")

UPDATE在数据库层面是原子操作,配合stock__gt=0条件,天然杜绝超卖。这是Django商城项目里最重要的一行代码,没有之一。面试里被问到“你们如何防止超卖”,能把这个逻辑讲清楚,基本就过关了。

不过,仅凭这一行还不够。下单不只是扣库存,还要创建订单、订单项、清空购物车、记录库存流水,这些操作必须在一个事务里,要么全成功要么全失败。

2.2 下单事务和行锁的正确姿势

用一个事务包住整个下单流程,并配合select_for_update()把SKU行锁住:

from django.db import transaction def create_order(sku_id, user, address, quantity=1): with transaction.atomic(): sku = ProductSKU.objects.select_for_update().get(id=sku_id) if sku.stock < quantity: raise InsufficientStock("库存不足") sku.stock -= quantity sku.save() order = Order.objects.create( user=user, address=address, total_amount=sku.price * quantity, status="pending", ) OrderItem.objects.create( order=order, sku=sku, product_title=sku.product.title, spec=sku.spec, price=sku.price, quantity=quantity, ) return order

select_for_update()会在事务内对目标行加锁,其他事务想改这一行,只能等当前事务提交。两个注意点:一是必须放在atomic()事务里,否则锁不会生效;二是表引擎必须是InnoDB,MyISAM不支持行锁,锁的是整张表。

关于扣库存的策略,我后来还做了个优化:很多项目用“下单锁库存、支付成功再实际扣减”的方式。具体做法是在SKU上维护stock(可售库存)和locked_stock(锁定数量)两个字段,下单时stock减一、locked_stock加一,支付成功再把locked_stock减掉。这样既不会在支付阶段超卖,又能配合超时取消订单释放锁定库存。如果你的项目是MVP,先用“下单即减库存”也能跑,但一定要给超时取消订单留接口,否则未支付订单会一直占着库存。

2.3 订单状态机:别让状态散落在代码里

订单状态我强烈建议集中成一个状态机。常见状态有:待支付pending、已支付paid、已发货shipped、已完成completed、已取消cancelled。不要在每个接口里到处写if order.status == 1,状态一多就乱。用一个字典把“允许的状态转移”集中定义:

ORDER_STATUS_TRANSITIONS = { "pending": ["paid", "cancelled"], "paid": ["shipped", "cancelled"], "shipped": ["completed"], }

任何状态变更先进这个表做合法性校验,不合法直接抛异常。后端所有订单状态变更统一走一个transition_order_status(order, target_status)函数,比散落几十处if判断好维护得多。这一段也是我当时面试准备时反复梳理过的点:状态机、幂等、事务边界,是电商业务里三个最容易答出深度的方向。

3. 商品列表页性能优化:一次真实的慢查询排查

3.1 一条列表页,几十条SQL

商品列表页是电商网站的流量入口。我第一版列表页是“能用但很慢”,用户翻页要等接近两秒。用django-debug-toolbar一看,一个页面发了一百多条SQL。典型问题长这样:

# 每件商品取一次分类名,N+1 for sku in sku_list: print(sku.product.category.name)

SKU外键关联Product,Product又关联Category,每次访问.category.name都会触发一次查询。列表里50个SKU,就多出50条重复的category查询。这就是经典的N+1查询。

3.2 select_related和prefetch_related到底怎么选

解决N+1查询的方法是让ORM一次性查出关联数据,重点在于选对方法:

  • select_related:用于多对一、一对一关系,生成SQL JOIN,把关联表一次查出来。
  • prefetch_related:用于多对多、一对多、反向外键,先查主表,再用IN查询查关联表,最后在Python端组装。
# 多对一:查SKU时把Product、Category一起JOIN出来 sku_list = ProductSKU.objects.select_related("product__category").filter(is_active=True) # 一对多:查商品时把多个SKU一次IN查询出来 product_list = Product.objects.prefetch_related("sku_set").filter(is_active=True)

select_related("product__category")是链式写法,意思是把“SKU→Product→Category”这条外键链路一次性查完。我在项目里的经验是:先开django-debug-toolbar看SQL条数,再决定用哪种,不要凭感觉写。面试被问“select_related和prefetch_related的区别”,能说出“JOIN vs IN查询、适合的关系类型不同、什么时候选哪个”,基本就能让面试官点头。

3.3 分页和缓存的组合拳

列表页另一个痛点是分页。Paginator本身很好用,但要注意在查询里加一个稳定的排序字段,比如order_by("id"),否则翻页的时候会出现重复或漏数据。我第一版漏了排序,被测试反馈翻到第3页和第2页有重复商品,加了排序才稳定。

中小电商用标准Paginator完全够用。数据量上来之后,建议改成基于id > last_id的keyset分页,但那是后续优化方向,别在项目初期过度设计。缓存方面,我的做法是:商品分类列表页做整页缓存,动态部分只有当前用户信息和购物车数量,把这两块用Ajax局部刷新。Redis缓存查询结果:

cache_key = f"sku_list_page_{category_id}_{page}" page_data = cache.get(cache_key) if page_data is None: page_data = list(ProductSKU.objects.select_related("product__category").filter(...)) cache.set(cache_key, page_data, 60 * 5)

顺带说一句,模板里生成商品详情链接,别硬编码URL,用reverse("goods:detail", args=[sku.id])。改路由的时候不用满模板替换,这是很多人搜“django reverse resolve”想知道的事。

4. 支付回调、异步任务与幂等处理

4.1 支付流程的主干逻辑

用户下单后,订单状态是“待支付”,前端带上订单号请求支付接口,支付平台返回支付链接,用户付款后,支付平台通过异步通知把结果推到你的回调地址。流程主干很清楚,但线上问题基本都出在回调这一段。

回调地址必须用HTTPS,并且要在后端配置好支付平台要求的公网可访问URL。回调接口本身要加上csrf_exempt,因为这个接口是支付平台调用的,不走用户会话,但必须做签名校验和金额校验,这个下面细说。

4.2 幂等:回调重复通知不可怕

支付平台的回调没有“只通知一次”的保证,同一笔订单可能重复回调。如果回调里不做判断,每收到一次都往用户余额里加钱,线上就出大事了。幂等处理我用的方案是“状态判断+事务锁”的组合:

from django.db import transaction def handle_payment_callback(order_no, paid_amount, channel_sign): # 先校验签名,失败直接拒绝 if not verify_sign(order_no, paid_amount, channel_sign): raise PaymentSignError with transaction.atomic(): order = Order.objects.select_for_update().get(order_no=order_no) if order.status != "pending": return # 已经处理过,直接忽略 if Decimal(paid_amount) != order.total_amount: raise PaymentAmountMismatch("回调金额与订单金额不一致") order.status = "paid" order.paid_at = timezone.now() order.save() PaymentRecord.objects.create( order_no=order_no, amount=paid_amount, channel="xxx", )

select_for_update()先锁住订单行,再检查状态,保证“检查状态→更新状态”是原子的。同一笔订单的并发回调只会有一次走到更新,另一个进来发现状态已经不是pending,直接退出。这就是幂等。金额校验也不能省,回调里的金额必须和订单金额一致才能改状态,防止渠道异常或者被人为篡改请求。

4.3 回调里不要做耗时操作

回调接口里除了验签、金额校验、改状态之外,尽量不要做发邮件、算佣金、同步库存这类耗时操作。这些应该放到异步任务里。我在项目里用Celery,回调本身只做最核心的状态变更,然后丢一个task给队列,支付成功短信、发票生成、推荐人佣金都在异步任务里处理。回调接口响应一定要快,支付平台如果超时,会继续重推,反而增加重复回调的压力。

另外,订单号设计也值得花心思。回调里要解析订单号,不要用自增主键id直接当订单号给用户看,容易泄露销量。我的做法是生成“年月日+随机串”的订单号,同时在数据库加unique约束保证全局不重复。

提示:支付相关的日志从第一天就要打好。订单号、回调参数、验签结果、金额比对结果,全部记录下来。线上排查支付问题,没有日志就是瞎猜。

5. 宝塔部署Django上线:全流程与隐藏坑

5.1 部署前的项目准备

很多人本地跑Django风生水起,一上线各种404。先说部署前应该做的事:

  • pip freeze > requirements.txt固定依赖版本;
  • settings.py里的DEBUG改为环境变量控制,不要写死;
  • 生成新的SECRET_KEY,不要把仓库里的key直接带上线;
  • 配置ALLOWED_HOSTS,把线上域名写进列表,不配置直接访问报错;
  • 数据库建议用MySQL或PostgreSQL,字符集配置utf8mb4,避免中文乱码。

提示:SECRET_KEY、数据库密码这类敏感信息,用环境变量或.env文件管理,别直接commit进代码仓库。这个习惯从第一天就要养成。

5.2 Nginx + Gunicorn配置

宝塔部署Django,我用的方案是“宝塔安装Nginx + Python项目管理器创建项目 + Gunicorn启动”。核心链路是:Nginx接收80/443端口请求,反向代理给Gunicorn,Gunicorn跑Django应用。

Gunicorn启动命令大致这样:

gunicorn config.wsgi:application -w 2 -b 127.0.0.1:8001

然后Nginx配置里加反向代理:

location / { proxy_pass http://127.0.0.1:8001; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /static/ { alias /www/wwwroot/myproject/staticfiles/; } location /media/ { alias /www/wwwroot/myproject/media/; }

proxy_set_header这三行必须写,否则Django拿不到真实客户端IP和协议,日志、支付回调里需要判断HTTPS的场景都会出错。

5.3 collectstatic和DEBUG=False的坑

本地开发时Django自己处理静态文件,上线后DEBUG=False,Django默认不再提供静态文件服务,必须执行collectstatic把静态文件收集到STATIC_ROOT,再由Nginx alias出去。配置上要加:

STATIC_URL = "/static/" STATIC_ROOT = os.path.join(BASE_DIR, "staticfiles")

然后执行:

python manage.py collectstatic --noinput

常见坑我一次性列出来:

  • 忘记collectstatic,页面白板,控制台全是404;
  • collectstatic之后改了CSS,忘记重新收集,线上还是老样式;
  • media用户上传文件没单独配置alias,商品图片全裂;
  • 数据库迁移没跑全,线上运行时报某张表不存在。

另外,线上DEBUG=False之后,如果页面报500,默认不会显示详细错误。记得配置LOGGING,把错误日志写到文件,排查问题靠日志而不是靠页面报错。

5.4 上线后的安全自查

部署完成不代表结束。电商项目还要做几件安全自查:

  • 后台管理路径改掉,不要把/admin直接暴露在公网,或者干脆限制后台IP访问;
  • 确认支付回调接口对外可访问,但必须有签名校验,不要裸奔;
  • 所有涉及金额变动、状态变更的接口加权限校验和操作日志;
  • 定期备份数据库。我在宝塔里设置了每天自动备份,这个习惯关键时刻救命。

如果想把Django做扎实,像《Django By Example》这类实战书值得读,里面商品、购物车、订单的章节会带你走一遍完整流程。但光看书不写项目,遇到并发和部署问题照样懵。我最大的感受是:Django做电商完全够用,真正的门槛不在框架本身,而在数据模型是否干净、事务边界是否清楚、回调是否幂等、部署配置是否合理。这些恰恰是教程里最常被跳过的东西。

我的习惯是,从第一行代码开始就保留订单号和回调日志,因为线上排查支付问题,没有日志就是瞎猜。如果你正在做类似的Django电商项目,建议先把这篇文章提到的几个坑对照自己的项目自查一遍,能省下不少在凌晨排查问题的时间。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 23:57:12

WOA优化VMD参数:鲸鱼算法实现信号自适应分解实战

简介&#xff1a;压缩包内提供基于鲸鱼算法&#xff08;WOA&#xff09;优化变分模态分解&#xff08;VMD&#xff09;参数的Python完整实现&#xff0c;面向信号处理、故障诊断及参数自适应寻优场景&#xff0c;适合需要自动确定VMD中心频率与调制指数等核心参数的研究者、工程…

作者头像 李华
网站建设 2026/9/8 23:55:35

PyTorch实现对偶GAN图像去雾:从原理到工程实战

简介&#xff1a;基于PyTorch实现图像去雾的对偶生成对抗网络&#xff0c;是一个包含完整Python源码、项目说明及详细代码注释的毕业设计项目。项目针对雾气导致图像对比度下降、细节丢失等问题&#xff0c;利用生成器与判别器相互对抗的方式恢复清晰无雾图像&#xff0c;适合计…

作者头像 李华
网站建设 2026/9/8 23:55:14

AI画板实测:GPT-6 Astra在原理图与PCB设计中的能力与局限

把同一个电源域的电容分两排放在芯片两侧&#xff0c;结果回流路径被拉得很长&#xff0c;纹波指标差了30%。这种问题AI不一定能看出来&#xff0c;但要靠它把所有细节都安排到位&#xff0c;现阶段还不现实。哪些可以放心交给AI适合让GPT-6 Astra处理的&#xff0c;是那些“规…

作者头像 李华
网站建设 2026/9/8 23:52:26

RS-485收发器选型实测:从MAX485到THVD1550,8款芯片横向对比

去年秋天&#xff0c;我接手的一批无刷电机控制器在客户车间的配电柜合闸瞬间&#xff0c;总线上挂着的半双工RS-485收发器一片接一片被打穿。上位机一直报通信超时&#xff0c;现场测A、B线对地电阻&#xff0c;好几块板子只有几十欧姆&#xff0c;拆下来看&#xff0c;清一色…

作者头像 李华
网站建设 2026/9/8 23:51:25

PyTorch实战:基于CRNN+CTC的车牌识别全流程解析

1. 为什么把车牌识别当作 PyTorch 实战项目1.1 车牌识别看起来简单&#xff0c;实际上卡在哪儿先说一个反直觉的现象&#xff1a;车牌识别在工程里看起来特别成熟&#xff0c;门口停车场、高速收费口都在用&#xff0c;似乎是个“老掉牙”的需求。但当我自己把任务拆开&#xf…

作者头像 李华