☰
Django插画鲜花商城毕设项目:从设计到答辩全复盘
2026/10/13 17:32:53 网站建设 项目流程

基于Django的插画鲜花商城系统毕设项目全复盘:从设计思路到远程调试再到答辩演示

插画鲜花商城这个毕设选题,是这几年我远程接过的项目里出现频率比较高的一个。原因很直接:它既有电商系统该有的完整业务链路——用户、商品、购物车、订单、支付、后台管理,又有“插画”这个视觉主题可以做差异化设计。做出来界面好看,业务逻辑完整,答辩时能讲的东西也多,不会像纯粹的管理系统那样干巴巴。

我前前后后带过好几个人完整走完这套项目,也接手过不少写到一半卡住的学生项目,今天就把整套基于Django的插画鲜花商城系统,从选题定位、技术选型、核心功能实现到远程调试、论文整理和答辩演示,一次性复盘清楚。如果你正准备做类似的毕设,或者刚拿到一个半成品项目不知道从哪里下手,这篇文章可以帮你少走很多弯路。

1. 项目定位与功能拆解:插画鲜花商城到底在做什么

1.1 为什么“插画鲜花”是个讨巧的毕设题目

选毕设题目最怕两件事:一是题目太大,功能没做完就答辩了;二是题目太小,一个CRUD系统撑不起论文篇幅,演示五分钟就结束。

插画鲜花商城刚好卡在中间。从电商角度看,它覆盖了完整交易链路,该有的模块一个不少;从视觉角度看,“插画”这个词天然地给了UI设计一个发挥空间——商品图可以是手绘风格的插画花束、包装盒、贺卡,整个站点的主色调、字体、版式都可以围绕这个主题去做。在毕业答辩这种需要“一眼看出工作量”的场合,一个视觉风格统一、有设计感的前台页面,比十个朴素的功能列表更能留下印象。

另外,这个题目还有很好的扩展空间。基础版可以做商品展示、购物车、订单、后台管理;进阶版可以加收藏、评论、优惠券、花语推荐、配送时间选择;如果你还有余力,还可以接一个模拟支付回调、做一个简单的数据统计看板。这些扩展功能既是答辩时的加分项,也是论文里可以多写一个章节的素材。

1.2 角色划分与整体业务流程

整个系统里其实只有三类角色:游客、登录用户、后台管理员。游客能浏览商品、搜索、查看详情、看插画专题,但加购物车和下单必须登录;登录用户在前台基础上拥有购物车、下单、查看订单、支付、收藏、评论等权限;管理员在后台管理商品分类、插画商品、库存、订单状态和用户状态。

业务流程核心是一条交易链路:浏览商品 → 加入购物车 → 确认订单 → 模拟支付 → 支付回调 → 订单状态流转 → 后台发货 → 用户确认收货。这个链路看起来简单,但每一步都有细节。比如购物车里的商品价格是取当前商品表价格还是加购时快照价格?下单之后库存是立即扣减还是支付后再扣减?支付回调怎么保证不重复处理?这些细节如果不提前想清楚,写代码的时候很容易把自己绕进去。

1.3 前台与后台功能模块清单

前台功能模块我建议拆成这样:

  • 用户模块:注册、登录、退出、个人信息维护、头像上传、密码修改。
  • 商品模块:插画鲜花商品列表、分类筛选、关键词搜索、商品详情展示、多图轮播、库存与价格展示。
  • 插画专题模块:按风格主题展示插画商品,比如复古花束、治愈系花草、国风插画、婚礼花艺等,这个模块是视觉亮点,也是和普通电商商城拉开差距的地方。
  • 购物车模块:加购、修改数量、删除、选中计算总价。
  • 订单模块:确认订单页、生成订单、订单列表、订单详情、取消订单、模拟支付。
  • 互动模块:商品收藏、商品评论、评论展示。

