Django4+Vue网上鲜花销售系统:从模型到部署的完整实践
2026/9/16 14:19:07 网站建设 项目流程

简介:这是一份基于Django+Vue的毕业设计网上鲜花销售系统完整项目,主要面向计算机相关专业毕业生及需要进行Web全栈练习的开发者。系统围绕鲜花展示、购物车、订单管理、用户管理、商家后台等核心模块展开,覆盖了从商品浏览、下单支付到订单跟踪的完整业务链路,适合用于毕业设计答辩或课程项目参考。压缩包共231个文件,包含93个Python代码文件、22个HTML与22个JS页面逻辑、11个CSS样式文件,以及PSD设计稿、JPG/PNG图片素材和docx说明文档,资源包整体约10.91MB,目录结构清晰便于按模块查阅。该资源已有194人学习,内容能帮助读者快速理解前后端分离开发思路、Django后端接口设计与Vue前端交互实现。拿到资源后可同时获得可运行代码、界面切图与设计源文件,对完成论文撰写、系统演示和二次开发均有较高参考价值。

1. 网上鲜花销售系统:一套Django 4 + Vue的完整可交付骨架

毕业设计选“网上鲜花销售系统”的人不少,但多数代码到手是零散的:前端页面和后端接口互相不认,CSS散落十几个文件不知道哪个属于哪个页面。这个项目的价值在于整理了一条完整链路:后端按 Django 4.x(项目标注为 4.26,对应官方版本线里的 4.2.x LTS)组织,前端用 Vue 配合一整套按页面拆好的样式表,index.css 管首页、list.css 管商品列表、detail.css 管详情页、login.css 管登录、user_center.css 管个人中心、dashboard.css 管商家后台,flowery.css 与 iconfont.css 负责全站主题与图标字体。从用户浏览、购物车结算到后台订单处理,链路是通的。

这套系统解决的核心问题是分工清晰:页面样式按模块拆分,后端视图按业务拆分,模板与静态资源分离。适合两类人。一是拿它做毕业设计答辩的人,可以从订单状态机、库存扣减这些点深入讲设计动机;二是想快速搭一个电商演示站的开发者,改改模型字段和样式就能复用到其他售卖场景。前者把它当系统设计练习,后者把它当脚手架。下面按技术模块拆开讲,命令和参数尽量给到能直接复现的程度。

2. Django模型与URL路由:先让商品数据能查、能看

2.1 商品、分类、订单的数据模型设计

一个鲜花销售系统最核心的数据关系是:分类 Category、鲜花 Flower、订单 Order、订单明细 OrderItem,加上 Django 自带的 User。写模型时几个字段细节值得注意。

# flower/models.py from django.db import models from django.contrib.auth.models import User class Category(models.Model): name = models.CharField('分类名', max_length=50) parent = models.ForeignKey('self', blank=True, null=True, on_delete=models.CASCADE) def __str__(self): return self.name class Flower(models.Model): name = models.CharField('花名', max_length=100) category = models.ForeignKey(Category, related_name='flowers', on_delete=models.CASCADE) price = models.DecimalField('价格', max_digits=8, decimal_places=2) stock = models.PositiveIntegerField('库存', default=0) language = models.CharField('花语', max_length=255, blank=True) color = models.CharField('颜色', max_length=50, blank=True) image = models.ImageField('主图', upload_to='flowers/%Y%m/', null=True, blank=True) detail = models.TextField('商品详情', blank=True) is_on_sale = models.BooleanField('是否上架', default=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) def __str__(self): return self.name class Order(models.Model): STATUS_CHOICES = ( ('pending', '待支付'), ('paid', '已支付'), ('shipped', '已发货'), ('done', '已完成'), ('canceled', '已取消'), ) user = models.ForeignKey(User, related_name='orders', on_delete=models.CASCADE) status = models.CharField('订单状态', max_length=20, choices=STATUS_CHOICES, default='pending') total_price = models.DecimalField('订单总额', max_digits=10, decimal_places=2, default=0) created_at = models.DateTimeField('下单时间', auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, related_name='items', on_delete=models.CASCADE) flower = models.ForeignKey(Flower, on_delete=models.CASCADE) quantity = models.PositiveIntegerField('数量') price = models.DecimalField('成交单价', max_digits=8, decimal_places=2)

三个容易忽略的点。Flower.price 和 OrderItem.price 是两份价格:商品表里的价格是当前售价,订单明细里的价格是成交单价,商品改价不影响历史订单,这是电商项目的常规做法。stock 用 PositiveIntegerField,天然不允许库存为负数,比在视图里硬判断要严谨。Order 里冗余 total_price 字段,避免每次展示订单列表都做跨表聚合,属于典型的读多写少优化。

模型定义完成后,生成并应用迁移:

