
简介这是一套面向Java开发者与医疗信息化学习者的医院管理系统完整源码围绕挂号、门诊、药房、住院等核心业务场景提供了一套可运行、可二次开发的信息化解决方案。系统采用Java技术栈构建涵盖后端服务、数据访问与权限控制等典型企业级设计适合用于课程设计、毕业设计或医疗业务逻辑的学习研究。资源包共1236个文件约8.91MB其中791个java源文件构成主要业务代码106个xml与17个yml负责配置与映射84个js、37个html及18个css支撑前端页面另有sql脚本、dockerfile、properties等部署与数据文件目录结构完整。目前已有681人学习下载。通过研读这套源码读者可以掌握挂号并发控制、电子病历、药品库存与住院费用统计等模块的实现思路理解医疗数据的表结构设计与接口集成方式并借鉴其权限控制与系统测试经验为自身项目开发提供可复用的参考。1. 从一份 Java 医院管理系统源码说起挂号、门诊、药房、住院到底怎么串起来很多同学拿到“医院管理系统源码数据库”这类资源第一反应是先把项目跑起来看界面结果卡在数据库连不上、挂号编号重复、药房扣库存对不上账。问题不在代码写得多烂而在于没先想清楚业务链路一个患者从挂号开始到门诊开方、药房发药、必要时转住院这条链上每一步都在改同一批数据谁先改、谁锁行、谁回滚决定了系统能不能用。这套系统本质是一个典型的 Java Web 单体应用常见做法是 Spring Boot MyBatis MySQL前端用 Thymeleaf 或 Vue 都行。它要解决的核心不是“增删改查”而是状态流转挂号单从“待就诊”到“已就诊”处方从“待发药”到“已发药”床位从“空闲”到“占用”再到“出院释放”。适合拿来做课程设计、毕业设计也适合工作两三年的人拿来练手事务和并发。2. 医院管理系统数据库表结构设计与挂号主键生成2.1 挂号、门诊、药房、住院四张核心表怎么拆先把表拆清楚后面写代码才不会乱。我一般会保留这几张主表字段只列关键的表名作用关键字段patient患者档案id、name、id_card、phoneregistration挂号单id、patient_id、dept_id、doctor_id、status、reg_noprescription处方主表id、reg_id、doctor_id、status、total_feeprescription_item处方明细id、prescription_id、drug_id、qtydrug药品库存id、name、stock、price、versionadmission住院单id、patient_id、bed_id、status、in_time、out_timebed床位id、ward_id、bed_no、status拆表的原则是挂号单和处方一对多处方和明细一对多住院单和床位一对一。不要把药品库存直接塞进处方明细里否则退药时库存回滚会非常难写。2.2 挂号编号生成别用自增 ID 当业务号挂号单对外要展示一个reg_no常见格式是GH202405200001。很多人图省事直接用自增主键结果换库或分表就崩。我一般用“日期 当日序号”的方式配合数据库唯一索引兜底CREATE TABLE registration ( id BIGINT PRIMARY KEY AUTO_INCREMENT, reg_no VARCHAR(32) NOT NULL, patient_id BIGINT NOT NULL, dept_id INT NOT NULL, doctor_id BIGINT, status TINYINT DEFAULT 0 COMMENT 0待就诊 1已就诊 2已取消, create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_reg_no (reg_no), KEY idx_patient (patient_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;uk_reg_no这个唯一索引是关键它保证即使并发下生成了相同编号数据库也会拒绝第二条应用层捕获异常后重试即可。status用 TINYINT 而不是字符串查询和索引都更省。2.3 用 MyBatis 写挂号插入并处理并发冲突生成编号的 Java 代码我一般放在 Service 层用“查当日最大序号 1”再插入失败重试三次public String createRegistration(Long patientId, Integer deptId) { for (int i 0; i 3; i) { String regNo buildRegNo(); // GH yyyyMMdd 4位序号 try { Registration reg new Registration(); reg.setRegNo(regNo); reg.setPatientId(patientId); reg.setDeptId(deptId); reg.setStatus(0); registrationMapper.insert(reg); return regNo; } catch (DuplicateKeyException e) { // 唯一索引冲突说明序号被抢重试 } } throw new RuntimeException(挂号编号生成失败请重试); }buildRegNo()里查SELECT MAX(reg_no) FROM registration WHERE reg_no LIKE GH20240520%取后四位加一。注意这里不要加锁靠唯一索引重试比锁表更轻。DuplicateKeyException是 Spring 对SQLIntegrityConstraintViolationException的封装捕获它比捕获Exception精准。3. 门诊开方与药房扣库存的事务控制3.1 处方提交为什么要放在一个事务里门诊医生开完处方点“提交”背后要做三件事写prescription主表、写prescription_item明细、扣drug库存。这三步必须原子否则会出现“处方有了但库存没扣”或者“库存扣了但处方没存”。Spring 的Transactional直接标在 Service 方法上Transactional(rollbackFor Exception.class) public void submitPrescription(Long regId, ListPrescriptionItem items) { Prescription p new Prescription(); p.setRegId(regId); p.setStatus(0); // 待发药 prescriptionMapper.insert(p); for (PrescriptionItem item : items) { item.setPrescriptionId(p.getId()); prescriptionItemMapper.insert(item); // 扣库存带乐观锁 int rows drugMapper.reduceStock(item.getDrugId(), item.getQty()); if (rows 0) { throw new RuntimeException(药品库存不足: item.getDrugId()); } } }rollbackFor Exception.class很重要默认只回滚运行时异常检查异常不会回滚。reduceStock用乐观锁版本号UPDATE drug SET stock stock - #{qty}, version version 1 WHERE id #{drugId} AND stock #{qty} AND version #{version}stock #{qty}这个条件防止超卖version防止并发覆盖。返回影响行数为 0 就抛异常整个事务回滚。3.2 药房发药时怎么保证不重复扣药房发药是另一个状态流转处方从“待发药”变“已发药”。这里最容易出的 bug 是重复发药患者拿着同一张处方跑两次药房。解决办法是在更新状态时加条件UPDATE prescription SET status 1, dispense_time NOW() WHERE id #{id} AND status 0只有status 0时才更新返回 0 行说明已经被发过了直接提示“该处方已发药”。这个技巧叫状态机更新比先查再改安全得多因为查和改之间可能有并发。3.3 药品库存的 version 字段怎么用version字段不是必须的如果并发量不大直接用stock qty条件更新就够了。但如果有多个药房窗口同时扣同一批药乐观锁能避免“两个事务都读到 stock10各扣 8最后 stock-6”的情况。代价是冲突时要重试我一般让前端提示“请重试”而不是在后台无限循环。注意Transactional方法内部调用同类方法不会触发事务因为 Spring AOP 代理的是外部调用。如果submitPrescription里调了本类的另一个Transactional方法事务不会生效。4. 住院床位分配与出院结算的落地细节4.1 床位占用用状态机还是行锁住院模块的核心是床位分配。常见做法是bed表加status字段0 空闲、1 占用。分配时UPDATE bed SET status 1 WHERE id #{bedId} AND status 0返回 1 行说明抢到了返回 0 行说明被别人占了。这比SELECT ... FOR UPDATE轻因为不需要锁等待。如果医院床位紧张、并发高可以再加一个admission表的唯一索引uk_bed_active保证一个床位同时只有一条未出院记录。4.2 出院结算的费用汇总 SQL出院时要算总费用挂号费 处方费 床位费。床位费按天算用DATEDIFFSELECT (SELECT reg_fee FROM registration WHERE id #{regId}) AS reg_fee, (SELECT IFNULL(SUM(total_fee),0) FROM prescription WHERE reg_id #{regId} AND status 1) AS drug_fee, (SELECT DATEDIFF(IFNULL(out_time, NOW()), in_time) * bed_price FROM admission a JOIN bed b ON a.bed_id b.id WHERE a.id #{admissionId}) AS bed_fee三个子查询分别取挂号费、已发药处方总费、床位费。IFNULL(out_time, NOW())处理还没点出院的情况。这个 SQL 可以直接映射到一个BillVO对象不用在 Java 里拼。4.3 出院时释放床位的顺序出院操作要按顺序先更新admission的out_time和status再更新bed的status 0。顺序反了会出现“床位空了但住院单还在”的脏数据。放在同一个事务里Transactional public void discharge(Long admissionId) { admissionMapper.discharge(admissionId); // status2, out_timeNOW() Admission a admissionMapper.selectById(admissionId); bedMapper.release(a.getBedId()); // status0 }discharge的 SQL 也要带条件WHERE id #{id} AND status 1防止重复出院。5. 源码跑起来之后三个必调的参数和排错清单5.1 数据库连接池和时区参数源码里application.yml的 JDBC URL 经常少参数导致中文乱码或时间差 8 小时。我一般改成spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: your_password hikari: maximum-pool-size: 10 connection-timeout: 30000serverTimezoneAsia/Shanghai解决时间差characterEncodingutf8解决中文allowPublicKeyRetrievaltrue解决 MySQL 8 的认证报错。maximum-pool-size别设太大课程设计环境 10 够用设 100 反而容易把数据库连接打满。5.2 挂号并发测试怎么做想验证挂号编号会不会重复可以用ab或 JMeter 压一下。简单点用 Java 起 20 个线程ExecutorService pool Executors.newFixedThreadPool(20); CountDownLatch latch new CountDownLatch(20); for (int i 0; i 20; i) { pool.submit(() - { try { registrationService.createRegistration(1L, 1); } finally { latch.countDown(); } }); } latch.await();跑完查SELECT reg_no, COUNT(*) FROM registration GROUP BY reg_no HAVING COUNT(*) 1如果没有结果说明唯一索引和重试逻辑生效了。这个测试比看代码有用得多。5.3 药房扣库存为负的排查路径如果发现drug.stock出现负数按这个顺序查先看reduceStock的 SQL 有没有stock #{qty}条件再看Transactional有没有生效可以在方法里故意抛异常看库存回不回滚最后看是不是有别的入口直接UPDATE drug SET stock stock - 1绕过了 Service。常见坑是定时任务或数据初始化脚本直接改库存没走事务。提示MySQL 的SELECT ... FOR UPDATE必须在事务里才生效自动提交模式下加了等于没加。如果非要用行锁记得先SET autocommit 0或开事务。5.4 住院模块的日期计算边界DATEDIFF算床位费时同一天入院同一天出院会返回 0实际应该收一天。我一般改成DATEDIFF(out_time, in_time) 1或者用TIMESTAMPDIFF(DAY, in_time, out_time)再判断。这个细节在课程设计答辩时经常被问提前处理掉能省很多解释。本文还有配套的精品资源点击获取