简介:面向毕业设计、Java全栈学习及微信小程序开发人群,这份资源提供了一套基于Spring Boot和微信小程序的居民健康监测系统完整实现。项目围绕用户授权登录、健康数据采集存储、异常实时预警、数据分析可视化及医生远程咨询等核心模块展开,前端包含小程序页面与后台界面,后端提供接口和业务逻辑,可直接用于课程设计、毕业设计或二次开发学习。压缩包共1612个文件,主要类型包括vue、js、json、java等核心代码,同时配以wxml、wxss小程序页面文件、png/svg图标资源、sql数据库脚本和md说明文档;另包含可执行脚本、备份文件及媒体示例,整体约29.94MB,目录结构清晰,便于按模块阅读和启动调试。已有114人学习下载。借助这份资料,可以完整跑通“用户-数据-预警-咨询”的典型流程,理解前后端分离项目的交互细节,并参考其封装方式快速搭建同类健康管理原型;对于需要提交毕设源码和文档的读者,也能从中获得完整的项目骨架、数据库设计参考以及基于微信小程序的前后端联调思路。
1. 为什么健康监测小程序比纯 App 更适合 Spring Boot 落地
居民健康监测这种场景,核心不是做出一个记录体温的输入框,而是把「微信身份识别 — 健康数据上报 — 异常提醒 — 医生查看」这条链路完整打通。纯 App 要么需要用户注册手机号,要么得额外做一套账号体系,而微信小程序天然拥有wx.login静默授权,能直接拿到 openid,后端配合 Spring Boot 的HandlerInterceptor或 Security 框架就能把用户身份收敛成一张用户表,省掉大量认证开发量。这套项目源码前端是 uni-app 编译的小程序包,后端是 Spring Boot 的 Java 工程,从资源包里的1-install.bat、2-run.bat、3-build.bat可以看出,它是按「安装依赖 → 本地运行 → 构建发布」这条路子组织的。适合两类人:一是要交 Java 课程设计或毕业设计的学生,想拿到一套前后端能跑的代码;二是刚接手小程序 + Spring Boot 全栈开发的工程师,想参考健康数据类项目里授权、实时告警和图表模块的写法。
2. Spring Boot 后端:从微信授权到健康数据入库的链路设计
后端不必把微信接口的一堆协议细节全部揉进业务代码,而是要先把「认证」和「健康数据」拆成两道清晰的线。认证线负责把code换成openid,数据线负责接收小程序上报的体温、心率、步数,并按统一的规则落库。
2.1 微信登录授权与 Spring Security 的边界划分
微信小程序的登录不是密码登录,而是wx.login拿到临时code,再由后端拿code去微信接口换session_key和openid。我见过不少项目直接在小程序端请求微信接口,这种方式把appSecret暴露给前端,是典型的安全问题。正确做法是后端封装一个AuthController,专门处理code交换。
@RestController @RequestMapping("/api/auth") public class AuthController { @Autowired private RestTemplate restTemplate; @Value("${wechat.appid}") private String appid; @Value("${wechat.secret}") private String secret; @PostMapping("/login") public Result login(@RequestBody LoginRequest request) { String url = "https://api.weixin.qq.com/sns/jscode2session" + "?appid=" + appid + "&secret=" + secret + "&js_code=" + request.getCode() + "&grant_type=authorization_code"; Map<String, Object> wxResp = restTemplate.getForObject(url, Map.class); if (wxResp == null || wxResp.get("openid") == null) { return Result.error("微信登录失败"); } String openid = (String) wxResp.get("openid"); User user = userService.findOrCreateByOpenid(openid); String token = JwtUtil.generateToken(user.getId()); return Result.success(token); } }这段代码把appSecret留在后端配置里,小程序只能拿到临时code,避免密钥泄露。code的失效时间是 5 分钟,且只能使用一次,所以后端要对wxResp.get("openid")做空判断,防止前端传一个已经用过的code。拿到openid后一般会把用户主键放进 JWT,后续请求都通过Authorization: Bearer token携带,Spring Boot 端用一个OncePerRequestFilter解析 token 并放入ThreadLocal,这样 Controller 里可以通过上下文拿到当前用户 id。
2.2 健康数据采集接口与 DTO/Entity 分离
健康监测里最常踩的坑是前端把病历本直接当作表结构传过来:用户手动填一个大型 JSON,后端直接new HealthRecord()接收。这样一旦前端多了字符串、小数、空值,后端毫无招架之力。建议定义带@Valid的 DTO,只暴露必要的字段。
public class HealthRecordDTO { @NotNull(message = "测量类型不能为空") private String type; // temperature / heart_rate / step_count @NotNull(message = "测量值不能为空") private BigDecimal value; private String unit; // ℃ / bpm / 步 private LocalDateTime measureTime; }对应的 Controller 接口需要严格指定返回值格式,而不是直接抛出400给小程序看。
@RestController @RequestMapping("/api/health") public class HealthRecordController { @Autowired private HealthRecordService healthRecordService; @PostMapping("/record") public Result addRecord(@RequestBody @Valid HealthRecordDTO dto, @RequestAttribute("userId") Long userId) { healthRecordService.addRecord(userId, dto); return Result.success(); } }这里@RequestAttribute("userId")是登录过滤器里塞进去的,避免业务代码再去解析 token。HealthRecordDTO里用BigDecimal而不是double,是因为体温、心率这类数据带小数,double会出现 36.6 变成 36.60000000001 的浮点误差。入库前按类型做一次范围检查,比如体温正常范围 30-45 ℃,超出直接拒收,这也是给后面的实时预警减负。
2.3 数据库表设计与 JPA/Hibernate 映射
居民健康监测核心表就三张:user、health_record、alert_log。health_record要支持按用户和时间范围查询,所以索引不能建错。
CREATE TABLE health_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, type VARCHAR(20) NOT NULL, value DECIMAL(8,2) NOT NULL, unit VARCHAR(10), measure_time DATETIME NOT NULL, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_user_time (user_id, measure_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;JPA 实体映射时需要注意LocalDateTime与 MySQL 的DATETIME类型对齐,在application.yml里加spring.jpa.hibernate.ddl-auto=update,但线上环境建议用 Flyway 或直接执行 SQL 脚本。idx_user_time这个联合索引是查询的命脉,统计某用户一周的体温趋势时,SQL 会先按user_id过滤再按measure_time排序,没有这个索引,数据量到十万级会出现明显的慢查询。JPA 用法上,我一般倾向于只用它的基础 CRUD,复杂统计查询直接写@Query原生日SQL,不要让 Hibernate 自动拼大循环。
3. 微信小程序端:uni-app 项目结构与 api 请求封装
资源包里main.css.bak、uni-fab.vue.bak是uni-app开发过程中对原始文件的手动备份,app.75fa1acb.css、chunk-vendors.a72b0961.css则是vite或webpack构建后生成的带哈希静态资源。这说明源码的前端不是原生小程序wxml,而是uni-app工程,通过 HBuilderX 或命令行编译成微信小程序。
3.1 从 uni-app 编译到微信小程序的构建脚本
三个.bat脚本是该项目的操作入口:
| 脚本 | 对应命令 | 作用 |
|---|---|---|
| 1-install.bat | npm install | 安装uni-app依赖与编译插件 |
| 2-run.bat | npm run dev:mp-weixin | 启动监听式编译,产物输出到dist/dev/mp-weixin |
| 3-build.bat | npm run build:mp-weixin | 执行生产构建,产物输出到dist/build/mp-weixin |
REM 1-install.bat cd /d %~dp0 call npm install --registry=https://registry.npmmirror.com echo install completed pausecd /d %~dp0是切换到当前脚本所在目录,避免双击脚本后工作目录出错。用npmmirror是国内环境的常规做法,如果不设置,uni-app相关依赖下载很容易卡住。构建完成后,需要在微信开发者工具中导入dist/dev/mp-weixin目录,切记不是导入整个项目根目录,否则开发者工具无法识别app.json。main.css.bak和uni-fab.vue.bak一般是在调试修改样式或悬浮按钮时留下的备份,可以直接忽略或手动删除。
3.2 请求封装与微信静默登录
小程序端最忌讳每个页面都写一遍uni.request,登录态一旦过期就要逐页改。建议抽一个request.js。
// /utils/request.js const BASE_URL = 'https://你的后端域名/api' function request(path, method, data = {}) { return new Promise((resolve, reject) => { const token = uni.getStorageSync('token') uni.request({ url: BASE_URL + path, method, data, header: { 'Authorization': 'Bearer ' + token }, timeout: 10000, success: (res) => { if (res.data.code === 401) { // token 失效,重新走微信登录 login().then(() => { resolve(request(path, method, data)) }) } else if (res.data.code === 0) { resolve(res.data.data) } else { reject(res.data.msg) } }, fail: (err) => reject(err) }) }) }timeout设 10 秒比较合理,健康数据上报一般不会超过这个时间,太长用户等待焦虑,太短弱网下容易误判失败。code===0是后端统一成功状态码,注意不要让后端返回 HTTP 200 但业务失败,在success回调里做业务判断即可。对于首次登录,页面onLoad时调一次uni.login获取 code,再用request调后端/api/auth/login,拿到 token 后写入 storage。这里要注意微信小程序的Authorization头不要写成token,后端拦截器读取的字段名字必须前后端一致。
3.3 健康数据录入页面的实时校验
录入页面通常是表单加图表,uni-app 里日期和时间选择器可以直接用picker组件,不用额外引库。一个高频坑是uni-datetime-picker放在scroll-view内,iOS 上会出现滚动穿透或选择器被截断。常见做法是给scroll-view加:prop="'catchtouchmove'",或者将弹层组件移出滚动容器。
<view class="form"> <picker mode="date" @change="onDateChange"> <view class="picker">{{ date }}</view> </picker> <uni-datetime-picker v-model="measureTime" type="datetime" :popup-style="{ zIndex: 999 }" /> <input type="digit" v-model="temperature" placeholder="体温 ℃" /> <input type="number" v-model="heartRate" placeholder="心率 bpm" /> <button @click="submitRecord">保存数据</button> </view>methods: { submitRecord() { if (!this.temperature || this.temperature < 30 || this.temperature > 45) { uni.showToast({ title: '体温超出合理范围', icon: 'none' }) return } request('/health/record', 'POST', { type: 'temperature', value: this.temperature, unit: '℃', measureTime: this.measureTime || new Date() }).then(() => { uni.showToast({ title: '已记录', icon: 'success' }) }) } }input的type="digit"会唤起数字键盘,适合输入体温。type="number"只能输入整数,如果心率想要支持小数点,也可以改成digit。前端校验只是体验优化,真正可靠的范围校验必须放在后端,因为后端才是最后的防线。
4. 实时监测预警与数据可视化的实现方案
摘要里描述了「实时监测」「异常预警」「数据分析可视化」几项能力,这里的实时并不是指 WebSocket 长连接推送,而是后端定时扫描最近几分钟的新增健康记录,命中阈值后通过微信订阅消息触达用户。这种方案在健康监测场景下足够用,且实现成本比 WebSocket 低很多。
4.1 基于阈值的异常检测规则
常规阈值建议做成配置表而不是写死在代码里。这里给出一个精简版规则判断服务。
@Service public class AlertRuleServiceImpl implements AlertRuleService { @Autowired private HealthRecordRepository recordRepository; @Autowired private AlertLogRepository alertLogRepository; @Scheduled(fixedDelay = 30000) public void scanRecentRecords() { LocalDateTime since = LocalDateTime.now().minusMinutes(5); List<HealthRecord> records = recordRepository.findByMeasureTimeAfter(since); for (HealthRecord r : records) { if (isAbnormal(r) && !isAlreadyAlerted(r)) { alertLogRepository.save(new AlertLog(r.getUserId(), r.getType(), r.getValue())); notifyUser(r); } } } private boolean isAbnormal(HealthRecord r) { if ("temperature".equals(r.getType())) { return r.getValue().compareTo(new BigDecimal("37.3")) > 0; } return false; } }@Scheduled(fixedDelay = 30000)表示上一次任务执行完 30 秒后再开始下一次,和cron相比,fixedDelay更担心任务超时重叠导致的数据重复扫描。这里要注意isAlreadyAlerted必须用「时间窗口 + 类型 + 用户」联合判断,否则同一个 38 度体温会在每 30 秒的扫描里给用户连发 10 条消息。我一般会在alert_log表里记录alert_time,然后查最近 15 分钟是否已有同类型告警。
4.2 微信订阅消息推送与定时任务调度
微信小程序不像公众号那样可以随时推送。必须让用户先订阅,用户每点击一次「允许」按钮,后端才有资格下发一条订阅消息。所以需要在小程序端引导用户主动订阅。
uni.requestSubscribeMessage({ tmplIds: ['告警消息模板ID'], success: (res) => { if (res['告警消息模板ID'] === 'accept') { uni.setStorageSync('subscribeAlert', true) } } })后端拿到 openid 后调用微信统一发送接口,注意订阅消息模板中的字段关键字需要和data里传的 key 完全一致。
public void sendAlertMessage(String openid, String type, String value) { String accessToken = getAccessToken(); String url = "https://api.weixin.qq.com/cgi-bin/message/subscribe/send" + "?access_token=" + accessToken; Map<String, Object> body = new HashMap<>(); body.put("touser", openid); body.put("template_id", alertTemplateId); body.put("page", "pages/detail/detail?type=" + type); body.put("data", Map.of( "thing1", Map.of("value", type), "amount2", Map.of("value", value) )); restTemplate.postForEntity(url, body, Map.class); }这里getAccessToken需要缓存,微信接口每天有调用上限,且 access_token 有效期只有 2 小时。我建议在后端用一个RedisCache或者简单的内存缓存存储 access_token,过期时才重新请求。订阅消息的重点是「量」,用户订阅一次只能发一条,如果健康系统有多个预警维度,要按维度引导订阅多次。
4.3 数据统计图表:后端聚合查询与前端 ECharts 展示
数据分析的常见展示是近 7 天体温折线图、心率分布图、步数柱状图。后端不应该把原始数据全部传给前端,而是直接在 SQL 里按天聚合。
SELECT DATE(measure_time) AS day, ROUND(AVG(value), 2) AS avg_value, MAX(value) AS max_value, MIN(value) AS min_value FROM health_record WHERE user_id = :userId AND type = 'temperature' AND measure_time >= :startTime GROUP BY DATE(measure_time) ORDER BY day;用 JPA@Query写这个接口时,返回类型要用List<Object[]>或定义一个聚合结果 DTO。前端用一个组件比如lime-echart或者ucharts绘制,把day放在 x 轴,avg_value放在 y 轴。
getWeekTrend() { request('/health/stats/temperature', 'GET', { days: 7 }).then((rows) => { this.trendData = { categories: rows.map(item => item.day), series: [{ name: '平均体温', data: rows.map(item => item.avg_value) }] } }) }ROUND(AVG(value), 2)把平均值保留两位小数,避免折线图出现一长串小数。聚合查询在数据量大时建议加measure_time索引,否则DATE(measure_time)上的函数计算无法使用索引。还有一个细节:前端拿到day时可能是2025-04-01字符串,用categories直接展示没问题,如果要做 x 轴时间轴缩放,就需要转换成时间戳。
5. 从资源包到线上:微信开发者工具与 Spring Boot 日志的联合排错
如果你刚把dist/dev/mp-weixin导入微信开发者工具,发现页面白屏或请求报错,最直接的排查路径不是反复重启工具,而是先看三个地方:小程序 Network 面板、Spring Boot 控制台日志、浏览器里的后端错误堆栈。这里有一个实用技巧:在 Spring Boot 后端加一个TraceIdFilter,让每次请求都带唯一 ID,前端 console 打印这个 ID,后端日志也打印同样的 ID,两边一对照就能知道是哪一跳断了。
@Component public class TraceIdFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; String traceId = req.getHeader("X-Trace-Id"); if (traceId == null || traceId.isEmpty()) { traceId = UUID.randomUUID().toString().replace("-", ""); } MDC.put("traceId", traceId); HttpServletResponse resp = (HttpServletResponse) response; resp.setHeader("X-Trace-Id", traceId); chain.doFilter(request, response); MDC.remove("traceId"); } }在logback-spring.xml里把%X{traceId}加进 pattern,比如%d{HH:mm:ss.SSS} [%X{traceId}] %-5level %logger{40} - %msg%n。这样小程序端发起请求时如果失败,可以先打开 Network 面板看响应头里的X-Trace-Id,再去后端日志里 grep 这个 ID,直接定位到异常发生的那一行。这个技巧在分离部署的调试环境里尤其有用,比打印console.log再肉眼找日志高效得多。
小程序端抓包不需要依赖额外工具。微信开发者工具自带 Network 面板,可以看到每个请求的 headers 和响应体。遇到白屏时,先看 AppData 里是否拿到 token,没有 token 就是登录链路断了。登录链路断了优先检查appid和secret是否配对。微信公众平台上如果用了 IP 白名单,后端服务器 IP 不在白名单里,jscode2session会一直返回40163错误,这个最容易忽略。
项目里的3-build.bat构建出的生产包,必须使用 HTTPS 域名而且要在微信公众平台配置 request 合法域名。开发阶段可以勾选「不校验合法域名」,但一旦发布,这个开关就不存在。我建议把BASE_URL抽成一个独立配置文件,dev环境用局域网 IP,build环境用正式域名,避免每次换环境都要改一遍源码。
本文还有配套的精品资源,点击获取