ARTICLE DETAIL

资讯详情

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

Java医院管理系统源码实战:挂号门诊药房住院全链路拆解

Java医院管理系统源码实战:挂号门诊药房住院全链路拆解 简介这是一套面向Java开发者与医疗信息化学习者的医院管理系统完整源码采用Java技术栈开发覆盖挂号、门诊、药房、住院等核心业务模块适合用于课程设计、毕业设计或企业级项目练手。压缩包共1236个文件约8.91MB以791个java源码为主体辅以106个xml配置、84个js与37个html前端页面另有sql脚本、yml配置、dockerfile及properties等部署与数据文件结构完整、层次清晰。系统涉及Spring Boot、MyBatis、数据库设计、权限控制与RESTful接口等知识点可帮助读者理解医疗业务逻辑与后端架构设计。目前已有681人学习下载适合希望提升Java编程技能、拓宽行业应用场景的开发者参考借鉴。1. 从一份 Java 医院管理系统源码说起挂号、门诊、药房、住院到底怎么串起来很多做 Java 课程设计或者想练手企业级项目的朋友都会搜「医院管理系统源码」但真正拿到一份能跑起来、业务闭环完整的代码并不容易。我最近拆了一套基于 Java 的医院管理系统覆盖挂号、门诊、药房、住院四大核心模块后端是典型的 Spring Boot MyBatis 组合前端用了 AdminLTE 和 Bootstrap 做管理后台数据库走 MySQL。它解决的不是「能不能跑」的问题而是「业务链路能不能串通」——从患者在线选医生挂号到医生接诊开处方再到药房扣库存发药最后住院登记和费用统计整条数据流是打通的。这套源码适合正在做 Java 课程设计的学生、想补业务经验的初中级开发者以及需要快速搭医疗类管理后台原型的团队。下面我按实际拆解和复现的顺序把技术栈、数据库设计、模块实现和踩坑点讲清楚。2. 技术栈选型与工程结构为什么是 Spring Boot MyBatis 而不是 JPA2.1 后端框架组合的实际考量这套系统后端用 Spring Boot 做快速启动和自动配置数据访问层选的是 MyBatis 而不是 JPA。原因很直接医院管理系统的查询场景大量涉及多表关联和动态条件比如「按科室日期医生职称查排班」「按患者ID时间段查诊疗记录」MyBatis 的 XML 映射能把这些 SQL 写得很灵活而 JPA 的 Criteria API 在这种场景下反而绕。常见做法是 MyBatis-Plus 做单表 CRUD复杂查询手写 XML这套源码基本遵循这个思路。工程结构是标准的 Maven 多模块或者单模块分层# 典型目录结构 src/main/java/com/hospital/ ├── controller/ # 挂号、门诊、药房、住院各一个 Controller ├── service/ # 业务逻辑接口 impl 分离 ├── mapper/ # MyBatis Mapper 接口 ├── entity/ # 患者、医生、药品、住院记录等实体 ├── config/ # 拦截器、跨域、MyBatis 配置 └── utils/ # 日期处理、编号生成、权限工具 src/main/resources/ ├── mapper/ # MyBatis XML 映射文件 ├── static/ # AdminLTE、Bootstrap、ionicons 等前端资源 ├── templates/ # 如果用了 Thymeleaf 放页面模板 └── application.yml # 数据源、端口、MyBatis 配置前端资源里能看到bootstrap.min.css、AdminLTE.min.css、ionicons.min.css、app.css说明后台界面是基于 AdminLTE 主题改的表格、表单、弹窗这些组件直接复用省去大量 UI 工作量。nginx.conf的存在说明部署时可能用 Nginx 做静态资源服务和反向代理redis.conf暗示系统用 Redis 做缓存或会话存储logstash.conf则可能是日志收集链路的一部分。这些配置文件不一定在本地开发时全用上但说明这套源码考虑过生产部署。2.2 数据库设计与核心表结构数据库用 MySQL表结构围绕医疗业务展开。核心表包括表名用途关键字段patient患者信息id, name, id_card, phone, genderdoctor医生信息id, name, department_id, title, scheduledepartment科室id, name, locationregistration挂号记录id, patient_id, doctor_id, visit_date, statusprescription处方id, registration_id, doctor_id, create_timeprescription_detail处方明细id, prescription_id, drug_id, quantitydrug药品库存id, name, stock, price, expire_datehospitalization住院记录id, patient_id, bed_id, admit_date, discharge_dateward病房id, department_id, bed_count, occupied挂号模块的并发控制是重点。同一医生同一时间段不能重复预约常见做法是在registration表上对doctor_id visit_date time_slot建唯一索引插入时捕获唯一键冲突而不是先查再插——后者在并发下必然出问题。-- 挂号防重唯一索引 ALTER TABLE registration ADD UNIQUE KEY uk_doctor_time (doctor_id, visit_date, time_slot); -- 药品库存扣减用乐观锁版本号 ALTER TABLE drug ADD COLUMN version INT DEFAULT 0;药品库存扣减用乐观锁UPDATE drug SET stock stock - #{qty}, version version 1 WHERE id #{id} AND stock #{qty} AND version #{version}影响行数为 0 就重试或报错。这是药房模块不超卖的关键。2.3 配置文件与启动步骤拿到源码后第一步是改application.yml里的数据库连接spring: datasource: url: jdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379 database: 0 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.hospital.entity如果本地没装 Redis先把 Redis 相关配置注释掉或者把缓存实现临时切到内存否则启动会报连接拒绝。数据库脚本一般放在src/main/resources/sql/或项目根目录的db/下先建库再导入。启动命令就是标准的mvn spring-boot:run或打成 jar 后java -jar hospital.jar。默认端口 8080访问http://localhost:8080进登录页。注意导入 SQL 时如果报外键约束错误先关掉FOREIGN_KEY_CHECKS按「科室 → 医生 → 患者 → 药品 → 挂号 → 处方 → 住院」的顺序导入避免依赖表还没建就插数据。3. 挂号与门诊模块从排班查询到电子处方的完整链路3.1 挂号模块的排班查询与预约落库挂号模块的前端逻辑是患者选科室 → 选医生 → 看排班 → 选时间段 → 确认预约。后端对应两个核心接口查排班和提交挂号。查排班时医生表里通常存的是schedule字段比如 JSON 格式的每周出诊安排或者单独建doctor_schedule表按日期存。常见做法是单独建表方便按日期范围查询和动态调整CREATE TABLE doctor_schedule ( id INT PRIMARY KEY AUTO_INCREMENT, doctor_id INT NOT NULL, work_date DATE NOT NULL, time_slot VARCHAR(20) NOT NULL, -- 如 08:00-09:00 max_patients INT DEFAULT 10, booked INT DEFAULT 0, INDEX idx_doctor_date (doctor_id, work_date) );提交挂号时除了插registration表还要更新doctor_schedule的booked字段。这两步必须在同一个事务里Transactional(rollbackFor Exception.class) public Result register(RegistrationDTO dto) { // 1. 扣减排班余量用乐观锁防超卖 int updated scheduleMapper.incrementBooked(dto.getScheduleId()); if (updated 0) { return Result.fail(该时段已约满); } // 2. 插入挂号记录 Registration reg new Registration(); reg.setPatientId(dto.getPatientId()); reg.setDoctorId(dto.getDoctorId()); reg.setVisitDate(dto.getVisitDate()); reg.setTimeSlot(dto.getTimeSlot()); reg.setStatus(已预约); registrationMapper.insert(reg); return Result.success(reg.getId()); }incrementBooked的 SQL 是UPDATE doctor_schedule SET booked booked 1 WHERE id #{id} AND booked max_patients返回影响行数。这样即使两个请求同时进来也只有一个能成功另一个拿到 0 行更新直接返回约满。这是挂号模块最核心的防并发逻辑少了这一步测试时可能看不出问题一上压力就翻车。3.2 门诊接诊与电子处方生成门诊模块的流程是医生登录 → 查看今日挂号列表 → 叫号接诊 → 写病历 → 开处方 → 提交。处方提交后药房模块才能看到待发药列表。处方主表和明细表是一对多关系。开处方时前端传一个药品列表后端先插prescription拿到主键再批量插prescription_detailTransactional(rollbackFor Exception.class) public Result createPrescription(PrescriptionDTO dto) { Prescription pres new Prescription(); pres.setRegistrationId(dto.getRegistrationId()); pres.setDoctorId(dto.getDoctorId()); pres.setDiagnosis(dto.getDiagnosis()); pres.setCreateTime(new Date()); prescriptionMapper.insert(pres); for (PrescriptionItem item : dto.getItems()) { PrescriptionDetail detail new PrescriptionDetail(); detail.setPrescriptionId(pres.getId()); detail.setDrugId(item.getDrugId()); detail.setQuantity(item.getQuantity()); detail.setUsage(item.getUsage()); prescriptionDetailMapper.insert(detail); } // 更新挂号状态为已接诊 registrationMapper.updateStatus(dto.getRegistrationId(), 已接诊); return Result.success(pres.getId()); }这里有个容易忽略的点处方提交时并不扣药品库存扣库存发生在药房发药环节。如果门诊开处方就扣库存患者没去拿药库存就白白少了。所以药房模块的「发药」操作才是库存扣减的触发点。3.3 门诊模块的权限与数据隔离医生只能看到自己的挂号列表和患者病历不能查其他医生的。常见做法是在 Controller 层从 Session 或 Token 里取当前医生 ID拼到查询条件里GetMapping(/today) public Result todayList(HttpServletRequest request) { Integer doctorId (Integer) request.getSession().getAttribute(doctorId); if (doctorId null) { return Result.fail(未登录); } ListRegistrationVO list registrationService.listByDoctorAndDate(doctorId, new Date()); return Result.success(list); }如果系统用了 Spring Security 或 Shiro可以在拦截器里统一做角色判断但数据级隔离只能看自己的还是得在 SQL 的WHERE里加doctor_id #{doctorId}。这一点在测试时容易被忽略因为管理员账号能看到全部一切正常换成医生账号才发现越权。提示如果接诊列表查出来是空先确认registration表的visit_date是不是当天以及status是不是「已预约」。很多新手导入测试数据时日期写的是过去医生登录自然看不到。4. 药房与住院模块库存扣减、床位分配和费用统计4.1 药房发药与库存扣减的原子操作药房模块的核心是「发药」动作药师看到待发药处方列表点击发药系统扣减药品库存并记录发药日志。库存扣减必须和发药记录插入在同一事务且用乐观锁或stock quantity条件防止超卖。Transactional(rollbackFor Exception.class) public Result dispense(Integer prescriptionId) { ListPrescriptionDetail details detailMapper.listByPrescriptionId(prescriptionId); for (PrescriptionDetail d : details) { // 条件更新库存足够才扣 int rows drugMapper.reduceStock(d.getDrugId(), d.getQuantity()); if (rows 0) { throw new BusinessException(药品[ d.getDrugId() ]库存不足); } } // 记录发药状态 prescriptionMapper.updateStatus(prescriptionId, 已发药); dispenseLogMapper.insert(new DispenseLog(prescriptionId, new Date())); return Result.success(); }reduceStock的 SQLUPDATE drug SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}。只要有一味药库存不够整个事务回滚不会出现「扣了一半」的情况。药房模块还常见过期预警可以加一个定时任务或查询条件WHERE expire_date DATE_ADD(NOW(), INTERVAL 30 DAY)把临期药品列出来。4.2 住院登记与床位分配住院模块的流程是医生开住院单 → 护士站登记 → 分配床位 → 记录治疗和费用 → 出院结算。床位分配要防止同一床位被两个患者同时占用做法和挂号类似在ward表上用条件更新-- 床位占用只有空闲才能分配 UPDATE ward SET occupied occupied 1 WHERE id #{wardId} AND occupied bed_count; -- 住院记录插入 INSERT INTO hospitalization (patient_id, ward_id, bed_no, admit_date, status) VALUES (#{patientId}, #{wardId}, #{bedNo}, NOW(), 在院);出院时反向操作更新住院记录状态为「已出院」ward.occupied减一。费用统计通常按住院天数 × 床位费 药品费 诊疗费汇总可以在出院时生成一条费用汇总记录也可以实时查询。常见做法是实时查询避免数据不一致。4.3 住院费用统计的 SQL 实现费用统计涉及多表关联用 MyBatis XML 写比较清晰select idsumCostByHospitalization resultTypejava.math.BigDecimal SELECT IFNULL(SUM(d.price * pd.quantity), 0) AS drug_cost FROM prescription p JOIN prescription_detail pd ON p.id pd.prescription_id JOIN drug d ON pd.drug_id d.id WHERE p.registration_id IN ( SELECT id FROM registration WHERE patient_id #{patientId} ) /select床位费按DATEDIFF(discharge_date, admit_date) * bed_price算诊疗费如果有单独表就再关联。这里容易出的问题是时间范围没对齐——门诊处方和住院处方混在一起算。如果系统里门诊和住院共用prescription表需要加一个type字段区分统计时带上条件。注意住院模块的日期计算用DATEDIFF时如果discharge_date为空还在院要先用IFNULL(discharge_date, NOW())处理否则统计结果会是 NULL。5. 避坑与排查这套源码跑起来最容易卡的五个地方5.1 启动报 Redis 连接失败现象mvn spring-boot:run启动时抛RedisConnectionFailureException应用起不来。原因application.yml里配了 Redis但本地没启动 Redis 服务或者端口不对。解决本地装一个 Redis 并启动或者把 Redis 相关配置注释掉把缓存实现临时换成内存。如果代码里用了Cacheable可以先把注解注释确认业务能跑通再补缓存。5.2 挂号提交时报唯一键冲突现象同一患者重复提交挂号或者并发测试时部分请求报Duplicate entry。原因registration表建了doctor_id visit_date time_slot唯一索引重复插入被数据库拦截。解决这是预期行为前端应该捕获这个异常并提示「该时段已约满」。如果测试时不想被拦截先删掉唯一索引但生产环境必须保留。更好的做法是在 Service 层先查再插但并发下仍可能冲突所以唯一索引是最后一道防线。5.3 药房发药后库存变成负数现象发药操作后查drug表stock出现负数。原因扣库存的 SQL 没加stock #{qty}条件或者用了「先查再扣」的写法并发下两个请求都查到库存足够然后都扣。解决改成条件更新UPDATE drug SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}检查影响行数。如果已经是负数手动修正数据然后补上条件。5.4 医生登录后看不到挂号列表现象医生账号登录今日挂号列表为空但管理员账号能看到。原因查询条件里带了doctor_id 当前登录医生但测试数据的doctor_id和登录账号的doctor_id对不上或者visit_date不是当天。解决查一下registration表里doctor_id和visit_date的实际值确认和登录医生匹配。导入测试数据时日期用CURDATE()而不是写死的过去日期。5.5 前端页面样式丢失或 404现象登录页能打开但 CSS 没加载页面排版全乱。原因static/目录下的 AdminLTE、Bootstrap 资源路径不对或者 Nginx 配置的静态资源根目录和实际不符。解决检查 HTML 里引用的路径是/css/bootstrap.min.css还是/static/css/...和 Spring Boot 的静态资源映射规则对齐。如果用 Nginx确认root或alias指向src/main/resources/static或打包后的static目录。6. 进阶技巧用 Playwright 做挂号流程的自动化回归这套系统功能模块多每次改完代码手动点一遍挂号、门诊、药房、住院很费时间。我一般会写一个 Playwright 脚本做冒烟测试覆盖「登录 → 挂号 → 医生接诊 → 开处方 → 药房发药」这条主链路。Playwright 支持 Java和项目技术栈一致不用额外装 Node。// Playwright for Java 冒烟测试示例 import com.microsoft.playwright.*; public class HospitalSmokeTest { public static void main(String[] args) { try (Playwright playwright Playwright.create()) { Browser browser playwright.chromium().launch( new BrowserType.LaunchOptions().setHeadless(false)); Page page browser.newPage(); // 1. 登录 page.navigate(http://localhost:8080/login); page.fill(#username, admin); page.fill(#password, admin123); page.click(#loginBtn); page.waitForURL(**/index); // 2. 挂号 page.click(text挂号管理); page.click(text新增挂号); page.selectOption(#department, 内科); page.selectOption(#doctor, 张医生); page.fill(#visitDate, 2025-06-01); page.click(#submitReg); page.waitForSelector(.success-msg); // 3. 药房发药 page.click(text药房管理); page.click(text待发药); page.click(tr:first-child .dispense-btn); page.waitForSelector(.dispense-success); System.out.println(主链路冒烟通过); browser.close(); } } }这段脚本的关键是选择器要和页面元素 ID 对齐。如果页面用的是 AdminLTE 的表格tr:first-child .dispense-btn能定位到第一行的发药按钮。跑之前确保数据库里有测试数据否则挂号那一步会因为没排班而失败。我一般会在测试前用 SQL 重置数据保证每次跑的环境一致。参数方面setHeadless(false)是方便调试时看浏览器操作CI 环境改成true。waitForURL和waitForSelector是必须的否则页面还没加载完就点下一步脚本会随机失败——这种「玄学」问题多半是等待没做好。从那以后我每次改完挂号或药房模块都强制走一遍这个冒烟脚本确认主链路没断再提交。希望帮到你。本文还有配套的精品资源点击获取
返回列表