简介:本资源是一套基于SSM框架开发的Java毕业设计项目——养老院管理系统,面向计算机专业本科生及Java初学者,解决养老机构信息化管理中的多角色协同、健康与服务流程数字化等实际问题。压缩包共1462个文件,87.59MB,涵盖200个Java后端逻辑类、192个JSP页面模板、364个JS交互脚本、177个PNG界面图标及146个CSS样式文件,辅以MySQL建表SQL、XML配置、演示视频MP4等,结构完整、前后端分离清晰,便于理解B/S架构下权限控制与业务模块划分。已有176人学习下载,适合课程设计参考、毕设快速启动与SSM实战复现。读者可直接部署运行,获取含管理员/护工/家属三端功能的全链路源码、详细说明文档及操作演示视频,覆盖房间预定、体检预约、服药记录、费用结算、事故上报等16类核心业务,代码规范、注释充分,具备良好可读性与二次开发基础。
1. 为什么一个养老院管理系统,成了Java毕业设计里最常被选又最容易翻车的项目?
“基于SSM框架的Java毕业设计:养老院管理系统”——这个标题在高校毕设选题库、代码托管平台和二手资料市场里高频出现,不是因为它多前沿,而是它精准踩中了三个现实约束:业务逻辑清晰可拆解(入住、护理、巡检、费用、家属沟通)、数据关系典型(老人、员工、房间、床位、健康记录多对一/一对多)、技术栈边界明确(Spring+SpringMVC+MyBatis足够覆盖,不强求微服务或高并发)。但正因“看起来简单”,大量学生在中期答辩时卡在登录态失效、家属端消息不同步、健康数据导出格式错乱、演示视频里点击无响应等细节上。这不是代码写得少,而是没把“养老场景的真实约束”翻译成技术实现:比如夜间巡检记录必须带时间水印防篡改、家属查看权限要按老人ID硬隔离、费用账单需支持按月冻结不可修改。本文不讲SSM原理,只聚焦——如何用最小改动让这个系统真正跑通、能演示、经得起老师点开任意模块追问。适合正在赶毕设进度、已搭好基础框架但卡在联调阶段的开发者。
2. 从零初始化SSM工程:避开Maven依赖地狱的3个关键决策
2.1 选哪个Spring版本?别碰5.3.x之后的“新特性陷阱”
很多同学直接用IDEA新建Spring Boot项目再降级为SSM,结果在DispatcherServlet配置上耗掉两天。真实经验是:锁定Spring 4.3.29.RELEASE + MyBatis 3.4.6 + MySQL Connector/J 5.1.47。这三个版本组合在JDK 8u202环境下兼容性最稳,且与高校机房常见Tomcat 8.5.32完全匹配。新版Spring 5.x强制要求JDK 8+但实际依赖javax.annotation包,在部分老旧IDE插件里会报NoClassDefFoundError,而4.3.x系列仍使用JSR-250标准,兼容性更鲁棒。
<!-- pom.xml 关键依赖片段 --> <properties> <spring.version>4.3.29.RELEASE</spring.version> <mybatis.version>3.4.6</mybatis.version> <mysql.connector.version>5.1.47</mysql.connector.version> </properties> <dependencies> <!-- Spring核心 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <!-- MyBatis整合 --> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>1.3.2</version> </dependency> <!-- MySQL驱动(注意:用5.1.x,不用8.0.x) --> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>${mysql.connector.version}</version> </dependency> </dependencies>提示:
mysql-connector-java必须用5.1.47而非8.0.x。后者默认启用serverTimezone=UTC校验,而本地MySQL服务若未显式配置时区,会导致java.sql.SQLException: The server time zone value 'XXX' is unrecognized。5.1.47版本默认关闭此校验,省去配置?serverTimezone=Asia/Shanghai的麻烦。
2.2 web.xml里DispatcherServlet的url-pattern必须是“/”,不是“*.do”
这是演示视频里“点击按钮没反应”的头号原因。很多教程教初学者写<url-pattern>*.do</url-pattern>,导致静态资源(CSS/JS/图片)被SpringMVC拦截却无对应Controller处理,返回404。正确做法是:
<!-- web.xml --> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> <!-- 关键:这里是"/",不是"*.do" --> </servlet-mapping>然后在spring-mvc.xml中显式放行静态资源:
<!-- spring-mvc.xml --> <mvc:resources mapping="/static/**" location="/static/" /> <mvc:resources mapping="/upload/**" location="/upload/" /> <!-- 同时配置默认servlet处理器,避免静态资源404 --> <mvc:default-servlet-handler />这样所有/static/css/app.css、/upload/photo.jpg请求由容器默认Servlet处理,而/elder/list、/nurse/checkin等业务路径才交由SpringMVC分发。
2.3 数据库连接池选Druid而非DBCP:一个参数救回90%的“页面加载转圈”
毕业设计演示时最尴尬的场景:老师点开“老人信息列表”,浏览器转圈10秒后报500错误。查日志发现com.mysql.jdbc.exceptions.jdbc4.CommunicationsException: Communications link failure——本质是MySQL连接被空闲超时关闭,而DBCP连接池未做有效性检测。Druid自带testWhileIdle机制,且配置简单:
<!-- spring-dao.xml 中的dataSource配置 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="url" value="jdbc:mysql://localhost:3306/nursing_home?useUnicode=true&characterEncoding=utf8" /> <property name="username" value="root" /> <property name="password" value="123456" /> <!-- 关键:开启空闲检测 --> <property name="testWhileIdle" value="true" /> <!-- 每30分钟检测一次空闲连接 --> <property name="timeBetweenEvictionRunsMillis" value="1800000" /> <!-- 连接空闲30分钟即回收 --> <property name="minEvictableIdleTimeMillis" value="1800000" /> <!-- 检测SQL,MySQL用SELECT 1 --> <property name="validationQuery" value="SELECT 1" /> </bean>血泪经验:
minEvictableIdleTimeMillis必须≤timeBetweenEvictionRunsMillis,否则检测线程永远不触发。我们设为相同值(1800000毫秒=30分钟),确保空闲连接在30分钟内被清理并重建,彻底解决演示时首次访问慢的问题。
3. 养老业务模型落地:把“老人-房间-护理计划”关系映射成可运行的三层结构
3.1 数据库表设计:用“状态字段”替代“逻辑删除”,避免家属端看到已退住老人
很多同学用is_deleted=1标记已退住老人,结果在“家属登录后查看老人信息”功能里,忘记加AND is_deleted=0条件,导致家属能看到已离院老人的隐私数据。正确做法是:用status枚举字段(0=在住,1=已退住,2=转院,3=去世),并在所有查询中强制WHERE status IN (0,2)(允许家属查看转院记录但不可编辑)。
-- elder_info 表关键字段 CREATE TABLE `elder_info` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL COMMENT '姓名', `room_id` bigint(20) DEFAULT NULL COMMENT '所属房间ID', `status` tinyint(4) NOT NULL DEFAULT '0' COMMENT '状态:0-在住,1-已退住,2-转院,3-去世', `create_time` datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (`id`), KEY `idx_room_status` (`room_id`,`status`) -- 联合索引加速按房间查在住老人 ) ENGINE=InnoDB DEFAULT CHARSET=utf8;注意:
idx_room_status索引不是可选项。当老师随机点开“302房间老人列表”时,数据库需快速过滤出room_id=302 AND status=0的记录。若只建room_id单列索引,MySQL仍需扫描所有302房间的记录再逐行判断status,1000条数据可能慢至800ms;联合索引可直接定位到目标行。
3.2 Service层事务控制:护理记录提交必须原子化,否则出现“签到成功但未生成工单”
养老院核心流程:护士点击“开始巡检” → 系统生成一条nurse_checkin记录(含时间戳、位置、体温)→ 同时向work_order表插入一条待处理工单(如“302房间呼叫铃异常”)。若这两步不在同一事务,可能出现护士看到“签到成功”提示,但后台工单未生成,导致隐患漏处理。
// NurseService.java @Transactional(rollbackFor = Exception.class) public void startCheckIn(CheckInDTO dto) { // 步骤1:保存巡检主记录 NurseCheckIn checkIn = new NurseCheckIn(); checkIn.setNurseId(dto.getNurseId()); checkIn.setRoomId(dto.getRoomId()); checkIn.setTemperature(dto.getTemperature()); checkIn.setCheckTime(new Date()); nurseCheckInMapper.insert(checkIn); // 步骤2:根据巡检结果决定是否生成工单 if (dto.getAbnormalFlag() == 1) { WorkOrder order = new WorkOrder(); order.setRoomId(dto.getRoomId()); order.setContent("巡检发现异常:" + dto.getAbnormalDesc()); order.setStatus(0); // 0-待处理 workOrderMapper.insert(order); } }关键点:
@Transactional必须标注在public方法上,且该方法不能是this.startCheckIn()自调用(Spring AOP代理失效)。若需在Service内部调用,应注入自身Bean:@Autowired private NurseService self;然后self.startCheckIn(dto);。
3.3 Controller层参数校验:家属修改密码必须验证原密码,且新密码不能与原密码相同
演示视频里老师常问:“家属能随便改别人密码吗?”——这暴露了权限校验缺失。正确流程是:家属登录后进入“个人中心”,点击“修改密码”,前端传参{oldPassword: "123", newPassword: "456", confirmNewPassword: "456"},后端必须:
- 根据当前登录家属ID查出加密后的原密码;
- 用BCrypt比对
oldPassword是否匹配; - 检查
newPassword与oldPassword明文是否相同(防止用户输错还点确认); - 检查
newPassword与confirmNewPassword是否一致。
// FamilyController.java @PostMapping("/updatePassword") @ResponseBody public Result updatePassword(@RequestBody PasswordUpdateDTO dto, HttpServletRequest request) { // 1. 从Session获取当前家属ID(非前端传的id,防篡改) Long familyId = (Long) request.getSession().getAttribute("familyId"); if (familyId == null) { return Result.fail("未登录"); } // 2. 查询当前密码(加密存储) String currentEncrypted = familyMapper.selectPasswordById(familyId); if (!BCrypt.checkpw(dto.getOldPassword(), currentEncrypted)) { return Result.fail("原密码错误"); } // 3. 防止新旧密码相同(明文比对) if (dto.getOldPassword().equals(dto.getNewPassword())) { return Result.fail("新密码不能与原密码相同"); } // 4. 更新密码(BCrypt加密) String newEncrypted = BCrypt.hashpw(dto.getNewPassword(), BCrypt.gensalt()); familyMapper.updatePasswordById(familyId, newEncrypted); return Result.success("密码修改成功"); }玄学提醒:
BCrypt.gensalt()不要传参。网上教程常写BCrypt.gensalt(12),但在SSM低版本下可能因SecureRandom初始化失败导致NullPointerException。无参调用使用默认强度10,足够毕业设计安全需求。
4. 演示视频必过要点:让老师点开任意模块都“有响应、有数据、有逻辑”
4.1 登录页必须预置三组测试账号,且首页自动显示欢迎语
老师不会等你手动输入账号密码。在login.jsp底部硬编码三组账号,并在index.jsp顶部用JSTL显示:
<!-- login.jsp 底部提示 --> <div class="login-tips"> <p><strong>演示账号(可直接点击登录):</strong></p> <p>管理员:admin / 123456</p> <p>护士:nurse001 / 123456</p> <p>家属:family001 / 123456</p> </div><!-- index.jsp 顶部欢迎语 --> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <c:choose> <c:when test="${not empty sessionScope.user}"> <h2>欢迎,${sessionScope.user.name}!</h2> <p>您当前身份:<c:out value="${sessionScope.user.roleName}" /></p> </c:when> <c:otherwise> <h2>请先登录</h2> </c:otherwise> </c:choose>实操技巧:
sessionScope.user对象必须在登录成功后存入Session。在LoginController.login()方法末尾加:request.getSession().setAttribute("user", user);。user对象需包含name和roleName字段(如"管理员"、"张护士"、"李明(王建国家属)"),让老师一眼看懂角色。
4.2 “老人信息列表”页必须带搜索+分页,且默认显示最近入住的5位老人
老师常点开列表页说:“数据太长,拉不到底”。解决方案:用PageHelper做物理分页,并设置默认查询条件。
// ElderController.java @GetMapping("/list") public String list(Model model, @RequestParam(defaultValue = "1") Integer pageNum, @RequestParam(defaultValue = "5") Integer pageSize) { PageHelper.startPage(pageNum, pageSize); // 默认查status=0(在住)且按create_time倒序 List<ElderInfo> list = elderService.listByStatus(0); PageInfo<ElderInfo> pageInfo = new PageInfo<>(list); model.addAttribute("pageInfo", pageInfo); return "elder/list"; }<!-- elder/list.jsp 分页HTML --> <div class="pagination"> <span>共 ${pageInfo.total} 条,当前第 ${pageInfo.pageNum} 页</span> <c:if test="${pageInfo.hasPreviousPage}"> <a href="?pageNum=${pageInfo.prePage}&pageSize=${pageInfo.pageSize}">上一页</a> </c:if> <c:if test="${pageInfo.hasNextPage}"> <a href="?pageNum=${pageInfo.nextPage}&pageSize=${pageInfo.pageSize}">下一页</a> </c:if> </div>避坑点:
PageHelper.startPage()必须在listByStatus()执行前调用,且该方法内不能有其他数据库操作(如先查总数再查列表),否则分页失效。pageInfo对象自带hasPreviousPage/hasNextPage布尔值,比手算pageNum>1更可靠。
4.3 “费用账单”导出Excel必须用Hutool,拒绝Apache POI的JDK版本冲突
演示时老师可能要求“导出本月账单”。用Apache POI易因ooxml-schemas版本与JDK 8不兼容报错。Hutool的ExcelUtil封装简洁且无依赖冲突:
// FeeController.java @GetMapping("/export") public void exportFee(HttpServletResponse response, @RequestParam Long elderId, @RequestParam String month) { // 1. 查询数据(示例:查2023-10月账单) List<FeeRecord> records = feeService.listByElderAndMonth(elderId, month); // 2. 构建Excel表头 List<String> headers = Arrays.asList("日期", "项目", "金额(元)", "状态"); List<List<Object>> rows = new ArrayList<>(); for (FeeRecord r : records) { rows.add(Arrays.asList( r.getFeeDate(), r.getFeeItem(), r.getAmount(), r.getStatus() == 0 ? "已支付" : "未支付" )); } // 3. 使用Hutool导出(无需额外配置) ExcelUtil.exportExcel(rows, headers, "养老院费用账单_" + month + ".xlsx", response); }落地检查:
pom.xml中添加Hutool依赖即可:<dependency> <groupId>cn.hutool</groupId> <artifactId>hutool-all</artifactId> <version>5.8.22</version> </dependency>版本5.8.x兼容JDK 8,且
ExcelUtil.exportExcel()自动处理HTTP响应头、流关闭,避免IllegalStateException: getOutputStream() has already been called。
5. 避坑指南:那些让答辩老师皱眉的5个具体问题及解法
5.1 现象:登录后跳转到空白页,浏览器控制台报Uncaught ReferenceError: $ is not defined
原因:jQuery未正确加载,常见于<script>标签顺序错误或CDN地址失效。
解决:
- 将jQuery放在所有业务JS之前;
- 改用本地文件而非CDN(毕业设计环境网络不可控):
<script src="${pageContext.request.contextPath}/static/js/jquery-3.6.0.min.js"></script> - 在
web.xml中确认<url-pattern>/static/**</url-pattern>已放行静态资源。
5.2 现象:新增老人时,房间下拉框为空,但数据库里已有房间数据
原因:RoomController.list()方法未加@ResponseBody,返回的是视图名而非JSON,前端AJAX接收不到数据。
解决:
// RoomController.java @GetMapping("/list") @ResponseBody // 必须加!否则返回"room/list"字符串而非JSON public List<RoomInfo> list() { return roomService.listAll(); }5.3 现象:家属端修改老人紧急联系人,提交后数据库没更新
原因:MyBatis的<update>语句中parameterType写错,或#{}占位符字段名与实体类不一致。
解决:
- 检查Mapper XML中
parameterType是否为com.example.entity.EmergencyContact; - 检查
<update>内SET子句是否用#{contactName}而非#{name}(实体类字段名必须完全匹配); - 在MyBatis日志中开启
log4j.logger.org.apache.ibatis=DEBUG,观察实际执行SQL是否含contactName字段。
5.4 现象:演示视频里点击“生成巡检报告”按钮,页面弹出“请求失败”,但后台无报错日志
原因:跨域问题(前端端口8080,后端Tomcat端口8080但上下文路径不同)或CSRF拦截。
解决:
- SSM项目默认无跨域配置,需在
spring-mvc.xml中添加:<mvc:cors> <mvc:mapping path="/nurse/**" allowed-origins="*" allow-credentials="true"/> </mvc:cors> - 若用表单提交,禁用CSRF(毕业设计无需安全加固):在
spring-mvc.xml中注释掉<mvc:interceptors>里的CSRF配置。
5.5 现象:导出的Excel打开提示“文件损坏”,用WPS可打开但Excel报错
原因:HTTP响应头Content-Type错误,未设为application/vnd.openxmlformats-officedocument.spreadsheetml.sheet。
解决:
- Hutool的
ExcelUtil.exportExcel()已自动设置正确Header; - 若手写导出,必须在
response.setContentType()后加:response.setHeader("Content-Disposition", "attachment; filename=" + URLEncoder.encode("账单.xlsx", "UTF-8"));
6. 让答辩加分的3个细节技巧:从“能跑通”到“像真系统”
6.1 给每个关键操作加“二次确认弹窗”,并记录操作日志
老师会关注系统是否具备生产级严谨性。在删除老人、退住、修改费用等敏感操作前,强制弹窗确认:
// common.js 全局函数 function confirmDelete(msg, url) { if (confirm("确定要" + msg + "?此操作不可撤销!")) { window.location.href = url; } } // elder/list.jsp 中的删除链接 <a href="javascript:confirmDelete('删除这位老人', '/elder/delete?id=${e.id}')">删除</a>同时,记录操作日志到数据库(非必须但极大加分):
// LogService.java public void saveOperationLog(String operator, String module, String action, String detail) { OperationLog log = new OperationLog(); log.setOperator(operator); log.setModule(module); // "老人管理" log.setAction(action); // "删除" log.setDetail(detail); // "ID=1001,姓名=张三" log.setCreateTime(new Date()); logMapper.insert(log); }价值点:答辩时老师问“怎么保证数据安全?”,你可答:“所有敏感操作均需二次确认,且完整记录操作人、模块、动作、详情,日志表独立存储,支持追溯。”
6.2 用CSS实现“夜间模式”切换按钮,展示前端工程能力
毕业设计常被质疑“全是后台,前端就抄模板”。加一个简单的深色模式切换,成本极低但效果显著:
/* static/css/dark-mode.css */ body.dark-mode { background-color: #1a1a1a; color: #e0e0e0; } body.dark-mode .card { background-color: #2d2d2d; border-color: #444; }// 页面底部JS document.getElementById('darkModeToggle').addEventListener('click', function() { document.body.classList.toggle('dark-mode'); // 保存用户偏好到localStorage localStorage.setItem('darkMode', document.body.classList.contains('dark-mode')); }); // 页面加载时读取偏好 if (localStorage.getItem('darkMode') === 'true') { document.body.classList.add('dark-mode'); }心法:这个功能不增加后端负担,纯前端实现,却能让老师直观感受到你对用户体验的思考。答辩时可强调:“我们考虑了护工夜间值班场景,深色界面减少屏幕眩光。”
6.3 演示视频结尾加“系统架构图”PPT页,用PlantUML手绘风格
不要用Visio画精美架构图,用PlantUML代码生成手绘风,显得真实:
@startuml skinparam defaultFontName "Droid Sans" skinparam nodesep 30 skinparam ranksep 40 title 养老院管理系统架构 [Browser] --> [Tomcat] [Tomcat] --> [Spring MVC] [Spring MVC] --> [Service] [Service] --> [DAO] [DAO] --> [MySQL] note right of [Browser] 家属/护士/管理员 三端统一入口 end note note left of [MySQL] 数据库设计: - 老人、房间、护理计划 - 状态字段驱动业务流 end note @enduml将此代码粘贴到 PlantText在线工具 生成PNG,插入视频结尾。老师看到“状态字段驱动业务流”这句话,立刻明白你理解了养老业务的核心约束。
我带过的某高校毕业设计小组,有位同学在答辩最后30秒放出这张图,老师当场问:“状态字段怎么驱动的?举个例子。”他流畅答出“退住老人status=1后,家属端查询自动过滤,费用模块禁止生成新账单”,顺利拿到优秀。这种细节准备,比多写100行CRUD代码更有说服力。
希望帮到你。
本文还有配套的精品资源,点击获取