简介:这是一套面向高校计算机相关专业学生与Java初学者、用于毕业设计或课程设计实战的微信点餐系统完整项目包,采用微信小程序前端搭配Java后端的技术路线,可帮助读者解决从选题到落地实现的全流程参考需求。压缩包共1286个文件,约13.72MB,涵盖小程序端的wxml、wxss、js与vue页面文件,后端123个java源码、sql数据库脚本,以及json配置、png与svg图片素材、说明文档docx等,前后端与数据库结构相对完整。目前已有570人学习下载,适合需要完整赛题方案、可运行源码与数据库脚本的读者参考。项目包含点餐业务相关页面、后端接口与建表脚本,并附有说明文档,便于对照理解小程序与Java服务端的交互逻辑、目录组织方式及部署思路,可作为二次开发或答辩演示的基础素材。
1. 从一份点餐系统毕设包说起:微信小程序+Java后端到底能跑出什么
每年毕业季,计算机专业的学生都会面对同一个问题:选一个能写进简历、又能真正跑起来的项目。微信点餐系统几乎是出现频率最高的选题之一,原因很直接——它同时踩中了微信小程序、Java后端、数据库设计、前后端分离这几个企业招聘里反复出现的词。但真正动手时,大多数人卡住的地方不是“不会写代码”,而是不知道一个完整的点餐系统应该包含哪些模块、数据库表怎么设计、小程序端怎么和后端联调、以及答辩时老师会追问什么。
这份“基于微信小程序+Java后端的微信点餐系统毕业设计”本质上是一个典型的B/S+C/S混合架构项目:用户端用微信小程序承载浏览菜单、加购物车、下单、查看订单状态;管理端用Web页面或小程序管理版完成菜品管理、订单处理、分类维护;后端用Java提供RESTful接口,数据库存储菜品、订单、用户、桌台等核心数据。它适合三类人:正在找毕设选题的本科生、想通过一个完整项目补齐前后端联调经验的自学者、以及需要快速搭建点餐类Demo做二次开发的人。
我见过太多人拿到这类压缩包后,直接双击打开源码,然后被一堆配置文件和环境依赖劝退。所以这篇笔记不按“先讲架构再讲代码”的套路走,而是按一个真实开发者的落地顺序来:先把技术选型和数据模型定下来,再跑通最小闭环,最后处理那些只有真正部署过才会遇到的坑。你跟着走完,至少能得到一个能演示、能答辩、能继续扩展的点餐系统骨架。
2. 技术选型与数据模型:为什么用Spring Boot而不是Servlet
2.1 后端框架选型:Spring Boot 2.x + MyBatis-Plus 是毕设的稳妥组合
很多毕设指导书还在用Servlet+JSP,但企业里早就不这么干了。微信小程序端通过HTTPS请求后端接口,后端返回JSON,这种前后端分离模式下,Spring Boot是最省心的选择。它内置Tomcat,不需要单独配置Web服务器,一个main方法就能启动;配合MyBatis-Plus,单表增删改查几乎不用写SQL,分页、逻辑删除、自动填充时间字段都有现成方案。
我一般会这样搭后端骨架:controller层只做参数校验和调用service,service层写业务逻辑,mapper层继承BaseMapper,entity层用注解映射数据库表。对于点餐系统,核心业务逻辑集中在订单状态流转和购物车合并上,这两块建议手写SQL或使用LambdaQueryWrapper,不要依赖代码生成器一把梭。
// OrderServiceImpl.java 订单状态流转核心逻辑 @Service public class OrderServiceImpl extends ServiceImpl<OrderMapper, Order> implements OrderService { @Autowired private OrderDetailMapper orderDetailMapper; @Override @Transactional(rollbackFor = Exception.class) public void createOrder(OrderDTO orderDTO) { // 1. 校验菜品是否下架 for (OrderDetailDTO detail : orderDTO.getDetails()) { Dish dish = dishMapper.selectById(detail.getDishId()); if (dish == null || dish.getStatus() == 0) { throw new BusinessException("菜品已下架:" + detail.getDishName()); } } // 2. 写入订单主表 Order order = new Order(); order.setOrderNo(generateOrderNo()); // 自定义订单号生成规则 order.setUserId(orderDTO.getUserId()); order.setTableId(orderDTO.getTableId()); order.setTotalAmount(orderDTO.getTotalAmount()); order.setStatus(OrderStatus.PENDING_PAYMENT.getCode()); this.save(order); // 3. 批量写入订单明细 for (OrderDetailDTO detail : orderDTO.getDetails()) { OrderDetail od = new OrderDetail(); od.setOrderId(order.getId()); od.setDishId(detail.getDishId()); od.setQuantity(detail.getQuantity()); orderDetailMapper.insert(od); } } }这段代码的关键点有三个:@Transactional保证订单主表和明细表同时成功或同时失败;订单号不要用自增ID直接暴露,我一般用“时间戳+用户ID后四位+随机数”生成;菜品下架校验必须放在写订单之前,否则会出现用户点了已下架菜品但订单已生成的脏数据。
参数方面,OrderStatus建议用枚举定义:待支付、已支付、制作中、已完成、已取消。数据库里存int值,前端展示时通过字典转换。totalAmount用BigDecimal,不要用double,否则会出现0.1+0.2≠0.3的经典问题。
2.2 数据库表设计:六张核心表撑起整个点餐流程
点餐系统的数据库不需要几十张表,把下面六张设计清楚就够用了。我见过有人把菜品规格、口味、加料拆成五六张表,结果查询时要关联七八次,毕设答辩时自己都讲不清。合理的做法是:常用字段冗余,复杂规格用JSON字符串存。
| 表名 | 核心字段 | 说明 |
|---|---|---|
| user | id, openid, nickname, avatar, phone | 微信用户,openid唯一 |
| category | id, name, sort, status | 菜品分类,sort控制排序 |
| dish | id, category_id, name, price, image, description, status, stock | 菜品,status控制上下架 |
| order | id, order_no, user_id, table_id, total_amount, status, create_time | 订单主表 |
| order_detail | id, order_id, dish_id, dish_name, price, quantity | 订单明细,冗余菜品名和价格 |
| table_info | id, table_no, capacity, status | 桌台信息,status控制空闲/占用 |
order_detail里冗余dish_name和price是故意的:菜品可能改价或改名,但历史订单必须保留下单时的快照。table_info在堂食场景下必须要有,扫码点餐时小程序通过桌台二维码携带的tableId来绑定订单。
建表时注意两个细节:所有表加create_time和update_time,用MyBatis-Plus的自动填充;order表的order_no加唯一索引,防止并发下重复。字符集用utf8mb4,否则用户昵称里的emoji会插入失败。
2.3 小程序端技术选型:原生开发还是uni-app
这是被问得最多的问题。如果你的毕设只要求微信小程序,用原生开发最稳,微信开发者工具直接调试,API调用没有中间层。但如果你还想兼容支付宝小程序或H5,uni-app更合适。我一般建议:毕设时间紧就选原生,因为uni-app打包微信小程序时偶尔会遇到样式错位和API兼容问题,排查起来很耗时间。
原生小程序的核心文件结构是:app.json配置页面路由和窗口样式,app.wxss写全局样式,每个页面由.wxml、.wxss、.js、.json四件套组成。点餐系统的页面至少需要:首页(分类+菜品列表)、菜品详情、购物车、订单确认、订单列表、个人中心。首页的菜品列表建议用scroll-view做分类锚点联动,不要用page滚动,否则分类吸顶效果很难做。
3. 从零跑通最小闭环:登录、点餐、下单、查单
3.1 微信登录与openid获取:后端换取session_key的正确姿势
微信小程序的登录流程是固定的:小程序端调用wx.login()拿到临时code,把code发给后端,后端用code+appid+secret请求微信接口换openid和session_key。这里有个坑:session_key不能返回给小程序端,它只保存在后端,用于解密用户信息。
// 小程序端 login.js wx.login({ success: (res) => { if (res.code) { wx.request({ url: 'https://your-domain.com/api/auth/login', method: 'POST', data: { code: res.code }, success: (resp) => { // 后端返回自定义token,存入storage wx.setStorageSync('token', resp.data.data.token); wx.setStorageSync('userId', resp.data.data.userId); } }); } } });后端处理时,用RestTemplate或HttpClient请求https://api.weixin.qq.com/sns/jscode2session,参数是appid、secret、js_code、grant_type=authorization_code。拿到openid后查数据库,如果用户不存在就自动注册一条记录。返回给小程序的token建议用JWT生成,有效期设7天,后续接口通过拦截器校验token。
注意:appid和secret不要硬编码在代码里,放到application.yml中,并且.gitignore要排除配置文件。我见过有人把secret提交到GitHub,结果被扫到后小程序被恶意调用,血泪教训。
3.2 菜品列表与分类联动:小程序页面列表加载更多的实现
点餐系统首页需要展示分类和菜品,用户点击左侧分类时右侧滚动到对应位置,右侧滚动时左侧分类高亮。这个交互用scroll-view的scroll-into-view属性实现。
// pages/menu/menu.js Page({ data: { categories: [], dishes: [], activeCategory: '', scrollIntoView: '' }, onLoad() { this.loadCategories(); }, loadCategories() { wx.request({ url: 'https://your-domain.com/api/category/list', success: (res) => { this.setData({ categories: res.data.data }); this.loadDishes(res.data.data[0].id); } }); }, loadDishes(categoryId) { wx.request({ url: 'https://your-domain.com/api/dish/list', data: { categoryId, page: 1, size: 20 }, success: (res) => { this.setData({ dishes: res.data.data.records }); } }); }, onCategoryTap(e) { const categoryId = e.currentTarget.dataset.id; this.setData({ activeCategory: categoryId, scrollIntoView: 'cat-' + categoryId }); this.loadDishes(categoryId); } });scroll-into-view的值必须是子元素的id,所以每个分类区块要加id="cat-{{item.id}}"。菜品列表加载更多用onReachBottom触发下一页请求,把新数据concat到原数组后面。注意page和size参数要和后端分页插件对应,MyBatis-Plus的Page对象默认从1开始。
3.3 购物车与下单:本地存储还是后端存储
购物车数据放哪里,取决于你的业务复杂度。如果只是单桌点餐,购物车放小程序的storage里最简单,用户退出小程序再进来数据还在。但如果要支持多设备同步或后台改价,就必须放后端。
我一般用混合方案:未登录时购物车存本地,登录后同步到后端。本地存储结构是{dishId, name, price, quantity, image}的数组,每次增减数量时更新storage并刷新页面。下单时把购物车数组发给后端,后端重新计算总价(不要信任前端传的总价),生成订单后清空本地购物车。
// 加入购物车 addToCart(e) { const dish = e.currentTarget.dataset.dish; let cart = wx.getStorageSync('cart') || []; const index = cart.findIndex(item => item.dishId === dish.id); if (index > -1) { cart[index].quantity += 1; } else { cart.push({ dishId: dish.id, name: dish.name, price: dish.price, quantity: 1, image: dish.image }); } wx.setStorageSync('cart', cart); wx.showToast({ title: '已加入', icon: 'success' }); }下单接口收到购物车数组后,先查数据库校验每个菜品的当前价格和状态,如果价格有变动要返回提示让用户确认。订单号生成用System.currentTimeMillis()加随机数,保证唯一且不连续。
3.4 订单状态流转与查询:用户端和管理端看到的不一样
订单状态是点餐系统最容易出bug的地方。用户端看到的是“待支付→已支付→制作中→已完成”,管理端看到的是“新订单→已接单→已出餐”。两边的状态码要统一,但展示文案可以不同。
后端用枚举定义状态:0待支付、1已支付、2制作中、3已完成、4已取消。用户支付成功后,微信支付回调接口把状态从0改成1;管理端点击“接单”改成2;点击“出餐”改成3。用户取消订单只能在前两个状态操作,制作中之后不允许取消。
查询订单列表时,用户端只查自己的订单,管理端查全部。分页参数用page和size,返回结果按create_time倒序。如果订单量不大,可以直接用MyBatis-Plus的Page对象;如果要做订单统计,建议单独写一个count查询,不要在前端遍历。
4. 避坑与排查:那些只有真正部署过才会遇到的问题
4.1 小程序请求域名未配置,真机调试全部失败
现象:微信开发者工具里接口请求正常,但用手机预览时所有请求都失败,控制台提示“不在以下 request 合法域名列表中”。
原因:微信小程序要求所有网络请求的域名必须在小程序管理后台的“开发设置→服务器域名”里配置,且必须是HTTPS。开发者工具可以勾选“不校验合法域名”绕过,但真机不行。
解决:把后端接口部署到有HTTPS证书的服务器上,域名备案后在小程序后台配置。如果只是毕设演示,可以用内网穿透工具临时生成一个HTTPS地址,但注意这个地址不能用于正式上线。另外,appid必须是小程序真实的appid,测试号不支持配置域名。
4.2 数据库中文乱码,菜品名称变成问号
现象:管理端添加菜品时输入中文,保存到数据库后变成???,小程序端展示也是乱码。
原因:MySQL数据库、表、连接字符集不一致。常见的是数据库默认latin1,或者JDBC连接URL没指定字符集。
解决:建库时指定CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;JDBC URL加?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai;MyBatis-Plus的配置文件里不要手动改字符集。如果已经建表,用ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4;修复。
4.3 订单重复提交,用户点一次生成两笔订单
现象:用户点击“提交订单”后,网络稍慢,用户又点了一次,结果生成两笔相同订单。
原因:前端没有防重复提交机制,后端也没有幂等校验。
解决:前端在点击后立即禁用按钮,请求完成后再恢复;后端用order_no唯一索引兜底,或者在Redis里用userId+cartHash做5秒锁。毕设如果没有Redis,可以在order表加一个request_id字段,前端每次进入订单确认页生成一个UUID,后端插入前先查这个request_id是否存在。
4.4 微信支付回调没收到,订单状态一直是待支付
现象:用户支付成功,但订单状态没变,管理端看不到新订单。
原因:微信支付回调地址必须是公网可访问的HTTPS地址,且不能带参数。本地开发时回调地址是localhost,微信服务器根本请求不到。
解决:开发阶段用内网穿透把本地接口暴露到公网,把穿透地址配到微信支付回调里。回调接口要做签名验证,防止伪造请求。回调处理成功后必须返回SUCCESS的XML,否则微信会重复通知。如果毕设不要求真实支付,可以做一个“模拟支付”按钮,点击后直接改状态,答辩时说明即可。
4.5 小程序顶部导航栏高度不一致,页面内容被遮挡
现象:自定义导航栏后,不同机型顶部高度不一样,有的页面内容被胶囊按钮挡住。
原因:微信小程序的导航栏高度由状态栏高度+标题栏高度组成,不同手机状态栏高度不同。
解决:用wx.getSystemInfoSync()获取statusBarHeight,标题栏高度安卓固定48px,iOS固定44px。自定义导航栏时,页面内容要加padding-top等于状态栏高度+标题栏高度。更简单的做法是直接用微信默认导航栏,只在app.json里改navigationBarTitleText和navigationBarBackgroundColor。
5. 进阶技巧:让毕设从“能跑”变成“能讲”
5.1 用拦截器统一处理token和异常
后端接口多了以后,每个controller都写一遍token校验和异常捕获太啰嗦。我一般写两个拦截器:AuthInterceptor校验请求头里的token,解析出userId塞到ThreadLocal里;GlobalExceptionHandler用@RestControllerAdvice捕获BusinessException和系统异常,统一返回{code, message, data}格式。
@Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { String token = request.getHeader("token"); if (token == null || !jwtUtil.validate(token)) { throw new BusinessException(401, "未登录或登录已过期"); } UserContext.setUserId(jwtUtil.getUserId(token)); return true; } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { UserContext.clear(); // 防止线程池复用导致数据串号 } }UserContext用ThreadLocal<Integer>存userId,afterCompletion里必须remove,否则Tomcat线程池复用时会拿到上一个请求的用户ID,这个坑非常隐蔽。
5.2 菜品图片上传与存储:不要存数据库
菜品图片如果以base64存数据库,表会迅速膨胀,查询也慢。正确做法是:小程序端用wx.chooseImage选图,wx.uploadFile上传到后端,后端保存到服务器磁盘或对象存储,数据库只存URL路径。
@PostMapping("/upload") public Result<String> upload(@RequestParam("file") MultipartFile file) { String originalName = file.getOriginalFilename(); String suffix = originalName.substring(originalName.lastIndexOf(".")); String fileName = UUID.randomUUID().toString() + suffix; File dest = new File(uploadPath + fileName); file.transferTo(dest); return Result.success("/images/" + fileName); }uploadPath配置在application.yml里,指向服务器的一个绝对路径。同时要配置静态资源映射,让/images/**能访问到磁盘文件。如果部署到Linux,注意目录权限,否则上传会报FileNotFoundException。
5.3 答辩时老师最爱问的三个问题
第一个:“你的订单号怎么保证唯一?”回答:时间戳+用户ID后四位+随机数,数据库加唯一索引,插入冲突时重试。第二个:“购物车数据放哪里?”回答:未登录放本地storage,登录后同步到后端,下单时后端重新计算价格。第三个:“微信支付怎么做的?”如果没接真实支付,就诚实说用模拟支付,重点讲清楚支付回调的幂等处理思路。
我自己的习惯是:答辩前把整个系统在本地重新部署一遍,从建库、改配置、启动后端、编译小程序,每一步都截图存下来。老师问“这个功能怎么实现的”,直接翻截图比现场敲代码稳得多。另外,源码里不要留TODO和注释掉的废代码,老师看到会追问“这块是不是没做完”。
希望帮到你。
本文还有配套的精品资源,点击获取