后台管理功能模块:

  • 商品管理:商品分类的增删改查,商品上架下架,插画图集上传,库存价格编辑。
  • 订单管理:订单列表、按状态筛选、发货操作、查看订单详情。
  • 用户管理:用户列表、禁用启用、重置密码等基础操作。
  • 数据统计:简单统计订单量、销售额、商品销量Top榜,用图表展示,不需要太复杂,能说明业务状况即可。

在给项目做功能设计时,我的建议是先画一张业务流程草图,把你想要的功能都列出来,然后标优先级:哪些是第一版必须做的,哪些是可以后补的。毕设项目最怕的不是功能少,而是功能多到做不完导致每个模块都是半成品。

2. 技术方案选型与数据库设计:为什么是Django以及模型怎么建

2.1 技术选型对比:Django、Flask、SSM怎么选

很多人在选题之后第一个纠结的就是框架。我直接说结论:如果这个项目是Java技术栈要求的毕设,那SSM或者Spring Boot是硬性规定,没得选;但如果是Python方向,或者学校允许自由选择,那优先选Django。

原因有三点。第一,Django自带Admin后台,这意味着后台管理可以快速搭建出来,省下很多时间。你只需要在admin.py里注册模型,再把后台和自定义管理页面做点整合,后台就能用。第二,Django的ORM非常成熟,CRUD操作效率很高,写个商品列表、订单查询基本不用手写SQL,对新手友好。第三,Django自带用户认证体系,登录、会话、权限这些电商系统绕不开的模块,不用从零造轮子。

Flask虽然轻量灵活,但很多功能需要自己组装,比如用户认证要装Flask-Login,ORM要额外选择,对于毕设这种时间紧任务重的场景,自由反而是负担。你可能会花不少时间在“怎么把这些库串起来”而不是“完成业务功能”上。

做一个简单对比:

对比维度DjangoFlaskSSM / Spring Boot
上手难度中等,但套路清晰简单,但自行组装多较高,依赖配置多
自带后台自带Admin无需自己搭建
用户认证内置完整方案靠扩展库靠框架组件
ORM成熟较弱MyBatis需写SQL
毕设开发效率高中中低

2.2 数据库模型设计与关键字段解释

数据库设计是整个项目的骨架,模型建错了后面改起来非常痛苦。我以一个标准的插画鲜花商城为例,把核心模型都过一遍。

用户模型这块,尽量用Django自带的用户系统扩展,不推荐完全重写。如果要加手机号、头像、昵称这些字段,做法是新建一个Profile模型,用OneToOneField关联内置User,然后在UserProfile里存扩展信息。这样做的好处是登录认证逻辑完全复用,不用自己处理密码哈希和会话。

from django.db import models from django.contrib.auth.models import User class UserProfile(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile') nickname = models.CharField(max_length=30, blank=True) avatar = models.ImageField(upload_to='avatar/', blank=True, null=True) phone = models.CharField(max_length=20, blank=True) created_at = models.DateTimeField(auto_now_add=True)

商品分类模型要支持层级结构,将来可以扩展一级分类、二级分类。虽然插画鲜花商城可能只需要“鲜花”“干花”“绿植”“礼盒”这种平级分类,但设计父子关系并不亏,前台展示时可以通过parent__isnull=True筛选顶级分类,也方便以后加子分类。

class Category(models.Model): name = models.CharField(max_length=50) parent = models.ForeignKey('self', on_delete=models.CASCADE, null=True, blank=True, related_name='children') cover = models.ImageField(upload_to='category/', blank=True, null=True) sort = models.IntegerField(default=0)

商品模型是最核心的。价格要选用DecimalField,不要用FloatField,因为浮点数在涉及金额运算时会有精度问题。库存字段用PositiveIntegerField,强制非负。商品状态用布尔值表示上下架。

class Product(models.Model): category = models.ForeignKey(Category, on_delete=models.PROTECT, related_name='products') name = models.CharField(max_length=120) subtitle = models.CharField(max_length=200, blank=True, verbose_name='副标题/插画风格标签') price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='售价') stock = models.PositiveIntegerField(default=0, verbose_name='库存') cover = models.ImageField(upload_to='products/cover/', verbose_name='封面图') detail = models.TextField(blank=True, verbose_name='商品详情') tags = models.CharField(max_length=255, blank=True, verbose_name='插画风格标签,多个用逗号分隔') status = models.BooleanField(default=True, verbose_name='是否上架') sales = models.PositiveIntegerField(default=0, verbose_name='销量') created_at = models.DateTimeField(auto_now_add=True)

