ARTICLE DETAIL

资讯详情

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

SSM+Vue前后端分离:个人健康管理系统开发实战解析

SSM+Vue前后端分离:个人健康管理系统开发实战解析 在开发个人健康信息管理系统之前我其实考虑过很多方案。有过用纯HTML加jQuery的念头也心动过直接用现成的开源健康类App但最后都被自己否了。原因是体检报告、日常体征数据、运动记录这些零散信息散落在不同平台和纸面单据上想集中管理并且能自由定制功能还是得自己动手做一套。最终我选定了SSMSpring SpringMVC MyBatis作为后端基础框架前端使用Vue来构建交互页面做成了现在这个前后端分离的个人健康信息管理系统。开发过程中踩坑不少也沉淀了一些实操经验今天就系统性拆解一下这套系统的核心设计思路、关键实现和常见问题希望能给正在做类似项目的人一些参考。这套系统适合什么人参考如果你是Java后端新手想弄明白SSM框架如何与Vue进行数据交互或者你是有一定经验的前端想了解一个完整管理系统从数据库设计到接口联调的全过程又或者你纯粹想给家人做一个健康数据记录工具——这篇文章的内容应该都够你用。1. 项目定位与整体架构设计1.1 为什么选择SSM Vue的组合先说后端。很多人看到现在Spring Boot那么火会疑惑为什么我还守着SSM这套稍微老一点的框架。实际上SSM框架的核心组件Spring IoC、SpringMVC的请求分发机制、MyBatis的ORM映射这些知识在Spring Boot项目中同样是基础中的基础。Spring Boot只是做了大量自动配置底层还是这些东西。自己用SSM搭建一遍对请求从浏览器到Controller再到Mapper的完整调用链会理解得更通透而且搞懂了SSM切到Spring Boot几乎是零成本。再看前端选Vue而不是React或者Angular原因很纯粹。Vue的模板语法直白单文件组件结构清晰对个人项目来说学习曲线最平缓同时生态里像Element UI、Vue Router、Vuex这些配套方案都非常成熟。一个几百页健康管理需求的中小型系统Vue完全撑得起来而且开发效率非常高。1.2 系统功能模块划分在动工之前我先把整个系统的功能边界画清楚。个人健康信息管理系统听起来范围很大但实际聚焦下来核心无非就四大块用户管理注册、登录、个人信息维护、密码修改。健康档案基本信息录入、既往病史、过敏史、家族病史。体征数据身高、体重、血压、血糖、心率等指标按日期记录支持趋势展示。健康提醒与建议根据BMI和血压等指标生成简单的健康评估建议设置周期性体检提醒。这四个模块覆盖了“记录-展示-评估”这条完整链路。刚开始做的时候千万别贪多比如想加医疗报告上传、在线问诊、药物管理这些功能复杂度会瞬间爆表。先跑通一个最小可用闭环后续再渐进式增加模块这是个人项目最稳妥的推进方式。我实际开发时第一版就是上面四个模块系统稳定运行后第二版才加上用药提醒和报告文件管理。1.3 前后端分离的通讯约定前后端分离意味着前端静态页面和后端Java应用是两个独立的部署单元通过HTTP接口进行数据交互。这就要求两端必须提前约定好数据格式和接口路径。我这里统一采用RESTful风格接口数据交互格式固定为JSON返回结构定死成下面这个模板{ code: 200, message: 操作成功, data: { } }code表示业务状态码200成功40x表示参数或权限问题500表示服务器异常。message给前端做提示展示data承载实际业务数据。这样约定的好处是前端可以统一在axios拦截器里处理异常不需要每个接口单独写错误判断。接口路径按资源命名比如POST /api/user/register用户注册POST /api/user/login用户登录GET /api/health/records健康记录列表POST /api/health/records新增健康记录GET /api/health/trend?typeweight体重趋势数据PUT /api/health/profile更新个人信息这套约定写进项目README前后端各拿一份开发时直接按契约并行推进完全不互相等。2. 数据库设计与后端核心实现2.1 数据表设计与字段规划个人健康系统数据量不大但对数据准确性要求高所以表结构设计要充分考虑字段约束。我设计了四张核心表字段规划如下。用户表t_user字段名类型说明idbigint主键自增usernamevarchar(50)登录名唯一索引passwordvarchar(100)BCrypt加密后的密码nicknamevarchar(50)昵称phonevarchar(20)手机号create_timedatetime注册时间健康档案表t_profile字段名类型说明idbigint主键user_idbigint关联用户idgendertinyint性别1男2女birth_datedate出生日期heightdecimal(5,2)身高cmblood_typevarchar(10)血型medical_historytext既往病史allergy_historytext过敏史family_historytext家族病史体征数据表t_health_record字段名类型说明idbigint主键user_idbigint关联用户idrecord_datedate记录日期weightdecimal(5,2)体重kgsystolicint收缩压高压diastolicint舒张压低压heart_rateint心率blood_sugardecimal(4,2)空腹血糖mmol/Lremarkvarchar(255)备注健康提醒表t_reminder字段名类型说明idbigint主键user_idbigint关联用户idremind_typevarchar(20)提醒类型体检/吃药/运动remind_timedatetime提醒时间contentvarchar(255)提醒内容statustinyint0未触发1已触发这里有个细节需要注意user_id全部加上普通索引因为所有查询都带用户维度不加索引的话数据量大了以后查询会明显变慢。MySQL在建表时顺手加上ENGINEInnoDB DEFAULT CHARSETutf8mb4utf8mb4才能完整支持中文和特殊符号。2.2 JWT鉴权机制与实现细节健康数据是非常敏感的个人隐私系统不可能不登录就直接访问。我采用的是JWTJSON Web Token方案做无状态鉴权。登录流程是这样的用户提交用户名和密码后端校验通过后生成一个签名Token返回给前端前端把Token存在localStorage里每次请求在HTTP头部带上Authorization: Bearer token后端通过拦截器校验Token的合法性和有效期。Token生成可以借助io.jsonwebtoken这个Java库核心代码相对简洁public String generateToken(Long userId) { Date now new Date(); Date expiryDate new Date(now.getTime() 7 * 24 * 60 * 60 * 1000); return Jwts.builder() .setSubject(String.valueOf(userId)) .setIssuedAt(now) .setExpiration(expiryDate) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }上面的设置我直接把Token有效期定成了7天。如果你是个强迫症可以改成2小时过期、7天续签方案但对个人项目来说7天足够用户不用频繁重新登录体验也更好。后端写一个JwtInterceptor拦截器在SpringMVC配置中注册拦截除/api/user/login和/api/user/register之外的所有接口。拦截器里解析Token如果解析失败直接返回401状态码前端收到401则跳转登录页。2.3 密码安全存储与敏感操作防护密码必须是加密存储的绝对不能明文入库。我选择BCrypt算法加密密码而不是MD5或SHA。原因是BCrypt内置随机盐值同样的密码每次加密结果都不同能有效抵抗彩虹表攻击而且算法本身设计为慢哈希暴力破解成本极高。Spring Security的BCryptPasswordEncoder可以直接拿来用不需要引入一整套Spring Security框架。注册时加密BCryptPasswordEncoder encoder new BCryptPasswordEncoder(); String encodedPwd encoder.encode(rawPassword);登录时校验if (encoder.matches(rawPassword, user.getPassword())) { // 密码正确放行登录 }有一点务必提醒matches方法第一个参数是明文第二个参数是数据库里的密文顺序写反了会一直提示密码错误。这种细节问题排查起来特别费眼力一旦遇到怎么都不对的情况先检查参数顺序。2.4 健康评估逻辑与BMI计算既然叫健康管理系统光记录数据还不够系统要能对数据做基础分析。我在后端HealthService里封装了健康评估算法以BMI身体质量指数和血压为核心指标。BMI计算公式为体重kg除以身高m的平方。身高在库里存的是厘米需要先转换成米。示例代码如下public HealthAssessment assess(HealthRecord record) { double heightM profile.getHeight() / 100.0; double bmi record.getWeight() / (heightM * heightM); String bmiLevel; if (bmi 18.5) { bmiLevel 偏瘦; } else if (bmi 24) { bmiLevel 正常; } else if (bmi 28) { bmiLevel 偏胖; } else { bmiLevel 肥胖; } String bpLevel 正常; if (record.getSystolic() 140 || record.getDiastolic() 90) { bpLevel 偏高; } else if (record.getSystolic() 90 || record.getDiastolic() 60) { bpLevel 偏低; } // 组装建议文案… }评估逻辑不复杂关键是阈值要根据国内通用的医学参考范围来定不要自己拍脑袋改标准。体重、血压的合理区间的数值我建议参考权威医学指南系统提示语要温和、非诊断性质加上一句“数据仅供参考如有不适请及时就医”既负责任又规避风险。3. Vue前端核心实现与实操过程3.1 项目搭建与环境配置前端的开发环境我使用的是统一方案——Node.js 16Vue CLI 5或Vite。如果你电脑上还没装Node.js可以去官网下载LTS版本一路下一步装好即可然后命令行验证node -v npm -v如果提示命令不存在检查是否把Node.js的安装目录加到了系统环境变量Path中。Windows系统有时候安装完不会自动刷新环境变量新开的终端窗口才生效。创建Vue项目vue create health-frontend选择预设的时候我建议选Manually select features然后把Router、Vuex勾上。CSS预处理器选Sass/SCSS因为Element UI定制主题的时候SCSS方便很多。项目创建完之后安装UI组件库npm install element-ui这里如果是Vue 3项目需要安装element-plus对应关系别搞混了。Vue 2对应Element UIVue 3对应Element Plus版本不对会导致组件无法正常渲染。3.2 Axios请求封装与拦截器配置前端与后端交互的核心就是axios。如果直接用最原始的ajax写法每个接口都要写一遍配置代码冗余且难以维护。我把axios实例统一封装在一个模块里同时加上请求拦截器和响应拦截器。新建src/utils/request.jsimport axios from axios import { Message } from element-ui import router from ../router const service axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器自动携带token service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers[Authorization] Bearer token } return config }, error { return Promise.reject(error) } ) // 响应拦截器统一处理返回数据 service.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 { if (error.response error.response.status 401) { Message.error(登录已过期请重新登录) localStorage.removeItem(token) router.push(/login) } else { Message.error(网络异常请稍后重试) } return Promise.reject(error) } ) export default service这里面的设计逻辑值得细说。请求拦截器把Token注入统一做掉了不用每个页面自己处理。响应拦截器里如果后端返回code不等于200全局弹错误消息如果HTTP状态是401说明Token过期或无效直接清理本地存储并跳回登录页。前端各个页面不用关心这些统一的逻辑专心处理业务数据就可以。实际使用的时候,页面里调用长这样import request from /utils/request // 获取体征记录列表 async function getRecords(params) { const data await request.get(/health/records, { params }) this.tableData data }3.3 路由管理与菜单权限控制前端页面分两类不需要登录的登录页、注册页和需要登录后才能看的首页、健康记录、趋势图表等。这个区分在路由配置里通过meta.requiresAuth字段标识。const routes [ { path: /login, component: Login }, { path: /dashboard, component: Layout, meta: { requiresAuth: true }, children: [ { path: , name: Dashboard, component: Dashboard, meta: { title: 健康总览 } }, { path: records, name: HealthRecords, component: HealthRecords, meta: { title: 体征记录 } }, { path: trend, name: Trend, component: Trend, meta: { title: 趋势分析 } } ] } ]然后配置全局路由守卫router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这段代码就解决了未登录用户直接访问内部页面的问题。比如用户手动在地址栏输入/dashboard没有Token会被一律引导回登录页。这里有个坑后续我还会细说就是刷新页面时store里的状态会丢失需要再从localStorage补数据这个问题的处理我在后文详细展开。3.4 表单校验与体验优化注册和健康数据录入都涉及表单前端校验是保证数据质量的第一道关口不能完全依赖后端。Element UI的el-form配合rules规则做表单校验非常方便。以注册表单为例rules: { username: [ { required: true, message: 请输入用户名, trigger: blur }, { min: 3, max: 20, message: 长度在3到20个字符之间, trigger: blur } ], password: [ { required: true, message: 请输入密码, trigger: blur }, { min: 6, max: 20, message: 密码长度需在6到20位之间, trigger: blur } ], confirmPassword: [ { required: true, message: 请再次输入密码, trigger: blur }, { validator: (rule, value, callback) { if (value ! this.form.password) { callback(new Error(两次密码输入不一致)) } else { callback() } }, trigger: blur } ] }体重、血压这类数值型字段不光要校验非空还要做合理范围校验。比如体重在10到300 kg之间血压在30到250 mmHg之间超出范围直接拦截避免用户录入明显错误的数据。这一层校验能省下后面大量数据清洗的功夫。还有一个小细节分享下日期选择器el-date-picker默认返回格式可能是Date对象或特定字符串要在value-formatyyyy-MM-dd设置一下格式否则提交给后端的数据格式大概率对不上Java端的时间解析导致后端报错或数据入库变成null。这种格式不一致的坑非常隐蔽。3.5 数据可视化展示与趋势图实现体征数据趋势图我选用的是ECharts。为什么选它因为功能全、文档详细、示例丰富而且中文社区活跃遇到问题搜一下基本都有答案。在Vue 2项目中使用ECharts先安装依赖npm install echarts4说明一下版本ECharts 5的按需引入方式有些变化Vue 2项目实测下来ECharts 4的兼容性更省心。如果你喜欢尝鲜用5也可以但注意模块引用的写法要跟着官方文档调整。按需引入以减小打包体积import echarts from echarts/lib/echarts import echarts/lib/chart/line import echarts/lib/component/tooltip import echarts/lib/component/grid import echarts/lib/component/legend import echarts/lib/component/dataZoom图表初始化放在mounted生命周期里因为此时DOM元素才真正渲染完毕。如果放在created里document.getElementById会拿到null。这个顺序问题是新手频发的报错点。mounted() { this.chart echarts.init(this.$refs.trendChart) this.loadTrendData() } methods: { async loadTrendData() { const data await request.get(/health/trend, { params: { type: weight } }) this.chart.setOption({ title: { text: 体重趋势 }, tooltip: { trigger: axis }, xAxis: { type: category, data: data.map(i i.recordDate) }, yAxis: { type: value, name: kg }, series: [{ name: 体重, type: line, data: data.map(i i.weight), smooth: true }] }) } }图表组件记得在beforeDestroy生命周期里调用this.chart.dispose()销毁实例防止页面切换后内存泄漏。还有一点很关键当页面所在容器尺寸发生变化时例如侧边栏折叠图表不会自动重绘需要调用this.chart.resize()来刷新。如果不是响应式布局这个细节可以先跳过。4. 集成联调与常见问题排查4.1 跨域请求的两种情况与解决方案前后端分离开发时跨域问题几乎必遇到。浏览器同源策略默认阻止前端页面请求不同域名的接口。我遇到的情况分成这么几类一是开发环境下前端跑在localhost:8080后端跑在localhost:8081端口不一样构成跨域。解决办法最简单的就是Webpack devServer配置代理在vue.config.js里设置module.exports { devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } } }这样前端请求/api/user/login开发服务器自动转发到http://localhost:8081/api/user/login浏览器看到的请求是同源的跨域问题消失。开发环境强烈建议用这种方式而不是在后端加CORS跨域资源共享配置因为代理配置更灵活上线改Nginx也方便。二是生产环境下前后端通常由同一个Nginx提供服务通过不同的location路径区分。前端静态文件放根目录接口请求反代到后端服务server { listen 80; server_name health.example.com; location / { root /usr/share/nginx/html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files配置很关键它确保前端路由在刷新页面时不出现404。Vue Router默认是history模式刷新/dashboard时Nginx要能找到对应的静态资源配置托底到index.html让Vue接管路由否则会出现“刷新后白屏”的经典问题。如果你不太想折腾服务端配置也可以把Vue Router改成hash模式URL会多出一个#号但能避开这个问题。4.2 后端接收不到前端参数的排查思路联调时最常出现的“后端拿不到前端传的值”的情况原因通常集中在三处其一前端用request.post(/health/records, this.form)时axios默认把对象序列化成JSON格式放在请求体里而后端Controller如果直接用HealthRecord record接收必须加RequestBody注解。如果只写了一个普通对象参数不加注解Spring会尝试从表单参数里取值结果全是null。PostMapping(/health/records) public Result addRecord(RequestBody HealthRecord record) { // ... }其二前端传的是字符串后端字段类型是Date或BigDecimalJSON反序列化时格式不对会直接报错。建议全局配置Jackson的日期格式Bean public Jackson2ObjectMapperBuilderCustomizer jacksonCustomizer() { return builder - { builder.simpleDateFormat(yyyy-MM-dd HH:mm:ss); }; }前端提交日期字符串时用yyyy-MM-dd这种格式两边匹配上就不会出问题。其三字段名大小写不一致。Java后端习惯驼峰命名前端JSON如果用了下划线或大小写拼错反序列化时对不上。可以后端使用JsonProperty指定别名但我更建议前后端约定好统一用驼峰并且前端定义数据模型时与后端字段一一对应写一个对照表贴在项目文档里联调时能省掉大量扯皮。4.3 页面刷新后用户信息丢失的问题Vuex state是存储在内存中的刷新页面后内存清空store里的用户信息就没了。很多项目第一次上线后都反馈列表页刷新白屏或跳回登录页原因就在这。解决办法有几种我的做法是用户登录成功时把用户基本信息nickname、头像等非敏感数据同时同步到localStorage。刷新时在App.vue的created生命周期或者路由守卫里读一次localStorage重新提交到Vuex。const storedUser localStorage.getItem(userInfo) if (storedUser) { store.commit(SET_USER, JSON.parse(storedUser)) }Token本来就存放在localStorage刷新后重新读取即可并不需要重新登录。那什么情况下要重新登录呢7天Token过期或者用户主动登出拦截器检测到401才会跳回登录页。这套机制跑了一段时间体验稳定。4.4 打包部署与上线注意点前端构建产线包npm run build构建完成后dist目录里就是静态文件包含index.html和一堆带hash的js、css文件。把这整个目录上传到服务器放到Nginx配置的root路径下即可。注意每次重新构建文件名hash会变化覆盖旧文件前建议先清空原目录避免旧文件残留占用磁盘。后端打包就用Mavenmvn clean package -DskipTests生成的可执行JAR包用java -jar方式运行。生产环境我建议把日志输出重定向到文件比如nohup java -jar health-server.jar logs/app.log 21 这样即使终端关闭服务也会在后台持续运行。排查问题时直接看logs/app.log比在黑窗口里翻滚动日志舒服太多了。数据库方面有个容易忽视的地方服务器时区与本地不一致的话日期时间字段入库会偏移几个小时。连接字符串上加serverTimezoneAsia/Shanghai可以解决这个问题。4.5 常见报错速查报错现象可能原因解决方案前端请求一直pending后端没启动或端口不一致检查后端进程和代理配置401 UnauthorizedToken缺失或已过期重新登录检查拦截器放行路径500 Caused by SQLSyntaxErrorExceptionSQL语句与实际表结构不匹配检查Mapper XML对齐字段名前端白屏控制台报TypeError: Cannot read property xxx of undefined接口返回结构与页面预期不一致打印response.data对比字段名中文显示乱码数据库连接字符集配置错误连接URL加characterEncodingutf8刷新页面404history路由缺少托底配置Nginx加try_files配置这里再单独讲一个我实际踩过的坑。有一段时间健康记录保存一直报500日志显示空指针异常排查了半天发现是前端体重字段留空提交后端record.getWeight()拿到null然后参与BMI计算直接NPE。后来我在后端增加了完整性的参数校验并且在计算前判断是否为空。前后端双重校验才能做到系统稳定只靠一端总会有疏漏。5. 安全防护细节与数据隐私保护5.1 防SQL注入与XSS攻击的基础措施个人健康系统涉及大量敏感个人信息安全层面不能只用一套JWT就高枕无忧。先说MyBatis的SQL注入防护。MyBatis的#{}预编译方式能有效防止SQL注入使用${}则存在注入风险。我在代码审查时要求所有动态条件拼接只能使用#{}比如select idqueryByCondition resultTypeHealthRecord SELECT * FROM t_health_record WHERE user_id #{userId} if testtype ! null AND record_type #{type} /if /select#{}在底层通过PreparedStatement参数占位符来实现用户输入永远作为数据处理不会影响SQL结构。如果用${type}做字符串替换用户输入 OR 11就可能透传进SQL语句后果不堪设想。这里需要特别警觉。XSS攻击方面前端框架Vue自带模板转义能力{{ }}插值输出的内容会自动转义HTML标签所以多数场景下基础防护是有的。但如果是通过v-html指令渲染服务端返回的富文本就要格外注意需要对内容做清洗过滤。前端可以安装xss这个npm库对用户输入内容在提交前做过滤。同时后端也要做一层输入校验前端过滤并不是安全的终点绕过前端直接请求接口的攻击路径是真实存在的。5.2 日志脱敏与隐私数据加密生产环境下系统日志里不要打印明文密码、完整身份证号、手机号。我自己习惯在日志输出前封装一个脱敏工具类比如手机号只显示前3位后4位中间用星号代替。虽然健康系统更多是个人使用但养成这个习惯意味着代码随时可以被扩展成多人使用的产品或交给别人运维安全性会高很多。敏感字段入库加密也是可以考虑的方向。比如家庭成员的电话号码用AES对称加密后存储查询时解密展示。需要注意加解密密钥不能硬编码在代码里可以通过环境变量或外部配置注入这样即使源码泄露密钥也不会暴露。6. 功能扩展思路与后续规划6.1 当前系统可以做哪些增强基础系统跑通之后后续可以扩展的方向其实非常多。我自己的计划是在现有的基础之上考虑逐步增加下面这些模块。周期趋势智能分析根据30天的体重和血压数据用简单线性回归计算变化斜率预测未来一周趋势。这个功能不需要引入复杂的机器学习框架普通的数学计算就能实现后端写好接口前端用ECharts的预测值虚线展示。电子报告管理允许用户上传PDF或图片格式的体检报告后端用MinIO做对象存储前端支持在线预览和下载。数据用OSS对象存储服务保存比存数据库更合理医疗报告文件体积不小数据库只存元数据和文件路径。多设备数据接入现在的智能手表、体脂秤都提供开放的API或支持通过健康平台同步可以对接数据来源做自动采集减少手动录入的麻烦。数据导出功能导出Excel或CSV格式的数据档案方便用户把数据带走或提供给医生参考。后端可以用Apache POI或者EasyExcel生成文件前端直接下载。6.2 从个人项目到可分享产品的距离如果目的是把项目发布到开源社区或者给别人试用需要额外做几件事完善项目文档写清楚环境要求、部署步骤和配置项说明补充基础单元测试尤其是健康评估算法和JWT鉴权逻辑这种核心部分提供Docker部署文件让用户一行命令就能启动实际上去掉个人化的硬编码配置比如数据库连接信息和密钥尽量通过环境变量配置而不是写死在配置文件里。我在最初设计时就是按“一个可分享的项目”来要求自己的代码注释、接口文档、部署文档都同步跟进。后面把项目开源出去别人看了能跑起来甚至参与贡献这件事带给我的成就感比系统本身更大。就我个人的实际体验来说SSM Vue这套组合做个人健康管理系统复杂度恰好适中——它足够让你把Java后端和前端工程化的核心知识从头到尾串一遍又不会复杂到让个人开发者望而却步。开发过程中养成的接口契约意识、数据安全意识、日志规范习惯在我后来的工作中帮助很大。这套代码目前我已经用了大半年每天记录体重和血压月末看一眼趋势图健康数据终于不再是体检报告上的一堆数字而是自己有掌控感的变化轨迹。如果你也正在动手做管理系统类的项目可以按这个思路从最小闭环出发一步一个脚印把它打磨到自己满意的状态。
返回列表