简介:一套以Django框架构建的完整电商系统源码,项目基于Python 3.6环境,实现了商家入驻与审核、商品发布管理、会员注册及手机验证、购物车增减、订单结算、模拟支付和省市区街道四级联动地址选择等核心业务。代码遵循MVT设计模式,通过ORM简化数据库操作,前端基于Bootstrap,适合正在学习Django Web开发的初学者和需要快速搭建电商原型的开发者参考。压缩包共561个文件,以70个py源码、55个pyc编译文件为主,辅以37个js脚本、18个html页面、16个css样式以及大量jpg、png图片素材,整体大小96.63MB,文件类型覆盖了项目运行、界面展示与交互逻辑所需的主要资源。目前已有1771人学习下载,源码可直接运行,能够帮助读者完整理解从数据建模、视图逻辑、模板渲染到订单支付流程的Django全栈实现思路,也可作为毕业设计或课程项目的改造基础。 看到这个标题,我大概能猜到你准备入坑Django电商开发了——“django框架下开发的完整版的电商”,这句话听起来简单,但真正落地其实是一整套工程。我在实际项目中用Django做过不止一套电商系统,从商品发布、购物车到订单履约、支付回调、后台运营管理,每一步都埋着不少细节。这篇文章不会去贴大段官方文档,而是按我真实做项目时的思考路径来拆,把模块划分、模型设计、常见坑位、部署要点全部过一遍。不管你是刚学完Django基础想找个完整项目练手,还是已经被安排了电商需求但心里没底,这篇对你的参考价值都很大。
1. 项目整体设计与思路拆解
1.1 “完整版电商”不等于是把一个系统做成一坨
电商项目最大的误区就是“我什么功能都要做”。商品列表、搜索排序、购物车、优惠券、秒杀、支付、物流、评价、退款售后、用户积分……你要是上来就把这些铺开,项目大概率做不完,而且维护成本极高。
我自己的做法是先把电商系统的核心链路拆出来,我会加粗一条主线:商品上架 → 用户浏览 → 加入购物车 → 创建订单 → 支付 → 订单状态流转。这个主流程之外的功能属于支线,先规划但不一定首期实现。所谓“完整版”,指的应该是主流程从用户到后端再到管理后台全部跑通,而不是功能无限堆叠。
按这个逻辑,我会把项目拆成四个业务app:
goods:商品与类目、品牌、SKU、库存users:用户注册登录、收货地址、收藏trade:购物车、订单、支付与退款coupon:优惠券、(后期可以扩展营销活动)
这样拆的好处很明显:后期新增功能时不需要改老代码的核心模型,每个人维护各自的app边界,代码结构一眼能看明白。我第一次做电商时把所有模型都塞在同一个models.py里,改一个字段牵一发动全身,后来拆开才舒坦。如果你刚开始写,不要嫌拆app麻烦,这个动作后面会帮你省下大量时间。
1.2 为什么选Django做电商后端
这不是凑热门,而是电商业务对后端框架有比较硬性的要求:
- 自带Admin后台:电商系统离不开运营后台,Django Admin可以让你在几个小时内搭出可用的后台管理界面,商品、订单、用户都能直接维护,这在项目前期非常重要。
- ORM强大:电商涉及的查询非常复杂,多表联查、聚合统计、分页排序,Django ORM加
select_related、prefetch_related用得熟练,能覆盖绝大部分场景,不需要写裸SQL。 - 安全默认值高:电商涉及资金交易和用户隐私,Django默认开启CSRF防护、SQL注入保护、XSS过滤,你不用额外做太多工作就能达到一个基础的合规水准。
- 开发效率高:一个商品的增删改查,用DRF(Django Rest Framework)写API,配合Django的表单校验和序列化器,前后端联调非常快。
当然,Django不是没有短板,比如对于高并发场景,它的同步模型确实不如Go或Node.js那套轻量,但绝大多数电商系统瓶颈都在数据库和缓存层,框架本身很少成为天花板。项目落地阶段用Django做后端是性价比很高的选择。
2. 核心数据模型设计与实现
2.1 app创建与业务边界划分
开项目第一步是创建目录和app。命令很基础,但怎么组织才是关键。我会用一个统一的命令序列把项目搭起来:
django-admin startproject ecommerce cd ecommerce python manage.py startapp goods python manage.py startapp users python manage.py startapp trade python manage.py startapp coupon创建完app后,记得在INSTALLED_APPS里依次注册,否则makemigrations不会识别模型。这里提醒一个新手常犯的问题:app名称尽量用复数或业务名词,不要叫shop、core这种太宽泛的名字,业务名词能帮你明确聚合根的边界。
usersapp还要做一件关键事:在settings.py中指定自定义用户模型:
AUTH_USER_MODEL = 'users.User'最好在一开始就做这个配置,因为Django默认的User模型字段太少了,电商用户一般需要手机号、昵称、头像、性别、积分等字段,后期再换自定义模型会非常痛苦。这个配置要在第一次migrate之前完成,否则数据库结构已经固定,再改容易出问题。
2.2 商品模型与SKU设计
电商商品模型,最核心的是**SPU(Standard Product Unit)和SKU(Stock Keeping Unit)**的区分。SPU是商品概念,比如“iPhone 15 Pro”;SKU才是具体可购买的版本,比如“iPhone 15 Pro 256G 黑色”。如果这两层不分开,后面库存、价格、规格会乱成一锅粥。
我项目里的简化模型长这样:
from django.db import models class Category(models.Model): name = models.CharField(max_length=50, unique=True, verbose_name='类目名') parent = models.ForeignKey('self', null=True, blank=True, on_delete=models.CASCADE, verbose_name='父级类目') class Product(models.Model): name = models.CharField(max_length=200, verbose_name='商品名称') category = models.ForeignKey(Category, on_delete=models.PROTECT, verbose_name='类目') sales = models.IntegerField(default=0, verbose_name='累计销量') created_at = models.DateTimeField(auto_now_add=True, verbose_name='创建时间') class Sku(models.Model): product = models.ForeignKey(Product, related_name='skus', on_delete=models.CASCADE, verbose_name='所属商品') spec = models.CharField(max_length=100, verbose_name='规格描述') price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='售价') stock = models.PositiveIntegerField(default=0, verbose_name='库存') image = models.ImageField(upload_to='skus/%Y/%m/', blank=True, verbose_name='规格图')重点说一下字段选择:
price必须用DecimalField,不要用FloatField。浮点数在二进制中无法精确表示,比如0.1 + 0.2可能会变成0.30000000000000004,金额计算出这种问题在电商平台上是致命的。stock用PositiveIntegerField从数据库层面保证不能为负数,但这个约束在并发高时不够用,后面下单时还要配合数据库锁。Category中的parent自关联用于支持多级类目,这样不用为每个层级建单独的表。on_delete=models.PROTECT用在类目上,防止商品还在挂着的时候类目被误删。
2.3 订单模型与状态管理
订单是电商系统里最复杂的模型,因为它同时关联用户、商品快照、地址、金额明细、支付流水,而且订单状态是不断流转的。
class Order(models.Model): ORDER_STATUS = ( ('PENDING_PAYMENT', '待支付'), ('PAID', '已支付'), ('SHIPPED', '已发货'), ('COMPLETED', '已完成'), ('CANCELLED', '已取消'), ) order_no = models.CharField(max_length=32, unique=True, verbose_name='订单号') user = models.ForeignKey('users.User', on_delete=models.PROTECT, related_name='orders', verbose_name='下单用户') total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='订单总额') status = models.CharField(max_length=20, choices=ORDER_STATUS, default='PENDING_PAYMENT', db_index=True, verbose_name='订单状态') receiver_name = models.CharField(max_length=50, verbose_name='收货人') receiver_phone = models.CharField(max_length=11, verbose_name='收货电话') receiver_address = models.CharField(max_length=200, verbose_name='收货地址') created_at = models.DateTimeField(auto_now_add=True, verbose_name='下单时间') paid_at = models.DateTimeField(null=True, blank=True, verbose_name='支付时间') class OrderItem(models.Model): order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE, verbose_name='所属订单') sku = models.ForeignKey('goods.Sku', on_delete=models.PROTECT, verbose_name='商品SKU') product_name = models.CharField(max_length=200, verbose_name='商品快照名称') price = models.DecimalField(max_digits=10, decimal_places=2, verbose_name='成交价') quantity = models.PositiveIntegerField(default=1, verbose_name='数量')这里有个细节值得注意:OrderItem里存了product_name和price,这叫商品快照。为什么要冗余?因为用户下单后,商品可能会改名、下架甚至改价,但订单里的交易信息必须保持下单时的状态,否则对账和售后都说不清。这个冗余字段看似重复,实际是业务上的刚需。
订单号不建议用自增ID,因为会暴露平台真实销量,而且多系统交互时容易撞号。我一般用时间戳加随机数生成:YYYYMMDDHHMMSS + 用户ID + 4位随机数。为了在并发时不重复,order_no字段必须加unique=True,创建时偶发冲突就重试一次。
状态管理方面,不要只靠改字段值,支付回调成功后要写一条流水日志,比如谁在什么时候把状态从“待支付”改成“已支付”,操作人是谁。这样出问题可以追溯。
3. 关键功能实现与踩坑记录
3.1 文件上传与图片处理:开发时很快,上线后掉链子
商品肯定要传图片,Django帮我们封装了ImageField,但实际用起来有几个坑。
我在项目里这样配置图片上传,先确保依赖装了:因为ImageField依赖Pillow,没安装的话迁移直接报错。
pip install Pillow然后settings.py里配置:
MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'模型里定义好上传字段后,如果是在Django Admin里维护,图片上传直接就能用了。但如果你是写API接口,需要接收上传文件,视图代码要注意处理request.FILES:
# 简化示例:使用DRF序列化器接收图片 class ProductImageUploadSerializer(serializers.ModelSerializer): class Meta: model = ProductImage fields = ['sku', 'image'] def validate_image(self, value): # 限制文件大小不能超过5MB if value.size > 5 * 1024 * 1024: raise serializers.ValidationError('图片大小不能超过5MB') # 技术文件类型 valid_types = ['image/jpeg', 'image/png', 'image/webp'] if value.content_type not in valid_types: raise serializers.ValidationError('只支持jpg/png/webp格式') return value我这里补充一个生产中容易忽略的点:upload_to一定按时间分目录,比如skus/%Y/%m/,这样图片分散在不同月份的目录下,单目录文件数可控,后端备份和清理也比较方便。不要把所有图片都塞到一个目录里,文件多了之后访问和备份都会变慢。
关于文件类型校验,不要只看扩展名。有人传一个shell.jpg,实际是PHP脚本或HTML文件,这种文件如果被Nginx当静态文件服务了,可能存在安全风险,建议加上content_type校验和文件头检查。用Django本身的validate_image_file_extension也能挡掉一部分,但还不够,代码里手动校验一次心里踏实。
3.2 购物车与订单流程:库存扣减必须要加锁
购物车实现有两种方案:存Cookie和存数据库。
我的建议是:未登录用户用Cookie购物车,登录后进入数据库购物车。但实际上小项目里为了省事,直接在服务端存购物车表也可以,因为一旦用户需要跨设备同步购物车,Cookie方案就要重写。
购物车的模型就三件事:用户、SKU、数量,这个很简单。复杂的是从购物车创建订单的过程,这里的库存扣减是电商并发问题最集中的地方。
我最早实现时写的代码是:
sku = Sku.objects.get(id=sku_id) if sku.stock >= quantity: sku.stock -= quantity sku.save()这段代码在并发场景下必出事故。两个请求同时读到stock=10,都判断库存够,都执行扣减,最后库存可能变成-8,或者实际卖出去的数量超过库存。
正确做法是用Django提供的select_for_update()行锁:
from django.db import transaction with transaction.atomic(): sku = Sku.objects.select_for_update().get(id=sku_id) if sku.stock < quantity: raise APIException('库存不足') sku.stock -= quantity sku.save() # 在这里继续创建订单,整个流程在一个事务里select_for_update()会锁住这行,直到事务结束,后到的请求只能等前一个执行完。这样库存就不会超卖。注意,select_for_update()必须在事务里才生效,所以外层要套transaction.atomic()。
3.3 用户认证与权限控制
Django内置的登录认证很完善,但电商要扩展。我用的是AbstractUser扩展字段,同时使用JWT做接口认证。这样Web前端和移动端都能复用同一套账号体系。
权限控制方面,区分普通用户和管理员是电商的安全底线。普通用户只能查自己的订单,不能查别人的;管理员才有权限操作商品上下架、订单发货。
class IsOwnerOrReadOnly(permissions.BasePermission): def has_object_permission(self, request, view, obj): if request.method in permissions.SAFE_METHODS: return True # 订单只能由本人操作,管理员走后台 return obj.user == request.user我发现不少刚做电商的同学上来就对每个视图写一堆if request.user.is_authenticated的判断,这样零散又容易漏。建议直接用DRF的权限类,集中管理,一个类加在视图上,比到处写条件判断干净得多。
4. 后台管理、前后端整合与性能优化
4.1 Django与Vue整合:两种模式,按项目规模选
热词里看到了“django vue整合”,这也是电商项目常见的组合。Django做后端API、Vue做前端页面,两者的整合有两种主流方式:
模式一:前端构建后由Django托管将Vue项目npm run build之后,把dist目录的内容拷贝到Django的static目录或templates目录,由Django统一托管。这种模式适合前后端由同一个人维护、部署环境简单的场景,一个Nginx站点就搞定。
模式二:完全前后端分离Django只负责提供API(DRF),Vue独立部署在Nginx上,通过/api路径或跨域访问后端接口。这种模式是电商项目的标配,适合团队协作、有独立前端开发任务的场景。
我做项目偏好第二种,因为前端发布和后端发布互不干扰,出了问题也容易定位。但如果你只是个人练习,第一种省事得多。无论哪种模式,后端API都应该保持纯粹,不要在前端页面里塞太多Django模板语法,否则前后端逻辑就混在一起了。
4.2 让Django Admin成为运营后台的利器
Django的Admin是电商项目选型时被严重低估的功能。很多教程只用默认的注册方式,实际上把Admin配置好了,能省掉一个独立的运营后台开发成本。
@admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ['order_no', 'user', 'total_amount', 'status', 'created_at'] list_filter = ['status', 'created_at'] search_fields = ['order_no', 'user__username', 'receiver_name'] ordering = ['-created_at'] # 对于金额字段,设置为只读,防止运营误改金额 readonly_fields = ['total_amount', 'order_no']实战中Admin里最常用的技巧是:
list_display把最重要的字段显示在列表页,一眼掌握订单全貌。list_filter按状态筛选,运营每天操作最多的就是查看待发货订单。search_fields支持跨表搜索,比如user__username,客服找单子会很快。inlines用来在一个页面上同时操作主表和子表,比如订单页直接看到所有商品明细,非常直观。- 不要给普通运营开Admin的全局权限,只给指定模型的管理权限,或者更稳妥的是只用自定义的Django Admin站点,把管理界面和用户端隔离。
我遇到过不少团队,不晓得Django有Admin就去专门开发了一套后台管理,开发周期至少多两周。如果你要快速交付一个电商项目,Admin用起来,这是Django实际提高交付效率最明显的地方。
4.3 查询性能优化:Preload的必要性
Django默认是懒加载,访问外键时才会再查一次数据库,如果列表页循环里有10条订单、每条订单查20个关联对象,就会产生200条SQL。这个问题在电商后台尤其明显,商品列表、订单列表非常容易把数据库拖死。
解决方案很简单,查询时用select_related(针对外键、一对一):
orders = Order.objects.select_related('user').all()用prefetch_related(针对多对多、反向关联):
orders = Order.objects.prefetch_related('items__sku').all()加上之后,数据库查询数量从几百条降到几条。电商项目的性能优化,第一步永远是看ORM有没有产生N+1查询,这是性价比最高的方式。除此之外,Redis缓存热门商品的详情、在列表页缓存前几页的数据,也能明显降低数据库压力。
5. 部署上线与常见问题
5.1 用宝塔面板部署Django的完整步骤
项目开发完之后,部署上线又是一个关卡。我最近几次部署用的都是宝塔面板,它对Python项目的支持比较成熟,步骤如下:
- 在宝塔中安装Python项目管理器,创建一个新项目,Python版本选3.10或更高。
- 把代码上传到服务器,创建虚拟环境,安装依赖:
pip install -r requirements.txt- 安装
gunicorn,用Gunicorn启动Django应用:
gunicorn ecommerce.wsgi:application -b 127.0.0.1:8000 --workers=3 --timeout=60workers数量一般设为CPU核心数 * 2 + 1,比如2核4G服务器就设5个左右,太多了反而会因为上下文切换降低效率。
- 在Nginx里配置反向代理:
server { listen 80; server_name your_domain.com; location /static/ { alias /www/wwwroot/your_project/static/; } location /media/ { alias /www/wwwroot/your_project/media/; } location / { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有两个容易踩的坑:
- 部署前一定要执行
python manage.py collectstatic收集静态文件,否则Django Admin的CSS、JS全部加载不出来。 settings.py里要把DEBUG = False,同时设置ALLOWED_HOSTS = ['你的域名'],否则访问会被拒绝。
数据库建议用MySQL,而不是默认的SQLite。上线后我用MySQL才意识到SQLite在并发写入时容易锁库,电商场景数据量一大就顶不住。你可以用宝塔一键装好MySQL,然后修改DATABASES配置,再用makemigrations和migrate同步表结构。
5.2 线上常见问题与排查速查表
电商项目上线后,问题往往集中在下面几个地方,我整理成一张排查表,每个问题后面是我实际用的排查思路。
| 问题现象 | 大概率原因 | 排查方法 |
|---|---|---|
| 用户头像/商品图片打不开 | Nginx没配/media/代理,或MEDIA_ROOT路径不对 | 先访问/media/xx.jpg确认报错类型,再看Nginx日志 |
| Django Admin样式全丢 | 没执行collectstatic | 执行命令,确认STATIC_ROOT和Nginx静态目录指向同一路径 |
| 数据库连接数超限 | MySQL最大连接数默认偏小,而Django每条连接不释放 | 调大max_connections,或使用连接池工具 |
| 订单状态一直显示待支付 | 支付回调地址没有配置到公网可达的URL | 检查支付平台异步通知地址,不能用内网IP |
| 接口访问很慢 | 框架代码N+1查询,或Redis未缓存热点数据 | 开Django Debug Toolbar查看SQL次数,定位后优化 |
我一般排查问题时有个习惯:先把DEBUG打开临时看错误栈,定位完立刻关掉。生产环境日志必须记录到文件,前端调用接口出错时看Django的日志文件,比前端控制台报错信息要完整得多。
最后,分享一下我做这套项目的心得
做Django电商项目,最大的感受到后期越明确:千万不要被“完整版”三个字迷惑,先把从用户下单到支付成功这条主链路跑通,再去填支线功能。模型设计上,用户模型和订单模型一定要留扩展空间,但功能上要克制,先做最少可行版本。我最初设计订单表时加了20多个字段,最后实际用上的只有一半,后期的业务调整反而被多余字段绑住了手脚。电商系统对细节的要求比普通管理系统高很多,库存扣减、金额精度、订单状态流转这些地方,每一步都要想清楚为什么这么设计。如果能从头再做一遍,我会把更多精力放在接口的健壮性上而不是堆功能,因为线上出问题时,能快速定位并恢复才是最重要的。
本文还有配套的精品资源,点击获取