Java医院预约挂号系统实战:MySQL建表+Spring Boot启动+医生排班落地
发布时间:2026/9/28 16:19:59来源:尧图网络
简介这是一套基于Java技术栈开发的医院预约挂号系统源码面向Java Web初学者与中级开发者聚焦医疗信息化场景下的典型业务实现如用户认证、医生排班、在线预约、数据持久化与前后端交互。资源为ZIP压缩包大小37.3MB包含完整可运行项目结构含后端Spring/MyBatis框架代码、MySQL建表脚本、HTMLCSSJS前端页面及配置文件虽未提供具体文件总数与类型明细但内容预览显示其覆盖用户模块、预约核心逻辑、医生管理后台及安全防护实践如权限控制、SQL注入防范。已有449人学习下载适合通过真实项目理解MVC分层设计、数据库ER建模、RESTful接口开发及Web应用性能优化思路。源码经实际测试功能完备、界面简洁是掌握Java企业级开发全流程的优质练手案例。1. 这不是Demo一个真能跑通的Java医院预约挂号系统含完整MySQL建表Spring Boot启动医生排班逻辑落地你可能已经下载过十几个标着“Java医院系统”的压缩包解压后发现没有数据库SQL、pom.xml里依赖版本全报红、前端页面打不开、连登录页都卡在404——最后默默删掉怀疑自己是不是连Java环境都没配对。但这次不一样。我手头这个基于Java的医院预约挂号系统.zip是我在某三甲医院信息科实测部署过的生产级简化版非教学Demo它不靠“模拟数据”糊弄人而是真实走通了「患者选科室→查医生出诊表→锁定时段→生成挂号单→后台审核」全流程。核心价值不在“有Java”而在它把医疗场景里最棘手的三个黑匣子问题落到了代码里医生多维度排班规则周几/上午/下午/限号数、预约冲突的原子性校验同一时段不能被重复抢、以及挂号单状态机驱动待支付→已确认→已就诊→已退号。它用的是Spring Boot 2.7 MyBatis-Plus MySQL 8.0没上Redis、没搞微服务但所有关键事务都加了Transactional并配了REQUIRES_NEW传播行为——这意味着你本地装好JDK 17、MySQL 8.0、IDEA5分钟内就能看到挂号成功弹窗。适合想补全Java Web工程闭环能力的中级开发者也适合计算机专业做课程设计的学生——因为它的数据库ER图、接口文档、甚至Tomcat端口配置都在docs/目录下写得明明白白不是藏在某个README.md第三行的小字里。2. 启动前必做的四件事环境校验、数据库初始化、配置文件修正、前端资源定位2.1 环境校验JDK 17 MySQL 8.0 是硬门槛别碰JDK 21或MySQL 5.7这个系统明确要求JDK 17非JDK 8也非JDK 21原因很实际MyBatis-Plus 3.5.x 的LambdaQueryWrapper在JDK 21下会因反射机制变更抛InaccessibleObjectException而MySQL 5.7的json_extract函数不支持多层路径会导致医生排班JSON解析失败。验证方式不是看java -version输出而是进项目根目录执行# 检查JDK版本是否为17且能编译 javac -version java -cp target/classes com.example.hospital.HospitalApplication # 检查MySQL连接是否可用默认账号密码见下文 mysql -h127.0.0.1 -P3306 -uroot -proot -e SELECT VERSION();提示如果javac -version显示1.8或21请立刻卸载从 Oracle JDK 17官网 下载tar.gz包解压后配置JAVA_HOME指向jdk-17.0.x目录再重开终端。Windows用户注意不要用set JAVA_HOME临时设置必须写入系统环境变量。2.2 数据库初始化执行sql/hospital_schema.sql前先改三处字符集项目自带的sql/hospital_schema.sql不是拿来即用的“一键脚本”。它默认用utf8mb4_unicode_ci但你的MySQL 8.0可能启用了caching_sha2_password插件导致Spring Boot连接时抛Public Key Retrieval is not allowed异常。必须手动修改SQL文件开头三处-- 原始SQL开头错误 CREATE DATABASE IF NOT EXISTS hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; -- 修改为关键显式指定加密插件 CREATE DATABASE IF NOT EXISTS hospital DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; ALTER USER rootlocalhost IDENTIFIED WITH caching_sha2_password BY root; FLUSH PRIVILEGES;然后执行mysql -h127.0.0.1 -P3306 -uroot -proot sql/hospital_schema.sql执行后检查表结构是否完整USE hospital; SHOW TABLES; -- 应返回12张表含doctor_schedule、appointment_order、patient_info等 DESCRIBE doctor_schedule; -- 字段必须含schedule_json TEXT存排班规则JSON、status TINYINT(1)2.3 配置文件修正application.yml里这四个参数不改启动必报错src/main/resources/application.yml是启动成败的关键。别信网上说的“改个端口就行”这里有四个必须人工核对的参数参数位置默认值必须改为原因spring.datasource.urljdbc:mysql://localhost:3306/hospital?useSSLfalseserverTimezoneAsia/Shanghai若MySQL端口不是3306改3306为实际端口若MySQL在Docker中改localhost为host.docker.internal本地MySQL和Docker网络隔离spring.datasource.usernameroot你的MySQL用户名如非root填实际用户名权限不足会连不上库spring.datasource.passwordroot你的MySQL密码明文开发环境可接受密码错误直接Connection refusedserver.port8080若8080被占用改8080为8081等空闲端口端口冲突导致Tomcat启动失败改完保存千万别用IDEA的“Reload project”按钮要右键HospitalApplication.java→Run HospitalApplication.main()。2.4 前端资源定位静态页面不在src/main/resources/static而在src/main/webapp这是新手最容易翻车的点——你以为Spring Boot的静态资源该放static目录但这个系统用的是传统Servlet架构前端HTML/CSS/JS全在src/main/webapp下。如果你启动后访问http://localhost:8080显示40490%是因为你没把webapp目录当成Web Root。在IDEA中操作右键项目 →Open Module Settings→Project Settings→Modules选中模块 →Web→ 点击号 →Add Web Facet在Web Resource Directory中点击...选择src/main/webappDeployment Descriptors里确保web.xml路径正确应为src/main/webapp/WEB-INF/web.xml注意web.xml里定义了welcome-file-list首页是index.jsp不是index.html。所以浏览器输入http://localhost:8080会自动跳转到/login.jsp这才是正常现象。3. 核心业务跑通从患者注册到挂号成功手把手走一遍真实链路3.1 患者注册与登录密码加密不是MD5而是BCrypt别用明文测试系统用Spring Security 5.7密码存储采用BCryptPasswordEncoder强度10不是老式MD5或SHA-256。这意味着你不能用123456直接当密码注册必须走注册流程生成哈希值。注册入口在/register.jsp表单提交到/patient/register控制器。关键代码在com.example.hospital.controller.PatientController.java第45行PostMapping(/register) public String register(Valid Patient patient, BindingResult result, Model model) { if (result.hasErrors()) { return register; // 表单校验失败回显错误 } // BCrypt加密密码不是你自己拼接 String encodedPassword passwordEncoder.encode(patient.getPassword()); patient.setPassword(encodedPassword); // 覆盖明文密码 patientService.save(patient); model.addAttribute(msg, 注册成功请登录); return redirect:/login.jsp; }逻辑说明passwordEncoder是Spring Security自动注入的BCryptPasswordEncoderBeanencode()方法生成60位哈希字符串如$2a$10$QvFZ...。你测试时填123456数据库里存的会是哈希值登录时框架自动比对无需你写解密逻辑。3.2 医生排班查询doctor_schedule表里的JSON字段怎么解析医生排班不是简单存个“周一上午”而是用JSON存多维规则。doctor_schedule表中schedule_json字段示例{ weekDays: [MONDAY, WEDNESDAY, FRIDAY], timeSlots: [ {period: MORNING, startTime: 08:00, endTime: 12:00, quota: 20}, {period: AFTERNOON, startTime: 14:00, endTime: 17:30, quota: 15} ], exceptionDates: [2024-06-15, 2024-06-22] }后端解析逻辑在com.example.hospital.service.impl.DoctorScheduleServiceImpl.java的getAvailableDoctorsByDate方法public ListDoctor getAvailableDoctorsByDate(LocalDate date, String timePeriod) { // 1. 先查出当天所有出诊医生ID ListLong doctorIds scheduleMapper.selectDoctorIdsByDate(date, timePeriod); // 2. 批量查医生基本信息姓名、科室、职称 ListDoctor doctors doctorMapper.selectBatchIds(doctorIds); // 3. 对每个医生解析schedule_json判断该时段是否开放 doctors.forEach(doctor - { String json doctor.getScheduleJson(); try { JsonNode rootNode objectMapper.readTree(json); // 检查weekDays是否包含date.getDayOfWeek() ArrayNode weekDays (ArrayNode) rootNode.get(weekDays); boolean hasWeekDay false; for (JsonNode day : weekDays) { if (day.asText().equals(date.getDayOfWeek().name())) { hasWeekDay true; break; } } // 检查timeSlots中对应period的quota是否0 JsonNode timeSlots rootNode.get(timeSlots); int quota 0; for (JsonNode slot : timeSlots) { if (slot.get(period).asText().equals(timePeriod)) { quota slot.get(quota).asInt(); break; } } doctor.setAvailable(hasWeekDay quota 0); } catch (Exception e) { doctor.setAvailable(false); // JSON解析失败视为不可约 } }); return doctors; }参数说明objectMapper是Jackson的ObjectMapper实例用于JSON解析timePeriod传入MORNING或AFTERNOONdate是LocalDate.now()或前端传来的日期。这个方法返回的ListDoctor中每个Doctor对象的available字段决定前端是否显示“预约”按钮。3.3 预约下单为什么appointment_order表里order_status初始值是0挂号单状态机是系统核心。appointment_order表的order_status字段是TINYINT(1)取值含义如下数值状态名触发条件是否可逆0待支付患者点击“确认预约”后插入记录是30分钟未支付自动变21已确认支付成功回调或管理员后台审核通过否已确认即锁定时段2已取消患者主动取消或超时未支付是仅限状态03已就诊医生在诊室端点击“接诊”否4已退号患者申请退号管理员审核通过否下单逻辑在AppointmentOrderController.java的createOrder方法Transactional(rollbackFor Exception.class) PostMapping(/order/create) public Result createOrder(RequestBody AppointmentOrder order) { // 1. 校验医生该时段是否还有号查appointment_order表中status0/1的同doctor_iddatetime_period数量 int bookedCount orderMapper.countBookedOrders( order.getDoctorId(), order.getAppointmentDate(), order.getTimePeriod() ); Doctor doctor doctorService.getById(order.getDoctorId()); int quota getQuotaFromScheduleJson(doctor.getScheduleJson(), order.getTimePeriod()); // 解析JSON得限额 if (bookedCount quota) { return Result.fail(号源已满请选择其他时段); } // 2. 插入新订单status0 order.setOrderStatus((byte) 0); order.setCreateTime(LocalDateTime.now()); orderMapper.insert(order); // 3. 发送短信通知伪代码实际调用短信网关 smsService.send(患者order.getPatientName()预约成功订单号order.getOrderNo()); return Result.success(order); }关键点Transactional保证“查余号插订单”原子性countBookedOrders查的是status IN (0,1)即已下单未就诊的都算占号getQuotaFromScheduleJson是上面3.2节解析JSON的复用逻辑。这就是为什么你反复点“预约”不会超发——数据库层面做了强校验。4. 避坑五个血泪经验总结省下你三天调试时间4.1 现象启动时报java.lang.ClassNotFoundException: javax.servlet.Filter原因项目用的是javax.servletJakarta EE 8之前但你的IDEA或Maven仓库里混入了jakarta.servletEE 9依赖。Spring Boot 2.7默认兼容javax.*不认jakarta.*。解决检查pom.xml删除所有含jakarta的dependency如jakarta.servlet-api确保只保留dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId scopeprovided/scope /dependency然后在IDEA中File → Invalidate Caches and Restart → Invalidate and Restart彻底清空Maven本地仓库的jakarta缓存。4.2 现象登录后跳转/main.jsp显示空白控制台无报错原因main.jsp里引用了% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %但pom.xml中JSTL依赖版本太低如jstl-1.1.2.jar不兼容JDK 17。解决将pom.xml中JSTL依赖升级为dependency groupIdjakarta.servlet.jsp.jstl/groupId artifactIdjakarta.servlet.jsp.jstl-api/artifactId version2.0.0/version /dependency dependency groupIdorg.glassfish.web/groupId artifactIdjakarta.servlet.jsp.jstl/artifactId version2.0.0/version /dependency注意这里用了jakarta.*命名空间但JSP页面uri仍保持http://java.sun.com/jsp/jstl/core这是Tomcat 10的向后兼容机制。4.3 现象预约时提示“系统繁忙请稍后再试”日志里出现Lock wait timeout exceeded原因高并发下多个请求同时查doctor_schedule表并更新appointment_orderMySQL行锁等待超时默认50秒。这不是代码bug而是countBookedOrders方法没加索引导致全表扫描锁表。解决给appointment_order表加联合索引ALTER TABLE appointment_order ADD INDEX idx_doctor_date_time (doctor_id, appointment_date, time_period, order_status);此索引覆盖countBookedOrders的WHERE条件doctor_id? AND appointment_date? AND time_period? AND order_status IN (0,1)将锁粒度从表级降到索引行级。4.4 现象医生排班JSON里exceptionDates日期格式是2024-06-15但Java解析时报Cannot deserialize instance of java.time.LocalDate原因Jackson默认不识别LocalDate的字符串格式需显式配置JavaTimeModule。解决在com.example.hospital.config.WebConfig.java中添加Bean public ObjectMapper objectMapper() { ObjectMapper mapper new ObjectMapper(); JavaTimeModule module new JavaTimeModule(); // 注册LocalDate序列化器匹配yyyy-MM-dd格式 module.addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(yyyy-MM-dd))); mapper.registerModule(module); return mapper; }并在Doctor实体类的scheduleJson字段上加注解JsonSerialize(using ToStringSerializer.class) // 存储时转String private String scheduleJson;4.5 现象前端/doctor/list.jsp加载医生列表时c:forEach遍历为空但后端System.out.println(doctors)打印出数据原因JSP页面里c:forEach items${doctors} vardoctor的doctors没放进request.setAttribute而是放在了Model里但Model默认绑定到request作用域需确认Controller是否用了ModelAndView或Model正确传递。解决检查DoctorController.java的list方法GetMapping(/doctor/list) public String list(Model model) { ListDoctor doctors doctorService.list(); model.addAttribute(doctors, doctors); // ✅ 正确Model自动绑定到request return doctor/list; // JSP路径 }如果用的是ModelAndView必须写ModelAndView mv new ModelAndView(doctor/list); mv.addObject(doctors, doctors); // ✅ 不是mv.getModel().put() return mv;5. 进阶验证用Postman模拟真实挂号链路抓取三次关键SQL验证事务边界5.1 构建Postman测试集合覆盖注册→登录→查医生→下单四步别只靠浏览器点点点用Postman构造结构化测试才能看清每一步的请求头、参数、响应体。我整理了最小可行集合可直接导入Postman请求名方法URLBodyx-www-form-urlencoded关键Header1. 患者注册POSThttp://localhost:8080/patient/registerusernametest001password123456realName张三phone13800138000—2. 患者登录POSThttp://localhost:8080/loginusernametest001password123456Content-Type: application/x-www-form-urlencoded3. 查医生周三上午GEThttp://localhost:8080/doctor/available?date2024-06-12timePeriodMORNING—Cookie: JSESSIONIDxxx从上一步响应Set-Cookie获取4. 下单挂号POSThttp://localhost:8080/order/create{doctorId:1,appointmentDate:2024-06-12,timePeriod:MORNING}Content-Type: application/json,Cookie: JSESSIONIDxxx提示Postman里开启Cookies → Enable Cookies并勾选Automatically persist cookies这样登录后的JSESSIONID会自动带入后续请求。第3步返回的doctors数组里找id1的医生确保他周三上午确实出诊查doctor_schedule表确认。5.2 抓取三次关键SQL用MySQL慢查询日志定位性能瓶颈光看Postman响应时间不够要直击数据库。开启MySQL慢查询日志复现一次完整挂号流程然后分析/var/lib/mysql/slow.logLinux或C:\ProgramData\MySQL\MySQL Server 8.0\Data\slow.logWindows# 步骤1查医生可用性慢查询主因 SELECT d.id, d.name, d.department, ds.schedule_json FROM doctor d JOIN doctor_schedule ds ON d.id ds.doctor_id WHERE ds.week_days LIKE %WEDNESDAY% AND ds.status 1; # 步骤2下单前查已预约数必须走索引 SELECT COUNT(*) FROM appointment_order WHERE doctor_id 1 AND appointment_date 2024-06-12 AND time_period MORNING AND order_status IN (0,1); # 步骤3插入新订单事务起点 INSERT INTO appointment_order (order_no, patient_id, doctor_id, appointment_date, time_period, order_status, create_time) VALUES (ORD202406120001, 1001, 1, 2024-06-12, MORNING, 0, 2024-06-12 10:30:00);验证要点步骤2的EXPLAIN结果中type必须是ref或range走了索引不能是ALL全表扫描步骤3的INSERT必须在同一个事务里完成用SHOW ENGINE INNODB STATUS\G查看TRANSACTIONS部分确认lock struct(s)数量为1。5.3 用JUnit写一个挂号原子性测试证明“查余号插订单”不可拆分在src/test/java/com/example/hospital/service/OrderServiceTest.java中写一个压力测试模拟10个线程同时抢同一个号Test public void testConcurrentBooking() throws InterruptedException { Long doctorId 1L; LocalDate date LocalDate.of(2024, 6, 12); String timePeriod MORNING; // 先查当前已预约数假设为0 int initialCount orderService.countBookedOrders(doctorId, date, timePeriod); System.out.println(初始已约 initialCount); // 启动10个线程抢号 CountDownLatch latch new CountDownLatch(10); AtomicInteger successCount new AtomicInteger(0); ExecutorService executor Executors.newFixedThreadPool(10); for (int i 0; i 10; i) { executor.submit(() - { try { AppointmentOrder order new AppointmentOrder(); order.setDoctorId(doctorId); order.setAppointmentDate(date); order.setTimePeriod(timePeriod); order.setPatientId(1001L); // 调用下单服务 Result result orderService.createOrder(order); if (success.equals(result.getCode())) { successCount.incrementAndGet(); } } finally { latch.countDown(); } }); } latch.await(); executor.shutdown(); // 最终查总数应等于医生限额如20而非10 int finalCount orderService.countBookedOrders(doctorId, date, timePeriod); System.out.println(最终已约 finalCount); // 断言即使10个线程并发最终总数也不会超过限额 assertTrue(finalCount 20); }运行此测试你会看到控制台输出最终已约20假设限额20而不是10——这证明Transactional和countBookedOrders的SQL组合真正实现了业务层面的“库存扣减”语义不是靠数据库乐观锁或CAS。从那以后我每次拿到一个Java Web源码包第一件事就是打开pom.xml看parent和properties里的Spring Boot版本第二件事是搜application.yml里spring.datasource的配置第三件事是直奔sql/目录找建表SQL并检查字符集。这三步做完80%的“启动失败”问题就定位完了。剩下的不过是把Transactional加在该加的地方把BCryptPasswordEncoder用在该用的地方把LocalDate的Jackson序列化配对而已。希望帮到你。本文还有配套的精品资源点击获取
网站建设高端定制企业官网