
简介面向Java Web学习者和毕业设计人员的医院门诊挂号系统完整项目后端采用Spring、SpringMVC、MyBatisSSM整合框架前端结合Vue与JSP技术实现患者在线挂号、科室管理、医生排班、就诊记录查询等核心业务。压缩包约15.3MB内含项目源码、MySQL 5.7数据库脚本以及项目功能介绍文档源码按控制层、业务层、数据访问层分层组织目录结构清晰便于对照学习SSM整合流程和前后端数据交互。项目基于JDK1.8与Tomcat7运行环境部署经过严格调试导入数据库脚本并配置后即可稳定运行配套文档对各功能模块及业务流程进行说明可帮助读者快速掌握系统全貌为二次开发和功能扩展提供基础。目前已有2736人学习/下载对于需要完成Java Web方向毕业设计或医院信息化实战项目的开发者是一份完整、可靠且可直接借鉴的参考资料。1. 医院门诊挂号系统的SSM落地为什么这套组合现在还有生命力第一次听说用SSM JSP做挂号系统时我第一反应是“这不是十年前的标配吗”。但这两年实际帮人接手维护过两个医院内网项目后我发现这套技术栈在中小型医院信息系统里的存量非常大部署简单、人员上手快、不需要微服务那套基础设施门诊挂号和排班这种业务量级用SSM完全扛得住。这个项目就是一套完整的医院门诊挂号系统后端用Java SSM框架前端用Vue和JSP结合附带项目源码、MySQL数据库脚本和功能介绍文档业务覆盖了患者注册登录、科室浏览、医生排班、在线挂号、挂号记录查询、后台管理这几条主流闭环。适合两类人一类是准备用Java Web方向做毕业设计的学生可以直接拿这套源码当底子另一类是需要在医院内网环境搭建或维护挂号系统的开发人员可以参考它的表结构和业务流程。下面按我拆项目的顺序展开。2. 三层架构与Service层事务SSM在挂号场景下怎么分工2.1 Spring容器管理IoC、依赖注入与事务边界SSM的核心是Spring的IoC容器。正常情况下Service不直接new一个MapperController也不直接new Service而是全部通过注解交给Spring管理。看到这份源码时第一眼先找的就是Service、Controller、Repository这些注解有没有打全有没有直接new出来的对象。Service public class RegistrationServiceImpl implements RegistrationService { Autowired private RegistrationMapper registrationMapper; Autowired private ScheduleMapper scheduleMapper; Override Transactional(rollbackFor Exception.class) public boolean createRegistration(Registration reg) { // 扣减号源余量返回受影响行数作为并发校验 int updateRows scheduleMapper.decreaseAvailableCount(reg.getScheduleId(), 1); if (updateRows 0) { throw new RuntimeException(号源余量不足或排班不存在); } // 写入挂号单 int insertRows registrationMapper.insert(reg); return insertRows 1; } }这段代码是挂号系统的核心事务。Transactional(rollbackFor Exception.class)这个参数很关键默认情况下RuntimeException才触发回滚但加上Exception.class后任何Exception子类都会回滚。很多人在SSM项目里发现“数据写了一半没回滚”多半就是没写这个参数。另外一个细节是decreaseAvailableCount返回的是int——这是MyBatis执行update语句后返回的受影响行数。在并发场景下两个请求同时来抢最后一个号数据库行锁保证只有一个update成功另一个要么等待要么受影响行数为0这样就不会出现“号被抢完但还能挂号”的幽灵号问题。2.2 SpringMVC的请求链路与JSP视图解析SpringMVC的处理流程是请求先进DispatcherServlet然后通过HandlerMapping找到对应的Controller方法方法处理完返回一个ModelAndView再经由ViewResolver解析成具体的JSP页面。这套挂号系统的Controller层写法很典型可以直接照抄Controller RequestMapping(/registration) public class RegistrationController { Autowired private RegistrationService registrationService; Autowired private DepartmentService departmentService; RequestMapping(/toAdd) public String toAdd(RequestParam(scheduleId) Integer scheduleId, Model model) { Schedule schedule registrationService.findScheduleById(scheduleId); model.addAttribute(schedule, schedule); return registration/add; } RequestMapping(/save) public String save(Registration reg, HttpSession session) { Patient patient (Patient) session.getAttribute(loginPatient); reg.setPatientId(patient.getId()); reg.setStatus(0); // 0已挂号,1已就诊,2已取消 registrationService.createRegistration(reg); return redirect:/registration/myList; } }这里有两个容易忽略的参数细节。第一Model对象往JSP页面传值后在JSP里用${schedule.doctorName}、${schedule.deptName}这种方式直接取这就是EL表达式与Model数据绑定的配合。第二save方法里从HttpSession里拿当前登录患者信息而不是信任页面上传的patientId这个习惯可以防止用户伪造参数去给别的患者挂号。视图解析器在SSM的XML配置里的写法通常是bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/jsp// property namesuffix value.jsp/ /bean这意味着Controller返回的字符串registration/add会落到/WEB-INF/jsp/registration/add.jsp。如果视图解析失败最常见的原因是返回的字符串和JSP文件路径对不上后面避坑章还会展开。2.3 MyBatis映射器与动态SQL号源扣减的核心写法MyBatis在SSM中的作用是简化JDBC操作。这里要关注它的XML映射文件尤其是排班表中号源余量的更新。update iddecreaseAvailableCount UPDATE schedule SET available_count available_count - #{count} WHERE id #{scheduleId} AND available_count #{count} /update这条SQL的精髓是用available_count #{count}这个条件做数据库层面的乐观锁。执行update时如果余量不够受影响行数就是0Java代码检测到这个0就能立刻抛出异常回滚事务。这比先查再判断的写法要安全得多因为在“先查后改”的间隙另一个请求可能已经把最后一个号抢走了。MyBatis用起来还有个需要注意的地方Mapper接口和XML的namespace必须一一对应接口方法名和XML里的id必须严格一致。很多人在这个项目里遇到BindingException: Invalid bound statement基本都是因为这两个对不上。3. 环境搭建与启动验证从数据库导入到Tomcat部署3.1 MySQL 5.7数据库脚本导入拿到项目后不要急着改代码先把数据库跑起来。我用的是MySQL 5.7项目指定的版本如果你装了MySQL 8.0也能跑但要注意驱动名和时区参数的差别后面避坑章会说。连接MySQL后执行导入脚本。项目里提供的是.sql脚本文件命令行操作方式mysql -uroot -p你的密码 --default-character-setutf8mb4 hospital_ssm.sql如果你用的是Navicat就新建一个数据库字符集选utf8mb4排序规则选utf8mb4_general_ci然后右键“运行SQL文件”导入脚本。导入完成后用show tables;检查核心表是否存在USE hospital; SHOW TABLES;常见的表有t_admin管理员表、t_patient患者表、t_department科室表、t_doctor医生表、t_schedule排班表、t_registration挂号单表。如果发现表名和你预想的不一样先打开数据库脚本体看表结构不要直接改代码里的表名一般以脚本为准。3.2 修改JDBC配置并导入开发工具打开src/main/resources下的jdbc.properties或db.properties核心配置是这么几行jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai jdbc.usernameroot jdbc.password123456如果你用的是MySQL 5.7和Tomcat 7/8驱动写com.mysql.jdbc.Driver没问题如果用了MySQL 8.0或更高版本驱动必须改成com.mysql.cj.jdbc.Driver且serverTimezone参数通常需要显式声明否则会因为时区问题启动报错。导入项目到IDEA时注意SSM项目一般有src/main/java、src/main/resources、src/main/webapp三层目录结构。IDEA里操作路径是File → New → Project from Existing Sources → 选择项目根目录 → 选择Maven。等依赖下载完成后检查pom.xml里的依赖是否完整。如果下载很慢建议把Maven镜像换成阿里云的。3.3 启动Tomcat并验证主流程Tomcat版本我用的是Tomcat 8.57都能跑8.5最稳妥。IDEA里配置Tomcat的路径Run → Edit Configurations → 添加Tomcat Server → LocalApplicationServer选到Tomcat安装目录Deployment里把项目war exploded部署上去。启动后观察控制台日志看到 Things likeLoaded JDBC driverandMapped {[/registration/save]}就说明Spring容器和MyBatis映射都初始化成功了。访问路径通常是http://localhost:8080/ssm9008/context path可能不同先走一遍患者注册→登录→选科室→查看排班→挂号→查看挂号记录的流程。我拿到一个SSM项目习惯是先打开web.xml看SpringMVC的DispatcherServlet配置里面能看出路径匹配规则servlet-mapping servlet-namespringmvc/servlet-name url-pattern//url-pattern /servlet-mappingurl-pattern是/所有请求都交给SpringMVC处理静态资源CSS、JS、图片必须在SpringMVC的配置里额外放行。如果放出的是/还是*.do处理思路完全不同。这个细节在避坑章节展开。4. 核心业务模块拆解挂号单生成、排班联动与页面交互4.1 从选科室到生成挂号单的完整链路这个流程可以拆成前端三个页面和一个后端入口。第一个页面是科室列表页展示所有科室点击科室后跳到医生排班页选择某个排班时段后点击“挂号”弹出确认框提交到/registration/save。后端拿到请求后的处理逻辑是三步构造Registration对象、设置患者ID和初始状态、调用Service层事务方法。前面已经看到了Service代码这里补一个查询排班详情的方法Override public Schedule findScheduleById(Integer id) { Schedule schedule scheduleMapper.selectById(id); if (schedule ! null) { Doctor doctor doctorMapper.selectById(schedule.getDoctorId()); schedule.setDoctorName(doctor.getDoctorName()); schedule.setDeptName(departmentMapper.selectById(doctor.getDeptId()).getDeptName()); } return schedule; }这段代码暴露了SSM项目的一个常见特点表格之间通过外键关联但页面展示需要冗余字段。比如schedule表里存了doctor_idJSP页面上要显示医生姓名和科室名称所以Service层组装数据时把关联信息填充进去。如果你在二次开发时要加字段记得同步检查Service层有没有补充查询。4.2 医生排班与号源余量的联动实现排班表的结构大致是字段类型说明idint主键doctor_idint医生IDdept_idint科室IDwork_datedate出诊日期time_slotvarchar上午/下午/晚间total_countint总号源数available_countint剩余号源数这里的核心逻辑是管理员录入排班时初始化total_count和available_count患者每挂一次号available_count减1减到0时这个时段就不能再挂。前端的展示逻辑是判断available_count 0才显示挂号按钮。排班管理这块在后台管理模块里一般是管理员登录后进入排班管理页面可以新增排班记录、设置总号源数。这里有个设计取舍用是“每天手动设置排班”还是“按周规则自动生成”这套系统用的是前者简单直接适合毕设演示。4.3 Vue与JSP配合的页面交互细节项目前端是Vue和JSP混合的实际情况是JSP负责页面骨架和JSTL标签跳转Vue通过CDN方式引入负责局部数据绑定和异步交互。这种做法在SSM项目里很常见JSP负责服务端渲染Vue负责局部动态刷新两者通过AJAX接口衔接。看一个典型的挂号记录列表页面交互// 在JSP页面内嵌的Vue代码 new Vue({ el: #registrationApp, data: { pageData: [], currentPage: 1, totalPage: 0 }, created: function () { this.loadData(); }, methods: { loadData: function () { var self this; $.ajax({ url: /registration/myListData, type: GET, data: { pageNum: self.currentPage, pageSize: 10 }, dataType: json, success: function (res) { self.pageData res.list; self.totalPage res.totalPage; }, error: function () { alert(加载挂号记录失败); } }); } } });后端对应一个查询方法返回JSON数据。这里的关键是后端返回的字段名要和Vue模板里的{{ }}插值对应比如registTime、doctorName、deptName、statusText等。如果Vue页面显示的字段全是undefined基本都是因为实体类字段名和JSON默认序列化字段名不一致。JSP这块还要注意JSTL标签的引入路径% taglib prefixc urihttp://java.sun.com/jsp/jstl/core %在web.xml所在的项目里JSTL依赖要在pom.xml里配上否则JSP页面一用到c:forEach就会报错。5. 避坑SSM JSP在挂号场景下的五个高频问题5.1 MySQL驱动类名报错导入后直接ClassNotFoundException现象启动Tomcat时控制台报ClassNotFoundException: com.mysql.jdbc.Driver或者连接数据库时被拒绝。原因本地MySQL版本和驱动不匹配。MySQL 5.7配的驱动是com.mysql.jdbc.Driver如果本地是MySQL 8.0以上这个类仍然会触发警告但不一定会出错更常见的是时区异常。另一种常见情况是pom.xml里没加mysql-connector-java依赖或版本太低。解决打开pom.xml确认依赖版本另外把驱动类名改成com.mysql.cj.jdbc.DriverURL参数加上时区dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.30/version /dependencyjdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai5.2 JSP页面中文乱码患者姓名显示成问号现象从数据库查出患者姓名后在JSP页面上显示为???或者控制台打印SQL日志时中文正常但页面上乱码。原因多层编码不一致。MySQL数据库表是latin1或gbkJSP页面是Content-Type: text/html; charsetUTF-8SpringMVC的CharacterEncodingFilter没有配置或过滤顺序不对。解决第一步先确认数据库表字符集用SHOW CREATE TABLE t_patient;看表的CHARSET如果不是utf8mb4或utf8需要转换ALTER TABLE t_patient CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;第二步检查web.xml里有没有注册编码过滤器注意url-pattern要覆盖所有请求filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping注意forceEncoding要配成true这个参数保证response输出时也强制用UTF-8否则只过滤了请求没过滤响应。5.3 幽灵号源并发挂号导致余量被超卖现象页面显示还有1个号两个患者同时点挂号结果都成功了号源余量变成负数。原因代码里先select查余量、判断大于0、再update减一这三个操作不是原子的。两个请求同时查到余量为1都通过了判断各自执行update后余量变成-1。这是并发环境下典型的“非原子性操作”问题。解决把判断逻辑下沉到SQL里用带条件的update实现原子扣减已经在第2章贴过decreaseAvailableCount的XML写法。统一使用这个MySQL原子操作严格依赖available_count #{count}条件。这是在SSM项目里最容易翻车的细节如果毕设被问到“怎么防止超卖”能讲清楚这条SQL就已经比大多数同学深入很多。5.4 静态资源加载失败前端页面裸了现象JSP页面打开只有文字没有样式控制台一堆404报错信息类似Failed to load resource: the server responded with a status of 404 (Not Found) /css/style.css。原因web.xml里DispatcherServlet的映射是/把静态资源请求也拦截了。SpringMVC默认没有开启静态资源处理所以浏览器请求CSS和JS会404。解决在SpringMVC配置文件中添加资源映射mvc:resources location/static/ mapping/static/**/ mvc:annotation-driven/同时需要确保mvc:annotation-driven/存在它负责注册RequestMappingHandlerAdapter等处理器。配置完后/static/js/app.js这类路径就能正常加载不经过Controller方法。5.5 连接超时与时区不一致数据库偶尔断连现象系统运行一段时间后第一次访问特别慢或报错The last packet successfully received from the server was 56,580,137 milliseconds ago时区问题则报The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。原因MySQL的wait_timeout默认8小时SSM项目的数据库连接池如果没有做空闲连接回收超过8小时的空闲连接会被MySQL服务端断开连接池不知道就继续派发给业务线程导致第一次访问报错。时区问题则是MySQL版本差异导致的5.7不强制要求serverTimezone8.0默认时区是UTC且校验严格。解决JDBC URL里加上serverTimezoneAsia/Shanghai连接池配置空闲回收参数。如果用的是dbcp或druid在Spring的dataSource配置里加property nametestWhileIdle valuetrue/ property nametimeBetweenEvictionRunsMillis value60000/ property namevalidationQuery valueSELECT 1/这段配置的意义是每分钟检测一次空闲连接用SELECT 1做探活有问题的连接提前清理掉避免把“死连接”发给业务方法。6. 在源码基础上做二次开发取消预约功能与AOP操作日志6.1 添加一个取消预约入口拿到这套源码后建议不要只跑通就完了想办法动一个小功能点来验证自己的理解。我一般会让学生先加“取消预约”功能这个功能虽然简单但涉及整个SSM链路的改动。先打开RegistrationMapper.xml加一条SQLupdate idcancelById UPDATE t_registration SET status 2 WHERE id #{id} AND status 0 /update注意AND status 0这个条件——只有已挂号状态才能取消已就诊的不能取消这能避免数据状态被错误覆盖。然后Service层要同时把号源余量加回去Override Transactional(rollbackFor Exception.class) public boolean cancelRegistration(Integer regId, Integer scheduleId) { registrationMapper.cancelById(regId); scheduleMapper.increaseAvailableCount(scheduleId, 1); return true; }还必须在t_registration表里加一个patient_id的查询条件吗其实不用因为前端请求里带着regId后端通过regId查一遍挂号记录确认归属再执行操作会更安全。可惜原项目没有这层校验所以这个位置是一个很好的改进点把“按id取消”升级为“按id patientId取消”。6.2 用AOP记录操作日志第二个推荐加的改进点是AOP操作日志。门诊挂号系统有比较强的审计需求——谁在什么时候给哪个患者挂了号。用Spring AOP可以在不改原业务代码的前提下记录这些操作。Aspect Component public class OperationLogAspect { private static final Logger logger LoggerFactory.getLogger(OperationLogAspect.class); Around(execution(* com.hospital.service.RegistrationService.createRegistration(..))) public Object logRegistration(ProceedingJoinPoint pjp) throws Throwable { long start System.currentTimeMillis(); Object result pjp.proceed(); long spent System.currentTimeMillis() - start; logger.info(挂号操作耗时:{}ms, 结果:{}, spent, result); return result; } }这个切面的execution表达式表示拦截RegistrationService接口里所有createRegistration方法的执行。pjp.proceed()会调用原始方法前后你可以做日志记录、耗时时长统计这些横切逻辑。把这套代码加到项目里再观察一下AOP是否真的生效注意spring-aop依赖和aop:aspectj-autoproxy配置别漏掉。我记得自己当年第一次切AOP没生效查了一圈才发现是Spring配置文件里少了aop:aspectj-autoproxy/这行。从那以后我每次拿到一个新的SSM项目都会先做一个完整的业务链路走查确认每一层的参数传递和注解都符合预期再开始改代码。二次开发时坚持“条件性更新”的SQL写法并发问题能挡掉一大半。希望这些拆解和踩坑记录对你跑通这套医院门诊挂号系统有帮助。本文还有配套的精品资源点击获取