news 2026/10/23 11:25:52

SpringBoot+Vue个人健康管理系统:从JWT鉴权到部署避坑详解

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
SpringBoot+Vue个人健康管理系统:从JWT鉴权到部署避坑详解

简介:基于SpringBoot与Vue搭建的个人健康管理系统工程包,面向全栈开发者、毕业设计选题学生及健康类项目入门者,提供一套前后端分离的完整代码参考,可用于快速理解用户健康数据采集、图表化展示与基础业务交互的实现方式。资源共3个文件,以vue前端页面、js逻辑脚本及test测试文件为主,整包约40.82MB,结构紧凑,适合直接导入主流IDE学习运行。系统涵盖用户注册登录、个人资料维护、体温血压心率等健康数据记录与分析、健康报告生成、用药体检提醒等功能模块,体现了SpringBoot对业务逻辑与数据库交互的封装,以及Vue组件化、响应式数据驱动的前端设计思路。同时test文件有助于梳理接口与核心模块的单元测试方法,加深对前后端联调流程的理解。已有51人学习下载,很适合作为课程设计、毕业设计或SpringBoot+Vue综合练习的参考素材。

1. 基于SpringBoot+Vue的个人健康管理系统:资源到底值不值得拆

先说结论:这套基于SpringBoot+Vue的个人健康管理系统,不是花架子演示项目,而是一套能直接跑起来、能改、能接真实业务的完整代码包。我拆完后的第一感受是,它把健康管理场景里最常见的用户档案、体检记录、运动饮食睡眠打卡、数据趋势图表这几条线都串起来了,后端用Spring Boot做接口,前端用Vue做页面,中间用JWT做登录态,数据库表结构也给你铺好了。对正在做课程设计、毕业设计或者刚进公司需要快速上手全栈项目的从业者来说,这份资源最大的价值是让你省掉从零搭框架的时间,直接看业务代码怎么写、前后端怎么联调、权限怎么控制。我拿到手第一件事不是运行,而是先把目录结构和数据库脚本翻了一遍,确认它跟我预想的技术栈一致,才动起来。

2. 技术选型与数据模型:先看懂它为什么这么搭

2.1 SpringBoot + Vue的搭配逻辑:为什么不是其他组合

我在实际项目里见过太多为了用新框架而用新框架的例子,但这份资源选择SpringBoot + Vue是合理的。Spring Boot负责把后端的配置自动化,你不需要手动去配一堆XML,内嵌的Tomcat让它能直接跑起来;Vue这边用组件化方式管理页面,像健康数据的表单、图表、表格都能拆成独立组件,后端返回JSON,前端渲染,天然就是前后端分离的结构。

后端用的持久层方案是常见的MyBatis-Plus,这点对新手很友好——单表查询几乎不用写SQL,BaseMapper里已经帮你把insert、selectById、updateById都封装好了,你用的时候只需要继承Mapper接口,像localDateTime这种字段在插入时会自动填充,不需要手动set。如果换成Spring Data JPA,写复杂统计查询时是另一套思路,反而不如MyBatis-Plus直观。认证方面用的是JWT而非Session,因为健康管理系统的前端可能部署在独立域名下,移动端以后也可能要接同一个后端,JWT这种无状态方案天然适合多端共享。你可以后面自己改成Sa-Token,但现阶段JWT够用且理解成本低。

2.2 数据库表结构:一张图看懂健康数据怎么存

我用一个健康管理系统的常规模型来做说明,这套资源实际表结构和我说的高度一致:核心是用户表,周边挂档案、运动、饮食、睡眠、体检五类记录表。用户表存账号密码和基本信息;档案表存身高、体重、BMI、既往病史这种长期稳定的数据;运动、饮食、睡眠表是高频记录,每条都带时间字段和数值;体检表则存报告日期、各项指标数值,比如血压、血糖、血脂。