商品多图场景建议单独建一个商品图模型,利用ManyToOne关系关联到商品,这样上传多张插画图时比较干净,不会把JSON字段搞得很难维护。

class ProductImage(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, related_name='images') image = models.ImageField(upload_to='products/images/') sort = models.IntegerField(default=0)

购物车有两种实现方案,一种用Session存储,一种用数据库存储。我推荐数据库存储,因为对毕设来说购物车数据可以持久化,用户换设备登录数据还在,演示起来比较直观。购物车表字段要保证用户和商品唯一约束,同一商品重复加入时更新数量而不是新增记录。

class CartItem(models.Model): user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='cart_items') product = models.ForeignKey(Product, on_delete=models.CASCADE) quantity = models.PositiveIntegerField(default=1) selected = models.BooleanField(default=True, verbose_name='是否选中') created_at = models.DateTimeField(auto_now_add=True) class Meta: unique_together = ('user', 'product')

订单和订单项要分开。一个订单包含多个商品条目,所以订单表只存订单编号、用户、总金额、状态、收货信息这些公共字段,订单项表记录每个商品的名称快照、价格快照、数量。注意,订单项里存的商品名称和价格必须是下单那一刻的快照,不能通过外键实时去查商品表。否则之后商品改价或者删除,历史订单的数据就会乱套。这是很多人会忽略的细节。

class Order(models.Model): ORDER_STATUS = ( ('unpaid', '待付款'), ('paid', '待发货'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ) order_no = models.CharField(max_length=32, unique=True) user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='orders') total_amount = models.DecimalField(max_digits=10, decimal_places=2) receiver_name = models.CharField(max_length=30) receiver_phone = models.CharField(max_length=20) receiver_address = models.CharField(max_length=255) status = models.CharField(max_length=20, choices=ORDER_STATUS, default='unpaid') created_at = models.DateTimeField(auto_now_add=True) class OrderItem(models.Model): order = models.ForeignKey(Order, on_delete=models.CASCADE, related_name='items') product = models.ForeignKey(Product, on_delete=models.CASCADE) product_name = models.CharField(max_length=120, verbose_name='商品名称快照') product_price = models.DecimalField(max_digits=8, decimal_places=2, verbose_name='商品价格快照') quantity = models.PositiveIntegerField(default=1) image = models.ImageField(upload_to='order/items/', blank=True, null=True)

订单编号不建议用数据库自增id直接展示,最好生成一个带日期时间的唯一编号,形如“202506070001”,这样演示时看起来更规范,也方便后续对接业务识别。

2.3 项目目录分层与开发环境组织

项目建议按Django的标准多App方式组织,而不是把所有逻辑塞在一个App里。一个App管一类业务,后期维护和论文里的模块划分都会清晰很多。

我常用的组织方式是这样:

project/ ├── manage.py ├── config/ # 项目的配置目录 │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── apps/ │ ├── users/ # 用户模块 │ ├── products/ # 商品和分类模块 │ ├── carts/ # 购物车模块 │ ├── orders/ # 订单模块 │ └── payments/ # 模拟支付模块 ├── static/ # 本地静态资源 ├── media/ │ ├── products/ │ └── avatar/ ├── templates/ # 模板文件 └── requirements.txt

把业务拆成多个App还有一个好处,就是论文里可以按模块写,每章都有内容可写,自然就出了篇幅。远程调试时定位问题也不用整个项目翻来翻去。

