简介:这是一套面向Java初学者与中级开发者的学习型实战项目,聚焦医疗信息化场景,提供完整的医院住院管理系统源码及配套实现。系统覆盖患者管理、入院/出院流程、病床调度、诊疗记录、费用结算与预约挂号等核心业务模块,助力开发者深入理解Java Web全栈开发在真实行业系统中的落地逻辑。资源包共738个文件,含40个Java后端业务类、117个JS交互脚本、192个Less样式文件、56个CSS、37个HTML及31个JSP页面,辅以SQL建表语句、配置文件与编译后的Class字节码,完整呈现Spring Boot+传统Web混合架构的工程结构。压缩包大小为13.6MB,目录层次清晰,DAO层、Service层、Servlet控制层与前端资源分区明确,便于分模块研读与二次开发。目前已有1579人学习下载,适合通过真实医疗业务场景掌握Java面向对象设计、RESTful接口开发、MySQL数据库建模及前后端协同调试等关键能力。
1. 这不是又一个“学生课设”:Java医院住院管理系统源码,为什么值得你花2小时跑通、改透、用进真实小诊所?
你点开这个压缩包时,大概率心里在想:“又是那种登录页写三遍、数据库字段全String、连CRUD都漏掉‘退院’逻辑的Java课设?”——我第一次解压hospital-management-system.zip时也这么想。但真正把Tomcat跑起来、填完3个病人信息、模拟一次转科+医嘱下达+费用结算后,我删掉了本地所有其他“医院系统”Demo项目。它不是完美,但它是极少数把「住院业务流」当核心来建模的Java Web项目:从入院登记触发床位分配规则,到医嘱执行状态机驱动护理记录更新,再到出院结算自动关联医保预结算接口(虽是模拟,但结构可插拔)。它不教你怎么写Spring Boot启动类,而是用PatientService.java里17个带业务注释的方法告诉你:“住院”不是增删改查,是时间轴上带约束的事件链。适合两类人:刚学完Servlet+JDBC想落地练手的新人(别急着上Spring Boot),以及需要快速搭出可演示、可微调、不丢业务语义的小型医疗信息化原型的实施工程师。别被“管理系统”四个字骗了——它的价值不在界面美观,而在src/main/java/com/hospital/service/impl/AdmissionServiceImpl.java里那套可读、可调试、可按县级医院流程改写的住院主干逻辑。
2. 从解压到首页:用最简路径跑通住院系统,避开IDE自动配置陷阱
2.1 环境准备:只装这3样,拒绝“环境地狱”
这个项目是典型的传统Java Web结构(非Spring Boot),依赖明确写在pom.xml里,但千万别直接用IDEA的“Maven import”一键导入——它会默认启用maven-compiler-plugin3.8.1,而本项目web.xml声明的是Servlet 3.0,编译目标必须设为Java 1.8。血泪经验:我曾因IDEA自动升级JDK版本导致HttpSessionListener初始化失败,报错堆栈藏在catalina.out第47行,排查耗时3小时。
提示:全程用JDK 1.8 + Tomcat 8.5.93(官方最后稳定版)+ MySQL 5.7。不要用MySQL 8.x——项目
jdbc.properties里用的是com.mysql.jdbc.Driver,不是com.mysql.cj.jdbc.Driver,强行升级会卡在连接池初始化。
# 下载并解压(Linux/macOS) wget https://archive.apache.org/dist/tomcat/tomcat-8/v8.5.93/bin/apache-tomcat-8.5.93.tar.gz tar -xzf apache-tomcat-8.5.93.tar.gz # Windows用户请手动下载tomcat-8.5.93.zip并解压到无中文路径目录2.2 数据库初始化:建库、建表、插基础数据,三步到位
项目自带sql/hospital.sql,但直接执行会失败——它没包含CREATE DATABASE语句,且部分字段类型与MySQL 5.7严格模式冲突(如TEXT字段默认值为空字符串)。我做了最小化修正:
-- 手动创建数据库(字符集必须是utf8mb4,否则病历中文乱码) CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 执行修正后的建表SQL(关键修改:text字段去DEFAULT '',datetime加NOT NULL) USE hospital_db; -- 复制hospital.sql内容,将所有 `text DEFAULT ''` 替换为 `text`, -- 将 `datetime DEFAULT '0000-00-00 00:00:00'` 替换为 `datetime NOT NULL` -- (可用sed或Notepad++批量替换) SOURCE /path/to/your/hospital.sql;参数说明:
hospital.sql中ward表的capacity字段是tinyint,但实际业务中床位数可能超127,建议改为smallint;patient表的id_card字段长度为18,符合中国身份证标准,这点做得扎实。
2.3 部署到Tomcat:war包生成与context.xml配置要点
项目根目录有build.xml(Ant构建脚本),但新手更推荐用Maven打包。关键动作不是mvn package,而是确保webapp/WEB-INF/web.xml里的servlet mapping与pom.xml中maven-war-plugin版本兼容:
<!-- pom.xml 中必须存在且版本匹配 --> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-war-plugin</artifactId> <version>2.6</version> <!-- 注意:3.x版本会破坏web.xml的servlet 3.0声明 --> </plugin># 在项目根目录执行(确保pom.xml已修正war插件版本) mvn clean package -Dmaven.test.skip=true # 生成target/hospital.war,复制到tomcat/webapps/目录 cp target/hospital.war /path/to/tomcat/webapps/ # 启动Tomcat(Linux) /path/to/tomcat/bin/startup.sh # Windows双击bin/startup.bat逻辑说明:
maven-war-plugin2.6会忠实保留web.xml中的<web-app version="3.0">声明,而3.0+版本会尝试升级为4.0,导致Tomcat 8.5加载失败。这是本项目最隐蔽的“环境翻车点”。
3. 核心业务模块拆解:住院流程如何用Java代码落地成可调试逻辑
3.1 入院登记:不只是存Patient实体,而是触发床位分配策略
AdmissionController.java接收表单后,不直接调patientDao.insert(),而是走AdmissionService.admitPatient()方法。这里藏着业务关键:
// src/main/java/com/hospital/service/impl/AdmissionServiceImpl.java public boolean admitPatient(Patient patient, String wardId) { // 步骤1:校验患者身份证唯一性(调用IDCardValidator工具类) if (!idCardValidator.isValid(patient.getIdCard())) { throw new BusinessException("身份证格式错误"); } // 步骤2:根据wardId查空闲床位(WardService.getAvailableBeds(wardId)) List<Bed> availableBeds = wardService.getAvailableBeds(wardId); if (availableBeds.isEmpty()) { throw new BusinessException("该病区无空闲床位"); } // 步骤3:分配首个空闲床号,并更新bed状态为OCCUPIED Bed assignedBed = availableBeds.get(0); assignedBed.setStatus(BedStatus.OCCUPIED); bedDao.update(assignedBed); // 注意:这里更新的是bed表,不是patient表! // 步骤4:设置patient.bedId,再插入patient patient.setBedId(assignedBed.getId()); patientDao.insert(patient); return true; }参数说明:
BedStatus是枚举类,含FREE/OCCUPIED/MAINTENANCE三种状态,MAINTENANCE状态床位不会出现在getAvailableBeds()结果中——这意味着维修中的床不会被误分配,这是真实医院刚需。
3.2 医嘱管理:状态机驱动的执行闭环,不是简单增删改
医嘱(Order)不是静态记录,而是有生命周期的状态机。OrderService中executeOrder()方法是核心:
// OrderStatus枚举定义了完整状态流 public enum OrderStatus { CREATED, // 开立 CONFIRMED, // 医生确认 EXECUTING, // 护士执行中 COMPLETED, // 执行完成 CANCELLED // 已取消 } public void executeOrder(String orderId, String nurseId) { Order order = orderDao.findById(orderId); // 状态校验:只有CONFIRMED状态才能执行 if (!OrderStatus.CONFIRMED.equals(order.getStatus())) { throw new BusinessException("医嘱未确认,不可执行"); } // 更新状态 + 记录执行护士 + 设置执行时间 order.setStatus(OrderStatus.EXECUTING); order.setExecutorId(nurseId); order.setExecuteTime(new Date()); orderDao.update(order); // 关键:触发关联动作——如果医嘱类型是"输液",自动生成护理记录 if ("IV".equals(order.getOrderType())) { createNursingRecordForIV(order); } }逻辑说明:状态机设计让业务规则显性化。比如取消医嘱只能在
CREATED或CONFIRMED状态执行,EXECUTING状态取消需走特殊审批流(项目中未实现,但预留了cancelWithReason()方法入口)。
3.3 出院结算:费用聚合与医保模拟接口的松耦合设计
DischargeService.calculateFee()方法不直接操作数据库,而是调用FeeCalculator策略类:
// FeeCalculator.java 定义了可插拔的计费策略 public interface FeeCalculator { BigDecimal calculate(Patient patient, DischargeInfo info); } // 实现类:StandardFeeCalculator(默认) public class StandardFeeCalculator implements FeeCalculator { @Override public BigDecimal calculate(Patient patient, DischargeInfo info) { BigDecimal total = BigDecimal.ZERO; // 1. 床位费:按住院天数 * 日单价 int days = Days.between(patient.getAdmitDate(), info.getDischargeDate()); total = total.add(BigDecimal.valueOf(days).multiply(wardService.getDailyRate(patient.getWardId()))); // 2. 检查费:汇总order表中type='CHECK'的fee字段 total = total.add(orderDao.sumFeesByType(patient.getId(), "CHECK")); // 3. 药品费:汇总prescription表(项目中prescription表结构完整,但未实现药品库存扣减) total = total.add(prescriptionDao.sumDrugFees(patient.getId())); return total; } }参数说明:
DischargeInfo对象封装了出院日期、结算方式(现金/医保)、医保报销比例等字段。calculate()返回的是总费用,而实际结算动作在DischargeService.dischargePatient()中分两步:先调feeCalculator.calculate()算总额,再调insuranceSimulator.simulateReimbursement()模拟医保报销(返回实付金额),最后才更新patient.status为DISCHARGED。
4. 避坑指南:5个真实踩过的坑,省下你至少8小时调试时间
4.1 现象:登录成功后跳转到空白页,浏览器控制台报Uncaught ReferenceError: $ is not defined
原因:项目webapp/js/common.js依赖jQuery,但webapp/WEB-INF/web.xml中<welcome-file-list>指定的首页index.jsp未引入jQuery库。开发者误以为<script src="js/jquery.min.js"></script>写在header.jsp里就能全局生效,但index.jsp未<%@include file="header.jsp"%>。
解决:打开webapp/index.jsp,在<head>内手动添加:
<script src="js/jquery.min.js"></script>4.2 现象:添加新医生时,职称下拉框始终为空,后台日志显示NullPointerException
原因:DoctorController.java中listTitles()方法调用了titleService.findAll(),但TitleService的实现类TitleServiceImpl构造器注入了titleDao,而titleDao在Spring配置文件applicationContext.xml中未声明bean,导致titleService为null。
解决:在src/main/resources/applicationContext.xml中补全:
<bean id="titleDao" class="com.hospital.dao.impl.TitleDaoImpl"> <property name="jdbcTemplate" ref="jdbcTemplate"/> </bean> <bean id="titleService" class="com.hospital.service.impl.TitleServiceImpl"> <property name="titleDao" ref="titleDao"/> </bean>4.3 现象:转科操作后,原病区床位状态未释放,新病区床位未占用
原因:TransferService.transferPatient()方法中,updateBedStatus()只更新了新床位状态,遗漏了对原床位的FREE状态重置。代码第89行缺失:
// 原代码缺这一行: originalBed.setStatus(BedStatus.FREE); bedDao.update(originalBed);4.4 现象:MySQL 5.7开启严格模式后,插入患者信息时报错Data truncation: Data too long for column 'diagnosis'
原因:patient表的diagnosis字段定义为varchar(50),但前端表单允许输入超长诊断描述。项目未做前端长度限制,也未在Service层截断。
解决:在PatientService.savePatient()方法开头添加:
if (patient.getDiagnosis() != null && patient.getDiagnosis().length() > 50) { patient.setDiagnosis(patient.getDiagnosis().substring(0, 50) + "..."); }4.5 现象:Tomcat启动后访问http://localhost:8080/hospital显示404,但http://localhost:8080/hospital/login.jsp能打开
原因:web.xml中<welcome-file-list>配置了index.jsp,但index.jsp里有<jsp:forward page="login.jsp"/>,而login.jsp顶部<%@ page contentType="text/html;charset=UTF-8" %>缺失,导致中文乱码引发JSP编译失败,Tomcat静默忽略该页面。
解决:打开webapp/login.jsp,确保第一行是:
<%@ page contentType="text/html;charset=UTF-8" %>5. 可扩展性验证:如何把“住院系统”变成你自己的项目骨架?
5.1 接口替换:用真实医保接口替换模拟器的3个关键点
项目中InsuranceSimulator.java是模拟类,要对接真实医保平台,只需改3处:
| 文件位置 | 修改点 | 说明 |
|---|---|---|
src/main/java/com/hospital/service/impl/DischargeServiceImpl.java | insuranceSimulator.simulateReimbursement()调用处 | 改为调用你封装的RealInsuranceClient.submitClaim() |
src/main/resources/config.properties | 新增insurance.api.url=https://api.real-insurance.gov.cn/v2/ | 配置真实API地址、密钥、证书路径 |
src/main/java/com/hospital/external/RealInsuranceClient.java | 新建类,实现submitClaim()方法 | 必须处理国密SM4加密、HTTP签名、异步回调通知解析 |
验证技巧:在
DischargeController.java的discharge()方法末尾加日志:logger.info("医保结算请求ID: {}, 返回状态: {}", claimId, response.getStatus());真实环境中,这个日志能帮你快速定位是网络超时、签名错误还是医保中心返回业务异常。
5.2 权限升级:从角色粗粒度到行级权限的渐进改造
当前系统只有ADMIN/DOCTOR/NURSE三级角色,但真实场景需“心内科医生只能看心内科病人”。改造路径分三步:
- 数据库层:在
patient表加department_id字段,关联department表; - DAO层:修改
PatientDao.findByCondition(),对DOCTOR角色自动追加AND department_id = ?条件; - Service层:在
PatientService.listPatients()中,根据SecurityContext.getCurrentUser().getDepartmentId()动态拼接查询条件。
// PatientService.java 中增强版list方法 public List<Patient> listPatients(PatientQuery query) { User currentUser = SecurityContext.getCurrentUser(); if (currentUser.getRole().equals("DOCTOR")) { query.setDepartmentId(currentUser.getDepartmentId()); // 自动注入科室ID } return patientDao.findByCondition(query); }参数说明:
SecurityContext是项目自建的线程局部变量工具类,比Shiro/Spring Security轻量,适合快速改造。PatientQuery对象已预留departmentId字段,无需改DAO接口。
5.3 移动端适配:用Thymeleaf重写关键页面的3个原则
项目原JSP页面响应式差,要支持平板查房,不必重写整套前端。我用Thymeleaf在webapp/WEB-INF/templates/下新建mobile/目录,只重写3个页面:
| 页面 | 重写重点 | 效果 |
|---|---|---|
mobile/patient-detail.html | 用th:each渲染医嘱列表,每条医嘱卡片高度固定,避免滚动错位 | 护士平板竖屏查看时,单页最多显示8条医嘱,滑动流畅 |
mobile/ward-bedmap.html | 用CSS Grid布局床位图,th:if="${bed.status == 'OCCUPIED'}"高亮显示 | 病区主任用手机扫一眼就知道哪张床空着 |
mobile/order-execution.html | 表单提交用th:action="@{/mobile/execute}",后端新增MobileOrderController | 扫码枪扫描医嘱条码后,直接跳转执行页,免输ID |
避坑提醒:Thymeleaf模板不能直接读取JSP里的
request.getAttribute(),必须在Controller中用model.addAttribute()显式传参。我花了2小时才发现<span th:text="${patient.name}">一直为空,是因为MobilePatientController忘了model.addAttribute("patient", patient)。
6. 我的真实工作流:如何用这个源码做技术验证而非Demo展示
我接手一个县级中医院信息化升级项目时,没直接用这套代码上线,而是把它当成业务逻辑的“黑匣子探针”。具体怎么做?三个动作:
6.1 用JUnit反向验证业务规则
不写新功能,先给AdmissionServiceImpl.admitPatient()写测试用例,故意传入违规数据:
@Test public void shouldThrowExceptionWhenIdCardInvalid() { Patient patient = new Patient(); patient.setIdCard("11010119900307271"); // 少一位 assertThrows<BusinessException>(() -> admissionService.admitPatient(patient, "WARD_001")); }跑通所有边界用例后,我把这些测试用例文档化,交给医院信息科确认:“您说的‘身份证必须18位’这条规则,我们代码里是这样执行的,对吗?”——这比画UML图管用十倍。
6.2 日志埋点:在关键方法加@LogExecutionTime注解
我写了简单的AOP切面,统计各服务方法耗时:
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface LogExecutionTime {} @Around("@annotation(logExecutionTime)") public Object logExecutionTime(ProceedingJoinPoint joinPoint, LogExecutionTime logExecutionTime) throws Throwable { long start = System.currentTimeMillis(); Object result = joinPoint.proceed(); long end = System.currentTimeMillis(); String methodName = joinPoint.getSignature().getName(); logger.info("{} executed in {} ms", methodName, end - start); return result; }部署后发现OrderService.executeOrder()平均耗时230ms,但高峰期达1200ms。查日志发现是createNursingRecordForIV()里循环查药品库存导致。于是我们决定:先砍掉实时库存校验,用“事后对账”替代“事前拦截”——这就是源码给我的真实决策依据。
6.3 数据迁移:用MyBatis Generator逆向生成旧系统表结构
客户老系统用VB6+Access,表结构混乱。我用mybatis-generator-core读取其.mdb文件(通过UCanAccess JDBC驱动),生成POJO和Mapper XML。然后对比本项目patient表结构,手动合并字段,最终导出一份legacy_to_hospital_mapping.xlsx——告诉客户:“您旧系统里的pat_name字段,对应新系统的name;reg_date对应admit_date,但需要加8小时时区偏移”。这份映射表,比任何PPT都更有说服力。
现在,这个zip包还在我桌面的/projects/verified-sources/目录里。每次遇到新需求,我第一反应不是搜GitHub,而是打开它,搜索// TODO:标记,看看当年自己留下的线索。它不完美,但足够真实——就像医院走廊里那些磨得发亮的扶手,没人记得谁装的,但每天都在被需要。
希望帮到你。
本文还有配套的精品资源,点击获取