简介:这是一套面向计算机专业学生与Java全栈开发者的居民健康监测系统完整源码,采用Spring Boot后端与Uniapp前端(微信小程序)组合开发,适合作为课程设计、毕业设计或全栈练手项目。系统实现居民健康数据录入与监测、体温血压血糖记录、个人健康报告查看、图片上传存储,以及后台管理员对用户信息与权限的统一管理,覆盖前后端联调、数据库配置与文件上传等典型业务场景。资源包共1608个文件,约30.5MB,其中vue与js文件构成前端页面与交互逻辑,java文件承载后端接口与业务处理,json、yml负责配置与数据定义,png、svg、jpg等图片资源用于界面展示,另有sql脚本与bat启动脚本辅助部署。目前已有95人学习下载。读者可据此掌握Spring Boot接口设计、Uniapp跨端开发、健康数据建模与上传路径处理等要点,并参考application.yml中的数据库连接配置快速完成本地运行与二次开发。
1. 从一份能跑起来的健康监测源码说起
居民健康监测系统这个词,做 Java 课程设计或者毕业设计的人应该不陌生。但真正拿到一份能跑通的源码,和自己从零搭一个,完全是两回事。这份基于 Spring Boot 和 Uniapp 的居民健康监测系统,解决的就是「后端接口 + 跨端前端」这条链路上最耗时的部分——数据模型怎么设计、接口怎么分层、Uniapp 怎么同时适配小程序和 App。它适合正在做健康管理类项目、需要一套可运行参考实现的开发者,也适合想学 Spring Boot 分层架构和 Uniapp 跨端开发的学生。源码包里包含完整的后端服务、前端页面和数据库脚本,拿到手就能本地跑起来看效果。
2. 后端骨架拆解:Spring Boot 分层与数据模型怎么落地
2.1 为什么是 Spring Boot + MyBatis 这套组合
健康监测系统的核心数据是「用户—健康记录—预警」三层结构。用户表存基本信息,健康记录表按时间维度存血压、血糖、心率等指标,预警表则根据阈值规则触发。这种结构用 Spring Boot + MyBatis 来落地是最顺手的:Spring Boot 负责依赖注入和 Web 层,MyBatis 负责把健康记录的动态查询条件(按用户、按时间段、按指标类型)映射成 SQL。
常见做法是把项目分成 controller、service、mapper、entity 四层。controller 只做参数校验和响应封装,service 写业务规则(比如血压超过 140/90 就写一条预警),mapper 对应 XML 或注解 SQL。这样拆的好处是,后面要加新的健康指标类型,只需要在 entity 和 mapper 里加字段,service 层改一下阈值判断逻辑就行,不用动 controller。
我一般会在 application.yml 里把 MyBatis 的 mapper-locations 和 type-aliases-package 配好,避免启动时找不到 XML 或者实体类别名报错。数据库连接池用 HikariCP,Spring Boot 默认就是它,不用额外配。
2.2 健康记录接口的实现步骤
先看一个典型的健康记录新增接口。假设前端传过来的是 JSON,包含 userId、systolic、diastolic、heartRate、recordTime 这几个字段。
// HealthRecordController.java @RestController @RequestMapping("/api/health") public class HealthRecordController { @Autowired private HealthRecordService healthRecordService; @PostMapping("/record") public Result addRecord(@RequestBody @Valid HealthRecordDTO dto) { // 参数校验由 @Valid 完成,这里直接调 service healthRecordService.saveRecord(dto); return Result.success("记录已保存"); } @GetMapping("/records/{userId}") public Result getRecords(@PathVariable Long userId, @RequestParam String startDate, @RequestParam String endDate) { // 按用户和时间段查历史记录 List<HealthRecordVO> list = healthRecordService.queryByUserAndDate(userId, startDate, endDate); return Result.success(list); } }这段代码里,@Valid配合 DTO 上的@NotNull、@Min等注解做参数校验,省去了手写 if-else。Result是统一响应封装,包含 code、msg、data 三个字段。queryByUserAndDate在 service 层会拼装 MyBatis 的动态 SQL 条件,mapper XML 里用<if test="startDate != null">来控制。
参数说明:userId 是用户主键,startDate 和 endDate 是字符串格式的日期,service 层会转成 LocalDate 再传给 mapper。如果前端传的日期格式不对,会在类型转换时抛异常,所以建议在 DTO 里用@DateTimeFormat指定格式。
2.3 预警规则的触发逻辑
预警不是简单地在插入记录时判断一下就行。实际场景里,同一个用户可能在短时间内多次测量,如果每次都触发预警,会刷屏。我一般会在 service 层加一个「最近一次预警时间」的判断:如果距离上次预警不到 30 分钟,就只更新记录不重复写预警。
// HealthRecordServiceImpl.java @Override public void saveRecord(HealthRecordDTO dto) { HealthRecord record = new HealthRecord(); BeanUtils.copyProperties(dto, record); healthRecordMapper.insert(record); // 判断是否需要触发预警 if (record.getSystolic() >= 140 || record.getDiastolic() >= 90) { // 查最近一次预警时间 LocalDateTime lastWarnTime = warningMapper.selectLastWarnTime(dto.getUserId()); if (lastWarnTime == null || Duration.between(lastWarnTime, LocalDateTime.now()).toMinutes() > 30) { Warning warning = new Warning(); warning.setUserId(dto.getUserId()); warning.setType("血压偏高"); warning.setRecordId(record.getId()); warning.setCreateTime(LocalDateTime.now()); warningMapper.insert(warning); } } }这里用Duration.between算时间差,比手动算毫秒数可读性好。阈值 140/90 是常见的高血压判断标准,但实际项目里应该做成可配置的,比如放在数据库的配置表里,或者用@Value从 yml 读。源码里如果写死了,你可以自己改成配置项。
3. Uniapp 前端:跨端页面与接口联调
3.1 页面结构怎么组织
Uniapp 的页面放在 pages 目录下,每个页面一个文件夹,里面是 .vue 文件。健康监测系统一般需要这几个页面:登录/注册、首页(数据概览)、记录列表、记录详情、新增记录、个人中心。首页用卡片展示最近一次血压、血糖、心率,记录列表用 scroll-view 做无限滚动。
跨端适配的关键是:不要用只在某一端支持的 API。比如uni.request是通用的,但uni.getSystemInfoSync在 H5 和 App 端返回的字段有差异。我一般会在common目录下封装一个request.js,统一处理 token 注入和错误提示。
// common/request.js const BASE_URL = 'http://localhost:8080/api'; function request(options) { return new Promise((resolve, reject) => { uni.request({ url: BASE_URL + options.url, method: options.method || 'GET', data: options.data || {}, header: { 'Content-Type': 'application/json', 'Authorization': uni.getStorageSync('token') || '' }, success: (res) => { if (res.statusCode === 200 && res.data.code === 200) { resolve(res.data.data); } else if (res.data.code === 401) { // token 过期,跳登录 uni.reLaunch({ url: '/pages/login/login' }); reject(new Error('登录已过期')); } else { uni.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(new Error(res.data.msg)); } }, fail: (err) => { uni.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); } export default request;这段封装里,BASE_URL在开发时指向本地后端,打包上线时要改成正式域名。Authorization头从本地存储取 token,如果后端用 JWT,这里就是Bearer xxx的格式。401 处理是跳登录页,用uni.reLaunch而不是navigateTo,因为要清空页面栈。
3.2 新增记录页面的表单与校验
新增记录页面需要输入收缩压、舒张压、心率、测量时间。Uniapp 的<input>组件在 H5 和 App 端表现基本一致,但type="number"在 iOS 上会弹数字键盘,在安卓上可能还是全键盘。我一般用type="digit"来强制数字键盘。
<template> <view class="form-container"> <view class="form-item"> <text>收缩压 (mmHg)</text> <input type="digit" v-model="form.systolic" placeholder="请输入收缩压" /> </view> <view class="form-item"> <text>舒张压 (mmHg)</text> <input type="digit" v-model="form.diastolic" placeholder="请输入舒张压" /> </view> <view class="form-item"> <text>心率 (次/分)</text> <input type="digit" v-model="form.heartRate" placeholder="请输入心率" /> </view> <button @click="submit">保存记录</button> </view> </template> <script> import request from '@/common/request.js'; export default { data() { return { form: { systolic: '', diastolic: '', heartRate: '', recordTime: new Date().toISOString() } }; }, methods: { async submit() { // 简单校验 if (!this.form.systolic || !this.form.diastolic) { uni.showToast({ title: '请填写血压值', icon: 'none' }); return; } try { await request({ url: '/health/record', method: 'POST', data: { ...this.form, systolic: Number(this.form.systolic), diastolic: Number(this.form.diastolic), heartRate: Number(this.form.heartRate) } }); uni.showToast({ title: '保存成功', icon: 'success' }); setTimeout(() => uni.navigateBack(), 1500); } catch (e) { // 错误已在 request 里提示 } } } }; </script>表单校验只做了非空判断,实际项目里还应该校验数值范围,比如收缩压不能超过 300。recordTime用toISOString()生成,后端用@DateTimeFormat接收。提交成功后用setTimeout延迟返回上一页,让用户看到 toast。
3.3 跨端调试的常见配置
Uniapp 的manifest.json里要配好 appid 和各端参数。微信小程序端需要在mp-weixin里填 appid,App 端需要在app-plus里配权限。如果后端接口在本地,微信小程序开发工具需要勾选「不校验合法域名」,否则请求会被拦截。
H5 端跨域问题用 devServer 的 proxy 解决,在vue.config.js里配:
// vue.config.js module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '/api' } } } } };这样前端请求/api/health/record会被代理到http://localhost:8080/api/health/record,开发时不用改 BASE_URL。打包上线时把 BASE_URL 改成正式域名即可。
4. 避坑与排查:从启动失败到跨端白屏
4.1 后端启动报「找不到 mapper XML」
现象:Spring Boot 启动时抛Invalid bound statement (not found),或者 mapper 接口注入失败。
原因:MyBatis 默认只扫描 resources 目录下的 XML,如果 XML 放在 java 目录下,或者 mapper-locations 没配,就会找不到。
解决:在 application.yml 里加mybatis.mapper-locations: classpath:mapper/*.xml,并确保 XML 文件的 namespace 和 mapper 接口全限定名一致。如果 XML 放在 java 目录下,需要在 pom.xml 的 build 里配 resources 包含**/*.xml。
4.2 数据库连接池报「连接超时」
现象:启动时 HikariCP 报Connection is not available, request timed out。
原因:数据库地址、用户名、密码配错,或者数据库服务没启动。也可能是连接池最大连接数太小,并发请求时不够用。
解决:先检查 application.yml 里的spring.datasource配置,用 Navicat 或命令行连一下数据库确认能通。如果连接数不够,把spring.datasource.hikari.maximum-pool-size调到 20 左右。开发环境一般 10 就够。
4.3 Uniapp 小程序端请求被拦截
现象:微信小程序开发工具里请求后端接口,控制台报「不在以下 request 合法域名列表中」。
原因:微信小程序要求所有请求域名必须在后台配置,开发时可以用「不校验合法域名」绕过。
解决:在微信开发者工具右上角「详情」→「本地设置」里勾选「不校验合法域名、web-view(业务域名)、TLS 版本以及 HTTPS 证书」。上线前需要在微信公众平台配置正式域名。
4.4 App 端白屏或页面不渲染
现象:Uniapp 打包成 App 后,启动白屏,或者某个页面空白。
原因:可能是路由配置错误,或者页面路径在 pages.json 里没注册。也可能是某个 API 在 App 端不支持,导致 JS 报错。
解决:先看 HBuilderX 的控制台有没有报错。检查 pages.json 里 pages 数组是否包含了所有页面路径。如果用了uni.getSystemInfoSync的某些字段,在 App 端可能返回 undefined,加个默认值。白屏最常见的原因是首页路径写错,或者condition配置影响了启动页。
4.5 健康记录时间格式不一致
现象:前端传的日期后端解析失败,或者存到数据库后时间差了 8 小时。
原因:前端用toISOString()生成的是 UTC 时间,后端如果按本地时间解析,就会差 8 小时。数据库字段类型如果是 datetime,也要注意时区。
解决:统一用yyyy-MM-dd HH:mm:ss格式传输,后端 DTO 上加@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")。数据库连接 URL 加serverTimezone=Asia/Shanghai。
5. 进阶技巧:把健康数据导出成趋势图
5.1 用 ECharts 在 Uniapp 里画血压趋势
Uniapp 里画图表,常见方案是 uCharts 或者 ECharts 的 uni 版本。ECharts 功能全,但包体积大;uCharts 轻量,但配置项少。如果只是画血压、血糖的折线图,uCharts 够用。我一般会在记录列表页顶部加一个趋势图,按时间轴展示最近 7 次的收缩压和舒张压。
// 在页面里引入 uCharts import uCharts from '@/components/u-charts/u-charts.js'; export default { data() { return { chartData: { categories: [], series: [ { name: '收缩压', data: [] }, { name: '舒张压', data: [] } ] } }; }, methods: { async loadChart() { const records = await request({ url: '/health/records/' + this.userId }); // 取最近 7 条,按时间正序 const recent = records.slice(-7); this.chartData.categories = recent.map(r => r.recordTime.slice(5, 10)); this.chartData.series[0].data = recent.map(r => r.systolic); this.chartData.series[1].data = recent.map(r => r.diastolic); // 初始化图表 new uCharts({ type: 'line', context: uni.createCanvasContext('healthChart', this), width: uni.upx2px(750), height: uni.upx2px(400), categories: this.chartData.categories, series: this.chartData.series, legend: { show: true } }); } } };这段代码里,categories是 X 轴标签,取日期的月-日部分。series是两条折线,分别对应收缩压和舒张压。uni.upx2px把 upx 转成 px,保证不同屏幕宽度下图表尺寸一致。注意 canvas 的 id 要和createCanvasContext里的 id 一致。
5.2 后端加一个统计接口
趋势图的数据如果每次都从记录列表里取,数据量大时会有性能问题。更好的做法是后端加一个统计接口,按天聚合。
-- 按天统计平均血压 SELECT DATE(record_time) AS day, AVG(systolic) AS avg_systolic, AVG(diastolic) AS avg_diastolic FROM health_record WHERE user_id = #{userId} AND record_time >= DATE_SUB(NOW(), INTERVAL 7 DAY) GROUP BY DATE(record_time) ORDER BY day ASC;这个 SQL 用DATE()函数把时间截断到天,然后按天分组求平均。DATE_SUB(NOW(), INTERVAL 7 DAY)取最近 7 天。mapper 里返回一个List<Map<String, Object>>,service 层转成前端需要的格式。
5.3 一个我踩过的坑:canvas 在 iOS 上导出白图
之前用 canvas 画完图表后,想加一个「保存图片」功能,用uni.canvasToTempFilePath导出。在安卓上正常,在 iOS 上导出的是一张白图。后来发现是 canvas 绘制是异步的,draw方法还没执行完就调了导出。解决办法是在draw的回调里再调导出,或者加一个setTimeout延迟 200ms。从那以后我每次用 canvas 导出图片,都强制在 draw 回调里走一遍导出逻辑,不再依赖外部时序。
希望帮到你。
本文还有配套的精品资源,点击获取