
简介Web开发与移动端技术的融合正在改变传统餐饮行业的服务模式。在软件开发领域前后端分离架构已成为主流前端通过微信小程序提供轻量级交互体验后端则借助Django框架快速构建稳定的业务逻辑。微信小程序凭借其开发成本低、生态成熟、分发便利等优势成为校园项目与中小企业原型验证的热门选择而Django自带Admin后台和ORM机制能大幅提升API开发效率。从用户登录、菜品浏览到购物车与订单流转系统设计需兼顾数据一致性、事务处理和接口安全。JWT认证机制保障了用户身份的可靠性车辆状态与价格快照的设计则体现了电商系统的核心原则。本文围绕在线点餐系统的完整开发流程深入解析数据库设计、接口封装、联调部署及常见问题排查为初学者提供一个从零落地的全栈实践参考。1. 项目整体设计与技术选型思路毕业设计选“在线点餐”这个题目说实话是看了很多届学长学姐的经验之后才定下来的。不是因为它多高大上而是因为它涵盖的知识点非常完整——有用户端交互、有业务逻辑、有后台管理、有数据库设计最关键的是生命周期可控三个月时间足够把核心功能做完、写好论文、做出答辩演示。这个项目确定下来的技术栈是前端微信小程序、后端Python Django。很多同学纠结要不要上vue或react做一个后台管理页面我的结论是除非你确实学有余力否则别加。毕设的评审重点是你的业务逻辑是否完整、数据库设计是否规范、前后端交互是否顺畅而不是页面数量。把小程序端的体验做扎实比多做一套后台界面更划算。1.1 为什么是微信小程序而不是原生App或H5选微信小程序作为前端载体主要是三方面考虑。第一是开发成本。小程序使用WXML WXSS JavaScript语法接近H5有前端基础的人一两周就能上手。原生安卓或iOS开发不仅要学两套语言还要处理机型适配、应用商店审核这些额外成本作为一个毕设项目来说性价比不高。第二是分发与演示方便。答辩的时候老师用微信扫一个体验版二维码就能在手机上直接看效果不需要安装APK不需要连接开发工具。这台设备能跑换一台设备也能跑能减少很多“在你电脑上没问题在我电脑上就报错”的尴尬。第三是生态成熟。微信登录、支付虽然毕设一般不做真实支付、地图定位、图片上传这些都有官方API而且社区里踩坑记录非常多出了问题基本都能搜到答案。这一点在赶论文的时候尤其重要——没有什么是比“文档齐全”更让人安心的了。Django这边同理。它自带的Admin后台能让你免费拿到一套数据管理界面虽然样式朴素了点但拿来演示“商家管理菜品”这个场景完全够用。配合Django REST FrameworkDRF写API整个后端的开发量比Spring Boot能省三分之一左右这对时间紧张的大四学生来说是实打实的优势。1.2 在线点餐系统的核心业务模块开工之前先把业务边界划清楚这是整个项目最重要的一步。在线点餐听起来简单但如果不加控制地发散你会发现自己居然要做购物车、订单、支付、优惠券、会员积分、配送调度……一个比一个复杂。我的做法是只保留三个端、五条核心链路用户端小程序浏览菜品、加入购物车、提交订单、查看订单状态、个人资料修改。商家端Django Admin菜品分类管理、菜品上下架、订单状态流转待接单 → 制作中 → 已完成。公共模块用户登录微信授权 手机号绑定、文件上传菜品图片。五条核心链路分别是用户登录流程、菜品浏览流程、购物车加购流程、下单流程、订单状态流转流程。其余的功能比如优惠券、拼单、评价晒单统一砍掉在论文“后续展望”里写一句“可作为未来扩展方向”就够了。这样设计的好处是每一部分工作量都可控同时数据的关联关系又足够复杂——用户、菜品、购物车、订单、订单明细五张表之间都有外键关系写论文的时候ER图很丰满答辩介绍的时候逻辑也清晰。1.3 项目目录结构与代码组织项目采用前后端分离的目录方式分成backendDjango工程和miniprogram微信小程序工程两个同级目录这样两个端可以独立打包、独立部署。online-ordering/ ├── backend/ │ ├── manage.py │ ├── requirements.txt │ ├── config/ # 项目配置 │ ├── apps/ │ │ ├── users/ # 用户模块 │ │ ├── dishes/ # 菜品与分类模块 │ │ ├── orders/ # 购物车与订单模块 │ │ └── common/ # 通用工具 │ └── static/ # 上传文件、静态资源 └── miniprogram/ ├── app.js ├── app.json ├── pages/ │ ├── index/ # 首页菜品列表 │ ├── cart/ # 购物车 │ ├── orders/ # 订单列表 │ ├── order-detail/ # 订单详情 │ └── profile/ # 个人中心 └── utils/ └── request.js # 封装的网络请求目录结构最大的原则是按业务模块切分而不是按技术类型切分。很多同学喜欢把所有的views放一个文件、所有的models放一个文件一旦功能多起来就会变成巨型文件看着头疼改着也头疼。Django的app机制本来就是鼓励按业务拆分的要充分利用框架的约定。2. 后端核心实现Django 模型与接口设计后端侧的深度决定了这个毕设的评分上限。很多同学的毕设后台就是一套简单的增删改查——我的建议是增删改查可以做但业务状态流转一定要做对这部分才是答辩时能和老师对话的深度所在。2.1 数据库模型设计五张表的关联关系在线点餐系统的数据模型我最终落在五张核心表上。设计的时候反复推敲过商品表和SKU库存量单位要不要拆开后来考虑到毕设场景用不到规格属性就简化成“菜品表 一个默认图片字段”。如果要做多规格可以在菜品表下挂一个sku_set但为了控制复杂度我放弃了。这五张表分别是User用户表继承Django自带用户模型新增openid和avatar字段。openid是小程序用户的唯一标识前端wx.login()拿到code后后端通过微信接口换得直接存数据库做唯一约束。Category分类表字段就是name和sort_order。排序很重要首页菜品分类的展示顺序从sort_order取而不是按ID。Dish菜品表title、description、price、image、category外键、is_available。价格字段用DecimalField(max_digits8, decimal_places2)这个类型在事务计算中能避免浮点数精度问题。is_available没上榜但非常关键——菜品下架是逻辑删除不会影响历史订单的展示。Order订单表order_no订单号、user外键、status、total_amount、remark、created_at。状态字段用SmallIntegerField加 choices 常量而不是直接用字符串存这样既保证可读性又能约束取值范围。OrderItem订单明细表order外键、dish外键、price下单时快照价格、quantity。这里必须解释清楚明细表里为什么要有dish外键同时又要冗余price因为菜品价格会变但订单历史价格不能跟着变。订单表与订单明细表是一对多关系订单明细与菜品是一对一关系。这个设计保证了下单时的数据快照后续菜品改价或删除历史订单的账单依然准确。2.2 基于 DRF 的接口设计与视图实现后端API使用的是Django REST Framework。DRF对毕设项目最大的价值在于序列化器帮你省了一大堆手写 JSON 序列化代码而且它自带的Browsable API页面可以直接在浏览器里调试接口写前端的时候非常方便。接口设计上我遵循REST风格按资源划分为POST /api/auth/login/ # 小程序登录code换openid GET /api/categorys/ # 分类列表 GET /api/dishes/ # 菜品列表支持分类过滤 GET /api/dishes/id/ # 菜品详情 POST /api/cart/ # 添加购物车 GET /api/cart/ # 获取购物车 PUT /api/cart/id/ # 修改购物车数量 DELETE /api/cart/id/ # 删除购物车条目 POST /api/orders/ # 创建订单 GET /api/orders/ # 我的订单列表 GET /api/orders/id/ # 订单详情写视图的时候特别注意了ModelViewSet和APIView的搭配使用。购物车、订单这类需要校验业务逻辑的接口我选择用APIView手写逻辑理由是有时候业务规则比增删改查的通用流程更复杂——比如下订单时要开启事务把购物车数据搬入订单明细的同时清空购物车这一步用ModelViewSet很难优雅表达。下订单接口的核心逻辑是这个class OrderCreateView(APIView): permission_classes [IsAuthenticated] def post(self, request): user request.user # 基于购物车生成订单必须用事务 with transaction.atomic(): cart_items CartItem.objects.filter(useruser).select_related(dish) if not cart_items.exists(): return Response({detail: 购物车为空}, statusstatus.HTTP_400_BAD_REQUEST) order Order.objects.create( useruser, order_nogenerate_order_no(), statusOrder.Status.PENDING, total_amountsum([item.dish.price * item.quantity for item in cart_items]) ) for item in cart_items: OrderItem.objects.create( orderorder, dishitem.dish, priceitem.dish.price, quantityitem.quantity ) cart_items.delete() # 下单成功后清空购物车 return Response({order_no: order.order_no}, statusstatus.HTTP_201_CREATED)这里最关键的两个点是transaction.atomic()保证购物车搬移过程的原子性任何一个环节失败都不会产生半截订单sum计算总价用的Decimal类型避免浮点数精度问题。我看过不少同题的毕设下单逻辑是把整个购物车数据直接JSON序列化塞进订单的一个TextField里。这种做法前期省事后期想要按菜品维度做数据统计就非常痛苦而且清晰度远不如一对多关联表。如果目标不仅仅是“能交差”还是建议用标准的关联表设计。2.3 用户认证方案微信登录与JWT小程序端没有传统的账号密码登录流程后端认证我用了JWTJSON Web Token。方案选的是djangorestframework-simplejwt然后在它的基础上包装了一个微信登录接口整个流程是小程序端调用wx.login()获取临时code。后端向微信服务器请求https://api.weixin.qq.com/sns/jscode2session换取openid和session_key。后端在User表里按openid查用户查不到就注册新用户。用RefreshToken.for_user(user)生成JWT返回给前端。后续请求在小程序端把token放进请求头的Authorization: Bearer token。后端用IsAuthenticated校验身份。这里要注意小程序的code是临时凭证5分钟有效后端换取完openid之后不要存code也不要直接信任前端传过来的openid——正确做法是后端拿code去微信接口换不能让前端把openid直接传过来否则任何人都可以伪造身份登录。Django侧还需要把JWT认证加到REST Framework配置里REST_FRAMEWORK { DEFAULT_AUTHENTICATION_CLASSES: [ rest_framework_simplejwt.authentication.JWTAuthentication, ], DEFAULT_PERMISSION_CLASSES: [ rest_framework.permissions.IsAuthenticated, ], }需要匿名访问的接口比如菜品列表在视图上单独加permission_classes [AllowAny]覆盖。2.4 菜品图片上传与访问菜品图片这块大多数毕设直接用URL链接填进去省事但我还是建议做一个本地上传接口理由有二一是答辩演示时如果网络不稳定外部图片加载会非常慢二是图片存在本地能让你的系统看起来更完整、更像一个能独立部署的产品。Django端做上传接口也不复杂核心是配置MEDIA_URL和MEDIA_ROOT再在urls.py里加一行static路由。小程序端用wx.uploadFile上传临时路径后端接收到文件后存到media/目录返回存储的相对路径给前端拼接完整地址。不过这里有个坑Django默认的FileField存储文件名是原始文件名中文名和特殊字符可能导致小程序端加载失败。我建议重写保存逻辑统一按日期随机串重命名def dish_image_upload_path(instance, filename): ext filename.split(.)[-1] new_name f{uuid.uuid4().hex}.{ext} return fdishes/{datetime.now():%Y/%m}/{new_name}这种做法还能顺带解决目录过大的问题——按年月分目录后续排查时也方便。3. 前端核心实现微信小程序页面与交互小程序端是用户直接接触的部分也是毕设演示时长最长的部分UI的精致程度直接决定了答辩的第一印象。3.1 页面架构与底部导航设计小程序端规划了四个底部导航Tab首页点餐、购物车、订单、个人中心。对应app.json里的tabBar配置。这里有一个容易被坑的地方tabBar的图标不允许使用网络图片必须使用本地静态资源而且图标文件大小不能超过40KB。网上很多教程用的示例图标都是png格式但小程序的 tabBar 其实也支持svg不过要注意兼容性。建议直接用字体图标转png尺寸按81px设计。首页是核心业务页面布局采用左分类、右菜品的经典样式。左侧是滚动分类栏右侧是用scroll-view包裹的菜品列表。这里有一个性能优化细节如果在scroll-view里渲染几十个菜品图片快速滑动时会出现明显的白屏闪烁。解决方案是用小程序提供的image组件的懒加载属性lazy-load或者直接用wx:for配合wx:key减少重复渲染。分类切换时右侧列表要做两种处理如果只是切换分类可以用scroll-view的scroll-into-view定位到对应分类区域如果是需要整页刷新菜品则直接重新请求接口。我采用后者虽然多一次网络请求但实现更简单、逻辑更稳定。3.2 购物车数据的管理与同步策略购物车是整个前端交互逻辑最复杂的部分难点在于数据要能够在多个页面间共享同步。小程序中没有Vuex那样的全局Store所以我们要自己设计一个轻量方案。我采用的是本地缓存 接口同步的双层策略购物车数据在本地缓存的键叫cart_items数据结构是一个数组每个元素是{dish_id, title, price, image, quantity}。用户加购、减购时同步更新本地缓存同时调用后端购物车接口做持久化。请求失败时以本地缓存为准下次进入小程序时再尝试同步。这个策略的好处是用户就算断网也能把菜加到购物车体验上很流畅不过下单操作必须保证网络畅通因为后端要校验库存和价格。加购操作的逻辑如下addToCart(dish) { const cart wx.getStorageSync(cart_items) || []; const index cart.findIndex(item item.dish_id dish.id); if (index -1) { cart[index].quantity 1; } else { cart.push({ dish_id: dish.id, title: dish.title, price: dish.price, image: dish.image, quantity: 1 }); } wx.setStorageSync(cart_items, cart); this.updateCartBadge(cart); // 更新 tabBar 购物车角标 this.syncCartToServer(cart); // 异步同步到后端 }关键是updateCartBadge这个函数用wx.setTabBarBadge设置购物车角标数字让用户有明确的操作反馈。很多同学会漏掉这一步导致用户加了东西但完全看不到反馈体验很减分。关于同步策略我踩过一个坑如果用户在短时间内连续快速点击加购会出现多次重复的POST请求后端可能把这些都当成独立购物车条目而不是累加。解决办法是加一个简单的防抖——每次加购后设置一个500毫秒的定时器如果期间没有新的操作才真正把购物车同步到服务器。3.3 小程序端API请求的封装要点网络请求这块不建议在每个页面里直接写wx.request一定要封装成统一的request工具函数统一处理三件事认证头的注入、错误码的统一识别、会话过期的自动处理。// utils/request.js const BASE_URL http://127.0.0.1:8000/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { const token wx.getStorageSync(token); wx.request({ url: ${BASE_URL}${path}, method, data, header: { Content-Type: application/json, Authorization: token ? Bearer ${token} : }, success(res) { // 约定返回格式 { code, data, message } if (res.statusCode 200) { resolve(res.data); } else if (res.statusCode 401) { // token失效重新登录 wx.removeStorageSync(token); wx.navigateTo({ url: /pages/login/login }); reject(new Error(请先登录)); } else { wx.showToast({ title: res.data.message || 请求失败, icon: none }); reject(new Error(res.data.message || 请求失败)); } }, fail(err) { wx.showToast({ title: 网络异常, icon: none }); reject(err); } }); }); } module.exports { request, BASE_URL };有一个细节容易被忽略在小程序开发工具里http://127.0.0.1:8000可以正常请求但是在真机预览时这个地址指向的是手机本机而不是你的电脑。真机调试时必须在开发者工具的“详情-本地设置”里勾选“不校验合法域名”并且把BASE_URL改成你电脑的局域网IP比如http://192.168.1.100:8000。这个“电脑能跑、手机不行”的问题是每年毕设答辩现场最容易翻车的事故。3.4 提交订单与支付逻辑的简化处理真实点餐系统都有支付环节但毕设项目通常不会真的对接微信支付——因为商户号申请和个人资质限制太多很多同学根本走不完流程。我的替代方案是“货到付款”模式也就是订单创建后状态直接是“待接单”不需要真实资金流转。这个取舍在论文里是这样写的“考虑到演示环境和学生身份限制系统采用先下单后支付的设计支付模块可通过后续对接微信支付接口完成。” 这样既规避了资质问题也体现了论文的完整性。提交订单的页面从购物车进入读取缓存的购物车数据展示商品清单和总价同时让用户填一个备注比如“不要辣”“多加一份米饭”。提交按钮触发POST /api/orders/成功后清除本地购物车缓存跳转到订单列表页。整个交互链路短流程清晰演示时也非常直观。4. 联调、部署与疑难杂症排查实录校园网环境、Windows/Mac混用、Python版本不一致……毕设开发中遇到的环境问题往往比业务代码的死板 bug 更折磨人。这一部分是我最想分享给后来者的内容。4.1 从零配置 Django 开发环境Django项目的环境配置踩得最多的坑是Python版本和包版本的匹配。Python 3.12发布后很多依赖包还没有及时适配直接pip install django可能装不上最新版。我的建议是不要追求最新版本用稳定组合。毕设开发的验证过组合软件推荐版本备注Python3.10.x兼容性最好的版本Django4.2.xLTS长期支持版djangorestframework3.14.x稳定版djangorestframework-simplejwt5.2.x需和DRF匹配django-cors-headers4.x解决跨域问题创建虚拟环境是必须的一步这不是可有可无的规范而是实实在在的保命措施python -m venv venv # Windows激活 venv\Scripts\activate # macOS/Linux激活 source venv/bin/activate pip install -r requirements.txt装了django-cors-headers之后必须在settings.py里加两处配置否则小程序请求会报跨域错误。这是新手最容易漏的地方INSTALLED_APPS [ ... corsheaders, ] MIDDLEWARE [ corsheaders.middleware.CorsMiddleware, ... ] CORS_ALLOW_ALL_ORIGINS True # 开发环境直接全开开发环境无所谓的但部署上线时CORS_ALLOW_ALL_ORIGINS一定要关掉改成白名单不然别人网站可以直接调用你的接口还是有点吓人的。4.2 购物车提交订单时“总和对不上”的问题这个问题我调试了整整一个下午购物车里明明显示的是58元提交订单后后台算出的是57.8元差了两毛钱。排查之后发现原因出在数据库的DecimalField和 Pythonfloat的转换上。我在前端把价格Number(dish.price)传给了后端Django再把JSON数字转Decimal中间经过了浮点运算出现精度误差。解决方案是价格相关的参数永远不传数值而是传字符串或者后端以ID为准从数据库重新读取价格不信任前端传来的任何价格字段。我在后端下单逻辑里直接改为从购物车关联的菜品表取出价格前端传来的字段全部忽略。这个修改也符合电子商务系统的基本安全原则——客户端价格不可信。4.3 微信小程序真机预览连不上Django这个问题一句话概括就是“小程序的网络请求必须走HTTPS或者被标记为信任的域名”但这只是表面。更实际的情况是开发者在本地用python manage.py runserver 0.0.0.0:8000起服务然后手机和电脑连同一个WiFi小程序真机预览时请求局域网IP然后报错request:fail。这个问题的排查顺序应该是在电脑上浏览器访问http://127.0.0.1:8000/api/dishes确认服务正常运行。在同一网络下的手机上访问http://电脑局域网IP:8000/api/dishes确认防火墙不会阻断端口。这一步很关键Windows的防火墙经常默认阻止Python进程访问局域网。在微信开发者工具里勾选“详情 - 本地设置 - 不校验合法域名、web-view业务域名、TLS版本以及HTTPS证书”。如果还是没有请求成功打开电脑的“允许应用通过防火墙”把Python的专用网络访问权限打开。只要跑通一次真机联调后面的开发就顺了。但要提醒的是每次切换网络比如从宿舍换到教学楼WiFi电脑IP会变小程序里的BASE_URL也要跟着改。这个我在开发中遇到了至少三次后来直接写了一个配置页面来改地址不用反复改代码。4.4 常见问题速查表这段做成了一个排查清单实际开发中遇到的大部分问题都可以按图索骥现象可能原因解决方案小程序请求报request:fail域名没配白名单 / 没有关闭域名校验 / 防火墙拦截勾选“不校验合法域名”检查防火墙端口Django启动后访问 404urlpatterns路径少了一层 /ALLOWED_HOSTS没配置检查ALLOWED_HOSTS [*]检查URL路由图片上传成功后小程序显示不出来图片路径是相对路径拼接URL出错前端保存完整MEDIA_URL 文件路径购物车数量为负数前端连续点击“减少”按钮加防抖或判断quantity 1时直接删除条目订单状态一直不变商家端没有处理接单逻辑在Django Admin自带的订单模型里手动修改状态字段登录报code2Session错误 400小程序的 AppSecret 配置错误或已重置登录微信公众平台重新获取 AppSecretPython 3.12 下mysqlclient安装失败没有合适的wheel包换上PyMySQL在__init__.py里pymysql.install_as_MySQLdb()列表滑动卡顿页面渲染数据量过大 / 图片未懒加载使用lazy-load给wx:for加wx:key4.5 本地开发到部署上线的完整流程如果导师要求你答辩后把系统部署到云服务器上这个情况还挺常见的部署流程也要提前演练一遍。我的部署方案是Nginx uWSGI Django MySQL小程序端仍然通过开发者工具加载不涉及静态资源托管。部署的关键步骤# 1. 服务器安装依赖 sudo apt update sudo apt install python3-pip python3-venv nginx mysql-server pip install uwsgi # 2. 拉取代码并安装依赖 git clone 你的仓库地址 cd online-ordering/backend python3 -m venv venv source venv/bin/activate pip install -r requirements.txt # 3. 收集静态文件和数据库迁移 python manage.py collectstatic --noinput python manage.py migrate python manage.py createsuperuser # 4. 配uWSGI uwsgi --http :8000 --module config.wsgi --master --processes 4 --threads 2 # 5. 配Nginx反向代理/api和/media/转发到8000端口Nginx的配置文件核心就是反代具体来说是把静态文件和/media/路径交给Nginx直接处理/api/开头的请求转发给uWSGI。部署时还有几个坑DEBUG False之后Django不会自动处理静态文件需要collectstaticALLOWED_HOSTS必须改成你的域名或服务器IP上传的媒体文件建议单独建一个目录并做软链接否则重新部署代码时容易丢失用户上传的图片。部署完成后记得用systemctl或者supervisor守护uWSGI进程不然服务器一断开SSH整个后端服务就跟着挂了。5. 复盘这个毕设项目能扩展出什么做完这套在线点餐系统再回头看这个题目我觉得它最值得做的地方不是“完成”而是“留白”——技术栈的每一部分都有进一步深入的空间论文的每一章都能引出一个值得探讨的问题。如果还想在答辩前再拔高一点有几个方向我认为比较适合短期扩展一是把菜品推荐加进来。基于用户的历史订单计算菜品共现频率推荐逻辑不复杂但能展示你懂一点协同过滤算法的知识。二是加入商家端的Web管理界面。不用多精美只需在Vue或React里做一套菜品和订单的管理页面替换掉Django Admin的默认页面整套系统的完整性会立刻提升一个档次。注意这里要加一层角色权限不然用户也能登录管理后台乱改。三是订单状态的消息推送。使用小程序的订阅消息功能用户下单后推送“商家已接单”的消息通知。这个很实用也是小程序原生能力的一个亮点。开发这套毕设的过程中我收获最大的不是Django的某一个功能也不是小程序组件的某个API而是真正理解了一个业务系统从需求分析到架构设计再到最终落地的完整闭环。它能让你在面试的时候聊出“订单金额为什么用Decimal而不是float”“购物车数据为什么要本地缓存和服务端同步双写”“为什么下单要放在事务里”这些有深度的问题这些都比“我会用Django写接口”更有说服力。最后分享一个小技巧项目代码里一定要写满注释尤其是那些你当时琢磨了很久才想通的地方。答辩前一个月你可能还会看得懂等到答辩前一周再打开自己写的代码如果没有注释你会怀疑这是不是自己写的。本文还有配套的精品资源点击获取