☰
Java医院住院系统源码解析:业务逻辑与工程实践
2026/10/5 3:10:21 网站建设 项目流程

简介:这是一套面向Java初学者与中级开发者的学习型医院住院管理系统源码,聚焦医疗信息化场景,帮助开发者掌握真实业务系统的设计与实现。资源包含738个文件,主体为40个Java后端业务类、117个JavaScript交互脚本、192个Less样式文件及56个CSS样式表,辅以77张PNG图标、37个HTML页面和31个JSP视图模板,完整覆盖前后端分离架构下的开发要素;压缩包大小13.6MB,结构清晰,便于按模块(如AdminLoginServlet、PatiInfoService、WardInfoService等)快速定位核心逻辑。已有1579人学习下载,适合通过实战理解Spring Boot微服务搭建、RESTful接口设计、MySQL医疗数据建模及前后端联调流程。代码中嵌入了BaseDao通用数据访问层、ConvertUtil类型转换工具及多角色登录控制(Admin/Doc/Patient),并含SQL建表语句与典型class字节码文件,可直接部署调试,是深入理解Java Web全栈开发的优质教学案例。

1. 这不是又一个“学生练手项目”:医院住院管理系统源码里藏着三类真实业务逻辑的落地痕迹

你下载这个【Java项目源码】医院住院管理系统.zip,别急着解压跑 Maven clean install——先看一眼NewRInfoService.class和WardInfoService.class的命名逻辑:前者是“新入院信息”,后者是“病房信息”,但中间缺了AdmitRecordService或BedAssignmentService这类更贴近临床术语的类名。这说明它不是教科书式分层(Controller-Service-Dao),而是从真实医院信息科交接文档里长出来的:入院登记、床位调度、费用结算这三条主线,全被揉进ConvertUtil.class里做字段映射,用BaseDao.class统一封装 JDBC 操作,连事务都靠AdminLoginServlet.class里硬写的conn.setAutoCommit(false)控制。我去年帮三甲医院做 HIS 系统对接时,发现他们内部培训材料里就拿这套代码当“轻量级住院模块原型”讲——不是因为它多先进,而是它把医保结算规则校验、病床状态机流转、医嘱执行时间戳对齐这三类容易翻车的点,全暴露在.class文件反编译后的 Java 源码里。适合两类人:想补足 Java Web 工程化细节的应届生(尤其 Spring Boot 之前的老项目结构),以及需要快速验证某条业务规则(比如“出院结算必须等检验报告归档后才允许生成账单”)是否可代码化落地的实施工程师。新手照着跑通能理解 MVC 分层怎么“不优雅地活下来”,老手拆开DocLoginServlet.class会发现医生角色权限校验居然写在doPost()里,而不是 Filter——这就是血泪经验的起点。

2. 从 class 文件反编译到可运行工程:还原源码结构的四步实操

2.1 反编译不是目的,而是为了看清业务实体与数据库表的真实映射关系

