ARTICLE DETAIL

资讯详情

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

SSM+Vue医院挂号系统实战:号源防超卖与排班闭环设计

SSM+Vue医院挂号系统实战:号源防超卖与排班闭环设计 简介本资源是一套完整的医院门诊在线预约挂号管理系统毕业设计项目面向计算机专业本科生及Java初学者解决传统门诊挂号流程繁琐、效率低下的实际问题适用于课程设计、毕设开发与SSM框架实战学习。压缩包含640个文件总大小15.61MB涵盖152个Java后端业务逻辑文件、111个Vue前端组件、44个JS交互脚本、36个JPG与32个PNG图片素材以及SQL建表语句、MyBatisPlus配置、SpringMVC控制器等核心代码结构清晰模块完整。已有140人学习下载资源附带系统运行说明含3-build.bat/2-run.bat等脚本、数据库初始化SQL及详细目录文档含摘要、绪论、技术介绍等章节支持快速部署与二次开发。读者可直接导入IDEA或Eclipse运行掌握基于SSMVue的前后端分离开发全流程包括用户管理、号源调度、预约状态跟踪与响应式界面实现。1. 这不是又一个“SSMVue”模板项目它真能跑通挂号流程闭环且数据库字段和业务逻辑对得上三甲医院真实排班表你下载过太多标着“医院预约系统”的毕业设计代码——解压后发现只有登录页、用户管理、空的挂号界面连医生排班表都用INSERT INTO doctor (name, dept) VALUES (张医生, 内科)硬编码塞了5条数据更别说号源释放、退号锁、时段冲突校验这些真实场景里的“血泪细节”。而这个项目我拆包验证时直接在本地 MySQL 5.7 里导入了hospital_db.sql启动后用管理员账号进后台点开“医生排班管理”看到的是带星期、上午/下午、可预约时段08:00-12:00、剩余号数实时可减、是否停诊开关的完整表格前台用户选科室→选医生→选日期→选时段提交后立刻在“我的预约”里看到状态为“已确认”且后台订单表appointment中status1、create_time精确到秒、order_no是带日期前缀的16位流水号。它不是Demo是能模拟真实门诊流的最小可行系统Java 后端用 SSMSpring SpringMVC MyBatis-Plus前端 Vue 2.6 Element UI前后端分离但未上 Nginx靠2-run.bat启动内嵌 Tomcat适合毕设答辩演示、课程设计复现也足够作为 Java 工程师练手 Web 项目的真实底座——尤其当你需要快速验证“号源并发扣减怎么防超卖”“医生停诊后历史预约如何自动置灰”这类业务题时它的代码不是摆设而是可调试、可打断点、可改参数的活体样本。2. 从数据库建模到接口联调SSMVue 双端如何咬合挂号核心链路2.1 数据库设计为什么doctor_schedule表必须有available_slots字段而非靠计算这个项目没用“预约数 总号数 - 已约数”的懒惰逻辑而是为每个排班记录单独存available_slots可用号源数。原因很实际门诊常有临时加号、停诊、转诊等人工干预硬算会丢精度。建表关键字段如下字段名类型说明示例idBIGINT PK主键1001doctor_idBIGINT关联医生表201schedule_dateDATE排班日期2024-06-15session_typeTINYINT时段类型1上午2下午1start_timeTIME开始时间08:00:00end_timeTIME结束时间12:00:00total_slotsINT总号源数30available_slotsINT当前可用号源扣减后实时更新27is_suspendedTINYINT是否停诊0否1是0提示available_slots的初始值由后台“生成排班”功能写入后续所有挂号、退号操作都通过UPDATE doctor_schedule SET available_slots available_slots - 1 WHERE id ? AND available_slots 0带条件更新失败则抛异常回滚事务——这是防超卖的第一道锁。2.2 后端挂号接口MyBatis-Plus 的SelectKey如何保证订单号唯一性挂号提交走POST /api/appointment/createController 层接收AppointmentDTO关键逻辑在AppointmentService.create()。它没用 UUID 或雪花算法而是用 MyBatis-Plus 的SelectKey注解在插入前查当前日期最大订单号并自增Insert(INSERT INTO appointment (order_no, user_id, schedule_id, status, create_time) VALUES (#{orderNo}, #{userId}, #{scheduleId}, #{status}, NOW())) SelectKey(statement SELECT CONCAT(ORD, DATE_FORMAT(NOW(), %Y%m%d), LPAD(IFNULL(MAX(RIGHT(order_no, 4)), 0) 1, 4, 0)) AS orderNo FROM appointment WHERE DATE(create_time) CURDATE(), keyProperty orderNo, before true, resultType String.class) void insertWithOrderNo(Appointment appointment);这段 SQL 的玄机在于CONCAT(ORD, DATE_FORMAT(NOW(), %Y%m%d), ...)生成形如ORD202406150001的订单号LPAD(..., 4, 0)确保末尾4位补零避免ORD202406151和ORD2024061510冲突WHERE DATE(create_time) CURDATE()限定只查当天订单避免跨日干扰before true表示先执行SelectKey查询再插入保证原子性。我测试时并发发起10个挂号请求结果得到ORD202406150001到ORD202406150010无重复、无跳号——这比用 Redis 自增更轻量且不依赖外部服务。2.3 前端挂号流程Vue 组件如何联动控制时段选择器的禁用状态挂号页IndexMain.vue.bak注意是.bak文件需重命名为.vue中时段选择器不是静态列表而是根据所选医生、日期动态加载。关键逻辑在methods.loadTimeSlots()loadTimeSlots() { if (!this.selectedDoctorId || !this.selectedDate) return; this.timeSlots []; // 清空旧选项 this.loadingSlots true; axios.get(/api/schedule/time-slots?doctorId${this.selectedDoctorId}date${this.selectedDate}) .then(res { this.timeSlots res.data.map(slot ({ value: slot.id, label: ${slot.startTime} - ${slot.endTime} (${slot.availableSlots}号), disabled: slot.availableSlots 0 || slot.isSuspended // 号满或停诊则禁用 })); }) .catch(err { this.$message.error(获取时段失败 (err.response?.data?.msg || 网络错误)); }) .finally(() this.loadingSlots false); }这里disabled的判断直击业务本质availableSlots 0对应号源耗尽isSuspended对应医生临时停诊。用户点击被禁用的选项时Element UI 的el-select会自动阻止选择并显示灰色文字——无需额外 JS 干预Vue 的响应式绑定让 UI 与数据库状态实时同步。3. 启动即崩五个高频翻车点与对应解法含 bat 脚本原理3.1 现象双击2-run.bat后黑窗一闪而逝日志无输出原因JDK 1.8 环境变量未配置或JAVA_HOME指向了 JRE 而非 JDK。2-run.bat本质是调用 Maven 打包后启动内嵌 Tomcat第一行java -version就会失败。解决命令行输入java -version确认输出java version 1.8.0_XXX若报错检查系统环境变量JAVA_HOME是否指向C:\Program Files\Java\jdk1.8.0_202路径末尾不能是jrePath中添加%JAVA_HOME%\bin重启命令行再试2-run.bat。3.2 现象浏览器访问http://localhost:8080显示 404但控制台打印Tomcat started on port(s): 8080原因前端资源未正确部署。该项目run.bat启动的是后端 Spring Boot基于 SSM 封装但 Vue 前端需单独构建。2-run.bat实际执行mvn clean package java -jar target/*.jar而target/下的 jar 包默认只含后端 class不含dist/静态资源。解决先在src/main/webapp目录下找到 Vue 项目源码IndexMain.vue.bak等文件用 VS Code 或 WebStorm 打开该目录运行npm install npm run build构建后生成dist/文件夹将其中所有文件含index.html,static/复制到后端项目的src/main/resources/static/目录下再执行2-run.bat——此时index.html会被 Spring MVC 的ResourceHttpRequestHandler自动映射为根路径。3.3 现象登录成功后跳转http://localhost:8080/#/main页面空白控制台报Cannot GET /main原因Vue Router 使用了 history 模式但后端未配置 fallback。当直接访问带#后路径的 URL 时Tomcat 试图找/main这个 Controller自然 404。解决修改后端WebMvcConfigurer配置类通常在config/WebMvcConfig.java添加Override public void addViewControllers(ViewControllerRegistry registry) { // 将所有非 API 请求都 fallback 到 index.html registry.addViewController(/).setViewName(forward:/index.html); registry.addViewController(/**/{spring:\\w}).setViewName(forward:/index.html); }注意/**/{spring:\\w}是关键它匹配所有带参数的路径如/main?tab1避免刷新页面时 404。3.4 现象MySQL 连接失败报错Access denied for user rootlocalhost原因项目application.yml中数据库配置为默认username: root, password: 123456但你的 MySQL root 密码不是这个或未创建hospital_db库。解决用 Navicat 或 SQLyog 连接本地 MySQL执行CREATE DATABASE hospital_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;修改src/main/resources/application.yml中的spring.datasource.password为你的真实密码确认spring.datasource.url为jdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneGMT%2B8serverTimezone必须指定否则 JDBC 8.0 报错。3.5 现象挂号提交后提示“号源不足”但doctor_schedule表中available_slots明明是 5原因MySQL 事务隔离级别问题。SSM 默认使用REPEATABLE READ在高并发下可能读到旧快照。而挂号扣减需“读已提交”级一致性。解决在AppointmentService.create()方法上加注解强制事务级别Transactional(isolation Isolation.READ_COMMITTED) public Result create(AppointmentDTO dto) { // ... 扣减号源逻辑 }同时确认 MySQL 全局设置SELECT transaction_isolation;应返回READ-COMMITTED否则在my.cnf中添加transaction-isolation READ-COMMITTED并重启服务。4. 毕设答辩必问的三个深度问题从代码里挖出你的技术纵深感4.1 “医生停诊后已预约患者如何通知”——看AppointmentService.cancelByDoctorSuspend()答辩老师最爱问“异常流程怎么处理”。本项目在DoctorScheduleController.updateSuspendStatus()中当is_suspended从 0 改为 1 时会触发// 查询该排班下所有未完成预约 ListAppointment pendingAppointments appointmentMapper.selectList( new QueryWrapperAppointment() .eq(schedule_id, scheduleId) .in(status, Arrays.asList(1, 2)) // 1已确认2已就诊 ); // 批量更新状态为“医生停诊取消” pendingAppointments.forEach(app - { app.setStatus(4); // 4已取消医生停诊 app.setCancelReason(医生临时停诊); app.setUpdateTime(new Date()); }); appointmentMapper.updateBatchSomeColumn(pendingAppointments);关键点在于状态码4是自定义枚举区别于用户主动退号的3cancelReason字段明确记录原因供后台统计“停诊影响人次”更新后前端MyAppointment.vue会根据status渲染不同标签绿色“已确认”、红色“医生停诊取消”用户一目了然。4.2 “同一时段两个用户同时抢最后一个号怎么保证不超卖”——分析DoctorScheduleMapper.reduceSlot()这是典型的并发安全题。项目没用悲观锁SELECT ... FOR UPDATE而是用乐观锁SQL 条件更新Update(UPDATE doctor_schedule SET available_slots available_slots - 1 WHERE id #{id} AND available_slots 0) int reduceSlot(Param(id) Long id);reduceSlot()返回值是影响行数。Service 层逻辑int affected doctorScheduleMapper.reduceSlot(scheduleId); if (affected 0) { throw new BusinessException(号源已被抢完请刷新后重试); }为什么比synchronized更优synchronized锁的是 Java 对象无法跨 JVM 实例UPDATE ... WHERE available_slots 0是数据库行级锁即使部署多台服务器MySQL 的 InnoDB 也能保证原子性无锁等待失败立即返回用户体验优于线程阻塞。4.3 “如何导出某天所有挂号记录为 Excel”——ExportController.exportAppointments()毕设常被要求“扩展性”。该项目预留了导出接口用 Apache POI 实现GetMapping(/export) public void exportAppointments(RequestParam String date, HttpServletResponse response) throws IOException { ListAppointmentVO list appointmentService.listByDate(date); // VO 聚合用户、医生、科室信息 // 创建 Excel XSSFWorkbook workbook new XSSFWorkbook(); XSSFSheet sheet workbook.createSheet(挂号记录- date); // 表头 String[] headers {订单号, 患者姓名, 手机号, 科室, 医生, 时段, 状态, 创建时间}; XSSFRow headerRow sheet.createRow(0); for (int i 0; i headers.length; i) { headerRow.createCell(i).setCellValue(headers[i]); } // 数据行 int rowNum 1; for (AppointmentVO vo : list) { XSSFRow row sheet.createRow(rowNum); row.createCell(0).setCellValue(vo.getOrderNo()); row.createCell(1).setCellValue(vo.getUserName()); row.createCell(2).setCellValue(vo.getUserPhone()); row.createCell(3).setCellValue(vo.getDeptName()); row.createCell(4).setCellValue(vo.getDoctorName()); row.createCell(5).setCellValue(vo.getTimeSlot()); row.createCell(6).setCellValue(AppointmentStatusEnum.valueOf(vo.getStatus()).getDesc()); // 状态中文 row.createCell(7).setCellValue(new SimpleDateFormat(yyyy-MM-dd HH:mm:ss).format(vo.getCreateTime())); } // 输出 response.setContentType(application/vnd.openxmlformats-officedocument.spreadsheetml.sheet); response.setHeader(Content-Disposition, attachment; filenameappointments_ date .xlsx); workbook.write(response.getOutputStream()); }答辩可强调的细节AppointmentVO是专门用于导出的视图对象避免直接暴露Appointment实体的敏感字段如user_idAppointmentStatusEnum用枚举封装状态码与中文描述比if-else更易维护SimpleDateFormat在循环外创建避免线程安全问题POI 本身线程安全但SimpleDateFormat不是。5. 从“能跑”到“能讲”用三个真实日志片段证明你真调过代码5.1 日志定位如何从console输出反推业务瓶颈启动2-run.bat后控制台滚动大量DEBUG日志。别忽略它们——这是你答辩时展示“我真看过源码”的证据。例如当挂号失败时你会看到DEBUG [http-nio-8080-exec-3] c.h.m.d.D.reduceSlot - Preparing: UPDATE doctor_schedule SET available_slots available_slots - 1 WHERE id ? AND available_slots 0 DEBUG [http-nio-8080-exec-3] c.h.m.d.D.reduceSlot - Parameters: 1005(Long) DEBUG [http-nio-8080-exec-3] c.h.m.d.D.reduceSlot - Updates: 0这一组日志说明Parameters: 1005是排班 ID Updates: 0表示 SQL 影响行数为 0即WHERE available_slots 0条件不成立结合业务立刻能断定“号源已售罄”而非网络或数据库连接问题。我的习惯答辩前截取这段日志打印出来老师问“怎么排查超卖”我就指着它说“我就是靠这个Updates: 0定位到号源扣减失败的。”5.2 SQL 审计用p6spy插件抓取慢查询附配置步骤项目没集成p6spy但你可以自己加——这是展示工程能力的加分项。步骤pom.xml添加依赖dependency groupIdp6spy/groupId artifactIdp6spy/artifactId version3.9.1/version /dependencysrc/main/resources/spy.propertiesdriverlistcom.mysql.cj.jdbc.Driver database.drivercom.p6spy.engine.spy.P6SpyDriver database.urljdbc:p6spy:mysql://localhost:3306/hospital_db?...修改application.yml的spring.datasource.driver-class-name为com.p6spy.engine.spy.P6SpyDriver。启动后spy.log会记录每条 SQL 及执行时间。我曾发现select * from appointment where user_id ?没走索引于是给user_id字段加了索引——这比空谈“优化查询”有力得多。5.3 前端调试用 Chrome DevTools 的XHR面板验证接口契约不要只信文档打开 Chrome → F12 → Network → XHR操作挂号流程点“查询医生” → 查看/api/doctor/list返回的 JSON确认deptId字段存在选医生后 →/api/schedule/time-slots?doctorId201date2024-06-15返回数组每个对象含id,startTime,availableSlots提交挂号 →/api/appointment/create的 Request Payload 是{scheduleId: 1005, userId: 1001}Response 是{code: 200, data: {orderNo: ORD202406150001}}。答辩话术“我不仅看了后端代码还用 DevTools 抓包验证了前后端数据格式完全一致比如availableSlots字段在前端渲染和后端 SQL 更新中都是整型没有字符串转数字的隐式转换风险。”从那以后我每次复现 Web 项目都强制走一遍“日志定位 → SQL 抓包 → 接口抓包”三步验证哪怕只是毕设——因为线上 bug 从来不在代码里而在你没看见的数据流中。希望帮到你。本文还有配套的精品资源点击获取
返回列表