拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java SSM+微信小程序厨艺教学平台毕设实战指南

Java SSM+微信小程序厨艺教学平台毕设实战指南

简介:这是一套面向计算机专业本科生的高分毕业设计级在线厨艺平台实战项目,适用于课程设计、期末大作业及毕设参考,解决高校学生缺乏完整全栈开发案例的痛点。资源包共1043个文件,涵盖88个Java后端核心类、145个JS与115个Vue前端组件、60个WXML与62个WXSS小程序页面样式、227个PNG与162个SVG界面资源,以及2个SQL数据库脚本和3个BAT一键部署脚本,完整支撑SSM+微信小程序双端协同运行,压缩包仅17.71MB,轻量易部署。已有43人学习下载,体现其在教学实践中的初步认可度。用户可直接获取经导师验收通过的可运行系统:含前后端源码、MySQL5.7+建库脚本、Navicat可视化配置说明、IDEA与微信开发者工具适配配置,以及后台管理、菜谱浏览、视频教学、食谱分享、评论收藏等全部功能模块,代码结构清晰、注释规范,便于理解MVC分层逻辑与小程序与SpringBoot接口交互机制。

1. 这不是又一个“微信小程序+SSM”套壳项目:它真能跑通厨艺教学闭环,从菜谱发布、视频上传、用户跟练到学分认证

你搜“Java SSM 微信小程序 毕业设计”,满屏是带“高分”“源码+数据库+论文”的压缩包——但90%解压后连登录都卡在wx.login返回errCode: -1,或者 MySQL 启动就报Unknown column 'create_time' in 'field list'。这个标题里的“在线厨艺平台”不一样:它把「教」和「学」真正拆成了可验证的动作链——老师上传一道「宫保鸡丁」,必须填刀工时长、火候等级、失败预警点(比如“花生炒糊了”);学生跟练时,小程序端调用手机摄像头做3秒动作捕捉(切配/翻炒/装盘),后端用 OpenCV 简单帧差法比对关键帧节奏,再结合手动提交的成品图打分。整套流程不依赖AI大模型,纯靠 Java 业务逻辑+MySQL 约束+小程序原生能力落地。适合计算机专业本科生做毕设——代码量可控(核心模块<8000行)、部署门槛低(Windows Server + JDK8 + MySQL5.7 + 微信开发者工具)、答辩时能现场演示「学生上传跟练视频→教师批注→生成学分证书」全链路。如果你正被导师催进度、被答辩组质疑“业务太假”,这篇就是你打开压缩包前该先读的实操地图。

2. 搭建环境:避开 JDK8 与 MySQL5.7 的经典兼容雷区,用最小配置跑通登录主流程

2.1 JDK8 与 Tomcat8.5 的版本锁死策略

很多同学用 JDK11 或 JDK17 解压即报错java.lang.UnsupportedClassVersionError,因为 SSM 框架(Spring 4.3.x + SpringMVC 4.3.x + MyBatis 3.4.x)编译目标字节码是52.0(JDK8)。必须确认:

java -version # 输出必须是类似:java version "1.8.0_291" # 若为 11 或 17,请卸载并重装 JDK8u333(推荐)

Tomcat 必须选 8.5.x(非 9.x 或 10.x),否则web.xml中<servlet>标签会因 schema 版本不匹配而启动失败。下载地址:https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.99/bin/apache-tomcat-8.5.99.zip(注意:用 8.5.99 而非最新 8.5.100,后者已移除对web.xmlDTD 的宽松解析)。解压后修改conf/server.xml,将默认端口8080改为8081(避免与微信开发者工具调试端口冲突):

<Connector port="8081" protocol="HTTP/1.1" connectionTimeout="20000" redirectPort="8443" />

提示:不要用 IDE 内置 Tomcat 插件——它常自动升级 servlet-api 版本。务必用独立解压的 Tomcat 目录,将webapps/ROOT清空后放入项目target/online-cooking.war(Maven 打包后生成)。

2.2 MySQL5.7 安装与字符集强制统一

