Java SSM+Vue微信小程序健康管理系统实现全解析

发布时间:2026/10/26 8:31:05
Java SSM+Vue微信小程序健康管理系统实现全解析

简介:一套基于微信小程序的健康管理系统完整源码,面向需要快速搭建健康类小程序应用的开发者、Java后端学习者及毕业设计选题学生。项目采用前后端分离架构,后端以Java为核心,整合SpringBoot、SSM(Spring、SpringMVC、MyBatis)与MySQL 5.7+,前端使用Vue构建管理界面,并配套微信小程序端实现健康数据的记录、查看与日常管理,可帮助理解企业级项目结构与接口设计。压缩包共1137个文件,约16.61MB,包含java、vue、js、wxml、wxss、png、svg等类型,覆盖后端业务逻辑、前端组件、小程序页面、界面图标及数据库SQL脚本,目录组织规范便于分类检索。已有256人学习下载,适合用来研究SSM框架整合、Vue组件化开发、小程序与后台数据交互,或直接作为健康管理系统的二次开发基础。

1. 一套 Java-ssm-vue 微信小程序健康管理系统源码,先看清它到底解决了什么

拿到“Java-ssm-vue基于微信小程序的健康管理系统实现源码”这个标题,很多人第一反应是先解压、先看 README。其实更有用的是先想清楚场景:用户在小程序里记录血压、血糖、体重,查看健康档案和体检报告,预约医生;运营和医生在 Vue 管理端维护用户档案、录入报告、处理预约。这套源码把这个业务拆成了三块——SSM 后端统一提供接口和权限,Vue 管理端做后台操作,微信小程序端是用户每天打开的那个入口。它适合四类人:做毕业设计要完整工程的学生,中小健康机构想快速上线微信端的小团队,以及手里有类似需求、想拿一个清晰工程做二次开发的 Java 工程师。这个系统不炫技,但结构扎实,值得照着跑通并投入改造。

2. 三层结构拆开看:SSM 管数据、Vue 管后台、小程序管 C 端

2.1 这套源码为什么用 SSM,而不是直接上 Spring Boot

SSM 是 Spring、SpringMVC、MyBatis 的组合:Spring 负责对象管理和事务,SpringMVC 负责接收 HTTP 请求并路由到业务方法,MyBatis 负责把 SQL 和 Java 方法做映射。健康管理系统这种项目,数据模型不算复杂,也就是用户、健康档案、健康记录、预约、体检报告这几张核心表,SSM 的“Controller → Service → Mapper”三层结构足够清晰。

很多 Java 技术面试题里都爱问 SSM 的执行流程,从请求进 DispatcherServlet 开始,到 HandlerMapping 找到 Controller,再到 Service 调 Mapper,最后以 JSON 返回。这套源码正好把这条链路完整展示出来。如果换成 Spring Boot + JPA,虽然写起来快,但 SQL 被封装在框架里,出了慢查询反而是黑匣子,排查起来更费劲。SSM 的 MyBatis 让你把 SQL 牢牢握在自己手里,适合需要精确控制查询逻辑的业务,比如健康记录按时间范围统计、体检指标关联查询这类场景。

2.2 Vue 管理端:它不是给用户用的,是给运营和医生用的

管理端是典型的单页后台:医生查看患者列表、录入体检结果,运营配置健康资讯和预约排班。Vue 在这里的定位就是高效地做表格、表单、路由跳转和状态管理。用 Vue Router 划分页面,比如/user管用户、/record管健康记录、/report管体检报告,左侧菜单对应路由,右侧内容区切换组件。

这套源码的管理端一般还会配一个vue.config.js做开发环境转发,因为浏览器里的 Vue 页面跑在 8081 端口,后端跑在 8080 端口,直接fetch('/api/user/list')会被浏览器拦跨域,必须把/api转发到后端起服务的地方。注意,这里说的转发是开发服务器的一个便捷配置,不要把前后端跨域问题留到部署阶段才处理。

