拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

SpringBoot+Vue3+MyBatis+MySQL实战:构建养老智慧服务平台

SpringBoot+Vue3+MyBatis+MySQL实战:构建养老智慧服务平台

1. 项目背景与需求拆解

1.1 养老服务平台到底在解决什么问题

先说结论:这个项目的核心价值不在于“技术有多新”,而在于它把一套典型的业务闭环用技术手段跑通了。养老智慧服务平台,本质上做的是“养老服务供需两端的信息化接管”——一边是老人、家属、护工、社区管理员,另一边是健康档案、工单调度、费用结算、紧急告警这些后端流程。没有这套系统之前,很多养老机构靠的是微信群接龙、Excel表格排班、纸质健康档案,信息割裂且追溯困难。

做这个项目,最直接的动机就是替代人工台账。我见过不少实际落地的养老机构,护工排班、老人用药提醒、探访记录,全是手写或口头交接,一旦发生纠纷,连基本证据链都拿不出来。所以这类平台通常要求四大核心能力:老人档案数字化、服务工单流转、健康数据采集、权限分级管理。技术上自然就倒逼出一个前后端分离架构:管理端给机构运营人员用,客户端给家属或护工用,数据统一沉淀到 MySQL。

1.2 这套技术组合的普适性

SpringBoot + Vue3 + MyBatis + MySQL,在今天的 Java 后端项目里属于最标准的“人民群众组合”。它不是最潮的,但一定是最稳的。SpringBoot 负责把服务端组件黏合起来,MyBatis 负责 SQL 可控的持久层操作,Vue3 负责前端响应式交互,MySQL 负责关系型数据存储。对养老平台这类业务来说,事务性强、报表需求多、权限模型复杂,这套组合的匹配度非常高。

我经常跟人讲,选技术栈不是选秀,是选“出问题后你能查明白的”。SpringBoot 的生态资料最全,MyBatis 的 SQL 排查链路最短,Vue3 的社区问答量极大,MySQL 更是几乎所有 Java 开发者的起点。这个项目的源码能跑通,意味着你后续做任何 CRUD 业务系统——进销存、教务管理、物业工单——都能复用同一套骨架。

2. 技术选型与架构设计思路

2.1 为什么用前后端分离,而不是服务端渲染

养老平台的访问场景很特殊:管理后台是固定设备,家属端是手机浏览器,护工端可能是平板。如果沿用传统的 Thymeleaf 或 JSP 服务端渲染,每次改版都要重新发后端包,而且前后端并行开发效率极低。前后端分离的核心收益是:后端只出 JSON 接口,前端独立部署,两端各自迭代。

具体到这个项目,Vue3 负责的页面包括登录、老人档案列表、工单处理、健康数据图表、排班日历等,交互密度不低。用 SPA 模式可以做到局部刷新、组件复用,体验上比整页刷新好一个档次。尤其是健康数据图表这类模块,如果用服务端渲染,前端还得靠 jQuery 手动操作 DOM,代码可维护性会很差。

2.2 SpringBoot 在后端扮演的角色

SpringBoot 在这个项目里最大的价值不是“自动配置”这种概念,而是它把项目启动成本降到最低。一个养老平台后台,需要 Web 能力、事务管理、定时任务、文件上传、邮件通知(比如告警邮件),SpringBoot 的 starter 机制让你加依赖就能用,不需要像 Spring MVC 时代那样写一堆 XML 配置。

我在这个项目里最常用的几个组件:

  • spring-boot-starter-web:提供 RESTful API 基础
  • spring-boot-starter-validation:参数校验,避免脏数据入库
  • spring-boot-starter-quartz:定时任务,比如每天凌晨生成健康日报
  • mybatis-spring-boot-starter:MyBatis 与 SpringBoot 的官方整合包

这里有个细节容易踩坑:SpringBoot 版本和 MyBatis starter 版本存在兼容关系,不建议盲目追新。比如 SpringBoot 3.x 要求 MyBatis starter 至少 2.3.x 以上,且底层基于 MyBatis 3.5+。如果你用 SpringBoot 2.x,就不要强行升级 MyBatis 到 3.5.10 以上,某些 SQL 方言行为会有细微差异。