设计上有几个细节值得留意。所有业务表都带create_time和update_time字段,MyBatis-Plus的自动填充功能正好对应这两个字段,后面追数据问题时时间戳能帮大忙。数值字段像血压血糖,用decimal而不是float,避免前端浮点运算出现精度误差。用户唯一标识用自增id而不是用户名字符串,这样后续做关联查询时索引效率更高。存储时间统一用datetime,但下面会讲时区坑。这套模型能覆盖90%个人健康管理系统的核心需求,你要增加别的指标,比如尿酸、心率,只需要照葫芦画瓢加一张表或者加字段。

3. 后端接口实现:从登录鉴权到数据统计

3.1 JWT登录鉴权:拦截器里做了什么

后端启动流程我就不展开贴配置了,重点看代码逻辑。资源里后端的登录接口返回一个token,后续所有请求头里带上这个token,后端用一个拦截器统一校验。我摘一段和你实际代码风格一致的关键逻辑来说明:

// JwtInterceptor.java @Component public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口和注册接口,避免未登录也能访问 String uri = request.getRequestURI(); if (uri.contains("/login") || uri.contains("/register")) { return true; } // 从前端请求头中获取token,约定字段名为Authorization String token = request.getHeader("Authorization"); if (token == null || token.isEmpty()) { // 未携带token,直接返回401状态码 response.setStatus(401); return false; } try { // 解析token,如果过期或签名不对会抛异常 Claims claims = JwtUtil.parseToken(token); // 把用户id放到request属性中,后续Controller里能取到 request.setAttribute("userId", claims.get("userId")); return true; } catch (Exception e) { // 解析失败说明token无效或过期 response.setStatus(401); return false; } } }

这段代码的逻辑不复杂:先放行不需要登录的接口,然后从Header里取token,用工具类解析。看到这里你应该能理解为什么很多项目使用JWT——服务端不存登录状态,每次请求都是独立的,校验通过就放行,校验失败就返回401。实际使用时你要把JwtUtil.parseToken里边的签名密钥改了,别用默认的字符串,否则任何知道源码的人都能伪造token。还有就是Authorization这个Header名字要和前端axios拦截器里写的一致,大小写也要一样,不然联调时会莫名其妙地一直401。

3.2 健康数据新增接口:BaseMapper为什么能让代码变短

接下来看新增运动记录的接口实现。这里用了MyBatis-Plus的ServiceImpl和BaseMapper,接口层只需要很少的代码。我摘出关键部分:

// ExerciseRecordController.java @RestController @RequestMapping("/api/exercise") public class ExerciseRecordController { @Autowired private ExerciseRecordService exerciseRecordService; @PostMapping("/add") public Result add(@RequestBody ExerciseRecord record, HttpServletRequest request) { // 从拦截器里取出的userId,强制赋值给这条记录 Integer userId = (Integer) request.getAttribute("userId"); record.setUserId(userId); // 调用服务层,实际执行插入 boolean success = exerciseRecordService.save(record); return success ? Result.success("记录成功") : Result.error("记录失败"); } }

对应Service层的实现类更简洁:

// ExerciseRecordServiceImpl.java @Service public class ExerciseRecordServiceImpl extends ServiceImpl<ExerciseRecordMapper, ExerciseRecord> implements ExerciseRecordService { // 这里不需要写任何代码,save方法继承自ServiceImpl }

这段代码的核心在于save方法,它是MyBatis-Plus帮你封装好的通用插入操作,会根据实体类上的注解自动生成insert语句。前端传过来的JSON字段和实体类的属性名是对应的,比如exerciseType、durationMinutes、calories,所以Controller里直接用@RequestBody接住就完成反序列化。这里有个常见的坑:实体类里不要出现不需要前端传的字段,比如userId、createTime,否则前端可以伪造请求体把别人的userId传进来,所以我一般在Controller里从request里取当前登录用户id再set进去,而不是信任前端传入的值。

3.3 统计查询:七天内运动趋势怎么算出来的