2.3 小程序原生开发与 Vue 管理端是两套思路

同一个系统里出现 Vue 和小程序,不代表它们写法相同。Vue 管理端操作的是 DOM,数据双向绑定用v-model,页面是组件树;微信小程序原生开发用的是 WXML 和 WXSS,数据更新靠this.setData(),生命周期叫onLoad、onShow,事件绑定用bindtap而不是@click。如果硬把 Vue 的写法搬进小程序,很容易在this指向和异步更新上报错。

很多团队会为了多端复用选择 uniapp 打包小程序和 App,但这套源码直接采用小程序原生实现,好处是调用微信登录、手机号授权、订阅消息这些能力时路径最短、文档最直接。微信小程序的更新频率不低,原生写法能更快跟上平台变化。对健康管理系统来说,小程序端的核心功能基本是登录、填表、查记录、预约,原生开发完全够用。

2.4 一个请求从点击到写库的完整路径

先看典型目录结构,这对后续跑通工程非常关键:

目录/文件职责技术栈常见端口
backend后端接口服务Java + SSM + MySQL8080
admin-web管理端前端Vue 2/3 + Element UI8081
miniprogram用户端小程序微信原生无(开发者工具运行)
sql数据库脚本MySQL DDL/DML3306

小程序里用户点“保存血压记录”,实际发生的是:WXML 按钮触发bindtap,JS 里把表单数据交给wx.request发到https://你的域名/api/health/save;请求先到 SpringMVC 的 DispatcherServlet,被@RequestMapping路由到 HealthRecordController,Controller 调 HealthRecordService,Service 再调 HealthRecordMapper,最终通过 MyBatis 执行 INSERT 写进health_record表。返回时数据逐层倒着传回,最终以 JSON 回到小程序,wx.request的 success 回调里setData更新页面。管理端的用户查询、预约处理走的也是同一套后端接口,只是请求方从小程序换成了浏览器里的 Vue。

3. 先把 SSM 后端跑通:建库、配依赖、登录接口返回 token

3.1 初始化 MySQL:先建库再建表,避免外键报错

拿到源码后第一步永远是初始化数据库,不是先启动后端。很多新手直接把整个 SQL 文件往 Navicat 里一拖,结果报Cannot add foreign key constraint,原因多半是表创建顺序不对:子表先建,父表还没建。我一般会先手动建空库,再按依赖顺序执行建表语句。

