每年到这个时候,总有人问我同一个问题:有没有完整能跑、带源码、拿过来就能改能讲的毕业设计项目?基于SpringBoot的养老院管理系统,就是这类问题里的高频答案。这套系统不是我拍的脑袋,而是在网上流传度很高的一套开源教学案例,覆盖了登录鉴权、老人档案、床位管理、健康记录、费用结算这些典型场景,技术栈还是Java面试里最容易聊到的那一套。今天这篇文章就把它从头到尾拆一遍,不灌水,直接讲清楚需求怎么来的、表怎么设计的、核心代码怎么写的、跑起来会遇到哪些坑,以及怎么把它改造成你自己的项目。适合正在选毕业设计题目、刚学完SpringBoot想做第一个完整项目、或者手里已经拿到源码但不知道怎么讲的人。
1. 这个系统到底在解决什么问题:从需求清单说起
1.1 为什么养老院场景适合做一个管理系统
很多同学挑毕业设计题目,习惯往"电商系统""图书管理系统"这些烂大街的方向跑,结果答辩时老师一听题目就没兴趣。养老院管理系统的好处在于,它的业务足够真实,而且痛点非常具体。
你去翻任何一家中小型养老院的日常,基本逃不开这几件事:老人信息靠纸质档案登记,家属来问病情要翻本子;床位有没有空余靠一张Excel表,甚至靠前台脑子记;护工每天记录的体温、血压写在纸片上,换班之后信息就断档了;月底算费用最头疼,床位费、护理费、餐饮费、医药费混在一起,算错一笔就要扯皮。
管理系统本质上就是把这一堆"台账+经验"变成"数据库+流程"。老人入住时填一张电子档案,分配床位后状态自动更新,健康数据每天录入可查,费用单到期自动生成。这些需求拆开看都不难,但合在一起就是一个非常标准的业务系统雏形:有实体关系、有状态流转、有角色权限、有数据统计。对教学项目来说,这个复杂度刚刚好,既不会像图书管理那样单薄,也不用像电商那样碰支付和库存这类重逻辑。
还有一点很实际:养老、医疗、社区服务这类选题本身就带着社会价值,答辩时你可以顺理成章地讲"系统能帮助养老院提升管理效率、降低信息差错",这种话术比"完善了系统的功能性和可用性"自然得多。
1.2 三种角色的权限边界与核心流程
这套系统的用户模型,我见过的大部分版本都围绕三种角色展开:管理员、护工、财务/前台。有的版本还会加一个家属端,但那属于后面扩展的事,先看基础三角色。
| 角色 | 核心菜单 | 关键操作 |
|---|---|---|
| 管理员 | 老人档案、床位管理、护工管理、费用管理、数据统计、系统管理 | 全部增删改查、分配床位、审核入住、查看统计报表 |
| 护工 | 我的老人、健康记录、每日护理提醒 | 查看分配给自己的老人列表,录入体温血压心率等体征,写护理备注 |
| 财务/前台 | 入住办理、退住办理、费用单管理、缴费记录 | 登记入住、生成费用单、确认收款、办理退住结算 |
权限控制不需要一上来就上Spring Security那套重武器。大多数版本用的是JWT + 拦截器,前端根据登录返回的角色类型动态渲染菜单,后端在拦截器里校验接口权限。这样的好处是代码量小,逻辑好讲,答辩时你能一句话说清楚"前端路由守卫控制页面可见性,后端拦截器控制接口访问权限,两层都过才算合法请求"。
核心业务流也很好梳理。入住流:登记老人信息 → 选择空闲床位 → 绑定家属联系方式 → 生成首月费用单。日常流:护工登录看到自己负责的老人 → 录入健康数据 → 管理员在统计页查看全院健康状况。退住流:财务结算未缴费用 → 释放床位 → 老人档案归档。这三个流程串起来,整个系统的数据库表结构基本就浮出水面了。
2. 技术栈选型:为什么这套组合适合毕设与案例学习
2.1 每个技术选型背后的取舍
先看一套典型配置,大部分流传版本都用这个组合:
| 技术组件 | 典型版本 | 选型理由 |
|---|---|---|
| Spring Boot | 2.7.x | 稳定,资料多,与MyBatis Plus兼容最好;3.x虽然新但很多老依赖要重新适配 |
| MyBatis Plus | 3.5.x | 单表CRUD不用写SQL,自带分页插件和逻辑删除,正好命中管理系统的CRUD主场景 |
| MySQL | 5.7 / 8.0 | 部署成本低,导入数据方便,面试官也熟悉 |
| 前端 | Vue 2 + Element UI | 后台管理界面组件齐全,表格、表单、弹窗、分页都是现成的 |
| 鉴权 | JWT + 拦截器 | 比Spring Security简单得多,答辩完全够用 |
| 构建工具 | Maven | 没啥好说的,Java标配 |
这里我特别想展开讲讲两个"为什么不"。
为什么不直接用Spring Security?因为这套系统根本没有复杂到需要过滤器链、方法级权限注解、CSRF防护这些东西。Spring Security配置起来一长串,很多同学照着抄都抄不明白,答辩时老师问一句"你这个过滤器链是怎么走的"就直接卡壳。JWT配合一个拦截器,二三十行代码就能实现同样的效果:登录成功签发token,拦截器解析token并放行白名单路径,逻辑直白,你能讲清楚,老师也能听懂。
为什么不用传统MyBatis写XML?管理类系统里百分之八九十的操作是单表增删改查,用MyBatis Plus的BaseMapper,一行Java代码都不用写就能拿到selectById、insert、updateById。真正需要手写SQL的,只有那些跨表统计查询,比如健康数据按天聚合、费用按月汇总,这些单独写在Mapper XML里维护就好。这种"简单操作框架化,复杂SQL显式化"的组合,才是最贴合项目体量的方案。
2.2 前后端分离的目录结构与一次完整请求流转
源码的项目结构通常长这样,你可以对照自己手里的包看看是不是同一套:
后端: src/main/java/com/xxx controller/ 接受请求,做参数校验 service/ 业务逻辑 mapper/ MyBatis Plus的Mapper接口 entity/ 数据表对应实体 config/ 拦截器、跨域、MyBatis Plus配置 common/ 统一返回结果、全局异常、工具类 src/main/resources application.yml mapper/*.xml 复杂SQL 前端: src/api 按模块封装的axios请求 src/views 页面组件 src/router 路由与守卫 src/utils axios实例、token存取 src/store 当前登录用户信息目录结构本身不算什么亮点,但统一返回结果和全局异常处理是你需要重点关注的。大部分版本都会定义一个Result类,包含code、msg、data三个字段,所有接口统一返回这个结构。前端axios封装里根据code判断成功失败,弹提示框。全局异常处理则保证数据库报错、参数校验失败时,返回给前端的是友好信息而不是一堆堆栈。这两块虽然代码简单,但项目里到处都是它们的影子,也是你答辩时能实打实讲出"工程化意识"的地方。
再看一次完整的请求流转,以登录为例:
用户点击登录按钮 → 前端axios发POST到/api/user/login→ 后端拦截器放行登录接口 → UserServiceImpl里比对用户名、密码(密码用MD5或者BCrypt哈希存储) → 查库成功则生成JWT返回 → 前端把token存入localStorage → 路由跳转到首页。
之后每一次请求,前端都会在header里带上Authorization: Bearer <token>,后端拦截器逐个校验。放行路径通常在WebMvcConfigurer里配置,大致长这样:
@Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtInterceptor) .addPathPatterns("/api/**") .excludePathPatterns("/api/user/login", "/api/common/**", "/api/register"); }这套流程你只要理清楚,后面所有接口的开发思路都会跟着清晰起来:每个Controller只管接收参数、调用Service、返回Result,鉴权的事交给拦截器,业务的事交给Service,查数据的事交给Mapper。各司其职,这就是分层架构的实际意义。
3. 数据库设计:从一张ER图到二十张表的思考过程
3.1 核心表的设计清单与关联关系
数据库是这套系统真正的骨架。别急着堆表,先围绕前面梳理的业务流,把核心实体列出来,再往每个实体上补字段。一个典型版本的表结构大概如下:
| 表名 | 作用 | 关键字段 |
|---|---|---|
| sys_user | 登录账户 | id, username, password, real_name, role_type |
| elder | 老人档案 | id, name, gender, birthday, id_card, phone, entry_date, status, bed_id |
| bed | 床位 | id, room_no, bed_no, status(0空闲/1占用/2维修) |
| care_giver | 护工扩展信息 | id, user_id, shift, max_count |
| elder_caregiver | 护工与老人关联表 | id, elder_id, caregiver_id |
| health_record | 健康体征记录 | id, elder_id, temperature, high_pressure, low_pressure, heart_rate, blood_sugar, record_time |
| fee_order | 费用单 | id, elder_id, fee_type, amount, status, create_time |
| notice | 公告通知 | id, title, content, create_time |
表与表的关系也很清晰:elder和bed是一对一,一个老人入住时占用一个床位;elder和health_record是一对多,一个老人每天可能有多条健康记录;elder和fee_order是一对多,一个老人在住期间会持续产生费用;care_giver和elder是多对多,所以中间有一张elder_caregiver关联表。
建表的顺序建议先四张主表:sys_user、elder、bed、care_giver,把登录和老人管理跑通,再加health_record和fee_order,最后按功能扩展notice、system_log这些边角表。一上来就把表建全不是本事,能在演进过程中把表设计得合理才是。
3.2 三个容易出问题的字段设计细节
第一个坑,逻辑删除与唯一索引冲突。MyBatis Plus的逻辑删除并不是真正的DELETE,而是执行UPDATE elder SET deleted = 1 WHERE id = ?。这本身没问题,问题在于如果某张表上有唯一索引,比如elder.id_card设了唯一,第一次删除的老人再想重新登记入住时,插入操作依然会撞唯一索引报错。很多同学在这个问题上卡一下午。
合理的做法有两种:一是干脆不建唯一索引,在Service层插入前先查一遍id_card(包含deleted=1的数据),存在就提示"该身份证已有档案,请查看是否已退住";二是建复合唯一索引(id_card, deleted),但这种方式在不同MySQL版本对deleted默认值的处理上容易出幺蛾子,教学项目我不太建议。第一种方案代码好写、逻辑好讲,也符合业务直觉。
第二个坑,金额字段必须用DECIMAL而不是double。这不是强迫症,而是浮点数在计算机里的存储方式决定了0.1 + 0.2可能等于0.30000000000000004。费用模块涉及对账,出现这种精度问题是无法接受的。数据库里字段类型用DECIMAL(10, 2),Java实体里对应BigDecimal,计算用add、subtract方法,而不是+、-。这是所有涉及钱的系统的底线。
第三个坑,状态字段要统一管理和业务联动。比如bed.status里的0、1、2,如果在代码里到处写死阿拉伯数字,今天写0明天写1,维护起来就是灾难。定义一个常量类或者枚举,把FREE、OCCUPIED、REPAIR命名清楚,代码里全用常量名。另外,状态之间最好有约束:只有空闲床位才能分配给老人,分配成功后床位要同步变更为占用,退住时床位释放。这个状态流转逻辑看着简单,实际是后面并发问题的重灾区。
4. 关键模块实现:老人档案、床位分配与健康记录
4.1 老人入住档案的保存链路
老人入住是整个系统的入口事件,几乎所有模块都会围绕它展开。所以你去看源码,它的saveElder方法通常不会只是往elder表插一条记录那么简单,而是一个牵涉多张表的事务操作。
我简化一下这个方法的典型逻辑:
@Transactional public void saveElder(ElderDTO dto) { // 1. 校验身份证是否已存在 if (elderMapper.selectByIdCard(dto.getIdCard()) != null) { throw new BusinessException("该身份证已存在档案"); } // 2. 锁定并占用一个空闲床位 Bed bed = bedMapper.lockFreeBed(dto.getBedId()); if (bed == null) { throw new BusinessException("床位不存在或已被占用"); } // 3. 保存老人信息 Elder elder = new Elder(); BeanUtils.copyProperties(dto, elder); elder.setStatus(1); elder.setBedId(bed.getId()); elderMapper.insert(elder); // 4. 更新床位状态 bedMapper.updateStatus(bed.getId(), 1); // 5. 绑定家属联系方式 dto.getFamilyList().forEach(item -> { elderFamilyMapper.insert(new ElderFamily(elder.getId(), item)); }); // 6. 生成首月费用单 feeOrderMapper.insert(new FeeOrder(elder.getId(), "床位费+护理费", firstMonthAmount)); }这个方法的精妙之处在于,它把"入住"这个业务动作的每一步都落到了数据库的实处。面试或答辩时你完全可以这样讲:入住不是一个简单的insert,它涉及档案表、床位表、家属表、费用表四张表的联动,所以我把它放在一个事务里,任何一步失败整个入住都回滚,不会出现"档案建了床位没占"这种脏数据。这个表述比一千句"我用了SpringBoot"都管用。
4.2 床位分配的状态机与并发防冲突
床位模块看着简单,其实藏着一个并发问题。如果两个管理员同时给两位老人分配同一个空闲床位,在没做任何保护的情况下,两个人都会先查到status = 0,然后都走后面的更新逻辑,结果一个床位住进去两位老人。
解决思路其实很朴素,别再"先查再改"了,把状态判断和更新合并成一条原子SQL:
public boolean tryOccupyBed(Long bedId) { int rows = bedMapper.updateStatusByOldStatus(bedId, 1, 0); return rows > 0; }对应的XML映射:
UPDATE bed SET status = 1 WHERE id = #{bedId} AND status = 0MySQL执行这条UPDATE时会对命中行加锁,两个并发请求同时到了数据库层面,只有其中一个会成功,另一个返回的影响行数为0。拿到0就知道"床位已经被抢走了",直接提示用户刷新后重选。这个方案不需要分布式锁,不需要额外中间件,几十行代码解决一个真实的并发问题,并且能在答辩时讲出"我是怎么考虑并发冲突的"。
床位状态机的全貌也要理清楚:空闲 → 占用发生在入住时;占用 → 空闲发生在退住结算后;空闲 → 维修和维修 → 空闲发生在管理员维护床位时。其中只有"空闲 → 占用"需要防并发,其余状态变更频率低、操作者少,普通更新就够了。这种"根据业务频率决定防御强度"的思路,本身就是一种架构判断力。
4.3 健康记录录入与趋势统计
健康记录是护工最常用的模块。护工登录后,先在"我的老人"页面看到分配给自己的老人列表,这个查询其实就是三张表的一次join:elder_caregiver关联care_giver和elder。录入表单包含体温、收缩压、舒张压、心率、血糖,保存时带上当前护工ID和记录时间。它背后的业务含义是,每个体征数据都能追溯到"谁在什么时间测的",将来老人健康出问题,能排查当时的记录人,这在医疗相关系统里叫数据留痕。
录入只是第一步,管理端更重要的是统计。比如想看某个老人最近一周的血压趋势,典型SQL是这样:
SELECT DATE_FORMAT(record_time, '%Y-%m-%d') AS day, ROUND(AVG(high_pressure), 1) AS avg_high, ROUND(AVG(low_pressure), 1) AS avg_low FROM health_record WHERE elder_id = #{elderId} AND record_time >= #{startTime} GROUP BY day ORDER BY day前端拿到这两个平均值数组,扔给ECharts画折线图就行。如果还想更进一步,可以做一个"健康异常预警名单":统计最近一周内出现过血压偏高的老人,按异常次数排序。逻辑也不复杂,只是在上面的SQL加一个HAVING AVG(high_pressure) > 140 OR AVG(low_pressure) > 90。这种从"查询"到"分析"的小升级,非常能体现你对业务的理解深度,也是我强烈建议你在答辩PPT里放一页的东西。
5. 跑通这个项目的完整过程与踩坑清单
5.1 环境准备与核心配置
如果你手里已经有一份源码,第一步不是看代码,而是把环境对齐。这套系统最常见的运行环境是:JDK 1.8或8+、Maven 3.6+、MySQL 5.7或8.0。前端如果是Vue2项目,Node建议用14到16版本,不要用18及以上,很多人在这一步就卡住了——旧依赖在高版本Node下会报OpenSSL相关的错误,网上搜一下全是"Node17导致webpack报错"的案例。
数据库导入注意几点:新建数据库时字符集选utf8mb4,然后执行源码包里的SQL文件。如果SQL文件里带了数据库名,建议改成你自己的库名再执行,省得后面连接串对不上。application.yml的配置是整个启动过程的命门,核心配置大概是这个样子:
spring: datasource: url: jdbc:mysql://localhost:3306/yanglao?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai username: root password: 123456 driver-class-name: com.mysql.cj.jdbc.Driver mybatis-plus: mapper-locations: classpath:mapper/*.xml global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0 configuration: map-underscore-to-camel-case: true有个非常经典的坑:MySQL 8.0的驱动类是com.mysql.cj.jdbc.Driver,MySQL 5.7是com.mysql.jdbc.Driver,两者不通用。如果你启动时报Loading class com.mysql.jdbc.Driver. This is deprecated或者直接提示找不到类,先检查这里的驱动配置和你的MySQL版本是否匹配。
5.2 前后端联调中的五个高频雷区
项目能启动只是万里长征第一步,前后端联调才是bug重灾区。我把这几年被问得最多的问题整理成一张表,你对照排查即可。
| 问题现象 | 根因 | 解决方案 |
|---|---|---|
| 前端调用接口报跨域 | 后端没有配置CORS | 写一个CorsConfig,注册CorsFilter,allowedOriginPatterns用*,注意allowCredentials(true)时不能用allowedOrigins("*") |
| 后端返回的日期是一串数字 | Jackson默认把Date序列化成时间戳 | 在application.yml里配置spring.jackson.date-format=yyyy-MM-dd HH:mm:ss和time-zone=GMT+8,或字段上加@JsonFormat |
| 登录成功但访问其他接口全部401 | 拦截器放行路径不对,或前端没带token | 检查拦截器excludePathPatterns是否包含登录接口;检查axios请求拦截器是否把token塞进了header |
| 分页数据不对或SQL方言报错 | 分页插件没配置或版本不匹配 | MyBatis Plus 3.5.x某些版本需要额外引入mybatis-plus-jsqlparser依赖,并给PaginationInnerInterceptor设置DbType.MYSQL |
| 上传图片后刷新页面404 | 自定义上传目录没有映射到静态资源 | 实现WebMvcConfigurer的addResourceHandlers,把磁盘路径映射到/upload/** |
这五个问题里面,跨域和日期格式是出现频率最高的。尤其是跨域,很多人搜到一段配置就粘贴,结果allowedOrigins("*")和allowCredentials(true)同时出现,反而把程序搞挂了。记着一个原则:允许凭证时,来源不能是通配符,要写成具体的来源或者allowedOriginPatterns("*")。
5.3 拿到源码后第一步改什么
不管从哪里拿到的源码,第一件事永远是三连改:数据库连接配置、后端端口、前端请求地址。这三处不改,项目永远跑不起来。
application.yml里的spring.datasource.username/password,八成和你的本机root密码不一致。server.port如果默认8080而本机已有服务占用,改成8081,同时记得把前端request.js里的baseURL同步改掉。- 前端封装axios的文件,通常在
src/utils/request.js或src/api/index.js里,确认baseURL是http://localhost:8080/api还是走Vue的proxy代理。两者都可以,但必须和你的后端端口保持一致。
改完这三处,启动后端,启动前端,登录页面能出来、菜单能点开、数据能查到,才算是真正跑通了。然后你再开始读代码,按第4节的思路,把核心表的增删改查链路走一遍。项目跑通以后最有价值的一步,是把每个页面对应的后端接口找出来,看它是怎么从Controller到Service再到Mapper的。这个"追链路"的过程,比看十篇教程都管用。
6. 如何把它改造成你自己的项目和答辩亮点
6.1 项目改名与品牌替换的三步走
手里这套源码直接拿去交,老师一搜就能搜到一模一样的,所以改造是必须的。最先要动的就是"门面"。
第一步,改后端pom.xml的<artifactId>和<name>,改成你自己的项目代号。第二步,改前端package.json的name和index.html的<title>,这个决定浏览器标签页上显示什么。第三步,全局搜索源代码里旧项目名出现的地方,前端登录页的标题、后台导航栏的logo文字、首页的欢迎语,全部替换成你的项目名。用IDEA的全局替换功能,几分钟就能搞定。
这里提醒一句:不要动包名。com.xxx.yanglao这种包名如果全局替换成新的,IDEA会提示你同步改目录结构,改完之后一堆import引用出错,纯属给自己找麻烦。一般评审老师不会盯着包名看,但会盯着页面标题和系统名称看,你把显示层的东西改干净,辨识度就已经很高了。
6.2 三个值得做的功能增强方向
光改名还不够,加功能才是拉开差距的方式。我推荐三个改动量不大、但讲出来很能加分的方向。
第一个,引入Redis做验证码存储和token管理。登录页加一个图形验证码,生成后把校验码存到Redis,设置两分钟过期,登录时校验通过再删除。再把JWT的token也放到Redis,可以实现管理员手动踢人下线。改动集中在登录模块和拦截器,核心业务表不用动,但技术栈里多了一个高并发系统常用的组件,技术含量立刻上去了。
第二个,做一个健康异常消息通知。当某位老人的最新血压超过阈值时,系统自动生成一条通知记录,管理员首页的铃铛图标显示红点,点击看到"XX老人血压偏高,请及时关注"。它本质就是一张notice表加一个定时任务或保存健康记录时的检查逻辑,但把"数据录入"升级成了"业务闭环",评委很好这口。
第三个,加一个数据大屏首页。把已有的统计接口利用起来,在首页展示入住率、床位分布、老人年龄结构、月度费用收入、健康异常人数,用ECharts画成图表。这套系统的表结构完全支撑这些数据,你只需要写几个统计SQL和前端几个图表组件,但视觉冲击力是所有增强方案里最强的,答辩开场一放,第一印象就拿住了。
6.3 答辩讲解的一条叙事主线
最后聊答辩。很多同学在台上紧张,是因为不知道讲什么,然后就开始念代码。代码是最不该念的东西。我建议你按这条线组织讲稿:业务痛点 → 技术方案 → 数据设计 → 核心接口 → 一个具体的坑和解决过程。
开场30秒说清楚"系统给谁用、解决什么问题",比如"这套系统服务养老院管理人员和护工,解决老人档案分散、床位状态不透明、健康数据难追溯、费用结算易出错四个问题"。然后讲架构选型,强调为什么用SpringBoot+MyBatis Plus、为什么用JWT而不是Spring Security。接着用第4节讲核心表关系,把elder、bed、health_record、fee_order四张表的关系画出来。再挑两个亮点往深了讲,排第一的一定是床位分配那条原子UPDATE防并发,这正是体现你思考深度的地方。
最后留一个"缺陷与改进"的话头,讲讲逻辑删除和唯一索引的冲突,以及你打算引入Redis做验证码的计划。这个环节不是让你暴露短板,而是展示你有排查问题、复盘问题、规划演进的能力。老师问"你还有什么可以优化的",你要是能顺着这个话题答上三五句,这个项目的基本盘就稳了。
我自己带过的学生里,用类似这套系统作为基础做二次开发的不少。最开始大家也都是一样的问题:数据库连不上、跨域报错、日期格式不对,但真正坚持把每个坑记下来、把核心链路捋清楚的人,答辩结果普遍不会差。项目的价值不在于功能多炫,而在于你能把一个完整流程讲闭环。拿到源码之后,建议你别急着交差,从第6节里挑一个小功能,比如健康异常通知,自己动手写一遍。写完你会发现,框架里的那些概念突然就落地了,再也不是飘在空中的名词。