python manage.py makemigrations flower python manage.py migrate

makemigrations 把模型变化翻译成迁移文件,migrate 把迁移应用到数据库。

提示:如果执行 makemigrations 报 Unknown command,优先检查 settings.py 的 INSTALLED_APPS 是否注册 flower 应用,而不是急着重装 Django。

几个关键字段的选型理由整理如下:

字段/属性用途选型注意点
price当前销售价用 DecimalField,避免 FloatField 的二进制浮点误差
language花语CharField,只做展示,不参与计算
stock库存PositiveIntegerField,为 0 时前端应隐藏加购按钮
total_price订单总额冗余存储,下单时一次性写入
is_on_sale上下架开关列表页查询必须过滤,否则下架商品仍可通过 URL 访问

2.2 商品列表与详情的 URL 路由

项目的 list.css 和 detail.css 对应商品列表、商品详情两个页面。Django 里最直接的方式是用通用视图减少样板代码。

# flower/views.py from django.views.generic import ListView, DetailView from .models import Flower class FlowerListView(ListView): model = Flower template_name = 'list.html' context_object_name = 'flowers' paginate_by = 12 def get_queryset(self): qs = super().get_queryset() qs = qs.filter(is_on_sale=True) category = self.request.GET.get('category') if category: qs = qs.filter(category_id=category) return qs class FlowerDetailView(DetailView): model = Flower template_name = 'detail.html' context_object_name = 'flower'

ListView 的 get_queryset 做了两件事:过滤出上架商品,支持按分类参数筛选。paginate_by = 12 表示每页 12 条,Django 会把分页器对象 page_obj 传给模板,模板里用{% for flower in flowers %}遍历当前页数据,用{% for page in page_obj.paginator.page_range %}渲染分页码。

URL 路由配置在 config/urls.py:

from django.urls import path from flower.views import FlowerListView, FlowerDetailView urlpatterns = [ path('list/', FlowerListView.as_view(), name='flower_list'), path('detail/<int:pk>/', FlowerDetailView.as_view(), name='flower_detail'), ]

detail 的 URL 把主键 pk 作为路径参数,模板里用{% url 'flower_detail' pk=flower.pk %}生成链接。这样 URL 路径调整时模板不用跟着改,是 Django 推荐的命名 URL 做法。

2.3 模板加载链:从 common.css 到 list.css 的按需引入

页面能正常显示样式的前提是 Django 能找到静态文件。项目里 common.css 全站引用,list.css 只在列表页出现,是典型的按需加载。

<!-- templates/list.html --> {% extends "base.html" %} {% load static %} {% block extra_css %} <link rel="stylesheet" href="{% static 'css/common.css' %}"> <link rel="stylesheet" href="{% static 'css/list.css' %}"> {% endblock %}

模板头部先{% load static %},再用{% static %}把资源路径映射为实际 URL。这一步依赖 settings.py 的配置:

# settings.py STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static']

STATICFILES_DIRS 指向那堆 css 文件所在的 static 目录,开发环境下 Django 会自动从这个目录提供静态文件。生产环境要执行 collectstatic 把所有静态文件收拢到 STATIC_ROOT,再交给 Nginx 托管,第 5 章展开。

另外注意,这个项目的 Vue 部分不承担路由,而是负责页面内的交互组件:商品卡片上的数量加减、购物车角标、轮播切换,都由 Vue 实例挂载到 Django 渲染出的 DOM 节点上。模板出首屏,Vue 做交互,两者在 list.html、detail.html 这类模板里各占其位。

3. 购物车与订单结算:把 session 里的临时数据变成正式订单

3.1 购物车存 session 还是存数据库

购物车有两种典型实现:建一张 CartItem 表存数据库,或者把购物车数据放在 session 里。这个项目采用后者,原因在于购物车属于临时意图,游客没登录也可能往购物车里加东西,强行要求登录再购物会提高跳出率。

session 购物车的结构是一个嵌套字典:外层 key 是商品 ID,内层是商品名、单价、数量。视图里的读写模式如下:

# cart/views.py from django.shortcuts import redirect, get_object_or_404 from flower.models import Flower def cart_add(request, flower_id): flower = get_object_or_404(Flower, pk=flower_id, is_on_sale=True) cart = request.session.get('cart', {}) key = str(flower.id) item = cart.get(key) if item: item['quantity'] += 1 else: cart[key] = { 'name': flower.name, 'price': str(flower.price), 'quantity': 1, } request.session['cart'] = cart return redirect('cart_detail')