CREATE DATABASE IF NOT EXISTS health_manage DEFAULT CHARSET utf8mb4 COLLATE utf8mb4_general_ci; USE health_manage; CREATE TABLE sys_user ( id BIGINT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(100) NOT NULL, nickname VARCHAR(50), phone VARCHAR(20), openid VARCHAR(64), role TINYINT DEFAULT 0 COMMENT '0-用户 1-医生 2-管理员', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB COMMENT='系统用户表'; CREATE TABLE health_record ( id BIGINT AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL, record_type VARCHAR(20) COMMENT 'blood_pressure/blood_sugar/weight', record_value VARCHAR(50), record_time DATETIME, remark VARCHAR(200), INDEX idx_user_time (user_id, record_time) ) ENGINE=InnoDB COMMENT='健康记录表';

建库时用utf8mb4而不是utf8,因为健康备注里可能存表情符号或特殊字符,utf8mb4才是完整兼容的字符集。sys_user表里我把openid单独设为一列,这是小程序登录绑定用的;role字段控制权限,0 是普通用户,1 是医生,2 是管理员。health_record表加了一个复合索引idx_user_time,因为最常见的查询是“查某个用户某段时间的记录”,不加索引数据量大了会很慢。

3.2 Maven 依赖:Spring 5、MyBatis、Druid 这一套组合

后端工程一般是用 Maven 管理的,pom.xml里的依赖组合要注意兼容性。Spring 5.2 配 MyBatis 3.5、mybatis-spring 2.0 是一套比较常见的组合,能避开很多版本冲突。常见做法是依赖统一用<properties>管理版本,方便后面升级。

<properties> <spring.version>5.2.22.RELEASE</spring.version> <mybatis.version>3.5.6</mybatis.version> <mybatis-spring.version>2.0.6</mybatis-spring.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-jdbc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>${mybatis-spring.version}</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.28</version> </dependency> </dependencies>

这里有一个关键点:mybatis-spring2.0 以下版本配 Spring 5 会出现Invalid value type for attribute 'factoryBeanObjectType'这类报错。所以如果在本地导入工程后启动直接失败,先检查这两个版本的搭配。MySQL 驱动用 8.x 时,连接字符串里driverClassName要写成com.mysql.cj.jdbc.Driver,用 5.x 驱动写com.mysql.jdbc.Driver,不要搞混。

3.3 SpringMVC + MyBatis 配置文件里的三个关键参数

SSM 工程的核心配置通常在spring-mybatis.xml或applicatonContext.xml里。重点看三个参数:数据源连接字符串、Mapper XML 扫描路径、驼峰映射开关。这三个参数任何一个配错,后端都跑不起来或数据查不对。

<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/health_manage?useUnicode=true&amp;characterEncoding=utf8&amp;useSSL=false&amp;serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="your_password"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> <property name="typeAliasesPackage" value="com.health.entity"/> <property name="configuration"> <bean class="org.apache.ibatis.session.Configuration"> <property name="mapUnderscoreToCamelCase" value="true"/> </bean> </property> </bean>

连接字符串里serverTimezone=Asia/Shanghai必加,MySQL 8 默认时区是 UTC,不加的话时间字段会差 8 小时,健康记录的时间显示会直接让你怀疑人生。characterEncoding=utf8不加,中文数据写库后读取容易乱码。mapUnderscoreToCamelCase必须开,否则数据库create_time字段映射不到 Java 实体里的createTime,查询结果全是 null。

3.4 登录接口案例:Controller-Service-Mapper 一次走完

登录接口是理解这套 SSM 工程最好的入口,因为它完整经过了 Controller → Service → Mapper 三层。后端返回的 token 后续会被小程序、管理端存放在本地,每次请求带在 Header 里。

@RestController @RequestMapping("/api/user") public class UserController { @Resource private UserService userService; @PostMapping("/login") public Result login(@RequestBody LoginDTO dto) { User user = userService.login(dto.getUsername(), dto.getPassword()); if (user == null) { return Result.error("用户名或密码错误"); } String token = JwtUtil.createToken(user.getId(), user.getRole()); return Result.ok(token); } }
@Service public class UserServiceImpl implements UserService { @Resource private UserMapper userMapper; @Override public User login(String username, String password) { String md5Password = MD5Util.encode(password); return userMapper.findByUsernameAndPassword(username, md5Password); } }
<mapper namespace="com.health.mapper.UserMapper"> <select id="findByUsernameAndPassword" resultType="User"> SELECT id, username, nickname, role, openid FROM sys_user WHERE username = #{username} AND password = #{password} </select> </mapper>

登录密码在数据库里一般存的是 MD5 值,所以 Service 层先做一次 MD5 再查库,不要把明文密码直接拼进 SQL。JwtUtil生成 token 时,我习惯把用户 id 和角色放进 payload,过期时间设成 24 小时。健康管理系统不像支付系统那样对 token 时效极度敏感,24 小时是比较舒服的折中:用户不会频繁掉线,泄露后影响面也可控。这段链路跑通,后端基本就活了。

4. 小程序端接入:wx.login 换 token、手机号绑定与健康记录上报

4.1 在微信开发者工具里导入工程并填对 appid

小程序端用微信开发者工具打开miniprogram目录,不是直接把整个后台仓库拖进去。导入后第一件事是检查project.config.json里的appid,如果用测试号,手机号授权这类能力会受限,最好换成真实的小程序 AppID。真机调试时,开发者工具右上角要打开“不校验合法域名”选项,这是本地联调阶段省时间的关键开关。但要注意,这个选项只对开发者工具有效,真机上必须配置合法域名后才能请求成功。

4.2 wx.login 拿 code 换 openid 的完整流程

小程序端登录不直接传用户名密码,而是先调wx.login拿一个临时 code,再把 code 发给后端。后端拿着 code 去微信接口换 openid 和 session_key,用 openid 去找或创建用户,最后签发自己的 token。这个 token 才是小程序后面请求业务接口时用的凭证。

// miniprogram/pages/login/login.js wx.login({ success(res) { if (res.code) { wx.request({ url: BASE_URL + '/api/wechat/login', method: 'POST', data: { code: res.code }, success: (res) => { const { token } = res.data.data; wx.setStorageSync('token', token); wx.reLaunch({ url: '/pages/index/index' }); }, fail: () => wx.showToast({ title: '登录失败', icon: 'none' }) }); } } });

code是五分钟内有效的临时凭证,只能使用一次。这里有个细节别漏:后端用 code 换 openid 时,调的是微信的jscode2session接口,需要 AppID 和 AppSecret。AppSecret 绝不能出现在小程序前端代码里,只能放在后端配置文件中。很多新手把 AppSecret 直接写在小程序 JS 里,等于把账号权限公开了,这在审核和实际运营中都是大雷。

4.3 getPhoneNumber 新接口:拿到的是 code,不是手机号

“微信小程序登录获取手机号”是搜索热度一直很高的功能,但很多新接触的人还在用老思路,以为button open-type="getPhoneNumber"回调里会直接返回手机号。现实是从某次基础库调整之后,回调里返回的是一个动态code,后端必须拿这个 code 调用微信的getuserphonenumber接口,才能换到真实的手机号信息。

<!-- miniprogram/pages/profile/profile.wxml --> <button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber"> 绑定手机号 </button>
// miniprogram/pages/profile/profile.js onGetPhoneNumber(e) { if (e.detail.code) { wx.request({ url: BASE_URL + '/api/wechat/phone', method: 'POST', data: { code: e.detail.code }, success: (res) => { wx.setStorageSync('userInfo', res.data.data); this.setData({ phone: res.data.data.purePhoneNumber }); } }); } }

注意后端换手机号的接口是POST https://api.weixin.qq.com/wxa/business/getuserphonenumber?access_token=ACCESS_TOKEN,body 里传{ "code": "回调拿到的code" }。后端拿到phone_info后,通常把purePhoneNumber存到sys_user表的phone字段。手机号属于敏感个人信息,健康管理系统里最好加密存储,至少别把完整号码直接打印到日志里。

4.4 request 封装:统一带 token 与 401 自动登出

小程序每个页面都直接写wx.request会很快失控。靠谱的做法是封装一个request.js,统一设置 BaseURL、自动带上 token、集中处理错误码。健康管理系统里接口很多,这个封装是后续所有页面的基础。

// miniprogram/utils/request.js const BASE_URL = 'https://yourdomain.com/api'; const request = (url, method = 'GET', data = {}) => { const token = wx.getStorageSync('token'); return new Promise((resolve, reject) => { wx.request({ url: BASE_URL + url, method, data, header: { 'Content-Type': 'application/json', 'Authorization': token ? `Bearer ${token}` : '' }, success(res) { if (res.statusCode === 401) { wx.removeStorageSync('token'); wx.reLaunch({ url: '/pages/login/login' }); reject(new Error('登录已过期')); return; } if (res.data && res.data.code === 200) { resolve(res.data.data); } else { wx.showToast({ title: res.data.msg || '请求失败', icon: 'none' }); reject(res.data); } }, fail(err) { wx.showToast({ title: '网络异常', icon: 'none' }); reject(err); } }); }); }; module.exports = { request, BASE_URL };

与后端约定好:业务成功返回code: 200,登录失效返回 HTTP 401。封装里 401 统一清 token 并跳回登录页,避免用户在多个页面里反复触发登录弹窗。健康记录需要身份归属,所以所有上报接口都必须带 Authorization,后端用拦截器统一校验。

4.5 健康记录上报页面与后端保存

以血压记录为例,小程序端页面很简单:用户选择测量时间和收缩压、舒张压,点保存后提交到后端。

// miniprogram/pages/record/blood.js submitBloodPressure() { const data = { recordType: 'blood_pressure', recordValue: `${this.data.systolic}/${this.data.diastolic}mmHg`, recordTime: this.data.measuredTime }; request('/health/record/save', 'POST', data) .then(() => { wx.showToast({ title: '保存成功', icon: 'success' }); this.setData({ systolic: '', diastolic: '' }); }); }

小程序端只做格式校验和提交,真正的业务校验在后端 Service 层:收缩压范围、舒张压范围、时间不能晚于当前时间等。这种双层校验是健康数据项目的基本要求,前端校验可以被绕过,后端必须兜底。保存成功后刷新记录列表,首页的趋势图数据源就是这些health_record记录。

5. 联调部署避坑:健康管理系统里最常见的 5 个翻车现场

5.1 真机测试请求全部被挡:开发工具能用,手机却不行

现象:开发者工具里开了“不校验合法域名”后接口全部正常,一到真机预览就全部报request:fail,控制台提示域名不在合法域名列表中。

原因:微信小程序真机运行有域名白名单校验。请求的域名必须是 HTTPS,且要在微信公众平台后台的“开发管理 → 开发设置 → 服务器域名”里配置到request合法域名。开发工具的“不校验”只是本地调试开关,真机不认。

解决:把后端接口部署到一台有备案域名的 HTTPS 服务器上,然后在微信公众平台后台添加 request 合法域名。域名需要配置 SSL 证书,且证书链完整,有些免费证书在部分微信版本里会握手失败。如果只是本地开发阶段调试,可以用局域网 IP 配合开发者工具的真机调试模式临时联调,但正式体验版和发布版必须走合法域名。

5.2 中文乱码乱到怀疑人生:全链路只要有一环是拉丁编码就出问题

现象:小程序端提交的用户备注、健康建议,在后端打印日志正常,但页面上显示???或大一类乱码。

原因:乱码往往不是单一环节造成的。MySQL 连接串没带characterEncoding=utf8,或建库时用了latin1,或 Tomcat 的 URI 编码不是 UTF-8,三条链路任何一处出问题,中文就会在传输过程中被转坏。

解决:先查库:SHOW CREATE DATABASE health_manage;,库和表都应该是utf8mb4。再查连接串,确认useUnicode=true&characterEncoding=utf8已带上。最后检查 Tomcat 的server.xml,在<Connector>上加URIEncoding="UTF-8"。这三处改完,中文基本不会再乱。排查乱码时不要只改一处就重启,三个位置一起核对,否则换着翻车。

5.3 查询结果全字段为 null:数据库有值,Java 对象接不住

现象:sys_user表里明明有数据,MyBatis 查出来nickname、createTime都是 null,但id和username正常。

原因:数据库列名create_time、nickname在 Java 实体里是createTime、nickname。nickname两边同名单词能映射上,create_time这种下划线风格如果不开启驼峰映射,MyBatis 找不到对应属性,就静默返回 null。这个问题如果不看日志,会误以为是查询条件写错,非常容易白耗一下午。

解决:在 MyBatis 全局配置里开启驼峰映射。XML 配置方式是在configuration里加<setting name="mapUnderscoreToCamelCase" value="true"/>,Spring Boot 则在application.properties里写mybatis.configuration.map-underscore-to-camel-case=true。老项目更常见的做法是在SqlSessionFactoryBean里注入配置,像第 3 章里那样。如果项目里大量使用resultMap,也可以在每个resultMap中显式写上column="create_time" property="createTime",一劳永逸但比较啰嗦。

5.4 自定义导航栏顶到刘海屏:顶部导航栏高度不能写死 64

现象:小程序自定义导航栏后,iPhone 14 Pro 上内容顶到状态栏,胶囊按钮和页面标题挤在一起;模拟器里正常,真机上错位。

原因:微信小程序的默认导航栏高度在不同机型上不一样,刘海屏、挖孔屏、灵动岛的状态栏高度都不同。写死64px或88px只对部分机型有效,这类问题在“微信小程序顶部导航栏高度”相关的搜索里常年被问。

解决:用wx.getMenuButtonBoundingClientRect()获取胶囊按钮的位置和尺寸,再结合wx.getSystemInfoSync()里的statusBarHeight计算导航栏高度。更省事的方案是用navigationStyle: custom配合官方navigation-bar组件,或者干脆保留微信默认导航栏,只改标题文字和背景色。健康管理系统的小程序页面不算复杂,我一般建议非定制需求就保留默认导航栏,没必要为了一致性引入自定义导航的适配成本。

5.5 管理端打包后部署 404:dist 文件不知道放哪

现象:Vue 管理端本地跑得好好的,npm run build打出 dist 目录后丢到服务器上,访问首页空白或刷新后 404。

原因:Vue 是单页应用,路由用的是 History 模式,刷新/user时服务器找不到user这个真实文件,直接返回 404。另外,如果直接把 dist 丢进 Tomcat 的webapps/ROOT,接口地址又是http://localhost:8080/api/...,浏览器里就会出现跨域。

解决:管理端推荐用 Nginx 托管 dist,配置里做两件事:一是所有非静态文件请求都重写到index.html,解决 History 路由刷新 404;二是把/api前缀的请求转发到后端起服务的端口。接口尽量保持同域,小程序端则天然要求 HTTPS 域名,所以正式环境最好是同一台服务器、同一个域名,通过路径区分/api和前端静态资源。开发时用 Vue 的 devServer 转发解决跨域,线上用 Nginx 转发,这套组合是健康管理系统前后端分离最常见的发布方式。

6. 管理端 Vue 跑通后的第一件事:给 token 加一个续期机制

Vue 管理端的工程一般要执行npm install安装依赖,再配好vue.config.js里的接口转发,然后npm run serve才能看到登录页。但跑通登录只是第一步,真正让管理端好用的是一个容易被忽略的细节:token 续期。

健康管理系统的使用场景里有大量长会话操作,比如医生连续录入 20 份体检报告,如果 token 在第 15 份时过期,用户回头就发现提交失败、跳回登录页,之前填的数据全丢。我给这类项目做管理端时,一般会预留一个刷新 token 的接口,前端在请求拦截器里遇到 401 时自动调用续期接口,续期成功则重放原请求,续期失败才清缓存回登录页。这个机制在 Vue 里实现并不复杂,路由守卫里也要同步判断 token 是否存在。

// admin-web/src/utils/request.js 节选 service.interceptors.response.use( response => response, async error => { const { response } = error; if (response && response.status === 401) { const refreshed = await refreshToken(); if (refreshed) { error.config.headers.Authorization = `Bearer ${getToken()}`; return service(error.config); } router.push('/login'); } return Promise.reject(error); } );

这里最需要注意的是避免并发请求同时触发多个刷新接口。我习惯用一个isRefreshing标志位,刷新期间到达的 401 请求先排队,等刷新完成后再统一重放。这个细节不做,刷新接口会被同一时刻冒出来的十几个请求打穿,反而制造新的翻车现场。回顾我自己接手的源码项目,九成跑不起来都不是框架问题,而是环境没对版、数据库没初始化、token 策略过于简单。拿到这套健康管理系统源码,按“建库 → 启后端 → 跑 Vue → 调小程序”的顺序走,先把最小闭环打通,再决定要改哪个页面。希望帮到你。

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