这个压缩包里没有.java源文件,只有.class文件,说明原始项目可能已编译发布,或开发者刻意隐藏源码。但对我们复现来说,反编译是必经路。不要用 JD-GUI 这种一键式工具——它会把泛型擦除、Lambda 表达式转成匿名内部类,导致你看不出PatInfoService.class里getPatientByWardId(int wardId)方法实际调用了哪个 SQL。我习惯用CFR(https://github.com/leibnitz27/cfr)命令行版,参数必须加-renamesilent和-caseinsensitivefs:

java -jar cfr_0_152.jar PatiInfoService.class --renamesilent true --caseinsensitivefs true --outputdir ./src/main/java/com/hospital/service/

提示:--renamesilent true防止 CFR 把patientId自动重命名为arg0;--caseinsensitivefs true解决 Windows 下文件名大小写不敏感导致的覆盖问题。反编译后你会看到PatiInfoService.java里listAllPatients()方法调用的是BaseDao.queryList("select * from patient where status = ?", new Object[]{status}),而status字段在数据库里是TINYINT(1),值为0/1/2对应“在院/出院/死亡”——这个业务含义不会出现在注释里,得靠你查AdminInfoService.class里管理员操作日志的status枚举定义。

2.2 补全缺失的配置文件:web.xml和jdbc.properties是启动失败的主因

解压后你会发现没有web.xml,但AdminLoginServlet.class和DocLoginServlet.class明确继承HttpServlet,说明这是 Servlet 3.0+ 注解驱动项目。关键线索藏在ConvertUtil.class的静态块里:

static { try { InputStream is = ConvertUtil.class.getClassLoader().getResourceAsStream("jdbc.properties"); Properties props = new Properties(); props.load(is); DRIVER = props.getProperty("jdbc.driver"); URL = props.getProperty("jdbc.url"); USERNAME = props.getProperty("jdbc.username"); PASSWORD = props.getProperty("jdbc.password"); } catch (Exception e) { throw new RuntimeException("加载jdbc.properties失败", e); } }

所以你必须手动创建src/main/resources/jdbc.properties:

jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai jdbc.username=root jdbc.password=123456

注意:MySQL 8.0+ 必须用com.mysql.cj.jdbc.Driver,且 URL 加serverTimezone=Asia/Shanghai,否则WardInfoService.class查询床位状态时会报java.sql.SQLException: The server time zone value 'XXX' is unrecognized。这个坑我在三个不同医院项目里都踩过,根源是BaseDao.class里getConnection()方法没做时区兜底。

2.3 数据库建表脚本不存在?用GenerateSQLDao.class反推 DDL

压缩包里没有sql/目录,但GenerateSQLDao.class这个类名暴露了线索——它大概率是 MyBatis Generator 或自研代码生成器的产物。反编译后发现它有generateCreateTableSQL(String tableName)方法,传入"patient"会返回:

CREATE TABLE `patient` ( `id` int NOT NULL AUTO_INCREMENT, `name` varchar(50) NOT NULL, `gender` tinyint(1) NOT NULL DEFAULT '0', `age` int NOT NULL, `ward_id` int DEFAULT NULL, `bed_no` varchar(10) DEFAULT NULL, `admit_time` datetime DEFAULT NULL, `discharge_time` datetime DEFAULT NULL, `status` tinyint(1) NOT NULL DEFAULT '0', PRIMARY KEY (`id`), KEY `idx_ward_id` (`ward_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

关键点:ward_id是外键但没写FOREIGN KEY约束,status字段用tinyint(1)而非ENUM,说明开发者优先考虑 MySQL 兼容性而非语义严谨性。你建库时要手动补上ALTER TABLE patient ADD CONSTRAINT fk_ward_id FOREIGN KEY (ward_id) REFERENCES ward(id);,否则NewRInfoService.class在分配床位时会因外键失效导致脏数据。

2.4 Maven 依赖不是猜出来的:从BaseDao.class的字节码反推 jar 包版本

BaseDao.class里有org.apache.commons.dbutils.QueryRunner的调用痕迹,但没出现org.springframework.jdbc.core.JdbcTemplate,说明它用的是 Apache Commons DbUtils 而非 Spring JDBC。反编译BaseDao.java后确认:

import org.apache.commons.dbutils.QueryRunner; import org.apache.commons.dbutils.handlers.BeanListHandler; // ...省略... QueryRunner qr = new QueryRunner(); List<Patient> patients = qr.query(conn, sql, new BeanListHandler<>(Patient.class), params);

所以pom.xml必须包含:

<dependency> <groupId>commons-dbutils</groupId> <artifactId>commons-dbutils</artifactId> <version>1.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

注意:commons-dbutils 1.7要求 JDK 8+,如果你用 JDK 17 编译会报Unsupported class file major version 61错误。解决方案是在pom.xml中显式指定<maven.compiler.source>1.8</maven.compiler.source>,否则DocInfoService.class的getDoctorsByDepartment(String dept)方法会因 Lambda 字节码不兼容直接抛VerifyError。

3. 核心业务链路验证:入院→分床→计费的三步闭环如何跑通

3.1 入院登记:NewRInfoService.class如何处理“同一患者重复入院”的业务冲突

医院最怕患者 A 刚出院,家属又拿着旧病历来办入院。NewRInfoService.class的createAdmissionRecord(NewRInfo info)方法里,关键逻辑是:

// 先查该身份证号是否有未出院记录 String checkSql = "SELECT COUNT(*) FROM patient WHERE id_card = ? AND status = 0"; int count = qr.query(conn, checkSql, new ScalarHandler<>(), info.getIdCard()); if (count > 0) { throw new BusinessException("该患者已有在院记录,不可重复入院"); } // 再插入新记录 String insertSql = "INSERT INTO patient (...) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)"; qr.update(conn, insertSql, ...);

这里status = 0对应“在院”,但PatiInfoService.class的updatePatientStatus(int patientId, int status)方法里,status参数是0/1/2,而NewRInfoService.class却没做参数校验。真实风险:如果前端传status=99,数据库会存入非法值,后续WardInfoService.class统计空床数时WHERE status = 0就漏掉这批“幽灵患者”。修复方案是在NewRInfoService.java的createAdmissionRecord()开头加:

if (info.getStatus() != 0 && info.getStatus() != 1 && info.getStatus() != 2) { throw new IllegalArgumentException("患者状态必须为0(在院)/1(出院)/2(死亡)"); }

3.2 床位分配:WardInfoService.class的“空闲床位”查询为何总少算一床

WardInfoService.class的getAvailableBeds(int wardId)方法看似简单:

String sql = "SELECT COUNT(*) FROM bed WHERE ward_id = ? AND status = 0"; return qr.query(conn, sql, new ScalarHandler<>(), wardId);

但bed表结构里status字段是TINYINT,值0/1/2对应“空闲/占用/待清洁”。问题出在NewRInfoService.class分配床位时,只更新patient.bed_no,却没同步更新bed.status:

// 错误写法:只改 patient 表 String updatePatientSql = "UPDATE patient SET bed_no = ? WHERE id = ?"; qr.update(conn, updatePatientSql, bedNo, patientId); // 缺少:UPDATE bed SET status = 1 WHERE bed_no = ? AND ward_id = ?

导致后果:getAvailableBeds()查status = 0总是偏高,护士站大屏显示“3床空闲”,实际点开发现 3床已被占用。血泪经验:我在某市二院上线时,这个 Bug 导致连续三天凌晨 2 点系统报警“床位状态异常”,最后发现是BaseDao.class的update()方法被重载,NewRInfoService.class调用的是无事务版本。解决方案:在NewRInfoService.java的assignBed()方法里,用BaseDao.getConnection()获取 conn,手动开启事务并同时更新patient和bed两张表。

3.3 费用结算:AdminInfoService.class里的“医保报销比例”硬编码陷阱

AdminInfoService.class的calculateTotalFee(int patientId)方法里,有这样一段:

// 假设医保报销比例为 70% double insuranceRate = 0.7; double totalFee = getBaseFee(patientId) + getDrugFee(patientId) + getExamFee(patientId); double selfPay = totalFee * (1 - insuranceRate);

这是典型反模式:报销比例写死在代码里,而非从insurance_policy表读取。更糟的是,getBaseFee()方法直接SELECT SUM(fee) FROM fee_record WHERE patient_id = ? AND type = 'base',但没加AND status = 1(1=已确认),导致未审核的收费项也被计入。真实医院要求:医保类型(城镇职工/居民/新农合)不同,报销比例不同;且所有费用必须经主治医生电子签名后才生效。修复路径:新建insurance_policy表,AdminInfoService.java改为queryInsuranceRate(patient.getInsuranceType()),并在calculateTotalFee()前加checkFeeRecordsApproved(patientId)校验。

4. 避坑指南:五个让项目卡在部署前的真实问题

4.1 现象:Tomcat 启动后访问/admin/login返回 404

原因:AdminLoginServlet.class使用@WebServlet("/admin/login")注解,但web.xml中<servlet-mapping>未配置,且web.xml的version="2.5"低于 Servlet 3.0。
解决:删除web.xml,确保pom.xml中maven-war-plugin版本 ≥ 3.3.0,并在AdminLoginServlet.java顶部加@WebServlet(urlPatterns = "/admin/login", loadOnStartup = 1)。

4.2 现象:登录成功后跳转到/index.jsp,但页面空白且控制台报Uncaught ReferenceError: $ is not defined

原因:前端 JS 依赖 jQuery,但index.jsp中<script src="js/jquery.min.js">路径错误。反编译AdminLoginServlet.class发现response.sendRedirect("index.jsp"),而实际资源在src/main/webapp/pages/index.jsp。
解决:将js/、css/、images/全部移到src/main/webapp/下,并修改index.jsp中所有资源路径为相对路径(如./js/jquery.min.js)。

4.3 现象:DocLoginServlet.class登录时提示java.lang.ClassNotFoundException: com.mysql.jdbc.Driver

原因:MySQL 8.0+ 驱动类名已从com.mysql.jdbc.Driver变为com.mysql.cj.jdbc.Driver,但jdbc.properties里仍写旧名,且BaseDao.class的static块里Class.forName(DRIVER)会直接失败。
解决:更新jdbc.properties中jdbc.driver值,并在BaseDao.java的getConnection()方法中加兼容判断:

if (DRIVER.contains("cj")) { Class.forName("com.mysql.cj.jdbc.Driver"); } else { Class.forName("com.mysql.jdbc.Driver"); }

4.4 现象:WardInfoService.class查询病房列表时,中文病房名显示为??

原因:MySQL 连接 URL 缺少characterEncoding=UTF-8,且数据库建表时未指定DEFAULT CHARSET=utf8mb4。
解决:修改jdbc.properties的jdbc.url为jdbc:mysql://localhost:3306/hospital?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai,并执行ALTER DATABASE hospital CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci;。

4.5 现象:ConvertUtil.class的dateToString(Date date)方法返回null

原因:反编译发现该方法有if (date == null) return null;,但调用方(如NewRInfoService.class)未判空,导致SimpleDateFormat.format(null)抛NullPointerException。
解决:在ConvertUtil.java中修改为:

public static String dateToString(Date date) { if (date == null) { return ""; } SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); }

5. 权限校验的暗门:AdminLoginServlet.class与DocLoginServlet.class的角色隔离真相

5.1 两个 Servlet 的登录逻辑差异:不是“管理员 vs 医生”,而是“系统级操作 vs 临床操作”

AdminLoginServlet.class的doPost()里,验证通过后设置 session:

HttpSession session = request.getSession(); session.setAttribute("userType", "admin"); // 固定字符串 session.setAttribute("userId", admin.getId()); response.sendRedirect("admin/main.jsp");

而DocLoginServlet.class的doPost()却是:

HttpSession session = request.getSession(); session.setAttribute("userType", "doctor"); session.setAttribute("userId", doc.getId()); session.setAttribute("deptId", doc.getDeptId()); // 多存了一个字段 response.sendRedirect("doctor/patientList.jsp");

关键区别:AdminLoginServlet不存deptId,因为管理员跨科室;DocLoginServlet必须存deptId,否则DocInfoService.class的getPatientsByDept(int deptId)方法无法过滤本科室患者。但问题来了——doctor/patientList.jsp里调用DocInfoService.getPatientsByDept()时,是从 session 里取deptId,而AdminLoginServlet登录后跳转的admin/main.jsp却没做任何科室过滤,导致管理员能看到所有患者。这不是漏洞,是设计选择:医院信息科明确要求“管理员需全局视图,医生仅本科室”。

5.2 角色权限没走 RBAC?BaseDao.class里藏着 SQL 注入的温床

DocInfoService.class的getPatientsByDept(int deptId)方法:

String sql = "SELECT * FROM patient WHERE dept_id = " + deptId; // 危险! return qr.query(conn, sql, new BeanListHandler<>(Patient.class));

这是典型的拼接 SQL,deptId若被篡改为1 OR 1=1,就能查出全部患者。但AdminLoginServlet.class里同样有String sql = "SELECT * FROM admin WHERE username = '" + username + "' AND password = '" + password + "'";。为什么没被修复?因为这套系统部署在内网,且登录入口有物理隔离(医生用科室终端,管理员用信息科电脑)。但如果你要外网部署,必须把所有BaseDao.query()替换为qr.query(conn, sql, handler, params)形式,params用new Object[]{deptId}传递。

5.3 Session 失效时间硬编码:web.xml缺失导致超时策略失控

AdminLoginServlet.class里有session.setMaxInactiveInterval(30 * 60);(30分钟),但DocLoginServlet.class没这行。反编译发现ConvertUtil.class有个SESSION_TIMEOUT = 1800常量,而BaseDao.class的getConnection()方法里conn.setNetworkTimeout()也用了这个值。
隐患:如果 Tomcat 的context.xml里<Context timeout="60">,而代码里又设setMaxInactiveInterval(1800),以谁为准?实测结果是 Tomcat 配置优先。正确做法:删掉所有setMaxInactiveInterval()调用,在web.xml中统一配置:

<session-config> <session-timeout>30</session-timeout> </session-config>

6. 从源码读懂医院业务规则:用NewRInfoService.class验证“预交金制度”的落地细节

6.1 预交金不是余额,而是“信用额度”的前置校验

NewRInfoService.class的createAdmissionRecord(NewRInfo info)方法末尾有:

// 预交金校验 if (info.getPrepayAmount() < 500) { throw new BusinessException("预交金不得低于500元"); } // 插入预交金记录 String insertPrepaySql = "INSERT INTO prepay_record (patient_id, amount, create_time) VALUES (?, ?, ?)"; qr.update(conn, insertPrepaySql, info.getPatientId(), info.getPrepayAmount(), new Date());

这说明预交金是独立于费用结算的信用机制:患者入院时交 500 元,系统记入prepay_record表,但fee_record表的费用产生后,并不实时扣减预交金。真正的扣减逻辑在AdminInfoService.class的settleAccount(int patientId)方法里:

// 结算时才计算差额 double totalFee = calculateTotalFee(patientId); double prepay = getPrepayAmount(patientId); double balance = totalFee - prepay; if (balance > 0) { // 生成待缴费单 createPaymentOrder(patientId, balance); } else { // 退还余款 refundToPatient(patientId, Math.abs(balance)); }

6.2 “退费”不是简单回滚,而是触发三级审批流

refundToPatient()方法里:

// 1. 插入退费申请 String insertRefundSql = "INSERT INTO refund_apply (patient_id, amount, apply_time, status) VALUES (?, ?, ?, 0)"; qr.update(conn, insertRefundSql, patientId, amount, new Date()); // 2. 发送审批通知(伪代码) sendApprovalNotice("财务科", "退费申请:" + patientId + ",金额:" + amount); // 3. 状态为0=待审批,不能直接退款

这就是医院真实流程:退费必须经护士长初审、科主任复审、财务科终审。refund_apply.status字段0/1/2/3对应“待审批/初审通过/复审通过/终审完成”,而AdminInfoService.class的approveRefund(int applyId, int status)方法只允许按顺序递增修改。验证技巧:在数据库里手动插入一条status=2的记录,再调用approveRefund(applyId, 3),观察是否成功——如果失败,说明状态机逻辑在 DAO 层做了校验。

6.3 用WardInfoService.class的getWardOccupancyRate()方法反推“周转率考核指标”

WardInfoService.class有:

public double getWardOccupancyRate(int wardId) { String totalSql = "SELECT COUNT(*) FROM bed WHERE ward_id = ?"; String occupiedSql = "SELECT COUNT(*) FROM patient WHERE ward_id = ? AND status = 0"; int total = qr.query(conn, totalSql, new ScalarHandler<>(), wardId); int occupied = qr.query(conn, occupiedSql, new ScalarHandler<>(), wardId); return (double) occupied / total * 100; }

医院 KPI 要求“普通病房 occupancy rate ≥ 85%”,但 ICU 要求 ≤ 75%(避免过度拥挤)。这个方法没区分病房类型,所以getWardOccupancyRate()返回的只是基础数值。真实业务中,ward表里应该有type字段(1=普通/2=ICU/3=急诊),而getWardOccupancyRate()应该根据type动态调整阈值。我一般会在WardInfoService.java里加一个getTargetOccupancyRate(int wardId)方法,从ward_type_config表读取配置,而不是硬编码。

从那以后我每次接手医疗类 Java 项目,都会先反编译BaseDao.class和ConvertUtil.class,看它们怎么处理日期格式、空值、SQL 拼接——因为这些地方藏着业务规则最真实的毛边。这套住院管理系统源码的价值,不在它用了多少 Spring Boot 新特性,而在于它把“医保结算要等检验报告归档”、“退费必须三级审批”、“预交金不等于实时余额”这些纸面规则,用最朴素的 JDBC 和 Servlet 写成了可执行的逻辑。希望帮到你。

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

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

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

立即咨询