2.3 MyBatis 在这个场景里的选择理由

JPA 和 MyBatis 之争老生常谈,但在这个养老平台场景里,MyBatis 是更理性选择。原因很简单:业务里有大量复杂报表查询和多表关联统计,比如“每个护理员本月完成工单数”“老人健康指标趋势”,这些 SQL 用 JPQL 写起来非常别扭,而 MyBatis 的 XML 映射文件天生就是给这种场景准备的。

另外,养老平台涉及敏感健康数据,很多机构要求数据库层面的留痕和审计。MyBatis 允许你在 XML 里手写 SQL,加上拦截器就能实现统一的数据权限过滤(比如护工只能看自己负责的老人)。这种控制力,用 JPA 的 Specification 也能做,但可读性和调试便利性差很多。

2.4 MySQL 数据库选择的现实考量

MySQL 在这个项目里没有悬念。养老平台的数据量级,单表百万级已经算很大了,MySQL 的 InnoDB 引擎配合合理的索引设计完全够用。而且这类项目大概率要部署在机构内网服务器或小型云主机上,MySQL 的部署运维成本最低,还不用额外支付软件授权费。

数据库设计上我坚持三个原则:表名业务化(elder_info、care_order)、所有表带 create_time/update_time 字段、金额字段用 decimal 而非 float。这些原则看似基础,但在实际维护中能救命。尤其金额字段,用 float 存账目,时间长了必然出现精度漂移,财务对不上账就是事故。

3. 核心功能模块与数据库模型设计

3.1 老人档案模块的设计要点

老人档案是养老平台的数据底座,几乎所有业务都围绕它展开。档案表除了基本信息(姓名、身份证、家属联系方式、入住时间),还要考虑扩展字段:紧急联系人、既往病史、过敏药物、护理等级。这些字段不能全塞进一张表,否则随着机构业务扩展,表结构会变得越来越臃肿。

我采用的做法是拆成两张表:elder_base_info 存固定字段,elder_ext_info 存 KV 结构扩展数据。这样做主表查询时不会因为 TEXT 字段拖慢速度,扩展字段变更时也不需要频繁 ALTER TABLE。实际上,很多机构连“是否失能”“失能等级”这类状态都经常调整,把它拆出去后用单独的表记录变更历史,更符合运营审计需求。

引用块提示:elder_base_info 表的主键建议用雪花 ID 而不是自增 ID。原因在于项目后期可能要做多机构数据汇总,自增 ID 在数据库迁移或多库合并时容易冲突,雪花 ID 能保证全局唯一,同时保持索引顺序性。

3.2 服务工单流转逻辑

服务工单是平台的“血压计”——从家属下单、护工接单、服务完成、满意度评价,整条链路决定了平台是否真正产生价值。数据库层面需要四张核心表:order_main(工单主表)、order_item(工单明细,比如具体服务项目)、order_status_log(状态流转日志)、order_evaluation(评价表)。

状态流转是这里最值得设计的部分。我定义了五态:待接单、服务中、已完成、已取消、已关闭。每个状态变更都必须在 order_status_log 里记录操作人、操作时间、变更前状态、变更后状态、备注。这个设计很关键——养老场景里一旦家属投诉“没按约定时间上门服务”,运营人员可以通过日志精确还原当时发生了什么。

3.3 健康数据采集与展示

健康数据模块包括血压、血糖、心率、体温等指标记录。这块的表设计比较容易走极端——有人搞一张超宽表,把十几种指标全塞进一行;有人搞 EAV 模式,每种指标一条记录。我的建议是这种指标数据适合用窄表:health_metric_record,字段包含 elder_id、metric_type、metric_value、measure_time。一张表覆盖所有指标,查询时按 metric_type 过滤即可。

这样设计带来的好处是扩展新指标时不需要改表结构。比如机构下个月新增“血氧饱和度”采集,直接在枚举里加一个类型就行。缺点是同一时间内多种指标的数据分散在多行,前端展示折线图时需要一次查询拿回全部类型,再在内存里转成按时间聚合的结构。这个问题在数据量不大(几万条)时完全无感,所以无需过度优化。

3.4 排班与护工管理

