说个真事儿。今年年初接手了一个社区智慧养老监护管理平台的开发项目,技术栈正好就是标题里这套:Java SpringBoot+Vue3+MyBatis+MySQL,前后端分离。做这种项目跟做普通的管理后台不一样,它牵扯到的不仅仅是老人信息的增删改查,还有健康设备的数据接入、实时告警、工单流转、家属端查询这些环环相扣的东西。如果只是照着“CRUD生成器”的思路糊一个系统出来,后续接设备、接大屏、接告警规则的时候一定会返工。这篇文章我把整个项目从选型到落地的核心细节全部拆开讲一遍,包括表结构怎么设计、MyBatis 怎么写复杂查询、Vue3 端的监控大屏怎么做、前后端联调有哪些坑,希望对正在做类似社区服务平台的兄弟有帮助。
1. 项目整体设计与技术选型思路
1.1 这类监护平台到底要解决什么核心问题
社区智慧养老,说白了就是居家养老和机构养老之间的过渡形态:老人住在自己家或者社区日间照料中心,社区服务中心统一管理健康数据、对接紧急求助、安排护工上门服务。所以系统的核心不只是一个“通讯录”,而是三条业务主线:
- 老人档案与家属绑定:每个老人对应一到多个家属账号,家属可以远程查看健康数据。
- 设备数据采集与异常告警:手环、血压计、血氧仪等设备定时上报数据,系统判定异常后生成告警工单。
- 服务工单与处置闭环:告警产生后需要派单给护工,护工上门处理完回填结果,形成完整记录。
明确了这几条主线,技术选型就不会跑偏。你需要的不是一个臃肿的微服务架构,而是一个稳定、能快速迭代、维护成本足够低的单体应用加合理分模块。
1.2 为什么选 SpringBoot + Vue3 + MyBatis + MySQL 这套组合
先说 SpringBoot。现在 Java 圈子做这种业务系统,SpringBoot 依然是默认选项,原因很朴素:约定优于配置,内嵌 Tomcat,打一个 jar 包就能跑,社区护工端哪怕部署在老旧电脑上也没有压力。版本我选的是 SpringBoot 2.7.x,搭配 JDK 8,主要考虑到部分现场环境的兼容性。如果你是新项目且确定 JDK 17 没负担,直接上 SpringBoot 3.x 也没问题,只是 MyBatis 的 starter 要注意用适配版本。
再说 MyBatis 而不是 MyBatis-Plus。这个选择可能有人不理解,我讲一下真实原因:这个项目里有大量的多表关联查询和动态条件筛选,比如“查某个社区所有老人的最新一条健康记录”,这种 SQL 用 MyBatis 手写可以精确控制每一行逻辑,SQL 执行效率也一目了然。MyBatis-Plus 虽然开发快,但遇到复杂查询时容易生成冗余条件,反而增加排查成本。团队里如果全是新手,手写 SQL 还能逼着他们吃透业务。
前端用 Vue3 + Vite,配合 Element Plus 做后台界面。Vue3 的组合式 API 写业务逻辑比 Options API 舒服太多,尤其监控大屏这种需要大量响应式数据的页面,逻辑复用非常方便。Vite 开发时的热更新速度比 Webpack 快得多,实测启动一个几十个页面的工程只要一两秒,开发体验完全不一样。
数据库用 MySQL 8.0,主要是社区项目数据量不会爆炸,单库单表足够支撑,InnoDB 的事务能力和行级锁在工单流转、健康数据写入这些场景下非常可靠。字符集统一 utf8mb4,避免生僻字和 emoji 符号写入报错。
1.3 前后端分离架构在社区项目里的落地方式
前后端分离在这个项目里带来的实际收益有两个:第一,后端接口和前端页面可以并行开发,互不阻塞;第二,部署灵活,前端打包出静态文件扔到 Nginx,后端 jar 包独立运行,后续如果要在现场部署边缘计算网关,前端页面依然能访问远端服务。
但分离架构也会带来两个必须处理的问题,一个是跨域,一个是鉴权。跨域我在后端用 CorsFilter 统一处理,允许指定来源地址(比如前端部署的 Nginx 域名),而不是直接allowCredentials(true)+allowedOriginPatterns("*")全放开,避免安全隐患。鉴权用 JWT,登录成功后前端把 token 存到 localStorage,Axios 请求拦截器统一带上 Authorization 头。
提示:JWT 适合这种短时效业务系统,但注意密钥要放到配置文件里,不要硬编码在代码中;token 有效期建议设 8-12 小时,现场护工的登录频次不算高,长一点反而体验更好。
2. 后端核心模块设计与 MyBatis 数据层实战
2.1 SpringBoot 项目结构怎么划分最清晰
这种项目千万别按“Controller/Service/Mapper”三层一刀切,时间一长就是大杂烩。我采用的是按业务模块分包,每个模块内部再分 mvc 子层。大致结构如下:
com.community.care ├── common │ ├── config // 跨域、JWT、MyBatis、Redis配置 │ ├── constant │ ├── exception // 全局异常处理器 │ ├── result // 统一返回结构 │ └── utils ├── system // 用户、角色、权限 │ ├── controller │ ├── service │ ├── mapper │ └── entity ├── elder // 老人档案与家属管理 ├── device // 设备管理与数据接入 ├── health // 健康记录与指标分析 ├── alert // 告警与工单 └── dashboard // 大屏统计接口统一返回结构我用的Result<T>包装,包含 code、message、data 三个字段。前端 Axios 响应拦截器统一判断 code 是否为 0,非 0 弹错误提示。这样接口层不会各写各的返回姿势,后面对接小程序端也省事。
全局异常处理是必须做的。业务异常用自定义 BizException,参数校验异常、SQL 异常、未知异常都分别处理。特别是数据库异常,不要直接把完整异常栈抛给前端,统一转成“系统繁忙,请稍后重试”避免泄露表结构信息。
2.2 MyBatis 核心配置与手写 SQL 的关键点
MyBatis 的配置看起来简单,但几个细节直接决定开发效率。我放一个最核心的 mybatis-config 配置思路:
mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.community.care.*.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl cache-enabled: falsemap-underscore-to-camel-case必须打开,数据库字段create_time才能自动映射到createTime。日志我这里开了 stdout 输出,开发环境非常有用的,可以看到每条 SQL 执行时传入的参数和影响行数,一旦查询结果和预期不一致,先看输出的 SQL 再调,不要瞎猜。
然后在 pom 里引入 mybatis-spring-boot-starter,版本选和 SpringBoot 2.7 匹配的 2.3.x,不要去引 3.x 版本否则会出现BindingException: Invalid bound statement这类问题。
XML 文件里写 SQL 时两个最容易踩的坑我提醒一下:
- 动态 SQL 中的
<号要转义成<,比如查询近 7 天数据时create_time > #{startTime}没问题,但create_time < #{endTime}必须转义,否则 XML 解析直接报错。 - 批量插入健康数据时用
<foreach>拼接 values,注意 MySQL 连接串要加allowMultiQueries=true或者用 rewriteBatchedStatements,否则批量操作性能上不去。
复杂查询我举一个真实的例子:监控大屏需要显示“每个社区今日告警数量”和“每个护工今日已处理工单数”。这用 MyBatis 写统计 SQL 再合适不过:
<select id="countTodayAlertByCommunity" resultType="map"> SELECT c.community_name AS communityName, COUNT(a.id) AS alertCount FROM alert_record a LEFT JOIN elder_info e ON a.elder_id = e.id LEFT JOIN community c ON e.community_id = c.id WHERE a.alert_time >= DATE_FORMAT(CURDATE(), '%Y-%m-%d 00:00:00') AND a.alert_time <= NOW() GROUP BY c.id, c.community_name ORDER BY alertCount DESC </select>这种写法执行计划清晰,也方便在数据库客户端里直接验证。
2.3 MySQL 表结构设计:从业务出发而不是照搬模板
表设计是这种项目最容易返工的点。我的核心体会是:不要一张大表搞定所有字段,把“基础档案”“状态信息”“绑定关系”“流水数据”分开建表。
老人基础表elder_info的核心字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| id | bigint | 主键 |
| family_user_id | bigint | 绑定的家属账号 |
| name | varchar(32) | 老人姓名 |
| gender | tinyint | 1男 2女 |
| id_card | varchar(18) | 身份证号 |
| birth_date | date | 出生日期 |
| community_id | bigint | 所在社区 |
| room_no | varchar(32) | 房间号/住址 |
| health_level | tinyint | 1健康 2重点关注 3高风险 |
| emergency_contact | varchar(32) | 紧急联系人 |
| emergency_phone | varchar(20) | 紧急联系电话 |
| status | tinyint | 1正常 2已注销 |
健康数据单独建表health_record,核心字段为心率、收缩压、舒张压、血氧、体温、设备编码、采集时间。这里要特别强调:健康数据是典型的时序数据,按时间查是最高频操作,所以recorded_time必须建索引,最好和elder_id建联合索引。数据量大以后可以按月分表,但社区项目前期单表加索引足够。
告警表alert_record一定要保留alert_source字段,标明告警是来自规则引擎自动判定还是家属手动上报,这直接关系到后续责任追溯。工单表task_order用order_no做唯一业务号,规则建议是yyyyMMddHHmmss + 4位随机数。
2.4 用 TypeHandler 处理数据库 JSON 字段
健康指标里有些数据是结构化的,比如手环上报的位置信息包含经度和纬度,护工上门服务的结果包含多项评分。这种情况用单独的字段存 JSON 字符串最省事,但 Java 实体类里想直接操作对象,就得用 TypeHandler。
我写了一个通用的 JacksonTypeHandler,继承 BaseTypeHandler<Object>,在setNonNullParameter里把对象序列化成 JSON 字符串存入数据库,在getNullableResult里把 JSON 反序列化成对应的对象类型。实体类字段上标注:
@TableField(typeHandler = JacksonTypeHandler.class) private LocationInfo locationInfo;这里要注意:XML 查询时如果返回类型是实体类且包含 JSON 字段,resultMap 里对应的字段也必须显式指定 typeHandler,否则反序列化不生效,前端拿到的可能是 null,排查半天才发现是 resultMap 漏了配置。
3. Vue3 前端架构与核心页面实现
3.1 用 Vite 搭建工程并初始化前端基础设施
前端这块我直接推荐用 Vite 的官方脚手架:npm create vite@latest care-web -- --template vue-ts,项目结构用 TypeScript 还是纯 JavaScript 视团队情况,我这边是纯 JS,因为现场维护的同事更熟悉 JS 语法。
初始化后第一步是装依赖,核心库就五样:vue-router、pinia、axios、element-plus、echarts。Element Plus 引入方式我建议全量引入,不要为了省那几百 KB 的体积在社区项目里做按需加载,全量引入的好处是团队新增成员时不用学习额外的 unplugin 配置,开发效率优先。
路由守卫必须写。系统里分成管理员、护工、家属三种角色,需要通过路由的 meta 字段控制页面访问权限。具体写法很简单:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })页面级的按钮权限可以在指令v-permission里做,但社区项目不建议过度设计,菜单控制到角色即可。
3.2 监控大屏的数据展示与实时刷新
监控大屏是这个项目最出效果的页面,也是前端代码里最值得打磨的部分。大屏我拆成了三个区域:顶部是社区总览指标卡(老人总数、今日告警数、待处理工单数、在线设备数),中间是健康告警滚动列表,右侧是社区分布统计图表。
实时刷新这里有个关键选择:用轮询还是 WebSocket。我的结论是:社区监护场景下轮询足够,10 秒一次的频率在现场完全可以接受。WebSocket 虽然响应更及时,但需要维护连接状态、断线重连、心跳保活,复杂度高一个量级。项目上线第一版用 Axios 轮询,等业务稳定后再升级 WebSocket 也不迟。
滚动列表我用 Vue3 的 TransitionGroup 做进入动画,老数据往下推,新告警从顶部插入。ECharts 初始化用ref拿到容器节点,注意在onBeforeUnmount里调用chart.dispose()释放实例,否则多切换几次 Tab 浏览器内存就涨上去了。
3.3 Axios 封装与后端联调的规范
Axios 封装有几个细节直接影响联调体验。请求拦截器统一加 token,响应拦截器统一处理业务码和 401 跳转。超时时间我设置为 30 秒,健康数据导出接口偶尔会有几秒钟的耗时,太短会误报超时。
文件导出这块单独提一下:后端返回的是文件流,不能用普通的 response 拦截器直接 JSON.parse,要设置responseType: 'blob',拿到 Blob 后自己创建下载链接。我最初的版本漏掉这个设置,导出 Excel 一直显示乱码,排查了半天才发现是响应类型没有对齐。
export function exportAlertList(params) { return request({ url: '/alert/export', method: 'get', params, responseType: 'blob' }) }另外,从 3.0 开始 Axios 的 params 序列化对数组的默认处理是a[]=1&a[]=2,后端 SpringBoot 接收这种格式会报错,建议后端接口用 List 接收时配上@RequestParam List<Long> ids,或者前端设置paramsSerializer。
4. 关键业务场景的落地细节
4.1 老人健康档案与家属绑定流程
老人建档是整个系统的入口,建档信息不可潦草。流程是:管理员录入老人基本信息,系统生成老人编号,然后家属手机号接收验证码完成绑定。绑定关系核心在elder_family_rel表,一个老人可以绑定多个家属,一个家属也可以关注多个老人(比如子女同时照顾两位老人)。绑定时必须校验手机号是否已注册,否则会出现家属账号存在但无法绑定的尴尬。
健康等级的初始判定在提交建档时根据慢病列表自动计算:有心脑血管疾病史的自动标为“重点关注”,如果同时有高血压和糖尿病,直接标为“高风险”。这里的分级规则后续上线后一定会反复调整,所以规则不要写死在 SQL 里,我放到了配置文件并预留了规则表,后续方便运营人员自己调整阈值。
4.2 告警工单的完整闭环
告警和工单是这个项目里唯一带状态机的业务模型,状态流转必须设计清楚。告警状态有:待处理、处理中、已完成、已忽略。工单状态有:待派单、已接单、已完成、已取消。
这里我踩过一个坑:最初告警表和工单表共用一段状态字段,结果告警明明还没处理,工单却先完单了,前端页面展示的逻辑越写越乱。后来彻底拆开,告警和工单通过alert_id关联,各自维护自己的状态。派单成功后同时更新两边状态,工单完成后反向更新告警状态,这个联动就清晰了。
护工端的工单详情页要显示老人基础信息、告警原因、紧急联系人电话,以及一键拨号入口。注意一键拨号在 PC 端不是很好实现,当前项目主要面向 PC 后台,接单操作还是靠电话沟通后手动点击确认。
4.3 健康趋势分析与数据导出
健康趋势分析是一个容易被低估的功能。很多系统只做数据采集,不做趋势展示,结果家属端打开页面看到一堆数字完全无感。我用 ECharts 的折线图展示 7 天、30 天的心率趋势,并在异常点上方用特殊标记标出告警时刻,前端看到图就能直观理解:“昨晚凌晨 3 点心率过高,触发了告警”。
数据导出我用的 Apache POI 生成 xlsx,导出范围控制在一个月以内,数据量太大容易响应超时。导出文件名用LocalDateTime.now()拼接yyyyMMddHHmmss后缀防止重复。如果现场有文件存储需求,可以考虑把导出的临时文件扔到 Minio 并生成短时下载链接,不要用本地磁盘临时文件目录,重启服务后文件还在但链接失效的问题很恶心。
5. 常见问题排查与避坑指南
5.1 数据库连接与时间相关问题
MySQL 8.0 连接串必须显式指定时区,否则部署到现场 Windows 服务器上,数据库报错信息里经常出现 “The server time zone value” 的异常。连接串上加serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true就稳了。allowPublicKeyRetrieval 这个参数经常被忽略,8.0 默认使用 caching_sha2_password 认证,首次连接需要获取公钥,不加这个参数驱动可能直接拒绝连接。
另外 SpringBoot 2.7 默认的数据库连接池 HikariCP 参数需要调一下:最大连接数maximum-pool-size建议 20,连接超时connection-timeout建议 30000 毫秒。社区项目并发不高,但现场如果有多个服务同时连同一个库,连接池太小会明显拖慢接口响应。
5.2 MyBatis 常见坑:缓存与映射问题
MyBatis 的二级缓存我默认是关闭的。这个项目涉及健康数据、告警信息,对实时性要求高,缓存带来的性能提升在这类写入频繁的场景下收益有限,反而容易出现“数据更新了但查询结果还是旧值”的灵异问题。开发环境开启本地缓存作为 SQL 调优参考可以,生产环境建议关闭。
还有一个映射问题非常隐蔽:实体类字段如果有is前缀,比如isAbnormal,MySQL 字段如果命名为is_abnormal,开启驼峰映射后 MyBatis 会自动映射成isAbnormal,看着没问题,但 Lombok 生成的 getter 方法名是getIsAbnormal而不是isAbnormal,某些框架反射取属性时会不一致。解决方式是实体类里避免is开头的布尔字段,改成abnormalFlag一类的命名,一了百了。
5.3 Vue3 响应式与组件通信的实战坑
Vue3 的reactive有一个限制很多人不知道:直接解构对象会丢失响应性。比如从 store 里取用户信息时写const { name, role } = store.userInfo,页面里修改 store.userInfo.name 后,这个解构出来的 name 不会自动更新。解决方式是用storeToRefs,或者干脆在模板里直接引用 store 对象。
组件通信上我踩过最烦的一个坑是:ECharts 图表在 el-tab-pane 里初始化时宽度经常是 0,导致图表被压缩成竖线。原因是 Tab 切换后隐藏的容器没有宽度信息。我的解决方式是图表初始化前判断容器宽度,如果为 0,等 Tab 切换激活事件后再调用nextTick初始化。
还有一个很常见的告警:前端开发环境请求接口报 CORS 错误,后端明明加了跨域配置还是报。检查一下是不是通过 Vite 的 proxy 代理,如果用了 proxy,后端就不要再配允许跨域了,代理转发属于同源请求,双重配置反而会在生产环境出奇奇怪怪的问题。
5.4 部署环境与版本兼容性问题清单
现场部署最容易出问题的就是环境版本不一致。我整理过一张对照表:
| 组件 | 版本 | 说明 |
|---|---|---|
| JDK | 1.8 或 17 | SpringBoot 2.7 用 1.8,3.x 用 17 |
| Maven | 3.6+ | 打包时 skiptests |
| Node.js | 16+ | Vite 5 需要 Node 18 以上 |
| MySQL | 8.0 | 字符集 utf8mb4 |
| Nginx | 1.22+ | 前端静态托管 + 反向代理 |
前端打包后如果出现路由刷新 404,是 Nginx 没有配try_files导致的,加上try_files $uri $uri/ /index.html;解决。后端接口用/api前缀并统一代理到 jar 服务端口,这样前端资源与接口域保持一致,避免生产环境额外的跨域配置。
6. 实操心得与经验收尾
这项目我从开发到上线稳定运行,最大的体会是社区类管理系统的成败不在技术多炫,而在数据链路是否闭环。健康数据从设备上报,经过接口落库,再到前端展示,最后触发告警、生成工单、处理回填,每一步都不能断。所以做的时候多花时间梳理状态流转,比多写几千行代码更有价值。
另外一个小建议:用 SpringBoot + Vue3 做这类系统时,接口命名和字段命名一定要从第一天就统一规范。比如时间字段统一叫xxxTime,不用xxxDate,状态字段统一用数字0/1而非字符串enable/disable。前期不规范后期改起来牵一发动全身,尤其是前端页面已经写死字段名的时候,改一个字段名要动七八个文件。
最后我再分享一个小技巧:健康数据的采集接口一定要做幂等。手环设备因为网络原因可能重复上报同一条数据,我在接口里通过device_code + recorded_time的唯一索引去重,重复请求直接返回成功但不实际插入。这种细节网上教程几乎不会提,但现场设备一旦上线,这就是保命的设计。项目源码整体打包后就可以直接部署,前端 Nginx 托管、后端 jar 服务化运行,给现场运行人员一份部署文档,基本上就是一键启动。这套结构后续要扩展 IoT 设备接入或者小程序家属端,也都有现成的扩展位。