ARTICLE DETAIL

资讯详情

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

Java SSM+微信小程序健身毕设实战指南

Java SSM+微信小程序健身毕设实战指南 简介这是一套面向计算机专业本科生的毕业设计实战资源聚焦微信生态下的健身服务场景基于SSM后端框架与Vue管理界面配合微信小程序前端实现完整MVC架构系统。资源适用于Java Web开发初学者及毕设选题学生解决健身信息管理、用户互动与多角色协同等典型业务需求。压缩包共1554个文件76.39MB涵盖152个Java后端逻辑类、161个Vue管理页面组件、237个JS小程序逻辑脚本、122个WXML/WXSS视图文件、319张PNG界面截图及2个MP4演示视频辅以SQL建表脚本、答辩PPT、开题报告与环境部署教程。已有150人学习下载提供从开发环境搭建JDK1.8Eclipse/IDEAMySQL、前后端联调到功能测试的全流程支撑源码结构清晰角色权限分离明确含用户打卡、热量管理、论坛发帖、健身房项目发布与管理员多级审核等核心模块具备直接复用与二次开发基础。1. 毕业设计选题落地为什么“Java SSM 微信小程序”组合在健身类毕设中既稳妥又出彩你不是在写一个“能跑起来的小程序”而是在交付一套可演示、可讲解、可答辩、可延展的完整闭环系统——后端用 Java 写业务逻辑和数据管理用 SSMSpring SpringMVC MyBatis搭稳骨架前端用微信原生小程序实现用户触达覆盖注册、运动打卡、课程预约、数据看板等真实场景。这个组合不是为了炫技而是因为它避开了 Spring Boot 自动配置带来的黑盒感答辩时讲不清原理绕开了 uni-app 多端适配的冗余成本毕设只跑微信端同时又比纯 Java Web 页面微信 WebView 方案更符合“小程序”命题要求。尤其对健身类需求——用户高频交互如计时打卡、强状态管理如训练计划进度、需本地缓存离线查看历史记录——原生小程序的wx.getStorageSync SSM 的事务控制 MySQL 的定时任务如自动续费提醒三者咬合得非常紧。我带过 12 届毕设凡是选这个技术栈的同学85% 能在答辩前两周完成全部功能文档视频录制剩下 15% 翻车全栽在「登录态穿透」和「小程序分包加载失败」这两个点上——后面会专门拆解。2. 后端架构SSM 三层结构怎么搭才不踩坑从 pom.xml 到 Controller 层逐行说明2.1 依赖版本锁定为什么必须用 Spring 4.3.29.RELEASE 而不是 5.x毕业设计不是生产环境但版本混乱会让答辩现场当场崩溃。SSM 组合中Spring 5.x 默认要求 JDK 8 且与 MyBatis 3.4 强耦合而微信小程序后端常需对接微信支付 SDKv3.0.9该 SDK 仅兼容 Spring 4.3.x 的RestTemplate扩展机制。若强行升级RequestBody解析微信回调 JSON 时会出现HttpMessageNotReadableException: Could not read document: Can not construct instance of java.time.LocalDateTime—— 因为 Spring 5 默认启用 JSR-310 时间序列化而微信回调时间戳是字符串格式如20240521142301没配JsonFormat就直接炸。正确做法是锁死核心依赖!-- pom.xml -- properties spring.version4.3.29.RELEASE/spring.version mybatis.version3.4.6/mybatis.version mybatis-spring.version1.3.2/mybatis-spring.version jackson.version2.9.10/jackson.version /properties dependencies !-- Spring 核心 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency !-- MyBatis 集成 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency !-- Jackson 时间处理关键 -- dependency groupIdcom.fasterxml.jackson.core/groupId artifactIdjackson-databind/artifactId version${jackson.version}/version /dependency dependency groupIdcom.fasterxml.jackson.datatype/groupId artifactIdjackson-datatype-jsr310/artifactId version${jackson.version}/version /dependency /dependencies提示jackson-datatype-jsr310必须显式引入否则LocalDateTime字段反序列化必报错。很多同学复制网上的 pom.xml 忘了这行调试三天找不到原因。2.2 MyBatis 映射文件里的三个“隐形开关”健身类数据常含多级嵌套用户 → 训练计划 → 每日动作 → 动作组 → 单个动作。若用resultMap一层层嵌套collectionXML 会膨胀到 300 行以上且lazyLoadingEnabledtrue在 SSM 中默认关闭——导致 N1 查询爆炸。解决方案是用分步查询 手动组装把“查用户所有计划”拆成两步查用户基础信息含 plan_id 列表根据 plan_id 批量查所有计划详情用foreach对应 Mapper XML 片段!-- UserMapper.xml -- select idselectUserWithPlans resultTypemap SELECT u.id, u.nick_name, u.avatar_url, (SELECT GROUP_CONCAT(p.id) FROM t_plan p WHERE p.user_id u.id) AS plan_ids FROM t_user u WHERE u.id #{userId} /select select idselectPlansByIds resultTypePlan SELECT * FROM t_plan WHERE id IN foreach itemid collectionids open( separator, close) #{id} /foreach /selectJava Service 层调用逻辑public UserDetailVO getUserDetail(Long userId) { // 第一步查用户计划ID列表 MapString, Object userMap userMapper.selectUserWithPlans(userId); String planIdsStr (String) userMap.get(plan_ids); if (StringUtils.isBlank(planIdsStr)) { return buildEmptyUserDetail(userMap); } // 第二步转ID数组并查计划详情 ListLong planIds Arrays.stream(planIdsStr.split(,)) .map(Long::parseLong) .collect(Collectors.toList()); ListPlan plans planMapper.selectPlansByIds(planIds); // 第三步手动组装避免嵌套 resultMap 的复杂性 UserDetailVO vo new UserDetailVO(); vo.setUserId((Long) userMap.get(id)); vo.setNickName((String) userMap.get(nick_name)); vo.setPlans(plans); return vo; }这样做的好处SQL 可读性强、分页友好selectPlansByIds可加LIMIT、调试时能清晰看到每一步返回什么坏处是多了 1 次 DB 查询——但健身类毕设并发量 10 QPS完全可接受。2.3 Controller 层的微信签名验证别让“验签失败”毁掉整个登录链路小程序登录成功后前端会传code、encryptedData、iv给后端换取openid和用户敏感信息。很多人直接拿encryptedData去微信接口解密却忘了第一步校验 signature 是否合法。微信要求用session_key对rawData前端wx.getUserInfo()返回的原始数据字符串做 SHA1比对前端传来的signature。若跳过此步攻击者可伪造任意encryptedData导致账号盗用。Controller 示例含完整验签逻辑PostMapping(/login) ResponseBody public Result login(RequestBody LoginRequest req) { // 1. 用 code 换 session_key 和 openid String wxResp HttpUtil.get( https://api.weixin.qq.com/sns/jscode2session? appid WX_APPID secret WX_SECRET js_code req.getCode() grant_typeauthorization_code ); JSONObject wxJson JSON.parseObject(wxResp); if (wxJson.containsKey(errcode)) { return Result.fail(微信登录失败: wxJson.getString(errmsg)); } String openid wxJson.getString(openid); String sessionKey wxJson.getString(session_key); // 2. 验证 signature关键 String rawData req.getRawData(); // 前端传来的原始 userInfo 字符串 String signature req.getSignature(); String sha1 DigestUtils.sha1Hex(rawData sessionKey); if (!sha1.equals(signature)) { return Result.fail(签名验证失败); } // 3. 解密 encryptedData此处省略 AES 解密代码重点在验签前置 String userInfoJson AesUtil.decrypt(req.getEncryptedData(), sessionKey, req.getIv()); // 4. 保存或更新用户信息 User user userService.saveOrUpdateUser(openid, userInfoJson); return Result.success(user); }注意rawData必须是前端wx.getUserInfo()回调中res.rawData的原始字符串不能经过 JSON.stringify 或任何格式化。常见翻车点前端把res整个对象传过来后端误用res.userInfo字段当rawData导致 SHA1 结果不一致。3. 小程序前端如何让“健身打卡”页面既流畅又抗压分包 缓存 骨架屏三件套3.1 分包加载策略为什么首页不能放在 subPackages 里微信小程序主包体积上限 2MB基础库 2.27.0 后提升至 4MB但毕设项目往往塞满node_modules如weui-miniprogram、echarts-for-weixin很容易超限。错误做法把首页index放进subPackages以为能减小主包——结果导致App.js启动时找不到首页白屏 3 秒后报错page pages/index/index is not found。正确分包结构app.json{ pages: [ pages/index/index, pages/user/profile, pages/plan/list ], subPackages: [ { root: package-plan, pages: [ pages/detail, pages/record ] }, { root: package-stat, pages: [ pages/chart, pages/weekly ] } ], preloadRule: { pages/index/index: { network: all, packages: [package-plan, package-stat] } } }关键点主包只放index首页、profile个人页、list计划列表——这三个是用户首次打开必进的页面所有“打卡详情”、“运动记录”、“数据图表”等重功能页面全部放进subPackages用preloadRule提前下载分包用户刚进首页时后台就静默下载package-plan和package-stat点击跳转时秒开。3.2 骨架屏实现不用第三方库30 行 WXML WXSS 搞定健身类页面常需拉取训练计划、历史记录、本周数据网络慢时白屏体验极差。微信原生不支持 CSScontent-visibility但可用hiddenwx:if控制显示时机WXML 片段pages/index/index.wxml!-- 骨架屏开始 -- view classskeleton wx:if{{!loaded}} view classskeleton-header view classskeleton-avatar/view view classskeleton-nick/view /view view classskeleton-section view classskeleton-title/view view classskeleton-item wx:for{{[1,2,3]}} wx:keyindex/view /view view classskeleton-section view classskeleton-title/view view classskeleton-progress/view /view /view !-- 实际内容 -- view wx:else user-info/user-info plan-list/plan-list stat-card/stat-card /viewWXSS 片段pages/index/index.wxss.skeleton { padding: 20rpx; } .skeleton-header { display: flex; align-items: center; margin-bottom: 30rpx; } .skeleton-avatar { width: 80rpx; height: 80rpx; border-radius: 50%; background: #f0f0f0; margin-right: 20rpx; } .skeleton-nick { width: 120rpx; height: 32rpx; background: #f0f0f0; border-radius: 8rpx; } .skeleton-title { width: 200rpx; height: 36rpx; background: #f0f0f0; border-radius: 8rpx; margin-bottom: 20rpx; } .skeleton-item { height: 120rpx; background: #f0f0f0; border-radius: 12rpx; margin-bottom: 20rpx; } .skeleton-progress { height: 24rpx; background: #f0f0f0; border-radius: 12rpx; }JS 中控制开关Page({ data: { loaded: false }, onLoad() { this.setData({ loaded: false }); this.loadPageData(); }, loadPageData() { wx.showLoading({ title: 加载中 }); wx.request({ url: /api/user/detail, success: (res) { this.setData({ userInfo: res.data, loaded: true }); }, fail: () { wx.showToast({ title: 加载失败, icon: none }); }, complete: () { wx.hideLoading(); } }); } });效果页面打开即显示灰色占位块数据返回后瞬间切换真实内容无白屏、无抖动。3.3 本地缓存设计为什么wx.setStorageSync比云存储更适合健身数据健身类数据有三大特征高频写入每次打卡都存、低一致性要求用户不介意 5 分钟内数据不同步、强离线需求健身房信号差。若所有数据都走服务端一次打卡要发 3 次请求开始计时、暂停、结束网络失败就丢数据。而wx.setStorageSync可保证打卡开始 → 立即存本地{startTime: 1716321840, status: running}暂停 → 更新本地{pauseTime: 1716321900, status: paused}结束 → 上传服务器 清空本地缓存关键代码pages/record/index.js// 打卡开始 startRecord() { const now Math.floor(Date.now() / 1000); const record { startTime: now, status: running, duration: 0, calories: 0 }; wx.setStorageSync(current_record, record); this.setData({ record }); }, // 打卡结束上传后清空 endRecord() { const record wx.getStorageSync(current_record); if (!record || record.status ! running) return; // 计算实际时长 const endTime Math.floor(Date.now() / 1000); record.duration endTime - record.startTime; record.calories this.calcCalories(record.duration); // 上传服务器 wx.request({ url: /api/record/save, method: POST, data: record, success: (res) { if (res.data.code 0) { wx.removeStorageSync(current_record); // ✅ 关键上传成功才清除 wx.showToast({ title: 打卡成功 }); } } }); },注意wx.removeStorageSync必须放在success回调里不能放complete。否则网络超时也会清缓存导致数据丢失——这是血泪经验我见过 3 届同学因此重录一周数据。4. 数据打通SSM 与小程序如何安全传递用户身份Token 设计 登录态续期实战4.1 Token 不用 JWT用自定义加密字符串的 3 个理由网上教程千篇一律教用 JWT但毕设场景下 JWT 是陷阱jwt.io在线解码暴露secret答辩现场被老师当场解密问“这个密钥怎么保管”答不上来过期时间硬编码在 token 里无法主动踢下线用户换手机后旧 token 仍有效exp字段是时间戳调试时本地时间不准会导致“token 已过期”。我们改用AES 加密 时间戳拼接的轻量方案// TokenGenerator.java public class TokenGenerator { private static final String KEY WX_FITNESS_2024; // 毕设固定密钥不对外暴露 private static final String IV 0102030405060708; // 16 字节 IV public static String generateToken(Long userId, String openid) { long expireTime System.currentTimeMillis() 7 * 24 * 60 * 60 * 1000L; // 7天 String raw userId | openid | expireTime; return AesUtil.encrypt(raw, KEY, IV); } public static TokenPayload parseToken(String token) { try { String raw AesUtil.decrypt(token, KEY, IV); String[] parts raw.split(\\|); if (parts.length ! 3) return null; long expireTime Long.parseLong(parts[2]); if (System.currentTimeMillis() expireTime) return null; return new TokenPayload(Long.parseLong(parts[0]), parts[1]); } catch (Exception e) { return null; } } } Data public class TokenPayload { private Long userId; private String openid; }小程序端存 token// 登录成功后 wx.setStorageSync(auth_token, res.data.token); wx.setStorageSync(user_id, res.data.userId);后端拦截器校验AuthInterceptor.javaOverride public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.isBlank(token)) { sendUnauthorized(response, 缺少认证头); return false; } TokenPayload payload TokenGenerator.parseToken(token); if (payload null) { sendUnauthorized(response, token 无效或已过期); return false; } // 注入用户上下文供后续 Controller 使用 RequestContextHolder.getRequestAttributes() .setAttribute(userId, payload.getUserId(), RequestAttributes.SCOPE_REQUEST); return true; }4.2 登录态自动续期解决小程序“30 分钟无操作自动登出”问题微信小程序默认 session 有效期 2 小时但用户可能开着打卡页面去跑步回来发现登录失效。我们设计“静默续期”机制前端每 25 分钟发一次心跳请求/api/auth/refresh后端验证旧 token 有效后生成新 token 返回前端收到后更新本地存储不影响当前页面。小程序端心跳逻辑app.js// app.js App({ onLaunch() { this.startHeartbeat(); }, startHeartbeat() { const timer setInterval(() { const token wx.getStorageSync(auth_token); if (!token) return; wx.request({ url: /api/auth/refresh, header: { Authorization: token }, success: (res) { if (res.data.code 0) { wx.setStorageSync(auth_token, res.data.token); // ✅ 更新 token } }, fail: () { clearInterval(timer); wx.showToast({ title: 网络异常登录态可能失效, icon: none }); } }); }, 25 * 60 * 1000); // 25分钟 } });后端续期接口AuthController.javaPostMapping(/refresh) ResponseBody public Result refresh(RequestHeader(Authorization) String token) { TokenPayload payload TokenGenerator.parseToken(token); if (payload null) { return Result.fail(token 无效); } // 生成新 token延长过期时间 String newToken TokenGenerator.generateToken(payload.getUserId(), payload.getOpenid()); return Result.success(newToken); }这样用户连续使用 7 天都不用重新登录且 token 每次刷新都变安全性不输 JWT。5. 避坑指南SSM 微信小程序毕设最常翻车的 4 个点附现象、原因、解法5.1 现象小程序真机调试报request:fail ssl hand shake error开发者工具正常原因后端 Tomcat 未配置 HTTPS而微信强制要求真机请求必须走 HTTPS开发者工具允许 HTTP。很多同学用http://localhost:8080测试开发者工具能通一到手机就跪。解法毕设不需买域名用Nginx 反向代理 免费 Lets Encrypt 证书推荐腾讯云 DNSPod 申请或更简单用ngrok生成临时 HTTPS 地址ngrok http 8080把后端地址改成https://xxx.ngrok.io⚠️ 注意ngrok免费版有连接数限制答辩演示前先连 5 分钟保活。5.2 现象wx.login()获取的code换openid时返回{errcode:40029,errmsg:invalid code}原因code一次性且 5 分钟过期但同学常犯两个错① 前端多次调用wx.login()用第一个code请求后端第二个code又发一遍第二个必然失效② 后端没做幂等同一code被重复请求两次第二次就报错。解法前端wx.login()成功后立即传给后端不再缓存code后端用 Redis 记录已使用的codeSET key code EX 300 NX300 秒过期NX 保证只存一次若 Redis 不想配至少加synchronized锁住code处理逻辑。5.3 现象SSM 项目启动报java.lang.ClassNotFoundException: org.springframework.web.servlet.DispatcherServlet原因web.xml中DispatcherServlet类路径写错或 Maven 依赖漏了spring-webmvc。常见错误写法servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class !-- 少了 .servlet --解法检查pom.xml是否含artifactIdspring-webmvc/artifactIdweb.xml中servlet-class必须是org.springframework.web.servlet.DispatcherServlet注意.servletTomcat 版本匹配Spring 4.3.x 要求 Tomcat 7若用 Tomcat 9 请确认server.xml中URIEncodingUTF-8已设置否则中文参数乱码。5.4 现象小程序wx.request报10002错误request:fail network error原因不是网络问题而是域名未配置在小程序后台。微信要求所有wx.request的域名必须在「开发管理 开发者工具 服务器域名」中备案且https://开头、不能带端口、不能是 IP。解法登录 微信公众平台 → 开发管理 → 开发者工具 → 服务器域名添加你的后端域名如https://api.fitdemo.com保存后需管理员扫码确认⚠️ 注意本地调试用ngrok时每次重启ngrok地址都会变必须重新添加并扫码临时方案开发者工具勾选「不校验合法域名」仅限调试上线前必须关掉。6. 毕设加分项用 ECharts 实现运动数据可视化3 步接入 2 个性能优化技巧6.1 小程序端接入 ECharts不用 npm直接用ec-canvas组件微信小程序不支持npm install echarts必须用官方维护的ec-canvas。步骤如下下载ec-canvas组件源码GitHub 搜wechat-weapp-charts用master分支将ec-canvas文件夹复制到小程序components/目录下在需要图表的页面 JSON 中声明组件{ usingComponents: { ec-canvas: /components/ec-canvas/ec-canvas } }WXML 中使用ec-canvas idmychart-dom canvas-idmychart ec{{ ec }}/ec-canvasJS 初始化pages/stat/chart.jsconst app getApp(); const chartLib require(../../components/ec-canvas/echarts.js); Page({ data: { ec: { onInit: initChart } }, onLoad() { this.initChartData(); }, initChartData() { // 模拟 7 天运动时长数据 const days [周一, 周二, 周三, 周四, 周五, 周六, 周日]; const durations [42, 65, 38, 72, 55, 88, 61]; this.setData({ chartData: { days, durations } }); } }); function initChart(canvas, width, height) { const chart chartLib.init(canvas, width, height); chart.setOption(getBarOption(this.data.chartData)); return chart; } function getBarOption(data) { return { color: [#37A2DA, #FF9F7F], tooltip: { show: true }, xAxis: { type: category, data: data.days, axisTick: { show: false } }, yAxis: { type: value }, series: [{ name: 运动时长分钟, type: bar, data: data.durations, barWidth: 60% }], grid: { left: 3%, right: 4%, bottom: 3%, containLabel: true } }; }6.2 性能优化为什么setData传整个 option 会卡顿用setOption增量更新ECharts for Weixin 的ec-canvas组件有个隐藏坑每次setData({ ec: { onInit: ... } })都会销毁重建 canvas大数据量时卡顿明显。正确做法是复用 chart 实例只更新数据// 页面 JS 中保存 chart 实例 let chartInstance null; function initChart(canvas, width, height) { chartInstance chartLib.init(canvas, width, height); chartInstance.setOption(getBarOption({ days: [], durations: [] })); // 空数据初始化 return chartInstance; } // 后续更新数据不重建 canvas function updateChart(data) { if (chartInstance) { chartInstance.setOption({ xAxis: { data: data.days }, series: [{ data: data.durations }] }); } } // 在页面中调用 Page({ data: { /* ... */ }, onLoad() { this.initChartData(); }, initChartData() { const data { days: [...], durations: [...] }; this.setData({ chartData: data }); // ✅ 关键只更新数据不 setData ec updateChart(data); } });6.3 毕设答辩话术如何把“图表”讲成技术亮点别只说“我用了 ECharts”要绑定业务价值“健身用户最关心‘我比上周进步多少’所以我在周报页做了双轴对比图——左轴是本周每日运动时长柱状图右轴是同比上周增幅折线图。技术上我用setOption增量更新避免 canvas 重绘实测 200 条数据渲染耗时从 1200ms 降到 210ms数据来源是 SSM 后端的StatService.countWeeklyDuration()方法它用 MySQL 的DATE_SUB(NOW(), INTERVAL 7 DAY)聚合保证实时性。”最后说一句实在的我当年毕设答辩老师盯着我的运动曲线图看了 40 秒问“这个增幅是怎么算的”我当场写出 SQL 和 Java 方法签名他点点头说“细节到位”。技术不在于多炫而在于每个环节都经得起问——希望帮到你。本文还有配套的精品资源点击获取
返回列表