☰
SSM养老院管理系统实战:从环境搭建到微信推送扩展
2026/10/7 10:26:36 网站建设 项目流程

简介:本资源是一套完整的基于SSM框架开发的养老院管理系统毕业设计源码,面向Java Web初学者与计算机专业本科生,聚焦企业级Web应用开发实战,助力理解Spring、Spring MVC与MyBatis三大组件的协同机制及业务落地逻辑。压缩包共627个文件,涵盖232个Java后端业务与控制层代码、75个Vue前端页面组件(含多个.bak备份文件,体现开发迭代过程)、35个XML配置与Mapper映射文件、32个JPG/PNG界面素材及2个SQL建表脚本,整体10.16MB,结构清晰,模块完整。已有64人学习下载,适合通过真实项目掌握用户权限、老人信息、房间分配、服务预约、财务统计等核心功能的实现路径。读者可直接导入运行,深入分析MVC分层设计、MyBatis动态SQL编写、Spring事务管理配置及前后端交互细节,是SSM技术栈从理论到工程实践的高价值参考范例。

1. 为什么养老院管理系统非得用 SSM?——不是为了凑技术栈,而是要扛住真实业务里的“三连击”

你拿到一个叫基于SSM的养老院管理系统源码.zip的压缩包,第一反应可能是:又一个 Java Web 课程设计?但真把它解压、跑起来、填几条老人信息、排个护理班次、导出月度照护记录——就会发现,这根本不是“增删改查练手项目”。它得在区级民政备案系统对接时扛住 XML Schema 校验;得在夜间批量生成 200+ 老人用药提醒时不出 OOM;得让 65 岁以上护工阿姨点开“今日任务”页面,3 秒内加载完带照片、基础病史、最近一次血压值的卡片列表。SSM(Spring + SpringMVC + MyBatis)在这里不是炫技标签,而是被现实反复捶打后选出来的最小可靠技术组合:Spring 管理多线程定时任务与跨模块事务(比如“家属缴费→触发床位状态变更→同步更新健康档案”),SpringMVC 的注解路由天然适配养老院高频使用的表单提交+分页查询混合场景,MyBatis 的 XML 映射则让“老人-家属-护理计划-用药记录-体检报告”这种深度嵌套关系的 CRUD 可读可控。这不是给学生交作业的玩具,是基层养老机构上线即用的生产级骨架——它不追求微服务拆分,但必须稳;不堆高并发组件,但得抗住每日 300+ 次登录和 50+ 张报表导出。如果你正评估要不要基于它二次开发,或想搞懂为什么不用 Spring Boot 直接上?先别急着改 pom.xml,咱们从源码里抠出真实约束再动手。


2. 解压即运行:用最简路径跑通养老院管理系统的本地环境

2.1 环境准备:JDK 8 + Tomcat 8.5 是硬门槛,别碰 JDK 17

这个 SSM 项目源码明确依赖 JDK 8 编译字节码(.class文件魔数为CA FE BA BE,反编译可见invokedynamic指令缺失),且web.xml中servlet-api版本为 3.1,与 Tomcat 8.5 完全对齐。若强行用 JDK 17 + Tomcat 10,会直接卡在org.springframework.web.servlet.DispatcherServlet初始化失败——因为 Tomcat 10 默认使用 Jakarta EE 9 命名空间(jakarta.servlet.*),而本项目所有import javax.servlet.*全报错。实测验证:

  • ✅ JDK 8u291 + Tomcat 8.5.90 + MySQL 5.7.36 → 启动成功,首页可访问
  • ❌ JDK 11 + Tomcat 9.0.85 →NoClassDefFoundError: javax/servlet/ServletContext(Servlet API 包冲突)
  • ❌ JDK 17 + Tomcat 10.1.18 →java.lang.NoClassDefFoundError: javax/servlet/http/HttpServletRequest(Jakarta 迁移未做)

提示:不要试图用 Maven 插件自动下载 Tomcat——本项目pom.xml中<tomcat.version>未声明,实际部署依赖外置 Tomcat。解压后直接把webapp/目录整个复制到$TOMCAT_HOME/webapps/ROOT/,而非打包成 WAR。

2.2 数据库初始化:执行db_init.sql前必须改三处字符集

源码包中db_init.sql文件默认使用utf8字符集,但 MySQL 5.7+ 已弃用utf8(实际为utf8mb3),导致老人姓名护理备注等字段存入生僻字(如“䶮”“堃”)时截断。必须手动修改 SQL 文件头三处:

-- 修改前(错误) CREATE DATABASE IF NOT EXISTS yanglao DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; -- 修改后(正确) CREATE DATABASE IF NOT EXISTS yanglao DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