护工管理模块的复杂度常被低估。一个护理员可能同时服务于多位老人,排班可能出现冲突,而且护理员的考勤直接关联薪资结算。这个模块我拆成了三个部分:staff_info(护工基本信息)、staff_schedule(排班计划)、staff_attendance(考勤打卡)。排班表设计时特别要注意“周期规则”字段,很多机构的排班是“做六休一”或“上一休一”,不能只存某一天的值,需要用 cron 表达式或自定义规则字段来存循环模式。

考勤这块容易和排班表产生数据冗余。我建议考勤表只存“实际打卡记录”,而“应出勤时间”始终从排班计划实时计算。这样如果排班调整,不需要回刷历史考勤。这是很多初学开发者容易犯的错误:把应出勤时间物化到考勤表里,一旦排班变更,历史数据就乱了。

4. 后端核心实现细节与实操要点

4.1 SpringBoot 项目结构规划

项目结构决定后续维护体验,我推荐按业务模块分包,而不是按技术类型分包。比较两种方式:

  • 按技术分包:controller/、service/、mapper/,所有业务混在一起,文件多了以后非常难定位。这是很多培训项目带出来的坏习惯。
  • 按业务分包:elder/、order/、staff/、health/,每个包内自含 controller、service、mapper。

这个项目我采用按业务分包,同时保留一个 common 包放全局异常处理、统一返回体、工具类。统一返回体我定义为 Result ,包含 code、message、data 三个字段。code 为 0 表示成功,非 0 表示业务错误码。这里需要注意:不要把 HTTP 状态码直接当业务码用,因为业务错误码需要具备自解释性,而 HTTP 状态码语义粒度太粗。

4.2 MyBatis 映射文件的编写规范

MyBatis 的 XML 文件是最容易“写飞”的地方。我总结了三个实用规范,实测能有效减少低级 bug。

第一,所有表名和字段名在 SQL 中显式列出,禁止使用 SELECT *。原因不仅是性能,更重要的是当表结构变动时,SELECT * 会在运行时才暴露问题,而显式字段在开发阶段就能通过代码检查发现。

第二,条件查询统一用动态 SQL,并注意 where 标签的用法。很多初学者会在 where 后面手工加 1=1 来拼接条件,这在小数据量场景没问题,但会严重影响查询优化器对索引的利用。MyBatis 提供了 标签,可以自动去掉多余 and/or。

第三,批量插入不要用 foreach 拼接单条 insert 语句的方式。在大数据量下,这种做法会导致 SQL 超过 MySQL 的 max_allowed_packet 限制。建议改用 MyBatis 的批量模式,或者使用 MySQL 的 INSERT INTO ... VALUES (...), (...), (...) 语法,控制每批 500-1000 条。

4.3 数据权限控制的落地

养老平台天然需要数据权限控制:护工只能看自己负责的老人,护士长可以看整个楼层,管理员可以看全机构。这种需求如果在每个 Service 里手工写条件,代码会严重重复。

我的做法是使用 MyBatis 拦截器,在 BaseMapper 层面做统一数据过滤。具体思路:自定义注解 @DataScope(type = "elder") 标记在查询方法上,拦截器解析当前登录用户的角色,自动在 SQL 后面追加 AND elder_id IN (...)。这样业务代码里完全不需要感知权限逻辑,权限规则调整时只改拦截器一处即可。

需要注意,这种方案并不适合所有场景,如果系统未来要升级为多租户架构,建议直接用 MyBatis-Plus 的租户插件。当前项目的体量下,拦截器方案是最轻量的选择。

4.4 事务与并发控制

养老平台的并发量虽然不高,但某些场景的并发问题必须提前预防。最典型的是“工单抢单”——多位护工同时点击接单,如果不加控制,一个工单可能被多人接走。

解决方案有三种思路:悲观锁(SELECT FOR UPDATE)、乐观锁(版本号字段)、状态机约束(UPDATE 语句带状态条件)。我的选择是第三种,因为工单表本身就有状态字段,直接使用 UPDATE order_main SET status = 2 WHERE id = #{id} AND status = 1,返回受影响行数,如果为 0 则说明已被他人接走。这种方式不引入额外锁开销,还能和状态机流转天然结合。

