ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Django花卉商城系统设计与实现——电商毕设实战指南

Django花卉商城系统设计与实现——电商毕设实战指南 最近后台收到不少毕设相关的求助其中选择最多的方向就是电商类管理系统。如果你也打算做一个商城又正好在看 Django那这篇基于 Django 的花卉商城系统设计与实现分享值得你花几分钟读完。这套系统不是我临时拼的而是完整跑通过的项目前台有商品展示、搜索筛选、购物车、下单支付后台有商品管理、订单处理、用户管理再加上配套的数据库设计、毕设文档和代码讲解一套流程全部走通。不管你是拿它做毕业论文还是想找一个 Django 实战练手的项目都能直接参考。做毕设和做企业项目有个很大的区别毕设不是功能越多越好而是“链路完整 核心亮点 能讲清楚”。所以这套系统在设计时就刻意控制复杂度没有上 Redis、消息队列这类东西而是把 Django 本身的 ORM、认证、Admin、表单这些能力吃透再用合理的业务设计把它们串起来。这样做的好处很明显开发周期短、代码自己看得懂、答辩时老师问的每个点你都能答上来。下面我按“设计思路 - 核心实现 - 代码讲解 - 部署排坑”的顺序把这套系统的底子给你扒清楚。1. 项目整体定位与方案设计1.1 选型思路为什么电商项目首选 Django很多同学在选框架时会纠结Flask 轻量、Spring Boot 主流、Node.js 生态大为什么我用 Django这里有个很现实的原因电商系统最怕的不是写业务而是写基础设施。用户登录注册、后台管理、数据库迁移、表单校验、分页、安全防护这些事几乎每个 Web 项目都躲不开。如果用 Flask这些都得自己装插件、自己组织对一个还没毕业的学生来说光是把认证和 Admin 这一套理顺就很费时间。而 Django 自带一整套“电池”尤其适合中后台管理类系统自带 Admin 后台不用写一行代码就能进入商品、订单、用户的管理界面对毕设来说是巨大的效率提升。ORM 与自动迁移改模型后一条makemigrations加migrate就能同步数据库不用手写建表语句。内置认证系统用户表、密码哈希、Session、登录装饰器都是现成的电商系统最核心的注册登录可以直接在此基础上扩展。Form 和 ModelForm处理表单提交、校验、错误提示非常顺手比手写 JS AJAX 来回传参可靠得多。Spring Boot 当然也能做但对新手来说 Java 的手感偏重而且要配置的东西多Flask 自由度太高反而容易在项目结构上翻车。Django 的“约定优于配置”和“全家桶”特性正好卡在毕设和实战项目的舒适区里。这套系统在设计时也遵循了 Django 的最佳实践一个项目包含多个 App每个 App 只负责一块独立功能。我没有把所有代码堆在一个views.py里而是拆成 user、goods、cart、order、admin 相关的几个模块这样后面讲代码、写文档、答辩拆解都轻松很多。1.2 功能模块梳理与数据库表设计商城类系统的功能看起来多但拆开就两句话用户能买管理员能管。前台是面向用户的购买链路后台是面向管理员的运营链路。前台模块我分成六个部分用户注册、登录、退出登录花卉商品的分类浏览与列表展示商品搜索支持按名称和描述模糊搜索商品详情页图片、价格、库存、简介购物车管理加入购物车、修改数量、删除订单确认、模拟支付与个人订单列表后台模块主要是对商品分类、商品信息、订单状态和用户进行管理。因为 Django Admin 自带大部分增删改查能力所以后台业务代码的编写量其实不大更多的是定制显示字段和过滤条件。数据库表设计是这套系统的地基我总共设计了六张核心表表名作用关键字段关联关系User用户信息继承 Django 内置用户表username, password, email, phone与订单、购物车关联Category花卉分类name, description, create_time一对多到商品表Flower花卉商品name, category, price, stock, image, description, status多对一到分类表CartItem购物车项user, flower, quantity关联用户和商品Order订单主表user, total_amount, status, address, create_time一对多到订单项OrderItem订单明细order, flower, price, quantity多对一到订单主表这里有一个容易被忽略的设计点订单里的商品价格必须单独存一份不能下单后查商品表因为商品价格后续可能会改动。购物车里存的是实时商品信息可以只存 quantity但订单一旦生成就要把当时的商品名称和单价快照到 OrderItem 里否则改价之后历史订单会对不上账。这个细节我每次讲代码都会重点提答辩时也是不错的加分点。分类和商品之间用外键关联删掉一个分类前需要先处理该分类下的商品否则会产生“无家可归”的孤儿数据。Django 默认的on_deletemodels.CASCADE是级联删除但这里我更推荐on_deletemodels.SET_NULL加上nullTrue这样删除分类时商品还能保留只是分类变成空实际运营场景里更安全。2. 核心业务逻辑与关键实现2.1 用户注册登录与认证细节用户模块我直接基于 Django 自带的django.contrib.auth做扩展没有重复造轮子。注册页面接收用户名、手机号、邮箱、密码和确认密码核心代码思路是这样的from django.contrib.auth.models import User from django.contrib.auth import login, logout from django.shortcuts import render, redirect def register(request): if request.method POST: username request.POST.get(username) password1 request.POST.get(password1) password2 request.POST.get(password2) if password1 ! password2: return render(request, register.html, {error: 两次密码不一致}) if User.objects.filter(usernameusername).exists(): return render(request, register.html, {error: 用户名已存在}) user User.objects.create_user(usernameusername, passwordpassword1) user.save() login(request, user) return redirect(goods:index) return render(request, register.html)这里有个新手很容易踩的坑创建用户时一定要用create_user不要直接User.objects.create()。create_user会自动帮我们做密码哈希而create()存的是明文密码一旦被看到整个系统的用户数据就等于裸奔了。登录我是用authenticatelogin处理的from django.contrib.auth import authenticate, login def user_login(request): if request.method POST: username request.POST.get(username) password request.POST.get(password) user authenticate(request, usernameusername, passwordpassword) if user is not None: login(request, user) return redirect(goods:index) return render(request, login.html, {error: 用户名或密码错误}) return render(request, login.html)页面上的登录状态通过模板里的request.user.is_authenticated来判断。如果用户没登录就点“加入购物车”我直接重定向到登录页并利用next参数在登录后跳回原页面这个交互细节很能提升使用体验很多剩作系统会忽略。受保护页面的控制我没有用装饰器硬写一堆if not request.user.is_authenticated而是用login_required一行代码搞定跳转。Django 的LOGIN_URL在 settings 里配置未登录用户访问受保护页面会自动被带到登录页。这个机制建议每个用 Django 做系统的同学都掌握也是答辩时老师大概率会问的点。2.2 商品列表、搜索与分页实现商品模块是整个系统最能展示“Django 业务基本功”的地方。列表页要解决的三个核心问题是筛选条件怎么写、分页怎么做、页面性能怎么保证。先看视图层的核心逻辑from django.core.paginator import Paginator from django.db.models import Q from goods.models import Flower, Category def product_list(request, category_idNone): keyword request.GET.get(keyword, ) products Flower.objects.filter(statuson_sale) if category_id: products products.filter(category_idcategory_id) if keyword: products products.filter( Q(name__icontainskeyword) | Q(description__icontainskeyword) ) products products.select_related(category).order_by(-create_time) paginator Paginator(products, 12) page_number request.GET.get(page) page_obj paginator.get_page(page_number) return render(request, goods/list.html, {page_obj: page_obj, categories: Category.objects.all()})说四个关键点第一查询集QuerySet是惰性的。上面写的products products.filter(...)在整个链路里只是“攒条件”真正执行 SQL 是在模板里遍历page_obj时。所以不用担心写多个 filter 会造成多次数据库查询Django 会在迭代时才执行一次。第二select_related是必加的。商品列表页必然要显示分类名称如果不在查询时用select_related(category)提前把分类查出来就会在每次循环商品时额外发一条 SQLN 个商品就是 N1 条查询。这个 N1 问题是数据库查询性能的重灾区毕设系统数据量不大可能看不出问题但在文档里写清楚优化思路就是明显的亮点。第三icontains和contains的区别要讲清楚。contains在 MySQL 里相当于 LIKE %关键词%是区分大小写的icontains不区分大小写。搜索功能一般用icontains因为对用户来说搜“Rose”和“rose”结果应该一样。第四分页器Paginator(products, 12)里的 12 是每页显示数。get_page会自动处理越界页码比如用户手动把 URL 改成?page100也不会报错而是返回最后一页。模板里我做了上一页/下一页和页码导航数据多的时候体验差异非常明显。商品详情页就简单很多直接def product_detail(request, pk): flower get_object_or_404(Flower, pkpk, statuson_sale) return render(request, goods/detail.html, {flower: flower})get_object_or_404在对象不存在时抛出 Http404 异常Django 会返回 404 页面不会让用户看到一堆丑陋的报错信息。这套系统里所有按主键查详情的操作都用了这个函数省去了大量的try...except样板代码。2.3 购物车、订单与模拟支付流程购物车有两种实现方案一种是存 Session一种是建数据库表。毕设系统数据量不大但为了后面扩展和用户跨设备购物体验我选择了建表方案也就是前面设计表时的 CartItem。加入购物车的核心逻辑from django.contrib.auth.decorators import login_required from cart.models import CartItem from goods.models import Flower login_required def add_to_cart(request, flower_id): flower get_object_or_404(Flower, pkflower_id) cart_item, created CartItem.objects.get_or_create( userrequest.user, flowerflower, defaults{quantity: 1} ) if not created: cart_item.quantity 1 cart_item.save() return redirect(cart:cart_detail)get_or_create是这里的关键。它先查“这个用户对这一个商品是否已经有购物车记录”有就返回现有记录没有就创建。如果用户连续点了两次“加入购物车”不会产生两条重复的记录而是把数量累加。这个写法比先filter再if判断要简洁得多也避免了并发下的重复插入问题。购物车页面的操作有三个改数量、删除、结算。改数量我用表单 POST 提交前端没有引入 Vue 或 React避免把毕设复杂度拉高。删除操作要注意确认弹窗否则用户手一滑购物车就清空了这个交互小细节在实际演示时非常加好感。订单流程是整个系统的重头戏也是老师比较爱问的部分。下单不再像加入购物车那样“点到为止”它要处理多张表的联动因此我引入了事务。Django 里用transaction.atomic()包裹即可from django.db import transaction from order.models import Order, OrderItem login_required def create_order(request): if request.method POST: cart_items CartItem.objects.filter(userrequest.user) if not cart_items.exists(): return render(request, cart/cart_detail.html, {error: 购物车为空}) address request.POST.get(address) if not address: return render(request, cart/cart_detail.html, {error: 请填写收货地址}) with transaction.atomic(): order Order.objects.create(userrequest.user, total_amount0, addressaddress, statuspending) total 0 for item in cart_items: if item.flower.stock item.quantity: raise ValueError(f商品 {item.flower.name} 库存不足) item.flower.stock - item.quantity item.flower.save() OrderItem.objects.create( orderorder, floweritem.flower, flower_nameitem.flower.name, priceitem.flower.price, quantityitem.quantity ) total item.flower.price * item.quantity order.total_amount total order.save() cart_items.delete() return redirect(order:order_success, order_idorder.id) return redirect(goods:index)这段代码里藏着两个容易翻车的点第一个是查库存和扣库存之间必须放在同一个事务里。如果没有transaction.atomic()假设用户下单选了 10 朵玫瑰库存只有 5 朵代码明明已经抛了异常前面的订单记录和item.flower.stock - item.quantity操作却可能已经写进数据库了结果就是订单创建失败但数据库被写脏。事务的all or nothing特性在这里是保命用的。第二个是创建 OrderItem 时把flower_name和price单独存一份。下单之后商品表改名、调价都不会影响历史订单的展示。很多同学做毕设时图省事直接存外键然后模板里去查商品实时信息表面上看功能差不多但一旦你点进“我的订单”里改过价的商品就会发现金额对不上排查半天都不知是哪里出了问题。模拟支付我没有对接真实接口而是在支付页面做一个倒计时 “确认支付”按钮点击后把订单状态从pending改为paid。毕设场景下把流程跑通比支付网关本身更重要。如果你想加点亮点可以用 Django 的信号post_save在订单状态变化时自动给用户发一条站内消息开发量和效果都很好。3. 代码讲解查询、删除与后台增强3.1 Django ORM 的查询与删除对象写法题目里特意提到了“django执行查询-删除对象”这是很多新手绕不过去的弯。Django 的 ORM 查询和删除与其说是语法问题不如说是对查询集生命周期的理解问题。先说查询。最常用的三兄弟是get、filter、exclude# get拿到唯一一条记录 flower Flower.objects.get(id1) # filter拿到满足条件的查询集 on_sale_flowers Flower.objects.filter(statuson_sale) # exclude排除掉某类记录 not_sold_out Flower.objects.exclude(stock0)get有个致命弱点如果没有记录它会抛DoesNotExist如果有超过一条它会抛MultipleObjectsReturned。所以在不确定数据库中数据是否唯一时不要轻易用 get。这就是为什么我上面的代码里用了大量get_object_or_404而不是get因为它已经把“取不到就抛 404”的逻辑帮你封装好了。删除对象的常规操作# 删除单个对象 flower.delete() # 删除查询集批量删除 Flower.objects.filter(statusoff_sale).delete()delete()返回一个元组(总删除数, 各类型详情)但别被它的返回值迷惑真正要注意的是级联删除。比如category Category.objects.get(id3) category.delete()如果 Flower 的外键是默认的on_deletemodels.CASCADE那么这条语句不只是删掉分类而是把这个分类下所有花卉商品一起删掉。这种“连坐”在一个真实的商城系统里很危险运营同学误删一个分类可能整个商品列表都空了。所以我在 1.2 节特意提到用SET_NULL而不是CASCADE前端判断category为空时显示“未分类”就行。批量更新也顺便说一下不要在循环里逐个save()# 错误示范N 次数据库写操作 for flower in Flower.objects.filter(statusoff_sale): flower.status on_sale flower.save() # 正确示范一条 UPDATE 语句 Flower.objects.filter(statusoff_sale).update(statuson_sale)这条数据量小看不出差别但数据量一大接口速度差距极其明显。这个思路也在文档的“项目优化说明”里写了一句老师看了会觉得你是真跑过项目不是光背概念的。3.2 视图、表单与模板的协作细节这段讲一下整个系统里“数据怎么从数据库流到页面上”的完整链路。Django 的 MTV 模型把一个请求拆成 Model操作数据 - Template渲染页面 - View业务逻辑 三段。我在选视图写法的时候没有全部用函数视图FBV也没有全部用类视图CBV而是按复杂度分开用。简单的一步逻辑用 FBV比如上面的product_detail需要处理列表分页的用 Django 内置的ListView能少写不少样板代码from django.views.generic import ListView from goods.models import Flower class FlowerListView(ListView): model Flower template_name goods/list.html context_object_name products paginate_by 12 def get_queryset(self): keyword self.request.GET.get(keyword, ) queryset Flower.objects.filter(statuson_sale).select_related(category) if keyword: queryset queryset.filter(name__icontainskeyword) return queryset模板渲染时变量传递靠context。FBV 里render(request, xx.html, {key: value})CBV 里就是 context_object_name 或重写get_context_data。前后端的数据交互主要靠表单表单提交必须带{% csrf_token %}否则会被 Django 的 CSRF 中间件拦下来报 403。很多新手被这个 403 折磨过其实只要记住一条只要是 POST 表单模板里就必须有{% csrf_token %}。加了之后 Django 会校验 cookie 里的 csrftoken 和表单隐藏字段是否匹配防止跨站请求伪造。再补一个模板继承的细节。我的系统里所有页面都继承自base.html公共的导航栏、尾部、资源引用写一次子模板只覆盖{% block content %}。这个习惯让整套系统的页面风格统一改导航栏只需要改一个文件也方便后期加“个人中心”这类新页面时保持整体布局。3.3 给后台管理“换皮”Django Admin 的增强配置后台我一开始直接用的是 Django 原生 Admin功能上完全没问题但外观确实比较“复古”。后来我把后台管理界面换成了Django Unfold效果提升非常明显代码改动量却很小。原生 Admin 注册模型就像这样from django.contrib import admin from goods.models import Flower, Category admin.register(Flower) class FlowerAdmin(admin.ModelAdmin): list_display (name, category, price, stock, status, create_time) list_filter (status, category) search_fields (name,) list_per_page 20这几行配置解决了很多问题list_display决定列表页显示哪些字段list_filter提供侧边栏筛选search_fields提供搜索框list_per_page控制每页条数。如果你想在后台给商品加“下架”按钮不需要写复杂的逻辑直接在actions里定义操作函数就行。Unfold 的接入也没有想象中复杂安装后把它加到INSTALLED_APPS放在默认的django.contrib.admin前面然后简单配置一下主题和侧边栏菜单即可。Unfold 带来的最直观好处是界面从“90 年代后台”变成了现代化的响应式管理面板演示的时候打开后台的一瞬间老师的观感会好很多。这背后是前端开发者把 Admin 模板做了二次封装我们作为使用者只需要关心配置不需要改源码。不过我也要提醒一句Unfold 和某些第三方 Django 管理插件有版本兼容问题安装前先看一眼它要求的 Django 版本别装完直接白屏。因为它是通过覆盖 Admin 模板工作的如果版本不匹配最常见的症状就是后台页面样式丢失排查思路我记得放在第 4 节的排错表里了。4. 部署发布与常见问题排查4.1 从本地跑通到部署上线要改哪些配置很多同学的毕设演示都是在python manage.py runserver下跑的这样交付完全没问题但如果你想正式一点把它部署到服务器上访问有几个配置必须动否则会踩到实打实的坑。第一件事是settings.py的拆分。不要把生产配置和本地配置写在同一个文件里我建议至少拆成settings_base.py、settings_prod.py和settings_dev.py通过环境变量切换。毕设项目不用搞得太复杂但“配置文件按环境隔离”这个意识要建立起来。第二件事是把DEBUG False。本地开发时 DEBUG 开启页面报错会直接显示详细的异常堆栈一旦部署到公网还开着 DEBUG别人能在报错页面看到你的源代码、数据库配置甚至密钥这属于安全事故了。关掉 DEBUG 之后记得还要补上ALLOWED_HOSTS只允许你服务器域名或 IP 访问否则 Django 会拒绝一切请求头中 Host 不匹配的请求。第三件事是静态文件和上传文件的处理。本地开发时 Django 自动帮你服务静态资源但生产环境必须通过 Nginx 或专门的静态文件服务来处理。settings 里要配置STATIC_URL /static/ STATIC_ROOT /path/to/collected_static/ MEDIA_URL /media/ MEDIA_ROOT /path/to/media/部署前先执行python manage.py collectstatic把各 App 下的静态文件统一收集到STATIC_ROOT然后把STATIC_ROOT和MEDIA_ROOT两个目录配给 Nginx 作为 alias 即可。很多新手部署完发现 CSS、图片全挂十有八九是这一步没做。我之前部署这套系统的推荐组合是Nginx Gunicorn Django MySQL。Gunicorn 用来跑 Django 应用gunicorn config.wsgi:application -w 4 -b 0.0.0.0:8000-w 4是开 4 个 worker 进程-b是绑定地址。Nginx 反向代理到127.0.0.1:8000再把对外端口设为 80。如果你只是想在本机演示跑 runserver 也行但如果要放到服务器上给室友、老师远程看这套 Nginx Gunicorn 方案最稳。还有一个小细节MySQL 的字符集要设置成 utf8mb4否则用户提交emoji表情到地址栏时入库会报Incorrect string value错误。这套系统默认用 SQLite 都能跑但提交给学校时用 MySQL 会更像“企业级”一点文档里可以把切换数据库的配置步骤写上。4.2 常见报错和排查技巧速查这部分直接给大家整理一份我项目中真实遇到过的报错清单比官方文档里的解释更贴近实战问题现象原因排查与解决思路No such table: goods_flower没有执行迁移检查makemigrations和migrate是否都跑过迁移文件是否存在relation xxx does not exist数据库和模型不同步先确认连接的是不是同一个数据库必要时对测试数据做migrate --run-syncdb模板里图片不显示MEDIA 路径没配确认MEDIA_URL和MEDIA_ROOT本地开发时在 urls 里加static() media()后台样式丢失Unfold 版本与 Django 不兼容看浏览器 Console 有无 500 报错回退版本或换 Admin 主题CSRF token missing or incorrect表单没写{% csrf_token %}POST 表单必加AJAX 方式要设置X-CSRFToken请求头django.core.exceptions.ImproperlyConfiguredsettings 里缺关键配置看堆栈指向哪个配置项按提示补上AttributeError: NoneType object has no attribute quantity购物车项取到了 None检查get_object_or_404或try except是否覆盖了不存在的情况Port 8000 is already in use上一个 runserver 没关lsof -i:8000找到进程后kill或换端口runserver 8001django.db.utils.OperationalError: no such table数据库路径不对检查DATABASES配置删除旧的 sqlite 文件重新 migrate这里重点展开一下“django执行查询-删除对象”对应的另一个坑删除对象时主键被占用导致DoesNotExist。场景是这样的用户在后台删除了某个商品但购物车表里还留着一条外键指向这个商品的 CartItem。访问购物车列表时item.flower.price会触发一次隐式查询此时商品已经不存在Django 就会抛DoesNotExist进而整个购物车页面 500。解决方案有两个一是写代码时统一用get_object_or_404并做好try...except二是在删除商品前清理关联的 CartItem。我更推荐第一种因为第二种如果忘了问题还是会漏。在实际项目里这种“外键指向已删除对象”的问题非常隐蔽表面上看代码没问题但就是时不时报错排查时多留个心眼。另外有一个很多新手会忽略的问题Django 的 时区设置。默认TIME_ZONE是UTC如果你不改成Asia/Shanghai订单创建时间会和北京时间差 8 小时。设置成TIME_ZONE Asia/Shanghai并保持USE_TZ False对很多国内应用场景最省心。如果你的业务要国际化就开USE_TZ True但毕设系统没有这个必要。4.3 答辩演示让老师觉得“这系统有东西”的细节最后聊一个很多人到答辩前才慌的问题系统功能都实现了但演示时不知道怎么讲。我总结了一套演示顺序亲测能有效提升老师的认可度。先演示前台完整购物流程从注册新用户开始说明用户名密码如何保存这里提create_user自动加密注册后去分类页挑几朵花加到购物车分别改一下数量和删掉一件最后结算下单。走到支付页面时停下来解释一下订单表和订单明细表的关系——外键快照字段flower_name和price的作用顺手提一句事务保证库存扣减和订单生成的一致。这一套走下来老师对你的代码理解能力已经认可一半了。再演示后台登录 Django Admin展示商品列表、分类筛选、下架操作以及用 Unfold 美化后的后台界面。说到后台时可以提 Admin 的list_display、search_fields这些配置说明你知道怎么用框架提升自己的开发效率而不是只会写死代码。最后留一个 1 分钟级别的“优化亮点”时间。比如你用了select_related解决 N1 查询问题你给商城加过站内消息通知你用了事务保证数据一致性。这些点不需要多挑两个讲透就行。道理很简单老师问的问题你答得越具体他就越觉得这个系统是你自己写的。如果只是背概念他追问两个“为什么”你就露馅了。我在整理这套系统时还有几个配套文件包括需求分析文档、数据库设计说明、核心代码注释版、演示脚本以及整个项目从创建到部署的完整步骤记录。文章里写的核心思路和关键代码基本就是文档里的精华。如果你把这篇文章里的每个要点都弄明白再去跑一遍我的源码那些看起来平常的代码块对你来说就是一套能拿出手的毕业设计作品了。最后再分享一个我个人带毕设项目时最深的体会判断一套系统好不好从来不是看功能有多少而是看核心链路稳不稳、你对自己代码的理解深不深。与其把精力花在堆花哨功能上不如把购物车、订单、权限、事务这几个关键点搞透。哪怕只做三个页面能讲清楚为什么这么设计也比十个页面却一问三不知要强得多。这套花卉商城系统的源码和文档我可以单独分享但我更建议你先按这篇思路自己把项目搭一遍遇到跑不动的地方再回来对照。动手踩过一遍坑才是你自己的东西。
返回列表