开发环境建议用虚拟环境管理依赖,把依赖导出到requirements.txt,这样换一台电脑环境也能快速恢复。店铺项目如果用了第三方库,导出依赖之后要核对一下版本,尤其是Django版本,4.x和3.x在某些写法上有差异,最容易踩坑。

3. 核心功能实操:用户、商品、购物车、订单与模拟支付怎么实现

3.1 用户注册登录与权限控制

用户模块优先复用Django自带的认证功能。登录视图可以用内置的LoginView,也可以自己写视图函数。自己写的好处是可以自定义登录模板和返回逻辑,对毕设来说控制更直观。

注册功能需要处理表单字段校验、用户名唯一校验,以及注册成功后自动登录或跳转登录。建议封装成一个表单类,在clean方法里做重复用户名检查。

from django import forms from django.contrib.auth.models import User class RegisterForm(forms.Form): username = forms.CharField(max_length=30, label='用户名') password = forms.CharField(widget=forms.PasswordInput, label='密码') confirm_password = forms.CharField(widget=forms.PasswordInput, label='确认密码') def clean_username(self): username = self.cleaned_data.get('username') if User.objects.filter(username=username).exists(): raise forms.ValidationError('该用户名已被注册') return username def clean(self): cleaned_data = super().clean() pwd = cleaned_data.get('password') confirm = cleaned_data.get('confirm_password') if pwd and confirm and pwd != confirm: raise forms.ValidationError('两次输入的密码不一致') return cleaned_data

视图里注册成功后,根据用户是否登录来决定是否开放购物车和订单入口。权限控制最简单的做法是用Django的@login_required装饰器,不需要登录的页面则不装饰。这样代码读起来很清楚。商品详情页、列表页开放给游客,购物车、订单相关视图统一加登录限制。

实测下来,用装饰器控制权限有一个好处:答辩时如果老师问到“你这个权限是怎么做的”,你可以直接演示未登录状态下访问购物车跳转到登录页,这句话比任何空口解释都有说服力。

3.2 插画商品展示与风格化分类检索

商品展示这块要抓住“插画”主题做文章。数据库里已经有Category模型和tags字段,前端就可以按分类、按标签两种方式筛选。

分类页的URL建议设计成纯语义化的路径,比如/products/category/flowers/,用分类名称的slug而不是id。Django里的做法是给Category加一个slug字段,保存英文短横线形式的标识,例如“dried-flowers”。这样做给答辩演示时的观感很好,老师看到URL就能知道你做了路由设计。

列表页的分页是必须的,直接用Django内置的Paginator。这里我踩过一次坑:Paginator返回的Page对象在模板中判断是否有下一页时,用page.has_next()和page.has_previous(),不要自己去算总页数。如果你在模板里用page.paginator.num_pages来决定循环页码,遇到数据量不合理时容易出现空页码。

详情页的图片轮播可以用一个简单的JavaScript插件实现,或者干脆自己写几行JS控制小图切换大图。毕设不需要引入复杂的前端框架,但要注意多图展示的体验。插画类商品,图片就是门面,建议统一处理图片尺寸,封面图比例保持一致,否则列表页会显得很乱。

如果你想让“插画”主题更突出,可以做一个专题页面,按风格展示。“治愈系花卉插画”“复古植物图谱”“国风花鸟画”这种专题,本质上还是按tags字段过滤商品,但页面设计成专题海报风格,整个项目的完成度立刻上一个档次。

3.3 购物车逻辑的加分细节

购物车模块很多人做成一个简单的“添加记录”就完了,但有几个细节值得做好。

加购时要做库存校验。如果当前库存为0,不能加购;加购数量默认1,但不要在购物车就限制死,因为用户在购物车还可以改数量,最终校验在下单时做。

购物车页面要能修改数量、删除商品、选中/取消选中,然后计算总价。总价的计算建议在前端用JavaScript实时算,同时在后端重新算一遍,以应对拿着过期数据下单的情况。

