资讯详情 基于SpringBoot+Vue的养老公寓管理系统:核心模块与部署实践
📅 2026/10/22 14:32:30
做养老公寓管理系统和你平时做的那些通用后台真不太一样。拆这套SpringBootVueMyBatisMySQL完整源码之前我先给一个判断这类项目真正的难点从来不在某个技术点而在于业务状态怎么建模、健康数据怎么串联、护理任务怎么闭环。这套企业级大健康养老公寓管理系统把老人档案、入住退住、床位分配、护理计划、健康体征、费用结算、后勤报修、经营报表全部串在一条线上适合正在做医疗健康类项目、想转行智慧养老方向、或者机构内部想用系统替代Excel表格的实施人员和开发者参考。我把它从部署到核心模块完整复现了一遍下面把设计思路、关键代码和踩过的坑都摊开讲。1. 项目整体设计与技术选型拆解1.1 为什么是SpringBootVueMyBatisMySQL这套组合选这套技术栈第一感觉是稳。养老公寓管理系统属于典型的传统企业级业务系统并发量不会爆炸但流程链路长、状态多、报表杂、权限细。SpringBoot负责快速搭建服务和统一配置Vue负责做前后端分离的界面MyBatis写SQL足够灵活MySQL在中小型机构的数据量下完全够用运维成本也低。这套组合在医疗、政务、传统行业里沉淀了很多年资料多招人也容易后续换人维护不会太痛苦。有人说既然要快为什么不直接上MyBatis-Plus甚至用JPA我的看法是这个项目的核心痛点是复杂条件查询、动态报表、多表联查。MyBatis手写SQL在这种场景下掌控力最强SQL性能出问题可以直接定位不会像JPA那样在N1和复杂统计上绕圈子。MyBatis-Plus确实能省掉单表CRUD的代码但如果源码把MP的BaseMapper写到底后续业务查询逻辑全拼LambdaQueryWrapperSQL的可读性和维护性反而差。所以这个项目选择原生MyBatis是符合企业级应用重业务、重维护特点的合理决定。前端选Vue不是因为React不好而是因为国内中小型后台管理系统的生态里Vue组件库的成熟度、中后台模板的丰富度确实最高。项目里大量列表、表单、弹窗、树形菜单用配套组件能在一两周内搭出完整管理台对于养老公寓这种预算有限、上线周期紧的项目这是实打实的优势。1.2 大健康养老场景到底大在哪里大健康三个字决定了这套系统不能只做老人登记床位管理收钱这类酒店式管理。真正的智慧养老机构里系统还要承担健康监测、慢病随访、护理执行、家属互动这些任务。它不像酒店管理系统那样入住即结束而更像预约挂号护理执行物资进销存的合体。具体来说大健康场景有几个特征数据从录入变成采集。老人生命体征可能来自智能手环、血压计、血糖仪系统需要定义好数据接入和处理路径。角色特别多。院长要看经营数据护士长要排班和分配任务护理员要接单和执行财务要出账家属要远程看老人的健康和费用情况。流程特别长。入院要评估、签约、分配房间、制定护理计划住进来之后每天有护理任务、健康记录退住还要结清费用、交接物品。流程任何一个环节断了线下运营就会乱。数据要能追溯。老人跌倒、异常体征、用药反馈这类事件系统里必须有留痕机制方便机构复盘和家长安心。我前阵子帮一家机构做调研发现他们还在用Excel加微信群管理老人信息护理员交接班靠嘴说。不是他们不想用系统而是市面上的产品要么太重要么根本不贴合机构流程。这套源码的价值正是把健康数据和照护流程这两条主线刻进了数据模型而不是只做一个表面光鲜的增删改查界面。1.3 总体模块划分与功能地图整个系统我建议按七个模块来理解每个模块对应机构里一个具体的业务职能模块核心功能涉及核心表系统管理用户、角色、权限、菜单、操作日志sys_user、sys_role、sys_menu入住管理咨询登记、入住评估、签约、退住办理elder、check_in_info、assessment床位管理楼栋/楼层/房间/床位的层次与状态流转building、room、bed护理管理护理等级、护理计划、任务生成与执行care_level、care_plan、care_task健康管理体征记录、体检、慢病随访、异常预警health_record、physical_exam费用管理按月度计费、收费、退款、对账charge_bill、charge_item、payment_log报表中心入住率、护理工作量、营收统计通过聚合SQL生成模块之间不是孤立的老人入住之后床位状态要变护理计划要生成费用账单要从入住日期开始计费老人体检查出异常系统要能生成预警并通知护士。理解了这条数据流后面看代码才会觉得顺。2. 核心功能模块与数据模型设计2.1 老人档案与健康数据怎么关联最合理老人档案表是整个系统的地基。字段量会很大我从这个项目中抽出几个关键点档案编号、姓名、性别、生日、身份证号、家属联系方式、健康等级、入住状态、评估状态。身份证号虽然正常都会存但我强烈建议在实体层做加密处理数据库里不要存明文页面展示要脱敏健康数据属于敏感数据这个底线必须守住。CREATE TABLE elder ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_no VARCHAR(32) NOT NULL COMMENT 档案编号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT COMMENT 1男 2女, birthday DATE, id_card VARCHAR(18), phone VARCHAR(20), emergency_contact VARCHAR(50), emergency_phone VARCHAR(20), health_level TINYINT COMMENT 1自理 2半自理 3全护理 4特护, assessment_status TINYINT COMMENT 评估状态, status TINYINT COMMENT 1待入住 2在住 3已退住, create_time DATETIME, update_time DATETIME ) COMMENT 老人档案;健康记录不要直接塞在档案表里。老人住一个月每天测两次血压血糖就是60条记录塞在同一张表会导致档案行爆炸查询也会变慢。正确做法是单独建一张高频写入的健康记录表通过elder_id关联CREATE TABLE health_record ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, record_date DATE NOT NULL, systolic INT COMMENT 收缩压, diastolic INT COMMENT 舒张压, heart_rate INT, blood_sugar DECIMAL(5,2), temperature DECIMAL(4,2), abnormal_flag TINYINT COMMENT 0正常 1异常, record_time DATETIME );这里有个设计心得体征数据表要保留异常标记字段。判断是否异常可以在后端做规则计算但标记一定要落库因为护理端需要按异常记录快速筛选报表端还要统计本月异常事件数到时候再临时算会很痛苦。2.2 入住退住与床位状态机床位管理如果不设计状态机很容易变成显示一张房间表这么简单。实际运营里一张床会有这些状态空置、预订、入住、维修、临时隔离。状态是会被流转的而且部分流转必须由前一个操作触发。正常流程是这样的家属来参观咨询系统里先建一条咨询记录老人确定入住做健康评估和护理等级评定评估通过后系统根据性别、护理等级、楼层偏好分配床位床位从空置变成预订老人正式办理入住床位变成入住老人出院/退住结清费用后床位回到空置如果床垫、护栏损坏床位要变成维修修好再转回空置。这套状态机落到代码里我的建议是不用复杂的状态机框架直接在service层写清楚状态流转合法性判断比如入住状态的老人在未退住前不允许把床位转成维修。逻辑直观新人接手也看得懂。多花一分钟设计状态后面省掉大量数据对不上的排查时间。2.3 护理任务闭环是系统的灵魂护理管理模块决定了这套系统是不是真的能落地。很多系统把护理做成一个记录表单护理员做完勾一下就算完事这其实是不够的。真正的闭环是护理主任为每位老人制定护理计划比如每天两次血压测量、三餐后服药提醒、定时翻身、康复训练系统按计划每天自动生成护理任务派发到对应护理员的待办池护理员领单、执行、上报完成遇到异常情况老人拒绝服药、体温偏高直接标记异常护士长看到异常任务进行复测和处理处理结果留痕。任务表的核心设计我摘出来你看看就明白为什么能闭环CREATE TABLE care_task ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, plan_id BIGINT COMMENT 护理计划ID, task_date DATE NOT NULL, task_type VARCHAR(32) COMMENT 翻身/喂药/体征测量/洗澡/康复训练, assignee_id BIGINT COMMENT 护理员ID, status TINYINT COMMENT 0待领取 1已领取 2执行中 3已完成 4异常 5已取消, start_time DATETIME, finish_time DATETIME, abnormal_remark VARCHAR(500), remark VARCHAR(500) );在做护理任务分页列表时一定要按status和时间段建好索引否则机构住了两三百位老人之后每天产生的任务量加查询会明显拖慢。还有一个实操经验任务生成用定时任务批量跑每天凌晨生成当天计划不要等护理员打卡后再生成否则早班护工根本来不及看。2.4 费用结算与账务完整性养老公寓收费和普通酒店不一样不是住一晚收一晚而是按月计费包含床位费、护理费、餐费、水电费、药品费等多个科目。系统需要支持月度账单自动生成退住时按天折算退款家属可以查到每一项明细。费用表设计有两个方向一张主账单表加一张明细表主表管汇总金额和支付状态子表管科目。我强烈建议不要只做一张费用流水表因为一个账单多次部分支付、冲正、退款单表会乱。CREATE TABLE charge_bill ( id BIGINT PRIMARY KEY AUTO_INCREMENT, elder_id BIGINT NOT NULL, bill_month VARCHAR(7) COMMENT 账单月份, total_amount DECIMAL(12,2), discount_amount DECIMAL(12,2), payable_amount DECIMAL(12,2), paid_amount DECIMAL(12,2), status TINYINT COMMENT 0未出账 1已出账 2部分支付 3已结清 4已退款, create_by BIGINT, create_time DATETIME ); CREATE TABLE charge_item ( id BIGINT PRIMARY KEY AUTO_INCREMENT, bill_id BIGINT NOT NULL, item_type VARCHAR(32) COMMENT 床位费/护理费/餐费, amount DECIMAL(12,2), start_date DATE, end_date DATE, remark VARCHAR(200) );用DECIMAL而不是DOUBLE存储金额这个属于财务常识了但每做一个新项目都会有人踩。DOUBLE有精度问题算完对不上账很麻烦。费用计算建议统一放到后端service层做前端只做展示避免不同浏览器算出来的结果不一致。3. 关键实现项目搭建与核心代码解析3.1 项目初始化与工程结构拿到这套源码先不要急着启动先看目录结构。后端是标准的Maven单模块工程但包结构分得很清楚我个人建议你保持这个分层别把代码全塞在controller里health-elder-system/ src/main/java/com/example/elder/ common/ -- 统一返回结果、全局异常、工具类 config/ -- 跨域、拦截器、WebMvc配置 controller/ -- 接口层 service/ -- 业务层 mapper/ -- MyBatis Mapper接口 entity/ -- 数据库实体 dto/ -- 入参/出参对象 src/main/resources/ mapper/ -- MyBatis XML application.yml frontend/ -- Vue工程为什么业务层不能省略养老公寓的业务规则分散在多个环节如果在controller里直接调用mapper退住的时候要先改床位状态、再结算费用、再更新档案状态事务边界根本控制不住。有service层事务注解才能覆盖一整条业务链后续也好加单元测试。3.2 数据库连接与核心配置启动之前重点看application.yml。这里有几个配置项简直是踩坑重灾区我直接给一份能稳定跑生产环境的配置server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/elder_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456 hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 servlet: multipart: max-file-size: 20MB max-request-size: 50MB mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.elder.entity configuration: map-underscore-to-camel-case: true jwt: secret: your-secret-key-change-me expire-hours: 24serverTimezoneAsia/Shanghai必须加上否则Java 8的LocalDateTime和MySQL的DATETIME会差8个小时老人体征记录时间全错位排查起来极其隐蔽。allowPublicKeyRetrievaltrue是MySQL 8.0连接时常见的报错来源连不上数据库先想到这个参数。map-underscore-to-camel-case: true是MyBatis的救命配置开启后数据库的create_time才能自动映射成实体里的createTime否则查出来全是null。连接池我建议用HikariCPSpringBoot默认就是它性能和稳定性都很好。如果是给机构内部用Druid的监控页面有一定价值但会增加配置复杂度看团队习惯。3.3 Mapper层的高频查询与动态SQL写法老人的列表查询是这个系统里最典型的需求按姓名模糊搜、按档案编号搜、按护理等级筛选、按入住状态筛选还得分页。用MyBatis的动态SQL一个标签就能解决查全部还是查某项的拼接问题代码可读性比在Java里拼SQL强太多。select idselectElderPage resultTypecom.example.elder.entity.Elder SELECT id, elder_no, name, gender, birthday, phone, health_level, status, create_time FROM elder where if testkeyword ! null and keyword ! AND (name LIKE CONCAT(%, #{keyword}, %) OR elder_no LIKE CONCAT(%, #{keyword}, %)) /if if testhealthLevel ! null AND health_level #{healthLevel} /if if teststatus ! null AND status #{status} /if /where ORDER BY id DESC /select注意LIKE别写成 %${keyword}%那会直接引入SQL注入风险用CONCAT拼参数就是安全写法。排序字段尽量用id或create_time用索引字段排序能避免大数据量下内存排序。健康数据批量插入也值得单独提。手环设备一旦接入每五分钟就会上报一批数据逐条insert基本不可用批量插入必须安排上insert idbatchInsertHealthRecord INSERT INTO health_record (elder_id, record_date, systolic, diastolic, heart_rate, blood_sugar, temperature, record_time) VALUES foreach collectionlist itemitem separator, (#{item.elderId}, #{item.recordDate}, #{item.systolic}, #{item.diastolic}, #{item.heartRate}, #{item.bloodSugar}, #{item.temperature}, NOW()) /foreach /insert实测下来一次插200条比循环200次单条插入快一个数量级。注意数据库的max_allowed_packet要调大一点默认值太小可能导致批量插入报错。3.4 Vue前端实现与接口联调要点前端部分先把axios封装搞好。我见过太多项目没做统一封装每个页面各写各的请求后端一改错误码格式前端就要改几十个文件。基础封装长这样import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { config.headers[Authorization] Bearer (localStorage.getItem(token) || ) return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { alert(res.msg) return Promise.reject(new Error(res.msg)) } return res.data }, error { if (error.response error.response.status 401) { location.href /login } return Promise.reject(error) } ) export default service统一处理token、统一处理业务错误码、遇到401自动跳登录页。这套逻辑几乎是中后台系统标配新建页面时直接import就好。老人列表页是最常见的页面形态用组件库的表格加分页组件。这里有个细节数据库里存的是状态码数字页面上要显示成在住/已退住这种中文别在模板里堆一堆三元表达式定义一个映射对象会干净很多。template div el-form inline el-input v-modelquery.keyword placeholder姓名/档案编号 clearable / el-select v-modelquery.healthLevel placeholder护理等级 clearable el-option label自理 :value1 / el-option label半自理 :value2 / el-option label全护理 :value3 / el-option label特护 :value4 / /el-select el-button typeprimary clickloadData查询/el-button /el-form el-table :datalist border stripe el-table-column propname label姓名 / el-table-column label护理等级 template #default{ row }{{ levelMap[row.healthLevel] }}/template /el-table-column el-table-column label状态 template #default{ row }{{ statusMap[row.status] }}/template /el-table-column el-table-column label操作 template #default{ row } el-button sizesmall typeprimary clickedit(row)编辑/el-button /template /el-table-column /el-table el-pagination v-model:current-pagequery.page v-model:page-sizequery.size :totaltotal layouttotal, prev, pager, next current-changeloadData / /div /template script setup import { reactive, ref, onMounted } from vue import { getElderPage } from /api/elder const levelMap { 1: 自理, 2: 半自理, 3: 全护理, 4: 特护 } const statusMap { 1: 待入住, 2: 在住, 3: 已退住 } const query reactive({ page: 1, size: 10, keyword: , healthLevel: null }) const list ref([]) const total ref(0) const loadData async () { const data await getElderPage(query) list.value data.records total.value data.total } onMounted(loadData) /script联调时最容易出问题的就是时间字段。后端返回LocalDateTime前端拿到的是2024-08-15T10:30:00这种带T的字符串直接展示很难看。前端用dayjs格式化或者后端在DTO里加JsonFormat注解两种方案选一种统一标准就行。我建议在前后端接口文档里直接约定好省得每个字段都加注解。3.5 JWT鉴权与权限控制系统里有院长、护士长、护理员、财务、家属多种角色权限控制绕不开。这个项目采用JWT方案登录成功生成token前端保存每次请求带上Authorization头后端用拦截器统一校验。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { if (!(handler instanceof HandlerMethod)) { return true; } String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } try { // 解析并校验token将用户信息放入ThreadLocal或RequestAttribute Claims claims JwtUtil.parseToken(token.replace(Bearer , )); request.setAttribute(userId, claims.get(userId)); return true; } catch (Exception e) { response.setStatus(401); return false; } } }对于养老公寓这种规模JWT加拦截器就够用了。不需要把Spring Security全家桶引进来配置复杂、学习成本高项目周期紧的话得不偿失。如果后面要对接第三方统一认证再考虑升级。4. 实际操作中的常见问题与排查技巧实录4.1 数据库连接和编码问题速查我把部署阶段最常见的数据库问题整理成了一张表照着排查基本能解决80%的启动失败现象原因解决方案连接报Public Key Retrieval is not allowedMySQL 8.0加密规则变化jdbcUrl加allowPublicKeyRetrievaltrue数据库中文乱码建库没用utf8mb4建库用utf8mb4连接串加characterEncodingutf8时间差8小时时区未指定连接串外加serverTimezoneAsia/Shanghai中文查询不到结果排序规则不是utf8mb4_unicode_ci表和字段统一utf8mb4_unicode_ci连接数耗尽连接池太小或连接泄漏调大maximum-pool-size排查未关闭的连接建库时直接这样写省得后面改CREATE DATABASE elder_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;4.2 MyBatis高频报错与SQL性能问题开发阶段最常见的报错就是Invalid bound statement (not found)。出现这个先查两处第一Mapper接口的namespace是否和XML文件里的namespace一致第二application.yml里的mapper-locations路径是不是把XML扫进去了。还有一个容易被忽略的点XML文件的SQL写错了但项目启动时不报错运行时才爆所以写完SQL一定要先跑一次测试方法别等到前端联调才发现。老人列表查询要特别注意N1问题。比如查询一批老人之后页面又要显示每位老人的最近一条健康记录如果循环里每条数据单独查一次两三百位老人就会产生几百条SQL。解决办法是先批量查询最近记录再用Map按elderId分组在Java内存里做关联SQL数量从N1降到2。4.3 跨域与前端接口联调问题前后端分离开发时跨域问题几乎必现。前端端口是5173后端是8080直接请求会被浏览器拦截。我的习惯是开发环境用Vite的proxy代理生产环境用Nginx反向代理都不需要后端开CORS。因为开CORS会有安全面扩大而且一旦前后端域名调整又要改后端。// vite.config.js export default { server: { proxy: { /api: { target: http://127.0.0.1:8080, changeOrigin: true } } } }生产环境Nginx配置后面会写。要注意的是如果接口请求方式用了DELETE或PUT有些旧版Nginx默认配置会拦截需要显式放行对应method或者统一改成POST。4.4 部署打包的几个经典坑前端用history路由模式打包后刷新页面会出现404。因为浏览器请求的是真实路径但服务器只有index.htmlNginx必须配置try_files回退。server { listen 80; server_name your-domain.com; root /opt/elder/frontend/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }注意location /api/的proxy_pass如果写成http://127.0.0.1:8080不带后面的/api/请求会被转发到http://127.0.0.1:8080/api/...因为原URI带了/api前缀Nginx会加上所以上面写法是标准的既保留了前缀又能正确转发。另一个坑是jar包运行后中文乱码启动命令加上-Dfile.encodingutf-8或者确保服务器locale是UTF-8。数据库备份也要提前安排crontab每天凌晨备份一次不然出问题连恢复的余地都没有。5. 部署上线与后续扩展5.1 一套能落地的生产部署方案小规模机构可以不用上Kubernetes直接用Docker Compose编排三个容器MySQL、后端、前端。下面这份配置是完整可跑的数据目录做了持久化重启不会丢数据version: 3 services: mysql: image: mysql:8.0 container_name: elder-mysql environment: MYSQL_ROOT_PASSWORD: changeit MYSQL_DATABASE: elder_db ports: - 3306:3306 volumes: - mysql-data:/var/lib/mysql command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci backend: image: elder-backend:latest container_name: elder-backend ports: - 8080:8080 environment: SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/elder_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai depends_on: - mysql frontend: image: elder-frontend:latest container_name: elder-frontend ports: - 80:80 depends_on: - backend volumes: mysql-data:生产环境要注意数据库密码不要在Compose文件里写死建议用环境变量或Docker secret。后端镜像构建时记得把日志目录挂载出来方便排查问题。前端镜像用Nginx作为基础镜像把dist目录拷进去再带上上面的Nginx配置。5.2 系统后续扩展的三个现实方向第一家属端小程序。老人家属最关心的是老人今天吃了什么、血压正不正常、最近账单花了多少钱。小程序端不需要做全功能只做档案查看、健康日报、费用查询、留言沟通就能显著提升机构服务口碑。后端接口按现有模块扩展即可重要的是把健康报告汇总接口做好。第二物联网设备接入。血压计、血糖仪、智能手环通过蓝牙或网关把数据传到后端设备数据的格式千奇百怪建议在健康管理模块前面加一层设备适配层统一转换成health_record的记录格式不要在每个设备厂家的接口里写业务。第三数据大屏。养老机构的院长办公室通常要放一块屏实时展示入住率、护理任务完成率、异常事件数量、当月营收。这些数据的聚合SQL在报表中心里已经有了前端可以用开源的大屏组件搭建接口复用现有报表模块。最后说点实际的个人体会做完这套系统我最大的感触是养老公寓系统真正考验人的不是SpringBoot、Vue或者MySQL的技术深度而是你有没有把业务想透。床位状态流转、护理任务闭环、费用按月折算这些环节在需求文档里只是一句话落地时全是细节。如果你准备拿这套源码去给机构做二次开发或自己学习我建议先花时间把数据库脚本完整看一遍搞懂每张表之间的关系再动代码。等业务模型在心里有了地图代码跑起来就是水到渠成的事。开发过程中多看日志、多用分页查询、多检查时区和编码比研究炫技写法有用得多。真正上线之后你会发现有太多计划外的事情要处理打印体检报告、生成出院小结、给家属群发通知、统计护理员月度绩效。这套系统的价值恰恰在于它把核心的数据链路和状态流转搭扎实了后续扩展功能时你会发现怎么加都不乱。这就是一个架构合理的企业级系统该有的样子。