简介:以Python与Django框架为核心的农产品销售系统毕业设计项目,面向计算机专业毕业生、需要完成课程设计或期末大作业的学生,以及想通过项目实战巩固Django开发技能的Python学习者。项目包含完整可运行的源码与数据库,覆盖个人中心、用户管理、商家管理、产品类型管理、农产品管理、系统管理、订单管理等核心业务模块,有助于理解Web系统的权限划分、数据建模和前后端交互逻辑,免去从零搭建的重复劳动。压缩包内共558个文件,大小为25.12MB,主要文件类型包括Python后端源码、Vue前端组件、JavaScript脚本、SVG图标、图片素材、CSS样式、SQL数据库文件等,并附带安装、运行、构建等自动化批处理脚本,便于快速配置依赖、初始化和启动项目。另有配套文档与运行教程,可帮助对照操作。目前已有152人学习,适合正在筹备毕业设计、希望掌握Django项目完整流程的读者。演示视频和逻辑讲解进一步降低了上手门槛,遇到报错时可结合教程排查修复,实用性较强。
1. 毕业设计里的农产品销售系统,先分清“能跑”和“能讲”是两件事
一个包含源码、运行教程、讲解和演示视频的毕业设计压缩包,接收方第一件事往往是解压、装依赖、python manage.py runserver,看到页面动起来就把它归为“能跑”。但答辩和面试真正检验的是一个系统的数据边界:商品、用户、订单、库存之间谁依赖谁,购物车到底该存数据库还是 session,并发下单会不会超卖。基于 Python + Django 的农产品销售系统正好能把这几件事聚在一个项目里讲透。这篇文章不介绍某个特定源码包的内容,只讲一线工程判断下,这类毕业设计系统从建模到跑通再到可讲解的常规做法。
2. 用 Django 搭起农产品销售系统的项目骨架与数据模型
2.1 为什么是 Python + Django:选型和环境准备
农产品销售系统的核心在业务规则,不在页面动效。Django 自带 ORM、Admin、Session、Auth,能把“商品 - 订单 - 用户”这条主线以最短路径跑通,这正是毕业设计最需要的节奏。相比 Flask 拼第三方库,Django 的所有环节都能在一个框架内闭环;相比 Spring Boot,Python 的模型定义和迁移成本低一个量级。
环境准备按常见做法来:Python 3.9 或 3.10,Django 用 3.2 LTS 或 4.x 都可以。SQLite 足够毕业设计演示,省去数据库安装步骤;如果学校指定 MySQL,要注意 mysqlclient 在 Windows 下的安装差异。另一个容易踩的坑是虚拟环境:不要用系统 Python 直接pip install django,先建 venv,否则换一台机器演示时依赖版本混乱,这是源码包交付后最常见的问题。
提示:先执行
python --version确认版本大于 3.8,再继续后面的操作。
2.2 创建项目和 app:django-admin startproject 的正确起手
从零搭建时,我一般不会把业务代码全塞进主项目目录,而是按业务域拆 app。农产品销售系统的常见拆分是 goods(商品)、orders(订单)、users(用户)三个模块,再加一个主配置目录承载 settings 和根路由。
django-admin startproject farm_shop cd farm_shop python manage.py startapp goods python manage.py startapp orders python manage.py startapp users# 将三个 app 加入 INSTALLED_APPS 后,先跑一次内置迁移,验证数据库链路 python manage.py migrate python manage.py createsuperuser python manage.py runserverstartapp生成的应用目录默认在项目根下,迁移时 Django 按 INSTALLED_APPS 顺序扫描 models,注册顺序不影响数据表创建,但影响 admin 后台的展示顺序,所以 goods 放在 orders 前面更自然。migrate会生成 auth、admin、session 等内置表,看到Applying auth.xxxx... OK就说明数据库连接正常;createsuperuser创建的账号用于后续进入后台录商品。
2.3 商品、订单、用户的数据模型怎么设计才像毕设
数据模型是这类项目最值得写进讲解稿的部分。用户直接用 Django 的 User 模型,再加 Profile 扩展手机号和收货地址;商品放在 goods 的 Product 表;订单与订单明细分离,这是销售系统的基本建模习惯。下面这段模型代码是核心骨架。
# goods/models.py from django.db import models class Category(models.Model): name = models.CharField('分类名', max_length=50, unique=True) parent = models.ForeignKey( 'self', on_delete=models.CASCADE, null=True, blank=True, verbose_name='上级分类' ) class Meta: verbose_name = '商品分类' verbose_name_plural = verbose_name ordering = ['id'] def __str__(self): return self.name class Product(models.Model): category = models.ForeignKey( Category, on_delete=models.PROTECT, verbose_name='分类' ) name = models.CharField('商品名称', max_length=120) price = models.DecimalField('售价', max_digits=10, decimal_places=2) origin_price = models.DecimalField( '原价', max_digits=10, decimal_places=2, default=0 ) stock = models.PositiveIntegerField('库存', default=0) sales = models.PositiveIntegerField('销量', default=0) image = models.ImageField( '商品图', upload_to='products/%Y/%m/', blank=True ) desc = models.TextField('详情描述', blank=True) is_active = models.BooleanField('上架', default=True) created_at = models.DateTimeField('创建时间', auto_now_add=True) class Meta: verbose_name = '农产品' verbose_name_plural = verbose_name ordering = ['-sales', '-created_at'] def __str__(self): return self.name价格用 DecimalField 而不是 FloatField。单价涉及精度,Float 在总价累加时会产生浮点误差,答辩时提一句“Decimal 保证金额精度”是加分点;库存用 PositiveIntegerField 防止负数;image 的 upload_to 按年月分目录,是后台大量传图后不卡目录的常规做法。Category 的 parent 自关联为“二级分类”留了口子,不写复杂嵌套,但能在讲解时把扩展方向说清楚。
订单模型放在 orders/models.py。order_no 手动生成唯一订单号,状态用 CharField 加 choices,订单金额在保存前算好,方便列表页直接显示而不用每次 join 明细表。
# orders/models.py from django.conf import settings from django.db import models class Order(models.Model): STATUS_CHOICES = [ ('pending', '待付款'), ('paid', '已付款'), ('shipped', '已发货'), ('completed', '已完成'), ('cancelled', '已取消'), ] order_no = models.CharField('订单号', max_length=32, unique=True) user = models.ForeignKey( settings.AUTH_USER_MODEL, on_delete=models.PROTECT, verbose_name='用户' ) status = models.CharField( '状态', max_length=10, choices=STATUS_CHOICES, default='pending' ) total_amount = models.DecimalField( '订单金额', max_digits=10, decimal_places=2 ) receiver = models.CharField('收货人', max_length=30) phone = models.CharField('联系电话', max_length=20) address = models.CharField('收货地址', max_length=255) remark = models.TextField('备注', blank=True) created_at = models.DateTimeField('下单时间', auto_now_add=True) class Meta: verbose_name = '订单' verbose_name_plural = verbose_name ordering = ['-created_at'] def __str__(self): return self.order_no class OrderItem(models.Model): order = models.ForeignKey( Order, on_delete=models.CASCADE, related_name='items', verbose_name='订单' ) product = models.ForeignKey( 'goods.Product', on_delete=models.PROTECT, verbose_name='商品' ) product_name = models.CharField('商品名称快照', max_length=120) price = models.DecimalField('成交单价', max_digits=10, decimal_places=2) quantity = models.PositiveIntegerField('数量', default=1) class Meta: verbose_name = '订单明细' verbose_name_plural = verbose_name def __str__(self): return f'{self.product_name} x {self.quantity}'这里最能体现工程习惯的是 product_name 快照字段。商品名称和价格在订单产生后可能被后台修改,如果不做快照,历史订单的金额和名称就会跟着商品表一起变。OrderItem 保存下单当刻的商品名和单价,Product 被删除或改价不影响历史数据。对应地,Product 与 OrderItem 的外键用on_delete=models.PROTECT,有订单关联的商品不允许直接删除,避免后台误删后明细查询不到商品。
常用字段设计归纳成一张表,讲解和复盘时对着说会清晰不少。
| 模型 | 关键字段 | 设计意图 |
|---|---|---|
| Category | parent 自关联 | 支持二级分类,不引入额外库 |
| Product | price, stock, sales | Decimal 保金额精度,库存禁止负数 |
| Order | order_no, status, total_amount | 订单号唯一,状态机用 choices 限定 |
| OrderItem | product_name, price | 防止商品改名/改价影响历史订单 |
| User | 内置扩展 Profile | 手机号和收货地址放扩展表 |
2.4 先让 admin 后台能录商品:迁移与超级用户
模型写完后生成并执行迁移:python manage.py makemigrations goods orders,然后python manage.py migrate。有模型改动时千万别直接改数据库表,迁移文件要一起提交到源码包里,这样别人拿到代码后只要 migrate 就能重建,不需要手动导入 SQL,这是源码交付时“能复现”的前提。
打完迁移后到 admin 里注册 Product 和 Order,先不做复杂展示,只保证能录入数据:
# goods/admin.py from django.contrib import admin from .models import Category, Product @admin.register(Product) class ProductAdmin(admin.ModelAdmin): list_display = ['name', 'category', 'price', 'stock', 'is_active'] list_filter = ['category', 'is_active'] search_fields = ['name']在进入购物车开发前把管理员账号建好,并且用后台手动录三到五个真实感的农产品,价格、库存、图片都填全。后续测试商品列表、购物车、订单时,有干净数据比现造数据省事得多。这一步也直接验证了 ImageField 的图片目录是否可写,有些系统跑起来很顺,一传图片就 500,多半是 MEDIA_ROOT 没配置,见第 4 章。
3. 商品浏览、购物车与订单提交:把核心业务跑通
3.1 商品列表页和详情页的视图怎么写
商品列表页按分类过滤和关键词搜索是基本功能,用函数视图写流程更直白,也便于在讲解时说明“一次请求的完整链路”。列表页只展示上架商品,用 is_active 过滤。
# goods/views.py from django.shortcuts import render, get_object_or_404 from .models import Category, Product def product_list(request): products = Product.objects.filter(is_active=True) category_id = request.GET.get('category', '') keyword = request.GET.get('q', '').strip() if category_id: products = products.filter(category_id=category_id) if keyword: products = products.filter(name__icontains=keyword) return render(request, 'goods/product_list.html', { 'products': products, 'categories': Category.objects.all(), }) def product_detail(request, pk): product = get_object_or_404(Product, pk=pk, is_active=True) return render(request, 'goods/product_detail.html', {'product': product})category_id 用空字符串做默认值,而不是用 None,是为了避免 URL 上出现?category=None时 filter 匹配到空值。用name__icontains做模糊搜索,不区分大小写。这里的查询条件虽然简单,却是 Django ORM 的典型用法,答辩时把filter与get的返回区别、__icontains对应的 SQL 效果讲出来,比背概念更有说服力。
模板代码不展开,只提一个关键点:列表页每张卡片只显示商品名、价格、加入购物车按钮,加入按钮的 form 直接 POST 到购物车接口,不要在商品列表页用<a href>触发库操作,避免爬虫或预加载带来误下单。
3.2 购物车不建表,用 session 存就够
农产品销售系统的购物车常见做法是不建数据库表,直接存 Django Session。理由很实际:未登录用户也能加购物车,订单生成时再强制登录;购物车数据不需要跨设备同步,session 落在服务端也更安全。session 里的结构用字典,键是商品 ID 的字符串形式,值是数量,读写非常轻。
# goods/views.py from django.contrib import messages from django.shortcuts import redirect, get_object_or_404 from .models import Product def add_to_cart(request, product_id): product = get_object_or_404(Product, pk=product_id, is_active=True) cart = request.session.get('cart', {}) key = str(product.id) current_qty = cart.get(key, 0) if current_qty + 1 > product.stock: messages.warning(request, f'{product.name} 库存不足') return redirect('goods:product_list') cart[key] = current_qty + 1 request.session['cart'] = cart request.session.modified = True messages.success(request, f'{product.name} 已加入购物车') return redirect(request.META.get('HTTP_REFERER', '/'))关键在request.session.modified = True这一行。Django 对 session 的修改检测基于对象标识,直接改cart字典内容时,外层 session 对象没有赋新值,某些版本不会自动保存,手动置 modified 是可靠做法。库存校验放在加入时做第一道拦截,真正扣库存放在订单提交时,两层校验各管一段。HTTP_REFERER回跳让用户留在列表页原来的位置,体验上比刷新后回到顶部好很多。
购物车详情页遍历 session 中每个 key,查询商品并拼出数量和小计。这里有一个要注意的点:session 里的商品可能已被下架或删除,查询时要用filter(pk__in=keys, is_active=True),而不是 for 循环里逐个 get,避免一次请求发起 N+1 次 SQL。
3.3 订单生成与库存扣减:一个事务把并发问题挡住
订单提交是整个系统里最值得认真对待的地方。错误代码常写成“先查库存,再扣库存”,但两个操作中间夹着网络延迟时,两个用户可能同时读到库存 1,然后一起下单成功,最终库存变成负数。Django 的解决方式是在事务里用select_for_update()对商品行加锁,锁住后另一个请求必须等当前事务结束才能读同一行。
# orders/views.py import uuid from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db import transaction from django.http import Http404 from django.shortcuts import redirect, render from goods.models import Product from .models import Order, OrderItem @login_required def checkout(request): cart = request.session.get('cart', {}) if not cart: messages.warning(request, '购物车是空的') return redirect('goods:cart_detail') if request.method == 'POST': try: with transaction.atomic(): total = 0 order_items = [] for product_id, quantity in cart.items(): product = Product.objects.select_for_update().get( pk=product_id, is_active=True ) quantity = int(quantity) if product.stock < quantity: raise ValueError(f'{product.name} 库存不足') product.stock -= quantity product.sales += quantity product.save(update_fields=['stock', 'sales']) total += product.price * quantity order_items.append((product, quantity)) order = Order.objects.create( order_no=uuid.uuid4().hex[:16].upper(), user=request.user, total_amount=total, receiver=request.POST.get('receiver', '').strip(), phone=request.POST.get('phone', '').strip(), address=request.POST.get('address', '').strip(), ) OrderItem.objects.bulk_create([ OrderItem( order=order, product=product, product_name=product.name, price=product.price, quantity=quantity, ) for product, quantity in order_items ]) request.session['cart'] = {} messages.success(request, '下单成功') return redirect('orders:order_detail', pk=order.pk) except ValueError as exc: messages.error(request, str(exc)) except Exception: messages.error(request, '下单失败,请稍后重试') return render(request, 'orders/checkout.html') @login_required def order_detail(request, pk): order = Order.objects.filter(pk=pk, user=request.user).first() if order is None: raise Http404 return render(request, 'orders/order_detail.html', {'order': order})transaction.atomic()保证订单、订单明细、库存扣减要么全部成功,要么全部回滚,不会出现“订单生成了但库存没扣”的中间态。select_for_update()在 PostgreSQL 和 MySQL 下都会对命中行加排他锁;SQLite 在写事务时整体锁库,毕业设计规模下效果一致,这句话写进讲解稿能体现对数据库行为的理解。total 在服务端根据数据库里的价格重新计算,完全忽略前端传值,这是订单系统的基本安全底线。
下单成功后清空 session 里的购物车,OrderItem 快照保证“我的订单”里的金额和商品名不会因商品表改动而失真。save(update_fields=['stock', 'sales'])只更新这两个字段,减少写库范围。必填项校验在演示代码里直接取 POST 参数,实际交付建议换 Django Form 完成。
4. 后台管理、权限与图片上传:让系统真正可交付
4.1 admin 后台的三个必配项:list_display、list_filter、search_fields
毕业设计的管理后台先把 Django 自带 admin 用好,比从零写一套后台系统省力得多。很多源码包里后台是原样注册没有配置,演示时点进 Order 列表看不到任何有用信息。常见做法是配好三个属性:list_display 决定列表展示哪些列,list_filter 决定筛选项,search_fields 决定搜索框查哪些字段。这三项配好,管理后台的可视化效果立刻上一个档次。
# orders/admin.py from django.contrib import admin from .models import Order, OrderItem class OrderItemInline(admin.TabularInline): model = OrderItem extra = 0 readonly_fields = ['product_name', 'price', 'quantity'] @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_editable = ['status'] inlines = [OrderItemInline] def has_delete_permission(self, request, obj=None): return False订单状态用 list_editable 直接在列表页下拉改,发货、完成、取消这类操作不用进详情页,演示时非常顺手。user__username跨表搜索的写法与 ORM 查询一致。has_delete_permission返回 False 后订单不可直接删除,对应模型层 PROTECT 的设计,符合真实销售系统“订单只可取消不可物理删除”的业务规则。admin 界面先不急着上第三方美化,把原生能力用透,答辩讲权限时更有底气。
4.2 普通员工的权限控制:Django 自带认证模型怎么分配
“后台谁能干什么”是管理系统的刚性要求。Django 自带 User、is_staff、is_superuser 和权限关联表,直接用于后台员工账号管理。一名客服只需要改订单状态,不应当能改商品价格。最简单的落地方式是新建“客服”组,勾选 orders 模块的查看和修改权限,再把员工账号拉进这个组。
# 在 shell 里创建组和权限,也可以在 admin 页面操作 from django.contrib.auth.models import Group, Permission group, created = Group.objects.get_or_create(name='客服') perms = Permission.objects.filter( codename__in=['view_order', 'change_order', 'view_orderitem'] ) group.permissions.set(perms)权限分配本身不需要写业务代码,Django 会在 admin 视图中根据request.user.has_perm自动判断,但 shell 里跑这段脚本能让你在演示时快速重建环境。组和权限的关系值得在讲解时理清:组包含权限,用户属于组,最终判断依据是user.has_perm('orders.change_order')。给客服组加上view_orderitem而不是change_orderitem,能保证后台详情页正常显示明细,又禁止修改历史成交价格。
4.3 商品图片上传:settings 与 MEDIA 配置别等最后才做
商品图片上传是农产品销售系统的必备环节,它的坑不在模型层,而在于 settings 和 URL 的配合。模型里写了 ImageField 之后,不配置 MEDIA_ROOT,上传时文件会写到默认空路径,浏览器访问图片 URL 返回 404。先把配置补全:
# settings.py MEDIA_URL = '/media/' MEDIA_ROOT = BASE_DIR / 'media'# farm_shop/urls.py from django.conf import settings from django.conf.urls.static import static urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT)MEDIA_ROOT 是文件落盘的实际目录,MEDIA_URL 是浏览器访问的 URL 前缀,两者分离。static()只在DEBUG=True时生效,生产环境通常换 Nginx 或 WhiteNoise 处理,但毕业设计本地演示阶段完全够用。ImageField 依赖 Pillow 库,环境里没装会在迁移或后台保存时报错,执行pip install Pillow即可。上传后浏览器仍打不开图片时,先确认 urls.py 里 static 追加的位置必须在 urlpatterns 定义之后,这是配置顺序的常见失误。
5. 把能跑的项目变成能讲的项目:订单导出 CSV 与演示验证技巧
5.1 用 StreamingHttpResponse 导出订单 CSV
演示视频里如果只是点开页面看列表,评委的注意力会很快分散。一个订单导出功能能让系统边界清晰很多。Django 里导出 CSV 的常用做法是StreamingHttpResponse,订单数量大时边生成边写响应,不占内存。content_type 要带 utf-8-sig,否则 Excel 打开中文会乱码。
# orders/views.py import csv from django.contrib.admin.views.decorators import staff_member_required from django.http import StreamingHttpResponse from .models import Order def _order_rows(queryset): yield ['订单号', '用户', '状态', '金额', '下单时间'] for order in queryset: yield [ order.order_no, order.user.username, order.get_status_display(), str(order.total_amount), order.created_at.strftime('%Y-%m-%d %H:%M:%S'), ] @staff_member_required def export_orders_csv(request): qs = Order.objects.select_related('user').order_by('-created_at') response = StreamingHttpResponse( _order_rows(qs), content_type='text/csv; charset=utf-8-sig', ) response['Content-Disposition'] = 'attachment; filename="orders.csv"' return response生成器函数_order_rows用 yield 逐行输出,第一条是表头,后面是数据。Content-Disposition里的 filename 决定下载文件名,浏览器会直接触发下载。select_related('user')避免每行订单查一次用户表,导出几千条订单时性能差异明显。把这个函数接到 URL 上,地址栏访问/orders/export/即可下载 CSV,这个技巧可以放在演示视频最后 30 秒。
5.2 用一段 shell 脚本验证“下单 - 扣库存 - 生成明细”全链路
没有自动化测试的毕业设计,至少要做一次从数据角度可证明的链路验证。在manage.py shell里跑一小段脚本,确认库存、销量、订单金额三处数据一致:
python manage.py shellfrom django.contrib.auth.models import User from django.test import Client from goods.models import Product from orders.models import Order, OrderItem product = Product.objects.create( name='验证用番茄', price=5, stock=10, is_active=True ) user = User.objects.create_user(username='demo_check', password='demo123456') client = Client() client.login(username='demo_check', password='demo123456') session = client.session session['cart'] = {str(product.id): 2} session.save() resp = client.post('/orders/checkout/', { 'receiver': '张三', 'phone': '13800000000', 'address': '测试地址', }) order = Order.objects.get(user=user) item = OrderItem.objects.get(order=order) print(resp.status_code, order.total_amount, item.price, item.quantity) print(product.stock, product.sales)这段脚本模拟了无浏览器的完整下单流程。Client 是 Django 测试客户端,session 手动写入购物车后调用订单接口,最后打印订单金额和明细。如果 total_amount 不等于 item.price * item.quantity,说明金额计算有偏差;如果库存没从 10 变成 8,说明事务没生效或扣减逻辑没执行。这套验证方式比手动点击更可重复,也能在答辩时作为“我验证过数据一致性”的依据。源码包交付时,把 README 里这几条命令跑通再打包,比任何讲解视频都能更快暴露依赖缺失问题。
本文还有配套的精品资源,点击获取