2026/11/2 21:05:45

SSM+JSP医院门诊挂号系统毕设源码解析:框架分工到部署避坑

SSM+JSP医院门诊挂号系统毕设源码解析:框架分工到部署避坑 简介基于SSM与JSP技术构建的医院门诊挂号系统面向需要完成Java毕业设计的本科生及Java学习者覆盖从数据库设计到前后端联调的全流程开发场景。该系统以实际门诊挂号业务为背景遵循软件工程规范依次完成需求分析、总体结构设计、数据结构设计、功能设计、安全设计、系统实现与功能测试等环节重点运用MySQL数据库与JSP动态页面技术适合作为毕业设计参考答案、课程设计或项目练手。压缩包约15MB内容涵盖完整源码、MySQL数据库脚本、开题报告、毕业论文、答辩PPT及使用说明源码可读性好、易于扩展便于本地部署调试与二次开发。已有59人学习可帮助正处于设计开发阶段的计算机专业学生快速获取成套参考文件满足论文撰写、代码调试与答辩准备等不同环节的需要。1. 拿到「基于SSMjsp的医院门诊挂号系统」这份毕设源码先别急着开IDESSMjsp的医院门诊挂号系统是JavaWeb毕业设计和课程设计里出现频率极高的一个题目整套技术栈是Spring、Spring MVC、MyBatis三框架整合配合传统JSP页面做展示层业务上覆盖科室维护、医生排班、在线预约挂号、后台数据统计。刚接触这份源码的人通常低估了它的复杂程度登录注册、号源查询、提交挂号看起来都是标准CRUD可真正跑通之后翻车点基本集中在数据库表设计、事务扣减和环境部署这几块而不是代码本身。这篇内容适合两类读者一到两周内要交付课设或毕设、需要快速读懂并运行整套系统的在校生以及想搞明白SSM前后端请求链路的Java入门开发者。下面先把骨架立住再把挂号主流程的代码逐段跑通最后给出部署时最值得留意的坑。2. 系统骨架拆解SSM框架分工与数据库表设计一套SSMjsp项目能不能快速上手取决于你能否在半小时内说清楚三件事请求进来之后由谁接收、SQL由谁执行、最终页面由谁渲染。这章先把框架职责和工程配置对齐再给出门诊挂号系统的核心表结构后续所有功能代码都是在这张骨架上长出来的。2.1 SSM三个框架在门诊挂号系统里到底各管哪一段以「患者点击查询号源」这个场景为例完整的请求链路是浏览器发起GET请求到Spring MVC的DispatcherServletDispatcherServlet根据RequestMapping找到对应的Controller方法Controller调用Service接口处理业务Service内部通过MyBatis的Mapper接口执行SQL最终把结果数据放进Model由InternalResourceViewResolver解析到WEB-INF下面的JSP页面JSP再用JSTL标签把数据渲染成HTML返回给浏览器。Spring在这套系统里扮演容器角色负责装配Service、Mapper这些Bean同时通过Transactional统一管理数据库事务Spring MVC管的是请求路由和参数绑定MyBatis把Java方法和XML里的SQL映射起来避免在Service层手动拼接SQL。JSP不是框架它是视图层技术但在这类传统项目里它承担了大部分页面渲染工作。这套组合之所以在毕设里长盛不衰是因为三层结构清楚、代码量适中而且JSP改起来比前后端分离方案更直观——改一个表格列名直接编辑JSP里的c:forEach就能看到效果。选择SSM而不是Spring Boot核心原因是教学体系和题目要求通常限定在这里。如果你拿到的题目写明了「基于SSM框架」就用SpringSpring MVCMyBatis不要擅自升级到Spring Boot评分标准往往卡着框架名来。真正需要留意的反而是springboot2集成jsp的目录限制这属于另一套玩法不在本文讨论范围内。2.2 用Maven搭建工程结构与三大配置文件工程结构决定了配置文件的加载顺序。常见做法是用Maven打成war包部署到Tomcat源码目录按controller、service、mapper、entity、vo分包JSP统一放在webapp/WEB-INF/views目录下这样浏览器无法直接访问JSP只能通过Controller转发进入算是一道最基础的访问控制。properties maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version5.1.20.RELEASE/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version3.5.6/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.6/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.21/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjstl/artifactId version1.2/version /dependency dependency groupIdjavax.servlet/groupId artifactIdjavax.servlet-api/artifactId version4.0.1/version scopeprovided/scope /dependency /dependencies这段pom.xml是SSM项目比较稳妥的一组坐标组合。Spring 5.1配Java 8在整个课程设计周期里几乎不会遇到兼容性问题MyBatis 3.5.6与mybatis-spring 2.0.6配合能让Spring接管SqlSessionFactory的生命周期Service里可以直接注入Mapper接口。MySQL驱动用8.0.21可以同时连接MySQL 5.7和8.0但连接URL里必须带上时区参数否则启动阶段就报错。javax.servlet-api千万不要省略但也不能去掉provided范围否则部署到Tomcat时会和容器自带的Servlet API冲突。Spring MVC的核心配置集中在spring-mvc.xml里负责包扫描、注解驱动、静态资源放行和视图解析。context:component-scan base-packagecom.hospital.controller/ mvc:annotation-driven/ mvc:resources location/static/ mapping/static/**/ bean classorg.springframework.web.servlet.view.InternalResourceViewResolver property nameprefix value/WEB-INF/views// property namesuffix value.jsp/ /beancomponent-scan只扫controller包Service和Mapper由spring.xml单独扫描这样Controller层可以注入Service但Controller不会在容器初始化时被重复代理。mvc:resources这一行很多人会漏掉JSP里引用的css、js、图片全部走/static路径不放行的话页面打开就是光秃秃的样式。InternalResourceViewResolver的前缀后缀意味着Controller里return user/index时实际解析的是webapp/WEB-INF/views/user/index.jsp这个对应关系心里要清楚排查404时才不会绕弯。web.xml决定请求入口和编码过滤器它是整个Tomcat加载顺序的起点。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 servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mappingCharacterEncodingFilter必须放在所有过滤器的最前面否则POST表单里的中文到Controller时已经是乱码后面怎么转都救不回来。DispatcherServlet的url-pattern写成/表示所有请求都进入Spring MVC但静态资源要依靠前面spring-mvc.xml里的mvc:resources放行否则css、js也会被Controller映射接管直接返回404。2.3 核心表设计从科室、医生到号源和挂号单门诊挂号系统的数据模型核心是四张表科室表、医生表、排班表、挂号表外加一张用户表存患者登录信息。表与表之间的关系是科室一对多医生医生一对多排班挂号记录则同时关联用户、医生和排班。设计时最容易犯的错是把科室名、医生名直接冗余进挂号表正确做法是存id用join去查展示名挂号费这种业务快照才值得冗余。CREATE TABLE t_department ( id int(11) NOT NULL AUTO_INCREMENT, name varchar(50) NOT NULL COMMENT 科室名称, description varchar(255) DEFAULT NULL COMMENT 科室介绍, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_doctor ( id int(11) NOT NULL AUTO_INCREMENT, dept_id int(11) NOT NULL COMMENT 所属科室, name varchar(30) NOT NULL COMMENT 医生姓名, title varchar(30) DEFAULT NULL COMMENT 职称主任医师/副主任医师/主治, fee decimal(10,2) NOT NULL DEFAULT 0.00 COMMENT 挂号费, intro text COMMENT 医生简介, PRIMARY KEY (id), KEY idx_dept (dept_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_doctor_schedule ( id int(11) NOT NULL AUTO_INCREMENT, doctor_id int(11) NOT NULL COMMENT 医生, work_date date NOT NULL COMMENT 出诊日期, time_slot tinyint(1) NOT NULL COMMENT 1上午 2下午, total_num int(11) NOT NULL DEFAULT 20 COMMENT 号源总数, remain_num int(11) NOT NULL DEFAULT 20 COMMENT 剩余号数, PRIMARY KEY (id), UNIQUE KEY uk_doc_date_slot (doctor_id, work_date, time_slot) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4; CREATE TABLE t_register ( id int(11) NOT NULL AUTO_INCREMENT, user_id int(11) NOT NULL COMMENT 挂号用户, doctor_id int(11) NOT NULL, dept_id int(11) NOT NULL, schedule_id int(11) NOT NULL COMMENT 排班ID, visit_date date NOT NULL, time_slot tinyint(1) NOT NULL, fee decimal(10,2) NOT NULL DEFAULT 0.00, status tinyint(1) NOT NULL DEFAULT 0 COMMENT 0待就诊 1已就诊 2已取消, create_time datetime DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4;t_doctor_schedule上唯一键uk_doc_date_slot非常关键它的作用是保证同一个医生同一天同一个半天只能有一条排班记录否则管理员不小心重复排班号源总量就会翻倍。t_register里的fee字段是从医生表快照过来的因为医生调价后历史挂号单仍然要保留当时的收费金额这是典型的业务快照冗余。status字段驱动着后续的退号和就诊状态流转很多同学省略这个字段导致退号功能无法实现后面再改表结构就麻烦了。用户表字段相对简单id、姓名、手机号、密码、创建时间手机号在业务里充当登录账号所以建议加唯一索引。3. 跑通挂号主流程注册登录、查号源、提交挂号的完整代码链登陆、查号、挂号是门诊系统的三条生命线代码量不大但链路完整。这章按「请求如何被拦截、SQL如何查余号、事务如何保证不超号」三个层次展开每一步都是可直接落进项目的最小实现。3.1 用户登录与权限控制拦截器怎么拦JSP请求JSP项目里的权限控制通常不用Spring Security一个HandlerInterceptor足够判断标准是session里有没有登录用户。拦截目标锁定在/user/**和/register/**路径下这两个路径是患者侧功能未登录一律踢回登录页。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { HttpSession session request.getSession(); Object user session.getAttribute(loginUser); if (user null) { response.sendRedirect(request.getContextPath() /toLogin); return false; } return true; } }这段preHandle逻辑足够简单session里没有loginUser就重定向到登录页并中断请求。注意重定向一定要拼request.getContextPath()否则项目部署在带上下文路径的Tomcat下时跳转链接会丢掉项目名前缀直接404。Controller里要记得提供/toLogin这个GET映射返回login.jsp视图。mvc:interceptors mvc:interceptor mvc:mapping path/user/**/ mvc:mapping path/register/**/ mvc:exclude-mapping path/toLogin/ mvc:exclude-mapping path/static/**/ bean classcom.hospital.interceptor.LoginInterceptor/ /mvc:interceptor /mvc:interceptors拦截器配置里最容易出问题的是exclude-mapping登录页本身的地址和所有静态资源必须放行否则浏览器请求登录页的css也走拦截器重定向来重定向去最终出现「重定向次数过多」的白屏报错。拦截路径按模块划分比全局拦截更可控管理员后台单独再配一个AdminInterceptor判断session里的角色字段这样普通用户手工拼/admin路径也进不去。3.2 按科室医生查余号MyBatis动态SQL怎么写查号源是挂号系统里查询条件最灵活的一个接口患者可能按科室筛、按医生筛、按日期筛所以SQL不能写死用MyBatis的动态标签最合适。先定义Mapper接口方法ListDoctorScheduleVO findAvailableSchedule(Param(deptId) Integer deptId, Param(doctorId) Integer doctorId, Param(workDate) String workDate);返回类型用VO而不是实体是因为这里要展示医生姓名、科室名、职称、挂号费等多张表join出来的结果而t_doctor_schedule表本身没有这些字段。新建一个DoctorScheduleVO字段对齐页面上表格的列查询结果就能直接渲染。Mapper XML是SSM里最值得细看的部分select idfindAvailableSchedule resultTypecom.hospital.vo.DoctorScheduleVO SELECT s.id AS scheduleId, d.id AS doctorId, d.name AS doctorName, d.title AS doctorTitle, dept.name AS deptName, s.work_date AS workDate, s.time_slot AS timeSlot, s.remain_num AS remainNum, d.fee AS fee FROM t_doctor_schedule s JOIN t_doctor d ON s.doctor_id d.id JOIN t_department dept ON d.dept_id dept.id where if testdeptId ! null and deptId ! 0 AND d.dept_id #{deptId} /if if testdoctorId ! null and doctorId ! 0 AND d.id #{doctorId} /if if testworkDate ! null and workDate ! AND s.work_date gt; #{workDate} /if /where AND s.remain_num gt; 0 ORDER BY s.work_date, s.time_slot /selectwhere标签会根据内部if自动拼AND如果三个条件都没传它会把多余的AND去掉这是MyBatis替代Java字符串拼接的核心手段。XML里大于号必须写成gt;直接写会导致XML解析失败这是MyBatis新手第一坑。条件只筛出remain_num大于0的排班页面根本不会展示已挂满的号源。resultType里的VO属性名和SQL别名是一一对应的数据库下划线字段和Java驼峰属性由mybatis的mapUnderscoreToCamelCase配置负责转换建议在mybatis-config.xml里把这个开关打开。3.3 提交挂号单事务注解与号源扣减挂号的核心不是insert一条记录而是如何保证多个患者同时抢最后一个号时不超卖。最稳妥的做法是把「扣减号源」和「插入挂号单」放进同一个事务并且扣减SQL自带剩余号数大于0的条件。Service public class RegisterService { Autowired private ScheduleMapper scheduleMapper; Autowired private RegisterMapper registerMapper; Transactional(rollbackFor Exception.class) public boolean createRegister(Register register) { int rows scheduleMapper.decreaseRemainNum(register.getScheduleId()); if (rows 0) { return false; } registerMapper.insert(register); return true; } }update iddecreaseRemainNum UPDATE t_doctor_schedule SET remain_num remain_num - 1 WHERE id #{scheduleId} AND remain_num 0 /update这个update语句是防超卖的关键。它把查询、判断、扣减三个动作压缩成一条带条件的UPDATE数据库的行锁保证同一时刻只有一个事务能执行成功update影响行数为0就说明号源已被抢完业务直接返回false。Transactional(rollbackFor Exception.class)保证两个数据库操作要么都成功要么都失败如果插入挂号单失败号源扣减会自动回滚如果返回false给ControllerController就别再展示成功页面。这套写法比先select再update的传统做法快得多也规避了并发下的竞态问题。Service返回boolean给ControllerController根据结果跳转到成功页或提示「该时段号源已约满」。把业务判断放在Service而不是Controller是为了保证这段逻辑可以被单元测试直接调用不用启动Tomcat就能验证。4. 管理员后台与门诊统计SSM的增改查和ECharts图表管理员端是这套SSMjsp系统里体量最大的一块涉及排班维护、挂号记录管理和统计图表也是答辩时最能展示完整性的部分。这章按三个核心页面来写每块代码都能直接对接到后台菜单。4.1 医生排班与号源管理页面排班管理的本质是对t_doctor_schedule表的增删改查管理员选择一个医生、一个日期、上午或下午设置号源总数后保存。这里最容易犯的业务错误是新增排班时没有初始化剩余号数导致前端显示「无号」。Controller RequestMapping(/admin/schedule) public class AdminScheduleController { Autowired private ScheduleService scheduleService; PostMapping(/save) public String save(DoctorSchedule schedule) { schedule.setRemainNum(schedule.getTotalNum()); scheduleService.save(schedule); return redirect:/admin/schedule/list?doctorId schedule.getDoctorId(); } }新增排班时剩余号数必须等于号源总数因为前端表单没有remain_num这个输入项Java对象里它是null不主动赋值落到数据库就是0页面再显示「约满」。这个坑几乎每个写门诊系统的同学都会踩一次。保存成功后用redirect重定向到列表页而不是直接return一个JSP视图目的是防止浏览器刷新时重复提交表单——刷新一个POST请求会触发浏览器「确认重新提交」的弹窗在答辩演示时非常尴尬。列表页用JSPJSTL渲染排班数据table classtable table-bordered c:forEach items${page.list} vars tr td${s.workDate}/td td${s.timeSlot 1 ? 上午 : 下午}/td td${s.totalNum}/td td${s.remainNum}/td td a href${ctx}/admin/schedule/edit?id${s.id}调整/a a href${ctx}/admin/schedule/delete?id${s.id} onclickreturn confirm(确认删除该排班);删除/a /td /tr /c:forEach /tabletimeSlot用三元表达式输出上午/下午比页面里嵌if-else简洁得多。删除操作加confirm确认弹窗避免管理员误删排班导致统计数据对不上。${ctx}这个变量需要在Controller里统一放入Model等价于${pageContext.request.contextPath}所有链接都从它开头拼项目换部署路径也不会图片断裂或链接404。4.2 挂号记录分页查询与状态操作后台挂号记录的列表页数据量随着时间增长会很大不能一次查全部分页是标配。这里用PageHelper插件它兼容SSM项目侵入性很小。GetMapping(/register/list) public String list(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, String keyword, Model model) { PageHelper.startPage(pageNum, pageSize); ListRegisterVO list registerService.findRegisterList(keyword); PageInfoRegisterVO pageInfo new PageInfo(list); model.addAttribute(page, pageInfo); return admin/register_list; }PageHelper.startPage之后的第一个SQL查询会自动被拦截拼上LIMIT所以startPage和service查询之间不能插入其他数据库操作否则分页会作用到错误的SQL上。PageInfo里自带总页数、当前页、上一页下一页这些导航数据JSP通过page.list拿当前页数据通过page.pages渲染页码按钮省去手写count查询的功夫。keyword参数支持按患者姓名或手机号模糊搜索适合线下导诊场景按电话找人。挂号状态操作是从「待就诊」流转到「已就诊」或者患者来电取消时变为「已取消」。状态流转的Service方法要判断当前状态不能让已取消的单子再次变成已就诊否则统计口径会乱。更新语句写WHERE id #{id} AND status 0更新行数为0说明状态已经被改过Controller提示「该挂号记录状态已变化请刷新后重试」。4.3 用ECharts画近七日门诊量曲线统计图是答辩现场的加分项SSM后端只需要提供一个返回JSON的接口前端用ECharts渲染。Controller方法加ResponseBody注解返回的对象会被Jackson自动序列化成JSON。GetMapping(/admin/statistics/weekly) ResponseBody public ListMapString, Object weeklyRegister() { return registerService.countDailyRegister(7); }select idcountDailyRegister resultTypemap SELECT DATE(create_time) AS visitDate, COUNT(*) AS cnt FROM t_register WHERE status ! 2 AND create_time DATE_SUB(CURDATE(), INTERVAL #{days} DAY) GROUP BY DATE(create_time) ORDER BY visitDate /selectSQL按天聚合挂号量排除已取消的记录DATE_SUB取最近7天窗口。这里的陷阱是如果某一天没有挂号数据GROUP BY结果里就没有这一天前端折线图会出现断点而不是补0。最简单的处理是在Java里把7天的日期补齐循环比对SQL结果缺失的天填0再返回给前端。补0这段逻辑放在Service里保证接口永远输出完整的7个点。JSP页面引入ECharts后用jQuery的$.get请求接口把数据映射到折线图var chart echarts.init(document.getElementById(chart)); $.get(${ctx}/admin/statistics/weekly, function(data) { chart.setOption({ xAxis: { type: category, data: data.map(d d.visitDate) }, yAxis: { type: value }, series: [{ type: line, data: data.map(d d.cnt), areaStyle: {} }] }); });这里data.map会在老版本浏览器上失效如果你用的是IE兼容要求的机房环境建议改成for循环遍历数据。areaStyle给曲线加了面积填充视觉上更饱满。ECharts文件从官网下载后放到webapp/static/echarts目录注意路径要以${ctx}开头不能写死。5. SSMjsp门诊挂号系统部署避坑从IDEA到Tomcat的5条血泪记录代码写通只是第一步这套传统JSP项目在部署环节让无数人熬夜而且坑非常集中在某几个固定位置。这章按「现象、原因、解决」的方式记录遇到问题直接对照排查。5.1 数据库连不上五个检查点一次过现象Tomcat启动不报错但点开页面一查数据就抛Cannot create PoolableConnection或Access denied for user rootlocalhost。原因SSM项目里数据库连不上有五个固定嫌疑驱动类名、时区、数据库名、用户名密码、MySQL服务是否启动。MySQL 8的驱动类是com.mysql.cj.jdbc.Driver写旧版的com.mysql.jdbc.Driver会直接启动失败不写serverTimezone会报时区错误数据库名不对则是Unknown database。解决检查jdbc.properties这是标准配置jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/hospital_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的密码这里characterEncodingutf8解决存储中文的问题serverTimezoneAsia/Shanghai解决日期差8小时的问题useSSLfalse省去本机连接的SSL握手耗时。排查时先用Navicat或命令行直接执行select 1确认MySQL服务和账号密码本身没问题再回头看驱动和URL不要一上来就怀疑代码。5.2 中文乱码页面显示三个问号现象JSP页面提交的中文到数据库变成「???」或者数据库里是中文页面上显示乱码。原因编码不一致发生在三个环节Tomcat接收请求时的编码、JSP文件本身的编码、MySQL连接的编码。只要有一环是latin1中文就丢。解决三处全改成UTF-8。web.xml里的CharacterEncodingFilter是第一节写的那个保证POST请求体以UTF-8解析所有JSP文件头写% page contentTypetext/html;charsetUTF-8 languagejava %MySQL连接URL带useUnicodetruecharacterEncodingutf8。如果数据已经乱码存进数据库不要把乱码数据留在表里继续测试直接drop表重新导入SQL脚本重来乱码是改不干净的趁早清库才是后悔药。5.3 JSP报错500、标签解析失败、JSTL找不到现象页面刷新报The absolute uri: http://java.sun.com/jsp/jstl/core cannot be resolved in either web.xml or the jar files deployed或者javax.servlet.jsp.JspException: /index.jsp (line: 5, column: 0)。原因JSTL标签库没有引入或者页面顶部没写taglib指令。IDEA里新建jsp项目时通常不会自动带JSTL依赖马上用到c:forEach就会报这个错。解决pom.xml里加上jstl 1.2的依赖JSP页面顶部加% taglib prefixc urihttp://java.sun.com/jsp/jstl/core % % taglib prefixfmt urihttp://java.sun.com/jsp/jstl/fmt %JSP文件报错还会出现在项目里混用了scriptlet和EL表达式时JSP里写%request.getParameter(id)%这类脚本片段时变量没有导入对应包会直接编译失败。传统JSP项目里scriptlet能不用就不用这块代码维护起来非常痛苦统一用EL加JSTL出问题的概率会低很多。5.4 传统JSP项目打包war部署到Tomcat的注意点现象IDEA里跑得好好的mvn clean package打出的war包放进另一台机器的Tomcat webapps目录启动无报错访问却404。原因传统jsp项目从IDEA内嵌容器切换到外部Tomcat时至少有四个变化访问路径多了项目名、不同Tomcat版本的Servlet规范不一致、可能缺少servlet-api依赖隔离、数据库配置还是本机地址。解决访问地址是http://ip:8080/项目名/登录页路径不是http://ip:8080/排查服务器上Tomcat是否是同一大版本千万别拿Tomcat 7跑Servlet 4.0的代码pom里servlet-api已经标了provided的话这个问题通常不会出现但要注意Tomcat的lib目录里别重复放servlet-api。还有一个非常隐蔽的点war包部署后JSP默认编译用的临时目录如果所在磁盘满了页面会白屏Tomcat的logs目录会同时输出大量Unable to compile class for JSP清理磁盘即可恢复。nginx支持jsp吗这个问题本质也在这里nginx本身不解析JSP它只转发静态请求动态请求要反向代理到后端的Tomcat让Tomcat编译执行JSP架构上nginx只做入口层。5.5 答辩前要检查的清单现象演示时功能正常但页面图片全裂、或管理员点菜单跳到一个已经失效的地址。原因图片和链接的路径被写成了绝对路径换一台机器部署后盘符或部署路径变了资源全找不到。这不是玄学就是路径处理不规范。解决全项目凡是出现资源地址的地方统一用${ctx}或${pageContext.request.contextPath}拼相对路径禁止写D:/upload/xxx.jpg。上传目录用项目内部相对路径upload/配合Spring MVC的静态资源映射暴露出去。数据库脚本要在演示的机器上完整执行一遍不要只拷贝导出的表数据而漏掉建库语句。离开前把本地Tomcat的logs目录清空再打包这样老师现场看到的日志是干净的出了问题也容易定位。提示答辩前把「注册 → 登录 → 选科室 → 挂号 → 管理员后台确认记录」这条主链路完整走两遍任何一步卡住都先看Tomcat日志不要漫无目的地改代码。6. 从跑通到交付并发验证、日志与演示前的小习惯项目跑通之后还有三件事能让你的交付质量明显比别人高出一截验证并发抢号、看日志确认SQL、固定一套演示前检查流程。并发验证不需要写复杂的压测脚本最简单的做法是用两个不同浏览器——比如Chrome正常窗口和Chrome无痕窗口登录两个账号同时去抢同一个医生最后一个剩余号。先让排班剩余号变成1然后两个窗口几乎同时点击「提交挂号」观察结果理论上必须是一个人成功、另一个人看到「号源已约满」。如果两个都成功了说明3.3里的update条件没写到位或者Service事务没生效需要回头看事务注解和Mapper XML。这个实验能在五分钟内测出防超卖是否真正可靠。日志是定位SSM问题的第一抓手。Tomcat的logs/catalina.out记录启动和运行期异常你遇到的MyBatis绑定异常、数据库连接失败、JSP编译错误全都能在里面看到第一行堆栈。MyBatis的完整SQL默认不打印调试阶段可以在mybatis-config.xml里把日志级别开到DEBUG这样每个SQL的执行参数和影响行数都会输出到Tomcat控制台排查查不到数据、更新没生效这类问题会快得多。演示时记得把日志级别调回INFO否则Console刷屏干扰现场操作。最后养成一个固定习惯交付前把项目里的三个文件单独拷出来保存分别是数据库SQL脚本、jdbc.properties、pom.xml。SQL脚本保证在任何一台新电脑上都能重建库jdbc.properties是换环境必改的入口pom.xml留着比对依赖版本。这三样东西备齐哪怕现场机器环境完全陌生也能在半小时内把系统重新拉起来。我自己交付这类SSMjsp项目时还会把war包解压后检查一遍WEB-INF里的class是否为最新时间戳避免以前编译的旧class被一起打包进去这个细节在新手身上出现概率不低。整个流程固化下来每次都是「检查清单走一遍再发出去」而不是临时抱佛脚。这套思路同样适用于这套医院门诊挂号系统——骨架立住、主流程跑通、部署坑绕开剩下的都是时间问题。希望帮到你。本文还有配套的精品资源点击获取