另外要注意 @Transactional 的使用边界。事务不要开在 Controller 层,应该开在 Service 实现类上,并且尽量保持事务短小。如果事务内包含远程调用(比如调用外部短信接口),一旦外部服务响应慢,数据库连接会被长期占用。这种情况建议把远程调用移到事务提交之后的事件监听器里。

5. 前端 Vue3 实现与工程化细节

5.1 前后端分离下的前端架构选择

Vue3 项目的工程化,第一个决策是用 Composition API 还是 Options API。这个项目的老人管理、工单列表、健康图表等页面都有大量状态联动,我推荐使用 Composition API。不是说 Options API 不能写,而是组合式函数(composables)确实能比 mixin 更清晰地组织逻辑。

比如健康数据的图表模块,可以封装一个 useHealthData composable,内部管理查询参数、加载状态、图表实例、数据转换逻辑。在多个页面复用时,直接调用 useHealthData() 即可。如果是 Options API,要么复制粘贴一大段 methods,要么用 mixin 引入不可见的数据来源,维护体验相差很大。

5.2 与后端接口约定的最佳实践

前后端联调最忌讳的是接口约定随意。这个项目我坚持用一套静态的 API 定义文件(TS 类型或 JSDoc),确保两端同步。后端定义好接口后,前端先根据接口文档生成对应的 TypeScript 类型,再开始写页面逻辑。这样可以避免“后端返回的字段名是 elderName,前端却写成了 name”这类低级问题。

具体落地时,我用 Axios 封装了一个 request 实例,统一处理三件事:baseURL 配置、token 注入、错误提示。后端的 Result 结构在响应拦截器里被解包,业务代码只需要接收 data 部分。遇到 code != 0 的情况,拦截器统一弹出错误消息,并且对 401 做跳转登录处理。这样每个具体接口的函数体非常干净,只保留业务逻辑。

5.3 权限菜单的动态渲染

管理后台的侧边栏菜单,通常需要根据登录用户的角色动态生成。这个项目的做法是登录后调用 /api/auth/menus 接口,后端根据用户角色返回权限树,前端用递归组件渲染成菜单。

这个方案比静态路由表好在哪里?好处是新增角色时不需要改前端代码。而且按钮级别的权限也可以通过类似机制控制——后端返回的权限标识数组,在前端注册一个 v-permission 自定义指令,判断元素是否渲染。

但要注意,前端权限控制只是提升体验,不能代替后端权限校验。任何人都可以通过浏览器的开发者工具把隐藏的按钮显示出来,所以后端接口必须再次校验操作权限。这个原则我在项目中反复强调。

5.4 Vue3 项目打包与部署

Vue3 项目的打包很常规,npm run build 生成静态文件,然后扔到 Nginx 下托管。但有一个关键配置容易踩坑:前端路由如果用了 history 模式,Nginx 需要配置 try_files 来支持前端路由回退。否则刷新页面就会出现 404。

配置示例:

location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }

如果是直接把前端 dist 目录塞进 SpringBoot 的 resources/static 下,有一个坑:接口路径和静态资源路径不能冲突。建议后端统一设置 server.servlet.context-path=/api,前端请求统一加到 /api 前缀,Nginx 里把 /api 转发到后端服务,其余请求指向静态文件。这种部署方式下,SpringBoot 内部不需要额外做静态资源映射。

6. 前后端联调、部署与环境搭建

6.1 本地开发环境搭建要点

搭建这套项目要准备四个基础工具:JDK 8 或 11(SpringBoot 2.x)或 JDK 17(SpringBoot 3.x)、Maven 3.6+、Node.js 16+、MySQL 5.7+。这里有个容易忽视的问题:Maven 仓库镜像和 Node 包的源都建议配置国内镜像,否则首次构建会卡在依赖下载上。

MySQL 版本选择上,我建议和项目的 application.yml 里的 driver 版本匹配。SpringBoot 2.x 自带 mysql-connector-java 8.0.x,连接 MySQL 5.7 和 8.x 都没问题。但如果你本地安装的是 MySQL 8.x,事后部署到生产环境用的却是 MySQL 5.7,就可能在 SQL 语法或排序规则上出现差异。所以测试环境和生产环境的数据库版本尽量保持一致。

6.2 数据库初始化与种子数据

