做毕设最怕的就是选题看着很“高端”,结果自己根本驾驭不了,最后赶在答辩前疯狂降级。如果你正在Java方向找项目,今天拆的这套个人运动健康管理系统,是我认为很值得参考的样本:技术栈是SpringBoot + Vue + MySQL,前后端分离,业务不复杂但闭环完整,能跑出图表、能讲清楚权限、能写出论文,完全符合“完整前后端代码+说明文档+LW+调试定制”这类毕设交付物的要求。
这套系统不是那种套皮管理系统,它围绕“用户—运动记录—健康指标—目标—趋势分析”这条数据链展开,既有常见的增删改查,又有卡路里计算、BMI计算、图表统计这类可以拿出来讲的设计点。无论你是刚开始学SpringBoot的学生,还是想快速搭一个项目作为毕设底子的同学,这篇内容都会帮到你。我会把项目拆成几个角度:先讲业务功能怎么定,再讲技术选型为什么这么选,接着给核心模块的实现思路和关键代码,最后把联调、部署、论文写作和常见坑一起整理出来。
1. 项目定位与功能拆解
1.1 为什么这个题目适合做Java毕设
每次有同学来问我选什么毕设题目,我都会推荐一类“业务明确、有完整数据链路、能同时体现前后端能力”的系统。个人运动健康管理系统就是典型的例子。
首先是需求不用瞎编。运动记录、健康档案、目标管理、统计图表,这些都是日常生活中很好理解的概念,不需要太多业务背景。其次是技术点覆盖面合适:后端有SpringBoot接口、数据库表设计、权限拦截,前端有Vue组件、路由、状态管理、图表展示,相当于把全栈开发的关键环节都过了一遍。最重要的,这个题目有“可讲述性”——答辩的时候,你可以从数据怎么采集、卡路里怎么计算、BMI怎么分级、趋势图怎么聚合统计这几个方向去讲,老师不会觉得空洞。
另外提一句,题目里说的“完整前后端代码+说明文档+LW”,在毕设圈里LW通常就是毕业论文。毕设交付并不只是代码跑通,更重要的是能拿出清晰的文档和论文,所以选一个业务边界明确、模块容易拆的系统,写起论文来也会省力很多。
1.2 功能清单与角色设计
这套系统的核心角色分两类:普通用户、管理员。每个角色的功能要尽量“小而完整”。
用户端的功能:
- 注册、登录、个人信息维护(可更新身高、体重、性别、年龄)
- 运动记录管理:添加、修改、删除个人运动记录,记录类型包括跑步、骑行、游泳、快走等
- 健康档案管理:录入体重、血压、身高,系统自动计算BMI
- 目标管理:设定每周运动时长目标或减重目标,查看完成进度
- 统计展示:查看体重变化折线图、运动时长柱状图、卡路里消耗趋势
管理端的功能:
- 用户列表管理:查看、禁用、重置密码
- 运动类型管理:维护运动类型及其代谢当量值(MET值)
- 系统公告管理:发布前端公告信息
- 整体数据统计:查看所有用户的运动汇总、活跃度排行
从业务流程来看,一个典型的使用过程是这样的:用户登录后进入首页,看到今天的运动概览和目标进度;点击“新增记录”,选择运动类型、填写时长和距离,系统根据用户体重自动算出卡路里并保存;然后用户到统计页查看最近一周/一个月的变化曲线,看到体重下降趋势,也算是对自己坚持运动的反馈。
1.3 数据库表结构怎么设计更合理
数据库是毕设答辩一定会被问到的部分,表结构别随便糊弄。我建议至少设计六张核心表,互相之间关系清晰:
| 表名 | 主要字段 | 说明 |
|---|---|---|
| user | id, username, password, nickname, gender, age, height, weight, role, create_time | 用户表,role区分普通用户和管理员 |
| sport_type | id, name, met_value, unit | 运动类型表,met_value用来计算卡路里 |
| sport_record | id, user_id, type_id, duration, distance, calories, record_date, create_time | 运动记录表,关联用户和运动类型 |
| health_record | id, user_id, height, weight, blood_pressure, bmi, record_date | 健康档案表,用于记录每天身体指标 |
| goal | id, user_id, goal_type, target_value, current_value, status, deadline | 目标表,目标类型可以是“时长”或“体重” |
| notice | id, title, content, create_time | 公告表,管理端发布 |
这里有几个设计上的细节要注意。第一,密码字段不要存明文,哪怕只是毕设,也要用BCrypt加密,答辩时这是一个加分点。第二,运动记录表里有一个type_id外键指向运动类型表,而不是直接存一个字符串“跑步”,这样方便以后扩展运动类型,也符合数据库规范化的要求。第三,健康档案表用record_date区分每天的数据,这样画体重趋势图时可以直接按日期聚合。
有些同学喜欢用MyBatis-Plus的代码生成器,直接根据实体类生成建表SQL,确实可以省不少时间。不过我还是建议至少在论文里把表结构图和字段说明表整理出来,毕竟数据库设计是评分的一部分。
2. 技术选型背后的逻辑
2.1 为什么是SpringBoot + Vue + MySQL,而不是其他组合
选技术栈不能只看“流行”,要看是不是适合自己、是不是方便交付。Java毕设领域里,SpringBoot + Vue + MySQL确实是最好的组合之一。
SpringBoot的优势不用多说:内嵌Tomcat、自动配置、生态成熟,搭一个Web项目只需要很少的配置;就算你之前没接触过,照着官方文档也能快速上手。Vue负责前端,组件化开发思路清晰,配合Vue Router和Element UI,几天时间就能把管理端界面搭得很像样。MySQL则稳定、免费,Navicat或者SQLyog都可以可视化操作,装好就能用。
很多人会纠结版本,我建议用比较成熟的稳定组合:JDK 8 + SpringBoot 2.7.x + MyBatis-Plus 3.5.x + Vue 2或Vue 3均可,MySQL 5.7或8.0都可以。这里特别提醒一句,现在SpringBoot 3.x已经发布,但它要求JDK 17,并且很多老教程不兼容,容易把人带沟里。如果你不想在环境配置上花太多时间,就老老实实用2.7.x。热搜词里经常出现“springboot版本太高”,说明踩这个坑的人真的不少。
2.2 MyBatis-Plus为什么值得用
很多本来用MyBatis的同学会纠结要不要用MyBatis-Plus,我的态度很明确:直接用。毕设不是开源项目,不需要炫技,效率高才是王道。
MyBatis-Plus是MyBatis的增强工具,简单CRUD它全部帮你封装好了。比如我要按用户ID查询他的运动记录,传统MyBatis要写XML和ResultMap,而MyBatis-Plus里一行LambdaQueryWrapper就搞定:
LambdaQueryWrapper<SportRecord> wrapper = Wrappers.lambdaQuery(SportRecord.class) .eq(SportRecord::getUserId, userId) .orderByDesc(SportRecord::getRecordDate); List<SportRecord> records = sportRecordMapper.selectList(wrapper);到了复杂统计的地方,它也不拦着你写SQL。比如统计最近一周每天运动总时长,我一般直接在Mapper里用注解写SQL,既直观又方便:
@Select("SELECT record_date, SUM(duration) AS total_duration " + "FROM sport_record WHERE user_id = #{userId} AND record_date >= #{startDate} " + "GROUP BY record_date ORDER BY record_date") List<Map<String, Object>> selectDailyDuration(@Param("userId") Long userId, @Param("startDate") String startDate);用MyBatis-Plus生成实体、Mapper、Service也方便,网上教程一搜一大把。不过我不建议把代码生成器当作偷懒工具一味套用,还是要把各个类之间的关系看懂,不然答辩被问到时容易露馅。
2.3 前后端接口约定与统一返回体
前后端分离的项目,最怕各写各的,接口格式对不上。所以项目一开始就要约定统一的请求风格和返回结构。
我自己的习惯是:后端所有接口路径加 /api 前缀,比如/api/user/login、/api/sport/list;返回结构统一是{ code, message, data }。放一个R类作为统一返回体:
public class R { private Integer code; private String message; private Object data; public static R ok(Object data) { R r = new R(); r.setCode(200); r.setMessage("success"); r.setData(data); return r; } public static R error(String message) { R r = new R(); r.setCode(500); r.setMessage(message); return r; } // 省略 getter/setter }对应的前端,在Vue项目里用axios封装一个request.js,统一处理接口请求、携带token、错误提示。这样后面新增模块的时候,所有开发模式都是套同一个模板,代码结构会很干净。
import axios from 'axios' import { Message } from 'element-ui' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.token = token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) } ) export default request3. 核心模块实现与实操步骤
3.1 登录鉴权:JWT + 拦截器
登录模块是每个系统都有的功能,但如果你只是做个用户表校验,就少了可以展示的亮点。我建议用JWT实现无状态鉴权,这也是前后端分离项目里最常见的方案。
JWT的原理不难理解:用户登录成功后,后端把用户信息加密成一个签名Token返回给前端;前端每次请求都把这个Token放在请求头里;后端通过拦截器解析Token,拿到当前用户ID。它不需要在服务端保存会话,接口天然支持横向扩展。
我一般会把JWT工具类单独拿出来,避免写得到处都是:
public class JwtUtil { private static final String SECRET = "your-secret-key"; private static final long EXPIRE = 7 * 24 * 60 * 60 * 1000L; public static String generateToken(Long userId, String role) { return Jwts.builder() .setSubject(String.valueOf(userId)) .claim("role", role) .setExpiration(new Date(System.currentTimeMillis() + EXPIRE)) .signWith(SignatureAlgorithm.HS256, SECRET) .compact(); } public static Long parseUserId(String token) { Claims claims = Jwts.parser() .setSigningKey(SECRET) .parseClaimsJws(token) .getBody(); return Long.valueOf(claims.getSubject()); } }然后在拦截器里校验Token,放行登录和注册接口:
public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token = request.getHeader("token"); if (token == null || token.isEmpty()) { response.setStatus(401); return false; } try { Long userId = JwtUtil.parseUserId(token); request.setAttribute("userId", userId); return true; } catch (Exception e) { response.setStatus(401); return false; } } }这一步做完,后端Controller里就可以从request里取当前用户ID,接口之间不会越权,功能也干净。
3.2 卡路里计算:MET值怎么用
运动记录是整个系统的核心数据,所以“卡路里”这个字段不能只让用户随便填,得让系统算出来。最简单的科学算法是用代谢当量MET值。
公式是:卡路里消耗(千卡)= MET值 × 体重(kg)× 运动时长(小时)。MET值可以理解为某种运动相对于静坐状态的代谢倍数,跑步通常在8.0到10.0之间,快走在4.3左右,骑行静速在6.8左右,游泳在6.0左右。我在运动类型表里就会提前维护好这些MET值。
举个例子,一个体重70kg的人,跑步40分钟:8.0 × 70 ×(40/60)≈ 373千卡。代码实现起来也很简单:
double calories = sportType.getMetValue() * user.getWeight() * (duration / 60.0); sportRecord.setCalories(Math.round(calories * 100) / 100.0);这里有个容易踩的小坑:duration用户填的通常是分钟,但公式里用的是小时,所以一定要先除以60。另外目标模块也可以复用这个概念,比如目标设置为每周消耗2000千卡,那么每新增一条运动记录后,就累加本周卡路里,更新目标进度,用户体验会很完整。
3.3 健康指标计算与BMI分级
BMI(身体质量指数)是健康管理系统中特别适合展示计算逻辑的点。它的公式是:体重(kg)除以身高(m)的平方。注意身高单位是米,但用户录入时通常用厘米,所以代码里要转换一下。
double heightM = healthRecord.getHeight() / 100.0; double bmi = healthRecord.getWeight() / (heightM * heightM); healthRecord.setBmi(Math.round(bmi * 10) / 10.0);算完之后还能给一句健康建议,比如BMI小于18.5偏瘦,18.5到23.9正常,24到27.9超重,28以上肥胖。这个逻辑放在Service层就行,前端拿到建议值直接展示。这种“计算出来的数据”比纯录入更值得写进论文,体现的是逻辑设计能力。
3.4 统计图表接口与前端ECharts展示
前端图表我推荐用ECharts,Vue集成它比较简单。系统里要展示两个核心图表:体重趋势折线图、最近七天运动时长柱状图。
体重趋势接口就是从health_record表里取最近30天的记录,按日期排序返回。运动时长统计接口就是前面提到的分组聚合SQL,按日期分组求和。这两个接口返回的都是List<Map<String, Object>>,前端循环取出来塞给ECharts的option就行。
Vue端我的做法是建一个Stats.vue页面,onMounted时拉取两个接口,然后分别初始化图表。核心代码大概是这样的:
const option = { tooltip: { trigger: 'axis' }, xAxis: { type: 'category', data: dateList }, yAxis: { type: 'value' }, series: [{ name: '运动时长(分钟)', type: 'bar', data: durationList }] }初始化图表时要注意一个很常见的问题:表格页和图表页在同一个Tab里,用Vue的v-if切换时,DOM还没渲染完成就调用echarts.init会拿不到容器。解决办法是在nextTick里再初始化,或者用setTimeout延迟一下。
3.5 Vue打包放进SpringBoot的两种方式
项目交付时,你可能会碰到“是不是要把前后端分开部署”的问题。这里给你说清楚两种方式。
第一种:开发环境前后端分离。Vue用开发服务器跑在8081端口,SpringBoot跑在8080端口,前端配置代理转发/api。这种方式适合开发和调试,代码改动热更新快。
第二种:部署环境统一打成JAR包。执行npm run build之后,Vue项目会在dist目录生成静态文件,把这些文件复制到SpringBoot项目的src/main/resources/static目录下面,然后正常mvn package打包,启动JAR后直接访问http://localhost:8080/就能看到整个系统。
使用第二种方式时,Vue项目的vue.config.js里一定要把publicPath设置成'./',否则打包出的静态资源路径是绝对路径,部署到SpringBoot后很可能白屏。另外,前端项目里的baseURL要改成空字符串或者/api,这样才能与自己后端的路径匹配。
3.6 管理端与权限控制细节
管理端我建议用角色字段直接区分管理员和普通用户,登录后把角色信息一并返回。前端在路由守卫里判断角色,后端在拦截器或注解里判断角色。比如管理员接口加上@RequireRole("admin")这样的自定义注解,或者更直接一点,在拦截器里解析出role后做一次判断。
这里有一个很多同学容易忽略的点:前端隐藏按钮并不等于安全。因为接口是公开的,别人直接调用接口就能越权操作。所以在后端必须做权限校验,尤其是用户管理、运动类型管理这些管理功能。哪怕只是加一个简单的角色判断,在答辩时也是可以讲出来的“安全设计”。
4. 调试、部署与常见问题排查
4.1 本地联调环境搭建顺序
我自己的开发顺序是先搭后端,再搭前端。具体是这样的:
- 安装MySQL,新建数据库
health_db,执行SQL脚本建表。 - 用IDEA打开SpringBoot工程,修改
application.yml里的数据库用户名和密码。 - 启动SpringBoot,用Postman测试几个核心接口,确认返回JSON正常。
- 用VSCode打开Vue工程,执行
npm install安装依赖。 - 修改
vue.config.js,配置代理到http://localhost:8080。 - 执行
npm run serve,浏览器访问http://localhost:8081。
联调的时候一定要先看后端控制台日志,很多“前端报错”其实是后端没启动或者接口路径不对。把前端报错信息和后端堆栈日志对照起来看,能节省大量时间。
4.2 常见问题速查表
这部分直接记录我实际踩过的坑,按优先级整理成表:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
| 前端请求接口返回404 | 代理没生效或接口路径不对 | 检查vue.config.js的proxy配置,确认后端Controller的@RequestMapping |
| 接口返回数据中文乱码 | MySQL表字符集不是utf8mb4 | 建表时指定字符集,连接URL加useUnicode=true&characterEncoding=utf8 |
| Vue打包后部署到SpringBoot白屏 | publicPath是绝对路径 | 在vue.config.js里设置publicPath: './' |
| 前端开发时跨域报错 | 前端和后端端口不一致 | 配置proxy代理,不要用后端@CrossOrigin硬解 |
| 启动时报端口被占用 | 系统里已有8080端口进程 | netstat -ano找到PID,taskkill杀掉占用进程 |
| 注册后密码在数据库是明文 | 没有使用加密工具 | 使用BCryptPasswordEncoder |
| SpringBoot启动报错“Invalid bound statement” | Mapper接口扫描不到 | 启动类加@MapperScan,或检查XML文件路径 |
| JWT拦截器放行了登录,但其他接口提示无token | 前端axios没带token | 在request.js请求拦截器里设置headers.token |
| 统计图表时间出现1970 | 后端时间字段没格式化 | 使用@JsonFormat或全局配置Jackson日期格式 |
4.3 论文(LW)结构与答辩重点
这部分很多人忽略,但毕设最终评分往往论文占一半。个人运动健康管理系统的论文结构我建议按这个顺序写:
- 摘要与绪论:写背景、意义、国内外现状,重点是引出选题。
- 关键技术介绍:SpringBoot、Vue、MySQL、MyBatis-Plus、JWT。
- 需求分析:画用例图,写功能需求和非功能需求。
- 系统设计:功能架构图、技术架构图、E-R图、数据库表结构。
- 系统实现:每个核心模块放截图和核心代码,配实现说明。
- 系统测试:功能测试用例表、结果分析。
答辩时老师经常问的几个问题,提前准备:
- “为什么选这个题目?”答:运动健康管理是真实需求,系统能帮助用户记录和了解自身健康状况。
- “JWT和传统的Session有什么区别?”答:无状态、跨域友好、适合分布式部署。
- “卡路里是怎么算的?”答:MET值 × 体重 × 时间,MET是运动强度的代谢当量。
- “系统怎么保证安全性?”答:密码加密、接口鉴权、前端路由守卫、参数校验。
这些问题都不难,只要自己真的动手写过,很容易答上来。
4.4 扩展方向:拿到代码之后怎么改
如果你拿到一套可运行的项目代码,建议不要满足于“能跑”,可以加几个增量需求,让项目更有自己的特色,也避免和同学撞题。
最容易做的是增加“打卡提醒”。用SpringBoot的定时任务(@Scheduled)每天定时检查用户是否有运动记录,没有就给用户生成一条站内提醒或公告。其次是增加运动目标的进度条展示,前端用进度条组件,后端在目标表里把current_value维护好即可。再有就是接入第三方图表库之外的东西,比如生成PDF健康周报,用itext导出。这些扩展点都能对应到你论文里的“系统实现”和“总结展望”。
另外一个扩展方向是把部署方式升级为Docker,写一个Dockerfile + docker-compose.yml,把自己的SpringBoot应用和MySQL容器化,这样不仅好展示,还能让老师觉得你具备现代工程能力。
最后想说的几句话
做完这个项目我最大的体会是:像这种全栈管理系统,真正难的不是某个算法或某个功能,而是把链路串起来。数据库字段差一个类型、前端图表初始化早了一帧、打包时少配了一个publicPath,任何一个环节出现问题都要花不少时间排查。但每解决一个问题,你对整个技术栈的理解就会更深一层。
个人运动健康管理系统这个题目,胜在“够用且不空”:需求清楚,模块不杂,适合从中学习SpringBoot+Vue全栈开发的完整流程。如果你正在为毕设发愁,建议先把这个系统的表结构和接口想清楚,再动手写代码。代码可以一行一行敲,但思路必须先把框架搭起来。后面再碰到类似的管理系统,基本就是同一套方法论套进去而已。希望这篇拆解能帮你少踩几个坑,顺利把毕设拿下来。