微信小程序传中文菜名、步骤描述时若出现乱码,90%是 MySQL 字符集没锁死。安装 MySQL5.7.44(Windows 版)后,必须修改my.ini(位于C:\ProgramData\MySQL\MySQL Server 5.7\):

[client] default-character-set = utf8mb4 [mysql] default-character-set = utf8mb4 [mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_unicode_ci init_connect='SET NAMES utf8mb4' skip-character-set-client-handshake = true

重启 MySQL 服务后,执行:

-- 验证全局设置 SHOW VARIABLES LIKE 'character_set%'; SHOW VARIABLES LIKE 'collation%'; -- 创建数据库时显式指定 CREATE DATABASE online_cooking DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

注意:utf8mb4是必须项!MySQL 的utf8实际只支持 3 字节 UTF-8(不支持 emoji 和部分生僻汉字),而小程序用户昵称、评论常含 emoji,utf8mb4才是安全底线。

2.3 微信小程序开发工具配置与 AppID 绑定

下载「微信开发者工具 Stable 版(v1.06.2312150)」,不要用 Nightly 版——其wx.getPhoneNumberAPI 在模拟器中返回undefined是已知 bug。新建项目时选择「小程序」→ 填写AppID(需在微信公众平台注册小程序获取),模板选「Empty Project」。关键配置在project.config.json:

{ "description": "online-cooking-miniprogram", "packOptions": { "ignore": ["node_modules/**", "dist/**"] }, "setting": { "urlCheck": false, "es6": true, "enhance": true, "postcss": true, "minified": true, "newFeature": true, "coverView": true, "scopeData": true, "compileHotReLoad": false, // 关闭热重载,避免 WebSocket 冲突 "useCompiler": true } }

提示:urlCheck: false是必须项!否则本地调试时wx.request会因未备案域名被拦截。此开关仅影响开发阶段,上线前需在「小程序后台→开发管理→开发版本→服务器域名」中配置合法域名。

3. 数据库设计:用 7 张表撑起厨艺平台核心业务,拒绝过度范式化

3.1 核心表结构与字段约束逻辑

本平台放弃“用户-角色-权限”三级RBAC,改用user_type ENUM('teacher','student','admin')直接区分身份——毕设场景下简化权限控制,且避免user_role表引发 N+1 查询。关键表设计如下(SQL 已适配 MySQL5.7):

表名主要字段设计意图索引建议
t_userid BIGINT PK,open_id VARCHAR(64) NOT NULL,nick_name VARCHAR(32),avatar_url VARCHAR(255),user_type ENUM('teacher','student','admin'),status TINYINT DEFAULT 1open_id为微信唯一标识,不可为空且不设 UNIQUE(同一用户可能换设备重新授权);status=0表示禁用账号KEY idx_openid (open_id),KEY idx_type_status (user_type,status)
t_recipeid BIGINT PK,title VARCHAR(100) NOT NULL,teacher_id BIGINT NOT NULL,difficulty TINYINT CHECK(difficulty BETWEEN 1 AND 5),cooking_time INT COMMENT '分钟',cover_img VARCHAR(255)difficulty用 CHECK 约束而非外键,避免冗余表;cooking_time存整数分钟,前端转为“X小时Y分钟”KEY idx_teacher (teacher_id),KEY idx_difficulty (difficulty)
t_stepid BIGINT PK,recipe_id BIGINT NOT NULL,step_order TINYINT NOT NULL,content TEXT,video_url VARCHAR(255),duration_sec INT COMMENT '视频时长秒'step_order保证步骤顺序,不设 AUTO_INCREMENT(避免插入中间步骤时序错乱);video_url存 COS 或七牛云直链UNIQUE KEY uk_recipe_order (recipe_id,step_order),KEY idx_recipe (recipe_id)
t_practice_recordid BIGINT PK,student_id BIGINT NOT NULL,recipe_id BIGINT NOT NULL,submit_time DATETIME,video_url VARCHAR(255),status ENUM('pending','graded','rejected') DEFAULT 'pending'status控制流程状态机:pending→graded(教师批阅)→rejected(退回重练);无外键关联t_user(防级联删除导致记录丢失)KEY idx_student (student_id),KEY idx_recipe_status (recipe_id,status)

注意:所有VARCHAR字段长度按实际需求设定(如nick_name最长32字,微信昵称限制),避免盲目设VARCHAR(255)浪费索引空间;TEXT类型仅用于content等可能超长字段,不用于搜索字段。

3.2 初始化数据脚本的关键补丁

解压包中的init.sql常遗漏两个致命补丁:

  1. 管理员账号初始化:t_user表必须有一条user_type='admin'记录,否则后台登录页login.jsp会因SELECT * FROM t_user WHERE user_type='admin'返回空而跳转失败。补丁 SQL:
INSERT INTO t_user (id, open_id, nick_name, avatar_url, user_type, status, create_time) VALUES (1, 'admin_openid_123', '系统管理员', 'https://example.com/avatar.png', 'admin', 1, NOW());
  1. 微信登录临时码校验表:小程序端wx.login()获取code后,Java 后端需调用微信接口换取session_key。此过程需临时存储code防重放,但原包常缺表。补丁 SQL:
CREATE TABLE t_login_code ( id BIGINT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL COMMENT '微信登录code', expire_time DATETIME NOT NULL COMMENT '过期时间,设为10分钟', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_code (code), KEY idx_expire (expire_time) );

提示:t_login_code表需配合定时任务清理过期记录(每5分钟执行DELETE FROM t_login_code WHERE expire_time < NOW()),否则表体积膨胀。

4. SSM 层关键业务实现:用 MyBatis 动态 SQL 处理菜谱多条件检索,绕开 PageHelper 分页陷阱

4.1 菜谱检索接口:动态 SQL 构建真实业务查询

用户搜索“川菜”“30分钟内”“难度≤3”的菜谱,传统WHERE拼接易出 SQL 注入,而 PageHelper 的startPage在复杂条件组合下常漏查。本方案用 MyBatis<if>标签构建动态 SQL:

<!-- RecipeMapper.xml --> <select id="selectByCondition" resultType="com.example.entity.Recipe"> SELECT r.*, u.nick_name AS teacher_name FROM t_recipe r LEFT JOIN t_user u ON r.teacher_id = u.id WHERE 1=1 <if test="keyword != null and keyword != ''"> AND (r.title LIKE CONCAT('%', #{keyword}, '%') OR r.description LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="difficulty != null and difficulty > 0"> AND r.difficulty &lt;= #{difficulty} </if> <if test="maxTime != null and maxTime > 0"> AND r.cooking_time &lt;= #{maxTime} </if> <if test="category != null and category != ''"> AND r.category = #{category} </if> ORDER BY r.create_time DESC </select>

Controller 层接收参数并调用:

@GetMapping("/recipes") public Result<List<Recipe>> searchRecipes( @RequestParam(required = false) String keyword, @RequestParam(required = false, defaultValue = "0") Integer difficulty, @RequestParam(required = false, defaultValue = "0") Integer maxTime, @RequestParam(required = false) String category) { RecipeQuery query = new RecipeQuery(); query.setKeyword(keyword); query.setDifficulty(difficulty); query.setMaxTime(maxTime); query.setCategory(category); List<Recipe> recipes = recipeService.selectByCondition(query); return Result.success(recipes); }

逻辑说明:RecipeQuery是专门封装查询条件的 DTO,避免 Controller 参数过多;&lt;=是 XML 实体转义,防止解析错误;LEFT JOIN关联教师昵称,减少前端额外请求。

4.2 微信手机号获取:getPhoneNumber接口的安全校验链

小程序端调用wx.getPhoneNumber获取加密数据后,Java 后端必须完成三步校验:

  1. 解密 session_key:用AESUtil.decrypt解密encryptedData(需引入commons-codec依赖);
  2. 校验 signature:将rawData+session_keySHA1 加密,与signature比对;
  3. 绑定手机号到用户:更新t_user表的phone字段,必须加乐观锁防止并发覆盖:
@Update("UPDATE t_user SET phone = #{phone}, update_time = NOW() " + "WHERE id = #{userId} AND version = #{version}") int updatePhone(@Param("phone") String phone, @Param("userId") Long userId, @Param("version") Integer version);

对应t_user表需增加version INT DEFAULT 0字段,每次更新version = version + 1。若updatePhone返回 0,说明版本冲突,需重试或提示用户刷新页面。

4.3 跟练记录状态机:用数据库事务保证状态流转原子性

学生提交跟练视频后,状态从pending变为graded,教师批阅时需同时更新t_practice_record和生成t_grade记录。MyBatis 无法跨表事务,必须用 Spring@Transactional:

@Service public class PracticeService { @Transactional(rollbackFor = Exception.class) public void gradePractice(Long recordId, Integer score, String comment) { // 1. 更新跟练记录状态 PracticeRecord record = new PracticeRecord(); record.setId(recordId); record.setStatus("graded"); record.setScore(score); record.setComment(comment); record.setGradeTime(new Date()); practiceRecordMapper.updateByPrimaryKeySelective(record); // 2. 插入评分记录(关联教师ID) Grade grade = new Grade(); grade.setRecordId(recordId); grade.setTeacherId(getCurrentTeacherId()); // 从 ThreadLocal 获取当前教师 grade.setScore(score); grade.setComment(comment); gradeMapper.insert(grade); // 3. 更新学生学分(触发器或此处直接更新) Student student = studentMapper.selectByPrimaryKey(record.getStudentId()); student.setCredit(student.getCredit() + score * 10); // 每分折算10学分 studentMapper.updateByPrimaryKeySelective(student); } }

注意:@Transactional必须作用于 public 方法,且调用方不能是本类内部方法(否则 AOP 失效);getCurrentTeacherId()通过SecurityContextHolder或自定义TeacherContext获取,避免硬编码。

5. 小程序端避坑指南:解决视频上传失败、顶部导航栏错位、登录态丢失三大高频问题

5.1 视频上传失败:wx.uploadFile的 size 与 duration 双重校验

小程序上传跟练视频时,常报错uploadFile:fail network error,表面是网络问题,实则因:

  • 服务端未配置文件上传大小:Tomcatconf/web.xml中<max-file-size>默认为 0(禁用上传),需改为:
<multipart-config> <max-file-size>104857600</max-file-size> <!-- 100MB --> <max-request-size>104857600</max-request-size> <file-size-threshold>0</file-size-threshold> </multipart-config>
  • 前端未校验视频时长与大小:用户上传 5 分钟 4K 视频必然失败。小程序端需预检:
// 上传前获取视频信息 wx.chooseVideo({ sourceType: ['album', 'camera'], compressed: true, // 强制压缩 maxDuration: 60, // 限制最长60秒 success: (res) => { const { tempFilePath, duration, size } = res; if (duration > 60) { wx.showToast({ title: '视频不能超过60秒', icon: 'none' }); return; } if (size > 50 * 1024 * 1024) { // 50MB wx.showToast({ title: '视频不能超过50MB', icon: 'none' }); return; } // 执行上传 wx.uploadFile({ url: 'https://your-domain.com/api/practice/upload', filePath: tempFilePath, name: 'video', formData: { recordId: this.data.recordId }, success: (uploadRes) => { console.log('上传成功'); } }); } });

5.2 顶部导航栏高度异常:statusBarHeight与navigationBarHeight的精准计算

iPhone X 及以上机型状态栏高度为 44px,但wx.getSystemInfoSync().statusBarHeight返回 44,navigationBarHeight却是 88(含状态栏),导致自定义导航栏遮挡内容。正确做法:

// app.js 中全局注入 App({ onLaunch() { const systemInfo = wx.getSystemInfoSync(); this.globalData.navBarHeight = systemInfo.statusBarHeight + 44; // 44 是导航栏固定高度 } }); // 页面 WXML 中使用 <view class="nav-bar" style="height: {{appData.navBarHeight}}px;"> <view class="status-bar" style="height: {{appData.statusBarHeight}}px;"></view> <view class="nav-title">我的跟练</view> </view>

CSS 配置:

.nav-bar { position: fixed; top: 0; left: 0; right: 0; z-index: 999; } .status-bar { background-color: #f8f8f8; } .nav-title { height: 44px; line-height: 44px; text-align: center; font-weight: bold; }

5.3 登录态丢失:wx.checkSession失效后的优雅降级

小程序wx.login获取的code5分钟过期,session_key2小时过期,但用户长时间停留后wx.checkSession()常返回fail。此时不应直接跳转登录页,而应:

  1. 静默刷新:调用wx.login()获取新code,后端用新code换取新session_key并续期用户 token;
  2. Token 续期机制:后端LoginController增加/refresh-token接口,接收旧 token 和新code,验证open_id一致后颁发新 token;
  3. 前端拦截器:在app.js的onShow中检查 token 有效期(存于wx.setStorageSync),剩余 <30 分钟则主动刷新:
// app.js onShow() { const token = wx.getStorageSync('token'); const expireTime = wx.getStorageSync('token_expire'); if (token && expireTime && Date.now() < expireTime - 30 * 60 * 1000) { // token 有效,无需操作 } else { // 刷新 token wx.login({ success: (res) => { wx.request({ url: 'https://your-domain.com/api/auth/refresh-token', method: 'POST', data: { code: res.code, token: token }, success: (r) => { wx.setStorageSync('token', r.data.token); wx.setStorageSync('token_expire', r.data.expireTime); } }); } }); } }

6. 毕设答辩实战技巧:用 3 个可演示细节证明你真做过,而不是套壳搬运

6.1 演示「教师批阅跟练」时,故意触发一次「退回重练」

答辩时别只演示“提交→通过→发证书”的理想流。现场操作:让学生账号提交一段明显切配不匀的视频(用手机拍3秒土豆丝),教师端批阅时输入评语“刀工不均匀,请重练第2步”,点击“退回”。此时学生端立即收到模板消息(需提前在微信公众平台开通模板消息功能),并在首页“待重练”Tab 出现新条目。这个细节证明:

  • 你实现了状态机闭环(pending→rejected→pending);
  • 你配置了微信模板消息(t_template_msg表及发送逻辑);
  • 你处理了前端 Tab 切换的数据隔离(tabbar页面用onShow重新拉取status=rejected记录)。

6.2 展示「MySQL 数据一致性」:用SELECT ... FOR UPDATE模拟并发抢课

厨艺平台有“热门课程限10人报名”需求,原包常简单用UPDATE t_course SET enrolled_count = enrolled_count + 1 WHERE id = ? AND enrolled_count < 10,这在并发下会超限。答辩时打开两个微信开发者工具实例,同时点击同一门课的“报名”,然后执行:

-- 查看锁等待 SELECT * FROM information_schema.INNODB_TRX; SELECT * FROM information_schema.INNODB_LOCK_WAITS;

展示trx_state='LOCK WAIT'的事务,并说明你用SELECT * FROM t_course WHERE id = 123 FOR UPDATE加行锁,再判断enrolled_count,确保绝对不超限。这比讲“我用了事务”有力十倍。

6.3 用EXPLAIN分析慢查询,现场优化一条 SQL

找到t_recipe表上未走索引的查询(如按category和difficulty组合查询),执行:

EXPLAIN SELECT * FROM t_recipe WHERE category = '川菜' AND difficulty <= 3;

若type=ALL(全表扫描),当场添加复合索引:

ALTER TABLE t_recipe ADD INDEX idx_category_diff (category, difficulty);

再执行EXPLAIN,展示type=ref和key=idx_category_diff。评委立刻明白你懂索引原理,不是只会复制粘贴。

我带过三届毕设,最常看到学生答辩时被问“你这个分页怎么做的”,一说“用 PageHelper”,评委就摇头——因为 PageHelper 的count查询在复杂条件组合下常不准。后来我要求学生必须手写COUNT(*)子查询,哪怕多写20行代码。技术深度不在框架多炫,而在你敢不敢掀开它的底裤看一眼。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表