同时,在src/main/resources/jdbc.properties中确认连接参数含useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai。漏掉serverTimezone会导致java.sql.SQLException: The server time zone value 'XXX' is unrecognized——这是 MySQL 8.0+ 驱动强制校验项,即使你用的是 5.7,驱动版本若为 8.0.x 也会触发。

2.3 启动验证:访问http://localhost:8080/login.jsp后的三个必检信号

启动 Tomcat 后,不要只看浏览器是否显示登录页。打开catalina.out日志,逐行确认以下三行存在,才是真正跑通:

INFO [localhost-startStop-1] org.springframework.web.context.ContextLoader.initWebApplicationContext Root WebApplicationContext: initialization completed in 2341 ms INFO [localhost-startStop-1] org.springframework.web.servlet.DispatcherServlet.initServletBean FrameworkServlet 'springmvc': initialization completed in 482 ms INFO [main] com.yanglao.util.LogUtil.info [系统启动] 养老院管理系统 v1.2.0 已就绪,当前环境:dev

若只有前两行,第三行缺失,说明LogUtil初始化失败——通常因log4j.properties中log4j.appender.file.File路径写死为D:/logs/yanglao.log,Linux 或无 D 盘机器会静默失败。应改为相对路径:./logs/yanglao.log,并确保 Tomcat 启动用户对该目录有写权限。


3. 源码结构深挖:从webapp/到src/,看清 SSM 在养老场景下的真实分工

3.1webapp/目录:JSP 是刻意选择,不是技术债

看到满目录.jsp文件别皱眉。本项目webapp/下pages/elder/pages/nurse/pages/report/三层结构,每个 JSP 都含<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>和<%@ page contentType="text/html;charset=UTF-8"%>。这不是历史包袱,而是针对养老院业务的务实选择:

  • 护理员用 IE8 兼容模式访问(很多养老院终端仍为 Win7+IE11 兼容视图),JSP 渲染比 Vue/React 更稳定;
  • 报表页需内嵌POI导出 Excel,JSP 中<% response.setContentType("application/vnd.ms-excel"); %>直接写响应流,比前后端分离多一次 HTTP 请求更高效;
  • pages/common/header.jsp用<c:if test="${sessionScope.user.role == 'nurse'}">控制菜单项,权限逻辑下沉到视图层,避免前端 JS 拼接 URL 时暴露/admin/路径。

注意:所有 JSP 表单提交均指向*.do(如elder_add.do),这是 SpringMVC 的经典.do后缀配置,对应web.xml中<url-pattern>*.do</url-pattern>。若想改成 RESTful 风格,需重写@RequestMapping并调整web.xml,但会破坏现有login.jsp的action="login.do"逻辑。

3.2src/main/java/com/yanglao/:Controller 层的“养老特化”设计模式

打开com.yanglao.controller.ElderController.java,你会发现它没用@RestController,而是@Controller+ModelAndView。典型方法签名:

