简介:面向高校计算机相关专业学生的第二课堂成绩单管理毕业设计项目,基于SSM(Spring+SpringMVC+MyBatis)框架,前端采用JSP,数据库使用MySQL,支持JDK1.8及Eclipse、MyEclipse、STS、IDEA等主流IDE。系统后台区分普通管理员与超级管理员:普通管理员可修改密码并处理录入单审核;超级管理员额外可维护普通管理员账号,管理思想道德与技术修养信息、发布新闻、管理注册用户,覆盖第二课堂成绩录入、审核、公示等典型业务场景。资源包共887个文件,大小约42.25MB,以JSP页面、Java源码与class文件、JAR依赖库、SQL脚本、JS/CSS/HTML前端资源及XML配置文件为主,另含文档说明和实操视频,目录结构便于按模块检索。目前已有82人学习下载。资源附带完整源码、数据库脚本、项目论文、环境工具包,并在说明文档中提供同框架项目的安装教程;后端业务按思想道德、技术修养、学生信息等Controller模块组织,可直接部署运行,也适合在毕业设计基础上继续扩展功能。
1. 第二课堂成绩单系统是什么:JSP+SSM 毕设项目的完整画像
如果你正在找 jspssm 大学生第二课堂成绩单系统这类源码,大概率是被毕业设计卡住了。第二课堂成绩单系统说白了就是把学生在课堂之外参加的活动——社团、志愿、竞赛、讲座——统一登记、审核、量化成一张可打印的成绩单。这个业务在高校里真实存在,而且每年都会有学生拿它当毕设题目,技术栈十有八九是 JSP 做页面、SSM(Spring + SpringMVC + MyBatis)做后端,配 MySQL 存数据。它之所以流行,是因为这套组合刚好覆盖了 JavaWeb 课程里几乎所有考点:MVC 分层、ORM 映射、拦截器、文件上传和权限控制。
这篇文章我不打算讲空泛的项目介绍,而是把你真正需要的东西列出来:表结构怎么设计、SSM 怎么整合、页面和控制器之间怎么传数据、部署到 Tomcat 时哪些坑必踩。我默认你手上那份源码不是你自己写的,而是下载下来要改造成自己项目的——所以每个环节我都会讲清楚“为什么这么做”以及“改哪里才能变成你自己的”。本文的目标只有一句:让这套 JSP+SSM 代码在你本地跑起来,并且你能在答辩时讲清楚每一个关键配置。
2. 为什么 JSP+SSM 在毕设里仍是主流:选型逻辑与工程结构
2.1 SSM 与 Spring Boot 的取舍:别被“过时”带偏
很多学生拿到这份源码第一反应是:为什么不用 Spring Boot?这个问题答辩老师也爱问。SSM 是 Spring、SpringMVC、MyBatis 三个框架手写整合,而 Spring Boot 把配置自动完成。但正因如此,SSM 项目能看出你对“整合过程”是否真的理解——配置数据源、扫描 Mapper、配置视图解析器、管理事务,每一步都是显式的。Spring Boot 把这些藏进自动配置里,反而不好讲。更重要的是,许多高校毕设题库里 JSP+SSM 仍然是出现频率极高的题目,导师也熟悉这套排错方式。所以别觉得它老,它能活这么多年就是因为教学价值高、坑明确、资料多。
我这里列一下常见做法里的版本搭配,与不同 JDK 的兼容情况:
| 组件 | 常见版本 | 说明 |
|---|---|---|
| JDK | 1.7 / 1.8 | 不建议上 JDK11+,老框架反射和 JSP 编译会有兼容问题 |
| Tomcat | 7.x / 8.x | 对应 Servlet 3.0/3.1,SSM 项目最稳的是 Tomcat 8.5 |
| Spring | 4.x / 5.x | 4.3.x 和 5.0.x 在毕设源码里最常见 |
| MyBatis | 3.2.x ~ 3.4.x | 太新的版本可能会跟旧 Spring 整合包冲突 |
| MySQL | 5.7 / 8.0 | 8.0 需要换驱动类名和增加时区参数 |
你拿到的源码里大概率自带一份pom.xml(Maven 工程)或者一堆 jar 包(非 Maven 工程)。Maven 工程最好办,因为依赖能一键下载;非 Maven 工程需要手动在 WEB-INF/lib 里补齐 jar 包。下面先讲 Maven 工程的导入方式。
2.2 导入 IDEA 并跑通 Tomcat:Maven 工程的一键启动
拿到源码后,第一步不是改代码,而是把工程导入 IDEA。打开 IDEA,选择File -> Open,定位到源码目录下的pom.xml,以 Maven 工程方式打开。IDEA 会自动下载依赖,这个过程是这套项目第一次“翻车”的高发地——原因通常是 Maven 镜像是默认中央仓库,下载极慢甚至超时。我一般会在settings.xml里把镜像换成阿里云公共仓库,写法如下:
<mirror> <id>aliyunmaven</id> <mirrorOf>*</mirrorOf> <name>阿里云公共仓库</name> <url>https://maven.aliyun.com/repository/public</url> </mirror>这里注意mirrorOf写成*会把所有仓库请求都转发到阿里云,对国内网络环境是最省事的做法。如果项目里同时配置了私服,这个通配符可能会覆盖私服地址,那就改成<mirrorOf>central</mirrorOf>,只代理中央仓库。
依赖下载完成后,配置 Tomcat。在 IDEA 里点Run -> Edit Configurations -> 加号 -> Tomcat Server -> Local,然后在Deployment选项卡里添加Artifact,选项目名:war exploded。这里有一个非常关键的参数:Application context要写成/还是/项目名?这取决于源码里的请求路径和路径写法。
如果项目里所有 JSP 的 hyperlink 都写了${pageContext.request.contextPath},那么 Application context 填/项目名也没问题;如果是action="/login"这种绝对路径,最好把 context 设成/避免 404。示例配置说明如下:
# Tomcat 配置要点 # 1. VM options 一栏不要填 -Xmx 之外的参数,避免内存不足 # 2. 端口保持 8080,如果被占用可以在 Server 选项卡里改成 8081 # 3. 打开 "After launch" 默认会用浏览器打开首页启动后如果控制台有一个INFO: Server startup in 5000 ms类似输出,说明 Tomcat 起来了。如果卡在Deploying web application archive很久,多半是数据库连接失败——因为 SSM 的contextLoaderListener在应用启动时就会初始化数据源,数据库没连上,整个应用就起不来。这是 SSM 项目最重要的一个特征:配置错了不会给你一个友好的错误页,而是直接启动失败。
2.3 SSM 整合的关键配置文件里到底写了什么
拿到源码后你一定会在resources或config目录下看到至少三个配置文件。很多学生最怕的就是这三个文件之间的关系。我直接说明白:
spring-mvc.xml:管控制器、视图解析器、静态资源放行。spring-dao.xml(或applicationContext.xml):管数据源、SqlSessionFactory、Mapper 扫描。web.xml:管两个容器的挂载顺序和拦截路径。
这是一份常见的spring-dao.xml数据源配置:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource"> <property name="driverClassName" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/second_class?useUnicode=true&characterEncoding=utf8"/> <property name="username" value="root"/> <property name="password" value="123456"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="dataSource"/> <property name="configLocation" value="classpath:mybatis-config.xml"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean>注意 url 里的&是 XML 转义,代表&,很多学生漏掉这个符号导致连接参数丢失。useUnicode=true&characterEncoding=utf8是为了让数据库里中文不乱码。如果数据库是 MySQL 8.0,driverClassName要改成com.mysql.cj.jdbc.Driver,并且 url 后面追加serverTimezone=Asia/Shanghai,否则会报时区错误。
3. 把成绩单系统拆成四张核心表:数据库设计与导入实操
3.1 表的字段设计与关联逻辑:为什么必须有中间表
第二课堂成绩单系统的数据模型不复杂,但少一张表就会在后续开发里痛不欲生。按我做过的同类项目,核心就四张表:学生表、活动表、成绩单主表、成绩单明细表。学生与活动是多对多关系,一个学生参加多个活动,一个活动有多个学生参加,所以需要一张关联表来记录“学生参加活动”这个事实,以及每次参与获得的学分和审核状态。
学生表(student)字段至少包括:学号(主键)、姓名、院系、专业、班级、入学年份。活动表(activity)字段包括:活动ID(自增主键)、活动名称、活动类型(志愿/社团/竞赛/讲座)、活动时间、主办方、可申请学时。这里活动类型用数字字典管理,比如 1 代表志愿,2 代表社团,3 代表竞赛,4 代表讲座,比直接存中文要规范得多,做统计查询也方便。
成绩单主表(transcript)是按学期或学年生成的,一个学生一个学年一条记录,存总学时、总积分、审核状态。成绩单明细表(transcript_detail)是学生参与的每条活动记录,存参与时间、获得的学时、学分、审核状态。主表和明细表是一对多关系。为什么要拆出主表而不是直接查关联表?因为成绩单是一个“盖章凭证”,一旦生成就要固定下来,后面活动数据变动不能影响已经打印的成绩单。这就是快照思想,答辩时讲到这一句会很加分。
3.2 建表 SQL 与初始化数据:一段能直接复用的脚本
下面这段 SQL 可以直接在 Navicat 或命令行里执行,注意先创建数据库再建表:
CREATE DATABASE IF NOT EXISTS second_class DEFAULT CHARSET utf8mb4; USE second_class; CREATE TABLE student ( stu_no VARCHAR(20) PRIMARY KEY COMMENT '学号', stu_name VARCHAR(50) NOT NULL COMMENT '姓名', dept_name VARCHAR(50) COMMENT '院系', major_name VARCHAR(50) COMMENT '专业', class_name VARCHAR(50) COMMENT '班级', enroll_year INT COMMENT '入学年份' ) ENGINE=InnoDB COMMENT='学生表'; CREATE TABLE activity ( act_id INT AUTO_INCREMENT PRIMARY KEY, act_name VARCHAR(100) NOT NULL COMMENT '活动名称', act_type TINYINT COMMENT '1志愿 2社团 3竞赛 4讲座', act_time DATETIME COMMENT '活动时间', org_name VARCHAR(100) COMMENT '主办方', hour_limit INT COMMENT '可获学时上限' ) ENGINE=InnoDB COMMENT='活动表'; CREATE TABLE student_activity ( id INT AUTO_INCREMENT PRIMARY KEY, stu_no VARCHAR(20) NOT NULL, act_id INT NOT NULL, join_time DATETIME COMMENT '参与时间', hours DECIMAL(4,1) COMMENT '获得学时', credit DECIMAL(4,1) COMMENT '获得学分', status TINYINT DEFAULT 1 COMMENT '1待审 2通过 3驳回', UNIQUE KEY uk_stu_act (stu_no, act_id) ) ENGINE=InnoDB COMMENT='参与记录表';student_activity表就是前文说的关联表。UNIQUE KEY uk_stu_act (stu_no, act_id)是防止同一个人重复参加同一个活动被记两次学分,这个约束在业务里非常关键。第二课堂成绩单系统里最容易出现的逻辑错误就是:同一个学生同一场活动录了两遍导致学时翻倍。这条唯一索引是从源头上堵住这个问题。
3.3 初始化一条测试数据:让页面能立刻看到效果
很多下载到的源码数据库脚本里数据是空的,登录进去什么都看不到。你需要自己插几条测试数据。这里注意,学生表的密码字段我没有设计进去——因为大多数毕设源码里用户表和密码是分离的,或用user表。如果你拿到的源码里登录验证是走student表,就补一个password字段;如果走单独的sys_user表,那就在那张表插管理员账号。常见做法是把管理员账号直接写死在 SQL 里:
INSERT INTO sys_user (username, password, role) VALUES ('admin', '123456', '1'); -- 注意密码在源码中通常不是纯明文 -- 如果源码用了 MD5,请把 123456 替换成对应的 MD5 值这里“对应的 MD5 值”通常是e10adc3949ba59abbe56e057f20f883e,即123456的 32 位 MD5。但不要盲目套用,因为有些源码会对密码加盐后再加密。怎么判断?打开源码里UserMapper.xml的selectByLogin语句,看 SQL 条件是password = #{password}还是password = md5(#{password}),前者说明需要预先加密。启动项目后如果能登录几次并把页面点翻,说明你的环境基本跑通了。
4. 从登录到成绩单导出:四个核心模块的 JSP+SSM 实现代码
4.1 登录拦截器:为什么每个页面都要做会话校验
SSM 项目里的登录状态管理一般用拦截器实现。如果某个 JSP 页面直接访问时不经过登录校验,那就是一个典型的未授权访问漏洞。在毕设答辩演示时,直接输入http://localhost:8080/admin.jsp如果能看到后台内容,老师会直接问你这个安全问题。这里给出一个实现拦截器的最简方式:
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() + "/login.jsp"); return false; } return true; } }然后在spring-mvc.xml里注册拦截器并排除登录接口:
<mvc:interceptors> <mvc:interceptor> <mvc:mapping path="/**"/> <mvc:exclude-mapping path="/login"/> <mvc:exclude-mapping path="/login.jsp"/> <mvc:exclude-mapping path="/static/**"/> <bean class="com.example.interceptor.LoginInterceptor"/> </mvc:interceptor> </mvc:interceptors>这里的mvc:exclude-mapping很重要,不排除掉静态资源和登录页,拦截器会把 CSS、JS、图片全部拦掉,页面会变成没有样式的裸 HTML,这是常见的翻车现场。/**匹配所有路径,login 接口和 JSP 都必须放行。
4.2 第二课堂成绩单查询:三表关联的 Mapper 写法
成绩单页面的核心操作是“按学号查询学生及其参与活动列表”。这个查询要跨越三张表:student、student_activity、activity。MyBatis 里有两种做法:resultType 多表联查返回 DTO,或者 resultMap 嵌套映射。对毕设来说,多表联查返回 DTO(数据传输对象)代码最少、最好讲、也最好调试。
先写 SQL 语句:
SELECT s.stu_no, s.stu_name, s.dept_name, a.act_name, a.act_type, sa.hours, sa.credit, sa.status FROM student s LEFT JOIN student_activity sa ON s.stu_no = sa.stu_no LEFT JOIN activity a ON sa.act_id = a.act_id WHERE s.stu_no = #{stuNo}然后写对应的 Mapper 接口:
public interface TranscriptMapper { List<TranscriptDTO> selectStudentTranscript(@Param("stuNo") String stuNo); }关键点在TranscriptDTO的字段设计上,建议用它接收混合数据而不是直接用实体类。如果直接拿Student实体接收,活动名称和学时这些字段就要额外写 getter,造成实体职责混乱。DTO 的好处在于可以专门为这个页面定制字段,不污染数据库实体。这份 DTO 里我一般会写这几个字段:stuNostuNamedeptNameactNameactTypehourscreditstatus。其中status在页面显示时还要做中文映射,比如 1 显示待审核、2 显示已通过、3 显示已驳回。这个映射动作在 JSP 里用 EL 表达式做比较麻烦,我一般在 Controller 里直接转换成字符串返回。
4.3 成绩单页面渲染:JSP 里用 JSTL 避免 Java 脚本
有些老源码在 JSP 里直接写<% %>Java 脚本,这在运行时没问题但极难维护。拿到手后建议看一页即可了解源码风格。如果发现是 JSTL + EL,那恭喜你代码干净;如果是<%= %>套着大段 Java,你后续加功能会很痛苦。但毕设答辩时老师不太会扣这个,他们更关心功能跑不跑得通。
成绩单页面最核心的一段 JSP 代码如下:
<c:forEach items="${transcriptList}" var="item"> <tr> <td>${item.stuNo}</td> <td>${item.actName}</td> <td> <c:choose> <c:when test="${item.status == 1}">待审核</c:when> <c:when test="${item.status == 2}">已通过</c:when> <c:otherwise>已驳回</c:otherwise> </c:choose> </td> </tr> </c:forEach>这里items="${transcriptList}"对应 Controller 里往 ModelAndView 塞的 List 名称。如果页面上什么都没有显示,先看三处:一是<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>有没有引入;二是 jstl.jar 和 standard.jar 有没有在 WEB-INF/lib;三是transcriptList这个 key 在 Model 里是否正确。前两个是 JSTL 不生效的最常见原因,现象是 JSP 页面直接显示 JSTL 标签源码而没有解析。
4.4 成绩单审核流程:Controller 如何串联 Service 和 Mapper
成绩单系统的第二个核心动作是“审核”。“学生申报 + 辅导员审批”是第二课堂成绩单系统的典型业务闭环。审核的本质是修改student_activity表的status字段,但高层设计上必须经过 Service 层。下面给出一个标准的 Controller + Service + Mapper 三段式:
@Controller @RequestMapping("/transcript") public class TranscriptController { @Resource private TranscriptService transcriptService; @RequestMapping("/audit") public String audit(@RequestParam("id") Integer id, @RequestParam("result") Integer result) { transcriptService.auditRecord(id, result); return "redirect:/transcript/list"; } }Service 层实现:
@Service public class TranscriptServiceImpl implements TranscriptService { @Resource private TranscriptMapper transcriptMapper; @Override public void auditRecord(Integer id, Integer result) { // result: 1通过 2驳回 transcriptMapper.updateStatus(id, result); } }Mapper 接口:
void updateStatus(@Param("id") Integer id, @Param("result") Integer result);这个过程里注意@RequestParam("result")绑定的是表单里提交的 input 值。有的源码喜欢传中文意义的值,如pass或reject,再在 Controller 里做映射。这样做的优势是前端模板里的可读性更好,但要多写一层 if 判断。对于毕设项目,我建议直接用数字传参,逻辑最简单,答辩时讲起来也不容易卡壳。事务方面,单个 update 语句天然具备原子性,不需要额外加@Transactional;但如果审核通过后还需要同步修改成绩单主表的累计学时,就需要在 Service 方法上加上事务注解:
@Transactional(rollbackFor = Exception.class) public void auditRecord(Integer id, Integer result) { studentActivityMapper.updateStatus(id, result); if (result == 1) { transcriptMapper.summaryHours(id); } }4.5 JSP 表单提交与请求路径:action 写法的两种风格
很多学生在 JSP 表单里写action="audit",然后抱怨提交到 404。这里的原因是相对路径和绝对路径的区别。action="audit"是相对当前 URL 路径去请求,如果当前页面是/transcript/list.jsp,它会提交到/transcript/audit,正好对;但如果你在浏览器里打开了/abc.jsp,再提交就会变成/abc/audit,直接 404。正确的写法有两种:
<!-- 方式一:绝对路径,带项目上下文 --> <form action="${pageContext.request.contextPath}/transcript/audit" method="post"> <!-- 方式二:依赖 base 标签统一加前缀 --> <base href="${pageContext.request.contextPath}/"> <form action="transcript/audit" method="post">第一种写法最直观,每一处都写很清楚但代码冗余;第二种只在<head>里写一个base标签,后面所有相对路径都自动带上前缀。如果源码里的<base>已经写好了,你只需要把href参数保证正确即可。这是 JSP 项目里最常见的“看着没问题但点不动”的原因,重点检查你的pageContext.request.contextPath是否与 IDEA 里配置的 Application context 一致。
5. 跑通这套源码的五个避坑点:从 JDK 到数据库的血泪实录
5.1 Tomcat 启动报 ClassNotFoundException:jar 包冲突或缺失
现象:Tomcat 启动时报java.lang.ClassNotFoundException: org.springframework.web.context.ContextLoaderListener,页面完全无法访问。原因有两种:一是在 IDEA 里没有把 Maven 依赖部署到 WEB-INF/lib,二是spring-web和spring-webmvc版本不一致。解决方法是打开Project Structure -> Artifacts -> Output Layout,查看Available Elements里有没有Maven: org.springframework:spring-web:5.x.x,如果没有就右键把它加进WEB-INF/lib。如果是非 Maven 工程,则直接检查web/WEB-INF/lib目录里有没有 spring-web jar 包。
这是 SSM 项目最容易出现的问题,因为 IDEA 的 Maven 工程不会自动把依赖打进来。每次增加依赖后,都要重新构建 Artifact。检查方式很简单,部署成功后打开 Tomcat 的webapps/项目名/WEB-INF/lib目录,看看里面有没有出现你新加的 jar 包。
5.2 MySQL 8.0 连不上:驱动类名和时区是两座大山
现象:启动报com.mysql.jdbc.Driver找不到或The server time zone value 'Öйú' is unrecognized。原因:MySQL 8.0 把驱动类从com.mysql.jdbc.Driver换成com.mysql.cj.jdbc.Driver,并且连接要求显式指定 serverTimezone。解决方法是去 Maven 的pom.xml里升级 mysql-connector-java 依赖,并修改数据源配置。
<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.26</version> </dependency>对应的数据源改为:
jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/second_class?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/ShanghaiAsia/Shanghai是东八区标准写法,不要写成GMT+8,因为后者在某些驱动版本里解析失败。这里有个血泪经验:如果 Tomcat 启动日志里出现中文乱码的时间报错,说明数据库连接已经通了,但时区参数没对齐。
5.3 JSP 页面中文乱码:Tomcat 默认编码不是 UTF-8
现象:页面上的中文显示为一串问号???。原因:Tomcat 8 以前默认使用 ISO-8859-1 处理 POST 请求参数,而 JSP 页面已经声明了 UTF-8。解决方法是显式设置 CharacterEncodingFilter,在web.xml中加一个过滤器,放在所有过滤器最前面:
<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> <init-param> <param-name>forceEncoding</param-name> <param-value>true</param-value> </init-param> </filter> <filter-mapping> <filter-name>encodingFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>forceEncoding这个参数很关键,设成true后请求参数和响应编码都会强制使用 UTF-8,不用再在每个 Controller 里写request.setCharacterEncoding。如果你检查源码时发现这个过滤器缺失,基本可以断定会遇到中文乱码坑。排查顺序是:先看 JSP 头部的pageEncoding,再看数据库连接 url 的 characterEncoding,最后看 Tomcat 的 server.xml 里的URIEncoding="UTF-8"。
5.4 刷新页面重复提交审核:缺一个重定向
现象:点一次审核通过后,按 F5 刷新,同一记录又被审核了一次,或者提示记录不存在。原因:Controller 的audit方法执行完后return "audit_success.jsp"直接转发到页面,没有重定向。刷新时浏览器会重新发送上一次的 POST 请求。解决方法是把成功后返回改成redirect:/transcript/list,让浏览器以 GET 方式重新请求列表页。
return "redirect:/transcript/list";这是一个分辨源码好坏的分水岭。如果你的源码里审核提交后直接返回视图名,说明作者的交互设计不够细致。这套逻辑同样适用于删除、新增操作。重定向后列表页会自动刷新,而且用户按 F5 也不会重复提交。
5.5 Mapper 接口和 XML 无法对应:namespace 写错查半天
现象:启动报Invalid bound statement (not found): com.example.mapper.TranscriptMapper.selectStudentTranscript。原因:MyBatis 的 Mapper XML 文件里namespace写的接口全限定名和实际接口路径不一致,或者 XML 文件没有被打到 classpath。第一优先级是检查 XML 文件在 target/classes 目录里是否存在——IDEA 有时候不会自动把.xml资源复制到编译目录,需要在pom.xml里显式声明:
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>false</filtering> </resource> </resources>这段配置把src/main/java下的 Mapper XML 一起编译打包。如果漏掉这个,运行时会找不到 XML,但编译期一点错都不报。这个坑特别隐蔽,半天查不出来。第二优先级是检查namespace="com.example.mapper.TranscriptMapper"是否和你 Mapper 接口的包名一致,注意是接口的全限定名而不是 XML 的文件名。第三优先级是检查<mapper resource="mapper/TranscriptMapper.xml">的路径是否大小写敏感,Ant 风格路径不要写错。
6. 让答辩加分的一个进阶技巧:成绩单 PDF 导出与 Excel 打印
成绩单系统的核心交付物是“成绩单”,线上页面能看、能审核只是过程,最后导出打印成正式凭证才是闭环。很多学生的毕设源码里只做到了页面展示,没有导出功能。但这个功能实现起来并不复杂,而且给答辩的整体印象分会高不少——因为它是“最后一公里”的落地动作。
常见做法是在前端用 JavaScript 实现页面打印,或者在后端用 POI 导出 Excel。两种都可以,但老师问到“怎么做”时,你讲后端生成 Excel 再用 POI 转 PDF 的方案,明显更专业。下面给一个用 Apache POI 导出成绩单 Excel 的核心代码:
public void exportTranscript(HttpServletResponse response, String stuNo) throws Exception { List<TranscriptDTO> list = transcriptService.selectStudentTranscript(stuNo); // 在内存里创建一个工作簿 Workbook workbook = new HSSFWorkbook(); Sheet sheet = workbook.createSheet("第二课堂成绩单"); Row header = sheet.createRow(0); header.createCell(0).setCellValue("学号"); header.createCell(1).setCellValue("活动名称"); header.createCell(2).setCellValue("学时"); header.createCell(3).setCellValue("学分"); for (int i = 0; i < list.size(); i++) { TranscriptDTO dto = list.get(i); Row row = sheet.createRow(i + 1); row.createCell(0).setCellValue(dto.getStuNo()); row.createCell(1).setCellValue(dto.getActName()); row.createCell(2).setCellValue(dto.getHours()); row.createCell(3).setCellValue(dto.getCredit()); } // 写输出流并告诉浏览器这是一个下载文件 response.setContentType("application/vnd.ms-excel"); response.setHeader("Content-Disposition", "attachment;filename=transcript.xls"); workbook.write(response.getOutputStream()); workbook.close(); }这里的核心是HSSFWorkbook,它对应 Excel 2003 的.xls格式,兼容性最好但单表最多 65536 行,对成绩单项目完全够用。如果要支持.xlsx和更多样式,换成XSSFWorkbook并把文件名改成.xlsx。注意导出的文件是乱码时,检查Content-Disposition里的文件名是否需要 URL 编码,中文文件名在 Chrome 和 IE 的解析方式不同。
实际生产里这个功能往往还会加一个限制:只有审核通过的数据才能出现在导出成绩单里。这个判断可以在 SQL 里做,也可以在 Controller 里过滤。我建议在 SQL 里做,因为数据量大时前端过滤会遇到分页断层的麻烦:
WHERE sa.stu_no = #{stuNo} AND sa.status = 2在这套第二课堂成绩单系统上,我最后悔的一件事是早期没把“导出内容需要走审核状态过滤”这个规则想清楚,导致导出的成绩单把待审核的数据也打出来了。这个错误在学生答辩演示时如果被发现,被追问的程度不亚于安全和漏洞问题。所以如果你拿到源码后要加导出功能,请一定先处理好审核状态过滤。
另外一个我常用的验证方法是:导出完 Excel 后,把文件里的数据跟页面上的汇总数字手工比对一遍。比如我先把页面里某个学生已有的学时加总,再打开导出的文件看合计是否一致。这不只是验证功能,也是在熟悉这套系统的业务逻辑。如果你能把这个对比过程熟练讲出来,答辩时老师会觉得你是真实做过而不是背稿子。
希望这些 JSP+SSM 的整合思路、表设计方式和避坑记录能帮到你。毕业设计不一定非要从零写,能把一套源码读懂、能说清楚每个模块为什么这么设计,本身就是合格的能力证明。
本文还有配套的精品资源,点击获取