项目源码里通常带一个 init.sql 或者 schema.sql,里面包含建表语句和基础数据。我拿到新项目的第一件事不是看代码,而是先执行数据库脚本,把库跑起来。因为看懂数据模型比看懂代码更高效——数据表之间的关系直接反映了业务关系的设计。

种子数据至少要包含:管理员账号(密码需要 BCrypt 或 MD5 加密后的值)、默认角色、菜单权限数据。如果你发现 init.sql 里的密码是明文,建议初始化后第一时间改成加密值并测试登录流程。SpringBoot 里集成 spring-security-crypto,用 BCryptPasswordEncoder 生成密文,而不是用 MD5——MD5 现在可以秒级爆破,不适合存密码哈希了。

6.3 启动过程的常见报错与排查

SpringBoot 项目启动失败,第一个要看的是控制台最下方的异常摘要,而不是整篇红色日志。常见几类报错:

  • 端口被占用:检查 server.port 配置值,用 netstat -ano | findstr 8080 找到占用进程。
  • 数据库连接失败:大概率是密码或 IP 配置错,先确认 application.yml 里的 url 是否带时区参数(serverTimezone=Asia/Shanghai)。
  • 表不存在:检查 mapper 对应的实体类和表名是否一致,MyBatis 默认并不会自动建表,需要手工执行 SQL 脚本。

前端启动失败则通常是 node_modules 安装问题。Vue3 项目对 npm 版本有要求,npm 7+ 的依赖树解析规则更严格,老项目直接 npm install 可能报 ERESOLVE。此时可以尝试 npm install --legacy-peer-deps,或者直接删除 package-lock.json 重新安装。

6.4 生产环境部署的推荐方案

生产部署我推荐两种方式:

  • 常规方式:后端打包成 jar,由 systemd 管理;前端打包成静态文件,由 Nginx 托管;数据库用独立 MySQL 实例。
  • 容器化方式:后端写 Dockerfile,前端用 Nginx 镜像打包,MySQL 用官方镜像,通过 docker-compose 统一编排。

容器化的好处是环境一致性,但也带来了额外学习成本。如果团队对 Docker 不熟,不建议为了“显得先进”强行容器化。我见过不少项目为了 Docker 而 Docker,最后整个团队都在和网络模式、卷挂载做斗争,得不偿失。

7. 常见问题与排查技巧实录

7.1 MyBatis SQL 查询结果映射失败

典型症状:接口返回的某个字段一直是 null,但数据库里明明有值。排查思路按顺序走:

  • 检查 XML 里的 resultMap,看 column 和 property 是否严格对应。特别注意下划线字段和驼峰字段的映射。
  • 如果开启了 mapUnderscoreToCamelCase=true,确认实体类字段命名规范性,比如 create_time 要对应 createTime。
  • 检查是否有多表联查时字段名冲突,两个表都有 create_time,结果映射到同一个属性上。

建议在开发环境开启 MyBatis SQL 日志输出,配置方式是在 application.yml 里设置 logging.level.com.project.mapper=debug。这样每次查询都能看到实际执行的 SQL 和参数,定位问题效率大大提高。这个配置建议从项目一开始就打开,写完再关。

7.2 Vue3 页面数据不更新的诡异现象

Vue3 的响应式系统基于 Proxy,通常不会出现 Vue2 那种新增属性不响应的问题。但有一个常见坑:用 reactive 定义对象,然后整体替换该对象时,会丢失响应式连接。比如:

const state = reactive({ list: [] }) // 请求后 state = { list: res.data } // 错误

正确写法是修改对象属性,而不是整体赋值:

state.list = res.data

另一个坑是 ref 在模板里自动解包,但在函数内部访问时需要用 .value。如果不小心在 setup 里返回了一个 ref 对象,模板中写 {{ userInfo }} 是正确的,但如果写 {{ userInfo.name }},并且在 script 里忘了 .value,控制台会报 undefined。这类问题通过 ESLint 的 vue/no-ref-as-operand 规则可以大部分拦截。

7.3 跨域问题的最终解决方案

