SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0社区养老服务平台开发实战 做这套系统之前我先在某社区服务中心的办公室待了一个下午看他们平时怎么运转。三台电脑一边开着Excel管理老人档案一边在微信群里排服务工单健康数据散落在纸质台账和手机照片里。需求其实说得很直白能不能做成一个Web平台把老人档案、服务预约、工单派发、健康预警这几件事串起来让管理员、家属和服务人员各看各的界面各干各的活。从第一版原型到现在这套SpringBoot2Vue3MyBatis-PlusMySQL8.0的社区养老服务平台系统中间反复推翻重来了三轮。源码和文档最后一起归档标题里特意写了“含文档”因为早年交付过太多“能跑但没人敢接手”的项目这次从数据库设计文档到部署手册全部补齐。这篇文章我会把这个系统从选型、建模、后端链路、前端大屏到部署上线的完整过程拆开来讲适合正在做Java Web管理系统的同学参考也适合手头有中小型项目、想系统梳理技术栈的朋友对照着看。1. 先看需求再谈技术为什么社区养老平台会用这套组合很多人在拿到这类项目时第一反应是“直接开写”但我建议先花点时间把业务角色理清楚。社区养老服务平台的用户不是单一群体而是好几拨人同时用老人本人很少真正登录系统大部分操作发生在家属端——代老人预约服务、查看健康数据社区管理员负责审核档案、派单、处理预警服务人员通过移动端或电脑端接单、上门、上报完成平台运营则看全局数据。一个看似简单的管理系统背后是至少四种身份、五六个核心流程的组合。理完角色再回来看技术选型就清楚多了。这类系统的本质是强业务、弱性能的管理类应用单社区的数据量撑死几千老人、每日几百条工单根本不需要分布式那套东西。我看过有同学为了练手一上来就拆微服务网关、注册中心、配置中心全上最后业务没做透光运维就把他劝退了。单体应用在这里不是妥协是合理的边界。1.1 技术选型的三个关键决策第一个决策是后端框架。我选的是Spring Boot 2.7.x而不是Spring Boot 3。Spring Boot 3虽然已经出了很久但强制JDK17起步部分自动化部署脚本和老的监控方案不兼容而Spring Boot 2.7配合JDK8无论是本地开发还是服务器部署都极其省心。尤其对这类中小型项目稳定压倒一切。第二个决策是ORM框架。MyBatis-Plus几乎是这类系统的默认答案。社区养老平台有大量“按条件查列表”的场景——按老人姓名模糊搜索、按慢病标签筛选、按时间段查工单状态MyBatis-Plus的LambdaQueryWrapper写起来是真的很顺条件拼接不需要拼SQL字符串。复杂报表我再单独写XML比如月度服务统计、健康预警趋势这种多表聚合SQL手写SQL反而能精确控制执行计划。JPA我也用过但复杂查询的调优门槛偏高交给团队里水平参差不齐的人维护时容易写出让人看不懂的“魔法”。第三个决策是前端框架。Vue3 Element Plus的组合在Vue生态里现在是绝对主流。Vue3的Composition API在组织大屏页面和复杂表单时逻辑复用比Vue2的Options API舒服太多。Element Plus基本覆盖中后台90%的组件需求表格、表单、弹窗、选项卡开箱即用。1.2 版本细节与基础环境说明后端我用JDK8 Spring Boot 2.7.18 MyBatis-Plus 3.5.x数据库选MySQL 8.0。MySQL 8.0在这里不只是“新版”这么简单窗口函数做月度统计、JSON字段存服务偏好、通用表表达式处理层级菜单都是实打实能简化代码的特性。Redis做验证码缓存、工单抢单的分布式锁和热点数据缓存这个后面细讲。前端使用Vite Vue3.2 TypeScript Pinia Element Plus ECharts。TypeScript前期会拖慢一点进度但项目进入维护期后收益非常大尤其是大屏页面里图表配置对象复杂没有类型提示真的很折磨。提示技术选型最忌讳“为了用而用”。如果你的部署环境只有一台2核4G的小服务器微服务、容器编排都是负担。把核心业务做好选你团队最熟的技术栈就是最优解。2. 数据库建模把“服务”这件事拆成状态机数据库设计决定这个项目后面开发顺不顺。社区养老平台的核心不是“老人档案表有多详细”而是服务工单和健康预警这两条业务线的状态流转要严谨。我重构时把整个模型划分成了五个模块基础档案、服务业务、健康管理、活动管理、系统权限。2.1 核心表结构与业务关系基础档案模块以老人表为核心字段大概有姓名、性别、出生日期、身份证号、联系方式、紧急联系人、居住地址、慢病标签、护理等级、服务状态。慢病标签我用了一个独立的字符串字段存逗号分隔的短语比如“高血压,糖尿病”查询时配合LIKE即可不需要建关联表。家属账号表通过老人ID关联老人一个老人可以绑定多个家属账号这样就解决了子女共同关注父母健康的问题。服务业务模块的核心是服务工单表。服务项目表先定义好平台能提供什么——助餐、助洁、助医、陪诊、理发、康复训练每项有名称、价格、时长、适用对象。服务工单表则是业务的中枢记录老人ID、服务项目ID、预约时间段、服务地址、创建人、派单状态、服务人员ID、完成时间、回访记录。工单明细可以做也可以不做如果一次预约包含多个服务项建议拆明细表否则直接放在工单主表即可。健康管理模块有健康数据表和健康预警表。健康数据表存老人每次测量的血压、血糖、心率、体温、体重测量方式可能是家属录入、服务人员上门测量或设备同步。健康预警表则是由数据和规则引擎计算出的结果包含预警类型、预警级别、处理状态、推送状态。代码里我用了简单的阈值规则写在配置里而不是数据库因为规则变化不频繁写死更直观。系统权限模块就是经典的RBAC用户表、角色表、菜单表、用户角色关联表、角色菜单关联表后面第三章会展开。2.2 工单状态流转设计“服务”在业务上是连续动作家属下单、管理员派单、服务人员接单、上门执行、完成确认、回访归档。每个动作之间是有顺序的直接用一个status字段随手UPDATE容易出乱子。我设计了一套状态机合法流转路径如下从待接单可以到已接单或已取消从已接单可以到服务中或已取消服务人员临时无法上门从服务中只能到已完成从已完成必须到已回访最后归档。这样设计的价值在于任何角色都不能跨状态乱改。比如家属不能把“服务中”的工单直接改成“已完成”管理员也不能跳过已回访直接归档。代码里我用状态常量类和一张状态流转Map组织转移规则public static final MapInteger, SetInteger VALID_TRANSITIONS new HashMap(); static { VALID_TRANSITIONS.put(Status.PENDING, new HashSet(Arrays.asList( Status.ACCEPTED, Status.CANCELLED))); VALID_TRANSITIONS.put(Status.ACCEPTED, new HashSet(Arrays.asList( Status.IN_PROGRESS, Status.CANCELLED))); VALID_TRANSITIONS.put(Status.IN_PROGRESS, new HashSet(Collections.singletonList( Status.COMPLETED))); VALID_TRANSITIONS.put(Status.COMPLETED, new HashSet(Collections.singletonList( Status.VISITED))); }每次更新状态前先校验源状态和目标状态的组合是否合法不合法直接抛业务异常。这个方法帮我拦下来一大堆“不可能发生”的脏数据。2.3 JSON扩展字段与健康预警落库思路老年人档案有个特点个性化信息多每家的情况都不一样。有人需要备注“必须把药掰开服用”有人要写“家中二楼需要搀扶”有人要记录“老人有轻微认知障碍上门需耐心引导”。为这些信息单独建子表就太重了我在老人表加了一个extra_infoJSON字段统一存这些非结构化的备注create table elder_info ( id bigint primary key auto_increment, name varchar(50) not null, id_card varchar(18), phone varchar(20), emergency_contact varchar(20), emergency_phone varchar(20), address varchar(200), diseases varchar(200), service_status tinyint default 1, extra_info json, create_time datetime default current_timestamp, update_time datetime default current_timestamp on update current_timestamp )健康预警的落库逻辑也不复杂。保存健康数据时程序里判断一条规则血压高于140/90或低于90/60、血糖高于7.0、心率快于100或慢于60命中任意一条就生成预警记录同时立即推送给绑定的家属账号。规则引擎对这类项目是过度设计写在Service层足够清晰也能保证实时性。3. 后端核心链路权限、工单抢单与实时告警系统跑通业务后后端真正值得讲的是三个点权限控制怎么做、工单抢单冲突怎么避免、紧急呼叫怎么实时推送到管理员端。3.1 接口鉴权方案与角色权限模型权限这块我用的是经典的RBAC模型加JWT Token。用户登录成功后后端签发一个有效期为24小时的JWT其中只放用户ID和角色编码不塞权限列表。后续每次请求通过拦截器解析Token拿到用户ID后从缓存加载权限集合避免频繁查库。系统角色有五个平台运营admin、社区管理员community_admin、服务人员staff、家属family、老人elder。菜单表里存的是页面路由和权限标识例如order:create、order:audit、health:view。后端接口的PreAuthorize注解里直接写权限标识前端按钮用自定义指令控制显隐两边共用一套字符串标识不容易错位。这是一个很关键的设计细节菜单表和权限标识分离。菜单表决定用户能看到哪些页面权限标识决定用户在页面上能做什么操作。某个管理员可能能看到“工单管理”菜单但只能审核、不能创建那就是菜单有、按钮权限没有的效果。3.2 服务工单抢单的并发控制社区的通知模式是新工单生成后服务人员的手机会弹出列表谁先抢到算谁的。如果实现方式是先SELECT查一下状态再UPDATE修改状态多个服务人员同时操作时必然出现重复抢单。解决思路是用一条原子SQL把“检查和更新”合并。抢单操作的核心语句如下update service_order set status 2, -- 已接单 staff_id #{staffId}, accept_time now() where id #{orderId} and status 1 -- 待接单这样只有当这条SQL影响行数为1时抢单才成功返回0就说明被别人抢先了。不需要分布式锁也不需要事务里加额外同步。这类系统并发量不高但这条SQL能挡住绝大多数误操作实惠有效。3.3 紧急呼叫的实时推送不只是WebSocket老人端有一键呼叫按钮点击后要求管理员端和服务人员端在1秒内弹出告警卡片。实现上我用了WebSocket 心跳 消息回执的组合方案。WebSocket握手阶段校验Token不合法直接拒绝连接。连接建立后客户端每30秒发送一次心跳包服务端超过60秒没收到心跳就主动断开这样能及时发现异常掉线。后端收到紧急呼叫事件后向所有在线管理员和服务人员推送告警消息消息体包含老人ID、姓名、地址、历史预警摘要、GPS坐标。消息可靠性上客户端收到推送后必须回复ack服务端维护一个待确认消息列表超过3秒没收到ack就重发一次。这套机制虽然没有引入MQ那么复杂但保证了“管理员打开着网站但网页最小化”时也不会漏掉紧急事件。在Nginx层我额外配置了WebSocket的升级请求不超时否则默认60秒就会断开长连接。注意WebSocket的鉴权必须在握手阶段完成不能等连接建立后再做否则攻击者可以先建立连接再慢慢认证资源就白白被耗掉了。4. 前端Vue3落地中后台界面与数据看板前端看着只是“页面”但真正落地时会遇到权限路由、按钮级控制、大屏性能这几个坎。我按项目推进顺序来讲。4.1 工程初始化与目录规范前端用Vite创建Vue3 TypeScript工程目录按业务划分src/ ├── api/ # 接口请求定义 ├── assets/ ├── components/ # 通用组件 ├── layouts/ # 后台布局 ├── router/ # 路由定义 ├── stores/ # Pinia状态 ├── views/ # 页面视图 │ ├── dashboard/ # 数据看板 │ ├── elder/ # 老人管理 │ ├── order/ # 工单管理 │ ├── health/ # 健康管理 │ └── system/ # 系统管理 └── utils/Axios封装统一放在utils/request.ts里请求拦截器自动注入Token响应拦截器统一处理错误码401跳登录页、500弹错误提示、业务码非0则使用后端的message提示。封装的目的不是“少写几行代码”而是让所有页面开发者都用同一套错误处理逻辑避免各写各的。4.2 动态路由与按钮级权限控制登录成功后前端请求后端拿到当前用户的菜单树和权限标识集合。菜单树动态注册到Vue Router里侧边栏根据菜单树渲染。这里有个常见的坑刷新浏览器时存储的菜单状态会丢失所以路由守卫里必须判断“是否已经注册过动态路由”没有注册就重新请求、重新注册否则刷新后就白屏。按钮级权限我用了一个自定义指令v-permissionel-button v-permissionorder:create新建工单/el-button指令内部判断用户权限集合不包含指定标识就直接把DOM移除。这比在每个页面里写v-if更整洁权限变更时也不用到处找判断逻辑。4.3 数据看板的ECharts实践数据看板是这个项目的门面社区工作人员每天一上班就看它。页面核心指标包括老人总数、今日工单量、服务完成率、待处理预警数、服务工单趋势、老人年龄分布、预警类型统计。ECharts我按需引入而不是全量引入能显著减小打包体积。图表配置我会抽成一个useChart组合式函数统一处理初始化、数据更新、窗口变化自适应和组件卸载时销毁避免内存泄漏。刷新策略上看板的轮询间隔设为10秒但轮询到的新数据只更新对应图表的option绝不重置整个组件。最初版本我图省事直接刷新整个看板结果每次刷新图表都会闪烁一下老爷子们盯着大屏看数据时观感很差。改成增量更新后图表无缝切换CPU占用还降了三分之一。表格数据多的时候比如工单列表上千条我直接用虚拟滚动一次只渲染可视区域的几十行页面滚动流畅度提升非常明显。Element Plus的表格在数据量大时会有性能瓶颈别硬扛该上虚拟滚动就上。5. 从开发机到服务器部署链路里的关键细节能够在本机跑通只是第一步真正让项目可用的是部署链路。我在这块踩过的坑不少单独拿出来讲讲。5.1 MySQL 8.0容易踩的三个坑第一个坑是驱动版本。用旧的mysql-connector-java 5.x连MySQL 8.0会直接报认证协议不兼容换成com.mysql.cj.jdbc.Driver和8.0.33版本的驱动就正常了。第二个坑是时区。MySQL 8.0的默认时区经常和服务器不一致连接串里必须显式声明jdbc:mysql://localhost:3306/community_elder?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai不写serverTimezone跑起来全是8小时偏差查“今日工单”永远对不上数。第三个坑是Docker化部署时忘了挂数据卷重装容器数据全没了。我现在的Compose文件里MySQL目录挂载到宿主机的/data/mysqlRedis数据挂到/data/redis这是底线。5.2 Nginx站点配置与服务转发前端是纯静态资源跑在Nginx上后端是Spring Boot应用地址在后面服务。Nginx负责两件事托管前端静态文件、把/api开头的请求转发给后端服务。Vue Router用了history模式刷新子页面时如果Nginx没有把404重写到index.html用户一刷新就看到白屏。这个配置每次都要检查location / { root /opt/community-front/dist; index index.html; try_files $uri $uri/ /index.html; }/api接口的转发要顺带处理一下请求体大小限制健康报告图片上传可能超过默认的1M我在location块里加了client_max_body_size 20m;。另外WebSocket升级请求不能走普通的proxy_read_timeout默认值我设置了60秒无心跳不超时具体数值和执行效果在3.3节已经验证过。5.3 Docker Compose编排与资源限制最终部署环境是一台2核4G的云服务器我用Docker Compose把四个服务串起来前端Nginx、后端Java应用、MySQL、Redis。version: 3.8 services: mysql: image: mysql:8.0.29 command: --default-authentication-pluginmysql_native_password environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_DATABASE: community_elder volumes: - /data/mysql:/var/lib/mysql ports: - 3306:3306 redis: image: redis:7.0 volumes: - /data/redis:/data ports: - 6379:6379 backend: image: openjdk:8-jdk-slim working_dir: /app volumes: - /opt/community-backend.jar:/app/app.jar environment: SPRING_PROFILES_ACTIVE: prod command: java -jar -Xms256m -Xmx512m app.jar ports: - 8080:8080 depends_on: - mysql - redis frontend: image: nginx:1.24-alpine volumes: - /opt/community-front/dist:/usr/share/nginx/html:ro - /opt/nginx.conf:/etc/nginx/conf.d/default.conf:ro ports: - 80:80 depends_on: - backend一定给每个容器加上资源限制不加限制的话MySQL和Java在低配服务器上可能抢内存抢到OOM。后端JVM参数我设置了-Xms256m -Xmx512m连接池HikariCP的maximum-pool-size设为20。这里有个反常识的教训连接池不是越大越好之前我把pool设成200数据库直接被几百个连接打垮。小项目20个连接绰绰有余。5.4 上线后的慢SQL排查部署完不等于结束我习惯把MySQL慢查询日志打开阈值设为1秒。上线一周后我发现“健康预警统计”报表接口响应了2秒多顺着慢查询日志找到一条SQL按预警时间汇总时日期字段用了函数包裹导致索引失效。-- 最初的写法函数导致索引失效 select date_format(alert_time, %Y-%m-%d), count(*) from health_alert where alert_time date_sub(now(), interval 30 day) group by date_format(alert_time, %Y-%m-%d);改成在聚合前先用索引过滤时间范围统计字段用原始时间配合月度索引看板秒开。这类问题在开发环境基本发现不了因为本地数据量小一定得在接近真实数据量的环境里压一遍。6. 文档体系这个项目为什么敢在标题里写“含文档”很多开源项目源码放出来就能跑但接手维护的人往往在第一个下午就卡在“不知道怎么启动”“数据库密码改哪里”“证书过期了怎么办”。标题里“含文档”这几个字意味着这套项目不光能跑还能被独立接手。6.1 文档目录结构我按这套体系整理的文档docs/ ├── README.md # 项目简介、技术栈、功能清单、快速启动 ├── database-design.md # 数据库设计说明 ├── api.md # 接口文档含请求/响应示例 ├── deploy.md # 部署手册 └── user-guide.md # 管理员操作手册另外还有两个SQL文件init.sql表结构初始化脚本、demo_data.sql演示数据脚本。没有演示数据的系统新人接手后连登录都不知道用什么账号。6.2 数据库文档与接口文档怎么写才有用数据库设计文档不是把建表SQL贴一遍就行。我每张表都写了三部分表功能说明、字段枚举值含义、状态流转说明。状态流转表尤其重要比如工单状态不是简单的“1待接单、2已接单”列一下就完事要说明每个状态能往哪些状态流转、由哪个角色触发、触发后执行什么动作。这份表既是开发实现状态机的依据也是业务人员理解流程的入口。接口文档我用Apifox维护定义好每个接口的请求参数、响应结构导出Markdown放进仓库。关键点是请求/响应示例一定是真实运行后复制出来的不能用脑子想的手工编写。我早期吃过亏手工写了一个“成功”响应示例等前后端联调时才发现字段名有一个S和s的差异白白排查了两小时。真实示例能直接拿去写联调mock效率高得多。6.3 部署文档的覆盖边界部署文档要回答“从一台干净服务器到系统跑起来”的所有问题包括服务器版本要求、环境变量清单、防火墙要开哪几个端口、证书文件放哪个目录、日志到哪里看、进程复活的命令是什么。我在文档里单独写了一节“常见故障速查”比如数据库密码改了之后要在哪里同步修改、Nginx配置改了之后怎么reload、Jar包启动失败先看最后200行日志。写这份文档时花了两个通宵但后来社区那边有个新人接手对着文档从零开始两个小时内就在本地把整套系统跑起来了。那一刻我就确定文档上花的时间比任何一次代码重构都值得。这套项目的技术栈没有任何花哨的地方Spring Boot、Vue3、MyBatis-Plus、MySQL都是当前Java Web开发里最稳的搭配但真正让它“可用”的是业务建模、权限边界、并发控制、部署细节和文档体系这一整套工程经验。如果你正在做类似的管理系统或者准备把某个项目整理成源码开源交付不妨按这个思路过一遍先把业务角色理清再把状态机设计好最后把部署和文档焊死项目就能经得起真实环境的检验。