加购接口返回的JSON建议包含两个值:当前购物车商品总数、加入结果提示。图标上的小红点数字更新起来很方便,这种小交互的完成度,答辩时老师是看得到的。

def add_cart(request, product_id): product = get_object_or_404(Product, id=product_id, status=True) if product.stock <= 0: return JsonResponse({'code': 0, 'msg': '商品库存不足'}) cart_item, created = CartItem.objects.get_or_create( user=request.user, product=product, defaults={'quantity': 1} ) if not created: cart_item.quantity += 1 cart_item.save() total_count = request.user.cart_items.aggregate(total=Sum('quantity'))['total'] or 0 return JsonResponse({'code': 1, 'msg': '添加成功', 'cart_count': total_count})

购物车这个模块做好之后,记得把加购、改数量、删商品三种操作都调试到位。很多学生项目做一半,购物车只能加不能删,这属于致命伤,演示的时候一旦出现,整场答辩的节奏就乱了。

3.4 订单流程:状态流转、库存扣减与并发处理

下单是整个系统里最需要严谨对待的部分。我的建议是:确认订单页从购物车读取选中的商品数据,展示商品列表、数量、价格、总金额,同时要求用户填写收货信息。如果项目已有用户收货地址表就选择地址,如果没有,就做成一个表单填写。

提交订单的后端逻辑要包在事务里,库存校验和扣减也必须在事务里完成。这里的关键是并发下不能超卖。用select_for_update()对商品行加锁是一种简单有效的方案,虽然对毕设来说并发量不大,但这段代码写到论文里能展示你考虑到高并发场景。

from django.db import transaction @transaction.atomic def create_order(request): selected_items = request.user.cart_items.select_related('product').filter(selected=True) if not selected_items.exists(): return JsonResponse({'code': 0, 'msg': '没有选中的商品'}) total_amount = 0 order_items = [] for item in selected_items: product = Product.objects.select_for_update().get(pk=item.product_id) if product.stock < item.quantity: return JsonResponse({'code': 0, 'msg': f'{product.name}库存不足'}) product.stock -= item.quantity product.sales += item.quantity product.save() total_amount += product.price * item.quantity order_items.append(OrderItem( product=product, product_name=product.name, product_price=product.price, quantity=item.quantity, )) order = Order.objects.create( order_no=generate_order_no(), user=request.user, total_amount=total_amount, receiver_name=request.POST.get('receiver_name'), receiver_phone=request.POST.get('receiver_phone'), receiver_address=request.POST.get('receiver_address'), status='unpaid', ) order.items.bulk_create(order_items) selected_items.delete() return JsonResponse({'code': 1, 'order_no': order.order_no})

这段逻辑里有几个细节值得注意。下单时立即扣库存,但是订单状态还是待付款,如果用户迟迟不支付,就涉及库存占用问题。毕设项目里可以简单处理,超过一定时间自动取消订单并释放库存,或者干脆不做这层设计,在答辩时说明这是一个可以扩展的点。另外一个细节是库存扣减用的是商品当前价格,没有用购物车里的价格,这是合理的,因为价格应该以提交订单时为准。

订单状态流转建议做成一个统一的状态机,不要在视图里到处散落状态判断。比如支付成功更新为paid,发货更新为shipped,确认收货更新为completed。取消订单时,如果状态是unpaid,要把已扣库存加回来。

3.5 模拟支付与回调的幂等处理

毕设项目里接真实支付平台不太现实,一方面是资质问题,另一方面是沙箱环境也可能卡审核。通用的做法是做模拟支付:用户点击“立即支付”后,跳到模拟收银台页面,页面上展示订单号和应付金额,点击“确认支付”后,后端直接更新订单状态为待发货,同时跳转到支付成功页。

但“点击确认支付就改状态”这种最朴素的写法有个隐患,就是如果老师问“支付回调失败怎么办”“重复回调怎么办”,你回答不上来。所以我建议即使做模拟支付,也要把回调的概念做出来。

