SpringBoot+Vue3养老智慧服务平台全栈开发实战:从数据库设计到部署排坑 1. 项目拆解养老智慧服务平台到底在解决什么问题1.1 业务背景与核心痛点养老智慧服务平台从项目名称就能看出它的定位——用一套完整的信息化方案把养老机构、社区居家养老服务、家属联动这几条线串起来。我这两年接触了不少类似的数字化项目一个很深的感触是养老行业的技术需求被严重低估了。很多人以为养老平台就是做个老人信息台账实际上真做起来涉及的业务复杂度和数据敏感度一点都不比一般的电商、OA系统低。这个平台的用户角色至少有四类机构管理员、一线护理人员、老人本人、老人家属。每类角色对系统的诉求完全不同。管理员要的是运营数据、床位状态、护理工单的全局视图护理人员要的是快速接收任务、记录服务过程、上报异常老人要用最简单的方式发起求助或查看服务安排家属则希望随时知道老人的健康指标和活动状态。一个平台同时满足这四类诉求业务模型的复杂度是实打实的。技术层面也一样SpringBoot Vue3 MyBatis MySQL 这套组合在我看来是这个体量项目里最舒服的搭配。SpringBoot负责把后端服务搭起来Vue3撑起前端交互MyBatis管好SQL映射MySQL存业务数据。前后端分离意味着前端页面和后端接口可以各自独立开发、独立部署对团队协作和后续迭代都更友好。下面的内容就围绕这套系统把我做完整个项目的设计思路、核心模块实现和踩坑过程完整过一遍。1.2 系统角色与核心业务闭环这套平台的一个关键设计是用工单把老人、护理人员、管理员串成闭环。老人发出求助或系统生成定期照护任务后平台自动生成一张服务工单护理员在移动端或Web端接单、执行、回写记录管理后台实时跟踪工单状态家属端能看到工单完成情况。这个闭环是整个系统的主干我把业务模块拆成这几块老人信息档案基础资料、家属绑定、健康标签、过敏史、紧急联系人。健康监测数据血压、心率、血糖、体温等周期性指标记录与趋势展示。照护工单管理工单创建、派单、接单、执行、回执、评价的完整状态机。服务排班与任务池护理员的班次管理定期任务自动生成。告警与异常上报指标超限、跌倒求助、未按时服务等异常触发的告警流。家属端联动家属通过小程序或Web页面查看老人状态、接收告警通知。运营统计看板床位利用率、工单完成率、服务满意度等核心运营指标。这套闭环的最大价值是过程留痕。以前养老机构对服务质量的把控靠纸质排班表和口头汇报管理基本是黑盒。有了工单流之后每一次服务都有时间戳、操作人、结果记录既方便内部分析也方便对家属交代。2. 技术选型思考为什么偏偏是这套组合2.1 后端为什么选 SpringBoot 而不是别的我见过很多团队一上来就在纠结框架选型其实对于养老智慧服务这类业务逻辑明确、CRUD密集、需要快速交付的中小规模系统SpringBoot就是最省事的选择。它的自动配置机制让我不用去折腾Spring XML配置内嵌Tomcat让部署变成打一个jar包丢到服务器上这么简单。具体到项目里我用了SpringBoot 2.7.x版本为什么不用3.x在后面排坑部分细说搭配了这几个关键依赖dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.1/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdcom.baomidou/groupId artifactIdmybatis-plus-boot-starter/artifactId version3.5.3.1/version /dependency有人会问既然用了MyBatis-Plus为什么标题里还强调MyBatis因为MyBatis-Plus本身就是MyBatis的增强插件底层的SQL映射原理、Mapper机制完全一致只是多了CRUD方法和分页插件这些开箱即用的能力。考虑到养老平台里大量基础数据操作都是标准CRUD用增强插件能省下大量重复代码同时遇到复杂统计查询我还是手写XML里的SQL两条路都能走。2.2 前端为什么选 Vue3前端这边Vue3的组合式APIComposition API是真正提升开发效率的东西。我记得做老人健康趋势页面的时候需要同时处理多个数据源的请求、状态切换、图表更新逻辑用Options API的时候数据、方法、生命周期钩子分散在各处改一个功能要上下翻代码换成组合式API之后把所有相关的响应式数据和操作函数收拢到一个setup逻辑块里维护起来舒服得多。我的前端技术栈是这样搭配的构建工具Vite开发环境下热更新速度比Webpack快一大截启动项目基本秒开。UI组件库Element PlusForm表格、弹窗、日期选择器这些后台管理高频组件开箱即用。状态管理PiniaVue3官方推荐比Vuex的TS支持和代码提示更好。HTTP请求Axios统一拦截器处理Token、错误提示。图表ECharts健康趋势曲线和数据看板的主力。Vue3的setup语法糖写业务页面非常顺手比如一个简单的老人列表卡片组件核心逻辑可以收在一个块里模板和逻辑的对应关系一目了然。这种写法对团队里熟悉React Hooks的同事也很友好心智模型几乎一致。2.3 持久层选型和SQL控制力的取舍MyBatis这套东西本质上就是把手写SQL的能力留在开发者的手里。养老平台虽然CRUD多但真正有价值的查询都在统计和报表侧比如按月统计各护理员的服务工时、按机构统计工单完成率、计算老人健康指标异常发生率。这类复杂查询如果走JPA的自动生成SQL会变得非常别扭而MyBatis的XML映射文件可以精确控制每一条SQL的写法。举个实际例子我写筛选出近7天内未完成任何服务工单且有健康监测异常的老人时需要跨三张表关联查询。JPA那种通过方法名推导查询的方式基本写不出来而MyBatis里直接写select idselectAbnormalUnservedElders resultTypemap SELECT e.id, e.name, e.room_no, COUNT(a.id) AS abnormal_count FROM elder_info e JOIN health_record a ON a.elder_id e.id WHERE a.record_time gt; DATE_SUB(NOW(), INTERVAL 7 DAY) AND a.is_abnormal 1 AND NOT EXISTS ( SELECT 1 FROM service_order so WHERE so.elder_id e.id AND so.create_time gt; DATE_SUB(NOW(), INTERVAL 7 DAY) ) GROUP BY e.id, e.name, e.room_no HAVING abnormal_count gt; 2 /select这种SQL的灵活度是持久层框架核心竞争力所在。也正因为如此我建议做这类业务系统的朋友别一味追求零SQL该手动写的时候还是要手动写对后续排查性能问题也有好处。3. 数据库设计与核心表结构解析3.1 业务表划分思路MySQL在这套系统里承担的是单一业务主库的角色。我设计表结构时遵循了几个原则业务边界清晰、敏感数据独立存储、高频查询字段合理冗余、时间字段统一用datetime。核心表大致这样划分模块表名核心字段老人档案elder_infoid, name, gender, birth_date, room_no, health_status, contact_phone家属关系family_memberid, elder_id, member_name, relation, phone健康数据health_recordid, elder_id, record_type, record_value, record_time, is_abnormal工单管理service_orderid, elder_id, worker_id, order_type, status, start_time, finish_time护理人员care_workerid, name, job_no, shift_group, phone告警记录alert_recordid, elder_id, alert_type, alert_level, content, is_handled操作日志operation_logid, user_id, action, target_type, target_id, create_time3.2 表关系设计里容易忽略的细节第一老人和家属是一对多关系不要图省事把多个家属塞进一个字段。一个老人可能同时绑定配偶、子女、护理顾问每个家属接收通知的偏好不一样拆成独立表才能在推送通知时做精细控制。第二工单表要保留冗余的老人姓名和床位号。虽然第三范式要求通过elder_id关联查询就够了但工单列表页和家属端展示频繁用到这些字段关联查一次就多一次索引回表。我直接冗余了一份快照字段查询速度明显改善付出的代价只是数据一致性需要靠写入逻辑保证。第三健康记录表要按老人ID建联合索引。这个表的写入量最大查询模式又非常固定——按老人查某个时间段的数据。我把索引设计成(elder_id, record_time)实测万级数据量下查询耗时稳定在十几毫秒内。3.3 数据库初始化SQL片段给出一段建表参考注意一下字段注释规范这个习惯在多人协作时代价很大CREATE TABLE elder_info ( id bigint NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 老人姓名, gender tinyint NOT NULL DEFAULT 0 COMMENT 性别 0-未知 1-男 2-女, birth_date date DEFAULT NULL COMMENT 出生日期, room_no varchar(20) DEFAULT NULL COMMENT 床位/房间号, health_status varchar(100) DEFAULT NULL COMMENT 健康简要状态, deleted tinyint NOT NULL DEFAULT 0 COMMENT 逻辑删除标记, create_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_room (room_no) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人信息档案表;这里有个实践经验所有业务表都加逻辑删除标记字段养老平台的数据涉及服务记录和责任追溯物理删除风险太高出问题说不清楚。4. 核心功能实现从前端页面到后端接口4.1 老人健康档案模块的具体实现健康档案模块是养老平台最基础的数据底盘。后端我用一个Controller提供档案的增删改查和详情查询接口Service层负责业务校验Mapper层通过MyBatis操作数据库。Controller层的书写方式我习惯把参数校验抛给注解让代码保持简洁RestController RequestMapping(/api/elder) public class ElderInfoController { Resource private ElderInfoService elderInfoService; PostMapping public ResultVoid addElder(RequestBody Valid ElderInfoDTO dto) { elderInfoService.addElder(dto); return Result.success(); } GetMapping(/{id}) public ResultElderDetailVO detail(PathVariable Long id) { return Result.success(elderInfoService.getDetail(id)); } }前端对应页面用Vue3的组合式API组织列表加载和搜索逻辑聚合在一个方法块里。Element Plus的表格组件绑定数据、分页组件触发查询这套模式在后台管理系统里非常成熟。需要注意的是表单提交前的校验一定要做双向前端Element Plus做提示友好后端Valid做数据兜底两者缺一不可。4.2 工单流转状态机的设计细节工单是平台里业务状态最复杂的模块我用状态机的方式来管理避免状态散落各处导致逻辑混乱。工单状态定义如下PENDING待接单ACCEPTED已接单EXECUTING执行中FINISHED已完成CANCELLED已取消EXCEPTION异常挂起状态流转规则待接单只能转到已接单或已取消已接单转到执行中执行中转到已完成或异常异常可以重新回到执行中。这个规则直接体现在Service层的校验方法里避免在Controller里写一堆if-else。接口设计方面我额外做了“自动派单”逻辑。当管理员创建定期服务计划后系统根据排班表自动把工单分配到值班护理员的任务池里护理员登录后看到今日待办。整个过程用了Spring的Scheduled定时任务配合Quartz做复杂调度也行但考虑到当前业务量自带的调度完全够用。收到报警后自动创建异常工单这一块是最容易出问题的。健康数据监测接口发现指标超限会同时做两件事写入告警记录并创建一条EXCEPTION类型的工单推送给值班人员。这里必须处理好事务边界我用了Spring的Transactional确保两者要么都成功要么都回滚避免出现告警存在但工单没生成的情况。4.3 数据看板的SQL聚合实践运营看板要展示几个核心指标今日工单总量、完成率、异常告警数、护理员服务排行。这些指标如果一个个查再在内存里汇总代码会又慢又丑。更好的做法是把统计逻辑下沉到SQL里用一条聚合查询拿到结果。SELECT DATE_FORMAT(create_time, %Y-%m-%d) AS day, COUNT(*) AS total_orders, SUM(CASE WHEN status FINISHED THEN 1 ELSE 0 END) AS finished_orders, SUM(CASE WHEN status EXCEPTION THEN 1 ELSE 0 END) AS exception_orders FROM service_order WHERE create_time gt; #{startTime} GROUP BY DATE_FORMAT(create_time, %Y-%m-%d) ORDER BY day DESC聚合查询对索引的要求不一样这类统计SQL即使走不上索引在数据量可控的范围内也还好。但如果真实业务数据量增长到百万级就要考虑建汇总表或引入定时任务预聚合这个点可以在系统演进过程中持续优化。5. 前后端联调与部署实战5.1 本地开发环境搭建开发环境我按后端一套、前端一套、MySQL一套的模式搭建。后端需要JDK 8、Maven 3.6、SpringBoot工程导入IDEA前端需要Node.js 16、npm或pnpm、Vite脚手架数据库用MySQL 8.0Windows上安装时注意选UTF-8字符集Linux发行版可以直接用rpm包安装也可以走docker-compose更方便。MySQL安装有几个高频问题值得提前留意。Windows环境下容易遇到服务启动失败多半是端口被占用或者my.ini配置里的basedir路径写错。Linux用rpm装的话要记得按顺序安装mysql-community-common、libs、client、server这几个包依赖顺序反了会提示缺少组件。还有MySQL 8.0默认的认证插件和旧版驱动不兼容连接报SSL错误时检查一下JDBC连接串把sslMode参数显式设置一下。后端配置文件application.yml里的数据库连接部分建议把连接参数写成环境变量可替换的形式spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/elder_care?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai username: ${DB_USERNAME:root} password: ${DB_PASSWORD:root} mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.eldercare.entity configuration: map-underscore-to-camel-case: true5.2 接口联调的关键细节前后端分离项目里联调是最容易翻车的地方。首要问题是跨域开发环境下Vite默认端口5173后端8080两者不同源必须处理。我在后端加了一个全局CORS配置类允许本地开发域名访问生产环境因为前端静态文件由Nginx托管反向代理到后端接口反而没有跨域问题。联调时接口返回结构必须统一。我定义了一个Result类所有接口都返回{ code, message, data }的结构前端Axios响应拦截器统一处理code为200的情况非200直接弹错误提示。这样前端不用每个接口单独处理错误逻辑。登录鉴权用的JWT方案前端把Token存在localStorage里Axios请求拦截器自动附加Authorization: Bearer {token}头。后端的拦截器校验Token有效性并解析出当前用户信息放进ThreadLocal方便Service层获取操作人。这套东西虽然基础但面试时候被问的概率极高值得自己动手写一遍。5.3 打包与部署流程后端部署走的是打包成jar 系统服务托管的路线。Maven执行mvn clean package -DskipTests打出一个可执行jar然后传到服务器上。我用systemd配置成服务开机自启、异常退出自动重启都省心了。下面这个unit文件可以直接用[Unit] DescriptionElder Care Platform Afternetwork.target [Service] Userappuser WorkingDirectory/opt/elder-care ExecStart/usr/bin/java -Xms512m -Xmx1g -jar /opt/elder-care/elder-care.jar Restartalways RestartSec10 [Install] WantedBymulti-user.target前端部署更简单执行npm run build生成dist目录然后把静态文件放到Nginx的html目录下。Nginx配置里把/api前缀的请求反向代理到SpringBoot服务前端路由用history模式时还要配置try_files回退到index.html。server { listen 80; server_name your-domain.com; root /var/www/elder-care; index index.html; location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }6. 实战排坑记录这些坑我帮你踩过了6.1 高频问题速查表现象原因解决办法前端请求接口一直404没有配Nginx反向代理或跨域未处理检查location /api配置开发环境检查CORS配置MyBatis查询返回null但SQL直接执行有结果实体类属性名和数据库字段驼峰映射没开启设置map-underscore-to-camel-case为true部署后界面正常但登录后刷新失效路由模式与Nginx配置不匹配history模式必须配try_files中文乱码数据库字符集或连接串问题库表统一utf8mb4连接串characterEncodingutf8MySQL启动失败端口冲突或路径配置问题检查3306占用检查my.ini路径日期格式显示不对时区问题连接串serverTimezoneAsia/Shanghai6.2 三个值得单独说的细节SpringBoot版本别盲目追新。我遇到过用SpringBoot 3.x搭项目结果部分老版本MyBatis增强插件和javax命名空间不兼容接口起动直接报错。如果团队对最新特性没有硬性需求SpringBoot 2.7.x搭配JDK 8是最稳妥的组合。等到需要升级时优先验证持久层框架的兼容性再动手。数据库排序的坑。默认排序规则在utf8mb4字符集下对中文排序可能不符合预期。需要按中文拼音排的话可以在字段定义时指定排序规则或者用ORDER BY CONVERT(name USING gbk)实现。别小看这个细节老人名册按拼音排序是家属端很常见的需求。体检数据实时推送的防抖处理。对接智能设备上报健康数据时接口可能短时间内收到大量相同指标的重复数据。我加了一个同老人同类型记录5分钟内只落库一条的防重逻辑用Redis做窗口计数器简单有效。如果不想引入Redis也可以在应用内存里加个带过期时间的Set单机场景完全够用。做这个项目的整个过程中我最深刻的体会是再好的技术选型也需要为业务逻辑服务。养老智慧服务平台的技术栈非常主流但真正决定系统价值的是工单闭环、告警联动、健康数据管理这些业务设计想得有多细。框架和工具解决的是怎么写代码的问题而系统能否真正在养老机构落地靠的是代码在解决什么问题的思考深度。最后再分享一个小技巧开发这种前后端分离项目时一定要把接口文档当一等公民对待。我用的是Apifox写完一个接口马上自动生成文档前端同事同步就能看到字段定义和示例。这个习惯省下来的沟通成本做一个项目下来比想象中多得多。