简介:基于SSM框架的养老服务系统Java毕业设计资源,面向需要完成类似选题的计算机专业学生,涵盖完整开发源码及开题报告、论文、PPT,可同时满足课题设计、论文撰写与答辩展示需求。资源共880个文件,以jsp动态页面、java业务逻辑、js/css前端交互、jar依赖库及图片素材为主,整体约28.65MB,目录结构清晰,包含前台展示、后台管理、数据库脚本等层次。功能上,前台支持网站公告、收费标准、用户注册、服务项目在线选择与支付、护理员查看与预约评价;后台区分管理员、护理员、注册用户三类角色,覆盖信息管理、预约审核、评价管理、支付记录管理等模块,涉及SSM框架集成、权限控制与在线支付流程。配套论文与PPT可辅助理解系统设计思路。已有44人学习下载,适合作为毕业设计、课程作业或SSM框架综合实践的参考蓝本,可直接搭建运行并在此基础上扩展优化。
1. 养老系统的技术选型为什么还选 SSM
养老机构的信息化往往不是从零开始,而是从一张张 Excel 表格开始的。护工排班、老人健康档案、家属探访记录、收费明细,散在行政人员的电脑和纸质文件夹里。真正要做一个养老服务系统时,团队最先遇到的问题不是功能怎么做,而是用什么架子去承载这些持续变化的业务规则。SSM(Spring + SpringMVC + MyBatis)在这个场景里仍然是可靠的选择,原因并不在于它新,而在于它分层清晰、事务可控、SQL 可调,能精确匹配养老业务中大量报表查询和复杂关联更新。本文不打算堆架构概念,直接按理论拆分、数据库设计、核心代码、部署验证的顺序,把基于 SSM 的养老服务系统从零搭起来。适合正准备用 Java 做毕业设计或中小型管理系统的开发者,也适合想从 Spring Boot 回头看 SSM 底层装配逻辑的在职工程师。
2. SSM 三层架构在养老项目里的分工与 Spring 整合配置
2.1 先分清 Controller、Service、Mapper 各自管什么
养老服务系统的业务边界很清晰,但代码边界如果不清晰,后期改一个排班规则会牵连到收费模块。SSM 的三层结构恰好能把这摊事拆开:Controller 层只接收 HTTP 请求和返回 JSON 或视图,Service 层处理业务规则和事务,Mapper 层只负责 SQL 交互。常见的错误是有人把 SQL 写在 Service 里,或者让 Controller 直接调用 Mapper,短期能跑,但一旦出现跨表事务,就会难以维护。
以“老人入住登记”为例,这个动作至少要完成三件事:写入老人基本信息、初始化健康档案、生成床位占用记录。Controller 只拿到前端传来的入住表单,Service 负责在一个事务里依次调用三个 Mapper 方法,任何一个失败则整体回滚。这种职责划分让代码的可测试性明显提升,后续为论文画时序图或模块图时,结构也更容易表达清楚。
2.2 Spring 容器管理下的 Mapper 注册与数据源配置
SSM 的整合难度不在框架本身,而在 Spring 容器不知道去哪里找 MyBatis 的 Mapper 接口。最常见做法是在 spring-mybatis.xml 里配置 SqlSessionFactoryBean,同时用 MapperScannerConfigurer 扫描指定包下的接口。数据源选用 Druid 连接池,因为养老系统涉及大量日志查询和报表统计,Druid 自带的监控面板对排查慢 SQL 很有用。
<!-- spring-mybatis.xml 核心配置片段 --> <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource" init-method="init" destroy-method="close"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/eldercare?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="root"/> <property name="initialSize" value="5"/> <property name="minIdle" value="5"/> <property name="maxActive" value="20"/> <property name="validationQuery" value="SELECT 1"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="typeAliasesPackage" value="com.eldercare.entity"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.eldercare.mapper"/> </bean>这段配置有两个容易踩的细节。第一,mapperLocations 指向 classpath:mapper 目录,所有 XML 映射文件必须放在 resources 下的同名路径,否则启动时 MyBatis 会报 Invalid bound statement;第二,MapperScannerConfigurer 不需要指定 sqlSessionFactoryBeanName,如果同时配置容易产生循环依赖。Druid 的 initialSize、minIdle、maxActive 要根据 Tomcat 的线程池大小来设,一般 5/5/20 足够容纳一个中型养老机构的并发访问量。
2.3 SpringMVC 配置 REST 风格接口与 JSON 转换
养老系统前端现在多采用 Vue 或简单的 HTML + Ajax,后端接口需要统一返回 JSON 格式的数据,而不是直接跳转 JSP 页面。SpringMVC 里要开启注解驱动,并注册 MappingJackson2HttpMessageConverter 处理对象到 JSON 的序列化。Fastjson 虽然速度快,但历史上有反序列化漏洞,一般业务系统更推荐 Jackson,Spring 对 Jackson 的集成也最平滑。
<!-- spring-mvc.xml --> <mvc:annotation-driven> <mvc:message-converters> <bean class="org.springframework.http.converter.json.MappingJackson2HttpMessageConverter"> <property name="objectMapper" ref="jacksonObjectMapper"/> </bean> </mvc:message-converters> </mvc:annotation-driven> <bean id="jacksonObjectMapper" class="com.fasterxml.jackson.databind.ObjectMapper"> <property name="dateFormat"> <bean class="java.text.SimpleDateFormat"> <constructor-arg value="yyyy-MM-dd HH:mm:ss"/> </bean> </property> </bean>这个配置解决了一个很实际的问题:Java 里的 java.util.Date 默认序列化为时间戳,前端拿到以后还要自己格式化。统一指定 yyyy-MM-dd HH:mm:ss 之后,前端可以直接展示。养老系统的健康评估时间、费用结算时间、探访登记时间都依赖这种标准格式,避免每张表自己处理一遍。
3. 养老服务系统的数据模型设计与核心功能代码实现
3.1 老人档案、护工排班、费用台账之间的表关系
数据模型是整个养老系统最值得花时间设计的部分。以常见的养老机构业务为基准,核心表可以拆成老人信息表、床位表、护工排班表、健康档案表、费用流水表五类。老人信息表保存姓名、身份证号、家属联系方式、入住日期;床位表记录楼栋、房间号、床位状态;健康档案表存过往病史、过敏药物、近期体检数据;费用流水表记录护理费、餐费、医疗费,每一条都关联老人 ID 和经办人 ID。
这五张表之间的外键关系不需要设计得太死板,尤其在费用流水表里,不建议直接引用订单主键,而是保存业务单据编号。例如某次体检费用,单据号可以写成 T20250101001,后台需要核对时通过单据号和费用类型去查关联表。这个设计能减少后期因为护理项目和收费项目不同步导致的脏数据,也能让论文里的 ER 图更规范。
3.2 健康档案模块的增删改查与状态字段控制
健康档案是养老系统里更新最频繁的数据模块。老人生病、转院、康复评估,每一次变化都需要保留记录,而不是在原记录上直接覆盖。常见做法是设计一张健康档案主表和一张档案变更记录表,主表只保留当前有效状态,变更表保留历史轨迹。
// 健康档案实体核心字段 public class HealthRecord { private Integer recordId; // 档案编号 private Integer elderId; // 老人 ID private String bloodType; // 血型 private String allergyInfo; // 过敏药物 private String chronicDisease; // 慢性病史 private Integer healthLevel; // 健康等级 1-5 private Integer status; // 1 有效 0 失效 private Date createTime; // getter/setter 省略 }// 新增健康档案时同时保留旧档案 public void addHealthRecord(HealthRecord record) { HealthRecord oldRecord = healthRecordMapper.selectActiveByElderId(record.getElderId()); if (oldRecord != null) { oldRecord.setStatus(0); // 旧记录失效 healthRecordMapper.updateStatus(oldRecord); } record.setStatus(1); healthRecordMapper.insert(record); // 写入变更日志,用于追溯 ChangeLog log = new ChangeLog(); log.setElderId(record.getElderId()); log.setOperateType("HEALTH_UPDATE"); log.setContent("健康档案更新为" + record.getHealthLevel() + "级"); changeLogMapper.insert(log); }这里的逻辑核心在于 status 字段。新的档案插入之前先锁定旧档案并置为失效,保证查询当前状态时永远只返回一条有效记录。变更日志的写入与主操作放在同一个方法里,由 Spring 事务统一提交或回滚。很多没有实际项目经验的人容易忽略变更日志,但真正上线之后,家属投诉健康信息被改错时,没有历史数据基本说不清。
3.3 MyBatis 动态 SQL 实现多条件组合查询
养老系统的搜索场景比想象中复杂:家属打电话来问“我家老人的护理等级是不是变了”,院办需要按姓名、健康等级、入住时间范围、床位状态任意组合筛选老人列表。使用 MyBatis 动态 SQL 可以避免写多个 Mapper 方法,只用一条 SQL 配合 where 标签处理空值判断。
<!-- ElderMapper.xml 多条件查询 --> <select id="searchElders" resultType="com.eldercare.entity.Elder"> SELECT e.elder_id, e.elder_name, e.id_card, e.bed_id, h.health_level FROM elder_info e LEFT JOIN health_record h ON e.elder_id = h.elder_id AND h.status = 1 <where> <if test="elderName != null and elderName != ''"> AND e.elder_name LIKE CONCAT('%', #{elderName}, '%') </if> <if test="healthLevel != null"> AND h.health_level = #{healthLevel} </if> <if test="startDate != null"> AND e.create_time >= #{startDate} </if> </where> ORDER BY e.create_time DESC </select>注意 health_level 的过滤条件是作用在左连接的健康档案表上,并且限制了 status = 1,否则一个老人有多条档案时会查出重复行。WHERE 标签在 MyBatis 中会自动去除第一个多余的 AND 或 OR,这是非常多人在手写 SQL 拼接时出错的地方。还有一个容易忽略的点是日期比较的转义,XML 中大于号要用>而非直接写>,之前有人在 Mapper 里写>=导致启动时 XML 解析报错。
3.4 费用结算中的 Spring 声明式事务与并发控制
费用模块最容易出问题的不是计算逻辑,而是重复扣费或并发更新。一个场景是月初统一结算护理费,执行两次就会产生两条费用流水。解决方式有两种,一是数据库层面给费用流水表加唯一索引,以“老人 ID + 账单月份”作为唯一键;二是在 Service 层加分布式锁或先查询后更新的操作加 synchronized 关键字。单机部署的 SSM 系统用 synchronized 在方法上做同步就够了,多实例部署时才需要考虑 Redis 分布式锁。
@Service public class FeeService { @Transactional(rollbackFor = Exception.class) public synchronized void settleMonthlyFee(Integer elderId, Integer operatorId) { // 检查本月是否已结算 int count = feeMapper.countByElderAndMonth(elderId, currentMonthStr()); if (count > 0) { throw new BusinessException("本月费用已结算,请勿重复操作"); } // 查询护理等级对应的费用标准 HealthRecord record = healthRecordMapper.selectActiveByElderId(elderId); BigDecimal baseFee = feeStandardMapper.getFeeByLevel(record.getHealthLevel()); // 生成费用流水 FeeRecord feeRecord = new FeeRecord(); feeRecord.setElderId(elderId); feeRecord.setAmount(baseFee); feeRecord.setFeeType("MONTHLY_CARE"); feeRecord.setCreateBy(operatorId); feeMapper.insert(feeRecord); } }这里事务注解放在类上或方法上都行,rollbackFor 指定异常类型为 Exception,否则 Spring 默认只在 RuntimeException 时回滚。如果业务代码抛出了如 BusinessException 这样的受检异常,不加 rollbackFor 就会看到数据只插了一半、无法回滚的现象。synchronized 只在单体应用内有效,需要跨进程并发控制时建议换用数据库悲观锁,也就是在查询本月结算记录时使用 for update 语句。
4. 权限控制、分页查询缓存与服务层日志监控的落地策略
4.1 基于拦截器的登录鉴权与角色区分
养老服务系统的使用者分为三类:院办管理员、护理人员、系统维护人员。不同角色的可见数据范围不相同,护工只能看到分配给自己的老人信息,管理员可以查看全部。使用 SpringMVC 拦截器做权限控制是最直接的方式,在 preHandle 方法中检查 session 里的登录用户和对应角色。
public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session = request.getSession(); User loginUser = (User) session.getAttribute("loginUser"); // 未登录统一返回 401 状态码,由前端跳转到登录页 if (loginUser == null) { response.setStatus(401); response.setContentType("application/json;charset=UTF-8"); response.getWriter().write("{\"code\":401,\"msg\":\"未登录或登录已过期\"}"); return false; } String uri = request.getRequestURI(); // 管理员角色能访问所有接口,护工只能访问 /nurse/ 前缀的接口 if ("nurse".equals(loginUser.getRole()) && !uri.startsWith("/nurse/")) { response.setStatus(403); response.getWriter().write("{\"code\":403,\"msg\":\"无权限访问\"}"); return false; } return true; } }注意 response 输出 JSON 时不要使用 JSP 的 forward 跳转,养老服务系统的前端多为独立页面,直接返回状态码让前端拦截器统一处理更干净。在配置拦截器时还需要排除登录接口和静态资源路径,否则前端在未登录状态下连验证码图片都加载不出来。
4.2 PageHelper 分页的参数配置与使用时最容易犯的错误
列表数据不分页会让查询越来越慢,这是养老系统上线一段时间后必然遇到的问题。PageHelper 是 SSM 项目里最主流的物理分页插件,它使用 MyBatis 拦截器在 SQL 执行前自动拼接 LIMIT 语句。引入依赖后在 MyBatis 配置里注册 PageInterceptor 即可。
<plugins> <plugin interceptor="com.github.pagehelper.PageInterceptor"> <property name="helperDialect" value="mysql"/> <property name="reasonable" value="true"/> <property name="supportMethodsArguments" value="true"/> </plugin> </plugins>PageHelper 使用时的规则非常明确:在要分页的查询语句前一行调用 PageHelper.startPage(pageNum, pageSize),紧随其后的第一条 Mapper 查询会被执行分页。很多人把 startPage 和查询语句之间插入了一段其他查询代码,此时分页会作用在错误的 SQL 上。合理设置 reasonable 为 true,这样当传入的 pageNum 超过最大页数时,PageHelper 会自动查询最后一页,避免接口因为越界而返回空数据。分页结果需要使用 PageInfo 来获取总数和总页数,而不是直接返回 List。
public PageResult<ElderVO> getElderPage(int pageNum, int pageSize, String keyword) { PageHelper.startPage(pageNum, pageSize); List<Elder> list = elderMapper.selectElders(keyword); PageInfo<Elder> pageInfo = new PageInfo<>(list); // 将实体列表转换为前端需要的脱敏列表后再返回 List<ElderVO> voList = list.stream().map(e -> new ElderVO(e)).collect(Collectors.toList()); return new PageResult<>(voList, pageInfo.getTotal(), pageInfo.getPages()); }很多开发者忽略 PageInfo 的泛型问题,前端需要的是分页总数,但拿到的却只是当前页的结果。PageInfo 构造时会自动从 Page 对象中提取 total 和 pages,不需要额外写 COUNT 查询。分页数据在转换 VO 时要注意 stream 转换会触发懒加载问题,建议在分页查询完成的同一事务内完成字段填充。
4.3 日志记录与慢 SQL 监控
养老服务系统涉及到费用和健康数据,操作日志不只是排查问题的工具,更是合规要求。建议在整个请求链路开启日志记录,同时 MyBatis 显式开启 SQL 日志输出,观察慢查询的位置。使用 Log4j2 或 Slf4j + Logback 都可以,在 logback.xml 中为 Mapper 包单独配置 DEBUG 级别即可。
<!-- logback.xml --> <logger name="com.eldercare.mapper" level="DEBUG"/> <Logger name="com.alibaba.druid" level="INFO"/>Druid 数据源自身提供了慢 SQL 统计功能。在数据源配置中加入 slowSqlMillis 参数,执行时间超过阈值的 SQL 会在监控页面展示。借助 Druid 的 stat 插件,可以统计每条 SQL 的执行次数、错误次数和最大耗时,快速定位到问题语句。在写毕业设计论文时,这部分监控截图也可以作为项目“非功能性设计”章节的真实材料。
4.4 前后端分离时 Session 接口改造的注意事项
如果前端使用的是 Vue 项目而并非 JSP,SpringMVC 的接口路径和返回结构都需要统一规范。统一返回结构是最容易被忽略的一点,如果有的接口返回 {code:0, data:{}},有的接口直接返回 List,前端封装 Axios 拦截器时就会异常难处理。建议定义 Result 类,统一为 code、message、data 三个字段,所有 Controller 方法的返回值都使用 Result。
前端跨域访问时需配置 CORS,SpringMVC 中可以直接在接口类上加 @CrossOrigin 注解,也可以实现 WebMvcConfigurer 全局配置。跨域配置需要注意 allowCredentials 设为 true 时 allowOrigins 不能设为 “*”,要显式写出前端部署地址。养老系统的管理员可能在内网环境直接用 IP 访问服务端,这时跨域配置需要把可能的本机 IP 和域名都加入允许列表。
5. 从 Tomcat 部署到安全加固:养老系统上线前的最后工作
5.1 打 War 包与 Tomcat 启动验证路径
SSM 系统一般选用 Tomcat 8.5 或 Tomcat 9 进行部署。在 Maven 的 pom.xml 中的打包方式设置为 war,在项目目录执行mvn clean package后生成 eledercare.war,将 war 包放入 Tomcat 的 webapps 目录下,启动 Tomcat 后自动解压部署。注意检查 JDK 版本与 Tomcat 的兼容性,Java 8 对 Tomcat 9 支持良好,Java 11 需要 Tomcat 9 以上版本。
启动只看启动日志里没有异常还不够。用curl http://localhost:8080/eldercare/login验证接口是否正常返回状态码,用bin/startup.sh启动完成后,通过ps -ef | grep tomcat查看进程是否选举存活。这一步经常踩到的坑是服务启动成功但访问 404,多是 SpringMVC 的根路径 context-path 没配好,可以检查 war 包的名称和访问路径是否一致。
5.2 服务器性能观察与 JVM 参数排查
Tomcat 默认的 JVM 堆内存较小,养老系统运行一两个月后,在访问高峰期往往会遇到 GC 频繁甚至内存溢出。建议在catalina.sh里配置 JVM 参数,按服务器物理内存调整堆大小。常用配置为堆最小 512M、最大 1G,新生代与老年代比例合理设置避免频繁 Full GC。
mkdir -p /data/tomcat/logs cd /data/tomcat bin/startup.sh tail -f /data/tomcat/logs/catalina.out日志里大量出现 OutOfMemoryError 时,优先使用jmap -dump:format=b,file=heap.bin 进程号导出堆快照,再用 MAT 分析哪个对象占据内存。养老项目里最容易发生的还是集合对象存储过多且没有清空,例如分页查询时把全部数据加载到内存后再过滤,而不是在 SQL 层完成过滤。查询语句尽量带上一次性筛选条件,从数据源处截断问题,比增加内存更能治本。
5.3 上线前的常见安全配置清单
养老系统的数据敏感性要求上线前做好安全加固。第一,登录接口必须加入验证码校验,防止暴力破解;第二,Druid 的监控页面需要配置访问密码,不能默认开放内网访问权限;第三,使用 Shiro 替换手写拦截器时要注意过滤器顺序,用户表和角色表的关联要在 Shiro 配置前就绪。
| 配置项 | 推荐设置 | 常见错误 |
|---|---|---|
| Druid 监控面板 | 设置 loginUsername/loginPassword | 不配置导致任意内网访问 |
| 连接池超时 | 设置 connectionTimeout 为 10000ms | 不设置导致数据库连接慢 |
| 事务超时 | @Transactional 的 timeout 设置为 -1 | 无限等待导致死锁 |
| 密码强度 | 强制包含大小写和数字 | 仅做非空校验 |
密码加密不能使用明文存储。推荐使用 Spring 的 BCryptPasswordEncoder 处理登录密码,每次校验随机盐值都不一样,即使数据库泄露也无法逆向还原。在论文中这部分可以写成“基于 BCrypt 的密码安全存储方案”,属于基本功但不做必被扣分的内容。
本文还有配套的精品资源,点击获取