做法是:支付接口不直接改订单状态,而是返回一个支付凭证;另写一个支付回调接口,负责根据凭证修改订单状态。页面提交后先调用支付接口拿到凭证,再调用回调接口。回调接口要做幂等判断,如果订单已经是paid状态,直接返回成功,不再重复处理。

class PayView(View): def post(self, request, order_no): order = get_object_or_404(Order, order_no=order_no, user=request.user) if order.status != 'unpaid': return JsonResponse({'code': 0, 'msg': '订单状态异常'}) # 生成本次支付的凭证 pay_no = generate_pay_no() request.session[f'pay_{order_no}'] = pay_no return JsonResponse({ 'code': 1, 'pay_no': pay_no, 'pay_url': f'/payment/mock-pay/?order_no={order_no}&pay_no={pay_no}' }) class MockNotifyView(View): def post(self, request): order_no = request.POST.get('order_no') pay_no = request.POST.get('pay_no') order = get_object_or_404(Order, order_no=order_no) if order.status == 'paid': return JsonResponse({'code': 1, 'msg': '重复通知忽略'}) # 校验pay_no有效性,这里可以根据自己规则校验 order.status = 'paid' order.pay_time = timezone.now() order.save() return JsonResponse({'code': 1, 'msg': '支付成功'})

这段代码虽然仍是模拟支付,但引入了支付凭证和回调接口两个概念,答辩时老师问起来,你能讲清楚真实支付平台也是类似的模式,只是需要验签和发送请求而已。这一小段逻辑,往往比做一个复杂的优惠券系统更能体现你理解业务。

3.6 后台管理:Django Admin与自定义管理页面结合

后台管理建议分两步走。第一步把核心表注册进Django Admin,包括商品、分类、订单、用户,这样开发阶段你能快速操作数据。第二步是做一个简单好看的后台首页,用卡片展示订单总量、销售总额、商品总数、用户总数,再加一个最近订单数量和销量排行的小列表。

操作订单时,Admin里可以直接做发货操作,也可以注册一个简单的Action方法批量修改状态。这里提供一个比较实用的注册写法:

from django.contrib import admin from .models import Order, OrderItem @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') list_per_page = 20 @admin.action(description='标记为已发货') def mark_shipped(self, request, queryset): queryset.update(status='shipped')

如果你的后台想要更强的定制,可以加一个simpleui或者django-import-export这类插件,但要控制插件数量,避免项目里塞了一堆用不上的依赖,最后运行环境出问题。如果团队有精力,我建议后台框架采用Django Admin为基础,做一个独立的数据概览页面,既能快速实现,屏幕上也好看,这部分不会消耗太多时间。

4. 远程调试、项目文档与答辩演示的实操经验

4.1 远程调试怎么准备才不乱

很多毕设是学生本地开发,然后通过网络远程请人指导或协作调试。这个环节最容易出问题,提前把环境准备好,能节省大量时间。

首先,远程调试前要把项目跑起来,确保本地能访问首页,再考虑远程介入。很多人项目根本没跑通就找人远程,结果一半时间花在装依赖和调配置上,真正的问题还没看。建议先把依赖导出完整,最好带上python version信息,再用虚拟环境安装一遍。

其次,准备统一的调试入口。如果对方需要看你的运行效果,最简单的就是约定一个测试账号、一个后台管理账号,以及几个已经构造好的数据样例。远程调试时最怕的就是上来就说“我这里报错了,你帮我看一下”,然后截图也不发,报错日志也不给。自己事先准备好这些材料,既能提高效率,也是良好的开发习惯。

最后,调试时一定要功能优先。数据库连不上、静态文件跨域、模板报这种运行时错误,先看控制台日志,Django的报错信息算比较友好的,会直接告诉你错误发生在哪个文件哪一行。远程调试时尽量开Django的DEBUG模式,否则报错页只显示服务器错误,谁也看不出原因。

4.2 毕设文档与论文结构怎么搭

