医院住院管理系统:Java高并发事务与医疗领域建模实战

发布时间:2026/10/31 9:34:06
医院住院管理系统:Java高并发事务与医疗领域建模实战 简介这是一套面向Java初学者与中级开发者的学习型医院住院管理系统源码聚焦医疗信息化场景帮助开发者掌握真实业务系统的设计与实现。资源包含738个文件主体为40个Java后端业务类、117个JavaScript交互脚本、192个Less样式文件及56个CSS样式表辅以77张PNG图标、37个HTML页面和31个JSP动态视图完整呈现前后端分离架构下的模块化开发实践压缩包体积13.6MB结构清晰含Spring Boot核心配置、MySQL建表SQL及典型Servlet服务类如AdminLoginServlet、PatiInfoService等。已有1579人学习下载读者可直接运行调试深入理解患者管理、床位调度、诊疗记录、费用结算等六大核心功能的代码组织逻辑并通过源码反推RESTful接口设计、DAO层封装、权限控制与Web安全防护等关键知识点是Java Web全栈开发的优质练手项目。1. 为什么医院住院管理系统不是“写个CRUD就完事”的Java练手项目你下载到的这个【Java项目源码】医院住院管理系统.zip表面看是毕业设计级的Java Web小系统但实际踩进去会发现它是一面照出你真实工程能力的镜子——不是考你能不能用Spring Boot搭起一个能增删查改的页面而是考你能不能在「患者入院→床位分配→医嘱执行→费用结算→出院归档」这条强约束、高耦合、多角色协同的业务链路上把Java的面向对象建模、事务边界控制、并发安全、数据一致性保障真正落地。很多开发者跑通首页登录后就以为“项目跑起来了”结果一测夜间批量结算账单就报OptimisticLockException一压测床位并发抢占就出现重复分配一导出万级病历PDF就OOM。这不是代码写得少而是没吃透医疗场景下“状态机驱动”和“领域事件沉淀”的设计逻辑。它适合两类人一是正准备Java开发岗面试、需要拿一个有业务纵深感的项目讲清楚技术选型理由的求职者二是刚接手医院信息化模块、急需理解“为什么HIS系统里一张住院证要拆成6张表关联”的一线工程师。别急着解压编译——先看清这张网的结点在哪。2. 从源码结构反推医疗业务域模型为什么Patient和InpatientRecord必须分离这个ZIP包解压后典型的目录结构是hospital-system/ ├── src/main/java/com/hospital/ │ ├── controller/ # Spring MVC层 │ ├── service/ # 业务逻辑层重点 │ ├── dao/ # 数据访问层含MyBatis XML映射 │ └── entity/ # 实体类注意命名 ├── src/main/resources/ │ ├── mapper/ # MyBatis SQL映射文件 │ └── application.yml # 配置重点关注数据库连接池参数 └── pom.xml # 依赖清单锁定Spring Boot 2.7.x MyBatis 3.4.x组合提示不要一上来就跑mvn spring-boot:run。先打开entity/目录你会看到Patient.java和InpatientRecord.java两个核心类——这是整个系统建模的起点。2.1 Patient与InpatientRecord的语义切割一次住院≠一个患者很多新手会把患者信息和住院记录揉进一个Patient实体导致后续所有操作都带“当前住院状态”判断。而本项目严格分离二者原因直指医疗合规性Patient代表自然人身份字段如idCardNo身份证号、name、gender、birthDate全生命周期唯一且不可变InpatientRecord代表一次住院行为字段如admissionTime、dischargeTime、bedNo、wardId、inpatientStatus住院中/已出院/转科可多次创建状态严格受控。这种分离直接决定了数据库设计patient表主键为idinpatient_record表外键patient_id指向它且inpatient_record.bed_no需配合ward_bed表做唯一性校验避免同一床位被两个住院记录同时占用。2.2 看懂service层的事务边界为什么admitPatient()方法上加了Transactional(rollbackFor Exception.class)打开service/InpatientService.java找到admitPatient()方法。它不是简单插入一条记录而是原子性完成5件事校验患者身份证号是否存在查patient表校验目标病区床位是否空闲查ward_bed表inpatient_record表联合判断插入新inpatient_record记录更新ward_bed表中该床位状态为“占用”记录operation_log日志含操作人、时间、关键字段快照。Transactional(rollbackFor Exception.class) public boolean admitPatient(AdmissionRequest request) { // 步骤1查患者 Patient patient patientMapper.selectById(request.getPatientId()); if (patient null) { throw new BusinessException(患者不存在); } // 步骤2查床位关键此处用SELECT FOR UPDATE锁住床位行 WardBed wardBed wardBedMapper.selectForUpdateByBedNo(request.getBedNo(), request.getWardId()); if (wardBed null || !FREE.equals(wardBed.getStatus())) { throw new BusinessException(床位不可用); } // 步骤3插入住院记录 InpatientRecord record new InpatientRecord(); record.setPatientId(patient.getId()); record.setBedNo(request.getBedNo()); record.setWardId(request.getWardId()); record.setAdmissionTime(new Date()); record.setInpatientStatus(ADMITTED); inpatientRecordMapper.insert(record); // 步骤4更新床位状态 wardBed.setStatus(OCCUPIED); wardBedMapper.updateById(wardBed); // 步骤5记日志异步不必须同步否则事务回滚时日志已写入 OperationLog log new OperationLog(); log.setOperatorId(SecurityContext.getUserId()); log.setOperationType(ADMIT); log.setTargetId(record.getId().toString()); log.setDetail(患者 patient.getName() 入住 request.getBedNo()); operationLogMapper.insert(log); return true; }参数说明Transactional(rollbackFor Exception.class)是硬性要求。因为医疗业务中任何一步失败如床位被抢、患者信息异常都必须让前面所有DB操作回滚否则会出现“记录已建但床位未锁”这种致命不一致。SELECT FOR UPDATE的使用是并发安全的核心——它在查询时对ward_bed表对应行加行锁阻塞其他事务对该床位的修改直到本事务提交或回滚。2.3 DAO层的MyBatis陷阱为什么inpatient_record的XML映射里有selectKey打开mapper/InpatientRecordMapper.xml你会看到插入语句中嵌套了selectKeyinsert idinsert parameterTypecom.hospital.entity.InpatientRecord selectKey keyPropertyid resultTypejava.lang.Long orderBEFORE SELECT NEXTVAL(seq_inpatient_record_id) /selectKey INSERT INTO inpatient_record ( id, patient_id, bed_no, ward_id, admission_time, inpatient_status ) VALUES ( #{id}, #{patientId}, #{bedNo}, #{wardId}, #{admissionTime}, #{inpatientStatus} ) /insert逻辑说明这里没用MySQL的AUTO_INCREMENT而是显式调用序列seq_inpatient_record_id。原因在于医院系统常需跨库同步如主库写、从库读报表自增ID在主从复制中可能产生间隙或冲突。序列由数据库统一管理更可控。orderBEFORE确保ID在INSERT前生成并注入到实体对象后续业务逻辑如日志记录、消息发送能直接使用record.getId()。3. 数据库设计暗线三张表如何守住“床位不重叠”底线医疗系统最怕的不是功能缺失而是数据逻辑矛盾。比如同一张床位在同一时段被分配给两个患者。本项目用三张表数据库约束应用层校验三层防护我们逐层拆解。3.1ward_bed表物理床位的静态档案CREATE TABLE ward_bed ( id BIGINT PRIMARY KEY, ward_id BIGINT NOT NULL, -- 所属病区 bed_no VARCHAR(20) NOT NULL, -- 床位号如A-101 status VARCHAR(20) DEFAULT FREE, -- FREE/OCCUPIED/MAINTENANCE create_time TIMESTAMP DEFAULT NOW(), UNIQUE KEY uk_ward_bed (ward_id, bed_no) -- 同一病区下床位号唯一 );关键点UNIQUE KEY uk_ward_bed (ward_id, bed_no)强制同一病区不能有重复床位号这是数据底座。3.2inpatient_record表住院行为的时间切片CREATE TABLE inpatient_record ( id BIGINT PRIMARY KEY, patient_id BIGINT NOT NULL, bed_no VARCHAR(20) NOT NULL, ward_id BIGINT NOT NULL, admission_time DATETIME NOT NULL, -- 入院时间 discharge_time DATETIME NULL, -- 出院时间NULL表示未出院 inpatient_status VARCHAR(20) NOT NULL -- ADMITTED/DISCHARGED/TRANSFERRED );关键点admission_time和discharge_time构成时间区间。判断床位是否空闲本质是判断新区间是否与历史区间重叠。3.3 核心校验SQL用数据库原生能力拦截重叠分配wardBedMapper.xml中有一个关键查询select idcheckBedConflict resultTypejava.lang.Integer SELECT COUNT(*) FROM inpatient_record WHERE bed_no #{bedNo} AND ward_id #{wardId} AND inpatient_status IN (ADMITTED, TRANSFERRED) AND ( (admission_time #{admissionTime} AND (discharge_time IS NULL OR discharge_time #{admissionTime})) OR (admission_time #{dischargeTime} AND (discharge_time IS NULL OR discharge_time #{admissionTime})) ) /select逻辑说明此SQL检查目标床位在admissionTime时刻是否已被占用。条件(admission_time #{admissionTime} AND (discharge_time IS NULL OR discharge_time #{admissionTime}))意为“存在一条记录其入院时间早于等于当前入院时间且出院时间晚于当前入院时间或尚未出院”。这是经典的区间重叠判定公式max(start1, start2) min(end1, end2)的变形。应用层调用此SQL返回0才允许分配否则抛业务异常。参数说明#{admissionTime}和#{dischargeTime}是传入的待分配时间段。注意discharge_time IS NULL代表“仍在住院”必须纳入判断。4. 避坑住院管理系统上线前必踩的5个深坑及血泪解法这5个问题我在3家三甲医院HIS对接中反复遇到不是文档里写的“常见问题”而是线上真会崩的玄学现场。每个都按「现象 → 原因 → 解决」给出可抄作业的方案。4.1 现象患者办理出院时系统卡死在“结算费用”按钮后台日志疯狂打印Deadlock found when trying to get lock原因dischargePatient()方法中先更新inpatient_record状态为DISCHARGED再插入billing_record最后更新ward_bed状态为FREE。当两个患者几乎同时出院且分到同一病区不同床位时事务A锁record_A→bed_A事务B锁record_B→bed_B若A又去查bed_B状态校验逻辑残留B又查bed_A形成AB-BA死锁。解决删除所有“先查床位再更新”的冗余校验ward_bed状态变更只由admit/discharge流程触发无需二次校验统一事务内DML顺序永远先操作inpatient_record再操作ward_bed最后操作billing_record在application.yml中配置spring.datasource.hikari.connection-timeout30000让死锁检测快速失败而非卡死。4.2 现象夜间批量生成费用账单时部分患者账单金额为0但医嘱执行记录明明存在原因BillingService.generateDailyBill()方法用SELECT * FROM medical_order WHERE execute_date ?查当日医嘱但execute_date字段是DATE类型而数据库中存储的是DATETIME如2023-10-01 14:22:05WHERE execute_date 2023-10-01无法匹配。解决// 错误写法隐式类型转换索引失效 QueryWrapperMedicalOrder wrapper new QueryWrapper(); wrapper.eq(execute_date, LocalDate.now()); // 正确写法用BETWEEN保证索引走 LocalDateTime start LocalDateTime.of(LocalDate.now(), LocalTime.MIN); LocalDateTime end LocalDateTime.of(LocalDate.now(), LocalTime.MAX); wrapper.between(execute_time, start, end); // 注意字段名是execute_time4.3 现象护士站PC端点击“转科”后患者在新病区床位列表里找不到但原病区床位仍显示“占用”原因transferPatient()方法中更新inpatient_record.ward_id和inpatient_record.bed_no后未同步更新ward_bed表中旧床位状态应设为FREE和新床位状态应设为OCCUPIED。解决// 必须在同一个事务内完成四步 // 1. 更新住院记录的新病区/床位 record.setWardId(newWardId); record.setBedNo(newBedNo); inpatientRecordMapper.updateById(record); // 2. 将原床位状态改为FREE注意原床位号需从老记录中查出 WardBed oldBed wardBedMapper.selectByBedNoAndWardId(oldBedNo, oldWardId); oldBed.setStatus(FREE); wardBedMapper.updateById(oldBed); // 3. 将新床位状态改为OCCUPIED WardBed newBed wardBedMapper.selectByBedNoAndWardId(newBedNo, newWardId); newBed.setStatus(OCCUPIED); wardBedMapper.updateById(newBed); // 4. 记录转科日志含新旧病区/床位4.4 现象导出Excel患者列表时中文全部变成??但数据库里存的是正常汉字原因pom.xml中poi依赖版本过低如3.17不支持UTF-8编码的Excel导出且application.yml未配置JDBC URL的useUnicodetruecharacterEncodingUTF-8。解决升级POIdependency groupIdorg.apache.poi/groupId artifactIdpoi-ooxml/artifactId version5.2.4/version !-- 必须5.0.0 -- /dependency修改JDBC URLspring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingUTF-8serverTimezoneAsia/Shanghai4.5 现象患者家属用手机微信扫码查看住院信息页面加载极慢Chrome DevTools显示GET /api/patient/detail?id123耗时8s原因PatientController.getPatientDetail()方法中为拼装完整信息依次调用查患者基本信息 → 查住院记录 → 查费用明细 → 查医嘱列表 → 查检验报告 → 查护理记录6次独立SQL且未加索引。解决建立复合索引ALTER TABLE inpatient_record ADD INDEX idx_patient_status (patient_id, inpatient_status);改用MyBatisSelect注解写单条JOIN SQL一次查出患者最新住院记录费用总额Select(SELECT p.*, r.id as record_id, r.bed_no, r.admission_time, COALESCE(b.total_amount, 0) as total_bill FROM patient p LEFT JOIN inpatient_record r ON p.id r.patient_id AND r.inpatient_status ADMITTED LEFT JOIN (SELECT patient_id, SUM(amount) as total_amount FROM billing_record GROUP BY patient_id) b ON p.id b.patient_id WHERE p.id #{patientId}) PatientDetailVO getPatientDetail(Param(patientId) Long patientId);5. 进阶验证用JUnit5H2内存库做“床位分配”核心逻辑的白盒测试光跑通前端不算数。真正的工程能力体现在你能把admitPatient()这个方法抽出来在无Web容器、无真实数据库的情况下用10行代码证明它在并发下不会分配重复床位。这才是Java工程师该有的肌肉记忆。5.1 搭建H2内存库测试环境3步搞定在test/java/下新建包com.hospital.service添加测试类SpringBootTest(classes {TestConfig.class}) // 指向自定义测试配置 class InpatientServiceTest { Autowired private InpatientService inpatientService; Autowired private WardBedMapper wardBedMapper; Test void testConcurrentAdmitSameBed() throws InterruptedException { // 准备插入一个空闲床位 WardBed bed new WardBed(); bed.setWardId(1L); bed.setBedNo(A-101); bed.setStatus(FREE); wardBedMapper.insert(bed); // 模拟10个线程同时申请同一床位 int threadCount 10; CountDownLatch latch new CountDownLatch(threadCount); AtomicInteger successCount new AtomicInteger(0); AtomicInteger failCount new AtomicInteger(0); for (int i 0; i threadCount; i) { new Thread(() - { try { AdmissionRequest request new AdmissionRequest(); request.setPatientId(1001L); request.setWardId(1L); request.setBedNo(A-101); boolean result inpatientService.admitPatient(request); if (result) { successCount.incrementAndGet(); } else { failCount.incrementAndGet(); } } catch (Exception e) { failCount.incrementAndGet(); } finally { latch.countDown(); } }).start(); } latch.await(); // 等待所有线程结束 // 断言成功数必须为1只有一个能抢到其余9个必须失败 assertEquals(1, successCount.get(), 并发分配同一床位成功数应为1); assertEquals(9, failCount.get(), 并发分配同一床位失败数应为9); } }关键配置TestConfig.java内容如下强制使用H2内存库并初始化表结构Configuration MapperScan(com.hospital.dao) public class TestConfig { Bean Primary public DataSource dataSource() { HikariConfig config new HikariConfig(); config.setJdbcUrl(jdbc:h2:mem:testdb;DB_CLOSE_DELAY-1;DB_CLOSE_ON_EXITFALSE); config.setUsername(sa); config.setPassword(); config.setDriverClassName(org.h2.Driver); return new HikariDataSource(config); } Bean public JdbcTemplate jdbcTemplate(DataSource dataSource) { return new JdbcTemplate(dataSource); } Bean public PlatformTransactionManager transactionManager(DataSource dataSource) { return new DataSourceTransactionManager(dataSource); } }5.2 测试通过后下一步必须做的3件事把testConcurrentAdmitSameBed()方法名改成testConcurrentAdmitSameBed_ShouldOnlyOneSucceedJUnit5要求测试方法名体现行为意图这是团队协作的基本素养在CI流水线如GitHub Actions中加入此测试在.github/workflows/test.yml中添加- run: ./mvnw test -DtestInpatientServiceTest确保每次PR都过此关把测试覆盖率报告接入SonarQube在pom.xml中添加JaCoCo插件确保InpatientService.admitPatient()方法行覆盖率达100%分支覆盖率达100%if/else、try/catch每条路径都测到。我带过的团队里凡是能把这个测试写出来并跑通的新人三个月内都能独立负责住院模块迭代。因为它逼你搞懂事务隔离级别怎么设、数据库锁怎么触发、并发场景怎么模拟、失败路径怎么覆盖。这些不是面试题答案是每天上线前你亲手签下的责任状。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询