SpringBoot医疗挂号系统:时段锁号源与药品库存原子校验实战
2026/9/16 15:32:13 网站建设 项目流程

简介:这是一套面向计算机专业本科生的SpringBoot医疗挂号管理系统毕业设计完整实现,涵盖源码、数据库脚本与毕业论文,适用于毕设开题、系统开发实践及Java全栈能力提升。系统基于B/S架构与MVC模式,实现患者挂号、医生排班、门诊管理、药品库存、体检预约、病例归档等核心医疗业务流程,技术栈覆盖Java 8、SpringBoot 2.x、MyBatis、MySQL 5.7、Vue前端及Ajax异步交互,适配IDEA+Maven+Tomcat开发环境。压缩包为RAR格式,大小37.46MB,包含可直接运行的后端源码、SQL建表与初始化脚本、前后端分离式页面资源及结构清晰的毕业论文文档,文件总数虽未提供,但典型类型已覆盖工程主干与交付必需内容。目前已有64人学习下载,读者可直接导入IDEA运行调试,快速掌握医疗类信息系统的设计逻辑、权限分层实现、RESTful接口规范及前后端联调要点,是兼具教学性、实用性与答辩通过率的高质量毕设参考方案。

1. 这不是又一个“增删改查”毕设:SpringBoot医疗挂号系统里藏着真实门诊流控逻辑

很多同学拿到「医疗挂号管理系统」源码第一反应是:不就是患者表+医生表+挂号记录表,外加几个CRUD接口?但真正跑通这个 SpringBoot 毕设项目后你会发现,它在AppointmentController.java里嵌了时段级号源锁定机制,在PrescriptionService.java中实现了划价前药品库存原子校验,而PatientRegistrationValidator类甚至用 JSR-303 + 自定义注解做了身份证号+手机号双因子实名核验。这不是教学演示工程,而是按三甲医院门诊部最小可行单元(MVP)建模的业务系统——它能跑通挂号、分诊、划价、发药全链路,且所有数据库操作都经 MyBatis 动态 SQL 封装,支持 MySQL 5.7 的INSERT ... ON DUPLICATE KEY UPDATE语义处理并发挂号冲突。适合 Java 初学者夯实 SpringBoot + MyBatis 整合能力,也值得有 2 年经验的开发者拆解其事务边界设计(比如为什么createAppointment()方法上 @Transactional 的 propagation = Propagation.REQUIRED 而非 REQUIRES_NEW)。如果你正被毕设答辩卡在「业务真实性」或「技术深度」上,这套带完整论文的源码,就是你调试localhost:8080时最该盯住的那几行 SQL 日志。

2. 从解压到可运行:SpringBoot 医疗挂号系统的环境对齐与启动验证

2.1 JDK 1.8 与 Maven 3.6 的强制兼容性校验

该系统明确要求 JDK 1.8 和 Maven 3.6,这并非历史包袱,而是由pom.xml中两个关键依赖决定的:mybatis-spring-boot-starter:1.3.2spring-boot-starter-web:2.1.18.RELEASE。前者在 JDK 9+ 下会触发java.lang.NoClassDefFoundError: javax/xml/bind/JAXBContext,后者则因 Spring Boot 2.1.x 系列未适配 Maven 3.8+ 的maven-resolver新 API 而报PluginResolutionException。因此必须执行严格版本检查:

# 验证 JDK 版本(必须输出 1.8.x) java -version # 验证 Maven 版本(必须输出 3.6.x) mvn -v # 若版本不符,需切换 JDK(以 macOS 为例) export JAVA_HOME=$(/usr/libexec/java_home -v 1.8) # Windows 用户请在系统环境变量中将 JAVA_HOME 指向 jdk1.8.0_XXX 目录

提示:IntelliJ IDEA 中需同步设置 Project SDK 为 1.8,并在 Settings → Build → Build Tools → Maven → Runner 中指定 Maven home path 为 3.6.x 安装目录,否则 IDE 内构建会静默失败。

2.2 MySQL 5.7 数据库初始化与字符集陷阱

系统使用utf8mb4字符集支撑中文姓名、诊断描述等长文本,但 MySQL 5.7 默认配置可能仅启用utf8(实际为utf8mb3),导致插入生僻字时报Incorrect string value错误。必须在创建数据库时显式声明:

-- 创建数据库(关键:CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci) CREATE DATABASE IF NOT EXISTS medical_db CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci; -- 创建用户并授权(假设用户名 medical_user,密码 medical_pass) CREATE USER 'medical_user'@'localhost' IDENTIFIED BY 'medical_pass'; GRANT ALL PRIVILEGES ON medical_db.* TO 'medical_user'@'localhost'; FLUSH PRIVILEGES;

接着执行src/main/resources/sql/medical_schema.sql初始化表结构。注意该 SQL 文件中所有VARCHAR字段均含CHARACTER SET utf8mb4子句,例如:

CREATE TABLE `doctor` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '医生姓名', `department_id` bigint(20) DEFAULT NULL COMMENT '所属科室ID', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

注意:若使用 Navicat 或 SQLyog 执行 SQL 文件,请在连接属性中勾选「使用 UTF8MB4 字符集」,否则客户端编码不匹配会导致中文乱码。

2.3 application.yml 关键参数解析与本地化配置

src/main/resources/application.yml是系统行为的总开关,其中 3 处配置直接影响功能可用性:

参数必填默认值修改建议作用说明
spring.datasource.urljdbc:mysql://localhost:3306/medical_db?useUnicode=true&characterEncoding=utf8mb4&serverTimezone=Asia/Shanghai检查 host/port/database 名是否与实际一致MySQL 连接字符串,serverTimezone=Asia/Shanghai防止时间字段存取偏差
spring.datasource.username/passwordroot/123456替换为 2.2 步骤创建的medical_user/medical_pass数据库认证凭据
mybatis.mapper-locationsclasspath:mapper/*.xml确保路径下存在DoctorMapper.xml等文件MyBatis XML 映射文件加载路径

启动前务必确认application.ymlserver.port未被其他进程占用(默认 8080),可临时改为8081

server: port: 8081

2.4 启动验证:从控制台日志定位核心服务就绪信号

在项目根目录执行mvn spring-boot:run后,观察控制台输出。成功启动的黄金标志不是 "Started Application",而是以下两行日志连续出现:

[main] o.s.b.w.embedded.tomcat.TomcatWebServer : Tomcat started on port(s): 8081 (http) with context path '' [main] c.m.Application : Started Application in 8.234 seconds (JVM running for 9.123)

此时访问http://localhost:8081/login应返回登录页 HTML(非 404)。若出现Failed to bind properties错误,说明application.yml中某参数格式错误(如冒号后缺少空格);若报Access denied for user,则是数据库用户名密码不匹配;若卡在Starting Quartz Scheduler且无后续日志,则需检查quartz.propertiesorg.quartz.jobStore.driverDelegateClass=org.quartz.impl.jdbcjobstore.StdJDBCDelegate是否与 MySQL 5.7 兼容(本系统已预置适配版本)。

3. 核心业务模块拆解:挂号、划价、门诊三大流程的 SpringBoot 实现逻辑

3.1 挂号流程:时段号源控制与并发安全设计

挂号功能集中在com.medical.controller.AppointmentController,其createAppointment()方法是业务入口。该方法并非简单插入appointment表,而是通过「先查后锁再插」三步策略保障号源不超卖:

@Transactional(rollbackFor = Exception.class) public Result createAppointment(@RequestBody AppointmentDTO dto) { // 1. 查询当前时段剩余号源(SELECT ... FOR UPDATE) Integer remain = appointmentMapper.selectRemainByTimeSlot(dto.getDoctorId(), dto.getVisitDate(), dto.getTimeSlot()); if (remain == null || remain <= 0) { return Result.fail("号源已满,请选择其他时段"); } // 2. 扣减号源(UPDATE ... SET remain = remain - 1) int updateCount = appointmentMapper.decreaseRemainByTimeSlot(dto.getDoctorId(), dto.getVisitDate(), dto.getTimeSlot()); if (updateCount != 1) { throw new RuntimeException("号源扣减失败,可能已被其他请求抢占"); } // 3. 创建挂号记录(INSERT INTO appointment) Appointment appointment = new Appointment(); appointment.setPatientId(dto.getPatientId()); appointment.setDoctorId(dto.getDoctorId()); appointment.setVisitDate(dto.getVisitDate()); appointment.setTimeSlot(dto.getTimeSlot()); appointment.setStatus("WAITING"); // 初始状态为待就诊 appointmentMapper.insert(appointment); return Result.success("挂号成功,就诊序号:" + appointment.getId()); }

逻辑说明:SELECT ... FOR UPDATE在 InnoDB 中会对查询结果集加行锁,阻止其他事务同时修改同一时段号源;decreaseRemainByTimeSlot使用UPDATE ... WHERE remain > 0条件确保扣减前再次校验,形成双重保险。这种设计比单纯UPDATE appointment_slot SET remain = remain - 1 WHERE id = ? AND remain > 0更易排查并发问题。

3.2 划价模块:药品库存原子校验与价格动态计算

划价操作在PrescriptionService.java中实现,核心是generatePrescription()方法。它需同时满足三个约束:① 处方药品库存充足;② 药品单价按采购价+15%毛利动态计算;③ 整个划价过程不可分割。代码通过@Transactional保证 ACID:

@Transactional(rollbackFor = Exception.class) public Prescription generatePrescription(Long patientId, List<PrescriptionItemDTO> items) { Prescription prescription = new Prescription(); prescription.setPatientId(patientId); prescription.setCreateTime(new Date()); BigDecimal totalAmount = BigDecimal.ZERO; for (PrescriptionItemDTO item : items) { // 1. 查询药品库存(SELECT stock FROM drug WHERE id = ? FOR UPDATE) Drug drug = drugMapper.selectById(item.getDrugId()); if (drug.getStock() < item.getQuantity()) { throw new BusinessException("药品【" + drug.getName() + "】库存不足,当前库存:" + drug.getStock()); } // 2. 计算划价金额:采购价 * (1 + 0.15) * 数量 BigDecimal unitPrice = drug.getPurchasePrice().multiply(BigDecimal.ONE.add(new BigDecimal("0.15"))); BigDecimal itemAmount = unitPrice.multiply(new BigDecimal(item.getQuantity())); totalAmount = totalAmount.add(itemAmount); // 3. 扣减库存(UPDATE drug SET stock = stock - ? WHERE id = ?) drugMapper.updateStock(item.getDrugId(), item.getQuantity()); // 构建处方明细 PrescriptionItem detail = new PrescriptionItem(); detail.setPrescriptionId(prescription.getId()); detail.setDrugId(item.getDrugId()); detail.setQuantity(item.getQuantity()); detail.setUnitPrice(unitPrice); prescriptionItemMapper.insert(detail); } prescription.setTotalAmount(totalAmount); prescriptionMapper.insert(prescription); return prescription; }

参数说明:PrescriptionItemDTO包含drugId(药品ID)、quantity(数量);Drug实体中purchasePrice为采购单价,stock为实时库存。此设计避免了「先生成处方再校验库存」导致的超卖风险,且所有数据库操作在同一个事务中完成。

3.3 门诊管理:医生排班与分诊状态机驱动

门诊模块的核心是ClinicService.java中的updateVisitStatus()方法,它实现了基于状态机的就诊流程控制。visit_status字段在patient_visit表中定义为 ENUM:WAITING,IN_CONSULTATION,FINISHED,CANCELLED。状态流转受严格规则约束:

public Result updateVisitStatus(Long visitId, String newStatus) { PatientVisit visit = visitMapper.selectById(visitId); if (visit == null) { return Result.fail("就诊记录不存在"); } // 状态机校验:只允许合法流转(如 WAITING → IN_CONSULTATION,禁止 FINISHED → WAITING) boolean isValidTransition = switch (visit.getStatus()) { case "WAITING" -> "IN_CONSULTATION".equals(newStatus) || "CANCELLED".equals(newStatus); case "IN_CONSULTATION" -> "FINISHED".equals(newStatus) || "CANCELLED".equals(newStatus); case "FINISHED", "CANCELLED" -> false; // 终态不可逆 default -> false; }; if (!isValidTransition) { return Result.fail("状态变更非法:从【" + visit.getStatus() + "】不能变更为【" + newStatus + "】"); } visit.setStatus(newStatus); visit.setUpdateTime(new Date()); if ("IN_CONSULTATION".equals(newStatus)) { visit.setConsultStartTime(new Date()); // 记录开始问诊时间 } if ("FINISHED".equals(newStatus)) { visit.setConsultEndTime(new Date()); // 记录结束问诊时间 } visitMapper.updateById(visit); return Result.success("状态更新成功"); }

技术要点:状态机逻辑未使用第三方库(如 Spring State Machine),而是用switch表达式硬编码流转规则,降低学习成本;consultStartTime/consultEndTime字段用于后续统计医生平均问诊时长,体现业务数据价值。

4. 毕设论文与源码协同:如何用 ER 图和时序图讲清系统设计深度

4.1 从medical_schema.sql逆向生成 ER 图的关键技巧

论文中 ER 图质量直接决定答辩老师对「数据库设计能力」的判断。本系统共 12 张表,但核心实体只有 5 个:patient(患者)、doctor(医生)、appointment(挂号)、prescription(处方)、drug(药品)。生成专业 ER 图需抓住三点:

  1. 主外键关系显式标注appointment.patient_idpatient.idprescription_item.prescription_idprescription.id
  2. 基数标注准确doctorappointment是 1:N(一名医生可接多个挂号),prescriptionprescription_item是 1:N(一张处方含多条药品明细)
  3. 弱实体识别prescription_item是弱实体,其主键id无业务含义,完全依赖prescription_id存在

推荐用PowerDesignerMySQL Workbench逆向工程。以 Workbench 为例:

  • 打开 EER Diagram → File → Import → Reverse Engineer MySQL Create Script
  • 选择medical_schema.sql→ 勾选「Place imported objects on canvas」
  • 自动生成图后,手动调整布局:将patient/doctor置于顶部,appointment/prescription居中,drug置于右侧,用正交连线替代斜线

提示:论文中 ER 图需添加图例说明「1」和「N」符号含义,并在图下方注明「本图基于 medical_schema.sql 逆向生成,已通过 MySQL 5.7 实际验证」。

4.2 用 PlantUML 绘制挂号流程时序图(附可直接编译代码)

时序图是展示 SpringBoot 分层调用最直观的方式。以下 PlantUML 代码描述挂号请求从 Controller 到 DB 的完整链路,复制保存为appointment_sequence.puml,用 PlantText 在线渲染:

@startuml title 挂号流程时序图 actor Patient participant "AppointmentController" as controller participant "AppointmentService" as service participant "AppointmentMapper" as mapper database "MySQL medical_db" as db Patient -> controller: POST /api/appointment {doctorId, visitDate...} controller -> service: createAppointment(dto) service -> mapper: selectRemainByTimeSlot(...) mapper -> db: SELECT remain FROM appointment_slot WHERE ... db --> mapper: 返回剩余号源 mapper --> service: 返回 remain service -> mapper: decreaseRemainByTimeSlot(...) mapper -> db: UPDATE appointment_slot SET remain = remain - 1 WHERE ... db --> mapper: 影响行数 mapper --> service: 返回更新结果 service -> mapper: insert(appointment) mapper -> db: INSERT INTO appointment VALUES (...) db --> mapper: 主键ID mapper --> service: 返回appointment.id service --> controller: Result.success(...) controller --> Patient: HTTP 200 {code:0, msg:"挂号成功"} @enduml

优势:该图明确标出每层职责——Controller 解析请求、Service 协调业务逻辑、Mapper 封装 SQL、DB 执行存储。答辩时可指着图说:「Controller 不做任何数据校验,所有业务规则都在 Service 层实现,符合 SpringBoot 分层架构最佳实践」。

4.3 论文「系统测试」章节的实操数据模板

毕设论文常因测试用例空洞被质疑。本系统提供 3 类可复现的测试场景,直接填入论文表格即可:

测试用例输入数据预期结果实际结果通过/失败说明
并发挂号超卖防护2 个线程同时请求同一医生同一时段最后 1 个号源1 个成功,1 个返回「号源已满」✅ 成功通过验证SELECT ... FOR UPDATE有效性
药品库存不足划价处方含 1000 片阿莫西林,库存仅 500 片返回「药品【阿莫西林】库存不足」✅ 成功通过验证drug.getStock() < item.getQuantity()校验
门诊状态非法流转将已结束就诊(FINISHED)的状态改为 WAITING返回「状态变更非法」✅ 成功通过验证状态机switch逻辑

技巧:在src/test/java下运行AppointmentControllerTest.java中的testConcurrentAppointment()方法,可复现并发测试;用 Postman 发送PUT /api/visit/status请求修改状态,快速验证状态机。

5. 毕设答辩高频问题应对:从源码细节到架构演进的深度准备

5.1 「为什么用 MyBatis 而不用 JPA?」—— 结合DoctorMapper.xml的底层控制权分析

答辩老师若问及 ORM 选型,切忌回答「因为简单」。应指向src/main/resources/mapper/DoctorMapper.xml中的真实代码:

<!-- DoctorMapper.xml 片段 --> <select id="selectDoctorsByDepartment" resultType="com.medical.entity.Doctor"> SELECT d.id, d.name, d.title, d.phone, COUNT(a.id) as appointment_count FROM doctor d LEFT JOIN appointment a ON d.id = a.doctor_id AND a.visit_date = #{visitDate} AND a.status = 'WAITING' WHERE d.department_id = #{departmentId} GROUP BY d.id, d.name, d.title, d.phone ORDER BY appointment_count DESC </select>

关键点:此 SQL 使用LEFT JOIN关联挂号表并按日期过滤,同时GROUP BY统计当日待就诊人数。JPA 的@Query注解虽可写原生 SQL,但无法像 MyBatis 这样灵活控制resultType映射到自定义 DTO(如DoctorWithCount),且 MyBatis 的<if>标签能动态拼接条件(如AND d.title = #{title}),而 JPA Criteria API 代码冗长。本系统需要大量此类报表查询,MyBatis 提供了更贴近 SQL 的控制粒度。

5.2 「如何保证 MySQL 5.7 下的时间字段准确性?」——application.yml与 JDBC URL 双重校准

时间问题常被忽略却极易翻车。本系统在application.yml中配置:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8

同时 JDBC URL 强制指定时区:

spring: datasource: url: jdbc:mysql://localhost:3306/medical_db?...&serverTimezone=Asia/Shanghai

原理:serverTimezone=Asia/Shanghai告诉 MySQL 驱动将 JDBC 时间转换为东八区时间存储;spring.jackson.time-zone确保 JSON 序列化时时间字段按北京时间格式化。二者缺一不可——若只配 JDBC URL,前端收到的2024-05-20T08:00:00.000+0000仍为 UTC;若只配 Jackson,数据库存入的可能是2024-05-20 00:00:00(UTC 时间)。

5.3 「如果升级到 SpringBoot 3.x 会遇到什么问题?」—— 基于pom.xml的兼容性断点扫描

当前pom.xmlspring-boot-starter-web:2.1.18.RELEASE与 SpringBoot 3.x 不兼容,主要断点有三处:

  1. JDK 版本跃迁:SpringBoot 3.x 要求 JDK 17+,而本系统强依赖 JDK 1.8 的javax.xml.bind包(用于 XML 报文解析)
  2. Hibernate 6.x 默认方言spring-boot-starter-data-jpa升级后默认使用MySQL8Dialect,但 MySQL 5.7 需显式配置spring.jpa.database-platform=org.hibernate.dialect.MySQL57Dialect
  3. Actuator 端点变更/actuator/health在 2.x 返回UP/DOWN,3.x 改为status: UP,前端监控脚本需适配

应对策略:答辩时可坦诚说明「当前选型以稳定性和毕设周期为优先」,并补充「若需升级,我计划分三步:① 先将 JDK 升至 11 过渡;② 替换mybatis-spring-boot-starter为 3.x 兼容版本;③ 用@ConditionalOnMissingBean保留旧版 HealthIndicator 兼容性」——展现技术演进思维而非盲目升级。

系统启动后访问http://localhost:8081/swagger-ui.html可查看全部 REST API 文档,其中/api/appointment接口的POST方法包含完整的请求体示例,这是你调试挂号流程最高效的起点。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询