论文这部分,很多人在代码写完之后才动笔,结果发现写起来很痛苦。我的经验是:代码每完成一个模块,就把这个模块对应的论文初稿写了。等到项目做完,论文其实已经有了初稿。

论文的经典结构可以分成:绪论(背景和意义)、相关技术介绍(Django、Python、MySQL、HTML/CSS/JS)、系统分析(可行性分析、需求分析)、系统设计(总体架构、功能模块设计、数据库设计)、系统实现(每个模块的截图和核心代码说明)、系统测试(测试用例和结果)、总结展望。

写系统实现的时候,不要大段贴代码,而是挑核心代码片段,配合截图解释思路。例如写订单模块时,就贴事务扣库存那段代码,再贴一张订单列表页面截图,一段文字说明你如何处理并发问题。这样论文写出来详略得当,老师也爱看。

数据库设计章节最好画ER图和表结构说明。表结构可以做一个表格,列出字段名、字段类型、说明、是否为空。如果你用了自定义用户扩展,要重点解释为什么扩展而不重新造轮子。这个细节比较容易被答辩老师问到。

4.3 演示环境的准备与答辩话术避坑

毕设演示最怕现场翻车。独立复习一条原则:演示环境必须是一个已经跑通的本地环境,不是临时搭建的。建议在答辩前一晚把数据库数据清理一遍,构造一套干净完整的演示数据,包括:一个管理员账号、一个普通用户账号、至少10个商品、几个不同状态的订单、几张好看的商品图。演示时不要临时创建数据,而是顺着已有数据走完整流程。

按我的经验,演示顺序最好是:游客浏览首页和商品列表 → 注册登录 → 加购 → 下单 → 模拟支付 → 后台管理 → 统计看板。这条流程每一步都验证一次,最后在“数据统计页”展示订单数据发生变化,形成闭环。

关于话术,我的建议是不要背稿。准备几个必答问题的答案就够了:为什么选这个题目、用户权限怎么控制的、订单并发怎么处理、支付流程是怎样的、测试用例怎么设计的。这些问题在上一节的代码里基本都有对应的实现,提前对着代码理一遍思路就行。

答辩时还有一个容易被问倒的地方是“你自己做了什么、用了哪些组件”。这个一定要提前说清楚,遇到用了现成插件的情况,要强调你理解了它的原理并做了一定扩展,而不是一句“我引入了某个组件”带过。

5. 常见问题排查与避坑实录:我在实操中踩过的坑

5.1 静态文件和媒体图片总是404

这是Django项目新手最常遇到的问题,也是远程调试时出现频率最高的。很多人在本地能打开页面,但图片加载不出来,控制台报404。

原因基本就出在配置上。开发环境下,Django默认不处理media文件,你需要自己在urls.py里加一行:

from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)

同时确保settings.py里配好了:

MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media' STATIC_URL = '/static/' STATICFILES_DIRS = [BASE_DIR / 'static']

再排查一个细节:图片字段的upload_to路径是相对于MEDIA_ROOT的,上传后访问路径是“域名 + MEDIA_URL + upload_to子路径 + 文件名”。如果upload_to写成了绝对路径,或者文件名里有中文,都可能导致访问不正常。建议上传文件名都改成时间戳加随机数的形式,避免中文字符和各种奇怪符号。

5.2 数据库迁移时总是发现问题

用Django的ORM建好模型后,运行迁移一般比较顺利,但如果中途改动字段,很容易出现迁移提示说“有未解决的冲突”或者修改了字段后又报“字段不存在”。

这里分享一个自己整理的排查顺序:第一步,检查有没有多个App之间模型互相引用,修改字段后有没有涉及外键变化;第二步,看看迁移文件目录下是不是已经生成了大量的迁移文件,如果模型还没稳定,建议不要频繁生成迁移,而是等模型设计比较完整后再一次性迁移;第三步,如果数据库已经有不一致的数据,比如有记录的外键关联到了不存在的id,迁移时就会报错,需要先处理脏数据。

