公司动态

SpringBoot+Vue健康管理系统实战:从数据库设计到部署上线

📅 2026/8/31 2:10:33
SpringBoot+Vue健康管理系统实战:从数据库设计到部署上线
简介本资源是一套面向计算机专业本科生毕业设计与课程实践的健康管理系统完整开发方案基于Spring Boot后端框架、Vue前端框架与MySQL数据库构建聚焦健康档案管理、实时监测、风险评估、个性化干预及医患互动等核心业务场景。压缩包共含前端Vue源码、后端SpringBoot工程、MySQL建库脚本及详细部署文档总计8.21MB文件结构清晰涵盖系统管理、用户权限、随访中心、健康百科等九大功能模块便于学习者快速理解全栈开发流程与医疗信息化系统设计逻辑。目前已有1639人下载学习适用于毕业论文选题参考、JavaVue综合实训项目复现或健康类信息系统二次开发基础支撑。1. 为什么做健康管理系统被纸质体检报告逼出来的项目先说个真实的场景。前两年公司组织年度体检我拿到手的是一沓A4纸上面密密麻麻印着几十项指标有的偏高有的偏低但医生只给了一句定期复查就结束了。最让人头疼的是去年和今年的体检报告格式居然还不一样想对比一下血糖血脂的变化趋势我得同时摊开两份纸质报告拿尺子对着看。我当时就想这种健康数据散落在各个医院、各个年份的体检报告里完全没有被利用起来而它们恰恰是最需要被连续追踪、被结构化存储、被直观呈现的东西。这个健康管理系统就是冲着这个痛点去的。它解决的不是看病的问题而是健康数据的日常管理问题——你可以把身高体重、血压血糖、运动记录、饮食摄入统一录入到一个系统里系统自动计算BMI、评估指标是否在正常区间生成趋势曲线和健康建议所有数据永久留存随时可查、可对比、可导出。我选SpringBootVue这套组合来做不是因为它们流行而是因为这套技术栈在个人项目里有着非常务实的优势SpringBoot让后端的接口开发、数据访问、权限校验变得极其省事一个注解就能搞定路由一个starter就能集成数据库连接池Vue的前端生态则让我不用花太多精力在DOM操作上把精力集中在数据绑定和图表展示上。对于管理系统这种典型的CRUD统计分析场景没有比这组合更成熟的搭配了。这个项目适合谁如果你正在做毕业设计、课程设计或者刚开始接触前后端分离开发想找一个不是玩具的完整项目练手那它很适合你。它覆盖了一条完整的链路数据库设计、后端接口开发、鉴权体系、前端页面交互、图表可视化、打包部署。看完这篇你能带走的不只是我跑通了一个Demo而是能独立复现一个可上线、可维护的系统。下面我会把整个设计过程、核心代码逻辑、还有我实际踩过的坑全部拆开讲清楚。2. 领域建模与数据库设计先把表的边界划清楚2.1 核心实体与关系梳理做管理系统很多人一上来就写代码结果写到一半发现字段对不上、关系理不清、前端要的数据后端根本没存然后开始返工。我在动手之前先花了一天梳理实体关系这是整个项目里最值得的一笔时间投资。健康管理系统的核心实体我最终收敛成了五个用户user、健康记录health_record、运动记录exercise_record、饮食记录diet_record、健康建议health_tip。外加几个辅助表用于支撑功能比如系统通知、用户收藏的健康文章。它们的业务关系是一个用户拥有多条健康记录一条健康记录包含一次测量时点的身高、体重、血压、血糖、心率等完整指标。运动记录和饮食记录与健康记录独立按日期和用户关联——因为用户可能在同一天既记录了体检指标又记录了运动量和三餐它们从不同维度支撑健康分析。这里有一个我特别想强调的设计要点健康记录不要设计成每个字段一张表的竖表模式。我见过不少课程设计把血压一张表、血糖一张表、体重一张表然后为了展示一个趋势图前端要同时调三个接口再合并数据。正确的做法就是用一张横表一行代表一次测量记录字段冗余一点没关系查询时的性能收益和代码简洁度是巨大优势。竖表的行数膨胀、JOIN频繁、索引难以设计这在个人项目里完全没有必要。CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT 关联用户ID, height DOUBLE NOT NULL COMMENT 身高cm, weight DOUBLE NOT NULL COMMENT 体重kg, sbp INT COMMENT 收缩压高压mmHg, dbp INT COMMENT 舒张压低压mmHg, fasting_blood_sugar DOUBLE COMMENT 空腹血糖mmol/L, heart_rate INT COMMENT 静息心率次/分, measure_date DATE NOT NULL COMMENT 测量日期, record_source VARCHAR(20) DEFAULT MANUAL COMMENT 数据来源MANUAL手动录入/IMPORT批量导入, remark VARCHAR(255) COMMENT 备注, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_user_date (user_id, measure_date) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;这个表的核心索引是(user_id, measure_date)的联合索引。为什么因为健康管理系统最频繁、最核心的查询就是查某个用户在某段时间内的所有记录用这个联合索引MySQL可以直接走索引范围扫描不需要额外的文件排序。这个索引设计我是在上线前做性能测试时才补上的当时数据量到3万条之后查一年的趋势数据慢到接近2秒加上索引后直接降到30毫秒以内。2.2 单位与数据字典隐藏的坑健康管理系统的表结构里最大的坑不是关系设计而是单位。身高用什么单位厘米还是米体重是千克还是斤国内的秤很多显示斤血糖是空腹血糖还是餐后血糖血压是坐着测的还是站着测的如果不在数据库层面做强制约定后面所有的计算逻辑都会乱套。我的做法是数据库层面统一使用国际单位身高存cm、体重存kg、血糖存mmol/L、血压存mmHg这些单位在字段注释里明确写清楚。前端录入时体重输入框允许用户切换kg和斤在提交前由前端统一换算成kg后端接口再校验一遍数值范围比如身高在50cm到250cm之间、体重在10kg到300kg之间才算合法。前后端都做校验不是冗余是因为后端校验是最后一道防线前端校验是为了用户体验——用户填错了能立刻被提示不用等提交到服务器再被驳回。还要注意一个细节BMI的计算依赖身高和体重如果某条记录里身高填了1.75意思是1.75米另一条记录里身高填了175意思是175厘米BMI算出来会差10000倍。数值范围校验之所以必须做就是因为这种单位理解不一致的脏数据会直接污染计算结果和趋势图。2.3 健康建议表与伪规则引擎的设计刚开始我想把健康建议的逻辑写死在Java代码里比如if (bmi 24) 返回体重偏重注意控制饮食。后来我改变了设计原因很简单写死在代码里的规则业务人员或者我自己想调整文案必须重新编译、重新打包、重新部署。这太重了。最终方案是新增一张健康建议表把规则阈值和建议文案解耦。每个建议由几个字段描述指标类型bmi、sbp、dbp、blood_sugar等、比较符号、、between、阈值上下界、建议等级INFO/WARNING/DANGER、建议内容。后端的评估服务查询所有启用的规则对当前记录逐条匹配命中的规则就是这条健康记录对应的建议。CREATE TABLE health_tip ( id BIGINT PRIMARY KEY AUTO_INCREMENT, indicator VARCHAR(30) NOT NULL COMMENT 指标标识如bmi/sbp/fbg, operator VARCHAR(10) NOT NULL COMMENT 运算符、、between, threshold_min DOUBLE COMMENT 阈值下限, threshold_max DOUBLE COMMENT 阈值上限, advice_content VARCHAR(500) NOT NULL COMMENT 建议内容, tip_level VARCHAR(20) DEFAULT WARNING COMMENT 建议等级, enabled TINYINT DEFAULT 1 );这个设计其实就是个极简版的规则引擎。它带来的好处非常直观想调整建议文案执行一条UPDATE语句就生效不需要动代码想增加一个血氧饱和度指标的建议INSERT一条记录即可。我还给用户表加了一个birth_date字段健康建议在计算时会根据年龄做差异化判断——同样是血压130/90对年轻人可能是警告对65岁以上老人可能就属于需要关注的临界值。年龄相关的基础信息在注册时完善之后评估逻辑就能自动适配不同人群这是写死在代码里很难做到灵活的。3. 后端SpringBoot实现从JWT认证到指标计算的完整链路3.1 工程结构与统一响应封装后端工程我用了标准的Maven单模块结构没有拆多模块——虽然很多人推荐微服务拆分但对这个体量的管理系统来说单模块足够拆了反而增加维护成本。结构如下health-backend/ ├── pom.xml └── src/main/java/com/health/ ├── HealthApplication.java ├── config/ # 跨域、全局异常、MyBatis-Plus配置 ├── controller/ # 接口层 ├── service/ # 业务逻辑层 ├── mapper/ # MyBatis-Plus数据访问层 ├── entity/ # 数据库实体 ├── dto/ # 请求响应对象 ├── common/ # 统一返回、分页、常量 └── util/ # JWT、加密等工具pom.xml里依赖就几个关键的spring-boot-starter-web、mybatis-plus-boot-starter3.5.3版本、mysql-connector-j、jjwt0.9.1JWT令牌、hutool-all工具类省得自己写日期转换、lombok。没有引入spring-cloud一整套东西那个对这个项目来说完全是过度设计。SpringBoot版本我用的2.7.x而不是3.x原因是3.x要求JDK17而且一些starter的兼容性需要额外适配。如果你只是想快速把项目跑起来、把精力花在业务逻辑上2.7 JDK8/11这套组合是最稳的网上查资料踩坑也少。统一响应封装是必须做的。我定义了一个ResultT类包含code、message、data三个字段。所有Controller接口都返回这个类型前端axios拦截器统一判断code如果code不等于200就统一弹出错误提示。这么做的好处是错误处理逻辑集中在前端拦截器里业务代码里不用每个请求都写一遍错误分支。Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(String message) { ResultT result new Result(); result.setCode(500); result.setMessage(message); return result; } }3.2 认证与权限一张表搞定管理员和普通用户这个系统有两类角色管理员和普通用户。管理员负责管理健康建议、查看文章、查看所有用户数据普通用户只能管理自己的数据。我一开始考虑过引入Spring Security 一套复杂的权限体系后来放弃了。不是Spring Security不好而是对这个项目来说太重了——它的过滤器链、认证管理器、权限表达式一套配置下来少说几百行。我更倾向于用一个轻量级的拦截器JWT方案这就是够用且可控的权限设计。用户表里加一个role字段值为ADMIN或USER。登录成功后后端生成JWT令牌把用户ID和角色放进token里。然后写一个拦截器实现HandlerInterceptor接口在preHandle方法里从请求头的Authorization字段取出token解析校验把用户信息放进ThreadLocal供后续业务方法使用。public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录和注册接口 if (request.getRequestURI().contains(/auth/)) { return true; } String token request.getHeader(Authorization); if (token ! null token.startsWith(Bearer )) { Claims claims JwtUtil.parseToken(token.substring(7)); if (claims ! null) { UserContext.set(claims.get(userId, Long.class), claims.get(role, String.class)); return true; } } response.setStatus(401); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }角色控制我用了一个自定义注解RequireRole(ADMIN)配合方法拦截来实现。在需要管理员权限的Controller方法上打上这个注解在拦截器里再判断一次用户角色。实现成本很低但权限控制的表达变得非常清晰。密码存储用的是BCrypt哈希这是Spring Security里的BCryptPasswordEncoder但我不需要引入整个Spring Security直接把那个类拷出来用或者用jBCrypt库也行。明文密码是绝对不存的哪怕是自己练手的项目也应该养成这个习惯。3.3 指标计算与趋势分析的Service层设计健康记录的核心计算逻辑有两个单次测量的健康评估和一段时间内的趋势变化。单次测量的健康评估核心是计算BMI和判断指标是否在正常范围内。BMI的公式非常简单但注意实现时的类型处理身高在数据库里存的是cm换算成米时要除以100计算结果是double保留一位小数。public double calculateBmi(HealthRecord record) { double heightM record.getHeight() / 100.0; return BigDecimal.valueOf(record.getWeight() / (heightM * heightM)) .setScale(1, RoundingMode.HALF_UP) .doubleValue(); }趋势分析则是调复杂度的关键点。前端需要展示近30天、近90天、近1年的体重变化曲线最粗糙的做法是前端把全部记录拉回来再自己过滤但数据量大了之后响应体越来越大。我的做法是后端提供一个趋势接口参数是userId、startDate、endDate、indicatorType要查询的指标比如weight、sbp、fasting_blood_sugarSQL里用WHERE条件过滤时间范围只返回目标指标和日期两个字段。为了保证曲线平滑我还做了一步处理如果某一天有多条记录只取当天平均值。这一步在SQL里用AVG函数配合GROUP BY DATE(measure_date)实现。多天平均的好处是用户一天测了三次血压晨起、午后、睡前曲线只显示一个点不会因为一天内的波动让整条曲线看起来像锯齿。3.4 定时任务与健康提醒健康管理不应该是用户打开系统才能看到数据那样活跃度会非常低。我加了一个定时任务每天上午9点扫描前一天有健康记录的用户分析记录中是否存在异常指标如果有异常给用户推送一条提醒在系统内生成通知记录。实现方式是在SpringBoot启动类加上EnableScheduling然后定义一个定时任务类。用Scheduled(cron 0 0 9 * * ?)指定每天9点执行。这里有个实践细节定时任务的查询不要直接扫全表而是先查出前一天有记录的用户ID列表再针对每个用户查询其最近3次的记录评估避免高频率的全表扫描。这个模块我做得比较克制没有引入消息队列。项目里所有提醒都是写库用户登录系统后在首页的通知栏看到未读数量。引入RabbitMQ、Kafka对这种体量的项目来说纯属给自己找事数据量到了一定级别再考虑异步化也完全来得及。4. 前端Vue的交互设计让健康数据看得懂而不是堆表格4.1 路由与权限控制的落地方式前端我用的Vue 3 Vite Vue Router 4 Pinia Element Plus ECharts。Vite作为构建工具比Webpack快太多尤其在开发环境冷启动和热更新体验上简直是质的飞跃。路由设计分为两大部分公共页面和需要登录的页面。登录页、注册页放在公共区首页、健康记录管理、趋势图表、健康建议、个人中心都放在需要授权的Layout布局下。const routes [ { path: /login, component: Login }, { path: /register, component: Register }, { path: /, component: Layout, redirect: /home, meta: { requiresAuth: true }, children: [ { path: home, component: Home, meta: { title: 健康总览 } }, { path: record, component: RecordManage, meta: { title: 健康记录 } }, { path: chart, component: TrendChart, meta: { title: 趋势分析 } }, { path: tip, component: HealthTip, meta: { title: 健康建议 } }, { path: profile, component: Profile, meta: { title: 个人中心 } } ] } ]权限控制落地在路由守卫里。每次路由跳转前检查meta.requiresAuth如果为true就看看Pinia store里有没有token和用户信息。没有就跳转登录页并且记录下当前要去的路径登录成功后自动跳回。前端光有路由守卫还不够按钮级的权限控制也要做。管理员能看到健康建议管理的入口和用户管理的入口普通用户看不到。我用了一个简单的自定义指令v-permission在按钮或菜单上标注需要的角色指令内部判断当前用户角色是否匹配不匹配就移除这个DOM元素。这种方法比在每个页面里写v-ifrole ADMIN干净得多。4.2 组件划分ECharts图表组件怎么和业务解耦趋势分析页面的核心是一堆图表体重趋势折线图、血压趋势双轴图、血糖变化曲线。如果直接在页面组件里写echarts.init页面会越来越臃肿而且图表需要在数据加载完成后重新渲染生命周期管理很容易出错。我封装了一个通用的BaseChart.vue组件它接收一个optionprop内部负责初始化ECharts实例、监听option变化并重新setOption、窗口resize时自动调用chart.resize、组件卸载时销毁实例。业务页面只需要根据接口数据组装出ECharts的option对象传进来完全不关心图表的生命周期。!-- BaseChart.vue -- template div refchartRef classchart-container/div /template script setup import * as echarts from echarts import { ref, onMounted, onBeforeUnmount, watch, nextTick } from vue const props defineProps({ option: { type: Object, required: true } }) const chartRef ref(null) let chart null function renderChart() { if (!chart) { chart echarts.init(chartRef.value) } chart.setOption(props.option) } function handleResize() { chart chart.resize() } onMounted(() { renderChart() window.addEventListener(resize, handleResize) }) watch(() props.option, () { nextTick(() renderChart()) }, { deep: true }) onBeforeUnmount(() { window.removeEventListener(resize, handleResize) chart chart.dispose() }) /script血压的双轴图是个难点。收缩压和舒张压的数值区间都在同一量级80-180之间所以其实不需要双Y轴一条轴就够。但如果你要同时展示体重kg和血压mmHg这两个量级差很多就要配置双Y轴了。ECharts里的做法是在yAxis数组里定义两个轴左侧轴关联体重系列右侧轴关联血压系列。这个封装组件只是接收option不关心业务逻辑所以双轴图的使用方式和其他图完全一样。4.3 表单校验与单位换算的前端处理健康记录的录入表单是整个系统里用户最常操作的表单它的体验直接决定了这个系统好不好用。我的表单分为两块基础身体数据身高、体重和生化指标血压、血糖、心率。单位换算的处理我放在表单提交前的拦截函数里。表单允许用户选择体重的显示单位kg/斤默认是kg。如果用户选了斤并输入了140提交时自动除以2转为70kg再传给后端。身高同理允许输入厘米不做复杂的米/厘米切换因为针对日常使用厘米是最直观的。表单校验用的是Element Plus的rules配置。我这里想分享一个心得数值类的校验不要只校验必填一定要校验范围。我遇到过用户把血压的收缩压填成了800BMI直接变成几百趋势图被这一个脏点拉得完全没法看。所以每个数值字段都加了范围校验身高50-250cm体重10-300kg血压40-260mmHg血糖0.5-30mmol/L心率30-220次/分。超出范围直接拦截并给出友好提示这一条规则让后续的统计计算省了无数心。5. 部署上线的完整操作记录从本地跑通到服务器可用5.1 环境准备与版本匹配部署环节是这个系统真正从毕设Demo走向能用的产品的分水岭。我在本地调试和线上部署时踩了不少环境相关的坑先把版本对应关系整理清楚。组件本地开发版本服务器版本说明JDK1.81.8SpringBoot 2.7.x推荐JDK8或11Maven3.8.x3.8.x后端打包构建Node.js16.20.216.20.2Vite 4要求Node 14.18MySQL8.08.0使用InnoDB引擎Nginx-1.24.0前端静态资源服务反向代理这里特别提醒一点Node.js不要图新直接上20的大版本有些老项目依赖原生模块编译不过。实测Vite 4 Node 16.20.2最稳如果用了node-sass这类有编译期依赖的包Node版本更要谨慎。5.2 数据库初始化与账号配置数据库初始化我用了一份init.sql脚本建库、建表、插入初始数据一步到位。首次部署时执行mysql -u root -p /opt/health-system/init.sql脚本里包含创建数据库health_system、五张业务表的CREATE TABLE语句、初始管理员账号admin/admin123密码字段存的是BCrypt哈希、默认的健康建议规则数据。线上数据库账号不要用root我是单独创建了一个专用账号只授予这个库的增删改查权限CREATE USER health_applocalhost IDENTIFIED BY 你的强密码; GRANT SELECT, INSERT, UPDATE, DELETE ON health_system.* TO health_applocalhost; FLUSH PRIVILEGES;这样即使后端被拖库数据库账号也拿不到系统级的权限安全边界至少是清晰的。SpringBoot的application-prod.yml配置文件单独放一份数据库连接地址用环境变量占位比如jdbc:mysql://${DB_HOST}:3306/health_system部署时在systemd里通过Environment注入。这样配置文件和代码彻底分离数据库密码不会出现在jar包里。5.3 前后端打包与Nginx反向代理后端打包mvn clean package -DskipTests # 目标文件target/health-backend-1.0.0.jar前端构建npm install npm run build # 构建产物dist/ 目录前端构建产物是一个纯静态目录我把它放到服务器的/opt/health-system/dist下用Nginx服务。同时Nginx配置反向代理把/api/开头的请求转发给后端的http://127.0.0.1:8080。server { listen 80; server_name your-domain.com; # 前端静态资源 root /opt/health-system/dist; index index.html; # 前端路由history模式所有非文件路径回退到index.html location / { try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这里有几个细节值得注意。try_files $uri $uri/ /index.html;这一行是必须的因为Vue Router使用了history模式如果没有它用户直接访问/home或者刷新/home页面会得到一个404。proxy_pass结尾的/也很有讲究/api/会被替换成http://127.0.0.1:8080/也就是说/api/record/list会转发为http://127.0.0.1:8080/record/list这样后端Controller里就不需要在路径上统一带/api前缀了。5.4 systemd守护进程配置后端Java进程不能直接java -jar跑在终端里终端一关进程就没了。我用systemd把它注册成系统服务这样它能开机自启、异常退出自动重启、日志也能被journald统一管理。[Unit] DescriptionHealth System Backend Afternetwork.target mysqld.service [Service] Userdeploy WorkingDirectory/opt/health-system EnvironmentDB_HOST127.0.0.1 EnvironmentDB_PORT3306 EnvironmentDB_NAMEhealth_system EnvironmentDB_USERNAMEhealth_app EnvironmentDB_PASSWORD你的强密码 ExecStart/usr/bin/java -Xms256m -Xmx512m -jar /opt/health-system/health-backend-1.0.0.jar --spring.profiles.activeprod Restarton-failure RestartSec10 [Install] WantedBymulti-user.target-Xms256m -Xmx512m是我根据服务器2G内存情况设置的堆大小别一上来就给1G甚至2G服务器是1核2G的话堆给512m已经够这个系统跑了。Restarton-failure是保证进程崩了能自动拉起来我之前遇到过凌晨一次内存溢出把进程带崩第二天早上用户才发现服务挂了加上这个之后再也没出过这种问题。配置写好后sudo systemctl daemon-reload sudo systemctl enable health-backend sudo systemctl start health-backend部署完成后整个验证路径就是访问http://服务器IP看到前端登录页用管理员账号登录新增一条健康记录查看趋势图再检查后端日志里有对应的SQL执行记录。一套走通系统就算正式上线了。6. 实测踩过的坑和对应的解决办法6.1 跨域配置拦了我一个小时前后端分离开发时前端跑在localhost:5173Vite默认端口后端跑在localhost:8080浏览器的同源策略会拦截所有POST请求。这个问题开发第一天就会遇到。解决方案是在后端加一个CORS配置类允许指定来源、指定请求头、指定方法。这里有个特别容易被坑的点跨域预检请求OPTIONS必须放行并且 allowedHeaders 一定要包含 Authorization。因为自定义的JWT令牌是放在请求头里的如果allowedHeaders没有显式加上Authorization浏览器预检阶段就直接失败了前端看到的就是Network Error这个错误信息非常有误导性会让你以为是网络问题。Configuration public class CorsConfig { Bean public CorsFilter corsFilter() { CorsConfiguration config new CorsConfiguration(); config.addAllowedOriginPattern(*); config.addAllowedHeader(*); config.addAllowedMethod(*); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration(/**, config); return new CorsFilter(source); } }注意配置里的allowCredentials(true)它表示允许携带Cookie。我的项目虽然用的是JWT而非Cookie但开了这个选项后前端请求头里携带Authorization不会被拦截。如果你不设这个字段即使代码里加了Authorization头浏览器也可能不让它生效。6.2 JWT过期后前端刷新页面的白屏问题这是一次典型的前端状态管理问题。JWT令牌默认有效期我设置的是24小时用户隔天打开系统时令牌已经过期但Pinia store里的用户信息还在因为刷新页面时Pinia会重新从localStorage读取。此时用户页面可以正常显示但所有接口调用都返回401。最糟糕的情况出现在某个页面的created钩子里。它依赖接口返回值来渲染图表理想流程是取到数据 → 渲染图表但当401时代码会进入catch分支图表不渲染页面看起来就是一个白屏。用户不知道发生了什么也不知道要重新登录。我的解决方案有两点。第一axios响应拦截器里全局处理401遇到401就清除本地存储的token和用户信息跳转登录页并且用ElMessage提示登录已过期请重新登录。第二在路由守卫里判断token是否存在而不是仅判断Pinia store里的对象——因为store是内存态刷新会清空但路由守卫要等store初始化完成后才能拿到正确的判断结果。// axios响应拦截器 service.interceptors.response.use( response response, error { if (error.response error.response.status 401) { localStorage.removeItem(token) localStorage.removeItem(userInfo) router.push({ path: /login, query: { redirect: route.fullPath } }) } return Promise.reject(error) } )6.3 ECharts在v-if容器里的渲染尺寸问题这是我花了最多时间排查的前端问题。图表页面的Tab切换用了el-tabs或者v-if来切换不同的图表卡片第一次切到趋势分析Tab时图表区域显示是正常的但一旦切换到别的Tab再切回来图表就变成了一块扁平的、被挤压的区域宽度或高度变成了0。问题根源是ECharts在初始化时读取的是容器DOM的宽度和高度如果此时容器被display: none隐藏它读到的尺寸就是0。即使容器在数据加载后显示了ECharts实例的尺寸仍然停留在初始化时的0值不会自动更新。解决方案有两个思路。第一个是用v-show而不是v-if来切换图表显示因为v-show只是CSS的display切换不会销毁DOMECharts实例持有的是同一个DOM引用尺寸信息不会被重置。第二个是如果非要用v-if因为懒加载确实能减少初始渲染开销那么每次创建图表实例之前先判断容器尺寸是否合法不合法就等nextTick后再初始化并在容器显示后调用chart.resize()。最终我的BaseChart组件里增加了对resize的监听并且在高阶封装中为每个图表传入了一个visibleprop当visible从false变为true时延迟到nextTick再调用chart.resize()。实测下来两种方式都能解决但v-show resize监听更省心。6.4 接口性能健康趋势查询的SQL优化数据量在几千条时趋势查询没感觉。但当我用脚本造了三万条测试数据后体重趋势接口的响应时间从50ms飙升到接近2秒。用MySQL的EXPLAIN一分析发现查询是全表扫描没有走任何索引。问题定位很清晰虽然health_record表上有(user_id, measure_date)的联合索引但趋势接口的SQL写成了WHERE user_id ? AND measure_date BETWEEN ? AND ?MySQL优化器在某些条件下会选择全表扫描因为表的记录数不算特别大优化器认为走索引的回表成本可能更高。我的解决方式是强制SQL走索引使用MyBatis-Plus的QueryWrapper指定索引QueryWrapperHealthRecord wrapper new QueryWrapper(); wrapper.eq(user_id, userId) .between(measure_date, startDate, endDate) .last(FORCE INDEX(idx_user_date));强制索引加上之后查询直接降到30ms以内。后来我意识到与其强制索引不如把趋势查询单独剥离出来用独立的通知表在写入时同步计算趋势汇总值查询时直接读汇总表。这个方案叫读模型与写模型分离虽然在这个体量下有点用力过猛但思路是值得借鉴的——如果查询越来越复杂、越来越慢就考虑为查询单独建一张表或者加缓存。6.5 时间字段序列化引发的JSON格式异常前后端联调时还有一个很隐蔽的坑后端返回的LocalDateTime字段默认序列化结果是2024-05-12T09:30:00中间带一个大写的T前端用new Date()解析在某些浏览器里没问题但用Element Plus的日期组件回显时就会报 invalid date。解决方案是在application.yml里统一配置Jackson的日期格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8这样所有LocalDateTime字段序列化成2024-05-12 09:30:00前端解析无压力。这个配置在本地开发时没暴露问题是因为前端的日期组件在某些实现里能自动兼容ISO格式但换一个组件就暴露出来了。这种本地没问题、上线出状况的坑最烦人提前统一格式能省很多心。7. 一些真心话与后续扩展方向项目做到这里功能基本完整代码也能跑、能部署、能上线。但你要问我这个系统最大的价值是什么我现在的答案是它让我把前后端分离开发的全流程走了一遍并且深刻认识到一个道理——系统好不好用核心不是技术栈多牛而是数据模型设计得好不好。健康管理系统的本质是把现实世界里散乱、多源、异构的健康数据转换成结构化、可查询、可计算的形式。数据库表设计时多花的那一天时间后面省下的可能是十个晚上的返工。单位的统一、时间字段的格式、索引的设计、状态字典的约定这些看着不起眼的小事才是系统稳定运行的关键。后续我计划在几个方向做扩展一个是接入设备数据现在市面上很多体脂秤、血压计都开放了蓝牙或云端API如果能自动同步数据就能彻底解放用户手动录入的负担一个是增加健康报告的PDF导出功能把一段时间的趋势和评估结果生成一份专业报告这对用户去线下就医时非常有用还有一个是用更细粒度的健康评分模型替代目前的规则引擎对用户的整体健康状态做一个综合评分用一个数字直观呈现今天的状态好不好。最后一个关于部署的小技巧分享如果你的服务器配置不高在application-prod.yml里把MySQL连接池的maximum-pool-size调小一点默认的HikariCP连接池会按CPU核心数计算默认值2核机器上默认给到10个连接但对于这种一人一个账号的管理系统同时在线人数通常个位数连接池给5个完全够用省下来的内存留给JVM堆更合适。截止到这里这个项目从选型、建模、编码到部署的全过程就都讲完了。里面所有的代码片段、配置、命令都是我从实际项目中拷出来的你用的时候按实际情况改改参数就行。如果还有哪个环节需要深挖欢迎评论区交流。本文还有配套的精品资源点击获取