简介:基于SSM与JSP技术构建的医院门诊挂号系统,面向需要完成Java毕业设计的本科生及Java学习者,覆盖从数据库设计到前后端联调的全流程开发场景。该系统以实际门诊挂号业务为背景,遵循软件工程规范,依次完成需求分析、总体结构设计、数据结构设计、功能设计、安全设计、系统实现与功能测试等环节,重点运用MySQL数据库与JSP动态页面技术,适合作为毕业设计参考答案、课程设计或项目练手。压缩包约15MB,内容涵盖完整源码、MySQL数据库脚本、开题报告、毕业论文、答辩PPT及使用说明,源码可读性好、易于扩展,便于本地部署调试与二次开发。已有59人学习,可帮助正处于设计开发阶段的计算机专业学生快速获取成套参考文件,满足论文撰写、代码调试与答辩准备等不同环节的需要。
1. 拿到「基于SSM++jsp的医院门诊挂号系统」这份毕设源码,先别急着开IDE
SSM+jsp的医院门诊挂号系统是JavaWeb毕业设计和课程设计里出现频率极高的一个题目,整套技术栈是Spring、Spring MVC、MyBatis三框架整合,配合传统JSP页面做展示层,业务上覆盖科室维护、医生排班、在线预约挂号、后台数据统计。刚接触这份源码的人通常低估了它的复杂程度:登录注册、号源查询、提交挂号看起来都是标准CRUD,可真正跑通之后,翻车点基本集中在数据库表设计、事务扣减和环境部署这几块,而不是代码本身。这篇内容适合两类读者:一到两周内要交付课设或毕设、需要快速读懂并运行整套系统的在校生,以及想搞明白SSM前后端请求链路的Java入门开发者。下面先把骨架立住,再把挂号主流程的代码逐段跑通,最后给出部署时最值得留意的坑。
2. 系统骨架拆解:SSM框架分工与数据库表设计
一套SSM+jsp项目能不能快速上手,取决于你能否在半小时内说清楚三件事:请求进来之后由谁接收、SQL由谁执行、最终页面由谁渲染。这章先把框架职责和工程配置对齐,再给出门诊挂号系统的核心表结构,后续所有功能代码都是在这张骨架上长出来的。
2.1 SSM三个框架在门诊挂号系统里到底各管哪一段
以「患者点击查询号源」这个场景为例,完整的请求链路是:浏览器发起GET请求到Spring MVC的DispatcherServlet,DispatcherServlet根据@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框架」,就用Spring+Spring MVC+MyBatis,不要擅自升级到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.source>1.8</maven.compiler.source> <maven.compiler.target>1.8</maven.compiler.target> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>5.1.20.RELEASE</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.6</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.6</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.21</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>jstl</artifactId> <version>1.2</version> </dependency> <dependency> <groupId>javax.servlet</groupId> <artifactId>javax.servlet-api</artifactId> <version>4.0.1</version> <scope>provided</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-package="com.hospital.controller"/> <mvc:annotation-driven/> <mvc:resources location="/static/" mapping="/static/**"/> <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver"> <property name="prefix" value="/WEB-INF/views/"/> <property name="suffix" value=".jsp"/> </bean>component-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-name>encodingFilter</filter-name> <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class> <init-param> <param-name>encoding</param-name> <param-value>UTF-8</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <servlet> <servlet-name>dispatcher</servlet-name> <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class> <init-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:spring-mvc.xml</param-value> </init-param> <load-on-startup>1</load-on-startup> </servlet> <servlet-mapping> <servlet-name>dispatcher</servlet-name> <url-pattern>/</url-pattern> </servlet-mapping>CharacterEncodingFilter必须放在所有过滤器的最前面,否则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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; 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`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;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 class="com.hospital.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>拦截器配置里最容易出问题的是exclude-mapping,登录页本身的地址和所有静态资源必须放行,否则浏览器请求登录页的css也走拦截器,重定向来重定向去,最终出现「重定向次数过多」的白屏报错。拦截路径按模块划分比全局拦截更可控,管理员后台单独再配一个AdminInterceptor,判断session里的角色字段,这样普通用户手工拼/admin路径也进不去。
3.2 按科室医生查余号:MyBatis动态SQL怎么写
查号源是挂号系统里查询条件最灵活的一个接口,患者可能按科室筛、按医生筛、按日期筛,所以SQL不能写死,用MyBatis的动态标签最合适。先定义Mapper接口方法:
List<DoctorScheduleVO> findAvailableSchedule(@Param("deptId") Integer deptId, @Param("doctorId") Integer doctorId, @Param("workDate") String workDate);返回类型用VO而不是实体,是因为这里要展示医生姓名、科室名、职称、挂号费等多张表join出来的结果,而t_doctor_schedule表本身没有这些字段。新建一个DoctorScheduleVO,字段对齐页面上表格的列,查询结果就能直接渲染。
Mapper XML是SSM里最值得细看的部分:
<select id="findAvailableSchedule" resultType="com.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 test="deptId != null and deptId != 0"> AND d.dept_id = #{deptId} </if> <if test="doctorId != null and doctorId != 0"> AND d.id = #{doctorId} </if> <if test="workDate != null and workDate != ''"> AND s.work_date >= #{workDate} </if> </where> AND s.remain_num > 0 ORDER BY s.work_date, s.time_slot </select><where>标签会根据内部if自动拼AND,如果三个条件都没传,它会把多余的AND去掉,这是MyBatis替代Java字符串拼接的核心手段。XML里大于号必须写成>,直接写>会导致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 id="decreaseRemainNum"> 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给Controller,Controller就别再展示成功页面。这套写法比先select再update的传统做法快得多,也规避了并发下的竞态问题。
Service返回boolean给Controller,Controller根据结果跳转到成功页或提示「该时段号源已约满」。把业务判断放在Service而不是Controller,是为了保证这段逻辑可以被单元测试直接调用,不用启动Tomcat就能验证。
4. 管理员后台与门诊统计:SSM的增改查和ECharts图表
管理员端是这套SSM+jsp系统里体量最大的一块,涉及排班维护、挂号记录管理和统计图表,也是答辩时最能展示完整性的部分。这章按三个核心页面来写,每块代码都能直接对接到后台菜单。
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请求会触发浏览器「确认重新提交」的弹窗,在答辩演示时非常尴尬。
列表页用JSP+JSTL渲染排班数据:
<table class="table table-bordered"> <c:forEach items="${page.list}" var="s"> <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}" onclick="return confirm('确认删除该排班?');">删除</a> </td> </tr> </c:forEach> </table>timeSlot用三元表达式输出上午/下午,比页面里嵌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); List<RegisterVO> list = registerService.findRegisterList(keyword); PageInfo<RegisterVO> 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 List<Map<String, Object>> weeklyRegister() { return registerService.countDailyRegister(7); }<select id="countDailyRegister" resultType="map"> 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 </select>SQL按天聚合挂号量,排除已取消的记录,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. SSM+jsp门诊挂号系统部署避坑:从IDEA到Tomcat的5条血泪记录
代码写通只是第一步,这套传统JSP项目在部署环节让无数人熬夜,而且坑非常集中在某几个固定位置。这章按「现象、原因、解决」的方式记录,遇到问题直接对照排查。
5.1 数据库连不上:五个检查点一次过
现象:Tomcat启动不报错,但点开页面一查数据就抛Cannot create PoolableConnection或Access denied for user 'root'@'localhost'。
原因:SSM项目里数据库连不上有五个固定嫌疑:驱动类名、时区、数据库名、用户名密码、MySQL服务是否启动。MySQL 8的驱动类是com.mysql.cj.jdbc.Driver,写旧版的com.mysql.jdbc.Driver会直接启动失败;不写serverTimezone会报时区错误;数据库名不对则是Unknown database。
解决:检查jdbc.properties,这是标准配置:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/hospital_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=你的密码这里characterEncoding=utf8解决存储中文的问题,serverTimezone=Asia/Shanghai解决日期差8小时的问题,useSSL=false省去本机连接的SSL握手耗时。排查时先用Navicat或命令行直接执行select 1,确认MySQL服务和账号密码本身没问题,再回头看驱动和URL,不要一上来就怀疑代码。
5.2 中文乱码:页面显示三个问号
现象:JSP页面提交的中文到数据库变成「???」,或者数据库里是中文,页面上显示乱码。
原因:编码不一致发生在三个环节:Tomcat接收请求时的编码、JSP文件本身的编码、MySQL连接的编码。只要有一环是latin1,中文就丢。
解决:三处全改成UTF-8。web.xml里的CharacterEncodingFilter是第一节写的那个,保证POST请求体以UTF-8解析;所有JSP文件头写<%@ page contentType="text/html;charset=UTF-8" language="java" %>;MySQL连接URL带useUnicode=true&characterEncoding=utf8。如果数据已经乱码存进数据库,不要把乱码数据留在表里继续测试,直接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 prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib prefix="fmt" uri="http://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留着比对依赖版本。这三样东西备齐,哪怕现场机器环境完全陌生,也能在半小时内把系统重新拉起来。我自己交付这类SSM+jsp项目时,还会把war包解压后检查一遍WEB-INF里的class是否为最新时间戳,避免以前编译的旧class被一起打包进去,这个细节在新手身上出现概率不低。整个流程固化下来,每次都是「检查清单走一遍再发出去」,而不是临时抱佛脚。这套思路同样适用于这套医院门诊挂号系统——骨架立住、主流程跑通、部署坑绕开,剩下的都是时间问题。希望帮到你。
本文还有配套的精品资源,点击获取