比较稳妥的做法是开发阶段数据库数据不重要的时候,直接删除迁移文件重建。但要注意,生产环境或保留数据的部署环境千万不要这么做。

5.3 订单重复提交与并发扣库存问题

我在前面已经给了事务和select_for_update的方案,这里再说排查思路。如果你在下单接口里没有加事务,可以模拟开两个浏览器窗口用同一个账号同时下单同一个商品,很快就会出现库存扣成负数的情况。

另一个常见问题是用户快速多次点击下单按钮,同一个订单生成了两次。处理办法有两个层面:前端点击提交后立即禁用按钮并显示“处理中”;后端判断如果用户有一笔待付款的同商品订单可以提示合并,或者给订单号加上唯一约束。最直接的做法还是导入事务,并在事务里判断购物车是否已被删除,如果购物车已经没有该项,说明可能已经被处理过。

5.4 部署运行环境差异导致的运行崩溃

本地开发好了,换一台电脑部署或者远程演示时跑不起来,原因通常有三个:一是依赖包版本不一致,比如本地Django是4.2,远程是3.2,语法可能就莫名出问题;二是数据库编码导致中文插入报错;三是相对路径写死了。

解决依赖问题,用requirements.txt锁定版本。数据库中文问题可以在MySQL里设置utf8mb4编码,创建数据库时指定字符集:CREATE DATABASE xxx DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci。写死相对路径的问题,项目里所有涉及文件路径的地方尽量基于BASE_DIR换算,不要直接写“C:\Users\xxx...”。

5.5 插画图片上传失败或页面卡顿

插画商品图片往往比较大,上传到ImageField时如果超过限制,可能会报错或者上传成功但页面加载很慢。Django里可以通过表单或后端逻辑限制文件大小,也可以用Pillow库在保存前做一次压缩。

这里给一个简单的压缩保存思路:

from PIL import Image import os def compress_image(image_field, output_size=(800, 800)): img = Image.open(image_field) img.thumbnail(output_size, Image.Resampling.LANCZOS) img.save(image_field.path, quality=85, optimize=True)

调用时机在图片字段保存之后。如果项目里有多个上传图片的地方,可以封装成工具函数统一调用。还有一个细节:ImageField依赖Pillow库,如果安装过程中报错,请检查Python版本和Pillow版本的兼容性,某些较新的Pillow版本要求Python 3.8以上才能安装,比较老的虚拟环境下需要固定pillow版本。

在实际操作里,我还遇到过上传图片成功但访问时方向错乱的情况,尤其是从手机上传的照片。Pillow读取图片EXIF信息会发现方向标记,但保存时不保留方向信息,就会导致横竖颠倒。这个问题虽然少见,但一旦发生在演示时就很尴尬。处理方式是读取EXIF中的orientation字段,在保存前旋转到正确的方向。

最后分享一点个人体会

做毕设项目,尤其是这种全栈电商类系统,最大的收获往往不是技术本身,而是学会怎么在一个时间窗口内把一个完整的作品交付出来。带了很多个项目之后,我越来越觉得,毕设最核心的是“可演示的完整性”,而不是功能数量多寡。哪怕少做一个专题页,也要保证用户从注册到下单一整个流程是顺畅的、闭环的。答辩老师看不懂代码细节,但一个能顺利跑完的业务流程,一个界面统一有设计感的页面,一眼就能看出你的完成度。

最后给你一个小建议:不管你的项目最后用了哪些库、哪些框架,一定要自己把每一行核心逻辑都过一遍,尤其是订单和支付这两块,因为这是答辩时最容易深入追问的地方。自己心里有数,演示时的底气完全不一样。

如果你正准备开始这个项目,建议先花一个晚上把数据库设计定下来,再花两天把用户和商品模块搭通,然后每天推进一个模块,最后留出三天做文档和演示准备。按这个节奏,时间是完全够用的。

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

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

立即咨询