
做养老公寓管理系统我一开始是真没想过会启动这个项目。直到有次帮一位做养老机构运营的朋友梳理业务流程发现他们还在用Excel登记老人信息、用微信群传体检报告、月底手工对账单光是错账和漏记就够财务加班好几天。后来正好要搞毕设级别的全栈项目我就索性把“夕阳红公寓管理系统”完整做了一遍后端用SpringBoot前端用Vue围绕老人档案、床位管理、健康记录、收费退费、家属沟通、护工排班这些真实场景从数据库设计一直写到部署上线。这篇文章不是课程式的“手把手教学”而是把我做这套系统的架构思路、核心实现和踩坑记录一次性倒出来看完你可以直接拿着思路去复现甚至二次开发成自己项目。如果你正准备用SpringBootVue做管理系统类的毕设或实际项目这篇尤其适合你。我会先讲清楚这套系统到底管什么事再把数据库怎么建、前后端关键代码怎么写、视频和文件附件怎么处理、部署时有哪些坑全部分享出来。1. 这套夕阳红公寓管理系统究竟在解决什么问题1.1 公寓运营的典型痛点养老公寓和普通酒店、长租公寓最大的区别在于住在这里的老人不只是有“房间”还有健康状态、医嘱、用药计划、家属探望记录、紧急联系人、护理等级等等。这些信息是动态的而且需要严格按照权限去访问——比如家属只能看到自己老人的情况护工只能看到自己负责楼层的老人。传统Excel管理模式有几个很致命的问题数据孤岛档案在行政手里健康记录在护士站手里收费在财务手里互相不同步。状态难追踪一个老人今天是自理、明天可能转介护床位的“空置/预留/占用/维修”状态基本靠脑子记。催费和退费混乱入住时交了押金中间加了护理项目月底统一算账人工核算极易出错。所以这套系统的定位就很明确它不是一个大而全的ERP而是围绕“老人从入住前咨询到入住后服务、最后退住结算”这条全生命周期把业务数据线上化、流程化。1.2 系统边界功能模块全景图我最终规划了下面这些核心功能模块每个模块对应独立的数据表和前后端页面模块核心功能核心数据老人档案入住登记、档案编辑、紧急联系人老人基本信息、家属信息健康管理体检记录、用药计划、健康评估体检指标、医嘱、每日用药房间管理楼栋楼层房间管理、床位状态房间类型、床位状态、床位价格入住管理入住申请、床位分配、退住办理入住单、退住单费用管理收费项目、账单生成、缴费记录、退费收费项、账单、流水护工管理护工信息、排班、护理任务护工表、排班表、护理记录家属端家属登录、查看老人动态、在线留言家属账号、留言系统管理用户、角色、权限、日志用户、角色、菜单、权限这套功能拆解下来后台管理端加家属端一共十来个菜单页面工作量适中非常适合作为SpringBootVue全栈练手项目。1.3 技术选型的取舍逻辑后端选了SpringBoot 2.7.x前端选了Vue 2配合Element UI。可能有朋友问现在Vue 3都普及了为什么还选Vue 2原因很简单稳定、资料多、生态成熟Element UI组件库对中后台系统的覆盖度极高老人档案表单、表格、弹窗这类场景几乎全是现成组件。如果你做毕设Vue 2 Element UI在答辩时也比较好讲——面试官和老师都熟这一套。SpringBoot这边我分了三层持久层用MyBatis-Plus因为它自带分页插件和逻辑删除省去很多基础CRUD代码。权限认证用JWT Spring Security状态无关适合前后端分离。存储用MySQL 8.0 MinIOMySQL存业务数据MinIO存体检报告PDF、老人照片、合同附件、监控视频切片。这套组合最大的优势是每个环节都有标准答案遇到问题一搜一大把解决方案。后面我会把关键部分的代码贴出来你照着写就能跑通。2. 数据模型设计先把老人、床位、账单这三条主线捋清楚我见过很多管理系统的数据库设计一上来就建三四十张表结果关联关系一团乱麻。我的习惯是抓“业务主线”老人、床位、账单所有表都围绕这三条主线延伸。2.1 老人档案与健康档案表设计核心表elder_info我这样设计CREATE TABLE elder_info ( id BIGINT NOT NULL AUTO_INCREMENT COMMENT 主键, elder_no VARCHAR(32) NOT NULL COMMENT 老人编号, name VARCHAR(50) NOT NULL COMMENT 姓名, gender TINYINT DEFAULT 0 COMMENT 性别 0未知 1男 2女, birth_date DATE DEFAULT NULL COMMENT 出生日期, id_card VARCHAR(18) DEFAULT NULL COMMENT 身份证号, phone VARCHAR(20) DEFAULT NULL COMMENT 本人手机, health_level TINYINT DEFAULT 1 COMMENT 护理等级 1自理 2半护 3全护, status TINYINT DEFAULT 0 COMMENT 状态 0在住 1临时外出 2退住, room_id BIGINT DEFAULT NULL COMMENT 当前房间ID, bed_id BIGINT DEFAULT NULL COMMENT 当前床位ID, entry_date DATE DEFAULT NULL COMMENT 入住日期, leave_date DATE DEFAULT NULL COMMENT 退住日期, deleted TINYINT DEFAULT 0 COMMENT 逻辑删除, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_elder_no (elder_no), KEY idx_status (status), KEY idx_room (room_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人档案表;这里有两个容易被忽略的点用elder_no作为老人编号而不是直接用自增ID。因为老人编号要对外展示比如参观时家属看到“A-2024-001”会比“ID137”正规得多。deleted逻辑删除字段所有业务表都带。养老公寓数据有留存要求物理删除风险太大逻辑删除是一层保险。健康档案我拆成了health_check体检记录和medication_plan用药计划两张表。老人每次体检都生成一条主记录明细用JSON字段存指标项血压、血糖、心率等。为什么不建体检明细表因为体检机构给的报告字段不固定有三高的老人测的项目多普通老人就测几项用JSON反而能兼容各种格式。查询时大不了在Java里做一层转换。2.2 房间与床位状态流转床位表bed_info我加了一个bed_status字段用TINYINT表示0空置、1已入住、2预留、3维修、4停用。同时给房间表room_info加了room_type单人间/双人间/套房、floor楼层、rent_price床位费单价。这块最关键的是“状态流转”的逻辑当老人入住分配床位时同时更新bed_info.bed_status 1和elder_info.room_id、bed_id老人退住时要确认该床位没有欠费记录再还原为空置。为了防止两个人同时抢同一张床我在分配接口里加了乐观锁Update(UPDATE bed_info SET bed_status 1, version version 1 WHERE id #{bedId} AND bed_status 0 AND version #{version}) int lockBed(Param(bedId) Long bedId, Param(version) Integer version);更新行数为0说明床位已经被别人占了直接提示用户重新选择即可。这个方案比用分布式锁简单得多公寓规模完全够用。2.3 收费项目与账单生成规则费用模块常常被做成简单的“充值扣费”但公寓实际运营要复杂得多费用分为床位费、护理费、餐费、医疗费、临时服务费缴费周期有按月预缴、按天计费、按次收费。我的做法是两张表charge_item收费项目字典维护项目名称、单价、计费方式。bill账单表包含老人ID、项目ID、期间start_date和end_date、金额、状态。每次生成月度账单时先根据charge_item里配置的规则计算再插入bill表。这个逻辑最好放到后端Service层用定时任务每月1号自动给所有在住老人生成当月账单。定时任务我用的Spring自带Scheduled配了cronScheduled(cron 0 30 0 1 * ?) public void generateMonthlyBills() { // 查所有在住老人循环生成账单 }注意定时任务不要用单线程等上一个任务跑完再跑下一个否则月底数据多的时候容易积压。我在启动类加了EnableScheduling同时给定时任务方法加了一个Redis分布式锁防止多实例部署时重复生成账单。3. 后端SpringBoot实战认证、权限、文件上传与接口规范3.1 JWT认证与角色权限控制前后端分离项目登录认证首选JWT。我的设计是用户登录成功 → 后端生成JWT包含userId、roleId、expiresAt → 前端存在localStorage里 → axios请求头带上Authorization → Spring Security的过滤器解析并放行。角色权限我用了最简单的RBAC模型用户表、角色表、菜单表、角色菜单关联表、用户角色关联表。前端的菜单是根据后端返回的权限列表动态生成的对应我在后面第4节会讲动态路由。Spring Security配置里有一个关键点SecurityFilterChain直接定义哪些接口放行、哪些需要认证。我用了一段自定义的JwtAuthenticationFilter// 伪代码示意 public class JwtAuthenticationFilter extends OncePerRequestFilter { Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String header request.getHeader(Authorization); if (header ! null header.startsWith(Bearer )) { String token header.substring(7); // 解析JWT若正常则设置SecurityContext用户信息 } chain.doFilter(request, response); } }这里可以放心把接口按前缀区分权限比如/api/elder/**需要管理员权限/api/family/**只允许家属角色访问。Spring Security里通过antMatchers(/api/family/**).hasRole(FAMILY)这样的写法控制非常简单。3.2 使用MinIO管理合同、体检报告等附件养老公寓系统里的附件五花八门入住合同、身份证照片、体检报告PDF、监控视频片段、老人生活照片。最开始我把文件全存本地磁盘后来发现备份麻烦换成了MinIO。MinIO是兼容S3协议的轻量级对象存储一个Docker命令就能启动docker run -p 9000:9000 -p 9001:9001 \ --name minio \ -e MINIO_ROOT_USERadmin \ -e MINIO_ROOT_PASSWORDadmin123456 \ -v /data/minio:/data \ minio/minio server /data --console-address :9001然后SpringBoot集成MinIO Java SDK。我封装了一个StorageService核心方法只有三个上传、下载、获取预签名URL。上传时按“模块/日期/文件名”存储比如合同存到contract/2024/06/xxx.pdf体检报告存到health/2024/06/xxx.pdf这样在MinIO控制台里一眼就能找到归属模块。有一个坑特别提醒MinIO预签名URL默认有效时间7天而且生成的URL是内网IP如果前端和后端在不同网络要配置MinIO的MINIO_SERVER_URL为公网域名。不然前端拿到URL访问不了图片。3.3 统一返回结构与全局异常处理写接口时我不会让每个Controller各自返回不同的JSON结构那样前端对接会很痛苦。我定义了一个通用类Data public class ResultT { private Integer code; // 200成功500失败 private String message; private T data; public static T ResultT success(T data) { ... } public static T ResultT error(String message) { ... } }配合RestControllerAdvice做全局异常处理把业务异常、参数校验异常、兜底异常全部统一成Result结构返回。这样前端axios拦截器只需要判断code ! 200就弹错误提示逻辑非常干净。接口规范我建议遵循RESTful风格但不过度迎合。列表接口统一叫page比如/api/elder/page新增用POST /api/elder删除用DELETE /api/elder/{id}。分页参数默认pageNum和pageSize返回对象用MyBatis-Plus的IPageT转成统一结构。4. 前端Vue落地从路由守卫到老年人信息大屏4.1 动态路由与权限按钮控制前端我直接用了自己封装的一套脚手架Vue 2 Vue Router Vuex Axios Element UI。首页布局是典型的中后台侧边栏布局左侧菜单、顶部导航、内容区router-view。动态路由的逻辑是这样用户登录成功后后端返回该用户的角色权限信息前端通过router.addRoutes动态添加路由。比如管理员登录能看到“系统管理”菜单家属登录只能看到“老人动态”和“我的留言”。// 路由守卫伪代码 router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (!token to.path ! /login) { next({ path: /login }); } else { if (store.getters.menuList.length 0) { // 拉取用户信息、生成动态路由 generateRoutes().then(() next({ ...to, replace: true })); } else { next(); } } });这里有个注意点调addRoutes之后要next({ ...to, replace: true })否则刷新页面时路由还没来得及注册就会跳到404。这个坑我帮好几个人排查过多半就是这里没处理好。权限按钮的控制我用了一个自定义指令v-permission在按钮上写v-permissionelder:add指令内部检查用户权限集合没有就el.parentNode.removeChild(el)。4.2 核心组件老人档案卡片、床位图、收费报表前端的难点通常是“可视化表达”。老人档案列表我没有用普通表格而是做成了卡片流一张卡片显示老人照片头像、姓名、护理等级、房间号、紧急联系人点击卡片弹抽屉显示详情。Element UI的ElCard组件加ElAvatar头像就能搞定。床位图是房间管理页最实用的部分。我的做法是一栋楼用tab切换楼层每个房间内画矩形表示床位床位颜色随状态变化绿色空置、红色已入住、黄色预留、灰色维修。点击床位弹出操作菜单——如果是空置状态可选择“分配老人”如果已入住可选择“查看老人”“调换床位”。这个组件的难点不在渲染而在于“从后端拿数据到计算床位状态”。我的接口设计是一次性返回当前楼层所有房间和床位状态{ floor: 3, rooms: [ { roomId: 301, roomType: 双人间, beds: [ { bedId: 3011, status: 1, elderId: 12 }, { bedId: 3012, status: 0 } ] } ] }前端就用嵌套v-for循环渲染非常简单。收费报表我直接用了ECharts统计每月收费总额、欠费排行、入住率趋势。ECharts在Vue 2里用vue-echarts包一下就能在组件里使用。数据接口统一是聚合统计的比如GET /api/statistics/monthlyIncome?year2024返回12个月的数组。4.3 视频播放与文件预览的实用方案m3u8、pdf等公寓系统有时需要查看公共区域监控回放或老人生日视频这些视频往往用流媒体切片格式存储比如m3u8。纯前端播放m3u8不用引入复杂的流媒体服务我们用的是hls.js通过npm安装npm install hls.js --save然后在组件里这样播放import Hls from hls.js; playVideo(url) { if (Hls.isSupported()) { const video this.$refs.video; const hls new Hls(); hls.loadSource(url); hls.attachMedia(video); } }这个方法的好处是免安装任何播放器插件浏览器原生video标签就能播。视频文件放在MinIO里后端提供一个接口返回m3u8的URL前端直接传进hls.js即可。PDF预览我推荐用vue-pdf或者直接浏览器嵌入iframe但要注意如果PDF是后端接口返回的二进制流前端需要转成Blob对象再用iframe显示。我封装了一个previewFile(fileId)方法用axios设置responseType: blob拿到Blob后URL.createObjectURL(blob)生成临时链接。5. 从开发到部署我踩过的坑和优化记录5.1 时间字段与前端时区不一致这个坑几乎每个项目都会遇到。MySQL的datetime类型存到Java的LocalDateTime返回给前端是带T的字符串比如2024-06-01T00:00:00Element UI的日期组件直接显示没问题但如果你用new Date(2024-06-01T00:00:00).getTime()计算在UTC8时区会得到2024-06-01 08:00:00。前端再格式化就会比实际时间多8小时。我的解决方案是全局统一后端在配置里设置JSON序列化格式为yyyy-MM-dd HH:mm:ss且统一用北京时间。具体做法是定义一个Jackson配置类Configuration public class JacksonConfig { Bean public Jackson2ObjectMapperBuilderCustomizer customizer() { return builder - { builder.serializers(new LocalDateTimeSerializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); builder.deserializers(new LocalDateTimeDeserializer( DateTimeFormatter.ofPattern(yyyy-MM-dd HH:mm:ss))); }; } }前端所有日期选择器也用value-formatyyyy-MM-dd HH:mm:ss确保传往后端的是格式化字符串。这样全链路字符串传输省掉时区转换的心智负担。5.2 Vue项目打包后刷新404的解决Vue使用的是history路由模式开发环境一切正常部署到Nginx后用户访问首页没问题但只要在子路由刷新比如/elder/listNginx就会报404因为Nginx找不到对应的文件。解决办法是Nginx配置一个try_fileslocation / { try_files $uri $uri/ /index.html; }这个配置告诉Nginx如果请求的URI找不到真实文件就回退到index.html让Vue Router自己接管路由。如果没有这句线上系统直接报废刷新必404。还有一个相关坑Vue打包后静态资源的路径。如果部署在服务器根路径publicPath用默认/就行如果部署在子路径比如http://xxx.com/admin/必须在vue.config.js里设置module.exports { publicPath: process.env.NODE_ENV production ? /admin/ : /, // ... };否则JS、CSS文件路径全是绝对路径找不到资源页面白屏。5.3 数据库连接池与慢SQL的优化开发阶段数据量小随便怎么查都行。一旦入住老人超过500人、每天产生几千条护理记录分页查询就会变慢。我的优化经验分三步走第一步给常用查询字段加索引。比如elder_info.status、bill.elder_id、health_check.elder_id。用MySQL的EXPLAIN看一下type是否变成ref或range避免全表扫描。第二步MyBatis-Plus分页插件开启后分页查询会自动加LIMIT但多表关联的分页还是需要自己优化SQL。我习惯先查主表分页ID再WHERE id IN关联查询子表避免大偏移量的LIMIT 100000, 20。第三步连接池配置。SpringBoot默认的HikariCP性能已经很好了但我还是会调整几个参数spring: datasource: hikari: maximum-pool-size: 20 minimum-idle: 5 connection-timeout: 30000 idle-timeout: 600000如果生产环境数据库不在同一台机器记得加socketTimeout参数避免网络抖动时连接卡死导致接口超时。6. 免费分享清单与二次开发建议6.1 项目结构说明因为标题是“免费分享”我必须把分享的内容说清楚。目前我整理好的一套工程文件包含后端完整源码SpringBoot 2.7 MyBatis-Plus Spring Security MinIO前端完整源码Vue 2 Element UI ECharts hls.js数据库初始化SQL脚本含建库、建表、基础字典数据、管理员账号接口文档Markdown格式涵盖所有接口的请求参数和响应示例Nginx部署配置示例和Dockerfile整个项目结构是这样的sunset-apartment ├── backend │ ├── src/main/java │ ├── src/main/resources │ ├── pom.xml │ └── Dockerfile ├── frontend │ ├── src/api │ ├── src/router │ ├── src/store │ ├── src/views │ ├── src/permission.js │ └── vue.config.js ├── sql │ └── init.sql └── README.md拿到之后按README一步步配置数据库、启动MinIO、改配置文件就能本地跑起来。6.2 后续可以扩展的方向做这套系统的过程中我积累了一些后续迭代的想法。如果你拿这套系统做二次开发可以考虑这几个方向接入物联网设备老人佩戴的跌倒检测手环、血压计数据自动同步到健康档案这是养老公寓最需要的智能化升级。家属移动端小程序现在家属端是Web页面换成微信小程序后家属使用门槛更低推送护理记录、探访提醒也更方便。智能排班算法护工排班目前是手动分配后续可以基于护理等级和护工工作量做自动排班优化。语音播报与呼叫系统老人呼叫护工时前端联动对接智能音箱或走廊显示屏提升响应速度。这些方向其实每一块都能单独写一篇文章如果你感兴趣在评论区告诉我我会把踩坑过程继续整理出来。最后再分享一个我个人的体会做管理系统一定不要一上来就抄代码先把业务流程弄明白。你跟着我这套项目的表结构和接口设计走一遍其实就能发现SpringBootVue本身不复杂复杂的是业务状态的变化和人与人之间的权限边界。把这些想通了哪怕换个“校园宿舍管理系统”“医院病房管理系统”你也能很快地套用这套骨架做出来。老规矩需要完整源码和SQL脚本的直接私信我就行免费拿走不搞付费那一套。