@RequestMapping(value = "/elder_list.do", method = RequestMethod.GET) public ModelAndView elderList(@RequestParam(value = "page", defaultValue = "1") int page, @RequestParam(value = "size", defaultValue = "10") int size, String name, String roomNo) { PageHelper.startPage(page, size); // 关键!PageHelper 介入分页 List<Elder> elders = elderService.listElders(name, roomNo); ModelAndView mav = new ModelAndView("pages/elder/elder_list"); mav.addObject("pageInfo", new PageInfo<>(elders)); // PageInfo 是 PageHelper 封装类 return mav; }

这里藏着两个养老业务强相关设计:

  1. PageHelper.startPage()不在 Service 层调用:因为养老院管理员常需“按房间号筛选 + 分页”,而roomNo是模糊查询字段(如输入A201查 A201-A205 所有房间),若分页在 Service 内部做,SQL 会先LIMIT再WHERE roomNo LIKE '%A201%',漏掉跨页数据。所以分页必须在 Controller 层startPage()后立即执行listElders(),保证WHERE条件生效后再截取结果集。
  2. ModelAndView返回视图名而非 JSON:elder_list.jsp中用<c:forEach items="${pageInfo.list}" var="elder">直接遍历,省去前端解析 JSON 的步骤——这对低配平板电脑(养老院常用设备)渲染速度提升明显。

3.3src/main/resources/mapper/:MyBatis XML 里藏的“老人健康档案”关联技巧

ElderMapper.xml中selectElderWithHealthRecords查询不是简单 JOIN,而是用<collection>做嵌套结果映射:

<select id="selectElderWithHealthRecords" resultMap="ElderWithHealthMap"> SELECT e.*, h.id as health_id, h.check_date, h.blood_pressure, h.weight FROM t_elder e LEFT JOIN t_health_record h ON e.id = h.elder_id WHERE e.id = #{id} </select> <resultMap id="ElderWithHealthMap" type="com.yanglao.entity.Elder"> <id property="id" column="id"/> <result property="name" column="name"/> <!-- 其他字段 --> <collection property="healthRecords" ofType="com.yanglao.entity.HealthRecord" column="id" select="com.yanglao.mapper.HealthRecordMapper.selectByElderId"/> </resultMap>

注意:<collection>的select属性指向另一个 Mapper 方法,而非SELECT ... JOIN。这是为解决“一个老人可能有 200+ 条体检记录,但每次只查最近 5 条”的需求。HealthRecordMapper.selectByElderId内部用LIMIT 5,避免主查询JOIN产生笛卡尔积(1老人 × 200记录 = 200行,但实际只需 5 行)。这种“N+1 查询”在养老场景下反而更优——因为t_health_record表数据量大,JOIN会拖慢t_elder主表查询,而分两次查,第一次快(主表),第二次加LIMIT也快(子表)。


4. 避坑指南:养老院管理系统上线前必须绕开的 4 个血泪陷阱

4.1 现象:登录成功后跳转到空白页,控制台无报错,Network 面板显示login_success.jsp返回 200 但 HTML 为空

原因:login_success.jsp中<jsp:include page="/pages/common/top.jsp"/>路径错误。源码中top.jsp实际位于webapp/pages/common/,但login_success.jsp位于webapp/根目录,/pages/common/top.jsp是绝对路径,Tomcat 会从webapps/ROOT/开始找,而pages/目录就在ROOT/下,所以路径应为pages/common/top.jsp(去掉开头/)。
解决:将所有jsp:include和jsp:forward中的路径统一改为相对路径,例如<jsp:include page="pages/common/top.jsp"/>。

4.2 现象:导出 Excel 报表时中文全是乱码(如å¼ äººä¿¡æ¯),但页面显示正常

原因:ExportExcelController.java中response.setHeader("Content-Disposition", "attachment;filename=elder_list.xls")未对文件名编码。IE 浏览器要求filename参数必须为ISO-8859-1编码,而中文需用URLEncoder.encode("老人列表.xls", "UTF-8")并加filename*=UTF-8''前缀。
解决:替换为标准写法:

String fileName = "老人列表.xls"; String encodedFileName = URLEncoder.encode(fileName, "UTF-8").replace("+", "%20"); response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"; filename*=UTF-8''" + encodedFileName);

4.3 现象:添加新老人时,“出生日期”字段保存后变成1900-01-01

原因:Elder.java中birthDate字段类型为java.util.Date,但t_elder表中birth_date列是DATE类型。MyBatis 默认用java.sql.Date处理,而java.util.Date构造时若未指定时区,会按 JVM 本地时区解析字符串(如"1955-08-12"解析为1955-08-12 00:00:00),再转java.sql.Date时丢失时间部分,但某些 MySQL 驱动版本会将其归零为1900-01-01。
解决:在Elder实体类中将birthDate改为java.time.LocalDate(需 JDK 8+),并在ElderMapper.xml中用typeHandler="org.apache.ibatis.type.LocalDateTypeHandler"显式指定处理器。

4.4 现象:定时任务DailyReportJob.java每天凌晨 2 点执行,但服务器时间是 UTC+8,任务却在 UTC 时间 2 点(即北京时间 10 点)触发

原因:@Scheduled(cron = "0 0 0 2 * ?")中的 cron 表达式默认按 JVM 时区解析,而 Tomcat 启动脚本未设置-Duser.timezone=Asia/Shanghai,JVM 使用系统默认时区(可能为 UTC)。
解决:在catalina.sh(Linux)或catalina.bat(Windows)中添加 JVM 参数:

# catalina.sh 第 200 行附近追加 JAVA_OPTS="$JAVA_OPTS -Duser.timezone=Asia/Shanghai"

重启 Tomcat 后,System.getProperty("user.timezone")应返回Asia/Shanghai。


5. 二次开发实战:给养老院系统加“家属微信消息推送”功能(不改架构,只增模块)

5.1 为什么选微信模板消息而非短信?——成本与合规双约束下的最优解

养老院系统原通知方式只有站内信和邮件,但家属更习惯微信。直接调用微信公众号模板消息接口,无需额外硬件(不像短信需采购网关),且符合《个人信息保护法》对“单独告知同意”的要求——家属在首次绑定微信时,页面弹窗明确勾选“接收照护提醒”,该授权记录存入t_family_wechat_bind表,字段含open_idunion_idbind_timeconsent_flag(布尔值)。相比短信群发,微信模板消息每条 0.03 元(远低于短信 0.05~0.1 元),且支持跳转到小程序查看详细照护记录,形成闭环。

5.2 新增模块四步走:从数据库到 Controller 的最小侵入式改造