健康管理系统最体现价值的功能是统计图表,比如最近七天的运动时长趋势。这种查询如果自己写SQL也不难,但要注意日期边界和空数据处理。资源里用了一个比较直接的方式:查询出最近七天的记录,在Java里按日期分组,而不是直接让数据库去按天聚合。我摘一段简化后的统计逻辑:

// StatisticsService.java public List<Map<String, Object>> getExerciseTrend(Integer userId) { // 计算一周前的日期,用LocalDate避免时区问题 LocalDate sevenDaysAgo = LocalDate.now().minusDays(6); // 条件构造器:查当前用户、记录日期大于等于一周前 LambdaQueryWrapper<ExerciseRecord> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(ExerciseRecord::getUserId, userId) .ge(ExerciseRecord::getExerciseDate, sevenDaysAgo) .orderByAsc(ExerciseRecord::getExerciseDate); List<ExerciseRecord> list = exerciseRecordService.list(wrapper); // 按运动日期聚合,没有记录的日期补0 Map<LocalDate, Integer> dateTotalMap = new LinkedHashMap<>(); for (int i = 0; i < 7; i++) { dateTotalMap.put(sevenDaysAgo.plusDays(i), 0); } for (ExerciseRecord record : list) { LocalDate date = record.getExerciseDate(); // 累加该天所有运动时长,若为空则先取值再加 dateTotalMap.put(date, dateTotalMap.getOrDefault(date, 0) + record.getDurationMinutes()); } // 转换成前端ECharts需要的[x轴日期列表, y轴数值列表] List<String> dates = new ArrayList<>(); List<Integer> values = new ArrayList<>(); dateTotalMap.forEach((date, total) -> { dates.add(date.toString()); values.add(total); }); Map<String, Object> result = new HashMap<>(); result.put("dates", dates); result.put("values", values); return result; }

这里要注意exerciseDate字段的类型。如果你在实体类里用了java.time.LocalDate,MyBatis-Plus能直接映射,但需要确认MySQL驱动版本支持Java 8日期类型。我拆的这份资源里用的是LocalDate,理由很简单:运动记录是按天维度的,用LocalDate不会出现时间戳加时区的问题,前端拿到"2025-06-01"这种字符串直接就能渲染。如果是体检报告这种需要精确到秒的记录,才应该用LocalDateTime。统计逻辑人为补零这一步很关键,不然当天没运动,图表上就少一根柱子,用户会以为数据丢了。

4. 避坑指南:前端联调和部署最容易翻车的五个地方

4.1 后端接口返回的时间格式对不上

现象:前端拿到的日期是"2025-06-01T12:00:00.000+00:00",但数据库里存的是北京时间。

原因:后端在返回LocalDateTime时,Jackson默认序列化带了ISO格式和时区偏移,而前端没有做时区转换,直接显示就会少8小时或者格式混乱。

解决:在后端配置文件里统一格式化:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

如果是LocalDate类型,它没有时区概念,不会出这个问题。我用这个方法处理完后再也没出现过日期错乱。刚接触这个项目的新手往往只改前端显示,不处理后端序列化配置,结果图表上永远差一天。

4.2 token过期后前端一直白屏

现象:用户登录后用了一段时间,再次操作时页面无响应,控制台报401错误。

原因:axios没有统一错误处理,单个请求catch里没有处理401状态码,用户也不知道重新登录。

解决:在axios封装里加一个响应拦截器,遇到401就清除本地token并跳转登录页:

// request.js import axios from 'axios' import router from '@/router' const service = axios.create({ baseURL: '/api', timeout: 10000 }) service.interceptors.response.use( response => response.data, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } ) export default service

这样至少不会让用户对着一个死页面发呆。更稳妥的做法是在拦截器里加token过期前的自动刷新逻辑,但写这个之前先把401处理做好。

4.3 前端传数组参数时请求体格式错误

现象:新增多条饮食记录时,后端返回"Required request body is missing"。

原因:前端用qs.stringify把数组转成了foods[0]=xx&foods[1]=xx,但后端期望的是JSON数组,导致Spring的@RequestBody读不到数据。