前后端分离项目,联调阶段必遇到跨域。常见表现是浏览器控制台报 CORS 错误,Request 是 OPTIONS 方法。解决方案有三个层次:

  • 临时方案:后端写一个 CorsFilter,放行所有来源和方法。适合开发环境,但生产环境不安全。
  • 标准方案:在 SpringBoot 里配置 CORS 规则,指定允许的 origin。注意不能配置为 *,因为涉及携带 cookie 的请求无法通过。
  • 最推荐方案:生产环境用 Nginx 反向代理,让前端请求和后端接口处于同一个域名之下,从根上规避跨域。

我的经验是跨域问题不要在开发环境过度纠缠,浏览器装一个 Allow CORS 插件临时解决,生产环境统一交给 Nginx。这样既省时间,也符合真实部署架构。

7.4 定时任务莫名的重复执行

SpringBoot 里用 @Scheduled 注解做定时任务,有时会出现任务重复执行。原因通常是部署了多个实例,或者应用被重复启动。排查方法很简单,看日志里是否有多个 Spring 启动横幅——如果有,说明有多个进程;另一种情况是配置了并行的 TaskScheduler,导致同一方法被并发调用。

解决方法:确保 @Scheduled 方法内不依赖实例状态,并把锁放到底层保证。最简单的做法是给方法加一个基于数据库的唯一执行记录表,任务开始时插入一条 task_execution_log(task_name、execution_time),插入成功才执行任务逻辑。这种方式在分布式环境下也能生效,比单机的 synchronized 锁可靠得多。

8. 项目扩展与二次开发建议

8.1 从单体到可扩展架构的演进路径

这个项目的基础架构是单体,但代码组织方式已经为后续演进留了空间。如果运营数据量快速上涨,优先考虑读写分离——MySQL 主从复制,SpringBoot 里通过 AbstractRoutingDataSource 实现多数据源路由。这是成本最低的扩容手段,比引入微服务全家桶划算得多。

如果业务上需要对接第三方系统(比如政府监管平台、智能穿戴设备),建议在 Service 层之下加一层防腐层 adapter。外部系统的协议变更,只影响 adapter 模块,主体业务代码不需要改动。这是我在很多实际项目中总结出来的经验——提前留好接口边界。

8.2 报表模块的优化空间

平台运营一段时间后,管理层一定需要一个“数据大屏”或周报月报功能。基于现有 MySQL 数据,可以直接写报表 SQL 实现,但复杂的统计查询要避免在业务高峰期执行。更稳妥的做法是新建一个只读的统计库,同步业务库的数据,报表查询只打统计库。

同步方案我推荐定时全量同步,因为这个体量的数据不需要实时的 Binlog 订阅。每天凌晨同步一次,白天统计查询就畅通无阻了。附带的好处是统计库可以做只读权限控制,降低误操作风险。

8.3 敏感信息的安全加固

养老平台涉及老人姓名、身份证、健康状况等个人信息,合规要求很高。安全加固至少要做三层:数据库层——身份证等敏感字段使用 AES 加密存储;接口层——通过报文日志脱敏,禁止打印完整身份证号;传输层——生产环境必须启用 HTTPS。

我见过不少项目把 HTTPS 部署放在最后才考虑,结果改造时发现前端资源引用、接口请求地址都写死了 http,改起来非常费劲。建议在项目初始化阶段就把 HTTPS 环境列进规划,前后端代码里所有地址都使用相对路径或协议相对路径,避免这个问题。

写在最后

做这类业务系统源码分享项目,我个人的体会是:技术方案永远服务于业务场景。养老服务平台看起来像是一个普通的 CRUD 系统,但真正深入后你会发现,权限粒度、状态流转、数据审计、敏感信息保护,每一处都隐藏着行业的真实约束。这个项目的源码价值不在于“能跑”,而在于它提供了一个可以挂载真实业务规则的骨架,拿来学习也好、做二次开发也好,至少不用从零开始踩一遍坑。

最后再分享一个实操小技巧:拿到任何一套前后端分离源码,第一步不要急着跑。先花半小时把数据库表结构画成 ER 图,把每个表的业务含义理清楚,再对接口文档看每个 API 操作哪几张表。这个过程比直接 debug 代码高效得多,而且能让你从作者视角理解整个系统的设计意图。这套方法你多试几次,能力提升速度会非常明显。

返回列表