两个关键细节。第一,price 转成字符串存储,因为 session 需要 JSON 序列化,Django 的 Decimal 类型不能直接进 JSON,展示价格时用 Decimal() 转回来再做乘法。第二,request.session['cart'] = cart必须显式赋值。Django 的 SessionMiddleware 只有在 session 键被重新赋值时才会标记修改,如果只改内部字典而不重新赋值,请求结束购物车数据不会持久化,这是新手最容易踩的坑。

3.2 修改数量与删除商品

购物车操作不止“加一件”,还要支持增删改。用一个统一视图处理数量修改和删除比较省事:

def cart_update(request): if request.method == 'POST': flower_id = request.POST.get('flower_id') quantity = int(request.POST.get('quantity')) cart = request.session.get('cart', {}) key = str(flower_id) if key not in cart: return redirect('cart_detail') if quantity <= 0: cart.pop(key, None) else: cart[key]['quantity'] = quantity request.session['cart'] = cart return redirect('cart_detail')

quantity 小于等于 0 时直接删掉该键,前端“删除”按钮也能复用这个接口,把 quantity 传 0 即可。这里用 POST 而不是 GET,避免修改型操作被浏览器预加载或爬虫触发。模板里的<form>记得带上{% csrf_token %},否则 POST 会被 Django 的 CSRF 中间件拦截。

3.3 结算:库存预检与事务扣减

结算的关键是避免超卖:两个用户同时买同一束花,库存必须正确扣减。做法是先做一轮库存预检,再在事务里对商品行加锁扣减:

from django.db import transaction def checkout(request): cart = request.session.get('cart', {}) if not cart: return redirect('cart_detail') if not request.user.is_authenticated: return redirect('login') # 第一轮:预检,避免创建订单后才发现库存不足 for key, data in cart.items(): flower = Flower.objects.filter(pk=int(key)).first() if not flower or flower.stock < data['quantity']: return render(request, 'cart.html', {'error': f'{flower.name} 库存不足'}) # 第二轮:事务内锁行,再扣库存、写订单 order = Order.objects.create(user=request.user, status='pending') total = 0 with transaction.atomic(): for key, data in cart.items(): flower = Flower.objects.select_for_update().get(pk=int(key)) flower.stock -= data['quantity'] flower.save() OrderItem.objects.create( order=order, flower=flower, quantity=data['quantity'], price=flower.price, ) total += flower.price * data['quantity'] order.total_price = total order.status = 'paid' order.save() request.session['cart'] = {} return redirect('order_detail', order_id=order.id)

select_for_update() 会锁定被选中的商品行,直到事务提交或回滚,从根上避免并发超卖。第一轮预检虽然查了两次数据库,但能保证“订单已创建但库存不足”这种脏数据不出现。真实项目里支付是异步回调:先建 pending 订单,支付成功再扣库存;毕业设计用同步方式展示状态流转反而更直观。

订单状态在模型里用 choices 约束,完整状态集如下:

状态含义用户操作后台操作
pending待支付取消订单关闭超时订单
paid已支付查看订单发货
shipped已发货确认收货查看物流
done已完成评价归档
canceled已取消释放库存

注意 canceled 状态必须释放库存。如果订单取消后库存不恢复,会出现“系统显示有库存但实际卖不了”的数据问题。库存恢复逻辑与结算时的扣减逻辑要对称。

4. 用户体系与页面权限:从 login.css 到 user_center.css 的完整闭环

4.1 注册、登录视图与 Django 内置表单

用户模块在 Django 里不需要从零造轮子,内置 auth 应用已经提供了 User 模型、会话、密码哈希和权限框架。项目只要把登录页和注册页的模板接到 login.css 上。

# user/views.py from django.shortcuts import render, redirect from django.contrib.auth import login from django.contrib.auth.forms import UserCreationForm def register(request): if request.method == 'POST': form = UserCreationForm(request.POST) if form.is_valid(): user = form.save() login(request, user) return redirect('index') else: form = UserCreationForm() return render(request, 'login.html', {'form': form})

这里直接用 UserCreationForm,它内置了用户名唯一性校验和密码强度检查。注意注册成功后调用了 login(request, user),用户注册完直接进入登录态,不需要跳转登录页再输一次密码。如果希望注册后强制走一遍登录流程,去掉 login 调用,改成 redirect('login') 即可。

登录页用类视图更简洁:

from django.contrib.auth.views import LoginView class MyLoginView(LoginView): template_name = 'login.html' redirect_authenticated_user = True

redirect_authenticated_user = True 会让已登录用户访问 /login/ 时自动跳回首页,避免登录页与用户中心之间来回跳。

4.2 用户中心的登录保护与数据展示

用户中心对应 user_center.css,页面展示个人资料和最近订单。这个页面必须登录才能访问,用装饰器做保护:

from django.contrib.auth.decorators import login_required @login_required(login_url='/login/') def user_center(request): orders = request.user.orders.all().order_by('-created_at')[:10] return render(request, 'user_center.html', {'orders': orders})