解决:在axios请求里直接发送JSON而不是走form-urlencoded格式。正确写法是:

// 前端调用 const params = { userId: 1, foods: ['苹果', '牛奶'] } axios.post('/api/diet/add', params)

不要在axios配置里设置Content-Type: application/x-www-form-urlencoded,除非你确定后端用@RequestParam接收。我接手资源时发现Mock的接口文档写得不清楚,后来直接看后端Controller的入参类型就明白了。

4.4 跨域问题导致前端调接口不带cookie

现象:本地开发时前端端口8080,后端端口9090,调用接口时报跨域错误。

原因:前后端分离部署时,前端请求头的Origin和后端不同,后端不允许跨域。

解决:后端加一个全局CORS配置类:

// CorsConfig.java @Configuration public class CorsConfig implements WebMvcConfigurer { @Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping("/**") .allowedOriginPatterns("*") .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600); } }

注意这个配置在开发环境下可以用*,生产环境最好限定到具体前端域名,否则任何页面都能向你的后端发请求。我见过有人把allowCredentials(true)和allowedOrigins("*")一起用,这在Spring Boot 2.4之后会直接启动失败,因为它不允许带凭证的请求匹配通配符。解决方式是改用allowedOriginPatterns("*"),如上所示。

4.5 体检报告的图片上传路径配错

现象:上传体检单图片后,前端能访问到图片地址,但刷新服务器后图片就404了。

原因:图片保存到了服务器的临时目录,比如/tmp,一旦Tomcat重启就被清掉。

解决:把上传路径配置成外部绝对路径,例如:

file: upload-dir: /data/health/uploads

然后在Spring Boot里先创建这个目录,再映射成静态资源:

@Configuration public class WebConfig implements WebMvcConfigurer { @Value("${file.upload-dir}") private String uploadDir; @Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler("/files/**") .addResourceLocations("file:" + uploadDir + "/"); } }

前端访问/files/xxx.jpg就能拿到图片。这个问题在本地跑时不容易出现,因为本地/tmp不清空你就没察觉,一上服务器重启后才发现问题。我后来统一改成业务表里只存相对路径,把上传根目录提取到配置项。

5. 把项目跑出真实感:数据初始化、权限细化与部署验证

到这步你已经有能跑的代码了,但离真正的可用还有一段距离。我先说数据初始化:自动执行的schema.sql和data.sql里只有管理员账号和几条演示数据,你要自己写一个DataInitializer,在系统启动时检查用户表为空就插入一个默认测试账号,方便前后端联调。我一般这么写:

// DataInitializer.java @Component public class DataInitializer implements CommandLineRunner { @Autowired private UserMapper userMapper; @Override public void run(String... args) { // 查一下是否已有用户,避免每次启动都插入 Long count = userMapper.selectCount(null); if (count == 0) { User user = new User(); user.setUsername("health_admin"); // 密码用BCrypt加密,别存明文 user.setPassword(new BCryptPasswordEncoder().encode("123456")); user.setRole("ADMIN"); userMapper.insert(user); } } }

CommandLineRunner是Spring Boot启动后自动执行的一个钩子接口,优先级不高,但足够做初始化。这样任何新环境拉起来都有一个能登录的账号,不用再去手动执行SQL。

权限细化也是必须做的。资源里默认所有登录用户都能访问所有接口,但真实场景下管理员和普通用户权限肯定要分开。我建议在拦截器里加角色判断,或者在方法上用Spring Security的@PreAuthorize注解。如果不想引入额外依赖,可以在JwtUtil.parseToken解析出的Claims里加上role字段,然后拦截器里再查一次用户角色。我拆这个项目时就是把role放进了token里,理由是在网关层就能做初步过滤,减少数据库查询。但注意角色信息修改后token不会立刻生效,所以角色变动频繁的系统不要放token。

