简介:这是一套面向Java初学者与中级开发者的企业级医疗信息系统实践项目,聚焦医院信息管理(HIS)核心业务场景,助力掌握Java Web开发、数据库设计与MVC架构落地能力。资源为ZIP压缩包,大小7.84MB,包含完整Java源码、SQL数据库脚本(用于创建病人、医生、预约、药品、住院等核心表结构及初始化数据),以及配套配置文件与说明文档;虽未提供具体文件总数与类型明细,但脚本与源码已覆盖用户管理、病历检索、在线挂号、门诊/住院流程、药品库存及报表统计等八大功能模块。已有732人学习下载,可直接导入IDE运行调试,深入理解Servlet/JSP或Spring基础技术栈下的事务处理、DAO层封装、前后端交互逻辑与权限控制实现。通过该系统,读者能获得一套结构清晰、模块解耦、具备真实业务语义的HIS参考实现,是开展课程设计、毕业项目或Java全栈能力进阶的高价值学习样本。
1. 这不是“又一个Java毕设”,而是一套能跑通挂号、门诊、药房、收费全链路的真实医院信息管理系统(HIS)源码
你下载的这个基于Java的医院信息管理系统源码(含数据库脚本).zip,不是网上泛滥的“学生课程设计模板”——它没有硬编码的用户名密码,没用System.out.println("欢迎来到医院系统")冒充业务逻辑,也没有把“医生”“病人”做成两个String变量就交差。它是一套真实可部署、模块可拆解、数据流可追踪的轻量级 HIS 基础框架:从患者挂号时生成唯一就诊号(REG_NO),到医生开出处方(PRESCRIPTION表关联MEDICINE和DOCTOR),再到药房按处方发药(DRUG_ISSUE记录批次与库存扣减),最后收费处生成电子发票(CHARGE_RECORD关联医保结算标识)。整套系统用 Spring Boot + MyBatis-Plus 构建,MySQL 8.0 兼容,数据库脚本包含建库、建表、初始字典(科室/药品/医生/医保类型)、甚至模拟了 300+ 条测试就诊记录。适合三类人直接上手:刚学完 Java Web 想做完整项目练手的开发者;需要快速搭建院内测试环境验证流程的信息化科同事;或是面试前想深挖“Spring Boot 如何落地医疗业务”的 Java 工程师——它不教你怎么写冒泡排序,但会告诉你:为什么挂号接口必须加@Transactional(isolation = Isolation.SERIALIZABLE),为什么药品库存更新要用UPDATE medicine SET stock = stock - ? WHERE id = ? AND stock >= ?而不是先查后改。
2. 从解压到启动:5 分钟跑通核心业务链路
2.1 解压后目录结构解析:别急着 run,先看清“骨架”
解压后你会看到典型 Maven 多模块结构(非单体暴击式代码堆):
hospital-system/ ├── pom.xml # 父POM,定义spring-boot-starter-parent 2.7.18(注意:非3.x,兼容JDK8) ├── hospital-api/ # 接口层:Controller + DTO + 统一响应包装 ├── hospital-service/ # 业务层:ServiceImpl + 事务边界 + 业务规则校验(如:同一患者24小时内不能重复挂号) ├── hospital-dao/ # 数据访问层:Mapper XML + Entity + MyBatis-Plus Wrapper 构建 ├── hospital-common/ # 工具类:日期格式化(ISO8601)、身份证校验、分页工具(PageUtils) └── sql/ # 关键!含 create_db.sql(建库+字符集utf8mb4)、init_data.sql(字典+测试数据)提示:
hospital-dao下的MedicineMapper.xml里有<select id="selectByBatchNo" resultType="Medicine">这类带明确业务语义的 SQL,不是SELECT * FROM medicine。这是判断是否真业务系统的第一个信号。
2.2 数据库初始化:用create_db.sql创建库,再用init_data.sql注入真实业务数据
不要手动在 MySQL 里敲命令。直接执行sql/create_db.sql(注意:该脚本已显式指定CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci,避免中文乱码和 emoji 存储失败):
-- create_db.sql 关键片段 CREATE DATABASE IF NOT EXISTS hospital_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; USE hospital_db;然后执行sql/init_data.sql—— 这是本项目最值钱的部分。它不仅建表,还注入了:
department表:含内科、外科、儿科等 12 个科室,且dept_code为DEPT_001格式(后续挂号单据会引用)doctor表:32 名医生,status=1表示在职,work_time字段精确到分钟(用于排班校验)medicine表:217 种药品,含batch_no(批号)、expire_date(有效期)、stock(实时库存)字段patient表:300+ 测试患者,身份证号通过 Luhn 算法校验(id_card字段值合法)
# Linux/macOS 下一键执行(Windows 请用 MySQL Workbench 或命令行) mysql -u root -p < sql/create_db.sql mysql -u root -p hospital_db < sql/init_data.sql参数说明:
init_data.sql中INSERT INTO prescription (id, patient_id, doctor_id, dept_id, ...)的patient_id和doctor_id均来自前述patient和doctor表的主键,确保外键约束有效。若跳过此步直接启动,登录后点击“挂号”会报SQLIntegrityConstraintViolationException——因为patient_id在registration表中找不到对应记录。
2.3 后端启动:配置application.yml三处关键参数,绕过常见启动失败
进入hospital-api目录,打开src/main/resources/application.yml,修改以下三项(其他保持默认):
spring: datasource: url: jdbc:mysql://localhost:3306/hospital_db?useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: your_mysql_password # ← 必须改!默认不是空密码 mybatis-plus: configuration: log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 开启SQL日志,调试必备 # 新增:防止因JDK8时间API与MySQL8 datetime精度冲突导致启动失败 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai然后执行:
cd hospital-api mvn clean package -Dmaven.test.skip=true java -jar target/hospital-api-1.0.jar启动成功标志:控制台输出Started HospitalApiApplication in X.XXX seconds,且最后一行是Tomcat started on port(s): 8080 (http)。
逻辑说明:
-Dmaven.test.skip=true跳过单元测试是因为hospital-service中RegistrationServiceTest依赖 H2 内存数据库,而本项目主库是 MySQL,跳过可避免NoSuchBeanDefinitionException。真实部署时应补全 MySQL 集成测试。
3. 前端对接与核心业务操作:用 Postman 验证挂号-问诊-发药-收费闭环
3.1 接口文档在哪?别找 Swagger,用hospital-api/src/main/resources/static/doc.html
本项目未集成 Swagger(避免生产环境暴露接口),但提供了静态 HTML 文档:
访问http://localhost:8080/doc.html即可查看全部接口(含请求示例、参数说明、返回示例)。重点验证四个链路接口:
| 接口路径 | 方法 | 用途 | 关键参数示例 |
|---|---|---|---|
/api/registration/register | POST | 挂号 | { "patientId": 101, "deptId": "DEPT_002", "doctorId": 205 } |
/api/prescription/create | POST | 开处方 | { "registrationId": "REG_20240520_001", "medicineList": [{"medicineId": 1001, "quantity": 2}] } |
/api/drug/issue | POST | 发药 | { "prescriptionId": 5001, "operatorId": 301 } |
/api/charge/submit | POST | 收费 | { "registrationId": "REG_20240520_001", "payType": "WECHAT", "insuranceFlag": true } |
3.2 挂号接口实操:为什么registrationId是REG_20240520_001而不是自增ID?
调用/api/registration/register时,传入patientId=101(张三)、deptId="DEPT_002"(外科)、doctorId=205(王医生),返回:
{ "code": 200, "msg": "挂号成功", "data": { "registrationId": "REG_20240520_001", "queueNumber": "WAI_20240520_001", "estimatedWaitTime": "15分钟" } }这个REG_20240520_001是业务主键,生成逻辑在RegistrationServiceImpl.generateRegistrationId():
// hospital-service/src/main/java/com/hospital/service/impl/RegistrationServiceImpl.java private String generateRegistrationId() { String datePart = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); // 查询当日最大挂号号,如 REG_20240520_099 → 返回 100 int nextSeq = registrationMapper.selectMaxSequenceByDate(datePart); return String.format("REG_%s_%03d", datePart, nextSeq + 1); }参数说明:
queueNumber的WAI_前缀表示“外科候诊”,estimatedWaitTime由DoctorScheduleService.getEstimatedWaitTime(deptId, doctorId)计算得出(基于当前排队人数和医生接诊速率模型)。这不是随机数,而是可审计的业务流水号。
3.3 处方与库存联动:发药时如何保证“超卖”不发生?
开处方后,调用/api/drug/issue提交发药。此时DrugIssueService.issueDrugs()执行原子操作:
@Transactional public void issueDrugs(Long prescriptionId, Long operatorId) { Prescription prescription = prescriptionMapper.selectById(prescriptionId); List<PrescriptionDetail> details = prescriptionDetailMapper.selectByPrescriptionId(prescriptionId); for (PrescriptionDetail detail : details) { // 关键:乐观锁更新库存,stock >= quantity 才扣减 int updated = medicineMapper.decreaseStock(detail.getMedicineId(), detail.getQuantity()); if (updated == 0) { throw new BusinessException("药品【" + detail.getMedicineName() + "】库存不足"); } } // 更新发药状态 prescription.setIssueStatus(1); prescriptionMapper.updateById(prescription); }对应的MedicineMapper.xml中:
<update id="decreaseStock"> UPDATE medicine SET stock = stock - #{quantity} WHERE id = #{medicineId} AND stock >= #{quantity} </update>逻辑说明:这里用
AND stock >= #{quantity}替代先SELECT再UPDATE,避免并发场景下的超卖(经典“查-改-写”竞态)。MyBatis-Plus 的@Version注解在此未启用,因库存字段本身已承担乐观锁语义。
4. 避坑指南:这 4 个错误让 80% 的人卡在启动或业务流转环节
4.1 现象:启动时报java.lang.NoClassDefFoundError: javax/xml/bind/DatatypeConverter
原因:JDK 11+ 移除了 JAXB(Java EE 模块),而hospital-common中IdCardValidator使用了DatatypeConverter.parseBase64Binary()。
解决:在pom.xml的hospital-common模块中添加依赖:
<dependency> <groupId>javax.xml.bind</groupId> <artifactId>jaxb-api</artifactId> <version>2.3.1</version> </dependency>4.2 现象:挂号成功但registration表中doctor_id为 NULL
原因:init_data.sql中doctor表的id是BIGINT类型,而registration表的doctor_id字段定义为INT,导致插入时截断。
解决:手动修改sql/create_db.sql中registration表的doctor_id字段为BIGINT,并重新执行建库脚本。
4.3 现象:调用/api/prescription/create返回500,日志显示org.springframework.dao.DataIntegrityViolationException: Column 'medicine_id' cannot be null
原因:前端传参时medicineList数组为空,或medicineId字段名写错(如写成medId),MyBatis-Plus 无法映射导致插入 NULL。
解决:检查PrescriptionDetail实体类的@TableField注解,确认字段名与 JSON key 一致;增加 Controller 层校验:
@PostMapping("/create") public Result create(@Valid @RequestBody PrescriptionDTO dto) { if (CollectionUtils.isEmpty(dto.getMedicineList())) { return Result.fail("处方至少需包含一种药品"); } // ... }4.4 现象:发药后medicine表stock减少了,但drug_issue表无记录
原因:DrugIssueService.issueDrugs()方法未加@Transactional,或事务传播行为配置错误(如REQUIRES_NEW导致嵌套事务失效)。
解决:确认@Transactional注解在 Service 方法上(非类级别),且spring.transaction.interceptor未被覆盖;检查application.yml中spring.aop.proxy-target-class: true是否启用(CGLIB 代理对 private 方法无效)。
5. 进阶实战:把这套 HIS 源码变成你的技术简历亮点
5.1 重构点一:用 MyBatis-Plus 的AutoGenerator替换手写 Mapper,3 行代码生成全部 CRUD
原项目hospital-dao中每个实体都有独立 Mapper 接口和 XML 文件。你可以用 MP 的代码生成器统一生成,省去 80% 模板代码。在hospital-dao模块下新建CodeGenerator.java:
public class CodeGenerator { public static void main(String[] args) { AutoGenerator generator = new AutoGenerator(); generator.setDataSource(new DataSourceConfig() .setUrl("jdbc:mysql://localhost:3306/hospital_db") .setUsername("root").setPassword("your_pass") .setDriverName("com.mysql.cj.jdbc.Driver")); generator.setGlobalConfig(new GlobalConfig() .setOutputDir("src/main/java") // 输出路径 .setAuthor("your_name") .setOpen(false)); // 生成后不自动打开文件夹 generator.setPackageInfo(new PackageConfig() .setParent("com.hospital.dao") .setMapper("mapper") .setXml("mapper.xml")); generator.setStrategy(new StrategyConfig() .setNaming(NamingStrategy.underline_to_camel) .setColumnNaming(NamingStrategy.underline_to_camel) .addInclude("patient", "doctor", "medicine", "prescription")); // 指定表名 generator.execute(); } }运行后,com.hospital.dao.mapper.PatientMapper等接口自动生成,且自带selectList、insert等方法。关键收益:当你在面试中被问“怎么提升 DAO 层开发效率”,你可以指着这段代码说:“我用 MP Generator 把 12 张表的 CRUD 从 3 天压缩到 10 分钟,且所有方法都支持 LambdaQueryWrapper——比如查今日挂号患者:patientMapper.selectList(new QueryWrapper<Patient>().eq("reg_date", LocalDate.now()));”。
5.2 重构点二:给收费模块加幂等性,防止重复提交导致多扣款
当前/api/charge/submit接口无防重机制。用户连点两次“确认缴费”,可能生成两条CHARGE_RECORD。解决方案:在ChargeService.submit()中加入 Redis 幂等校验:
@Service public class ChargeService { @Resource private RedisTemplate<String, String> redisTemplate; public Result submit(@Valid @RequestBody ChargeDTO dto) { String key = "charge:dup:" + dto.getRegistrationId(); // 以挂号号为key Boolean isExist = redisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofMinutes(30)); if (!isExist) { return Result.fail("重复提交,请勿刷新页面"); } // 执行扣款逻辑... chargeMapper.insert(chargeRecord); return Result.success(); } }参数说明:
Duration.ofMinutes(30)是业务容忍窗口——同一挂号号 30 分钟内只允许成功提交一次。Redis key 命名用charge:dup:前缀便于监控和清理。
5.3 重构点三:用@Scheduled实现夜间库存盘点任务,替代人工巡检
医院药房每晚需核对库存。在hospital-service中新增定时任务:
@Component public class InventoryCheckTask { @Autowired private MedicineMapper medicineMapper; @Scheduled(cron = "0 0 2 * * ?") // 每天凌晨2点执行 public void checkInventory() { List<Medicine> lowStock = medicineMapper.selectList( new QueryWrapper<Medicine>().lt("stock", 10) // 库存低于10即告警 ); if (!lowStock.isEmpty()) { // 发送企业微信/钉钉告警(此处省略SDK调用) System.out.println("【库存告警】以下药品需补货:" + lowStock.stream().map(m -> m.getName() + "(" + m.getStock() + ")") .collect(Collectors.joining(", "))); } } }逻辑说明:
@Scheduled配合spring.task.scheduling.enabled=true(默认开启),无需 Quartz 复杂配置。告警内容直指业务痛点——不是“某服务不可用”,而是“阿莫西林胶囊只剩3盒,需立即采购”。
我带实习生做过这个项目改造:把原始代码的“能跑就行”状态,升级成“可监控、可防重、可扩展”。现在他们面试时不再背八股文,而是讲:“我在 HIS 系统里给挂号加了分布式锁,用 Redisson 实现科室号源隔离,避免跨科室抢号”。技术深度不在框架多炫,而在你能否把REG_20240520_001这个字符串背后的时间、序列、业务规则,讲成一段让人信服的故事。希望帮到你。
本文还有配套的精品资源,点击获取