
刚把一个花卉商城的Django毕设完整交付出去包括程序、文档、代码讲解和后续的一条龙调整整个过程让我想把这套项目的设计和实现好好梳理一遍。如果你正在准备毕业设计或者想找一个完整的Django实战项目来练手这篇内容应该能帮你省下大量翻文档、踩坑的时间。这个花卉商城系统听起来就是一个典型的电商项目但它在毕设选题里其实非常讨巧既有功能复杂度又不会大到没法在几个月内完成而且展示性强、答辩时好讲。下面我会从需求拆解、数据库设计、核心功能实现、完整实操流程到常见坑位排查尽量把整套项目的来龙去脉说得透彻一点。很多东西是我实际写代码、调页面、跑部署时一遍遍试出来的不是教科书上的标准答案但对做毕设的人参考价值会更高。1. 项目整体设计与模块拆解1.1 核心需求画像做毕设项目最忌讳一上来就写代码先想清楚这项目到底要解决什么问题。花卉商城系统本质上是线上卖花但毕设场景下它需要覆盖完整的电商闭环用户能浏览花卉商品、查看分类、搜索、加入购物车、生成订单、模拟支付管理员能维护商品信息、处理订单状态、管理用户。这些模块串起来才能证明你掌握了从数据库到后端再到前端的一套完整能力。我在拆解需求时把用户角色分成三类游客可以浏览商品、查看详情无法下单通常用来展示系统的引流能力。注册用户登录后能管理购物车、下单、查看历史订单、维护个人信息。管理员登录django自带的后台或自建管理界面进行商品上下架、库存管理、订单发货等操作。这三类角色覆盖了权限管理的核心逻辑答辩时老师问起来也能说出个所以然。1.2 为什么选Django而非Flask或SpringBoot经常有人问我毕设选什么框架我的建议是除非你的选题是前后端分离 高并发否则Django永远是性价比最高的选择。道理很简单Django自带Admin后台、ORM、认证系统、模板引擎、表单处理、静态文件管理这些全是电商类项目的基础组件能省掉大把手写时间。对比一下FlaskDjango就像是自带精装修的房子Flask则是清水房。Flask灵活但需要自己搭结构对毕设来说时间成本太高SpringBoot虽然企业认可度更高但Java的学习曲线和配置复杂度对多数非科班或者基础一般的同学并不友好。还有一个现实因素Django的ORM和Admin是答辩利器。你不需要写一堆SQL就能展示数据关系后台界面直接给老师演示商品管理视觉效果好代码量还少。1.3 项目目录结构与App划分我习惯把项目拆成四个应用app分别是用户、商品、购物车、订单。如果你做的是一个更大一点的商城可以再加一个统计或者促销的app但毕设不需要过度设计。flower_shop/ # 项目根目录 ├── manage.py ├── flower_shop/ # 项目配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── users/ # 用户管理注册、登录、个人信息 │ ├── goods/ # 商品管理分类、商品、库存 │ ├── cart/ # 购物车增删改查、数量调整 │ └── orders/ # 订单创建、查询、状态流转 ├── templates/ # 共用模板 ├── static/ # 静态文件css、js、images └── media/ # 用户上传图片这种结构的好处是职责分明调试时定位问题快论文里写系统设计也清晰。很多同学喜欢把所有逻辑堆在一个app里最后几千行代码挤在一起改一处崩三处毕业答辩现场演示翻车就太难受了。2. 数据库模型设计从商品到订单的完整闭环2.1 用户模型的扩展方案Django自带的User模型能用但字段太少手机号、收货地址、头像全没有。我的做法是建立一个Profile关联表用OneToOneField指向内置User把额外信息放子表里。这样不破坏Django的认证机制又能扩展业务字段。from django.contrib.auth.models import User from django.db import models class Profile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, verbose_name用户) phone models.CharField(max_length11, blankTrue, verbose_name手机号) avatar models.ImageField(upload_toavatar/, blankTrue, verbose_name头像) address models.CharField(max_length200, blankTrue, verbose_name收货地址) created_time models.DateTimeField(auto_now_addTrue, verbose_name注册时间) class Meta: verbose_name 用户信息 verbose_name_plural verbose_name def __str__(self): return self.user.username这里有个细节ImageField需要安装Pillow库否则迁移时会报错。我先在虚拟环境里pip install pillow再跑makemigrations避免迁移中断。为什么用扩展表而不是直接继承AbstractUser如果你项目刚起步用AbstractUser自定义用户表确实更优雅。但毕设通常已经用到Django内置的用户登录、Admin管理贸然换用户模型需要重写大量引用关系风险高。Profile扩展是兼容性最稳妥的方案答辩时也讲得出设计理由。2.2 商品分类与商品信息的字段选择花卉商品比普通数码产品多几个特殊字段是否热门、上架时间、库存数量、销量、原价和现价以及一个用于首页大图展示的banner标记。class Category(models.Model): name models.CharField(max_length50, verbose_name分类名称) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父级分类) class Meta: verbose_name 分类 verbose_name_plural verbose_name def __str__(self): return self.name class Flower(models.Model): name models.CharField(max_length100, verbose_name商品名称) category models.ForeignKey(Category, on_deletemodels.CASCADE, verbose_name所属分类) price models.DecimalField(max_digits8, decimal_places2, verbose_name售价) original_price models.DecimalField(max_digits8, decimal_places2, default0, verbose_name原价) stock models.IntegerField(default0, verbose_name库存) sales models.IntegerField(default0, verbose_name销量) image models.ImageField(upload_toflower/, verbose_name商品图片) description models.TextField(blankTrue, verbose_name商品描述) is_hot models.BooleanField(defaultFalse, verbose_name是否热门) is_banner models.BooleanField(defaultFalse, verbose_name是否轮播展示) created_time models.DateTimeField(auto_now_addTrue, verbose_name上架时间)价格字段用DecimalField而不用FloatField这算是电商项目的老规矩。浮点数做金额运算会出现0.10.2不等于0.3的问题真到了订单结算环节会出大乱子。DecimalField在Python里也是Decimal类型配合decimal模块做金额计算才是安全的。你还可以在模型里加一个漂亮的property直接返回格式化后的价格字符串模板里调用就会更省事。property def format_price(self): return f{self.price}2.3 购物车与订单的关联设计购物车我没有建数据库表而是用了session存储。理由很直接购物车属于临时数据用户可能只是逛逛没必要每次操作都写入数据库。但如果项目要求用户在不同设备间同步购物车那还是要建表。订单设计上我拆成了订单主表和订单明细表。主表存一次购买行为的总金额、收货信息、订单状态明细表存每一件花卉的单价和数量。这样设计是符合电商标准模型的论文的数据库ER图也好画。class Order(models.Model): STATUS_CHOICES ( (pending, 待付款), (paid, 已付款), (shipped, 已发货), (completed, 已完成), (cancelled, 已取消), ) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name用户) order_no models.CharField(max_length32, uniqueTrue, verbose_name订单编号) total_amount models.DecimalField(max_digits10, decimal_places2, verbose_name订单总金额) receiver_name models.CharField(max_length50, verbose_name收货人) receiver_phone models.CharField(max_length11, verbose_name联系电话) receiver_address models.CharField(max_length200, verbose_name收货地址) status models.CharField(max_length20, choicesSTATUS_CHOICES, defaultpending, verbose_name订单状态) remark models.TextField(blankTrue, verbose_name订单备注) created_time models.DateTimeField(auto_now_addTrue, verbose_name下单时间) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems, verbose_name所属订单) flower models.ForeignKey(Flower, on_deletemodels.CASCADE, verbose_name商品) price models.DecimalField(max_digits8, decimal_places2, verbose_name成交单价) quantity models.IntegerField(default1, verbose_name购买数量)订单号我直接用uuid生成再转成字符串去掉横线加上时间前缀。这样即使同一秒有多笔订单也不会出现主键冲突。2.4 Django Admin后台的定制思路如果你做的是普通后台展示Django自带的Admin已经很有排面了。但直接裸用会有两个问题一是列表页显示的是默认的object标题看不出商品名二是分类筛选和搜索没配好管理员用起来很别扭。简单的定制就是注册模型时指定list_display、list_filter、search_fields。from django.contrib import admin from .models import Flower, Category class FlowerAdmin(admin.ModelAdmin): list_display (name, category, price, stock, sales, is_hot, created_time) list_filter (category, is_hot, is_banner) search_fields (name, description) list_editable (price, stock, is_hot) list_per_page 20 admin.site.register(Flower, FlowerAdmin)list_editable这个配置挺好用的它允许管理员在列表页直接改价格和库存不用点进详情效率高很多。给老师演示的时候这个交互效果也比默认后台强不少。3. 核心功能实现与代码讲解3.1 登录注册与会话管理Django自带的认证系统提供了authenticate和login方法不用自己写加密逻辑。注册时我一般用内置的UserCreationForm作为基础再自己写一个注册表单集成Profile字段。from django.contrib.auth.forms import UserCreationForm from django.contrib.auth.models import User from django import forms from .models import Profile class RegisterForm(UserCreationForm): phone forms.CharField(max_length11, requiredTrue, label手机号) email forms.EmailField(requiredFalse, label邮箱) class Meta: model User fields (username, phone, email) def save(self, commitTrue): user super().save(commitFalse) if commit: user.save() Profile.objects.create(useruser, phoneself.cleaned_data[phone], emailself.cleaned_data.get(email)) return user这里有个小小的细节email字段在User模型里本来就有直接在RegisterForm里声明就能覆盖。phone字段我单独存在Profile表里所以save方法里要同时写两个表。这种操作在答辩时也是常见的提问点最好能讲清楚为什么不能把所有字段都塞进User表。登录视图使用内置的LoginView就可以但我一般重写模板为login.html并且在登录成功后用next参数把用户带回他原来想访问的页面。这个体验比固定跳转到首页好很多。3.2 商品列表、分类筛选与详情页商品列表页用ListView配合django-filter或者手动写queryset过滤。我在项目里是手动过滤因为逻辑很简单class FlowerListView(ListView): model Flower template_name goods/flower_list.html context_object_name flowers paginate_by 12 def get_queryset(self): queryset Flower.objects.filter(stock__gt0) category_id self.kwargs.get(category_id) keyword self.request.GET.get(keyword, ) sort self.request.GET.get(sort, default) if category_id: queryset queryset.filter(category_idcategory_id) if keyword: queryset queryset.filter(name__icontainskeyword) if sort price_asc: queryset queryset.order_by(price) elif sort price_desc: queryset queryset.order_by(-price) elif sort sales: queryset queryset.order_by(-sales) return queryset排序这里我喜欢加一个switch式的映射这样用户点一次排序URL上出现sortprice_asc模板里再根据当前sort高亮对应的按钮。这个交互不难但能有效提升项目完成度的观感。详情页则是简单DetailView但我会额外做两个事情一个是在模板里展示同类花卉推荐另一个是点击加入购物车时通过AJAX提交不刷新页面试验。function addToCart(flowerId) { fetch(/cart/add/${flowerId}/, { method: POST, headers: { X-CSRFToken: document.querySelector([namecsrfmiddlewaretoken]).value } }) .then(response response.json()) .then(data { if (data.code 200) { alert(加入购物车成功); document.getElementById(cart_count).innerText data.cart_total; } }); }3.3 购物车逻辑session操作还是数据库操作我前面提到毕设项目我用session存购物车这里把具体写法放出来。Django的session本质上就是字典在视图里直接操作即可。def cart_add(request, flower_id): flower get_object_or_404(Flower, pkflower_id) cart request.session.get(cart, {}) if str(flower_id) in cart: if cart[str(flower_id)][count] flower.stock: cart[str(flower_id)][count] 1 else: cart[str(flower_id)] { name: flower.name, price: str(flower.price), image: flower.image.url if flower.image else , count: 1, } request.session[cart] cart request.session.modified True return JsonResponse({code: 200, cart_total: sum(item[count] for item in cart.values())})注意修改session之后一定要设置request.session.modified True否则Django可能认为session没有变化不写入存储。这个问题特别隐蔽我见过好几个人的购物车点击加购后刷新就消失就是这个原因。购物车页面的核心操作是加减数量、删除商品、计算总金额。总金额我选择在模板层算因为数据量小但更规范的做法是在视图里先聚合好传给模板。如果你要写论文建议后者代码逻辑更清晰也方便写单元测试。3.4 订单流程结算、生成、模拟支付下单是购物车的终点也是最容易出bug的地方。基本流程是这样确认登录状态未登录的用户跳转到登录页。从购物车会话中读取商品列表逐件校验库存是否足够。创建订单主表记录填写收货人信息。为每一件商品创建订单明细。扣减商品库存清空购物车。跳转到一个模拟支付页面点击确认支付后把订单状态从pending改成paid。第2步一定要做否则会出现用户下了单但库存不够的尴尬情况。我在项目里也把超卖问题处理了一下具体写法可以这样from django.db import transaction transaction.atomic def create_order(request): cart request.session.get(cart, {}) if not cart: return JsonResponse({code: 400, msg: 购物车为空}) user request.user total_amount 0 order_items [] for str_id, item in cart.items(): flower Flower.objects.select_for_update().get(pkint(str_id)) if flower.stock int(item[count]): return JsonResponse({code: 400, msg: f{flower.name}库存不足}) flower.stock - int(item[count]) flower.sales int(item[count]) flower.save() total_amount flower.price * int(item[count]) order_items.append(OrderItem(flowerflower, priceflower.price, quantityint(item[count]))) order Order.objects.create( useruser, order_nogenerate_order_no(), total_amounttotal_amount, receiver_namerequest.POST.get(receiver_name), receiver_phonerequest.POST.get(receiver_phone), receiver_addressrequest.POST.get(receiver_address), statuspending, ) OrderItem.objects.bulk_create(order_items) request.session[cart] {} request.session.modified True return JsonResponse({code: 200, order_id: order.id})select_for_update是行级锁确保在高并发下不会多个用户同时读到相同库存然后各自扣减这就是防止超卖的关键一行。虽然毕设未必会遇到并发测试但答辩时提到事务与锁老师会认为你有生产意识。模拟支付就很简单了直接一个视图把订单状态改成paid顺便把付款时间记上。如果你愿意加一点展示效果可以跳到模板页里做一个倒计时和下单成功动画整体观感会很加分。3.5 用户中心与订单管理用户中心负责展示个人信息、我的订单、收藏之类的功能。我的做法是写一个AccountView集中处理三个tab页的信息减少视图数量。订单列表页要区分状态我用Django的if标签在模板里做筛选。订单详情里把订单主表信息和明细表一起展示注意在查询时用select_related把user和flower表都join进来避免循环查数据库。def order_detail(request, order_id): order get_object_or_404(Order.objects.select_related(user).prefetch_related(items__flower), pkorder_id, userrequest.user) return render(request, orders/order_detail.html, {order: order})prefetch_related这个细节很多人会忽略但面试或答辩时拿出来讲就是加分项。它能一次把订单所有明细连同花卉商品一起查出来而不是每循环一个明细就发一次SQL。4. 从零实操一条龙搭建流程4.1 环境准备与虚拟环境做这个项目之前先确认你的Python版本在3.8以上Django版本我建议4.2 LTS或者3.2 LTS。Django 4.2是目前比较稳的长期支持版第三方库兼容性好网上教程也多。我习惯用venv建虚拟环境简单不折腾。python -m venv venv source venv/bin/activate # Windows下是 venv\Scripts\activate pip install django pillow如果你的项目还要做验证码、图表展示之类可以加装captcha和matplotlib但这些都不是必需品按需装就行。4.2 创建项目和Appdjango-admin startproject flower_shop cd flower_shop python manage.py startapp users python manage.py startapp goods python manage.py startapp cart python manage.py startapp orders创建App后记得在settings.py的INSTALLED_APPS里注册。同时把语言改成中文、时区改成上海。LANGUAGE_CODE zh-hans TIME_ZONE Asia/Shanghai USE_TZ TrueUSE_TZ建议保持True但要注意数据库里的DateTimeField在写入时会转成UTC展示时模板里会转回本地时间如果你发现自己存的时间比实际慢了8小时不用慌这是时区转换机制不是bug。4.3 配置数据库SQLite还是MySQL毕设项目直接用SQLite就够了零配置、文件即数据库、答辩现场不怕连不上。但如果你的项目要演示给老师看并且数据量比较大MySQL会让论文更有分量。Django连接MySQL需要在settings里改配置DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: flower_shop, USER: root, PASSWORD: yourpassword, HOST: 127.0.0.1, PORT: 3306, } }同时需要安装pymysql并在项目__init__.py里设置import pymysql pymysql.install_as_MySQLdb()这个操作说多了都是泪。以前MySQLdb库在Python3下兼容性不好pymysql就是替代品装好后记得unicode编码要统一否则会出现中文乱码。4.4 数据迁移和创建超级用户模型写好后执行迁移python manage.py makemigrations python manage.py migrate如果你新增了app但没写任何模型makemigrations时会提示没有变化这是正常的不用慌。创建管理员python manage.py createsuperuser用户名和密码自定义邮箱可以留空。这个账号用来登录Django Admin后台。4.5 模板与静态文件配置模板路径我在settings里统一指到根目录的templates文件夹静态文件指到static文件夹。TEMPLATES [ { BACKEND: django.template.backends.django.DjangoTemplates, DIRS: [BASE_DIR / templates], APP_DIRS: True, OPTIONS: { context_processors: [ django.template.context_processors.debug, django.template.context_processors.request, django.contrib.auth.context_processors.auth, django.contrib.messages.context_processors.messages, ], }, }, ] STATIC_URL /static/ STATICFILES_DIRS [BASE_DIR / static] MEDIA_URL /media/ MEDIA_ROOT BASE_DIR / media这里有个常见的坑DEBUGTrue时Django能直接跑静态文件但部署到服务器时经常出现样式全丢的情况。所以我在项目里加了静态文件收集配置后续部署时跑collectstatic就能解决。4.6 运行开发服务器与调试python manage.py runserver浏览器打开http://127.0.0.1:8000就能看到系统首页。如果你在管理后台上传商品图片后详情页图片加载不出来多半是没在urls.py里加media的路由。from django.conf import settings from django.conf.urls.static import static urlpatterns static(settings.MEDIA_URL, document_rootsettings.MEDIA_ROOT)这个路由只会在开发和DEBUG环境下生效生产环境要用Nginx托管media文件这块我在常见问题章节再细说。5. 实战中常见的坑与排查技巧实录5.1 找不到模板或静态文件404症状点击页面后报TemplateDoesNotExist或者浏览器里css文件出现404。排查思路分三步第一步看settings里TEMPLATES的DIRS是否包含templates目录路径。第二步看app目录下是否创建了templates/应用名/模板名的三层结构比如goods/templates/goods/flower_list.html。第三步看静态文件在浏览器里直接访问URL是否可加载如果静态文件404先去settings确认STATICFILES_DIRS配置项。实际排查过程中我发现九成的问题都是路径写错。Django模板加载规则是先查DIRS再查每个app的templates目录如果你在两个地方都有同名模板会优先加载DIRS里的很容易出现改了A文件但页面显示B文件的情况。5.2 外键查询导致的N1性能问题在商品列表页关联分类名称时很多人会写成for flower in flowers: category_name flower.category.name如果flowers有100条这条查询会执行100次非常要命。正确的做法是使用select_related。flowers Flower.objects.select_related(category).all()这样Django会用join把category一次取出来查询次数从101降到1。虽然毕设数据量小看不到速度差但答辩时这一条问答就能体现出你对性能优化的理解。5.3 中文乱码和数据编码问题Django 4.x默认全中文没问题但有些老项目或从别处拷贝的数据库会出现??或者UnicodeDecodeError。最常见的原因是MySQL数据库或表的编码不是utf8mb4。在建库时就指定CREATE DATABASE flower_shop DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;写连接时也可以在OPTIONS里加charsetOPTIONS: {charset: utf8mb4}开发中还要注意Python文件头部的编码注释虽然Python3默认utf-8但Windows下用记事本保存文件容易产生BOM头导致首行报错。建议直接用VS Code或PyCharm写代码别用记事本。5.4 页面POST表单报403 CSRF验证失败这个坑每个Django新手都会遇到。解决方案有几种在模板的表单里加{% csrf_token %}。如果是AJAX提交在headers里带上X-CSRFToken。暂时在View上加csrf_exempt但这种方法不推荐毕设答辩被问到安全性容易吃亏。我实际写项目时前端模板里统一加了模板标签AJAX部分统一通过一个JS函数读取cookie中的csrftoken。这样只要后端逻辑不变前端所有POST请求都是安全的也不会报403。5.5 订单状态异常与库存不一致假设用户下了一单但没支付数据库里库存先扣了如果用户不支付库存一直占着。更合理的做法是下单时不扣库存只是锁定库存支付成功后再真实扣减。我在毕设里简化成了“下单即扣库存”因为缺少超时取消订单的异步任务。如果你想让项目在答辩时更有亮点可以加一个订单超时自动取消的定时任务存一个过期时间字段每次用户访问订单列表时扫描一次将过期的pending订单改为cancelled并恢复库存。这个功能代码量不大但讲“系统健壮性”时非常加分。我实际写的时候用了Django-Crontab或者Celery都可以但Celery对毕设来说太重了。最轻量的是在自定义中间件里做一个简单的过期检查或者干脆在每次下单/查看购物车时触发一次检查函数。5.6 部署上线的内存和路径坑如果你要把项目放到云服务器上展示我强烈建议先跑一遍collectstaticpython manage.py collectstatic把静态文件统一收集到STATIC_ROOT指定的目录里再由Nginx转发。直接用runserver撑生产环境不仅性能差而且静态文件管理混乱。再有就是路径分隔符的问题Windows下开发用的是反斜杠Linux服务器上是正斜杠。所有自定义路径都尽量用PurePath拼接不要写死“/home/user”这种地址。5.7 答辩现场演示的稳定性建议最后说一个比较务实的点答辩前把用到的数据准备好比如花卉商品图片、分类、热门标记、几笔模拟订单提前都录入到数据库里不要把现场时间浪费在添加数据上。我当时还把下单流程里的支付环节改成了一键模拟避免演示时因为网络问题卡在支付页面。这种细节考虑越多答辩过程就越顺。有时候老师没耐心看完整流程你只要把最关键的注册、商品浏览、下单、后台管理这几个页面切换得干净利落就已经足够证明你的工作量了。写在最后我实际带过的毕设项目里花卉商城算是综合体验很好的类型。它不像社交或内容平台那样需要复杂推荐算法也不像管理系统那样缺乏商业闭环感电商的完整链路做下来从需求到设计再到编码你都走了一遍毕业设计无非就是想让你经历这个过程。如果你打算从零开始做这套系统我的建议是先把数据库模型图手绘出来再去动代码。模型定好了后面的视图和模板就像填格子一样顺畅。代码遇到问题也别太焦虑Django的错误提示已经很友善了按着报错堆栈逐行找基本上都能解决。真解决不了的把问题拆成最小复现缩小到某个函数或某个模板标签再搜索时会精准得多。