ARTICLE DETAIL

资讯详情

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

SSM+微信小程序厨艺教学系统实战部署指南

SSM+微信小程序厨艺教学系统实战部署指南 简介这是一套面向计算机专业本科生的高分毕业设计级在线厨艺平台实战项目适用于课程设计、期末大作业及毕设参考解决传统厨艺学习资源分散、互动性弱、缺乏系统化平台支撑的问题。资源包共1043个文件涵盖88个Java后端核心类、145个JS与115个Vue前端组件、60个WXML与62个WXSS小程序页面样式、227个PNG与162个SVG图标资源以及2个SQL建库脚本和配套bat部署脚本完整呈现SSMMySQL微信小程序全栈开发结构压缩包大小为17.71MB。已有43人学习下载体现其在教学实践中的初步认可度。用户可直接导入IDEA与微信开发者工具运行获得含用户登录、菜谱浏览、视频教学、食谱分享、评论收藏、后台内容管理等全功能闭环系统项目经导师指导与严格调试附带Navicat数据库操作指引与Maven构建说明目录模块划分清晰便于理解前后端通信机制与小程序生命周期设计。1. 这不是又一个“仿美团”小程序它把厨艺教学拆成了可落地的闭环服务链你见过多少个「在线厨艺平台」毕业设计十有八九是首页轮播图菜谱列表评论区后台连用户收藏都没做持久化。但这份基于 Java SSM MySQL 微信小程序的源码包真实跑通了「学一道菜→看视频→记笔记→练手拍照→老师点评→生成学习报告」的完整教学闭环——不是 Demo是能直接部署、注册、上传菜品视频、发起线上点评会的生产级最小可行系统MVP。它用 SSM 框架把教务逻辑压进三层架构Controller 层处理微信小程序端的code2Session登录态校验和手机号绑定Service 层封装了「按难度/食材/时长」三维度交叉筛选菜谱的复合查询Mapper 层则用 MyBatis 动态 SQL 实现了「用户历史练习记录 → 推荐相似难度新菜」的冷启动推荐。适合正在赶毕设 deadline 的计算机/软工本科生也适合想快速验证「轻量级技能教学 SaaS」模式的个人开发者——它不堆炫技功能没 WebSocket 实时弹幕、没 AI 图像识别切菜动作但每个模块都经得起调试器单步跟踪。提示这不是 Spring Boot 项目是标准 SSMSpring 4.3.x SpringMVC 4.3.x MyBatis 3.4.x结构JDK 要求 1.8MySQL 版本兼容 5.7 和 8.0建表语句已适配utf8mb4字符集。微信小程序端使用原生开发非 UniApp所有 API 均通过wx.request调用后端/api/**接口无任何第三方 SDK 依赖。2. 从零部署数据库建模、SSM 工程配置与小程序环境联调三步落地2.1 数据库初始化看清 17 张表如何支撑「教-学-评-管」四维业务解压后的database/目录下包含chuyi_platform.sql文件这是经过实际运行验证的全量建表脚本非 ER 图生成的空壳。重点不是表数量而是字段设计意图表名关键字段设计意图user_infoopenid VARCHAR(64),unionid VARCHAR(64),phone VARCHAR(11)微信登录态三重标识openid用于小程序内唯一识别unionid预留公众号打通phone为脱敏存储仅存后 4 位完整号存在加密字段phone_encryptedcourse_videovideo_url TEXT,cover_img_url VARCHAR(255),difficulty TINYINT DEFAULT 2视频地址存 CDN URL非本地路径封面图强制要求https://开头小程序image组件限制difficulty用 1~5 整数表示入门到大师级practice_recorduser_id BIGINT,course_id BIGINT,photo_urls JSON,score DECIMAL(3,1)用户练习记录表photo_urls存 JSON 数组如[https://cdn/1.jpg,https://cdn/2.jpg]score由教师手动录入支持小数点后一位teacher_commentrecord_id BIGINT,teacher_id BIGINT,content TEXT,is_public TINYINT DEFAULT 0点评内容表is_public1表示该点评允许其他学员查看用于优秀作业展示执行建库命令前务必确认-- 创建数据库字符集必须为 utf8mb4 CREATE DATABASE chuyi_platform CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 导入 SQLLinux/macOS 下 mysql -u root -p chuyi_platform chuyi_platform.sql -- Windows 下推荐用 Navicat 或 MySQL Workbench 执行避免 DOS 编码问题注意SQL 文件中INSERT INTO user_info的初始管理员账号密码为admin/123456明文首次部署后必须立即修改否则存在越权风险。修改方式UPDATE user_info SET password e10adc3949ba59abbe56e057f20f883e WHERE username admin;MD5 加密后的1234562.2 SSM 后端工程Spring 配置文件里的 4 处关键修改项目结构为标准 Maven 多模块chuyi-web、chuyi-service、chuyi-dao、chuyi-common但chuyi-web/src/main/resources/下的配置文件需针对性调整①jdbc.properties—— 数据库连接池参数jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/chuyi_platform?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrueuseSSLfalse jdbc.usernameroot jdbc.passwordyour_password_here # 关键maxActive 必须 ≥ 20否则高并发时出现 Could not get JDBC Connection jdbc.maxActive30 jdbc.maxIdle10说明serverTimezoneAsia/Shanghai解决 MySQL 8.0 时区报错allowPublicKeyRetrievaltrue是 8.0.21 版本必需参数maxActive30是实测阈值——当小程序同时打开 15 个菜品详情页时若设为 10会出现连接等待超时。②spring-mvc.xml—— 微信小程序接口白名单!-- 放行 /api/login、/api/user/bindPhone 等微信相关接口 -- mvc:resources mapping/api/** location/api/ / !-- 但禁止直接访问静态资源目录 -- mvc:interceptors mvc:interceptor mvc:mapping path/api/**/ bean classcom.chuyi.interceptor.AuthInterceptor/ /mvc:interceptor /mvc:interceptors说明AuthInterceptor是自定义拦截器校验请求头X-Auth-Token由小程序wx.login()获取 code 后后端调用微信sns/jscode2session接口生成的 session_key 加密 token未携带或过期则返回401 Unauthorized。③log4j.properties—— 日志分级埋点# 将微信登录日志单独输出便于排查 openid 绑定失败 log4j.logger.com.chuyi.controller.LoginControllerDEBUG,wechatLogin log4j.appender.wechatLoginorg.apache.log4j.DailyRollingFileAppender log4j.appender.wechatLogin.File${webapp.root}/logs/wechat-login.log log4j.appender.wechatLogin.DatePattern.yyyy-MM-dd说明wechat-login.log文件会记录每次code2Session请求的原始响应含openid、session_key、unionid当小程序端提示「登录失败」时直接查此日志比翻catalina.out高效 10 倍。④web.xml—— 字符编码过滤器位置!-- 必须放在所有 filter 之前否则 POST 请求中文参数乱码 -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping2.3 微信小程序端project.config.json与app.js的 3 处硬编码替换小程序源码在weixin-miniprogram/目录不可直接上传体验版必须修改以下位置①project.config.json中的appid{ description: 在线厨艺平台, packOptions: {}, setting: { urlCheck: true, es6: true, enhance: true, postcss: true, minified: true, newFeature: true }, compileType: miniprogram, libVersion: 2.28.3, appid: wx1234567890abcdef, // ← 替换为你自己的小程序 AppID projectname: chuyi-platform, debugOptions: { hidedInDevtools: [] } }②app.js中的 API 基础路径// app.js 第 12 行 App({ globalData: { // 修改为你的服务器域名必须 HTTPS baseUrl: https://your-domain.com/api/, // ← 关键不能写 http://localhost:8080 // 若用本地测试需在微信开发者工具「详情」→「本地服务」开启并设为 http://127.0.0.1:8080/api/ } })注意微信小程序强制要求request接口域名备案且 HTTPS本地调试时必须勾选「不校验合法域名、web-view业务域名、TLS 版本以及 HTTPS 证书」仅限开发环境。③pages/login/login.js中的登录态校验逻辑// login.js 第 45 行获取手机号成功后的回调 wx.getPhoneNumber({ success: (res) { const encryptedData res.encryptedData; const iv res.iv; // 发送至后端 /api/user/bindPhone 接口注意此处需传入当前用户的 openid从 globalData 获取 wx.request({ url: getApp().globalData.baseUrl user/bindPhone, method: POST, data: { encryptedData: encryptedData, iv: iv, openid: getApp().globalData.openid // ← 此处必须确保 openid 已通过 wx.login() 获取并缓存 }, success: (res) { if (res.data.code 200) { wx.switchTab({ url: /pages/index/index }) } } }) } })说明bindPhone接口会调用微信auth.getPhoneNumber接口解密手机号后端需配置appid和secret在config.properties中解密失败时返回400 Bad Request并带错误码。3. 核心业务链路从微信登录到生成学习报告的 7 个关键接口3.1 微信登录/api/login接口的三次握手小程序端wx.login()获取code→ 后端调用微信sns/jscode2session→ 生成本地token并返回。这个过程看似简单但实际藏着三个易错点①jscode2session返回的openid与unionid关系若用户只在小程序授权unionid为空因未绑定公众号若用户已关注同主体公众号unionid才有值项目中user_info.unionid字段允许 NULL但openid必填因此登录逻辑优先用openid查用户不存在则新建②token生成策略// LoginController.java public Result login(RequestBody MapString, String params) { String code params.get(code); // 1. 调用微信接口获取 session_key 和 openid String url https://api.weixin.qq.com/sns/jscode2session? appid wechatAppId secret wechatSecret js_code code grant_typeauthorization_code; JSONObject resp HttpClientUtil.doGet(url); // 自封装 HTTP 工具类 String openid resp.getString(openid); String sessionKey resp.getString(session_key); // 2. 生成 tokenopenid session_key 时间戳 MD5 String token DigestUtils.md5Hex(openid sessionKey System.currentTimeMillis()); // 3. 写入 Redis有效期 2 小时 redisTemplate.opsForValue().set(token: token, openid, 2, TimeUnit.HOURS); return Result.success(token); }说明token不是 JWT而是简单字符串避免引入额外依赖Redis key 命名为token:xxx方便批量清理session_key仅用于生成 token绝不存库微信安全规范。3.2 手机号绑定/api/user/bindPhone的解密陷阱微信小程序getPhoneNumber返回的encryptedData是 AES-128-CBC 加密需用session_key解密。项目中WeChatUtil.decryptPhoneNumber()方法已实现但必须注意session_key必须与code2Session获取的完全一致不能跨次复用iv是 Base64 编码解密前需Base64.decode(iv)解密后 JSON 字符串中purePhoneNumber字段才是真实手机号phoneNumber含区号前缀// WeChatUtil.java public static String decryptPhoneNumber(String encryptedData, String iv, String sessionKey) { try { byte[] keyBytes Base64.decode(sessionKey); // sessionKey 是 Base64 编码 byte[] ivBytes Base64.decode(iv); byte[] dataBytes Base64.decode(encryptedData); SecretKeySpec keySpec new SecretKeySpec(keyBytes, AES); IvParameterSpec ivSpec new IvParameterSpec(ivBytes); Cipher cipher Cipher.getInstance(AES/CBC/PKCS5Padding); cipher.init(Cipher.DECRYPT_MODE, keySpec, ivSpec); byte[] result cipher.doFinal(dataBytes); String jsonStr new String(result, StandardCharsets.UTF_8); JSONObject obj JSONObject.parseObject(jsonStr); return obj.getString(purePhoneNumber); // ← 关键不是 phoneNumber } catch (Exception e) { log.error(手机号解密失败, e); return null; } }3.3 菜品学习报告生成/api/report/generate的数据聚合逻辑用户完成一道菜的「观看视频→上传练习照片→获得教师评分」后调用此接口生成 PDF 报告。核心是ReportService.generateReport(Long userId, Long courseId)方法步骤 1关联查询course_video课程信息、practice_record练习记录、teacher_comment点评内容步骤 2用 iText 7 生成 PDFchuyi-common模块已集成itext7-kernel和itext7-layout步骤 3PDF 文件存入upload/report/目录返回相对路径供小程序wx.downloadFile// ReportService.java public String generateReport(Long userId, Long courseId) { // 1. 查询基础数据 CourseVideo course courseVideoMapper.selectById(courseId); PracticeRecord record practiceRecordMapper.selectByUserIdAndCourseId(userId, courseId); ListTeacherComment comments teacherCommentMapper.selectByRecordId(record.getId()); // 2. 构建 PDF String fileName report_ userId _ courseId _ System.currentTimeMillis() .pdf; String filePath /upload/report/ fileName; PdfWriter writer new PdfWriter(filePath); PdfDocument pdf new PdfDocument(writer); Document document new Document(pdf); // 添加标题、课程名、用户头像从 user_info.avatar_url 获取、练习照片最多 3 张、教师点评 document.add(new Paragraph(【厨艺学习报告】).setBold().setFontSize(16)); document.add(new Paragraph(课程 course.getTitle())); // ... 其他内容省略详见源码 ReportService.java 第 87 行 document.close(); return /report/ fileName; // 返回可被小程序访问的路径 }说明PDF 生成不依赖外部服务纯 Java 实现图片路径需为绝对 URL如https://your-domain.com/upload/photo/xxx.jpg否则 iText 无法加载。4. 避坑指南SSM 微信小程序联调中 5 个血泪经验总结4.1 现象小程序wx.request返回404但后端 Tomcat 日志显示请求未到达 Controller原因微信小程序request的url参数未加协议头如写成url: /api/login相对路径而baseUrl配置为https://domain.com/api/导致最终请求地址为https://domain.com/api//api/login双斜杠解决检查app.js中baseUrl末尾是否带/若带则接口调用时url应写为login不带/若不带则写为/login。统一约定baseUrl必须以/结尾所有接口url去掉开头/如wx.request({ url: user/bindPhone })4.2 现象MySQL 8.0 启动后SSM 项目报错java.sql.SQLException: Unknown system variable query_cache_size原因MyBatis 3.4.x 默认使用的mysql-connector-java版本过低5.1.x不兼容 MySQL 8.0 的系统变量解决修改pom.xml强制升级驱动dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.28/version !-- 必须 ≥ 8.0.16 -- /dependency注意同时需在jdbc.url中添加allowPublicKeyRetrievaltrue参数否则连接拒绝。4.3 现象小程序上传练习照片后后端MultipartFile接收为空file.isEmpty() true原因微信小程序wx.uploadFile的name参数必须为file与后端RequestParam(file) MultipartFile file的参数名严格一致解决检查小程序端代码wx.uploadFile({ url: https://your-domain.com/api/practice/upload, filePath: tempFilePath, name: file, // ← 必须是 file不能是 photo 或 image formData: { userId: 123, courseId: 456 }, success: (res) { console.log(res) } })4.4 现象教师点评提交后小程序端显示「提交成功」但数据库teacher_comment表无新增记录原因TeacherCommentController.submitComment()方法中RequestBody TeacherComment comment的recordId字段未被前端正确赋值导致 MyBatisINSERT语句record_id为 NULL而该字段在数据库中为NOT NULL解决前端提交前必须确保recordId存在// pages/comment/comment.js submitComment() { const recordId this.data.recordId; // 从上一页传递的 practice_record.id if (!recordId) { wx.showToast({ title: 记录ID丢失, icon: error }); return; } wx.request({ url: getApp().globalData.baseUrl teacher/comment/submit, method: POST, data: { recordId: recordId, content: this.data.content, isPublic: this.data.isPublic } }) }4.5 现象Tomcat 启动后访问http://localhost:8080/api/login返回405 Method Not Allowed原因LoginController.login()方法缺少PostMapping注解仅用RequestMapping(/login)默认接受 GET 请求而小程序wx.request默认为 POST解决明确指定请求方法RestController RequestMapping(/api) public class LoginController { PostMapping(/login) // ← 必须加 PostMapping public Result login(RequestBody MapString, String params) { // ... } }补充若用RequestMapping(value /login, method RequestMethod.POST)也可但PostMapping更简洁。5. 进阶技巧用 Redis 缓存加速菜谱搜索把响应时间从 800ms 降到 80ms5.1 为什么菜谱搜索需要缓存/api/course/search接口支持按「难度」「食材」「烹饪时长」三条件组合筛选SQL 如下SELECT * FROM course_video WHERE difficulty BETWEEN #{minDifficulty} AND #{maxDifficulty} AND (FIND_IN_SET(牛肉, ingredients) 0 OR FIND_IN_SET(猪肉, ingredients) 0) AND duration #{maxDuration} ORDER BY create_time DESC LIMIT 20;当ingredients字段存储为逗号分隔字符串如牛肉,胡萝卜,洋葱时FIND_IN_SET无法走索引10 万条数据下平均耗时 780ms。而用户每切换一次筛选条件就触发一次体验极差。5.2 缓存方案设计二级缓存 布隆过滤器防穿透项目采用「Redis 本地 Caffeine 缓存」二级结构但更关键的是缓存键的设计缓存键格式示例说明search:difficulty:2-4:ingredients:牛肉,猪肉:duration:30search:difficulty:1-3:ingredients:鸡肉,土豆:duration:20将筛选条件拼接为字符串MD5 后作为 Redis Keysearch:hot:1search:hot:1热门搜索词 Top10用ZSET存储分数为搜索次数缓存更新策略新增菜谱时清空所有search:*相关 key用redisTemplate.delete(search:*)每日 2:00 定时任务将昨日搜索 Top10 写入search:hot:*5.3 实现代码CourseSearchService.searchWithCache()Service public class CourseSearchService { Autowired private RedisTemplateString, Object redisTemplate; Autowired private CourseVideoMapper courseVideoMapper; public ListCourseVideo searchWithCache(SearchParam param) { // 1. 生成缓存 key String cacheKey generateCacheKey(param); // 2. 先查本地缓存Caffeine10s 过期 ListCourseVideo localCache localCache.getIfPresent(cacheKey); if (localCache ! null) { return localCache; } // 3. 再查 Redis ValueOperationsString, Object ops redisTemplate.opsForValue(); Object cached ops.get(cacheKey); if (cached ! null) { ListCourseVideo list (ListCourseVideo) cached; // 写入本地缓存减少 Redis 访问 localCache.put(cacheKey, list); return list; } // 4. 查库并写缓存 long start System.currentTimeMillis(); ListCourseVideo result courseVideoMapper.search(param); long cost System.currentTimeMillis() - start; log.info(DB search cost: {}ms, size: {}, cost, result.size()); // 5. 双写缓存Redis2小时 本地10秒 ops.set(cacheKey, result, 2, TimeUnit.HOURS); localCache.put(cacheKey, result); return result; } private String generateCacheKey(SearchParam param) { // 对参数排序再拼接避免相同条件不同顺序产生不同 key String ingredients String.join(,, Arrays.stream(param.getIngredients()) .sorted().toArray(String[]::new)); return search:difficulty: param.getMinDifficulty() - param.getMaxDifficulty() :ingredients: ingredients :duration: param.getMaxDuration(); } }说明localCache是 Caffeine 缓存chuyi-common模块已配置容量 1000过期时间 10 秒Redis 缓存 2 小时兼顾实时性与性能。5.4 效果验证压测对比数据用 JMeter 模拟 100 并发用户连续请求/api/course/search参数固定结果如下方案平均响应时间90% 响应时间错误率CPU 使用率未启用缓存782ms1240ms0%82%仅 Redis 缓存115ms180ms0%45%Redis Caffeine 二级缓存78ms112ms0%28%从那以后我每次上线新搜索功能都强制走一遍generateCacheKey()的单元测试确保参数排序逻辑不变——因为去年有次改了ingredients数组顺序导致缓存击穿凌晨三点被报警电话叫醒。希望帮到你。本文还有配套的精品资源点击获取
返回列表