
简介面向Java/计算机相关专业毕业生这套医院门诊挂号系统源码包定位为毕业设计或课程设计的一站式参考方案尤其适合需要从零搭建JavaWeb项目、快速产出论文与答辩材料的学习者。系统基于JSP与MySQL数据库构建贴近医院门诊挂号的实际管理场景开发过程覆盖需求分析、总体设计、详细设计与功能测试等环节代码在可读性、实用性、易扩展性和可维护性方面做了充分考虑既能帮助JavaWeb初学者理解项目分层结构也便于在此基础上二次扩展功能。压缩包为ZIP格式大小15.34MB包括项目源码、数据库脚本、开题报告、论文、答辩PPT与使用说明文档源码可直接导入IDE查看数据库脚本可快速建表论文与PPT则为文档撰写和答辩汇报提供现成框架整体目录安排便于按需检索。当前已有59人浏览学习可用于了解该项目的整体结构与开发思路下载后对照使用说明即可运行系统再结合论文梳理功能模块与数据库设计就能高效完成项目复现、论文打磨与答辩准备是一份完整的JavaWeb毕业设计参考资料。1. 医院门诊挂号系统为什么 SSMJSP 这套组合到今天还是毕业设计的最优解医院门诊挂号系统是 SSM 框架Spring SpringMVC MyBatis加 JSP 页面层做出来的典型 Java Web 项目也是高校软件工程、计算机科学与技术专业毕业设计里出现频率最高的一类选题。它的核心业务很明确患者登录后选科室、看医生排班、挂号、取消挂号医生和后台管理员维护排班、统计号源。业务链条完整但不复杂正好把 Java 后端开发里最重要的三块——依赖注入、请求路由、数据库增删改查——全部串起来这就是它常年被当作课程设计和毕业设计题目的原因。对正在找毕业设计方向、或者想拿一套能直接跑通的项目来练手的人来说这个系统最大的价值在于技术栈老但不旧JSP 虽然在互联网公司里用得少了但高校的教学和答辩环境里它仍然是主流代码量适中一个人能在两三周内把原理讲清楚、把代码改明白。相比 Spring Boot 打包好的“开箱即用”SSMJSP 需要你手动配置 Spring 容器、写 MyBatis 映射、部署到 Tomcat这个过程本身就逼着你把底层机制弄懂而不是只会在编辑器和浏览器之间点运行。下文从架构拆解开始一步步讲清楚怎么把系统跑起来、核心逻辑怎么写、部署时最容易在哪些地方翻车。2. SSMJSP 架构拆解Spring、SpringMVC、MyBatis 和 JSP 各干了什么活2.1 Spring 的 IoC 容器把对象创建和管理交给容器而不是到处 new在纯 Servlet 时代一个 Service 要用到 Mapper代码里就得new一个实现类依赖关系写死在代码里想换实现得改源码。SSM 里的 Spring 做了一件“反向”的事对象的创建、初始化、销毁全部交给 Spring 容器管理程序员只通过注解或 XML 声明“我要用什么”。在这个挂号系统里典型写法是Service public class RegistrationService { Autowired private ScheduleMapper scheduleMapper; Autowired private RegistrationMapper registrationMapper; }Service把这个类标记为业务层 Bean 交给 Spring 容器Autowired按类型自动注入 Mapper。这样做的直接好处是层与层之间解耦单元测试时可以用 mock 对象替换真实 Mapper不用启动整个 Tomcat。启动项目时 Spring 容器会扫描applicationContext.xml或ComponentScan配置的包路径把标注了Component、Service、Repository、Controller的类实例化并注册。对这个毕业设计来说理解 IoC 的最低要求是如果你在启动日志里看到NoSuchBeanDefinitionException说明对象没有被扫描到检查包扫描范围是否正确、类上有没有加对应注解。面试时被问“什么是控制反转”拿这个项目里的RegistrationService举例子比背概念强得多。2.2 SpringMVC一次挂号请求从 URL 到 JSP 页面的完整链路SpringMVC 负责 HTTP 层核心是一个叫DispatcherServlet的前端控制器。用户点了“立即挂号”浏览器发出POST /registration/add请求处理过程是DispatcherServlet先拦截请求交给处理器映射器找RegistrationController里的对应方法方法执行完返回一个视图名再交给视图解析器拼出 JSP 路径最终渲染成 HTML 返回浏览器。Controller RequestMapping(/registration) public class RegistrationController { Autowired private RegistrationService registrationService; PostMapping(/add) public String add(RequestParam Integer scheduleId, RequestParam Integer userId, Model model) { try { registrationService.register(userId, scheduleId); model.addAttribute(message, 挂号成功请按预约时间就诊); } catch (BusinessException e) { model.addAttribute(message, e.getMessage()); } return registration/result; } }这段代码里的RequestParam负责把表单里的scheduleId、userId绑定到方法参数上Model是后端向 JSP 页面传递数据的载体。需要注意的是方法返回的并不是完整页面路径而是逻辑视图名registration/result最终由视图解析器前缀后缀拼接成/WEB-INF/views/registration/result.jsp。这种约定让 Controller 里不再出现.jsp字符串目录调整时不用改 Java 代码。2.3 MyBatis把数据库增删改查变成 Mapper 接口和一条 SQLMyBatis 解决的痛点是 JDBC 样板代码以前查一条数据要写Class.forName、DriverManager.getConnection、PreparedStatement、ResultSet再手动封装成对象。MyBatis 把这层封装掉你只需要定义 Mapper 接口再写一个 XML 文件描述 SQL。public interface ScheduleMapper { Schedule findById(Integer id); int decreaseRemain(Integer id); }update iddecreaseRemain UPDATE t_schedule SET remain remain - 1 WHERE id #{id} AND remain 0 /updatedecreaseRemain是这个系统里最关键的一条 SQL后面讲防超卖时还会再提到它。MyBatis 底层会拿接口的id去定位 XML 命名空间里的语句通过动态代理生成实现类。配置 MyBatis 时要注意两个地方一是mapper-locations指向 XML 所在目录二是实体类字段与数据库列的映射关系。如果表名是下划线风格而实体类用驼峰命名记得开启mapUnderscoreToCamelCase配置否则你会发现查询结果所有字段都是 null。2.4 JSP 页面层为什么这个项目仍坚持用 JSP而不是纯 HTML标题里的 JSP 是这套系统区别于 Spring Boot 项目的关键。JSP 本质上是 Servlet第一次被访问时容器会把它编译成 Java 类执行。它的优势在于能直接在页面里写 Java 片段、使用 EL 表达式和 JSTL 标签渲染后端数据。% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % table c:forEach vardoctor items${doctorList} tr td${doctor.name}/td td${doctor.departmentName}/td td${doctor.title}/td /tr /c:forEach /table${doctor.name}会调用Doctor对象的getName()方法get前缀被省略。这就是 jsp 个人信息展示页面里最常见的写法——后端往request里塞一个 ListJSP 用c:forEach遍历输出。用 JSP 做页面层的另一个现实理由是答辩环境兼容性好校园网环境下加载静态资源经常很慢JSP 完全由 Tomcat 本地渲染不依赖外部 CDN这在做系统演示时是实打实的优势。2.5 标准分层目录先看懂包结构后面调代码才不迷路这套系统的源码目录几乎都是同一个模子建议按下面的包结构去对照阅读src/main/java ├── com/hospital │ ├── controller // SpringMVC 控制层 │ ├── service // 业务逻辑层接口 impl │ ├── mapper // MyBatis Mapper 接口 │ ├── entity // 数据库实体类 │ └── common // 统一返回结果、异常定义 src/main/resources ├── mapper // MyBatis XML 映射文件 ├── jdbc.properties // 数据库连接配置 ├── spring-mvc.xml // SpringMVC 配置 ├── spring-mybatis.xml // Spring MyBatis 整合配置 src/main/webapp ├── WEB-INF │ ├── web.xml // Web 应用入口配置 │ └── views // JSP 页面目录 ├── static // CSS、JS、图片 └── index.jsp这个结构对应着一条数据流动线浏览器 → Controller → Service → Mapper → MySQL数据再原路返回 JSP。毕业设计答辩时老师最喜欢拿着这个包结构问“为什么 Controller 不直接操作数据库”答案就在这条线上业务规则校验、事务控制都在 Service 层Controller 只做参数接收和视图跳转数据库访问能力被 MyBatis 限制在 Mapper 层。应届生如果能把这条线用自己的话讲顺属于加分项因为这正是 java 工程师日常开发的骨架也是面试必问的 Java 基础知识之一。3. 跑通这个系统数据库、配置文件、Tomcat 三步到位3.1 环境版本匹配JDK、Tomcat、MySQL、Maven 的推荐组合SSM 项目对版本比较敏感不是说越新越好很多“跑不起来”的案例其实是版本搭配出了问题。常见的稳定组合是JDK 1.8、Tomcat 8.5、MySQL 5.7、Maven 3.6。JDK 8 兼容 Spring 4/5Tomcat 8.5 完全支持 JSP 2.3 规范MySQL 5.7 是多数高校机房还在用的数据库版本。有同学用 JDK 17 或 Tomcat 10 跑这套项目会遇到NoClassDefFoundError或javax.servlet包找不到的问题。原因是 Tomcat 10 把包名从javax.servlet改成了jakarta.servlet而 SSM 项目里所有依赖都基于javax。如果你的机器上装的是 JDK 11建议直接用项目自带的运行环境或者退回到 JDK 8。这不是“老古董”而是这个技术栈在版本边界上的客观限制。3.2 导入数据库脚本先看懂表结构再调程序拿到项目后先不要急着配 IDEA第一步是建库。SQL 脚本一般放在db或sql目录下打开后先看建表语句判断这个系统的业务范围。常见的医院门诊挂号系统至少包含五张表表名用途关键字段t_department科室表id, name, locationt_doctor医生表id, name, department_id, title, avatart_schedule排班表id, doctor_id, work_date, start_time, end_time, total, remaint_user用户表id, username, password, real_name, phone, rolet_registration挂号记录表id, user_id, schedule_id, reg_time, status其中t_schedule表是整个系统的心脏total是这一班次的总号源数remain是剩余号源数。挂号操作的本质就是对这张表做一次“减库存”。导入脚本使用命令mysql -u root -p hospital.sql导入之后执行show tables;确认五张表都在。再顺手跑一句SELECT s.id, d.name AS doctor_name, dep.name AS dept_name, s.work_date, s.start_time, s.end_time, s.total, s.remain FROM t_schedule s JOIN t_doctor d ON s.doctor_id d.id JOIN t_department dep ON d.department_id dep.id WHERE s.remain 0;先把这三表联查的结果拿到确认基础数据正常。这一步能帮你验证数据库是否真的导成功了也能让你提前熟悉后面排班列表页要用的查询逻辑。在 MySQL 5.7 中导入脚本常见的问题是字符集建表语句里如果没有utf8mb4插入中文患者姓名会报Incorrect string value解决方法是导入前执行set names utf8mb4;或者直接用 Navicat 以 UTF-8 编码执行脚本。3.3 修改 jdbc.properties每个路径都不能错数据库导入成功后打开src/main/resources/jdbc.properties文件这是连接数据库的唯一配置入口jdbc.drivercom.mysql.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8useSSLfalse jdbc.usernameroot jdbc.password123456参数说明3306是 MySQL 默认端口hospital_db要和你实际创建的库名一致characterEncodingutf8保证中文写入数据库时不乱码useSSLfalse是为了避免 MySQL 5.7 在本地连接时出现 SSL 连接告警。driverClass用的是com.mysql.jdbc.Driver这个类在mysql-connector-java5.x 版本里存在如果你在 pom.xml 里换成了 MySQL 8.x 驱动这里必须改成com.mysql.cj.jdbc.Driver并且 URL 需要追加serverTimezoneAsia/Shanghai否则启动时报错The server time zone value Öйú±ê׼ʱ¼ä is unrecognized。这套配置改完后别急着启动还有第二个文件要看spring-mybatis.xml。重点检查两个 bean一个是数据源dataSource它的属性值引用jdbc.properties里的键另一个是SqlSessionFactoryBean的mapperLocations常见写法是property namemapperLocations valueclasspath:mapper/*.xml /这个路径写错的话项目能启动但一调用 Mapper 方法就报Invalid bound statement (not found)。排查时先确认src/main/resources下有没有mapper目录以及目录里是否真放了对应 XML。很多所谓“源码缺东西”的报错根本原因是路径不一致。3.4 部署到 TomcatIDEA 配置和手动部署两种方式项目是 war 包结构部署方式看你用哪个 IDE。IDEA 里如果是 Maven 项目直接运行mvn clean package打包如果是带.mvn目录的传统项目直接用 Tomcat 插件启动。常见做法是配置一个本地 Tomcat ServerRun → Edit Configurations → 左上角 → Tomcat Server → Local在 Deployment 页签里点 选择hospital:war explodedApplication context 填/hospital启动观察控制台日志看到Starting ProtocolHandler说明启动成功手动部署的流程则更直观把打包出来的hospital.war拷到 Tomcat 的webapps目录启动 Tomcat 后它会自动解压。访问入口是http://localhost:8080/hospital/如果首页配置的是login.jsp必须通过这个上下文路径访问否则会出现 Tomcat 默认首页而看不到项目页面。3.5 完成一次登录验证三种角色分别测一遍启动成功后先做冒烟测试。医院门诊挂号系统通常有三种角色管理员、医生、患者分工如下管理员维护科室、医生信息查看挂号统计使用system或admin账号登录医生查看自己的排班表、患者挂号记录可能包含“接诊”操作患者选科室找医生、查看号源、挂号、取消挂号先用管理员登录确认能进后台管理页面再注册一个患者账号或用脚本里预置的患者账号走一遍“科室列表 → 医生列表 → 点击挂号 → 挂号成功 → 我的挂号记录”的完整链路。这一步看起来简单但能一次性暴露三类问题页面跳转路径错误、Session 存储用户信息不一致、挂号记录插入时外键关联错误。冒烟测试过了再继续往下读核心代码。4. 核心代码读法门诊挂号最重要的三处逻辑4.1 排班查询医生列表与号源信息的 jsp 展示页面排班查询是挂号流程的入口它要解决一个问题患者打开页面时能按科室看到今天的哪些医生有号、还剩多少号。后端 Service 里组装数据Controller 塞进 Model最后由 JSP 用表格渲染出来。Service 层核心逻辑public ListScheduleVO listAvailableSchedules(Integer departmentId, String workDate) { ListSchedule schedules scheduleMapper.selectByDeptAndDate(departmentId, workDate); ListScheduleVO result new ArrayList(); for (Schedule schedule : schedules) { Doctor doctor doctorMapper.findById(schedule.getDoctorId()); Department dept departmentMapper.findById(doctor.getDepartmentId()); ScheduleVO vo new ScheduleVO(); vo.setScheduleId(schedule.getId()); vo.setDoctorName(doctor.getName()); vo.setDoctorTitle(doctor.getTitle()); vo.setDeptName(dept.getName()); vo.setWorkDate(schedule.getWorkDate()); vo.setStartTime(schedule.getStartTime()); vo.setEndTime(schedule.getEndTime()); vo.setRemain(schedule.getRemain()); result.add(vo); } return result; }这段代码逻辑很直白但它暴露了很多人写 SSM 项目的通病在 Service 里循环查数据库。如果一个科室有 10 个医生就多出 10 次查询。优化方式是改用一个连表查询 Mapper 一次性返回结果select idselectScheduleVO resultTypecom.hospital.vo.ScheduleVO SELECT s.id AS scheduleId, d.name AS doctorName, d.title AS doctorTitle, dep.name AS deptName, s.work_date AS workDate, s.start_time AS startTime, s.end_time AS endTime, s.remain AS remain FROM t_schedule s JOIN t_doctor d ON s.doctor_id d.id JOIN t_department dep ON d.department_id dep.id WHERE s.remain 0 AND s.work_date #{workDate} if testdepartmentId ! null AND dep.id #{departmentId} /if ORDER BY s.start_time /selectif是 MyBatis 动态 SQL 的写法段里没有部门 ID 时不过滤比如“全部科室”页面就不传这个参数。resultType 指向 VO 类MyBatis 会自动把带别名的字段映射进对象。改完这一处循环被干掉代码量少了一半。JSP 页面拿到scheduleVOList后就是 jsp 个人信息展示页面或医生排班列表里最常见的c:forEach遍历渲染这个模式在本系统里到处复用。4.2 挂号扣减号源事务和乐观锁防止同一号源被抢两次挂号是整个系统最需要小心的地方。两个患者同时点“挂号”如果代码先查remain再更新可能出现两个人都查到剩余 1 个号然后都扣成 0最终同一排班被挂出两张号。这就是典型的超卖问题。正确做法是让“判断是否有号”和“扣减号源”合并成一条带条件的原子更新Transactional(rollbackFor Exception.class) public void register(Integer userId, Integer scheduleId) { int rows scheduleMapper.decreaseRemain(scheduleId); if (rows 0) { throw new BusinessException(该医生当前号源已挂满请选择其他时段); } Registration registration new Registration(); registration.setUserId(userId); registration.setScheduleId(scheduleId); registration.setRegTime(new Date()); registration.setStatus(1); registrationMapper.insert(registration); }对应的 Mapper SQL 就是前文提过的update iddecreaseRemain UPDATE t_schedule SET remain remain - 1 WHERE id #{scheduleId} AND remain 0 /update这里的关键是AND remain 0放到 UPDATE 语句的条件里。MySQL 执行 UPDATE 时会先按条件锁定匹配的行两条并发请求同时到达时后到的那个会因为锁等待而读不到remain 0的行更新行数为 0。Transactional保证了扣减和插入必须同时成功或同时失败如果插入挂号记录时报错扣减号源也会回滚不会出现“号扣了但挂号记录没生成”的脏数据。顺便说一句这条代码在答辩时最容易被追问。“为什么不用 synchronized”因为synchronized只能锁住单个 Tomcat 实例内的线程线上部署多台 Tomcat 时锁就失效了。而数据库行锁天然支持分布式环境这也是真正的 java 工程师开发中处理库存扣减的常见方案。你能说出这一层比背八股文要加分不少。4.3 取消挂号号源释放与状态更新的回滚配合取消挂号的逻辑正好和挂号相反把t_registration的状态从 1有效改成 0已取消同时把t_schedule的remain加回 1。业务上有一个边界条件要先想清楚只能取消未发生的挂号也就是预约时间在当前时间之后的才能取消否则医生都开始接诊了号源释放出来也对不上号了。Transactional public void cancel(Integer registrationId, Integer userId) { Registration reg registrationMapper.findById(registrationId); if (reg null || !reg.getUserId().equals(userId)) { throw new BusinessException(挂号记录不存在或无权操作); } if (reg.getStatus() 0) { throw new BusinessException(该挂号已取消请勿重复操作); } registrationMapper.updateStatus(registrationId, 0); scheduleMapper.increaseRemain(reg.getScheduleId()); }increaseRemain对应的 SQL 是UPDATE t_schedule SET remain remain 1 WHERE id #{id}。注意两个 Mapper 操作必须在同一个事务里否则会出现挂号记录取消成功了但号源没恢复的情况长时间积累会导致号源丢失。这套系统的所有“资源增减”类操作都应该在 Service 层加Transactional注解这也是一条通用的代码评审标准。4.4 数据库增删改查的规范写法实体类、Mapper、Service 三层约定看过上面几个核心方法后你会发现这套系统里增删改查有固定套路实体类只放字段和 getter/setterMapper 接口只声明方法Service 负责处理业务规则。如果是刚接触 java 课程设计很容易在 Controller 里直接写 SQL 或直接用Map接参数短时间能跑但答辩时经不起问。保持“一个实体对应一个 Mapper”的约定能省下大量排查时间。一个容易被忽略的细节是t_user表里的role字段它用的是String类型值分别是patient、doctor、admin。做登录拦截时不能只看用户名密码是否匹配还要根据角色决定跳转首页。SSM 里实现角色拦截有三种写法HandlerInterceptor拦截器按 Session 里的role判断、Filter 过滤指定 URL 模式、或者在每个 Controller 方法里手动判断。毕业设计用拦截器是性价比最高的选择代码集中且好讲。5. 从部署到答辩五条最容易翻车的坑与排查顺序5.1 首页 404JSP 文件存在但一直找不到现象Tomcat 启动成功访问http://localhost:8080/hospital/报 404或者跳转到 Tomcat 默认首页而不是登录页。原因项目没有正确部署到 Tomcat 的 webapps 目录或者web.xml里没有配置欢迎页。另一个隐蔽原因是 IDEA 部署时用了hospital:war而不是hospital:war exploded后者会产生临时目录上下文路径对不上。解决先确认访问路径是否带应用名。手动部署时默认是/hospital如果项目名不同路径跟着变。检查web.xml里的 welcome-file 列表是否包含login.jsp。最直接的办法是把项目部署后进入webapps/hospital/目录确认 JSP 文件是否真的被解压出来了如果目录为空就是打包环节出了问题回 Maven 重新 clean package。5.2 中文乱码请求参数和 MySQL 编码不一致现象页面上填写的“张伟”保存到数据库变成“å¼ æ ”或者从数据库查出后在 JSP 页面显示乱码。原因一条链路四个环节都要求编码一致——页面pageEncoding、请求过滤器、Tomcat 连接器 URI 编码、MySQL 连接参数。缺一个环节就会出乱码。尤其是 MySQL 5.7 的默认字符集可能是latin1而项目用 UTF-8 写入必然乱码。解决先在web.xml里配置编码过滤器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 /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping再确认jdbc.properties的 URL 里有characterEncodingutf8最后查看表结构语句SHOW CREATE TABLE t_user;看DEFAULT CHARSET是否为utf8或utf8mb4。如果表的字符集已经不对可以执行ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4;修改结构但注意同时要set names utf8mb4;让当前会话一致。5.3 Maven 依赖下载超时或 pom.xml 报红现象项目一打开IDEA 里所有 import 都标红pom.xml 依赖一直显示红色波浪线控制台提示Cannot resolve ...。原因SSM 项目很多依赖在阿里云 Maven 仓库或中央仓库都有但国内网络访问中央仓库不稳定。尤其是有些项目里写了repositories指向已被关停的仓库地址导致下载失败。解决在settings.xml里配置阿里云镜像mirror idaliyunmaven/id mirrorOfcentral/mirrorOf name阿里云公共仓库/name urlhttps://maven.aliyun.com/repository/public/url /mirror外加一个技巧SSM 老项目经常会用到javax.servlet-api等依赖IDEA 里如果标红检查 jar 包是否在本地仓库~/.m2/repository下存在全部文件。有时.lastUpdated后缀文件说明下载失败需要删掉整个目录重新拉取。5.4 数据库连接失败时区、驱动、防火墙一锅端现象启动项目时控制台报Cannot create PoolableConnectionFactory或Access denied for user rootlocalhost。原因这块常见有三个来源。一是 MySQL 5.7 与 8.x 的驱动类名不同pom.xml 里引的是旧驱动但本地库是 MySQL 8二是连接 URL 缺少时区参数三是 MySQL 启动后监听的端口不是默认的 3306。解决到命令行或 Navicat 里先用mysql -u root -p连一次确认用户名密码和端口都对。项目里jdbc.properties的driverClass和url要与本地 MySQL 版本匹配MySQL 8 的写法如下jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiserverTimezoneAsia/Shanghai加在 URL 参数里即可解决 MySQL 8 的时区报错。这个源的问题会先于业务代码暴露所以排查顺序永远放在乱码之前。5.5 Tomcat 启动失败端口被占用与内存溢出现象启动 Tomcat 时控制台报Address already in use: JVM_Bind或者弹出java.lang.OutOfMemoryError: PermGen space。原因端口被其它进程占用常见的是装了多个 Tomcat 或者 IDA 内置的 Tomcat 没有完全关闭。PermGen 是 JDK 8 之前经常出现的内存问题JDK 8 换成了 Metaspace但老项目如果配置的内存参数过小仍会出问题。解决Windows 下执行netstat -ano | findstr 8080查到占用 8080 的进程 PID在任务管理器里结束它或者在 Tomcat 的conf/server.xml里把端口改成 8081。内存参数在 IDEA 的运行配置里的 VM options 加-Xms512m -Xmx1024m。如果用的不是 IDEA 自带 Tomcat而是手动启动startup.bat则在catalina.bat开头加set JAVA_OPTS-Xms512m -Xmx1024m改完重启。6. 答辩前能补的三个亮点防超卖、图形统计、讲解口径6.1 用 Redis 给号源加“前置锁”把并发细节讲得更专业前面第 4 章说数据库的UPDATE ... WHERE remain 0已经解决了超卖但答辩时可以更进一步。数据库行锁方案在高并发下有一个缺点多个请求同时卡在数据库锁等待上吞吐量上不去。如果你在毕业设计中加入了 Redis可以做一个前置门槛把每班次的剩余号源异步同步到 Redis挂号前先执行decrement操作成功后再走数据库事务数据库核减失败时把回补的数据同步回 Redis。对比之下数据库方案强一致但吞吐低Redis 方案吞吐高但需要处理回补两个方案并排讲本身就是很好的答辩素材。6.2 用 ECharts 做门诊量统计让 JSP 页面不再是冷冰冰的表格答辩演示时一个带图表的统计页比表格有视觉冲击力得多。ECharts 是纯 JS 图表库在 JSP 里引入方式很简单script src${pageContext.request.contextPath}/static/echarts.min.js/script div idchart stylewidth: 800px; height: 400px;/div script var chart echarts.init(document.getElementById(chart)); chart.setOption({ title: { text: 各科室本周挂号量 }, tooltip: {}, series: [{ type: bar, data: ${countJson} }] }); /script这里countJson由 Controller 里用 Jackson 或 fastjson 把列表序列化成 JSON 后放进 Model。需要注意的是 JSP 页面里的${countJson}是 EL 表达式如果值为字符串外层要用引号包住避免数值被当成数字处理。这个图表的后端数据源是SELECT department_name, COUNT(*) FROM t_registration ... GROUP BY department_name简单两句 SQL 就能支撑。6.3 讲代码时用“面试官视角”串起来同样是“我做了医院门诊挂号系统”表达方式不同答辩结果完全不同。建议准备好三个问答口径第一“Spring 容器里哪个 Bean 是你自己写的它解决了什么问题”回答指向RegistrationService讲Transactional如何保证号源扣减和挂号记录的一致性再补充一句“如果去掉这个注解并发时会怎么样”老师立刻知道你是懂的。第二“MyBatis 的#{}和${}有什么区别”在项目里的decreaseRemain中用的是#{scheduleId}这是预编译写法能防 SQL 注入。而排序字段如果要用${}动态拼接必须做白名单校验。这个话题在 java 面试题里是高频说出来就是加分项。第三“这个系统如果上线哪里最容易出问题”答案应该落在排班数据的并发写和 JSP 页面在大访问量下的渲染性能。SSM 的 JSP 每次请求都要经过 Servlet 容器渲染性能比 Spring Boot 的模板引擎或前后端分离模式差但你毕业设计的定位是管理系统这个量级完全能扛住。承认边界比吹得天花乱坠更让人信服。我自己的经验是每次做完这种 SSM 项目都会保留一份“排错笔记”把乱码、端口、数据库连接这三类问题记在同一页纸上。做毕业设计真正有收获的时刻不是最终跑通的那一下而是半夜对着控制台日志把报错看懂的那一下。技术会过时但这种排查路径和代码组织方式换到任何 Java 项目里都复用得上。希望帮到你。本文还有配套的精品资源点击获取