简介:基于微信小程序的校园外卖平台设计与实现完整毕业论文,面向计算机相关专业毕业生及有外卖平台开发需求的学生开发者。文档紧扣校园外卖场景,系统阐述从需求分析与可行性研究、系统功能与数据库设计到基于Java SSM框架、MySQL数据库及微信开发者工具实现全过程,并详细说明管理员、用户、商家三类角色的核心功能,如用户管理、菜品信息管理、订单领取管理,以及小程序端的浏览下单、支付查阅等操作,同时涵盖系统安全性、扩展性和易用性方面的设计考量。平台采用模块化架构,后端使用SSM框架实现稳定高效的数据处理,前端依托微信小程序提供便捷交互,数据库以MySQL保障数据一致性,整体设计贴合校园外卖场景的实际运营需求。既可作为毕业设计完整范本参考,也能作为小程序外卖项目初期的需求与实现蓝本。资源包内含1个doc文档,大小约1.58MB,章节编排规范完整,包含中英文摘要、目录及全篇正文。已有98人学习下载,适合需要获取完整项目框架、借鉴技术选型与模块划分、对照撰写论文的读者。
1. 为什么“基于微信小程序的校园外卖平台”能写出一篇合格的毕业设计
校园外卖看起来是一个很小的业务,用户点单、商家出餐、骑手配送,三个角色一条流程,但把它放在微信小程序这个载体上,技术链路并不短:小程序的登录态要和后端Session打通,商品和购物车的状态要能跨页面同步,订单在“待支付、待接单、备餐中、配送中、已完成”之间的迁移要处理超时和并发,支付回调要验签防伪造。做毕业设计的同学,往往一开始以为难点在UI和动画,实际写代码后才发现,难点在数据模型和状态一致性上。这篇文章会把从页面结构、数据库设计到接口鉴权和订单状态机的完整做法拆开讲,给准备做这类课题的同学一条能走到答辩的路。这个题目的及格线不是页面有多好看,而是三条角色链路能不能在真实数据下跑通,异常情况能不能被兜住。
2. 校园外卖平台的业务边界:角色、页面与登录
2.1 用户、商家、骑手三个角色怎么进同一个小程序
校园外卖平台的业务参与方通常分三类:学生用户、商家、配送骑手。在毕设场景里,骑手不需要做成独立App,而是复用同一个小程序,通过角色权限进入不同页面。常见做法是在用户表加一个role字段,登录后由后端返回角色,小程序再根据角色切换到对应首页。这套方案的好处是一套后端同时服务三类用户,前端通过wx.reLaunch或wx.switchTab跳转到不同入口,代码工作量可控,也方便在论文里写“多角色权限控制”这个技术点。
后端接口的权限校验不能只依赖前端隐藏按钮。小程序端代码可以被抓包或反编译,任何人都可能直接调用接口。所以在登录返回role之后,每个后端接口都要先解析当前用户的角色,比如商家订单列表接口必须校验role=merchant,骑手接单接口必须校验role=rider。角色字段的作用是前端入口分流,真正拦截要在后端做。
2.2 用一张路由表冻结页面清单
项目初期的页面规划,建议直接用一张表定下来,前后端并行开发时就不容易改来改去:
| 角色 | 入口页面 | 核心页面 | 进入方式 |
|---|---|---|---|
| 学生用户 | 首页 | 商家列表、商品详情、确认订单、订单列表、个人中心 | 底部TabBar |
| 商家 | 工作台 | 菜品管理、待处理订单、订单详情、营业统计 | 登录后按role跳转 |
| 骑手 | 任务大厅 | 待接单、执行配送、订单详情、业绩记录 | 登录后按role跳转 |
在app.json里配置页面路径时,有一个容易被忽略的限制:注册在tabBar里的页面只能被wx.switchTab调用,用wx.navigateTo会报错;tabBar页面之间传参不能靠URL,只能借助getApp().globalData或wx.setStorageSync。如果一开始把订单详情页也放进tabBar,后面做“从订单列表跳到详情”就会很别扭。正确做法是tabBar只放四个一级页面,订单详情、商家详情、确认订单都作为普通页面用wx.navigateTo打开。
2.3 登录返回的字段里藏着权限边界
微信小程序登录的标准流程是:wx.login拿到code,后端用code去jscode2session接口换openid,然后签发自定义token。登录接口的返回结构建议统一为:
{ "code": 0, "message": "ok", "data": { "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.xxx", "userInfo": { "uid": 10086, "nickname": "2024级同学", "avatar": "https://thirdwx.qlogo.cn/xxx", "role": "student" } } }小程序端把token存入本地缓存,之后每个wx.request都在请求头带Authorization。role字段决定前端跳转到学生首页还是商家工作台。要特别说明的是,这个role需要由后端根据管理后台的配置返回,而不能让用户自己传角色。假如登录接口接受前端传role=admin,那整个权限体系就形同虚设。
3. 校园外卖数据库怎么建:订单表、菜品快照与支付单
3.1 核心表拆分与字段说明
数据库是这个项目的底盘。使用MySQL时,核心表一般至少有用户表、商家表、菜品表、订单表、订单明细表、购物车表、支付表。用户表里区分三种角色,商家表单独存店铺信息,菜品表挂在商家下。以订单相关为例,下面是一组可以直接参考的建表语句:
CREATE TABLE `sys_user` ( `id` bigint NOT NULL AUTO_INCREMENT, `openid` varchar(64) DEFAULT NULL, `phone` varchar(20) DEFAULT NULL, `role` varchar(16) NOT NULL DEFAULT 'student', `nickname` varchar(64) DEFAULT NULL, `status` tinyint NOT NULL DEFAULT 1, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`openid`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_shop` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `user_id` bigint NOT NULL, `merchant_id` bigint NOT NULL, `rider_id` bigint DEFAULT NULL, `total_amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL COMMENT '10待支付 20待接单 30备餐中 40配送中 50已完成 60已取消 70退款中', `receiver_name` varchar(32) NOT NULL, `receiver_phone` varchar(20) NOT NULL, `receiver_address` varchar(255) NOT NULL, `create_time` datetime DEFAULT CURRENT_TIMESTAMP, `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_id` bigint NOT NULL, `dish_id` bigint NOT NULL, `dish_name` varchar(128) NOT NULL, `price` decimal(10,2) NOT NULL, `quantity` int NOT NULL, PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;表结构里有几个点需要展开说明。order_no是业务单号,用于前端展示和对账,必须加唯一索引;主键id只做存储和关联,不暴露给前端。order_shop.status用tinyint加注释,比直接存字符串可读性更高,也方便以后加状态。receiver_name、receiver_phone、receiver_address这三个字段是冗余设计,哪怕学生用户修改了默认地址,历史订单里的收货信息也不会被改变。
3.2 订单明细为什么必须做菜品快照
order_item表里存了dish_name和price,这也是外卖平台里很常见的设计。菜品可能改价、改名、下架,如果订单明细只存dish_id,将来查询历史订单时,就不得不去当前菜品表里取名称和价格,一旦菜品被删除,订单详情页就会显示空数据。更麻烦的是商家改价后,旧订单的金额记录也会被“篡改”。所以下单那一刻,后端必须把菜品名称、单价、数量完整地写入订单明细表。
提交订单时的处理顺序是:先校验购物车里每个菜品是否属于当前商家,再重新查询价格并计算总金额,最后写入订单和明细。前端传过来的总价只能作为展示参考,不能作为落库依据。价格在后端重算,配合数据库事务,才能保证订单金额可信。这部分的校验逻辑在论文里可以单独写成“金额可信度”小节,评委会比较看重。
3.3 订单号、幂等与库存约束
每天在同一家店点两次同一份饭,系统里应该出现两条订单,而不是因为用户手滑点了两次按钮就生成两条一模一样的订单。解决重复下单的方式是让前端在提交按钮按下后进入加载状态,同时后端用order_no做幂等控制。但按钮置灰只能减少误操作,真正可靠的做法是预下单:用户确认订单时,后端先生成一条状态为待支付的订单并返回order_no,前端拿到订单号后再调支付或模拟支付。支付接口要求传入order_no,如果用户重复提交支付,后端发现订单已经是待接单,直接拒绝重复入账。
库存扣减也是同一个思路。菜品表里要有stock字段,扣减时不能“先查再改”,而是执行一行条件更新:
UPDATE dish SET stock = stock - 1 WHERE id = #{dishId} AND stock > 0stock > 0这个条件配合受影响行数判断,可以在并发场景下避免超卖。如果更新影响行数为0,说明库存不足,整个下单事务回滚。
4. 微信小程序端怎么接后端:登录、请求封装、购物车与支付
4.1 用wx.login换后端token的最小流程
小程序端登录的代码可以封装成一个Promise,方便后续所有页面调用:
function loginAndGetToken() { return new Promise((resolve, reject) => { wx.login({ success: (res) => { const code = res.code wx.request({ url: `${getApp().globalData.baseUrl}/api/auth/login`, method: 'POST', data: { code }, success: (resp) => { if (resp.data.code === 0) { wx.setStorageSync('token', resp.data.data.token) wx.setStorageSync('userInfo', resp.data.data.userInfo) resolve(resp.data.data) } else { reject(resp.data) } }, fail: reject }) }, fail: reject }) }) }后端收到这个code后,再去调用微信的jscode2session接口,拿openid查用户、建用户、发token。需要注意两点:code只能用一次且有效期很短,如果后端频繁报40029,优先检查小程序端是否在多个地方重复使用同一个code;AppSecret只能保存在后端,不能写进小程序代码,否则只要有人抓包就能拿到密钥。
4.2 统一请求封装,把token过期拦在入口
每个页面都写一遍wx.request很痛苦,更痛苦的是token过期时每个页面都要重复处理。我一般在utils/request.js里封装一个方法:
function request(path, method, data) { return new Promise((resolve, reject) => { wx.request({ url: `${getApp().globalData.baseUrl}${path}`, method: method || 'GET', data: data || {}, header: { 'Authorization': wx.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode < 200 || res.statusCode >= 300) { reject(res) return } if (res.data.code === 0) { resolve(res.data.data) } else if (res.data.code === 401) { wx.removeStorageSync('token') // 重新走登录流程,登录成功后提示用户重试 loginAndGetToken().then(() => { wx.showToast({ title: '登录已过期,请重试', icon: 'none' }) }) reject(res.data) } else { wx.showToast({ title: res.data.message || '请求失败', icon: 'none' }) reject(res.data) } }, fail: reject }) }) }这里把接口返回格式统一为code、message、data,业务上是否成功只看code,HTTP状态码只表示网络请求有没有通。401安排在业务码里而不是HTTP状态码里,可以避免微信开发者工具底层的状态码处理差异。拦截401后先清掉旧token再重新登录,是为了防止用户下一步操作又带着过期token继续报错。
4.3 购物车放本地没错,但提交时必须后端重算
购物车可以放在小程序本地缓存,也可以放在后端。从毕设工作量来看,本地购物车实现更简单,但会带来两个问题:不同商家之间的购物车需要按merchantId分组;商品价格或库存变化后,本地购物车展示的数据可能是过期的。解决办法是在进入购物车页或提交订单前,调用一个后端校验接口,把本地购物车里的dish_id列表发给后端,由后端确认价格、库存、上下架状态,然后返回最新的购物车结算数据。
提交订单时,前端提交的应该是“后端校验过的数据”,而不是直接把本地购物车原样提交。严格一点的做法是:前端提交订单时只提交购物车车型列表,后端在事务里重新查询商品价格,再算出订单金额并落库。这样即使有人修改小程序提交参数,把菜品价格改成1分钱,后端也不会接受。
4.4 支付流程:不要在前端直接判断成功
毕设如果不接真实微信支付,可以做一个/api/pay/mock接口来模拟支付成功,后端在接口内直接更新订单状态和支付记录。如果接真实微信支付,标准流程是后端先创建订单,再调用微信支付统一下单接口,拿到timeStamp、nonceStr、package等参数返回给前端,前端再调wx.requestPayment拉起收银台。
这里的坑在于支付结果不要以前端回调为准。用户在收银台点完支付,微信返回的success只能说明“拉起收银台成功”,不能代表钱一定到账。正确做法是后端接收微信支付回调,验签、核对金额、更新支付单和订单状态;小程序端在支付完成后轮询订单详情,直到后端确认订单进入“待接单”,页面才展示支付成功。这个设计在答辩时很容易讲清楚,也能引出“回调幂等”的话题。
5. 订单状态机:超时、防超卖与支付回调节点
5.1 状态定义与迁移约束
订单状态不要散落在业务代码里到处用if-else,建议先定义一张状态迁移表:
| 当前状态 | 触发动作 | 目标状态 | 发起方 |
|---|---|---|---|
| 待支付 | 支付成功 | 待接单 | 支付回调 |
| 待支付 | 用户取消 | 已取消 | 用户 |
| 待支付 | 超时未付 | 已取消 | 定时任务 |
| 待接单 | 商家接单 | 备餐中 | 商家 |
| 待接单 | 商家拒单 | 已取消 | 商家 |
| 备餐中 | 商家出餐 | 配送中 | 商家 |
| 配送中 | 骑手送达 | 已完成 | 骑手 |
| 配送中 | 骑手异常上报 | 退款中 | 骑手 |
状态更新时,建议用“带旧状态条件”的更新语句:
# 订单状态更新,必须带旧状态条件,防止并发覆盖 updated = Order.query.filter_by( order_no=order_no, status=current_status ).update({ "status": target_status, "update_time": datetime.now() }) db.session.commit() if updated == 0: raise BizError("订单状态已变化,请刷新后重试")如果先查询订单再直接赋值保存,两个请求同时操作同一订单时,后一个请求会把前一个请求的状态覆盖掉。提交订单、支付回调、骑手接单都走这个条件更新,状态机就不会被并发请求打乱。
5.2 超时未支付怎么关单
待支付订单超过15分钟仍未支付,需要自动关闭。毕设项目不引入消息队列也能做到,常见做法是定时任务或者惰性关闭。惰性关闭指的是用户查询订单详情时判断是否超时,但数据统计会不准确;更推荐加一个每分钟执行一次的定时任务,扫描待支付订单。
from apscheduler.schedulers.blocking import BlockingScheduler from datetime import datetime, timedelta def close_expired_orders(): expire_time = datetime.now() - timedelta(minutes=15) rows = Order.query.filter( Order.status == PAY_WAIT, Order.create_time < expire_time ).all() for row in rows: row.status = ORDER_CANCELED row.cancel_reason = "支付超时自动关闭" # 释放库存,需要遍历订单明细逐个补回 release_stock(row) db.session.commit() scheduler = BlockingScheduler() scheduler.add_job(close_expired_orders, 'interval', minutes=1) scheduler.start()这段代码在单实例部署时没有问题。定时任务扫描条件里已经限定了status=待支付,不会把已支付订单关掉。库存回补的逻辑要放在同一个事务里,避免订单关闭成功但库存没加回来。
5.3 库存扣减用一条SQL解决超卖
下单时扣减库存,取消时回补库存,扣减操作必须使用原子SQL:
UPDATE dish SET stock = stock - 1 WHERE id = #{dishId} AND stock > 0这一步在写入订单明细之前执行,如果库存不足,更新影响行数为0,订单创建失败。把“校验库存”和“扣库存”合并成一条SQL,是避免超卖最简单可靠的方式。如果改成先查询stock,再在代码里判断是否大于0,最后更新stock = stock - 1,那么两个并发请求可能同时读到stock=1,然后都执行更新,最终库存变成-1。
5.4 支付回调到达时,先落支付单再改订单
支付回调是订单流程里最容易出问题的环节。微信支付或模拟支付成功时,回调可能因为网络原因重复推送,所以接口必须做幂等。处理顺序建议先写支付单,再更新订单状态:
- 先校验签名、确认金额,金额对不上直接返回失败;
- 用
transaction_id或pay_no作为唯一键插入支付记录,重复插入会走唯一索引报错; - 插入成功说明这是第一次回调,立即更新订单状态为待接单;
- 插入失败说明是重复回调,直接返回成功。
支付记录表要建独立唯一索引:
CREATE TABLE `order_pay` ( `id` bigint NOT NULL AUTO_INCREMENT, `order_no` varchar(32) NOT NULL, `pay_no` varchar(64) NOT NULL, `pay_type` varchar(16) NOT NULL, `amount` decimal(10,2) NOT NULL, `status` tinyint NOT NULL COMMENT '10待支付 20已支付 30已退款', `transaction_id` varchar(64) DEFAULT NULL, `pay_time` datetime DEFAULT NULL, PRIMARY KEY (`id`), UNIQUE KEY `uk_pay_no` (`pay_no`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;pay_no是支付平台生成的业务号,不能复用。即使某笔支付失败,下次发起支付也要生成新的pay_no,因为旧单号可能已经在某个中间状态被记录过。
6. 答辩前的验收表与两个反常规演示
6.1 准备一张功能验收表
论文定稿前,建议把关键功能跑一遍并记录结果,表格放在论文附录里很有说服力:
| 功能点 | 验证方式 | 预期结果 |
|---|---|---|
| 微信登录 | 测试号登录小程序 | 获取token并正确返回角色 |
| 角色路由 | 学生、商家、骑手三个账号登录 | 进入各自首页,不能访问他端页面 |
| 下单与库存 | 库存剩1份,同时下两单 | 一个订单成功,另一个提示库存不足 |
| 模拟支付 | 调用mock支付接口 | 订单状态从待支付变为待接单 |
| 取消订单 | 待支付、待接单状态下取消 | 状态变为已取消,库存回补 |
| 超时关单 | 修改订单创建时间后等待定时任务 | 待支付订单自动关闭 |
验收表里的“并发防超卖”一定要预先准备脚本或者用两个微信账号同时下单,答辩时能现场复现最好。如果现场网络不稳,至少要把录屏留好,直接播放给评委看。
6.2 论文里值得单独成章的三个点
一篇合格的毕业论文不需要把每个页面都写一遍,但下面三点值得单独成章:订单状态机设计,包括状态定义、迁移条件和约束;库存的原子扣减,解释为什么用条件更新而不是先查再改;支付回调的幂等设计,说明如何用唯一约束防止重复入账。这三个点分别对应业务逻辑、并发安全和支付一致性,是评委会深挖的高频题目。
6.3 演示时不要只走顺利路径
演示时故意展示两个“反常规”动作,效果比一路畅通好得多。第一个动作是反复点击提交订单按钮,演示后端生成的订单只有一条,用order_no唯一索引兜住了重复提交;第二个动作是把某个菜品库存修改为0,演示下单失败时页面给出的库存不足提示。这两个演示直接回应用了“并发”“异常”“幂等”这些关键词,比介绍页面更让评委记住你的系统。演示前记得在真机上完整跑一遍“登录到支付到配送完成”的链路,并把录屏保存,因为在答辩现场最容易出现的翻车点是手机连不上后端服务器,而开发者工具里一切正常。
本文还有配套的精品资源,点击获取