
簡介基于SSM框架的Java畢業設計項目——養老院管理系統的完整源碼包面向計算機相關專業學生及Java開發者可作為畢業設計參考或養老信息化系統的業務原型。系統采用B/S架構與MySQL數據庫后臺管理與前臺管理并行按管理員、護工、患者家屬三類角色分配權限管理員統籌老人、房間、預定、退房、體檢、飲食、事故、服藥、請假、費用與評價等模塊家屬端可查詢房間信息、預約體檢、在線請假、查看費用護工端負責老人信息維護、飲食記錄、健康信息及事故與服藥管理。資源共1462個文件以Java源碼、JSP頁面、JavaScript、CSS、圖片及SQL腳本為主包體約87.59MB附帶的說明文檔與演示視頻可幫助快速理解SSM框架整合、數據庫設計及業務流程實現。已有176人學習適合需要完整功能模塊參考、準備畢業設計或進行二次開發的Java學習者。1. 養老院管理系統的真實業務場景與選型背景第一次拿到這套基于SSM框架的養老院管理系統源碼時我并沒有急著看代碼而是先理了一下業務單據家屬要在線預約體檢、護工要登記服藥記錄、管理員要處理退房和費用結算。這些操作涉及三個角色、兩個端后臺管理和前臺管理背后是老人從入住到退房的完整生命周期。如果只是做個CRUD練手用Spring Boot加MyBatis Generator就能快速跑通但SSM這種分離式三層架構在事務控制、SQL調優和權限攔截上反而更“顯性”適合作為畢業設計展示完整工程能力。這套系統適合兩類人一是需要快速完成畢業設計并想講清楚設計思路的學生二是想接手或改造傳統SSM項目的初級Java工程師。下文我會從數據模型、權限控制、業務流實現到部署驗證把核心代碼和踩坑點逐一拆開。2. SSM三層架構與數據模型設計從ER圖到MySQL建表2.1 為什么用SSM而不是Spring BootSSM是Spring Spring MVC MyBatis的組合。和Spring Boot相比它需要顯式配置web.xml、spring-mvc.xml、mybatis-config.xml上手成本高但這也意味著你能看到Spring IoC容器如何組裝Controller、Service、Mapper。在畢業答辯時面試官常問的“Spring MVC請求流程”“MyBatis二級緩存”都能直接在工程里找到對應配置。我一般會建議保留SSM的XML配置不要為了省事改成全注解因為原始配置能更清晰地展示Bean的依賴關系。2.2 核心表結構拆解老人、房間、體檢、費用的關系從源碼里的styles.css.bak和.jsp.bak能看出這是一個JSP JSTL的老式項目但數據庫設計是完整的。我梳理出關系最重的幾張表old_man老人表核心字段包括old_id、name、health_status、room_id、nurse_id、family_id其中room_id關聯房間表family_id關聯家屬表。room_info房間表包含room_no、room_type、bed_count、room_status狀態值建議用0空閑 1占用 2維修而不是直接刪記錄方便后續統計入住率。check_record體檢表一個老人多次體檢所以設計成從表字段為old_id、check_date、result、doctor每次體檢生成一條記錄。pay_record費用表包含project_name、amount、pay_time、old_id費用項目如床位費、護理費、餐費可以用type字段區分。關鍵點在于老人與房間是多對一體檢與老人是一對多費用與老人也是一對多。這套模型的ER圖可以畫9張主表加6張關聯表足夠支撐畢業設計里的E-R圖章節。2.3 MySQL建表DML及字段設計說明這里我給出一個簡化但可直接運行的建表腳本重點在索引和外鍵的取舍CREATE TABLE old_man ( old_id INT NOT NULL AUTO_INCREMENT, name VARCHAR(32) NOT NULL COMMENT 老人姓名, gender TINYINT DEFAULT 0 COMMENT 0男1女, age INT DEFAULT 0, room_id INT DEFAULT NULL COMMENT 當前入住房間, family_id INT DEFAULT NULL COMMENT 綁定家屬賬號, nurse_id INT DEFAULT NULL COMMENT 負責護工, health_status VARCHAR(255) DEFAULT NULL COMMENT 健康評估摘要, PRIMARY KEY (old_id), KEY idx_room (room_id), KEY idx_family (family_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT老人基本信息表;注意幾點room_id我故意沒有建外鍵而是在Service層做業務校驗。原因是在畢業設計演示時如果刪除房間被外鍵攔截頁面報錯會打斷演示節奏。用索引代替外鍵可以保留性能又能在ServiceImpl里拋出友好異常“該房間已被老人占用”。字段注釋必須寫全用COMMENT描述每列含義答辯時老師一眼能看到你的表設計是否規范。2.4 數據權限的三種實現方式對比養老院系統涉及家屬、護工、管理員三種角色如果不在數據層做隔離家屬登錄后可能查到所有老人的體檢記錄。常見做法有三種方案實現方式優點缺點方案一SQL拼接在Mapper XML中用if testroleId2動態拼接where family_id #{userId}靈活針對性強容易漏拼接產生越權方案二MyBatis攔截器實現Interceptor接口自動追加數據權限SQL統一、不易漏配置復雜攔截器邏輯難排查方案三AOP切面在Service方法上做注解根據登錄用戶注入條件代碼侵入小需要定義自定義注解學習成本高我建議畢業設計用方案一。原因很簡單答辯時你能直接從Mapper XML里翻出權限SQL講給老師聽越權問題也最好復現。方案二和三雖然“高級”但一旦攔截器順序出錯所有分頁查詢都會受影響調試成本太大。3. 后臺管理核心模塊實戰基于Spring MVC的權限控制與業務流3.1 Spring MVC攔截器實現登錄與角色鑒權系統后臺管理包含家屬管理、護工管理、老人管理、房間預定、退房管理等多個模塊每個URL都需要校驗登錄狀態。配置一個HandlerInterceptor是最常規做法public class AuthInterceptor implements HandlerInterceptor { Autowired private AdminService adminService; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { Object user request.getSession().getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /login.jsp); return false; } // 管理員路徑要求角色為管理員 String uri request.getRequestURI(); if (uri.contains(/admin/) !admin.equals(((User) user).getRole())) { response.sendError(403); return false; } return true; } }這段代碼的邏輯是先從Session取loginUser為空就跳轉登錄頁如果請求路徑含/admin/但當前用戶角色不是admin返回403。需要注意兩點一是response.sendRedirect后必須return false否則請求會繼續放行二是不要把所有靜態資源css、js也攔截否則頁面加載不出樣式。我通常會在spring-mvc.xml里配置mvc:exclude-mapped排除/static/**和/login.jsp。3.2 Service層事務控制房間預定的并發防超賣房間預定模塊是后臺管理中最容易出并發問題的模塊。兩個管理員同時給不同老人預定同一個房間如果SQL只是簡單的select count(*)再update就會超賣。MyBatis配合Spring聲明式事務可以這樣處理Transactional(rollbackFor Exception.class) public boolean bookRoom(Integer roomId, Integer oldManId) { Room room roomDao.selectRoomForUpdate(roomId); if (room null || room.getRoomStatus() ! 0) { throw new BusinessException(房間不可預定); } roomDao.updateStatus(roomId, 1); roomBookDao.insert(new RoomBook(roomId, oldManId, new Date())); return true; }核心是selectRoomForUpdate這條SQL它用的是SELECT ... FOR UPDATE行級鎖SELECT * FROM room_info WHERE room_id #{roomId} AND room_status 0 FOR UPDATE;FOR UPDATE會鎖住該行直到事務提交。另一個管理員同時執行這條SQL時會被阻塞等第一個事務提交后才能讀到最新狀態。這里我要強調Transactional一定要寫在Service方法上而不是Controller或DAO上。因為FOR UPDATE的鎖范圍是整個事務如果事務邊界只包住DAO的select鎖會立即釋放防超賣就失效了。3.3 服藥管理與健康體檢的狀態機設計服藥管理模塊和健康管理模塊是護工主要操作的功能。我看了源碼里的數據結構發現服藥記錄其實是一個帶狀態的狀態機待服藥 - 已服藥 - 已漏服。如果用tinyint存狀態代碼里寫魔法數字后期改狀態判定會很痛苦。我自己實現時會用一個枚舉類public enum MedicineStatus { PENDING(0, 待服藥), TAKEN(1, 已服藥), MISSED(2, 已漏服); private final int value; private final String desc; MedicineStatus(int value, String desc) { this.value value; this.desc desc; } public static MedicineStatus fromValue(int value) { for (MedicineStatus status : MedicineStatus.values()) { if (status.value value) { return status; } } throw new IllegalArgumentException(未知狀態: value); } }在護工提交“已服藥”操作時Service層會校驗當前狀態必須是PENDING避免從MISSED直接跳到TAKEN。體檢結果模塊也有類似邏輯體檢預約后管理員填寫結果狀態從已預約變為已完成。這種狀態機驅動的寫法在答辯時可以講成“業務狀態流轉設計”比單純貼CRUD代碼有說服力得多。3.4 后臺管理界面的JSP復用與公共組件抽取源碼里的topNav.jsp.bak和setMenu.js.bak說明原項目用include方式復用頁面片段。我在改造時會把所有JSP拆成header.jsp、left_menu.jsp、footer.jsp再通過jsp:include引入。注意JSP中的% include和jsp:include區別前者是靜態編譯期合并后者是運行時包含。如果片段里有變量需要動態設置必須用jsp:include并傳入參數jsp:include page/common/header.jsp jsp:param namemenuActive valueroomBook/ /jsp:include這樣可以做到左側菜單的高亮與當前模塊聯動。setMenu.js.bak里的邏輯其實就是根據menuActive給對應菜單項加classactive。不要小看這個細節答辯演示時菜單高亮能直觀展示前后端傳參方式。4. 前臺家屬/護工端功能實現查詢、預約、記錄與狀態流轉4.1 家屬端“查詢房間信息”的MyBatis動態SQL前臺家屬登錄后常見操作是查詢空閑房間、預約體檢、請假、查看費用。查詢房間信息時不同家屬可能按房間類型、樓層、是否有陽臺篩選這時候MyBatis的where動態SQL就派上用場select idsearchRooms resultTypemap SELECT r.room_id, r.room_no, r.room_type, r.bed_count, r.room_status, f.floor_name FROM room_info r LEFT JOIN floor_info f ON r.floor_id f.floor_id where if testroomType ! null and roomType ! AND r.room_type #{roomType} /if if testfloorId ! null AND r.floor_id #{floorId} /if if testonlyFree ! null and onlyFree true AND r.room_status 0 /if /where ORDER BY r.room_id /selectwhere標簽會自動去掉第一個AND避免SQL語法錯誤。這里有個容易踩的坑if testonlyFree ! null在MyBatis中Boolean類型的字段如果為true判斷條件不能寫成onlyFree因為OGNL解析會有隱式轉換問題。我在做查詢時統一用onlyFree true的寫法。另外LEFT JOIN floor_info是為了在前臺頁面展示“一樓、二樓”這種友好名稱關聯字段floor_id最好在數據庫加索引。4.2 健康體檢預約時間沖突校驗與Quartz/Spring Task的取舍體檢預約模塊的業務規則是一個老人在同一時間段不能有兩條預約記錄。如果不用數據庫唯一索引就得在代碼里先查后插。我一般會在AppointService中加了這樣一段邏輯public boolean createAppoint(AppointDTO dto) { Integer count appointDao.countByOldManAndTime(dto.getOldId(), dto.getAppointTime()); if (count 0) { throw new BusinessException(該時間段已有體檢預約請換個時間); } // 剩余可預約名額 Integer used appointDao.countByDoctorAndTime(dto.getDoctorId(), dto.getAppointTime()); if (used dto.getMaxCount()) { throw new BusinessException(該醫生當前時段預約已滿); } appointDao.insert(dto); return true; }“預約提醒管理”模塊原項目里應該有定時掃描當天預約數據的邏輯。我見過很多畢業設計在這里用Thread.sleep循環這是絕對錯誤的做法。合理方案是使用Spring自帶的Scheduled注解Service public class RemindTask { Autowired private AppointDao appointDao; Scheduled(cron 0 0 8 * * ?) public void sendMorningRemind() { ListAppoint list appointDao.findTodayAppoint(); list.forEach(a - { // 給家屬發送短信或站內信這里僅做日志 System.out.println(提醒老人: a.getOldName() 于 a.getAppointTime() 體檢); }); } }cron表達式0 0 8 * * ?表示每天8點執行。注意Spring Task的Scheduled是單線程串行執行如果任務耗時過長會阻塞下一次執行。如果項目里還有“用藥提醒”等定時任務最好在spring-context.xml里配一個線程池用Scheduled(fixedDelay 1000)配合Async。對于畢業設計演示Scheduled已經足夠不需要引入Quartz增加復雜度。4.3 留言板與公告瀏覽的列表分頁性能優化前臺的公告和留言板都是高讀取、低寫入場景。用MyBatis分頁時我見過源碼里直接寫LIMIT #{offset}, #{pageSize}的但這樣每次都要count一次。更穩妥的辦法是PageHelper插件不過要注意PageHelper與Spring MVC的攔截器沖突問題。如果不想引插件可以用Map傳參select idselectNotices resultTypeNotice SELECT * FROM notice ORDER BY create_time DESC LIMIT #{offset}, #{pageSize} /selectService層計算offsetpublic PageResultNotice getNotices(int currentPage, int pageSize) { int offset (currentPage - 1) * pageSize; ListNotice list noticeDao.selectNotices(offset, pageSize); int total noticeDao.countAll(); return new PageResult(list, total, currentPage, pageSize); }這里的坑是如果pageSize從頁面傳入前端必須做數字類型校驗。有些同學直接把字符串拼到SQL里導致MyBatis報TypeException。還有一點公告表要加create_time索引否則數據量大時ORDER BY create_time DESC全表掃描答辯演示時被老師點出來會很尷尬。4.4 護工端的“事故管理”與“請假管理”的操作日志護工可以新增事故記錄、查看老人信息、登記飲食。事故管理比較敏感最好把“誰在什么時間改了什么字段”記錄下來。源碼里沒有日志表我會補一個operate_log表用AOP切面攔截寫操作Aspect Component public class OperateLogAspect { Autowired private OperateLogDao logDao; Before(execution(* com.nursing.service.*.*(..)) annotation(opLog)) public void recordLog(JoinPoint joinPoint, OpLog opLog) { String methodName joinPoint.getSignature().getName(); Object[] args joinPoint.getArgs(); logDao.insert(new OperateLog(opLog.module(), methodName, args.length 0 ? args[0].toString() : )); } }這個切面的含義是所有Service類中標注了OpLog注解的方法在執行前記錄一條日志。需要注意AOP代理默認使用JDK動態代理如果Service類沒有接口就會用CGLIB。SSM項目中Service通常都有接口所以沒問題。但要注意Aspect切面自身不能被代理否則會循環依賴。5. 部署驗證中的關鍵配置與常見坑5.1 JDK 8 Tomcat 8 的兼容性踩坑這套SSM源碼基于較老的Java Web規范我建議使用JDK 8和Tomcat 8.5組合千萬不要用Tomcat 10。因為Tomcat 10把javax.servlet包遷移到了jakarta.servlet老項目的web.xml和所有import javax.servlet.*會直接編譯失敗。配置Tomcat時要確認JRE指向的是jdk1.8而不是系統默認的更高版本。如果在Eclipse或IDEA里啟動時出現java.lang.UnsupportedClassVersionError說明編譯級別和運行環境不一致把Project Structure里的Project SDK和Modules Language Level都改成8。5.2 MySQL 5.7 與連接池參數源碼中的MySQL驅動如果是com.mysql.jdbc.Driver在高版本MySQL8.0以上會報ClassNotFoundException。解決方案有兩個換用com.mysql.cj.jdbc.Driver并改Maven依賴或者干脆沿用MySQL 5.7。數據庫備份文件.sql導入時遇到Unknown collation: utf8mb4_0900_ai_ci說明備份來自MySQL 8.0導入5.7會失敗。此時用文本編輯器把SQL里的utf8mb4_0900_ai_ci全部替換為utf8mb4_general_ci再重新導入。連接池我習慣配druid核心參數是initialSize5、maxActive20、minIdle5。如果部署機器內存只有512MB把maxActive降到10否則啟動時容易OOM。5.3 數據庫連接配置與編碼問題的最終檢查jdbc.properties是一個高頻出錯點。我給出一個可用的模板jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/nursing_home?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456注意characterEncodingutf8和serverTimezoneAsia/Shanghai缺一不可。如果后臺頁面中文亂碼但又確定這個配置沒問題就檢查JSP頁面頭部的% page contentTypetext/html;charsetUTF-8%還有web.xml中Spring MVC的CharacterEncodingFilter。這個Filter必須配置為第一個過濾器并且forceEncoding設為true否則POST請求中文參數仍會亂碼。最后驗證時登錄管理員賬號走一遍“家屬管理 - 添加家屬 - 家屬重新登錄 - 預約體檢 - 護工登記服藥 - 管理員結算費用”這條主線能跑通就說明所有表關聯和狀態流轉都正常。本文還有配套的精品資源點擊獲取