
简介面向计算机专业毕业设计与课程作业的一套小区果蔬预定系统实现方案采用Python语言、Django框架与MySQL数据库完成设计与开发。系统前台提供用户注册、登录、商品分类分区展示、价格与采摘日期查看、商品图片浏览、商品详情页、收藏功能以及购物车提交订单、会员积分兑换菜品、微信/支付宝/银联/扫码等在线支付入口后台分为管理员与注册用户两类角色管理员可管理用户、商品类别、商品信息、收藏、订单、评价、在线支付统计、配送和会员积分注册用户可维护个人资料、查看积分、收藏、订单评价及配送信息前后台功能覆盖完整。压缩包共334个文件以Python源码、pyc编译文件、HTML页面和JPG/PNG图片素材为主另含SQL数据库脚本及少量CSS/JS样式脚本整体包体大小约2.4MB便于本地部署、阅读和二次开发。此外资源内附毕业论文与答辩PPT可辅助理解系统设计思路、数据库表结构及论文撰写框架适合作为课程作业或毕业设计的直接参考。目前已有34人学习下载对需要搭建同类型小区果蔬预定管理系统的同学具有一定借鉴价值。1. 从下单到配送拆解基于Django的果蔬预定业务链路小区果蔬预定系统最明显的分界点在于预定这两个字用户下单时部分果蔬还在地里库存和生产周期是联动的所以订单流程必须出现待采摘这种中间状态而不仅仅是待支付和已完成。这套基于Python Django MySQL实现的系统把前台商品展示、购物车提交、在线支付回调、后台订单调度和会员积分兑换串成了一条完整链路。对比普通电商demo它最值得研究的是订单状态机、支付验签、积分事务这几个模块如何在Django里落地。正在准备答辩或想快速打通Django电商全流程的开发者照着这条线拆代码比刷教程更有效。2. Django ORM 与 MySQL 数据建模商品、订单、积分的表结构设计2.1 用户信息扩展与会员积分字段设计Django自带的User模型只覆盖账号和密码积分、手机号、配送地址这些业务字段不能直接往上挂。常见的做法是建一个Profile模型和User做OneToOne关联这样既不动框架源码又能通过request.user.profile直接拿到扩展字段。from django.db import models from django.contrib.auth.models import User class Profile(models.Model): user models.OneToOneField(User, on_deletemodels.CASCADE, related_nameprofile) points models.IntegerField(积分余额, default0) phone models.CharField(手机号, max_length11, blankTrue) address models.CharField(默认配送地址, max_length200, blankTrue) created_at models.DateTimeField(auto_now_addTrue)积分字段用IntegerField而不是FloatField避免后续积分兑换菜品时出现浮点误差。related_name设成profile之后视图和模板里可以通过request.user.profile.points直接读取积分不用再手动查一次Profile表。phone字段预留11位后续如果要发短信通知配送状态会用到。2.2 商品分类与商品信息表的关联查询商品列表页面需要按分类分区展示同时显示每个分类下的价格区间和采摘日期。Category和Product是一对多关系产品表通过外键引用分类表。class Category(models.Model): name models.CharField(分类名称, max_length50) sort_order models.IntegerField(前台排序, default0) class Meta: ordering [sort_order] verbose_name 商品分类 class Product(models.Model): category models.ForeignKey(Category, on_deletemodels.CASCADE, related_nameproducts) name models.CharField(商品名称, max_length100) price models.DecimalField(单价, max_digits8, decimal_places2) pick_date models.DateField(采摘日期) image models.ImageField(商品图片, upload_toproducts/, nullTrue, blankTrue) stock models.IntegerField(可预订库存, default0) description models.TextField(商品描述, blankTrue) class Meta: indexes [ models.Index(fields[category, price]), ]price用DecimalField(max_digits8, decimal_places2)价格精确到分就够了千万别用FloatField存价格MySQL里浮点数的精度问题会在订单对账时暴露出来。indexes里给category和price建了联合索引前台按分类筛价格时走这个索引数据量过万后能把查询时间从百毫秒级压到十毫秒级。分类页面的查询有一个细节如果用Category.objects.prefetch_related(products)会把该分类下的全部商品加载到内存。更好的写法是只取当前分类和价格区间from django.db.models import Min, Max def product_list(request, category_idNone): categories Category.objects.annotate( min_priceMin(products__price), max_priceMax(products__price) ).filter(min_price__isnullFalse) products Product.objects.select_related(category) if category_id: products products.filter(category_idcategory_id) return render(request, index.html, { categories: categories, products: products, current_category: int(category_id or 0), })annotate加上Min和Max一次查出每个分类的价格区间不用在模板里对商品列表做二次遍历。select_related(category)会在JOIN时把分类对象取出来模板里product.category.name不会触发额外SQL。商品列表对应资源包里的index.html前端按categories循环渲染分类区块商品详情页对应detail.html展示商品图片、价格、采摘日期和收藏按钮。2.3 订单、订单项与配送信息的级联约束订单表不能直接挂商品列表否则一个订单买了三样果蔬就要在订单表里存三条记录后面查订单明细、改配送状态都会变成噩梦。标准做法是拆成Order主表和OrderItem从表订单项只保留下单那一刻的成交价和数量商品价格后续变动不影响历史订单。class Order(models.Model): STATUS_CHOICES ( (0, 待支付), (1, 待采摘), (2, 配送中), (3, 已完成), (4, 已取消), ) order_no models.CharField(订单号, max_length32, uniqueTrue) user models.ForeignKey(User, on_deletemodels.CASCADE, related_nameorders) status models.IntegerField(订单状态, choicesSTATUS_CHOICES, default0) total_amount models.DecimalField(订单总额, max_digits10, decimal_places2) points_earned models.IntegerField(本次获得积分, default0) created_at models.DateTimeField(auto_now_addTrue) class OrderItem(models.Model): order models.ForeignKey(Order, on_deletemodels.CASCADE, related_nameitems) product models.ForeignKey(Product, on_deletemodels.PROTECT) price models.DecimalField(下单时单价, max_digits8, decimal_places2) quantity models.IntegerField(数量, default1) class Delivery(models.Model): order models.OneToOneField(Order, on_deletemodels.CASCADE, related_namedelivery) receiver models.CharField(收货人, max_length50) phone models.CharField(联系电话, max_length11) address models.CharField(配送地址, max_length200) deliver_time models.DateTimeField(实际配送时间, nullTrue, blankTrue)OrderItem里的product外键用on_deletemodels.PROTECT而不是CASCADE这样商品被删除时如果还有历史订单引用它数据库会直接报错阻止删除防止订单明细里出现悬空引用。状态值含义触发动作0待支付创建订单后默认1待采摘支付回调成功后2配送中管理员后台点击发货3已完成用户确认收货或管理员标记4已取消超时未支付或主动取消Delivery和Order用OneToOneField关联一个订单只能有一条配送记录对应资源包里的配送信息模块。这种设计在查询时直接用order.delivery.address就能拿到配送地址不需要额外的外键查询。3. 前台购物流程实现从商品列表到在线支付回调3.1 商品分区展示与价格排序逻辑小区果蔬预定系统的首页和普通电商首页不同它更强调今日可订的概念采摘日期是用户决策的关键因素。商品列表按分类分区展示后还需要提供按价格和采摘日期排序的能力这对应资源包里的index.html和static_index.html两个模板文件。def product_list(request, category_idNone): sort_by request.GET.get(sort, default) qs Product.objects.select_related(category).filter(stock__gt0) if category_id: qs qs.filter(category_idcategory_id) if sort_by price_asc: qs qs.order_by(price) elif sort_by price_desc: qs qs.order_by(-price) elif sort_by pick_date: qs qs.order_by(pick_date) return render(request, index.html, {products: qs})sort参数通过URL查询字符串传入例如/index/?sortprice_asc。order_by(price)是升序order_by(-price)是降序Django ORM对排序字段做反转只需加一个负号。filter(stock__gt0)把已售罄商品过滤掉避免用户点进详情页才发现没库存。排序字段后续要扩展时直接在if分支里加条件即可。3.2 购物车与会话管理用session还是独立表小体量的果蔬预定系统里购物车优先用session存储Redis都不需要。因为购物车是临时数据用户清空浏览器就没了没必要持久化到MySQL。Django的session框架默认支持Cookie和数据库两种后端这里用默认的数据库session即可。def add_to_cart(request, product_id): product Product.objects.filter(pkproduct_id, stock__gt0).first() if not product: return JsonResponse({code: 1, msg: 商品不存在或已下架}) cart request.session.get(cart, {}) cart[str(product_id)] cart.get(str(product_id), 0) 1 request.session[cart] cart request.session.modified True return JsonResponse({code: 0, cart_count: sum(cart.values())})cart的结构是{商品ID: 数量}key用字符串是因为session在序列化时会把整数key转成字符串取出来再int()转回来容易漏。request.session.modified True这行很关键不写的话Django不会把修改后的session数据写回存储特别是session中已经存在cart字段时只在原字典上做增减操作框架无法感知数据变化。购物车页面对应cart.html订单确认页对应place_order.html。从购物车跳到订单确认页时需要把购物车里的商品ID、数量和单价带到模板展示此时候选商品价格要以数据库实时价格为准不能信任session里存的旧价格。def place_order_page(request): cart request.session.get(cart, {}) if not cart: return redirect(/cart/) product_ids [int(pid) for pid in cart.keys()] products Product.objects.filter(pk__inproduct_ids) items [{ product: p, quantity: cart[str(p.id)], subtotal: p.price * cart[str(p.id)], } for p in products] total sum(item[subtotal] for item in items) return render(request, place_order.html, {items: items, total: total})这段代码的核心是把session里的商品ID映射到数据库对象再计算小计和总额。total在视图里算好模板直接展示避免在模板里做乘法运算。这里没有做库存二次校验真正的库存扣减在提交订单时用事务完成页面展示阶段只负责把数据带出来。3.3 支付沙箱对接与积分结算的回调视图项目摘要里提到了微信支付、支付宝、银联和扫码支付实际开发中一般先接支付宝沙箱因为沙箱环境不需要真实商户资质拿到一对测试密钥就能跑通全流程。支付回调最容易出bug的地方是支付平台异步通知你的服务器时怎么确认这笔通知是真的。def alipay_notify(request): if request.method ! POST: return HttpResponse(fail) params request.POST.dict() sign params.pop(sign, None) alipay AliPay( appid2021000000000000, app_notify_urlNone, app_private_key_stringprivate_key, alipay_public_key_stringalipay_public_key, sign_typeRSA2, ) if not alipay.verify(params, sign): return HttpResponse(fail) trade_status params[trade_status] if trade_status TRADE_SUCCESS: order_no params[out_trade_no] order Order.objects.select_for_update().get(order_noorder_no) if order.status 0: order.status 1 order.points_earned int(order.total_amount) order.save(update_fields[status, points_earned]) profile order.user.profile profile.points int(order.total_amount) profile.save(update_fields[points]) return HttpResponse(success)回调视图有四个关键点。第一verify验签是第一步签名不过直接返回fail。第二select_for_update()锁住订单行保证支付回调并发时订单状态只被修改一次。第三必须检查order.status 0再做积分累加否则支付宝重复通知时会重复加积分。第四返回纯文本success支付宝网关只认success字符串收到其他内容会认为通知失败并重新推送。AliPay构造器里的appid、私钥和支付宝公钥来自沙箱控制台不要硬编码在视图里用环境变量或settings.py统一管理。积分计算规则是支付多少钱获得多少积分所以直接用int(order.total_amount)把金额转成积分数值。后续要调整兑换比例比如一元换10积分改成int(order.total_amount) * 10即可。提示回调视图必须声明为csrf_exempt否则Django的CSRF中间件会在POST请求时直接返回403支付宝网关收不到success响应就会反复重试通知。用户中心页面对应user_center_order.html展示我的订单、我的收藏、我的积分和配送信息。这些数据都从当前登录用户的关联对象上取订单列表建议分页每页20条就够。4. 后台管理端订单调度、配送指派与积分核销4.1 基于Django Admin的管理员权限配置后台管理一般直接用Django Admin改造省去写CRUD页面的时间。项目摘要里涉及管理员信息、用户注册管理、商品类别、商品信息、收藏、订单、支付统计、配送、积分管理九大模块其中商品和订单是高频操作对象需要在Admin里做定制。from django.contrib import admin from .models import Category, Product, Order, OrderItem, Delivery, Profile admin.site.register(Category) admin.site.register(Product) admin.site.register(Profile) class OrderItemInline(admin.TabularInline): model OrderItem extra 0 admin.register(Order) class OrderAdmin(admin.ModelAdmin): list_display (order_no, user, status, total_amount, points_earned, created_at) list_filter (status, created_at) search_fields (order_no, user__username) inlines [OrderItemInline] list_per_page 20 actions [mark_delivering, mark_completed] admin.action(description标记为配送中) def mark_delivering(self, request, queryset): updated queryset.filter(status1).update(status2) self.message_user(request, f已更新 {updated} 个订单)list_display指定列表页展示哪些列list_filter在侧边栏生成按状态和创建时间的筛选器search_fields支持按订单号和用户名搜索。OrderItemInline把订单明细直接嵌在订单详情页里管理员点进订单就能看到买了哪些商品。mark_delivering用queryset.filter(status1).update(status2)而不是直接queryset.update(status2)防止把已取消的订单也改成配送中。update()是QuerySet的批量更新方法不会触发模型实例的save()适合这种纯状态流转。4.2 订单状态流转与配送信息联动订单状态从待采摘变为配送中时配送信息里的实际配送时间要一并更新。这个逻辑放在Admin action或模型方法里比放在视图更合适因为后台操作和前台用户操作走的不是同一条代码路径。def confirm_delivery(order): from django.utils import timezone order.status 2 order.save(update_fields[status]) delivery, created Delivery.objects.get_or_create(orderorder) delivery.deliver_time timezone.now() delivery.save(update_fields[deliver_time])get_or_create处理配送记录不存在时自动创建的场景。第一次发货时配送记录还没生成订单创建阶段只生成了Order和OrderItemDelivery是在管理员点击发货时才创建的。update_fields里只写deliver_time告诉MySQL只更新这一个字段减少写放大。前台用户在我的订单页面看到配送中状态后可以点击确认收货此时订单状态从2变成3。这会触发一个状态转换视图def confirm_receipt(request, order_id): order Order.objects.filter( idorder_id, userrequest.user, status2 ).first() if not order: return JsonResponse({code: 1, msg: 订单不存在或状态不允许}) order.status 3 order.save(update_fields[status]) return JsonResponse({code: 0, msg: 已确认收货})这个视图的重点是filter条件里同时带上userrequest.user这是防止水平越权的关键。如果不加这个条件用户传一个别人的订单ID就能把别人的订单确认收货。status2限制也重要只有配送中的订单才能确认收货状态为待支付或已取消的订单不允许直接跳转到已完成。4.3 用ORM聚合查询实现支付统计和积分汇总后台的在线支付统计模块需要展示今日收入、订单数、待处理订单数等指标这些数据用Django的聚合函数一条SQL就能查出来不需要写原生SQL。from django.db.models import Sum, Count, Q def admin_stats(request): from django.utils import timezone today timezone.now().date() base_qs Order.objects.filter(created_at__datetoday) stats base_qs.aggregate( total_incomeSum(total_amount, default0), order_countCount(id), paid_ordersCount(id, filterQ(status__gte1)), pending_deliveryCount(id, filterQ(status1)), ) return render(request, admin/statistics.html, {stats: stats})aggregate返回一个字典key是传入的别名value是对应聚合结果。Sum(total_amount)对所有订单金额求和Count(id)统计订单数。filter参数是Django 2.0之后支持的条件聚合写法相当于SQL里的SUM(CASE WHEN ... THEN 1 ELSE 0 END)。积分核销的报表逻辑类似核心是统计每天发放了多少积分、兑换菜品消耗了多少积分。Order.points_earned记录了每单产生的积分通过SUM(points_earned)就能算出每日积分发放总量。积分兑换菜品如果是一个独立的抵扣动作建议在Order表里再加一个points_deducted字段表示本次订单用了多少积分抵扣对账时一眼就能看清。5. 部署与调试MySQL中文编码、静态文件与支付回调本地验证5.1 MySQL中文编码与mysqlclient安装第一次跑这个项目最常见的坑是pip install mysqlclient编译失败。macOS或Linux环境需要先安装MySQL客户端开发库再装驱动Windows下推荐直接下载编译好的whl文件避免本地编译浪费时间。启动项目的路径一般是先确认Python版本在3.8以上创建虚拟环境然后依次安装django和mysqlclient。用VSCode开发的话建议先在.vscode/settings.json里指定Python解释器路径避免终端和调试器用的不是同一个环境。装好驱动后在settings.py里配置数据库连接字符集要显式声明成utf8mb4否则商品名称里出现emoji或生僻字时会报Incorrect string value错误。DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: veg_pre_order, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, OPTIONS: { charset: utf8mb4, init_command: SET sql_modeSTRICT_TRANS_TABLES, }, } }utf8mb4是MySQL 5.7以上对UTF-8的完整实现能覆盖四字节字符。init_command里的STRICT_TRANS_TABLES会把数据长度超限等错误变成硬报错开发阶段开着能尽早发现字段长度不足的问题。配置完成后按顺序执行迁移命令python manage.py makemigrations python manage.py migrate python manage.py createsuperusermakemigrations扫描models.py生成迁移文件migrate把迁移应用到数据库createsuperuser创建后台管理员账号这三步缺一不可。资源包里的client.conf是Nginx反代的配置文件如果部署在云服务器上需要把server_name改成你的域名并把proxy_pass指向Gunicorn或uWSGI监听的端口。5.2 静态文件与支付回调的本地调试资源包里包含main.css和reset.css还有index.html、cart.html、place_order.html等模板文件说明前端资源是拆开管理的。上线前需要执行collectstatic命令把所有静态文件收集到STATIC_ROOT目录否则Nginx无法通过别名访问到它们。python manage.py collectstatic --noinputcollectstatic会把各app的static目录复制到settings.py里配置的STATIC_ROOT下Nginx只需要指向这个目录不需要给每个app单独配置location。支付回调的本地调试有一个绕不开的问题支付宝和微信的异步通知只能访问公网地址。常见做法是用内网穿透工具把本机8000端口映射到一个公网域名然后在支付宝沙箱配置里填成回调URL。换域名后需要同步修改settings.py里的回调地址还要注意回调视图加csrf_exempt装饰器from django.views.decorators.csrf import csrf_exempt csrf_exempt def alipay_notify(request): # 回调验签与业务处理回调视图里的每个print或logger信息都不要删支付对账时这些日志是唯一的线索。建议把order_no、trade_status、sign校验结果都记录到文件日志里线上排查问题时这是最直接的手段。本文还有配套的精品资源点击获取