每年到毕业设计季,总有一批人被选题折腾得睡不着觉。又要好过审、又要能做出东西、又要有一定技术含量——说实话,"基于微信小程序的小饭桌服务平台"这个选题,我是越看越觉得适合拿来当模板聊一聊。它不夸张,但足够把微信小程序前端、SpringBoot后端、权限控制、订单状态流转这些基本功全部串起来,而且业务场景非常接地气:家长给孩子订午餐、上班族订成人套餐,属于典型的生活服务类平台项目,评委一听就知道你要做什么,不用费劲解释。
这篇文章我不会只贴代码,而是把选题思路、架构设计、核心数据模型、订单状态机、微信登录、测试支付、部署调试、答辩准备这些环节全部过一遍。无论你是准备拿这个题目自己做,还是想从同类项目中举一反三,应该都能少走很多弯路。
1. 为什么"小饭桌服务"是毕设里的性价比之选
1.1 从毕设评分角度拆解选题价值
很多人选毕设题目只关注"技术新不新",忽略了评分逻辑。一个能拿高分的毕设系统,通常要满足三个条件:业务完整、技术覆盖度够、演示起来直观。小饭桌服务恰好三点都占。
业务完整性上,它不是"增删改查"四个字能糊弄过去的。从用户注册、家庭信息管理、选餐下单,到商家接单、出餐状态推送、订单完成后的评价,再到管理后台的商家入驻审核和数据统计,整条链路是闭环的。这意味着你的功能模块清单可以写得很长,工作量在文档里一眼就能看出来。
技术覆盖度上,微信小程序登录、SpringBoot接口、Redis缓存、文件上传、WebSocket消息推送、二维码生成,这些全是常见考点。随便切入一个点,都能往深处问。比那些纯前端静态页面或者单一后台管理的题目,答辩时的深度完全不一样。
还有一个容易被忽略的点:小饭桌服务涉及两类用户——订餐的人(家长/成人用户)和提供服务的人(小饭桌商家),外加一个平台管理方。三端角色天然要求你设计权限模型,这比其他单角色系统高一个档次。
1.2 中小学与成人场景的差异化设计
标题里同时提到了"中小学小饭桌"和"成人小饭桌",很多人以为这是两个项目。其实放在同一套系统里,只需要在业务上做区分即可。
中小学场景核心是托管属性:家长关心菜品是否营养、孩子是否吃饱、有没有过敏原;价格敏感,通常是按月预订,按周结算。成人小饭桌场景更接近外卖:用户关心出餐速度、配送方式和单份价格,通常是当日预订、当餐配送。数据模型上不需要大改,但要在订单表里加上"订单类型"字段,在菜品表里加上"适合人群"标签,在套餐表里加上"预订周期"概念。
我在设计时会刻意保留这两个场景的差异,一方面是为了切题,另一方面是给答辩留扩展点:如果你被问到"这个系统能不能从孩子订餐扩展到成人订餐",你就可以直接讲场景差异和字段扩展方案,这比临时瞎编要从容得多。
2. 系统架构与数据模型:先把地基打牢
2.1 前后端分离的总体结构
技术选型上我建议保持最稳的组合:小程序原生前端 + SpringBoot + MyBatis-Plus + MySQL,再加Redis做缓存和临时状态存储。不要为了炫技引入微服务、分布式事务这类东西,毕设的评审时间有限,架构清晰比架构复杂更重要。
小程序端按角色分包管理:
- 用户端:首页、菜品列表、购物车、订单列表、订单详情、个人中心、孩子管理、评价页
- 商家端:菜品管理、订单管理、接单/出餐操作、今日统计
- 管理后台:用户管理、商家审核、菜单审核、平台统计报表
后端按业务域分包,建议是controller、service、mapper、entity、common、config六层结构。common里放统一返回结果封装、异常处理、JWT工具类、拦截器,config里放RedisConfig、WebMvcConfig、MybatisPlusConfig。干干净净,答辩被问代码结构时你心里有底气。
2.2 核心数据表的字段设计思路
数据库是毕设的评分重头,表设计是否合理,直接决定后面开发顺不顺畅。我按业务模块列一下核心表,以及关键字段的踩坑提醒。
用户表(user)除了常规的openid、昵称、头像外,一定要加一个role字段(0用户、1商家、2管理员),登录后所有权限判断都靠它。建议再加一个status字段控制是否被禁用,这在管理后台能用上。
孩子信息表(child)字段包括姓名、性别、学校、年级、班级、过敏原备注。过敏原这个字段是中小学小饭桌场景的加分项,家长下单时系统自动提醒商家,体现业务思考。
菜品表(dish)里除了名称、图片、价格、描述,还要有meal_type(早餐/午餐/晚餐)、diet_type(普通/儿童/成人)、stock(当日库存)、status(上架/下架)——注意库存不要直接减到负数,下单时要加库存校验。
套餐表(meal_package)支持"周一到周五午餐套餐"这种固定周期售卖,这是小饭桌和普通外卖的区别。套餐需要包含多个菜品,所以还要有套餐明细表(package_item)。
订单表(order)是核心中的核心,下面单独讲。
2.3 订单表与状态机:整个项目最该花时间的地方
订单表关键字段我建议这么设计:
| 字段 | 类型 | 说明 |
|---|---|---|
| order_no | varchar | 业务订单号,展示给用户,不能直接用数据库自增id |
| user_id | bigint | 下单用户 |
| child_id | bigint | 关联孩子,成人订单为空 |
| merchant_id | bigint | 小饭桌商家 |
| order_type | tinyint | 0学生餐 1成人餐 |
| total_amount | decimal | 订单总金额 |
| status | tinyint | 订单状态 |
| pickup_time | varchar | 取餐/配送时间段 |
| remark | varchar | 备注,如少辣、过敏提醒 |
订单状态我建议用枚举常量,别存字符串散着写。状态流转如下:
待支付 -> 已支付/待接单 -> 已接单 -> 出餐中 -> 待取餐/配送中 -> 已完成 -> 已取消一个常见的低级错误是取消订单直接从任何状态都能取消。正确的做法是只有"待支付"和"已完成之前"允许用户取消,已接单状态需要商家确认后才能取消。这块逻辑用状态机写清楚,答辩时你有东西讲。
// 订单状态枚举 public enum OrderStatus { UNPAID(0, "待支付"), PAID(1, "已支付待接单"), ACCEPTED(2, "已接单"), COOKING(3, "出餐中"), READY(4, "待取餐"), DELIVERING(5, "配送中"), COMPLETED(6, "已完成"), CANCELLED(7, "已取消"); private final int code; private final String desc; }在service层写一个统一的transition方法,任何状态变更都走这个方法,里面校验前状态是否合法,这样后面做申请退款、超时取消时才不会乱成一团。
3. 用户端的核心流程:点餐、下单、状态感知
3.1 点餐与购物车设计:要不要给家长一个"临时存储"
小饭桌点餐场景和普通外卖有个明显区别:很多家长是提前一周给孩子订下周的午餐,因此购物车不是必须的,但我仍然建议做。为什么?因为用户在前台的动线通常是"浏览 -> 犹豫 -> 加入购物车 -> 多次对比 -> 下单",没有购物车会让决策成本变高,产品功能也不完整。
购物车数据用Redis的Hash结构存储,key设计成cart:{userId},field是dishId,value是数量。这样每次增删改都走内存,小程序端展示时实时回源查菜品价格和库存,避免下单时才发现"价格变了"。
为什么不直接把购物车存MySQL?因为购物车本来就是临时数据,生命周期短,写表会频繁产生无效记录。Redis恰好支持过期时间,用户一周没访问,购物车自然清理,不占数据库空间。这个选型理由可以在文档里写,是个加分点。
3.2 下单事务与库存扣减:并发下不能让库存变负数
小饭桌的菜品库存通常不大,但预订高峰集中,比如周一早上很多家长同时下单。经典场景是两个家长同时点最后一份红烧鸡腿,如果代码只做"查询库存>0 -> 扣减",并发下就会超卖。
解决办法是使用MySQL的乐观锁。菜品表加一个version字段,在扣库存的SQL里直接带条件更新:
update dish set stock = stock - 1, version = version + 1 where id = #{dishId} and stock > 0 and version = #{oldVersion}如果更新影响行数为0,说明库存不足或数据被并发修改,直接抛业务异常提示"菜品库存不足"。下单接口整体加@Transactional,订单表和订单明细表、菜品库存、销售统计都放在一个事务里。这是面试也常问的知识点,答得出来很加分。
下单以后有一个小细节:鸡汤文档里很少写,那就是"待支付"状态下的订单要设置过期时间。我建议把订单创建时间写入Redis,key为order:unpaid:{orderNo},设置15分钟过期,并通过Spring的定时任务每分钟扫描一次,把超时未支付订单置为已取消,同时回补库存。没有这一步,你点"取消订单"时发现库存已经被扣掉了,用户端会看到大量幽灵库存,体验很差。
3.3 孩子信息管理的权限边界
中小学小饭桌的订单是"家长下单、孩子吃饭",所以孩子信息管理不是简单的增删改查。一个家长的账户下可以添加多个孩子,但商家出餐时看到的是孩子姓名和学校班级,不能看到家长手机号以外的隐私信息。反过来,家长端只能管理自己名下孩子的订单,不能看到其他家长给孩子订了什么。
实现上很简单,所有child表的查询都带上user_id条件,杜绝横向越权。这也是答辩时老师会故意问的:"我把请求里的child_id改成别人的,能不能查到别人的信息?"你要明确回答不能,因为我们所有SQL都是基于当前登录用户id去过滤的。这个细节我给你打包票,至少值5分的印象分。
4. 商家端与管理端:从"能跑"到"像样"的分水岭
4.1 商家接单与出餐通知怎么做
商家端的业务核心是订单处理。我的建议是商家端小程序里订单列表按状态分Tab展示:新订单、待出餐、待取餐、已完成。商家对新订单点"接单"后,订单状态从PAID变为ACCEPTED;点"出餐完成"后变为READY,这时需要通知家长来取餐。
通知这块有两个方案。简单做法是家长端小程序在订单详情页轮询订单状态,比如每15秒请求一次,缺点是慢且有延迟。更好的做法是商家端接单/出餐时,通过微信公众平台的订阅消息给家长推送一条模板消息。注意订阅消息是一次性的,用户下单时必须主动触发一次订阅授权,否则商家出餐时你没授权,消息就发不出去。这个坑非常典型,很多毕设做到最后才发现订阅消息发不出来。
我实际开发时会做一个双保险:商家操作时先发订阅消息,同时在订单列表页写一个下拉刷新的状态回查。这样即使订阅消息因为用户取消授权失败,家长下拉刷新也能看到最新状态。答辩时可以把这个降级策略讲出来,属于真实项目才会遇到的问题。
4.2 菜品管理与套餐业务的隐藏难点
商家端管理菜品有个验收标准:不要用multipart文件上传后直接存本地磁盘完事。因为微信小程序上线要求域名HTTPS,图片跨域访问和磁盘路径暴露都会出问题。我建议用云存储或配置一个虚拟路径映射,至少要写成可配置的访问URL前缀。
套餐业务是另一个隐藏难点。一个"儿童营养周套餐"包含周一至周五每天的午餐配置,内部其实是五组菜品明细,每组又有主食和菜品组合。因此套餐新建页面需要做成动态行编辑,前端用列表渲染数组,后端接收JSON数组后批量插入套餐明细表。展示的时候,按星期几逐天展示,而不是一个简单的菜品列表。这块做出来,文档的截图会非常好看,也显得你思考过业务。
4.3 管理后台:审核、统计与图表选型
管理后台我用的是Vue或纯后端的模板页面都可以。小饭桌平台的管理员核心操作是:审核商家入驻资质、审核菜品上下架、管理用户禁用、订单异常处理。
统计报表建议不要只写接口返回JSON,而是用ECharts画几个关键图表:近7日订单量折线图、菜品销量排行饼图、商家订单量柱状图。这些图表一旦出现在演示环节,视觉冲击力直接拉满,评委很少会为难一个系统里塞了图表的人。
统计查询不用做大宽表,按日聚合的订单表在数据量不大时直接SQL的GROUP BY就够。如果想让文档更好看,可以在service层写一个定时任务,每天凌晨统计前一天的订单数据写入报表汇总表,第二天页面查汇总表即可。设计上从"实时统计"变为"T+1离线统计",也更贴近真实业务。
5. 微信登录与支付:毕设最容易卡壳的两个环节
5.1 微信小程序登录完整链路
小程序登录流程看起来简单,实际上很多人第一步就被绕晕。完整链路是这样的:小程序端调用wx.login拿到临时code,把code发给后端,后端拿着code去微信接口服务交换openid和session_key,然后用openid作为用户唯一标识,如果数据库没有就自动注册,最后签发一个自己的token返回给前端。
// 简化版登录逻辑 @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { // 1. code换openid String url = "https://api.weixin.qq.com/sns/jscode2session?appid=" + appId + "&secret=" + appSecret + "&js_code=" + dto.getCode() + "&grant_type=authorization_code"; String result = restTemplate.getForObject(url, String.class); JSONObject obj = JSON.parseObject(result); String openid = obj.getString("openid"); // 2. 查库,不存在就创建用户 User user = userMapper.selectOne( new LambdaQueryWrapper<User>().eq(User::getOpenid, openid)); if (user == null) { user = User.builder().openid(openid) .nickname("微信用户").role(0).status(1).build(); userMapper.insert(user); } // 3. 签发JWT,返回给前端 String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.success(token); }几个容易出错的地方:第一,code只能用一次,而且有效期很短,前端拿到的code要马上传给后端,不能存起来。第二,appSecret绝不能放在小程序前端代码里,必须只保存在后端。第三,jscode2session接口返回的session_key不要在日志里打印,它涉及解密手机号等敏感能力,出安全问题会被面试官直接扣分。
登录之后的鉴权统一用拦截器解决。写一个JwtInterceptor,继承HandlerInterceptor,在preHandle里从请求头取Authorization,解析token,把用户id和角色放入ThreadLocal。拦截器注册时注意排除登录接口、健康检查接口、图片静态资源路径。
5.2 支付与退款:没有商户号怎么办
个人做毕设大概率没有微信支付商户号,所以常见的做法是使用微信支付沙箱环境,或者用模拟支付。模拟支付不是让你不做支付功能,而是把支付动作拆成两层:前端调起支付 -> 后端创建预支付单 -> 模拟回调通知支付成功。
我见过很多优秀毕设都采用"模拟支付按钮"的方式,在支付确认弹窗里写清楚"测试环境不会产生真实扣款",点击后后端直接生成支付成功回调,订单状态流转到已支付。关键在于,数据库里payment表还是要建,支付流水还是要记录,回调幂等处理还是要有。
// 模拟微信支付回调 @PostMapping("/mock/pay/notify") public Result mockNotify(@RequestBody PayNotifyDTO dto) { String orderNo = dto.getOrderNo(); // 幂等:如果该订单支付流水已存在直接返回成功 Payment payment = paymentMapper.selectByOrderNo(orderNo); if (payment != null && payment.getStatus() == 1) { return Result.success(); } // 记录支付流水 // 更新订单状态为 PAID // 记录订单操作日志 return Result.success(); }如果你确实有商户号,前端wx.requestPayment需要的参数是后端统一下单返回的五个字段,调用顺序也不要搞反:先小程序端调后端创建订单,后端再调微信统一下单,然后把参数返回前端。退款则是调用退款接口,注意证书配置。
6. 调试部署过程中的高频坑与排查思路
6.1 跨域、Token过期与拦截器顺序
本地开发小程序不配置合法域名时,必须在开发者工具里勾选"不校验合法域名"。我见过不少同学明明后端写了跨域配置,小程序请求还是报错,折腾半天才发现是开发工具设置的问题。
SpringBoot的跨域配置类型要匹配:如果是WebMvcConfigurer实现方式,注意重写addCorsMappings方法,allowedOriginPatterns要写"*",allowedMethods要写上GET,POST,PUT,DELETE,OPTIONS。特别是OPTIONS预检请求,很多拦截器没放行,前端看到"跨域请求失败"其实是拦截器先抛了错,和CORS没关系。
Token过期时间建议设置2小时,小程序端在响应拦截器里判断返回码为401时自动跳转到登录页重新执行wx.login。同时后端不要直接删用户的token——JWT是无状态的,过期时间到了自然失效,Redis里存一份黑名单用于用户主动退出登录即可。
6.2 图片上传与本地存储的坑
商家端上传菜品图片,最稳妥的方案是传到云存储桶,拿到URL存数据库。如果没有云环境,用本地存储要注意两点:
第一,SpringBoot默认静态资源路径只有classpath:/static,如果你把文件传到项目外的磁盘目录,必须自己实现WebMvcConfigurer的addResourceHandlers配置虚拟路径映射,否则前端URL直接404。第二,传到本地的文件,如果项目重启时清空了临时目录,图片就全丢了。所以要在配置里写明上传路径,不要用相对路径。
# application.properties upload.path=/data/upload/ upload.url.prefix=/upload/对应Java侧配置:
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/upload/**") .addResourceHandler("file:" + uploadPath); }6.3 真机预览与体验版的发布注意点
开发工具里一切正常,一上真机就白屏,这种问题我处理过很多次。大概率是你用了ES6新语法而基础库版本过低,或者域名没有配置到小程序后台的request合法域名里。真机调试时wx.request要求域名必须是HTTPS且已经备案,线上体验版没有"跳过域名校验"这个选项。
另外要注意小程序的包体积限制。你的项目要是把Vant组件库整个引进来再加上图片资源本地化,主包轻易就超2MB了。解决方法是:图片全部走网络URL,组件库按需引入,业务页面按角色分包。首页放在主包,商家端和管理端页面放到分包,大图片全用CDN。我见过一个同学因为包体积超限,临时把大量图片从项目里删掉,结果页面惨不忍睹——这种东西提前一周就要检查。
6.4 数据库连接与Redis未启动导致的启动失败
SpringBoot项目启动时报连接MySQL失败或者Redis连接被拒绝,通常是环境没对齐。毕设演示的时候最容易翻车的是:你的电脑没开Redis服务,结果登录接口一调就报错。建议把Redis配置成Windows服务开机自启,或者干脆写一个启动脚本,双击就把MySQL、Redis、后端jar包一起拉起来。
MySQL建议在文档里附带完整的数据表初始化SQL脚本,并且数据里要插入演示账号、一个测试商家、十几道菜品和几个模拟订单。评委点开页面时有现成数据可看,比看到空列表要舒服得多,这个细节一定要做。
7. 拿到源码之后,距离"高分毕设"还差这几步
7.1 源码不是终点:二开一个"你自己的"功能点
现在网上很多项目资源都附带源码和文档,拿到手之后千万不要直接交。一个班可能有五个人都在用同一套源码,老师一眼就能看出来。更关键的是,答辩时老师会问你某个功能怎么实现的,如果你连代码都没读过一遍,当场就会穿帮。
我的建议是,在原有项目上做一个"二开小功能",既不用重写全部代码,又能体现独立工作量。比如给商家端加一个"每周食谱预览"功能,按星期展示一周菜品并支持一键生成下周菜单;或者给管理后台加一个"商家评分排行榜",按月汇总商家评分自动生成排名。这种功能代码量不大,但数据表设计、前后端联调、图表展示全都涉及,讲出来就是一个完整的故事。
7.2 注意事项:数据库脚本、部署文档一定要自己跑一遍
拿到项目压包后,第一步不是看代码,而是把数据库脚本在本地执行一遍,然后按部署文档把项目跑起来。这一步能暴露大量问题:脚本版本不兼容、MySQL编码不是utf8mb4导致中文乱码、Redis版本过高不兼容、JDK版本和项目不匹配。提前跑一遍,所有环境问题都能在现场演示前解决,而不是在答辩时眼睁睁看着系统起不来。
部署文档需要涵盖:JDK、Maven、MySQL、Redis的安装与版本要求,数据库导入步骤,后端yml配置修改点,小程序AppID替换方式。如果项目文档里没有这些,你自己补一份,后面写论文也能直接用上。
7.3 答辩演示的话术准备:按"业务线"而不是按"模块"讲
演示系统时,大部分人习惯逐个模块讲:这是用户管理、这是订单管理、这是菜品管理——评委听着就想快进。换个思路,按业务线讲故事会好很多:模拟一个家长打开小程序浏览今日菜单,加入购物车,选择孩子信息并下单,15分钟后模拟商家端接单出餐,家长收到订阅消息通知,点击订单状态查看,最后在管理后台看到当日的订单统计。
这条线走下来,前端、后端、数据库、消息通知、状态流转全部覆盖到了。讲的过程中穿插设计取舍,比如"这里我为什么用Redis存购物车,因为临时数据生命周期短"或者"状态流转这里我设计了合法性校验,防止非法操作",比干念功能更有说服力。
我在实际带项目时最深的体会是,毕设做得好不好,不看你用了多炫的技术,而看你能不能把业务逻辑讲闭环。小饭桌平台的优势就在于,业务足够常见,每个角色都能在你做的系统里找到自己的操作路径。只要把数据模型设计清楚、把状态流转梳理明白、把三个角色协作的流程跑通,再加上你自己的一个小功能点,这套项目拿个不错的成绩是稳的。
最后提醒一点,如果你选择了这类带源码资源和文档的项目,一定要亲手把代码敲一遍,把每个接口的入参出参弄明白。答辩时老师最喜欢问的一个问题就是:"如果现在需求变成小饭桌商家支持配送上门,你需要在哪些表增加字段,订单状态要不要调整?"想清楚这个问题,你就真的把这个项目吃透了。