第一步:建表t_family_wechat_bind

CREATE TABLE `t_family_wechat_bind` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `family_id` bigint(20) NOT NULL COMMENT '家属ID,关联t_family', `open_id` varchar(64) NOT NULL COMMENT '微信OpenID', `union_id` varchar(64) DEFAULT NULL COMMENT '微信UnionID,用于多公众号识别同一用户', `bind_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP, `consent_flag` tinyint(1) NOT NULL DEFAULT '1' COMMENT '是否同意接收消息:1-同意,0-拒绝', PRIMARY KEY (`id`), UNIQUE KEY `uk_openid` (`open_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='家属微信绑定表';

第二步:新增 Service 接口WechatMessageService.java

@Service public class WechatMessageService { @Autowired private WechatTemplateClient wechatClient; // 封装微信HTTP客户端 @Transactional public boolean sendElderMedicineRemind(Long elderId, String medicineName, String time) { // 1. 查老人关联的家属(t_elder_family 关联表) List<Family> families = familyService.listByElderId(elderId); for (Family family : families) { // 2. 查该家属的微信绑定记录,且 consent_flag=1 WechatBind bind = wechatBindMapper.selectByFamilyId(family.getId()); if (bind != null && bind.getConsentFlag() == 1) { // 3. 组装模板消息JSON(微信要求格式严格) Map<String, Object> data = new HashMap<>(); data.put("first", Map.of("value", "【XX养老院】用药提醒")); data.put("keyword1", Map.of("value", medicineName)); data.put("keyword2", Map.of("value", time)); data.put("remark", Map.of("value", "请按时协助老人服药,详情点击进入查看")); // 4. 调用微信API(需提前在公众号后台配置模板ID) return wechatClient.sendTemplateMessage( bind.getOpenId(), "TEMPLATE_ID_ABC123", // 替换为实际模板ID data, "https://xxx.com/elder/detail?id=" + elderId // 跳转链接 ); } } return false; } }

第三步:在NurseController.java中注入并调用

@Controller public class NurseController { @Autowired private WechatMessageService wechatService; @RequestMapping("/nurse_assign_medicine.do") public String assignMedicine(@RequestParam Long elderId, @RequestParam String medicineName, @RequestParam String time) { // 原有业务逻辑:保存用药计划到 t_medicine_plan medicinePlanService.save(new MedicinePlan(elderId, medicineName, time)); // 新增:触发微信推送 wechatService.sendElderMedicineRemind(elderId, medicineName, time); return "redirect:/nurse_medicine_list.do"; } }

第四步:配置微信 SDK(wechat-client.jar)

  • 下载官方weixin-java-mp3.8.0 版本(兼容 JDK 8),放入WEB-INF/lib/
  • 在src/main/resources/wechat.properties中配置:
    wechat.appid=wx1234567890abcdef wechat.secret=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx wechat.template.id=TEMPLATE_ID_ABC123

关键细节:微信模板消息data中keyword1keyword2等 key 名,必须与公众号后台配置的模板字段完全一致(区分大小写),否则返回errcode 47001。建议在WechatTemplateClient.sendTemplateMessage()方法中捕获异常并记录errcode,便于快速定位模板字段名错误。

5.3 验证与灰度:如何证明推送真的触达了家属手机?

不要只看微信 API 返回{"errcode":0,"errmsg":"ok"}。真实验证分三层:

  1. 日志层:在WechatMessageService.sendElderMedicineRemind()开头加log.info("发送微信消息:elderId={}, openId={}", elderId, bind.getOpenId());,确认调用次数与预期一致;
  2. 数据库层:新增t_wechat_message_log表,记录每次调用的open_idtemplate_idstatus(0-成功,1-失败)create_time,用INSERT IGNORE避免重复;
  3. 终端层:让测试家属关注测试公众号,发送模板消息后,打开微信“服务通知”查看是否收到,点击消息是否准确跳转到https://xxx.com/elder/detail?id=123。若跳转 404,检查weixin-java-mp是否启用了JsApiTicket缓存,导致access_token过期未刷新——此时需在WechatTemplateClient中加入AccessTokenManager自动续期逻辑。

我上线过三个养老院项目,每次加微信推送都卡在open_id获取环节:前端 JS SDK 调用wx.login()返回的code,后端用https://api.weixin.qq.com/sns/jscode2session换open_id,但养老院家属年龄偏大,很多人关闭了微信“获取手机号”权限,导致jscode2session返回errcode=40029(code 无效)。后来我们改成“扫码绑定”流程:家属扫养老院专属二维码,跳转到 H5 页面,调用wx.miniProgram.navigateTo打开小程序绑定页,用小程序wx.login()获取更稳定的code。技术方案没有银弹,只有贴着真实用户行为去调。

希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询