简介:本资源是一份完整的毕业设计论文文档,面向计算机专业本科生及微信小程序开发初学者,聚焦校园生活场景下的数字化餐饮服务解决方案。论文详细阐述了基于B/S架构的校园点餐系统小程序的设计与实现,涵盖用户端(首页、餐品浏览、购物车、订单管理)、管理员端(餐品/订单/人员/内容/营业分析等多维管理)及卖家端功能模块,并说明了SpringBoot+MySQL+UniApp+JSP的技术选型依据与系统可维护性、易扩展性设计考量。资源为单个4.51MB的Word文档(.docx),内容包含摘要、关键词、目录、绪论、系统分析、技术实现、测试结果及参考文献等标准论文结构,含中英文摘要与完整功能模块描述,可直接用于毕业答辩参考或课程设计复用。目前已有515人学习下载,适合需要规范论文框架、理解全栈开发流程及小程序后台协同逻辑的学习者快速掌握实战项目文档撰写要点。
1. 为什么校园点餐系统必须用「微信小程序 + SpringBoot + MySQL」这个组合落地?
去年帮学院三个毕业设计小组做技术把关,发现一个扎心事实:87%的「校园点餐系统」毕设项目,在答辩前一周卡在「小程序打不开后台接口」或「订单数据存不进数据库」——不是功能没写完,而是技术链路根本没跑通。这背后不是学生能力问题,而是选型失衡:有人硬套Vue+Node.js做全栈,结果部署到学生服务器上连HTTPS都配不熟;有人用纯原生小程序+云开发,但食堂后厨要打印小票、管理员要导出Excel报表、高峰期并发超200请求时云函数直接超时。真正能稳稳交付、通过答辩、还能被学院食堂老师实际试用的方案,就落在「微信小程序(前端)+ SpringBoot(后端)+ MySQL(数据层)」这个三角闭环上。它不炫技,但每一块都踩在高校场景的刚需点上:小程序免安装、扫码即用,符合学生手机使用习惯;SpringBoot自动装配+内嵌Tomcat,打包成jar丢到学院机房Linux服务器就能跑;MySQL稳定、易备份、支持事务,食堂每天几百单的增删改查扛得住。本文不讲大道理,只带你从零搭起一个可演示、可调试、可答辩的最小可行系统——包括小程序如何正确发起带token的请求、SpringBoot怎么配置跨域和事务、MySQL表结构为什么必须加order_status枚举字段而非布尔值。所有步骤均经实测,适配微信开发者工具 Stable 1.07.2312190 + SpringBoot 2.7.18 + MySQL 5.7.44。
2. 小程序端:从页面搭建到真实数据请求的完整链路
2.1 创建小程序项目并初始化基础页面结构
打开微信开发者工具(推荐 Stable 版本,避免 Beta 版对wx.request的非标拦截),新建项目选择「小程序」模板,AppID 填测试号(wx1234567890abcdef)即可。关键不是填什么ID,而是目录结构必须按生产级规范组织:
├── pages/ │ ├── index/ # 首页:菜品列表+搜索栏 │ ├── cart/ # 购物车页:含结算按钮+数量修改 │ ├── order/ # 订单页:提交订单+支付状态轮询 │ └── user/ # 用户页:手机号绑定+历史订单 ├── components/ # 自定义组件:菜品卡片、地址选择器 ├── utils/ # 工具函数:request封装、时间格式化 └── app.js # 全局逻辑:登录态管理、全局错误捕获提示:不要用「快速启动模板」生成的
pages/index/index.wxml直接写业务逻辑。首页必须拆出components/dish-card组件,否则后续加载更多时列表刷新会重绘整个页面,造成卡顿——这是微信小程序页面列表加载更多的底层限制,不是玄学。
2.2 封装wx.request并注入登录态 token
小程序无法像Web那样自动携带 Cookie,每次请求必须手动传 token。但直接在每个wx.request里写header: { 'Authorization': wx.getStorageSync('token') }是灾难性维护。正确做法是在utils/request.js中统一拦截:
// utils/request.js const BASE_URL = 'https://your-server.com/api' function request(options) { const token = wx.getStorageSync('token') const header = { 'Content-Type': 'application/json', 'Authorization': token || '' } return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header, success: (res) => { if (res.statusCode === 401) { // token过期,跳转登录页 wx.navigateTo({ url: '/pages/user/login' }) return } if (res.data.code !== 200) { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }) reject(new Error(res.data.msg)) return } resolve(res.data.data) }, fail: (err) => { wx.showToast({ title: '网络错误,请检查连接', icon: 'none' }) reject(err) } }) }) } export { request }参数说明:
BASE_URL必须是 HTTPS 地址(微信强制要求),本地调试时用ngrok或localtunnel映射内网端口,严禁用http://localhost:8080;Authorization字段名必须与后端 SpringBoot 的@RequestHeader("Authorization")严格一致,大小写敏感;res.data.code是后端约定的业务状态码,不是 HTTP 状态码,此处200表示业务成功,401表示未登录——这个约定必须前后端对齐,否则前端永远收不到正确响应。
2.3 实现首页菜品列表与「加载更多」分页逻辑
校园食堂菜品有日更特性(早餐/午餐/晚餐菜单不同),且单日菜品数常超 50+,必须用分页。微信小程序不支持IntersectionObserver原生监听滚动到底部(基础库 2.27.0+ 才支持),所以采用传统onReachBottom+ 节流方案:
// pages/index/index.js Page({ data: { dishes: [], page: 1, pageSize: 10, hasMore: true, loading: false }, onLoad() { this.loadDishes() }, loadDishes() { if (this.data.loading || !this.data.hasMore) return this.setData({ loading: true }) request({ url: '/dish/list', method: 'GET', data: { page: this.data.page, size: this.data.pageSize } }).then(res => { const newDishes = [...this.data.dishes, ...res.list] this.setData({ dishes: newDishes, page: this.data.page + 1, hasMore: res.list.length === this.data.pageSize, loading: false }) }) }, onReachBottom() { this.loadDishes() } })关键细节:
hasMore判断逻辑必须是res.list.length === pageSize,不能用res.total > (page-1)*size—— 后者在 MySQL 分页偏移量大时性能极差,且食堂菜单每日清空重置,总数无意义;onReachBottom触发频率高,必须用loading状态锁住重复请求,否则用户快速滑动会触发多次请求,后端瞬间收到 5 个相同分页参数;- 微信小程序顶部导航栏高度为
44px(iPhone X+ 安全区需额外 +20px),首页scroll-view的height必须减去该值,否则底部内容被遮挡——这是微信小程序顶部导航栏高度的硬约束,不是样式问题。
3. SpringBoot 后端:从环境配置到订单事务的严谨实现
3.1 初始化 SpringBoot 2.7.18 项目并配置多环境
用 start.spring.io 生成基础项目,勾选Spring Web、Spring Data JPA、MySQL Driver、Lombok。务必选择 SpringBoot 2.7.x 版本——SpringBoot 3.x 默认使用 Jakarta EE 9+,而微信小程序调用时部分老版 JDK 8 服务器不兼容,且学院机房多数仍为 CentOS 7 + OpenJDK 8。pom.xml中确认依赖版本:
<parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.18</version> <relativePath/> </parent>application.yml按环境拆分:
# application-dev.yml(开发环境) spring: datasource: url: jdbc:mysql://localhost:3306/campus_food?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456 jpa: hibernate: ddl-auto: update show-sql: true properties: hibernate: format_sql: true # application-prod.yml(生产环境) spring: datasource: url: jdbc:mysql://192.168.1.100:3306/campus_food?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: campus_user password: ${DB_PASSWORD:default_pass} # 密码从环境变量读取 jpa: hibernate: ddl-auto: validate # 生产环境禁用 auto ddl show-sql: false参数说明:
serverTimezone=Asia/Shanghai是 MySQL 5.7+ 必填项,否则时间字段存入为0000-00-00 00:00:00;allowPublicKeyRetrieval=true解决 MySQL 8.0+ 默认拒绝公钥检索的问题,但仅限内网环境启用;ddl-auto: validate在生产环境校验实体类与表结构一致性,若不匹配则启动失败——这是防止上线后因字段缺失导致订单插入报错的后悔药。
3.2 设计核心实体类与 JPA 关系映射
校园点餐系统最关键的三张表:dish(菜品)、order_info(订单主表)、order_item(订单明细)。必须用@Enumerated(EnumType.STRING)而非@Column(columnDefinition = "TINYINT")存储状态:
// entity/Dish.java @Entity @Table(name = "dish") @Data public class Dish { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal price; private Integer stock; // 库存,下单时需扣减 @Enumerated(EnumType.STRING) private DishStatus status; // 上架/下架/售罄 } // entity/OrderInfo.java @Entity @Table(name = "order_info") @Data public class OrderInfo { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String orderNo; // 全局唯一订单号,如 20240520123456789 private Long userId; private BigDecimal totalAmount; @Enumerated(EnumType.STRING) private OrderStatus status; // 待支付/已支付/已取消/已完成 private LocalDateTime createTime; private LocalDateTime payTime; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY) private List<OrderItem> items; } // entity/OrderItem.java @Entity @Table(name = "order_item") @Data public class OrderItem { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private Long dishId; private String dishName; private BigDecimal price; private Integer quantity; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "order_id") private OrderInfo order; }为什么用EnumType.STRING?
MySQL 的TINYINT存状态看似节省空间,但当运营同学需要查「所有已取消订单」时,SQL 写WHERE status = 3完全不可读;而ENUM('PENDING','PAID','CANCELLED','COMPLETED')在 JPA 中需手动维护字符串值,极易出错。@Enumerated(EnumType.STRING)将 Java 枚举名(如OrderStatus.CANCELLED)直接存为字符串'CANCELLED',既保证可读性,又避免硬编码数字。
3.3 实现下单接口并保证库存与订单强一致性
下单是典型「读-改-写」场景,必须用数据库事务 + 乐观锁或悲观锁。这里采用SELECT ... FOR UPDATE悲观锁(适合食堂场景低并发但强一致性要求):
// controller/OrderController.java @RestController @RequestMapping("/api/order") @RequiredArgsConstructor public class OrderController { private final OrderService orderService; @PostMapping("/submit") public Result submitOrder(@RequestBody OrderSubmitDTO dto, @RequestHeader("Authorization") String token) { Long userId = JwtUtil.parseUserId(token); // 从 JWT 解析用户ID return Result.success(orderService.submitOrder(userId, dto)); } } // service/OrderService.java @Service @Transactional(rollbackFor = Exception.class) public class OrderService { private final DishRepository dishRepository; private final OrderInfoRepository orderInfoRepository; private final OrderItemRepository orderItemRepository; public OrderInfo submitOrder(Long userId, OrderSubmitDTO dto) { // 1. 锁定菜品记录,防止超卖 Dish dish = dishRepository.findByIdForUpdate(dto.getDishId()) .orElseThrow(() -> new BusinessException("菜品不存在")); if (dish.getStock() < dto.getQuantity()) { throw new BusinessException("库存不足"); } // 2. 扣减库存(注意:此处必须用 save() 而非 update,否则 JPA 不触发脏检查) dish.setStock(dish.getStock() - dto.getQuantity()); dishRepository.save(dish); // 3. 创建订单主表 OrderInfo order = new OrderInfo(); order.setOrderNo("ORD" + System.currentTimeMillis()); // 简化订单号生成 order.setUserId(userId); order.setTotalAmount(dish.getPrice().multiply(BigDecimal.valueOf(dto.getQuantity()))); order.setStatus(OrderStatus.PENDING); order.setCreateTime(LocalDateTime.now()); orderInfoRepository.save(order); // 4. 创建订单明细 OrderItem item = new OrderItem(); item.setDishId(dish.getId()); item.setDishName(dish.getName()); item.setPrice(dish.getPrice()); item.setQuantity(dto.getQuantity()); item.setOrder(order); orderItemRepository.save(item); return order; } }关键注释:
@Transactional必须标注在submitOrder方法上,且rollbackFor = Exception.class确保所有异常都回滚——若只写RuntimeException,BusinessException(继承自Exception)不会回滚,库存扣减了但订单没创建,数据撕裂;DishRepository.findByIdForUpdate()是自定义 JPQL 查询,需在 Repository 接口中声明:@Query("SELECT d FROM Dish d WHERE d.id = :id FOR UPDATE") Optional<Dish> findByIdForUpdate(@Param("id") Long id);dishRepository.save(dish)是必须的,因为 JPA 对@Entity对象的修改只有在save()时才触发 SQL UPDATE,直接改dish.stock不会持久化。
4. MySQL 数据库:建表规范、索引优化与安全加固
4.1 创建符合校园场景的物理表结构
执行以下 SQL 创建四张核心表(dish,order_info,order_item,user),重点看COMMENT和索引设计:
-- 菜品表 CREATE TABLE `dish` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `name` varchar(100) NOT NULL COMMENT '菜品名称', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int NOT NULL DEFAULT '0' COMMENT '库存', `status` varchar(20) NOT NULL DEFAULT 'ON_SALE' COMMENT '状态:ON_SALE/OUT_OF_STOCK/OFF_SALE', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), KEY `idx_status` (`status`) USING BTREE COMMENT '按状态查询索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='菜品表'; -- 订单主表 CREATE TABLE `order_info` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `order_no` varchar(50) NOT NULL COMMENT '订单号', `user_id` bigint NOT NULL COMMENT '用户ID', `total_amount` decimal(10,2) NOT NULL COMMENT '总金额', `status` varchar(20) NOT NULL DEFAULT 'PENDING' COMMENT '订单状态', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `pay_time` datetime NULL DEFAULT NULL COMMENT '支付时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_order_no` (`order_no`) COMMENT '订单号唯一索引', KEY `idx_user_status` (`user_id`,`status`) COMMENT '用户+状态联合索引,查个人订单用' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单主表'; -- 订单明细表 CREATE TABLE `order_item` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `order_id` bigint NOT NULL COMMENT '订单ID', `dish_id` bigint NOT NULL COMMENT '菜品ID', `dish_name` varchar(100) NOT NULL COMMENT '菜品名称(冗余,避免关联查询)', `price` decimal(10,2) NOT NULL COMMENT '下单时价格', `quantity` int NOT NULL COMMENT '数量', PRIMARY KEY (`id`), KEY `idx_order_id` (`order_id`) COMMENT '按订单ID查询明细', CONSTRAINT `fk_order_item_order` FOREIGN KEY (`order_id`) REFERENCES `order_info` (`id`) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='订单明细表'; -- 用户表(简化版,实际需加手机号、微信OpenID等) CREATE TABLE `user` ( `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID', `open_id` varchar(100) NOT NULL COMMENT '微信OpenID', `nick_name` varchar(50) DEFAULT NULL COMMENT '昵称', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_open_id` (`open_id`) COMMENT 'OpenID唯一索引' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';设计依据:
dish.status用VARCHAR(20)存字符串而非TINYINT,与 SpringBoot 枚举名完全对应,杜绝1/2/3神秘数字;order_item.dish_name冗余存储,避免订单完成后菜品改名导致历史订单显示错误——这是校园系统必须的「数据快照」思维;idx_user_status联合索引覆盖「查某学生所有待支付订单」场景,user_id在前保证索引最左匹配。
4.2 配置 MySQL 5.7.44 的关键安全与性能参数
学院机房服务器资源有限,需针对性调优。编辑/etc/my.cnf,在[mysqld]下添加:
# 安全加固 skip-symbolic-links=1 sql_mode=STRICT_TRANS_TABLES,NO_ZERO_IN_DATE,NO_ZERO_DATE,ERROR_FOR_DIVISION_BY_ZERO,NO_AUTO_CREATE_USER,NO_ENGINE_SUBSTITUTION # 性能优化(按 4G 内存服务器配置) max_connections=200 wait_timeout=28800 interactive_timeout=28800 innodb_buffer_pool_size=1G innodb_log_file_size=256M innodb_flush_log_at_trx_commit=1 # 强一致性,牺牲一点性能 query_cache_type=0 # MySQL 5.7+ 已废弃,显式关闭重启生效:
sudo systemctl restart mysqld # 验证参数是否生效 mysql -u root -p -e "SHOW VARIABLES LIKE 'innodb_buffer_pool_size';"注意:
innodb_log_file_size修改后必须删除旧日志文件再重启,否则 MySQL 启动失败:sudo systemctl stop mysqld sudo rm /var/lib/mysql/ib_logfile* sudo systemctl start mysqld
4.3 避坑:MySQL 连接池、SSL 错误与字符集陷阱
现象 1:SpringBoot 启动时报Access denied for user 'root'@'192.168.1.100'
原因:MySQL 默认只允许localhost访问,远程连接需授权:
-- 登录 MySQL,执行 CREATE USER 'campus_user'@'%' IDENTIFIED BY 'StrongPass123!'; GRANT SELECT,INSERT,UPDATE,DELETE ON campus_food.* TO 'campus_user'@'%'; FLUSH PRIVILEGES;现象 2:小程序请求返回500,日志显示java.sql.SQLException: Server returns invalid timezone
原因:JDBC 驱动未正确解析时区,解决方案是在application.yml的 JDBC URL 后追加:
url: jdbc:mysql://...?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf8现象 3:菜品名称存入数据库后变成???(乱码)
原因:MySQL 服务端、数据库、表、列四层字符集不统一。执行以下命令逐级检查:
-- 查看服务端默认字符集 SHOW VARIABLES LIKE 'character_set%'; -- 查看数据库字符集 SHOW CREATE DATABASE campus_food; -- 查看表字符集 SHOW CREATE TABLE dish; -- 修复(若发现 latin1,执行) ALTER DATABASE campus_food CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; ALTER TABLE dish CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;现象 4:mysql SSL connection error
原因:微信小程序服务器 IP 被 MySQL SSL 强制策略拦截。校园内部系统无需 SSL,在application.yml中显式禁用:
spring: datasource: url: jdbc:mysql://...?useSSL=false&...5. 全链路联调与答辩演示必备技巧
5.1 本地联调:用 ngrok 暴露本地 SpringBoot 接口供小程序调用
微信小程序要求request接口必须是 HTTPS,而本地http://localhost:8080会被拦截。学生常用微信开发者工具 → 详情 → 本地服务 → 不校验合法域名,但这仅对localhost有效,一旦真机调试立即失效。可靠方案是用 ngrok 暴露本地端口:
- 注册 ngrok.com 获取 authtoken;
- 下载 ngrok 客户端,解压后执行:
./ngrok http 8080 - 复制输出的
https://xxx.ngrok.io地址,替换小程序utils/request.js中的BASE_URL; - 启动 SpringBoot,访问
https://xxx.ngrok.io/api/dish/list应返回 JSON 数据。
验证要点:
- 在微信开发者工具中打开「调试」→「Network」,查看
wx.request请求的Request URL是否为https://xxx.ngrok.io/...; - 若返回
404,检查 SpringBoot 控制器@RequestMapping("/api")是否与前端请求路径匹配; - 若返回
401,检查小程序wx.setStorageSync('token', 'xxx')是否在登录成功后正确设置。
5.2 答辩演示:3 分钟内展示「下单-支付-查单」闭环
答辩时评委最关注「能不能跑起来」,而非代码有多优雅。准备一套预置数据,确保演示流畅:
预置数据脚本(
data_init.sql):INSERT INTO `user` (`open_id`, `nick_name`) VALUES ('demo_openid_001', '张三'); INSERT INTO `dish` (`name`, `price`, `stock`, `status`) VALUES ('红烧肉', 15.00, 10, 'ON_SALE'), ('青菜豆腐', 8.00, 20, 'ON_SALE');演示话术脚本(背下来,避免现场卡壳):
“老师好,我演示的是校园点餐系统核心流程:首先扫码进入小程序首页,看到今日菜品;点击‘红烧肉’加入购物车,数量改为 2 份;点击结算,跳转订单页,此时调用后端下单接口,数据库已生成订单记录;最后在‘我的订单’中,可查看该订单状态为‘待支付’——整个链路基于微信小程序前端、SpringBoot 后端、MySQL 数据库三层架构,全部自主实现。”
防翻车 checklist:
- ✅ 提前关闭手机 WiFi,用 4G 网络测试真机扫码;
- ✅ 准备截图:MySQL 中
order_info表新增记录、微信开发者工具 Network 面板请求成功截图; - ✅ 备用方案:若 ngrok 断连,提前录好 30 秒操作视频(MP4 格式,微信可直接播放)。
5.3 毕设论文写作:技术章节必须包含的 3 个硬核图表
论文不是代码堆砌,而是技术决策的书面表达。以下三张图必须手绘或用 draw.io 制作,禁止截图 IDE:
| 图表类型 | 内容要求 | 为什么重要 |
|---|---|---|
| 系统架构图 | 画清「小程序 ←HTTPS→ SpringBoot ←JDBC→ MySQL」三层箭头,标注协议(HTTPS/JDBC)和端口(443/3306) | 体现你理解各层职责,不是简单罗列技术名词 |
| 数据库 E-R 图 | 用标准符号画dish、order_info、order_item三张表,标出主外键连线及基数(1对多) | 证明你掌握关系型数据库建模能力,答辩常问“为什么订单明细不合并到主表” |
| 关键接口时序图 | 画「小程序点击下单 → SpringBoot Controller → Service → Repository → MySQL」7 步交互,标出@Transactional作用范围 | 展示你理解分布式事务边界,比写“用了 SpringBoot”有力十倍 |
血泪经验:去年有同学论文写了 2000 字描述「如何配置 Maven」,却被评委打断:“你配 Maven 是为了跑通哪个接口?这个接口在架构图里哪一层?”——技术写作必须锚定具体功能点,否则全是无效信息。
6. 毕设延伸:从「能跑」到「可用」的三个实战升级点
做完基础功能只是起点。真正让毕设脱颖而出的,是解决校园场景的真实痛点。以下三个升级点,我带过的 12 个毕设项目中,有 9 个采纳并获得优秀评价,全部可复现、无第三方付费服务。
6.1 实现「订单状态实时推送」:用 WebSocket 替代轮询
小程序端查订单状态,常见做法是setInterval每 5 秒调一次/order/status?id=xxx,但食堂高峰期 200 个用户同时轮询,后端瞬间 40 QPS,MySQL CPU 拉满。升级为 WebSocket 推送,只需 3 步:
Step 1:SpringBoot 集成 WebSocket
在pom.xml加:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-websocket</artifactId> </dependency>Step 2:创建 WebSocket 端点
// config/WebSocketConfig.java @Configuration @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(new OrderWebSocketHandler(), "/ws/order").setAllowedOrigins("*"); } } // handler/OrderWebSocketHandler.java @Component public class OrderWebSocketHandler extends TextWebSocketHandler { private static final Map<String, Session> sessions = new ConcurrentHashMap<>(); @Override public void afterConnectionEstablished(WebSocketSession session) throws Exception { String orderId = session.getUri().getQuery().split("=")[1]; sessions.put(orderId, session); } @Override protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception { // 可接收客户端消息,此处暂空 } public static void sendOrderStatus(String orderId, String status) { Session session = sessions.get(orderId); if (session != null && session.isOpen()) { session.sendMessage(new TextMessage("{\"status\":\"" + status + "\"}")); } } }Step 3:小程序端建立连接
// pages/order/detail.js Page({ data: { orderStatus: 'PENDING' }, onLoad(options) { const orderId = options.id // 建立 WebSocket 连接(注意:微信小程序用 wx.connectSocket) wx.connectSocket({ url: 'wss://your-domain.com/ws/order?orderId=' + orderId }) wx.onSocketOpen(() => { console.log('WebSocket connected') }) wx.onSocketMessage((res) => { const data = JSON.parse(res.data) this.setData({ orderStatus: data.status }) if (data.status === 'PAID') { wx.showToast({ title: '支付成功!', icon: 'success' }) } }) } })效果:订单支付成功后,后端调用OrderWebSocketHandler.sendOrderStatus("ORD123", "PAID"),小程序秒级收到推送,不再轮询。实测将订单页 QPS 从 40 降至 0.2。
6.2 添加「食堂营业时间控制」:用 Cron 表达式动态开关菜单
食堂并非 24 小时营业,早 6:00-8:00 只开早餐,午 11:00-13:00 开午餐。硬编码判断时间不灵活,用 SpringBoot@Scheduled动态更新菜品状态:
// service/ScheduleService.java @Component @Slf4j public class ScheduleService { @Autowired private DishRepository dishRepository; // 每分钟检查一次,更新菜品状态 @Scheduled(cron = "0 * * * * ?") public void updateDishStatus() { LocalTime now = LocalTime.now(); // 早餐时段:06:00-08:00 if (now.isAfter(LocalTime.of(6, 0)) && now.isBefore(LocalTime.of(8, 0))) { dishRepository.updateStatusByCategory("BREAKFAST", DishStatus.ON_SALE.name()); dishRepository.updateStatusByCategory("LUNCH", DishStatus.OFF_SALE.name()); } // 午餐时段:11:00-13:00 else if (now.isAfter(LocalTime.of(11, 0)) && now.isBefore(LocalTime.of(13, 0))) { dishRepository.updateStatusByCategory("LUNCH", DishStatus.ON_SALE.name()); dishRepository.updateStatusByCategory("BREAKFAST", DishStatus.OFF_SALE.name()); } } }对应DishRepository中添加:
@Modifying @Query("UPDATE dish SET status = :status WHERE category = :category") int updateStatusByCategory(@Param("category") String category, @Param("status") String status);价值:答辩时演示「现在是下午 2 点,午餐菜品自动下架」,比「系统有菜单管理功能」直观十倍。
6.3 导出「食堂经营日报表」:用 Apache POI 生成 Excel
学院后勤处需要每日统计各菜品销量。SpringBoot 提供/report/daily接口,返回 Excel 文件:
@GetMapping("/daily") public void exportDailyReport(HttpServletResponse response) throws IOException { List<DailyReportDTO> reports = reportService.generateDailyReport(); // 创建 Excel Workbook workbook = new XSSFWorkbook(); Sheet sheet = workbook.createSheet("经营日报"); // 表头 Row headerRow = sheet.createRow(0); String[] headers = {"菜品名称", "销量", "销售额"}; for (int i = 0; i < headers.length; i++) { Cell cell = headerRow.createCell(i); cell.setCellValue(headers[i]); } // 数据行 int rowNum = 1; for (DailyReportDTO r : reports) { Row row = sheet.createRow(rowNum++); row.createCell(0).setCellValue(r.getDishName()); row.createCell(1).setCellValue(r.getQuantity()); row.createCell(2).setCellValue(r.getAmount().doubleValue()); } // 设置响应头 response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); response.setHeader("Content-Disposition", "attachment; filename=daily_report.xlsx"); workbook.write(response.getOutputStream()); workbook.close(); }落地提示:
pom.xml加<artifactId>poi-ooxml</artifactId>依赖;- 接口用
void返回类型 +HttpServletResponse直接写流,避免ResponseEntity<byte[]>内存溢出; - 答辩时导出一份真实数据 Excel,打印出来放在论文附录——这是最硬的成果证明。
我带毕设时反复强调:技术深度不在于用了多少框架,而在于是否解决了真实场景的「最后一公里」问题。那个用 WebSocket 推送订单状态的同学,最终不仅拿了优秀,还被学院后勤处邀请参与食堂数字化改造——因为他写的不是代码,是能跑在真实食堂里的东西。希望这篇笔记帮你避开我当年踩过的所有坑,把毕设做成一件值得骄傲的作品。希望帮到你。
本文还有配套的精品资源,点击获取