ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Java医院住院系统源码解析:业务逻辑与工程实践

Java医院住院系统源码解析:业务逻辑与工程实践 简介这是一套面向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。我习惯用CFRhttps://github.com/leibnitz27/cfr命令行版参数必须加-renamesilent和-caseinsensitivefsjava -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.propertiesjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456注意MySQL 8.0 必须用com.mysql.cj.jdbc.Driver且 URL 加serverTimezoneAsia/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) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;关键点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(); ListPatient patients qr.query(conn, sql, new BeanListHandler(Patient.class), params);所以pom.xml必须包含dependency groupIdcommons-dbutils/groupId artifactIdcommons-dbutils/artifactId version1.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency注意commons-dbutils 1.7要求 JDK 8如果你用 JDK 17 编译会报Unsupported class file major version 61错误。解决方案是在pom.xml中显式指定maven.compiler.source1.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却没做参数校验。真实风险如果前端传status99数据库会存入非法值后续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 11已确认导致未审核的收费项也被计入。真实医院要求医保类型城镇职工/居民/新农合不同报销比例不同且所有费用必须经主治医生电子签名后才生效。修复路径新建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的version2.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 srcjs/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 缺少characterEncodingUTF-8且数据库建表时未指定DEFAULT CHARSETutf8mb4。解决修改jdbc.properties的jdbc.url为jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/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()里验证通过后设置 sessionHttpSession 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 角色权限没走 RBACBaseDao.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));这是典型的拼接 SQLdeptId若被篡改为1 OR 11就能查出全部患者。但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 timeout60而代码里又设setMaxInactiveInterval(1800)以谁为准实测结果是 Tomcat 配置优先。正确做法删掉所有setMaxInactiveInterval()调用在web.xml中统一配置session-config session-timeout30/session-timeout /session-config6. 从源码读懂医院业务规则用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)方法只允许按顺序递增修改。验证技巧在数据库里手动插入一条status2的记录再调用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普通/2ICU/3急诊而getWardOccupancyRate()应该根据type动态调整阈值。我一般会在WardInfoService.java里加一个getTargetOccupancyRate(int wardId)方法从ward_type_config表读取配置而不是硬编码。从那以后我每次接手医疗类 Java 项目都会先反编译BaseDao.class和ConvertUtil.class看它们怎么处理日期格式、空值、SQL 拼接——因为这些地方藏着业务规则最真实的毛边。这套住院管理系统源码的价值不在它用了多少 Spring Boot 新特性而在于它把“医保结算要等检验报告归档”、“退费必须三级审批”、“预交金不等于实时余额”这些纸面规则用最朴素的 JDBC 和 Servlet 写成了可执行的逻辑。希望帮到你。本文还有配套的精品资源点击获取
返回列表