简介一份面向高校毕业设计的火锅店点餐系统小程序完整项目包基于微信小程序、SSM框架与MySQL数据库开发整体采用Java和Vue技术栈可运行于Idea环境覆盖管理员与用户双角色。功能包含菜品查询与购买、餐桌预定、订单管理等管理员能维护菜品信息、处理订单用户可在线点餐并预约餐桌适合计算机相关专业学生用于毕业设计参考或二次开发。压缩包内共1042个文件以vue、java、js源码以及png、svg图片素材为主另有wxss、wxml小程序页面文件、sql数据库脚本、mp4演示视频和doc格式毕业论文、任务书等整体大小约61.61MB目录结构清晰完整。目前已有129人学习下载读者可直接获得可运行代码、数据库初始化脚本、配套设计文档与操作演示视频从环境搭建到功能实现均有详细指导能有效降低项目启动和论文撰写难度作为课程设计或毕业设计素材非常合适。1. 当微信小程序、SSM、MySQL 被放进同一张毕设任务书把微信小程序、SSM、MySQL 三种技术绑在一起做“火锅店点餐系统”在毕业设计里几乎是每年都会被复现的一套组合。原因是它覆盖了从订单入口到后厨出餐再到支付结算的完整业务闭环同时又刚好卡在“课程里学过、工作量足够、演示效果好”的三个点上。对着这个标题做系统我一般会先明确一个原则这套系统真正的难点不在于小程序界面的花哨程度而在于“下单到后厨出餐”这条链路上的数据一致性以及数据库脚本和后端接口能否在答辩时随时从头跑通。源码、数据库脚本、任务书、论文、视频教程这些交付物本质上是在一份可复现的工程上长出来的所以下面照着这条链路把方案拆开讲。2. 技术选型和目录先搭出“毕业设计级”的小程序SSM骨架2.1 为什么是这个组合以及它和 Spring Boot 的边界很多人在选题时会问SSM 都是老框架了直接用 Spring Boot 不是更省事对真实项目来说确实是这样但毕业设计里 SSM 仍被导师和评委接受原因在于配置暴露得多方便在答辩时讲“框架是怎么把请求从 controller 转发到 mapper 的”。SSM 的 XML 配置和 Mapper 文件是最好展开讲“原理”的部分而 Spring Boot 的自动装配反而容易让人觉得代码是抄来的。因此这里建议老老实实保留 SSM 三层结构不大改成 Spring Boot。小程序端这边原型工程一般用原生微信小程序而不是 uniapp 之类跨端方案。原生小程序对评委来说可读性更高不需要解释编译链路而且 wx.request、wx.login 这些 API 直接写在页面里改起来也直观。后端与 MySQL 的连接层用 MyBatis 的 XML 映射SQL 语句你能完全控制导师问“模糊查询怎么写”“多表关联怎么配”你直接把 XML 文件翻开讲就行。2.2 推荐目录结构以及每层该放什么拿到这个标题时我一般会把工程先切分四个目录再开始写代码hotpot-order/ ├─ miniprogram/ // 微信小程序前端 │ ├─ pages/ │ │ ├─ index/ // 首页桌台扫码进入 │ │ ├─ menu/ // 菜品列表与分类 │ │ ├─ cart/ // 购物车 │ │ ├─ order/ // 订单列表与详情 │ │ └─ profile/ // 个人中心与登录态 │ ├─ utils/request.js // wx.request 的 Promise 封装 │ └─ app.json ├─ ssm-server/ // SSM 后端工程 │ ├─ pom.xml │ └─ src/main/ │ ├─ java/com/edu/hotpot/ │ │ ├─ controller/ // REST 接口层 │ │ ├─ service/ // 订单/菜品核心逻辑 │ │ ├─ dao/ // MyBatis Mapper 接口 │ │ └─ entity/ // 与数据库表对应的实体 │ └─ resources/ │ ├─ mapper/ // Mapper XMLSQL 全在这里 │ └─ spring/ // applicationContext 等配置 ├─ database/ │ └─ hotpot.sql // 建库、建表、初始化数据脚本 └─ docs/ // 任务书、论文、演示视频素材这里有一个点必须提前确认后端包名不要用com.example。改成com.edu.hotpot这类带有项目含义的包名扫描配置、Mapper 扫描、论文里的架构图全部统一到这个命名空间。否则答辩时老师顺着包名看到“example”印象分立刻下降。模块职责可以按下面这张表去写文档和答辩稿模块技术实现职责小程序端原生微信小程序扫码、选菜、购物车、下单、订单列表、我的页面后端接口Spring MVC Controller接收 wx.request 请求校验参数返回 JSON业务逻辑Spring Service订单提交事务、库存与状态变更、菜品分类组装数据持久层MyBatis Mapper单表增删改查、多表 join、动态 SQL 模糊查询数据库MySQL 5.7 / 8.0存储菜品、桌台、订单、订单明细、用户2.3 版本取舍MySQL 5.7 还是 8.0Java 侧要注意什么这套系统里数据库脚本属于交付物所以脚本兼容性比“最新版本”更重要。开发机用 MySQL 5.7 是最稳妥的如果你已经在用 8.0问题也不大但要注意两点第一8.0 默认身份认证插件是 caching_sha2_passwordJDBC 驱动要用mysql-connector-java:8.0.x并在连接串上显式加上allowPublicKeyRetrievaltrue第二8.0 对 group by 的检测更严格写统计类 SQL 时 select 字段必须都出现在 group by 中否则答辩现场跑脚本会当场报错。驱动坐标和连接串的写法放到第 4 章接口实现里一起说。3. 火锅店点餐数据库脚本菜品、桌台、订单、下厨队列的表怎么落3.1 先定核心表名避免踩到 MySQL 保留字火锅店点餐的业务边界很明确扫码、看菜、加购物车、提交订单、后厨出餐、结账。这类系统表数量不需要太多六到八张完全够用。命名上有一个容易犯的错订单表不要叫order因为 order 在 MySQL 里是 ORDER BY 的保留字脚本执行时会报语法错误。常见做法是命名为orders订单明细叫order_item桌台叫desk菜品叫menu菜品分类叫category。3.2 建表脚本的核心片段下面这段 DDL 是可以直接拿去建库的保留了火锅店场景里最具区分度的字段CREATE DATABASE IF NOT EXISTS hotpot_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hotpot_db; CREATE TABLE category ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, name VARCHAR(30) NOT NULL UNIQUE, sort TINYINT NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT 菜品分类; CREATE TABLE menu ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, category_id INT UNSIGNED NOT NULL, name VARCHAR(50) NOT NULL, price DECIMAL(8,2) NOT NULL COMMENT 现价, unit VARCHAR(10) NOT NULL DEFAULT 份, stock INT NOT NULL DEFAULT 999, pic VARCHAR(255) DEFAULT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, KEY idx_menu_category (category_id), CONSTRAINT fk_menu_category FOREIGN KEY (category_id) REFERENCES category(id) ) ENGINEInnoDB COMMENT 火锅菜品; CREATE TABLE desk ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, no VARCHAR(10) NOT NULL UNIQUE COMMENT 桌号如 A01, seats TINYINT NOT NULL DEFAULT 4, qr_code VARCHAR(255) DEFAULT NULL ) ENGINEInnoDB COMMENT 桌台; CREATE TABLE orders ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 业务单号, desk_id INT UNSIGNED NOT NULL, user_openid VARCHAR(64) NOT NULL COMMENT 小程序用户标识, total_amount DECIMAL(10,2) NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 0 COMMENT 0待支付 1待接单 2制作中 3待收货 4已完成 5已取消, remark VARCHAR(200) DEFAULT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, paid_at DATETIME DEFAULT NULL, KEY idx_desk_status (desk_id, status), KEY idx_user (user_openid) ) ENGINEInnoDB COMMENT 订单主表; CREATE TABLE order_item ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, order_id BIGINT UNSIGNED NOT NULL, menu_id INT UNSIGNED NOT NULL, price DECIMAL(8,2) NOT NULL COMMENT 下单时快照价, quantity INT NOT NULL DEFAULT 1, KEY idx_order (order_id), CONSTRAINT fk_order FOREIGN KEY (order_id) REFERENCES orders(id) ) ENGINEInnoDB COMMENT 订单明细;这段脚本里有几个设计点值得在论文里展开menu 表存的是“下单时的快照价”在 order_item 里菜品改了价格不影响历史订单orders 表存 user_openid 而不是微型小程序里不稳定的用户 id后续要接微信生态不会断status 用 TINYINT 而不是字符串一是省空间二是让状态流转可以用做范围查询。3.3 订单状态流转表与统计脚本订单状态是答辩时必被问的部分。用一张表把状态关系画出来比口头解释顺畅状态值含义能进入的下一个状态0待支付1 或 51待接单2 或 52制作中33待收货44已完成无5已取消无数据库脚本里除了建表还要给评委员提供一个能直接看到业务成效的查询。比如“统计某天营业额”和“拉出某桌三日内的点单记录”-- 统计指定日期已完成订单金额 SELECT DATE(created_at) AS d, COUNT(*) AS order_cnt, SUM(total_amount) AS revenue FROM orders WHERE status 4 AND created_at 2024-11-01 GROUP BY DATE(created_at); -- 某桌近三天点过的菜 SELECT d.no, m.name, oi.price, oi.quantity FROM orders o JOIN desk d ON o.desk_id d.id JOIN order_item oi ON o.id oi.order_id JOIN menu m ON oi.menu_id m.id WHERE d.no A01 AND o.created_at DATE_SUB(NOW(), INTERVAL 3 DAY) ORDER BY o.created_at DESC;后一条 SQL 是典型的三表 joinMyBatis 里配置 resultMap 时正好能做对象嵌套。如果想让脚本再显眼一点可以加一个 MySQL 存储过程做“每日菜品销量榜”虽然业务意义一般但对毕业设计来说是个可讲亮点DELIMITER $$ CREATE PROCEDURE sp_daily_top(IN p_date DATE) BEGIN SELECT m.name, SUM(oi.quantity) AS qty FROM order_item oi JOIN orders o ON oi.order_id o.id JOIN menu m ON oi.menu_id m.id WHERE DATE(o.created_at) p_date AND o.status IN (2,3,4) GROUP BY m.name ORDER BY qty DESC LIMIT 10; END$$ DELIMITER ;调用时直接CALL sp_daily_top(2024-11-20)。存储过程不是为了炫技而是为了在后端某个“今日热销”接口里直接复用减少 Java 侧的聚合逻辑。3.4 数据库脚本交付时应该包含哪些文件很多同学交出去的是单个hotpot.sql但更专业的做法是拆成三份放同一目录01_schema.sql只放建库建表语句02_data.sql放菜品分类、火锅底料、常用配菜这些初始化数据03_routine.sql放存储过程和定时事件。这样答辩时老师问“数据库脚本在哪”你可以说“结构、数据、存储过程分开维护”同时避免重复执行脚本时报表已存在的错误。如果你是 MySQL 8.0 用户执行时留意表字符集设置是否为 utf8mb4不然 emoji 表情写入菜品备注会报错这个问题在论文的“系统测试”一节可以用来体现排错过程。4. SSM 后端三层的接口实现与 MyBatis 映射细节4.1 数据源配置与 JDBC 连接串的参数坑SSM 工程跑不起来的原因有一大半集中在数据库连接串里。先给一份可以直接用的jdbc.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://127.0.0.1:3306/hotpot_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.password123456驱动类要注意MySQL 5.7 老驱动用com.mysql.jdbc.Driver8.x 驱动用com.mysql.cj.jdbc.Driver写错会报 ClassNotFoundException。serverTimezoneAsia/Shanghai不写5.7 不报错但 8.0 会报时区错误useSSLfalse是本地开发必须关掉的不然启动日志里全是 SSL 警告。allowPublicKeyRetrievaltrue只对 8.0 的 caching_sha2_password 插件有效。4.2 Controller 接口清单与一个点餐接口的完整链路后端接口数量不用多点餐业务里前端真正高频调用的足够了方法路径说明参数GET/api/category/list菜品分类列表无GET/api/menu/list菜品列表支持按分类过滤categoryIdGET/api/menu/search按名称模糊查菜keywordPOST/api/order/submit提交订单deskId, itemsGET/api/order/list当前用户订单列表openid, statusPUT/api/order/status商家接单/用户确认收货orderId, status点餐系统里最容易写错的是提交订单接口。用户选完菜一次性提交后端要做的不是简单地 insert 一条记录而是“一次事务里同时完成订单创建、明细插入、库存扣减”三件事。用Transactional包住 service 方法Override Transactional(rollbackFor Exception.class) public MapString, Object submitOrder(OrderSubmitDTO dto) { // 1. 创建主订单 Orders order new Orders(); order.setOrderNo(generateOrderNo()); order.setDeskId(dto.getDeskId()); order.setUserOpenid(dto.getOpenid()); order.setStatus(0); order.setTotalAmount(BigDecimal.ZERO); BigDecimal total BigDecimal.ZERO; ListOrderItem items new ArrayList(); for (OrderItemDTO itemDTO : dto.getItems()) { Menu menu menuMapper.selectById(itemDTO.getMenuId()); // 2. 库存不足直接抛异常事务回滚主表插入 if (menu.getStock() itemDTO.getQuantity()) { throw new BizException(菜品[ menu.getName() ]库存不足); } OrderItem orderItem new OrderItem(); orderItem.setMenuId(menu.getId()); orderItem.setPrice(menu.getPrice()); orderItem.setQuantity(itemDTO.getQuantity()); items.add(orderItem); total total.add(menu.getPrice().multiply( BigDecimal.valueOf(itemDTO.getQuantity()))); } order.setTotalAmount(total); orderMapper.insert(order); // 3. 插入明细并扣库存 for (OrderItem item : items) { item.setOrderId(order.getId()); orderItemMapper.insert(item); menuMapper.deductStock(item.getMenuId(), item.getQuantity()); } MapString, Object result new HashMap(); result.put(orderNo, order.getOrderNo()); result.put(totalAmount, total); return result; }这段逻辑里必须注意明细里的 price 取的是数据库当前价格而不是前端传过来的价格。前端传价容易被篡改也容易出现用户下单后商家调价导致的历史订单价格漂移。库存扣减在 service 层做完后Mapper 里对应一个 update 语句update iddeductStock UPDATE menu SET stock stock - #{quantity} WHERE id #{menuId} AND stock #{quantity} /update这个 update 的 where 条件里带stock #{quantity}配合 MyBatis 返回的受影响行数可以判断是否发生超卖。即使将来不做秒杀这种写法也比“先 select 再 update”更稳。4.3 Mapper XML 中多表查询与 resultMap 的写法订单列表在火锅店场景里不会一屏全查出来通常按状态分页。写一个按用户 openid 和状态查订单列表的方法返回内容要带上每个订单下的菜品明细。这时候 resultMap 比给商品表放一个 orderId 数组字段更贴近“对象关联”的语义resultMap idOrderWithItems typecom.edu.hotpot.entity.Orders id propertyid columnid/ result propertyorderNo columnorder_no/ result propertydeskId columndesk_id/ result propertytotalAmount columntotal_amount/ result propertystatus columnstatus/ collection propertyitems ofTypecom.edu.hotpot.entity.OrderItem id propertyid columnitem_id/ result propertymenuId columnmenu_id/ result propertyprice columnprice/ result propertyquantity columnquantity/ /collection /resultMap select idselectUserOrders resultMapOrderWithItems SELECT o.id, o.order_no, o.desk_id, o.total_amount, o.status, oi.id AS item_id, oi.menu_id, oi.price, oi.quantity FROM orders o LEFT JOIN order_item oi ON o.id oi.order_id WHERE o.user_openid #{openid} if teststatus ! null AND o.status #{status} /if ORDER BY o.created_at DESC /select逻辑说不通的地方在于 LEFT JOIN 后的分页。如果订单很多分页别在 SQL 里直接 limit先查 order 主表 id 列表再 join 明细或者用子查询作为驱动表。初学者容易在这踩坑论文的“系统优化”部分刚好可以写这一段。4.4 小程序跨域与请求转发配置后端跑起来后小程序开发者工具里直接请求http://127.0.0.1:8080会遇跨域需要在 Spring MVC 里加一个 CORS 过滤器。常见做法不是给每个 Controller 加注解而是加一个全局配置Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/api/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }真机预览时127.0.0.1在小程序里指向手机自己要填电脑的局域网 IP这是毕设演示时常见的翻车点。配了“不校验合法域名”只在开发者工具里有效真机预览必须走局域网 IP 加服务端口。4.5 Maven 依赖与打包时的 Java 版本问题SSM 工程用 Maven 打包时pom.xml 里maven-compiler-plugin的 source/target 要与本机 JDK 匹配。JDK 8 配编译级别 1.8JDK 11 就配 11。如果代码里用了var或 List.of 这类高版本语法而项目在低版本 JDK 跑编译不会报错运行期会出现 NoSuchMethodError这个坑在大三学生的电脑上经常出现。打包用mvn clean package生成的 war 丢进 Tomcat 8.5 的 webapps能省去 IDE 里 Debug 启动的一堆环境变量配置。5. 小程序端联调把加载页做成演示时的加分项5.1 先保证“菜点得下去”请求封装与登录态缓存小程序端一次性把点餐主链路跑通比开发一堆边缘页面重要。请求封装这段我一般会在utils/request.js里做三件事统一 BASE_URL、自动带 token、统一 res.code 判断。代码段控制在三十行左右const BASE_URL http://127.0.0.1:8080/api; function request(path, method GET, data {}) { return new Promise((resolve, reject) { wx.request({ url: BASE_URL path, method, data, header: { content-type: application/json }, success(res) { if (res.data.code 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg, icon: none }); reject(res.data); } }, fail: reject }); }); } module.exports { request, BASE_URL };这段代码的要点在后端返回格式统一成{code, msg, data}。很多项目前后端各写各的联调时痛苦就痛苦在接口返回格式不统一。BASE_URL 放在常量里换测试环境只改一处这是整个小程序端最值得提前约定好的事情。5.2 修改刚进入的加载页面从白屏转圈到骨架屏“修改刚进入的加载页面”这个需求其实包含两层一是 app.json 里默认的 window 配置只能控制导航栏颜色和顶部标题不能在首页渲染前插入自定义 loading 视图二是开发者工具默认情况下页面 onLoad 时会先闪一下白屏等接口数据回来后才渲染菜品。解决思路是用骨架屏组件。小程序支持page-meta和自定义组件最简单做法是在菜单页 template 里给数据列表预留骨架结构view wx:if{{loading}} classskeleton view classskeleton-item wx:for{{[1,2,3,4,5]}} wx:key*this view classskeleton-img/view view classskeleton-line/view /view /view view wx:else view classmenu-item wx:for{{menuList}} wx:keyid text{{item.name}}/text text{{item.price}}/text /view /view加载逻辑在 onLoad 里配合 Promise。菜单数据来了之后先 setDataloading 翻成 false再写缓存下次启动优先读缓存并后台刷新的思路对这个小程序友好onLoad() { const cached wx.getStorageSync(menu_list); if (cached cached.length) { this.setData({ menuList: cached, loading: false }); } request(/menu/list, GET, { categoryId: 0 }) .then((data) { this.setData({ menuList: data, loading: false }); wx.setStorageSync(menu_list, data); }) .catch(() { if (!this.data.menuList.length) { this.setData({ loading: false }); } }); }参数说明wx.getStorageSync(menu_list)里缓存的是整份菜品设置过期时间可以用wx.setStorageSync(menu_expire, Date.now())取的时候比对时间戳避免菜品数据永远停留在上一次。5.3 一处验证技巧清缓存后看首屏耗时联调完毕的验收步骤我一般会指导这样操作开发者工具里清掉 storage切到“真机调试”然后观察首页从点开小程序到菜品图完整渲染的时间。这个时间如果超过 2 秒优先看后端返回数据大小而不是怀疑网络因为菜品图片过大的情况最多。把图片放到 OSS 或用懒加载后的耗时对比写在测试章节里是一个非常讨巧的演示素材也是这整套系统交付时最直观的体验提升证据。本文还有配套的精品资源点击获取