每次有人拿着“基于Python的网上产品销售网站”这种题目来跟我聊,我第一反应就是想问一句:你想要的是能交差的演示品,还是真能放上线跑的系统?这两个目标看起来差不多,实际做起来差别极大。Python做电商网站这个方向确实经久不衰,课程设计里年年有,个人项目里想做完整业务闭环的人也常选它,但网上随便一搜的大把开源代码,大部分只是“能打开、能列商品、能加购物车”的壳子,库存怎么不超卖、订单状态怎么流转、支付回调怎么处理、部署之后静态文件为什么全挂,这些真正影响可用性的细节反而没多少人讲。
这篇文章我会把这个项目的设计思路和实现路径完整捋一遍,适合正在做课程设计或毕业设计的同学,也适合学了Python想用完整项目练手的新人。我会从需求拆解讲到数据库表设计,从前台下单链路讲到后台管理,最后聊一聊部署上线时那些你必须提前知道的坑。
1. 动手前先拆需求:这个网上销售项目到底要做什么
1.1 同一个标题,背后是两条完全不同的路
“网上产品销售网站”这个题目,能做浅也能做深。浅的版本:商品列表、商品详情、购物车、登录注册、下单,然后假装支付成功,所有数据都能存进数据库就行。深的版本:要处理库存并发、订单状态机、后台商品上下架、支付回调对账、部署到服务器之后真实可访问。
我建议所有拿到这个题的人,先做一次需求分级。
必须有的核心功能:
- 商品模块:分类、列表、详情、搜索、上下架
- 用户模块:注册、登录、个人信息
- 交易模块:购物车、订单、支付、订单状态
- 后台模块:商品管理、订单管理、用户管理
可以往后放的功能:
- 商品规格(颜色、尺码、版本)
- 优惠券、满减、会员价
- 评价、收藏、浏览历史
- 物流跟踪、售后流程
- 数据统计报表
我见过不少人一上来就纠结评论功能怎么做、轮播图要不要加,结果核心的交易链路都没走通。项目不是功能越多越好,而是关键路径全都闭环才行。
1.2 角色和主流程:把用户故事画出来
做设计阶段,不需要写复杂文档,但至少要把两条主流程画清楚。
买家这条线是:注册登录 → 浏览/搜索商品 → 进入详情 → 加入购物车 → 提交订单 → 完成支付 → 查看订单状态。
管理员这条线是:登录后台 → 管理商品(新增、编辑、上下架) → 处理订单(发货、取消) → 查看用户列表。
你会发现,所有功能都围绕这三类核心数据转:商品、用户、订单。所以数据库建模阶段把这三块弄扎实,后面写代码会顺很多。我每次设计这类系统,都是先拿一张纸把这三块表和它们的关系画出来,画完才动手建项目。
1.3 技术栈的边界:这一版做到哪里算完
还有一件事值得提前说清楚:技术选型不是越潮越好。这个题目的核心是业务逻辑,不是框架炫技。我用过一堆花哨的方案,最后回头发现,稳定、常见、好部署才是第一位的。选一套主流组合,出了问题搜得到答案,这才是最实在的。
2. 技术选型:为什么Python配Django是这类项目的最稳组合
2.1 Python做电商网站,优势其实在业务层
很多人说Python慢,做电商不合适,这话不能说全错,但要分场景。对中小型的网上销售网站来说,瓶颈从来不在语言的执行速度,而在开发效率和业务迭代速度。Python语法简练,Django自带ORM、模板引擎、表单处理、后台管理、认证系统,一个人两三周就能把一个完整项目搭起来。换成Java那一套,同样规模的项目准备工作就多不少。
电商网站本质上是数据和流程密集型的系统:商品要存,价格要算,订单要流转,库存要扣。Python的ORM和数据处理能力正好匹配这个需求。你不必自己写复杂的JDBC,也不用纠结手动拼SQL,模型定义好之后,查询、关联、事务都有一套成熟方案。
2.2 Django与Flask的取舍:选错了后面会补课
这是拿到题目之后第一个需要做的选择题。我用一张表把关键差异列出来。
| 对比维度 | Django | Flask |
|---|---|---|
| 上手体验 | 全家桶一步到位,概念较多 | 轻量起步,hello world很快 |
| ORM与数据库迁移 | 自带ORM和migration体系 | 需要自己装SQLAlchemy并整合 |
| 后台管理 | 自带Admin,零代码可用 | 需开发或用第三方扩展 |
| 表单与校验 | 自带Form体系 | 需自己处理 |
| 适合场景 | 功能完整的业务系统 | 轻量API、前后端分离项目 |
| 学习成本 | 前期陡峭,后面省心 | 前期平缓,后期补功课 |
我的建议很明确:如果项目要求的是完整网站形态,包括后台、登录、表单提交这些页面,直接选Django。Flask不是不行,但你在做完功能之后会发现有一堆通用事情(CSRF保护、数据库迁移、Admin后台)都是后来才想起来补的,平白多花时间。这不是说Flask不好,而是在这个题目里,Django的出场成本更低。
2.3 除了Web框架,这套技术栈还需要什么
- 数据库:MySQL,业务数据都在这里;开发阶段可以先跑SQLite,部署时切MySQL
- 缓存/会话:Redis,用于首页缓存、验证码、并发锁场景(量不大时可以不引入)
- 前端:模板渲染为主,配合少量JavaScript;不要一上来就上前后端分离,那会让这个项目的复杂度翻倍
- 部署:Nginx + Gunicorn,云服务器一台就够
技术栈越少越好。能用框架自带的能力解决,就不要自己造轮子。
3. 数据库设计:商品、用户、订单三张核心表撑起整个系统
3.1 商品设计:先搞清楚SPU和SKU的区别
如果你只是卖“一本书”“一个杯子”这种没有规格差异的商品,一张商品表就够。但稍微想远一点,卖个T恤有颜色、尺码,卖个手机有存储版本,这时候商品就不是单一实体了,而应该拆成两层:SPU(商品)和SKU(具体售卖单元)。
SPU是“这个东西叫什么”,SKU是“哪一款能买”。比如“某款运动T恤”是SPU,“白色/M码/ 79元/ 库存200件”是SKU。详情页展示SPU信息,选择和下单时对应到SKU。
Django模型可以这样写:
class Category(models.Model): name = models.CharField(max_length=100) parent = models.ForeignKey("self", null=True, blank=True, on_delete=models.SET_NULL) class Product(models.Model): name = models.CharField(max_length=200) category = models.ForeignKey(Category, null=True, on_delete=models.SET_NULL) description = models.TextField(blank=True) main_image = models.ImageField(upload_to="products/", blank=True) is_active = models.BooleanField(default=True) created_at = models.DateTimeField(auto_now_add=True) class SKU(models.Model): product = models.ForeignKey(Product, related_name="skus", on_delete=models.CASCADE) spec = models.CharField(max_length=100, help_text="例如:白色 / M码") price = models.DecimalField(max_digits=10, decimal_places=2) stock = models.IntegerField(default=0)第一次做项目的人往往觉得这是过度设计,但等到需要给商品加规格的时候,再回头改表结构,牵涉的代码面就很广了。宁可一开始多建一张表,也不要等到后面返工。
3.2 用户模型:别在自建登录上浪费时间
Django自带的用户认证系统已经处理好了注册、登录、会话、密码加密这些事,不要重复实现。重复造轮子的典型后果是什么?密码用MD5直接存明文,登录态管理混乱,密码重置没人做。这些都是安全硬伤。
如果要加手机号、头像、收货地址这类用户信息,有两种常规思路:一是直接用AbstractUser扩展用户表,二是建一个独立的Profile表与User一对一关联。我倾向后者,因为Django的User模型在很多内部逻辑里被引用,扩展字段尽量不往上面堆。
3.3 订单表与订单明细表:金额必须做快照
订单是这个系统里最重要的数据,设计上两个原则必须遵守。
第一,订单金额不能下单后去商品表现算。商品改价很常见,但你三个月前的订单不能因为现在商品涨了价就金额变了。所以下单那一刻的单价、总价都要存进订单表。
第二,订单明细里要冗余商品名称和规格描述。商品以后可能改名、下架、删除,但订单历史记录必须保持原样,否则售后查单时看到的全是“商品已不存在”,那体验就太差了。
class Order(models.Model): order_no = models.CharField(max_length=32, unique=True) user = models.ForeignKey(User, on_delete=models.PROTECT) total_amount = models.DecimalField(max_digits=10, decimal_places=2) status = models.IntegerField(default=0) receiver_name = models.CharField(max_length=50) receiver_phone = models.CharField(max_length=20) address = models.TextField() created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, related_name="items", on_delete=models.CASCADE) product_name = models.CharField(max_length=200) sku_spec = models.CharField(max_length=100) price = models.DecimalField(max_digits=10, decimal_places=2) quantity = models.IntegerField()这里有个细节要注意:价格字段用DecimalField,不要用FloatField。浮点数在计算机里是近似表示,两个浮点数做加减乘除会出现0.1加0.2不等于0.3的问题。金额用浮点数存储,等到金额累计、对账的时候,精度错误会让你怀疑人生。钱的场景一律用定点小数。
3.4 订单状态用整数枚举,别用字符串满天飞
订单状态建议用整数常量表示,然后在代码里定义成类常量。比如:
class OrderStatus: PENDING_PAYMENT = 0 # 待支付 PAID = 1 # 已支付 SHIPPED = 2 # 已发货 COMPLETED = 3 # 已完成 CANCELLED = 4 # 已取消为什么不用字符串?一是整数在数据库里占空间小,二是防止“已支付”“支付完成”“已完成支付”这种同义不同名的脏数据。用了整数枚举,状态流转时候的判断逻辑也好写。另外,每个状态的切换条件要明确:待支付可以取消,已支付可以发货,已发货可以完成。这些规则后面做后台操作时会反复用到。
4. 商品浏览到下单支付:前台链路的实现思路与关键坑
4.1 商品列表与搜索:分页、过滤、排序是基本功
前台第一屏就是商品列表,这里最容易做粗糙的地方有三个:分页、分类过滤、搜索。
分页不能直接在视图里用列表切片硬算页码,Django自带Paginator,用起来很方便:
from django.core.paginator import Paginator from django.shortcuts import render def product_list(request): products = Product.objects.filter(is_active=True) keyword = request.GET.get("keyword", "") category_id = request.GET.get("category", "") sort = request.GET.get("sort", "new") if keyword: products = products.filter(name__icontains=keyword) if category_id: products = products.filter(category_id=category_id) if sort == "price_asc": products = products.order_by("price") elif sort == "price_desc": products = products.order_by("-price") else: products = products.order_by("-created_at") paginator = Paginator(products, 12) page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) return render(request, "products/list.html", {"page_obj": page_obj})这里的逻辑就是把用户选择的条件一项项叠加到查询集上,最后统一分页。注意Paginator拿到不存在的页码时会抛异常,但get_page()方法会自动处理成返回最后一页,体验更友好。
4.2 购物车:数据库方案还是Cookie方案,第一版怎么选
购物车有两种常见实现路径。
一种是游客购物车,用Cookie或localStorage存SKU和数量,好处是用户不登录也能加购,坏处是换设备数据丢失,后端无法干预。另一种是登录购物车,用数据库表存,用户登录后才能用,后台可以管理和统计。在这个项目里,我建议第一版直接做数据库购物车。理由很简单:实现简单,逻辑直观,而且电商网站下单本来就必须登录。
class CartItem(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE) sku = models.ForeignKey(SKU, on_delete=models.CASCADE) quantity = models.IntegerField(default=1) class Meta: unique_together = ("user", "sku")购物车页面做的事情很简单:遍历当前用户的CartItem,关联SKU和Product,算出每行小计和总价。加购接口要注意两点:一是同一个SKU被重复加入时应该更新数量而不是新增记录,这靠unique_together约束实现;二是加入购物车时要检查SKU是否处于可售状态(is_active),下架商品不能让用户加入。
4.3 下单与库存扣减:这是整个项目最核心的并发坑
购物车里挂着商品,用户提交订单,最危险的一步来了:库存扣减。
很多初版代码是这么写的:先读库存,判断库存够不够,够了就扣,不够就提示。这套逻辑在单用户单进程时没问题,但只要两个人同时买最后一个商品,两个请求都可能读到库存为1,然后都判断“够”,都扣成0,结果超卖。
正确做法是使用事务和行级锁,把“读库存判断 + 扣减”变成一个原子操作:
from django.db import transaction @transaction.atomic def create_order(user, sku_id, quantity): sku = SKU.objects.select_for_update().get(id=sku_id) if sku.stock < quantity: raise ValueError("库存不足") sku.stock -= quantity sku.save() # 创建订单和订单明细的记录……select_for_update()会在数据库层面给这一行加排他锁,直到事务提交才释放。第二个请求过来时,如果第一个请求还没提交,它只能等待;等锁释放后,它读到的库存已经是扣减后的值,这时候自然就发现不够了。
这个坑我第一次做的时候根本没意识到,直到用两个浏览器同时下单测试才暴露。后来想想,电商系统的技术难点基本都集中在“同时发生”的事件上,想明白这一点,你就知道为什么一定要用事务和锁了。
4.4 支付模块:模拟支付和真实支付各怎么做
课程设计阶段,支付可以做一个模拟收银台:用户提交订单后跳到支付页面,页面展示订单号和应付金额,点“确认支付”后把订单状态改成已支付。这个流程虽然不产生真实资金,但把支付前后状态变化的体验完整模拟出来了。
如果未来要接真实支付,流程会复杂一些,但核心逻辑也就三步:
- 用户下单后,后端调用第三方支付平台的统一下单接口,拿到支付链接或二维码
- 用户付款成功后,支付平台向你配置的回调地址发送通知
- 后端收到回调,校验订单号和金额,确保和下单时一致,再把订单状态置为已支付
这里有个关键经验:订单状态不能只靠前端跳转页返回就改。前端跳转可以伪造,而且支付平台支付成功后一定会回调,要把回调当作状态更新的唯一可靠来源,同时对重复回调做幂等处理。
5. 后台管理和订单流转:别让管理端变成摆设
5.1 Django Admin直接用,还是有选择地改造
很多教程喜欢教人从零写一套后台管理系统,包括登录页、侧边栏、数据表格、表单页,工作量相当大。但如果你用的是Django,框架自带的Admin已经能覆盖这个项目80%的管理需求。
我的建议分两种情况。
课程设计和毕设,直接注册模型到Admin,然后配置好显示字段、筛选条件、搜索字段,界面虽然朴素但功能齐全,导师演示完全够用。
如果是想上线给别人用,Admin也可以作为第一版的管理后台先用着,等业务流程稳定后再考虑基于Vue或者模板写一套更美观的定制后台。后台系统的核心是数据操作,不是视觉效果,先把业务跑通更重要。
5.2 把Admin配置到“能用顺手”的程度
只注册模型是不够的,默认的列表页不显示关键字段,根本没法管理。至少要配置列表字段、筛选器、搜索框,还有细节页里哪些字段可编辑。
@admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ["name", "category", "is_active", "created_at"] list_filter = ["is_active", "category"] search_fields = ["name"] @admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display = ["order_no", "user", "total_amount", "status", "created_at"] list_filter = ["status"] readonly_fields = ["order_no", "total_amount", "created_at"]一对多管理也是个常用技巧。在SKU管理页里,希望直接看到对应商品、编辑价格和库存,可以用InlineModelAdmin把SKU嵌套在Product页面里管理,这样一眼就能看到所有规格。
5.3 后台操作权限:普通用户和管理员绝对不能混在一起
Django自带auth框架,里面已经实现了用户、组、权限的完整体系。后台注册的模型,默认就支持按模型粒度设置增删改查权限。
你需要做的事情其实很简单:给管理员账号一个超级用户身份,普通注册用户只保留前台使用权限。后台入口一般用原有的/admin就能用,但如果你改了后台地址,也要注意不能把修改后的路径暴露在前台页面上,避免让人直接猜到管理入口。
5.4 订单操作的完整状态流转
后台处理订单的核心就是状态的合法流转。待支付订单可以被买家取消;已支付订单,管理员可以标记发货;已发货订单,买家确认收货后变成已完成;已取消订单不可再支付和发货。
if order.status == OrderStatus.PAID and action == "ship": order.status = OrderStatus.SHIPPED order.save()我的经验是,所有状态修改操作都集中在后台统一处理,前台只能提交指令,不要直接改状态字段。这样出了问题好排查,也方便以后加操作日志。
6. 部署上线与踩坑检查:把网站真正跑起来
6.1 部署方案:不用太复杂,够稳就够
本地开发环境通常是python manage.py runserver,但这个命令只能用于开发调试,不能用来对外提供服务。实际部署建议用这套常见组合:Gunicorn启动Django应用,Nginx做反向代理和静态文件服务,MySQL存数据,Redis按需引入做缓存。
核心部署步骤大致是这样:
- 服务器上创建Python虚拟环境,安装
requirements.txt - Django的
settings.py里把DEBUG设为False,填上服务器的域名或公网IP - 执行
python manage.py migrate迁移数据库,执行collectstatic收集静态文件 - 用Gunicorn启动服务:
gunicorn mysite.wsgi:application --bind 127.0.0.1:8000 - 配置Nginx把80端口请求转发到8000,同时为静态文件和媒体文件设置别名
- 用
systemd守护Gunicorn进程,保证重启服务器后服务能自动拉起
6.2 静态文件和上传图片为什么会全挂
这是部署阶段最高频的问题,没有之一。本地运行时,Django开发服务器会帮忙处理静态文件;但一旦进入生产模式(DEBUG=False),Django默认不再托管静态文件,你必须先执行collectstatic把所有静态资源收集到一个目录,然后在Nginx里指向这个目录。
上传的图片属于媒体文件,Nginx同样要单独配置:
location /static/ { alias /var/www/mysite/static/; } location /media/ { alias /var/www/mysite/media/; }这两个location缺一不可。只配置其中一个,就会出现页面打开正常但图片全裂的情况。我见过很多人部署完网站,首页文字正常,商品图片全挂,折腾半天最后发现就是media目录没配。
6.3 上线后常见问题自查清单
| 现象 | 常见原因 | 处理方式 |
|---|---|---|
| 打开域名显示500 | ALLOWED_HOSTS未包含该域名或IP | 在settings里添加 |
| 页面能开,图片CSS全无 | 静态文件未collect或Nginx未配置 | 执行collectstatic并检查配置 |
| 提交表单报CSRF错误 | 模板缺少csrf_token | 在form里加{% csrf_token %} |
| 时间显示与本地差8小时 | USE_TZ设置导致 | TIME_ZONE = "Asia/Shanghai" |
| 修改代码不生效 | Gunicorn没重启 | 重启systemd服务 |
| 首页加载很慢 | 大列表未分页或未加索引 | 列表页分页,常用查询字段加索引 |
6.4 上线前最后检查的几件事
部署完成不代表交付结束。我每次上线前都会过一遍这几项:DEBUG必须为False,SECRET_KEY不能出现在公开仓库里,数据库要有备份方案,后台管理员账号必须用强密码。本项目的数据库里存着用户密码哈希和订单数据,这个底线必须守住。
另外,迁移和collectstatic操作要在部署之前执行到位,上线后再发现数据库缺字段或静态文件缺失,就会影响真实用户了。
做这个项目的过程中,我最大的体会是:网上销售网站看着功能简单,真正做起来每一个环节都有值得抠细节的地方。设计阶段花时间把表结构和状态流转想透,实现阶段严格按流程来,部署阶段把静态文件和进程守护理顺,项目从头到尾会顺畅很多。如果你也正在做这个题目,我给你的建议是从数据库建模开始动手,先把商品、用户、订单这三块地基打好,后面加页面、加逻辑都只是往骨架上填肉。
我最后还想再补一句:选择Python做这类项目,最大的优势不是语言本身,而是它能让你把有限的时间花在业务逻辑的思考上,而不是跟底层细节纠缠。趁这个机会把电商系统的数据模型和交易流程吃透,以后做再复杂的后端项目,很多思路都是通用的。祝你能把这条链路完整走一遍,收获会比看十篇教程都大。