request.user.orders 是模型里 related_name='orders' 带来的反向查询能力,不需要写 order_set 这种默认命名。装饰器在未登录时跳转到 login_url,登录成功后 auth 中间件会把用户带回原始页面。

商家后台对应 dashboard.css,需要区分角色。演示项目用 is_staff 判断就够:

@login_required def dashboard(request): if not request.user.is_staff: return render(request, '403.html', status=403) low_stock = Flower.objects.filter(stock__lte=5) recent_orders = Order.objects.order_by('-created_at')[:20] return render(request, 'dashboard.html', { 'low_stock': low_stock, 'recent_orders': recent_orders, })

stock__lte=5 是 Django ORM 的“小于等于”条件查询,这里做低库存预警。毕业设计不必引入复杂的角色权限模型,一个 is_staff 布尔值足够支撑商家端与用户端的隔离。

4.3 整套 CSS 文件的分工与命名规则

把项目那堆 CSS 文件按页面梳理,会发现它是“一页一册、全局共用”的命名方式:

样式文件服务页面核心职责
common.css全站重置、栅格、按钮、表单基础样式
index.css首页轮播图、区块布局
list.css商品列表网格卡片、分页样式
detail.css商品详情图册、加购区、参数表
login.css登录/注册表单卡片居中
user_center.css用户中心侧边导航、订单列表
dashboard.css商家后台数据卡片、表格
flowery.css全站主题CSS 变量、配色、花语字体
iconfont.css全站字体图标定义
demo.css演示页组件示例展示

这个分工的好处是:修改首页样式不用去 user_center.css 里翻找,新增页面只需新建一个 css 文件并仿照现有模板引入。坏处是文件多了请求数增加,优化时要考虑合并或构建工具压缩,第 5 章演示一个按路径加载的方案。

5. 生产化与提效技巧:静态文件收集、Jinja2 共存与按路径自动加载样式

5.1 用 collectstatic 收编静态文件

本地开发时 Django 的 runserver 会自动提供 STATICFILES_DIRS 下的文件,但这是并发能力有限的调试机制。部署前执行:

python manage.py collectstatic --noinput

这个命令把各 app 下 static 目录和 STATICFILES_DIRS 指定的文件统一收拢到 STATIC_ROOT,之后由 Nginx 直接托管。媒体文件不走这条路,Flower.image 上传的商品图要在 settings.py 配置 MEDIA_URL、MEDIA_ROOT。本地调试想让 Django 同时服务两类文件,在 urls.py 追加:

from django.conf import settings from django.conf.urls.static import static if settings.DEBUG: urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

生产环境必须去掉这两个 static() 映射,否则图片请求全部经过 WSGI 进程,并发一上来直接卡死。

5.2 Jinja2 与 Django 模板的共存配置

项目技术标注里有 jijia,对应的是 Jinja2。Django 4 支持多模板引擎并存,TEMPLATES 是一个列表,按顺序匹配:

TEMPLATES = [ {'BACKEND': 'django.template.backends.jinja2.Jinja2', 'DIRS': [BASE_DIR / 'templates_jinja'], 'APP_DIRS': True}, {'BACKEND': 'django.template.backends.django.DjangoTemplates', 'DIRS': [BASE_DIR / 'templates'], 'APP_DIRS': True}, ]

Jinja2 渲染性能更好,能直接调用 Python 函数,语法更灵活。代价是原生模板的{% url %}{% csrf_token %}不能直接用,需要注入环境。演示站没必要全站迁移,我一般只在促销页这类高并发页面单独启用 Jinja2,其余保持 Django 模板。

5.3 按 URL 路径自动加载对应 CSS

项目页面多、CSS 文件多,如果每个模板都手工扩展 block,新增页面时容易漏。用一个 context processor 按路径映射 CSS:

# config/context_processors.py PAGE_CSS_MAP = ( ('/list', 'list'), ('/detail', 'detail'), ('/login', 'login'), ('/user', 'user_center'), ('/dashboard', 'dashboard'), ('/cart', 'cart'), ) def page_css(request): for prefix, css in PAGE_CSS_MAP: if request.path.startswith(prefix): return {'page_css': css} return {'page_css': 'index'}

base.html 里统一引入:

{% load static %} <link rel="stylesheet" href="{% static 'css/common.css' %}"> <link rel="stylesheet" href="{% static 'css/flowery.css' %}"> <link rel="stylesheet" href="{% static 'css/'|add:page_css|add:'.css' %}">

context processor 每个请求执行一次,按 URL 前缀命中 CSS 文件名,新增页面时只改 PAGE_CSS_MAP 一处。startswith 匹配的是前缀,商品详情这种带动态 ID 的路径同样适用。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询