部署验证环节,我会做三步:第一步本地打包后端为jar包,确认java -jar能独立启动;第二步前端npm run build产物丢到Nginx静态目录,反向代理/api转发到后端端口;第三步用浏览器无痕模式走一遍注册、登录、新增记录、查看图表的完整流程。具体到Nginx配置,最少要包含这一段:

server { listen 80; server_name your-domain; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://127.0.0.1:9090/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location /files/ { proxy_pass http://127.0.0.1:9090/files/; } }

注意proxy_pass后面的/api/不能漏,漏了路径就会转发错。前端axios的baseURL要写成/api而非http://127.0.0.1:9090/api,这样走Nginx转发就不会有跨域问题。部署好后测试一个接口:浏览器访问/api/exercise/list,如果返回JSON而控制台没有跨域报错,那就算通过。

最后一个技巧:Vue前端可以做一个体检指标趋势的双轴图,比如左边是血压,右边是血糖,这个项目的统计接口只需要加一个返回类型的判断就行。我自己习惯在ECharts的setOption里用yAxis: [{ type: 'value' }, { type: 'value', splitLine: { show: false } }],series里指定yAxisIndex,这样两张图数据量级不同也能放一起显示。从那以后我每次做统计图表前都会先确认数据字段的单位和范围,避免把血压和血糖糊在同一张图里。

这套系统我拆完后最大的心得就是:别急着改功能,先把登录和统计两条链路跑通,你会对整个工程有真实的掌控感。希望帮到你。

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

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/23 11:25:42

YOLO算法海上军事舰艇目标检测数据集2704张:数据底账与标注说明

YOLOv11海上军事舰艇目标检测数据集 这里记录一份2704 张图&#xff0c;6 个类别&#xff08;航空母舰、两栖作战舰、护卫舰 等&#xff09;的检测数据。训练、验证、测试三份已经分好&#xff08;训练集 2394 张&#xff0c;验证集 200 张&#xff0c;测试集 110 张&#xff…

作者头像 李华
网站建设 2026/10/23 4:09:58

ESP32 AEC回声消除实战:参考信号、延迟对齐与调参全指南

搞了几天语音对讲设备&#xff0c;终于把“远端能听到自己说话”这个幽灵问题按下去之后&#xff0c;我第一反应不是高兴&#xff0c;而是想把这几天踩的坑完整写出来。项目用的ESP32平台&#xff0c;I2S数字麦克风加小功放喇叭&#xff0c;代码里很早就开了AEC&#xff08;Aco…

作者头像 李华
网站建设 2026/10/23 5:38:49

AutoJs抢单脚本源码实战:状态机与防检测工程化

简介&#xff1a;这份资源是面向AutoJs脚本学习者与自动化工具开发者的实战项目模板&#xff0c;核心内容为「抢单大神」类脚本的完整源码&#xff0c;适合已掌握AutoJs基础语法、希望研究实际项目结构与交互逻辑的中级用户参考。压缩包为7z格式&#xff0c;仅含1个js文件&…

作者头像 李华
网站建设 2026/10/22 16:35:36

MATLAB实现Jacobi与Gauss-Seidel迭代法求解线性方程组

简介&#xff1a;面向数值计算学习者的MATLAB实现包&#xff0c;聚焦雅可比迭代法与高斯-赛德尔迭代法求解Axb型线性方程组。两种方法以迭代逼近替代直接求逆&#xff0c;尤其适合大型稀疏矩阵场景&#xff0c;便于理解迭代格式、收敛条件与算法差异。压缩包体积仅3KB&#xff…

作者头像 李华
网站建设 2026/10/21 13:16:10

STM32F401CCU6四外设同调实战:USB、USART、IIC与OLED避坑指南

简介&#xff1a;这份资源面向嵌入式开发初学者与进阶工程师&#xff0c;围绕STM32F401CCU6微控制器展开&#xff0c;整合USB、USART与IIC三大通信接口&#xff0c;并通过IIC驱动OLED显示屏&#xff0c;帮助读者掌握多协议协同开发与显示控制的完整流程。压缩包共318个文件&…

作者头像 李华