简介:这是一份基于SSM(Spring、SpringMVC、MyBatis)与MySQL实现的毕业选题管理系统完整源码,面向Java Web初学者、毕业设计选题阶段的学生及需要快速搭建后台管理项目的开发者。系统涵盖登录注册、选题发布、学生选题、教师审核、状态查询等典型功能,采用MVC分层思路,DAO层、Service层、Controller层职责分明,完整演示了从框架整合、业务分层到数据库交互的常用开发流程。压缩包共1150个文件,以jsp页面、java源码、xml配置、sql脚本及jar依赖为主,辅以css、js等前端资源,整体约60.44MB,目录结构清晰,便于按模块阅读与部署。目前已有65人浏览学习,适合用来理解SSM框架在真实项目中的落地方式。资料内含项目源码、SQL初始化脚本、配置文件及部署相关说明,可帮助读者减少环境搭建与排错时间,快速跑通并二次开发。
1. 基于 SSM 和 MySQL 的毕业选题管理系统,到底解决了什么问题
每年三月底,教务老师都会陷入同一种循环:学生在群里喊“题目满了”,老师反复核对 Excel,管理员最后还要手动汇总一份“各专业选题情况表”。这类问题的根源不是工作量大,而是多人同时操作同一份数据时缺少锁和状态机。基于 SSM 和 MySQL 实现的毕业选题管理系统,就是把学生选题、教师审批、管理员统计放到一个 Web 应用里,用数据库事务保证名额不被超卖,用状态字段记录流程进度。选择 SSM(Spring + SpringMVC + MyBatis),不是因为它新,而是因为 Spring 管业务对象、SpringMVC 管请求分发、MyBatis 管 SQL,边界清晰;MySQL 的 InnoDB 提供行级锁,恰好是选题场景最需要的能力。对刚拿到这份源码包的人,我的建议是先跑通部署,再动手改业务逻辑。
2. 先理清数据模型:选题系统的表结构和 MySQL 关键设计
2.1 从业务环节拆出五张核心表
毕业选题管理最常见的流程是:管理员维护学生和教师信息,教师填报题目,学生在规定时间内选题目,教师确认或拒绝,管理员按专业、指导教师查看进度。这背后最少需要三张业务表:学生表、题目表、选题记录表,再加上教师表和用户权限表,一共五张。
我一般不会在最开始就加动态表单或工作流引擎,校内选题系统的核心就两个:谁能选、选没选上。表结构越简单,后期越容易用 SQL 排查数据问题。下面将常见的字段对应关系整理成一张表,便于对照:
| 表名 | 作用 | 关键关联 |
|---|---|---|
| t_student | 学生信息 | 与选题记录一对多 |
| t_teacher | 教师信息 | 与题目一对多 |
| t_topic | 题目信息 | 与选题记录一对多 |
| t_selection | 选题记录 | 学生与题目的多对多关联 |
| t_user | 登录账号与角色 | 关联学生或教师 |
学生表保留student_no、student_name、major、class_name;教师表保留teacher_no、teacher_name、department;题目表保留唯一编号topic_code、题目名称、简介、允许人数、截止时间和出题教师 ID。选题记录表是最核心的,必须同时保留学生 ID、题目 ID、状态和操作时间。
2.2 MySQL 建表语句中的关键参数
下面是一份可直接执行的 MySQL 8.0 建表脚本,重点关注字符集、存储引擎和唯一索引的位置。
CREATE DATABASE IF NOT EXISTS graduation_topic DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE graduation_topic; CREATE TABLE t_student ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_no VARCHAR(20) NOT NULL, student_name VARCHAR(50) NOT NULL, major VARCHAR(100) NOT NULL, class_name VARCHAR(100) DEFAULT NULL, gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_student_no (student_no) ) ENGINE=InnoDB; CREATE TABLE t_topic ( id BIGINT PRIMARY KEY AUTO_INCREMENT, topic_code VARCHAR(30) NOT NULL, title VARCHAR(200) NOT NULL, description TEXT, max_people INT NOT NULL DEFAULT 1, deadline DATETIME NOT NULL, teacher_id BIGINT NOT NULL, UNIQUE KEY uk_topic_code (topic_code) ) ENGINE=InnoDB; CREATE TABLE t_selection ( id BIGINT PRIMARY KEY AUTO_INCREMENT, student_id BIGINT NOT NULL, topic_id BIGINT NOT NULL, status VARCHAR(10) NOT NULL DEFAULT 'pending', gmt_create DATETIME DEFAULT CURRENT_TIMESTAMP, gmt_modified DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_selection (student_id, topic_id), KEY idx_topic_status (topic_id, status) ) ENGINE=InnoDB;这段 DDL 有四个细节值得展开。
第一,字符集用utf8mb4,而不用utf8,因为 MySQL 的utf8最多三字节,遇到生僻字或 emoji 时会出现转化异常。第二,所有表都用 InnoDB,只有它支持事务和行级锁,选题提交必须依赖这两个能力。第三,唯一索引uk_selection放在数据库层兜底,即使应用层忘记查重,重复插入也会被拒绝。第四,status字段长度写10,三态状态值pending、approved、rejected正好够用,不要无意义地写成255。
2.3 处理“同一个题目被多人抢”的两种方案
最直接的方案是在插入记录前查一次已选人数,小于max_people就允许插入。但这种方式在并发下会失效:两个请求同时查询到人数未满,接着同时插入,最终超过容量。原因很简单,事务默认的隔离级别下,两次COUNT(*)都无法读到对方未提交的数据。
所以在业务层,我更倾向于使用SELECT ... FOR UPDATE锁住题目记录。执行下面这条 SQL 后,同一时间只有一个请求能读到这条题目数据,其他请求会等待锁释放。
SELECT id, max_people, deadline FROM t_topic WHERE id = ? FOR UPDATE;锁住题目记录后,后续的人数统计和插入操作就在同一个事务里有序执行,第二个事务读到最新数据时发现已经满员,业务层直接抛出提示。相比只靠业务代码判断,这种方式把并发控制下沉到了数据库,可靠性更高。
3. SSM 框架集成与配置:从依赖冲突到项目启动
3.1 Maven 依赖选择与版本搭配
拿到源码后,最先遇到的往往不是功能问题,而是依赖冲突。下面是一套经过验证的版本组合,适用 JDK 1.8、Tomcat 8.5、MySQL 8.0 的环境。
<properties> <spring.version>5.3.27</spring.version> <mybatis.version>3.5.15</mybatis.version> </properties> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-webmvc</artifactId> <version>${spring.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>${mybatis.version}</version> </dependency> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>2.0.7</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> </dependencies>需要留意,mybatis-spring版本不是越新越好,2.0.x 与 Spring 5.3 配合比较稳定。如果原始源码包使用的是 Spring 4 或更早版本,建议一并升级到上面这组,否则运行时很容易出现ClassNotFoundException。如果不是必须使用特定版本,不要混合 Spring 5 和 MyBatis 3.4 以下的老版本。
3.2 Spring 配置文件中的三个关键 bean
SSM 的配置通常会拆成applicationContext.xml、spring-mvc.xml和web.xml三份。其中applicationContext.xml承载数据源、事务管理器和 SQL 会话工厂,下面是核心片段。
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource"> <property name="driverClassName" value="com.mysql.cj.jdbc.Driver"/> <property name="url" value="jdbc:mysql://localhost:3306/graduation_topic?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"/> <property name="username" value="root"/> <property name="password" value="你的密码"/> </bean> <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean"> <property name="dataSource" ref="druidDataSource"/> <property name="mapperLocations" value="classpath:mapper/*.xml"/> </bean> <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer"> <property name="basePackage" value="com.graduation.dao"/> </bean>driverClassName使用的是com.mysql.cj.jdbc.Driver,这是 MySQL 8 驱动类,老项目里的com.mysql.jdbc.Driver兼容但会打印弃用警告。serverTimezone=Asia/Shanghai必须加上,否则第一次获取连接时会因为时区无法识别而报错。MapperScannerConfigurer的作用是自动扫描 DAO 接口,省去逐个注册 Mapper 的繁琐操作。
3.3 SpringMVC 配置中容易漏掉的静态资源映射
SpringMVC 的DispatcherServlet会拦截所有请求,如果不放行静态资源,前端页面里的css/js/images会全部返回 404。
<mvc:resources mapping="/static/**" location="/static/"/> <mvc:annotation-driven/>配置里还要加上视图解析器,用于把 Controller 返回的逻辑视图名解析为 JSP 或 HTML。如果前端使用 AJAX,需要保证jackson-databind在依赖中,否则 JSON 返回时会报HttpMediaTypeNotAcceptableException。
配置文件之间的职责可以用下表快速对照:
| 配置文件 | 主要关注点 |
|---|---|
| web.xml | DispatcherServlet 映射、Spring 监听器 |
| applicationContext.xml | 数据源、事务、MyBatis SqlSessionFactory |
| spring-mvc.xml | Controller 扫描、视图解析、静态资源、JSON 支持 |
3.4 启动后先看日志,再点页面
配置完成后,先不要急着一键部署到生产。在本地启动 Tomcat,观察catalina.out日志里是否出现Initializing Spring root WebApplicationContext和Registered Mapper interface这两行。前者表示 Spring 容器初始化完成,后者表示 MyBatis 的 Mapper 已扫描成功。等到日志出现Server startup in ... milliseconds,再打开浏览器访问系统首页。
4. 核心业务功能实现:选题、审批与统计
4.1 学生选题的 Controller-Service-Mapper 全链路
选题操作是整套系统的关键路径,代码结构可以拆成 Controller、Service、Mapper 三层。下面是 Service 层的核心实现,注意@Transactional必须加在 public 方法上,且不能在同类内部调用。
@Service public class SelectionServiceImpl implements SelectionService { @Autowired private TopicMapper topicMapper; @Autowired private SelectionMapper selectionMapper; @Transactional(rollbackFor = Exception.class) public void selectTopic(Long studentId, Long topicId) { // 加锁查询题目,等待锁释放后读到最新数据 Topic topic = topicMapper.selectByIdForUpdate(topicId); if (topic == null) { throw new BizException("题目不存在"); } // 推送当前的截止时间 if (topic.getDeadline().before(new Date())) { throw new BizException("该题目已过选题截止时间"); } long approvedCount = selectionMapper.countApproved(topicId); if (approvedCount >= topic.getMaxPeople()) { throw new BizException("该题目已满员"); } try { selectionMapper.insert(new Selection(studentId, topicId, "pending")); } catch (DuplicateKeyException e) { // 重复提交时,数据库唯一索引兜底 throw new BizException("你已经选过这个题目了"); } } }这里的关键逻辑都在数据库层:selectByIdForUpdate锁住目标题目,countApproved统计当前已审批通过的人数,insert依赖唯一索引阻止同一学生反复选题。业务层的校验只是提前给出提示,真正的并发安全由FOR UPDATE和唯一索引共同保证。
4.2 Mapper XML 中的锁查询与统计人数
对应的 Mapper XML 写法如下:
<select id="selectByIdForUpdate" resultType="Topic"> SELECT id, topic_code, title, max_people, deadline FROM t_topic WHERE id = #{id} FOR UPDATE </select> <select id="countApproved" resultType="long"> SELECT COUNT(*) FROM t_selection WHERE topic_id = #{topicId} AND status = 'approved' </select>FOR UPDATE只在 InnoDB 且事务未提交时有效。因此拿到Topic后不要做耗时的远程调用或循环,尽量在锁内只做校验和数据库操作。业务方法结束、事务提交后,锁才会释放。
4.3 教师审批与状态流转
教师审批时,一般只更新一条记录状态。为了安全,UPDATE语句要带上当前状态作为条件,防止两个教师同时处理同一条记录。
UPDATE t_selection SET status = #{targetStatus} WHERE id = #{id} AND status = 'pending'如果更新的影响行数为 0,说明记录已经被审批过,可以提示“该选题已处理”。状态字段的值建议统一用常量管理,避免在不同地方硬编码字符串。
| 状态 | 含义 | 可操作角色 |
|---|---|---|
| pending | 已提交,等待教师审批 | 教师可审批或拒绝 |
| approved | 审批通过 | 管理员可查看统计 |
| rejected | 已被拒绝 | 学生可重新选题 |
4.4 管理员按专业统计选题完成率
管理员页面需要看到“每个专业有多少人、多少人已选题”,这比一个个查学生效率高得多。一条分组查询可以完成:
SELECT s.major, COUNT(DISTINCT s.id) AS student_total, COUNT(DISTINCT CASE WHEN sel.id IS NOT NULL THEN s.id END) AS selected_count FROM t_student s LEFT JOIN t_selection sel ON s.id = sel.student_id AND sel.status = 'approved' GROUP BY s.major;LEFT JOIN保证没有选题记录的学生也计入统计,CASE WHEN只把已审批通过的记录算作有效选题。这样管理员的报表数据与学生列表始终共享同一套数据源,不会出现表格对不上的情况。
5. 部署、打包与源码交付中的常见坑
5.1 用 Maven 打 war 包
拿到源码后,建议先建一个干净的部署目录,再执行打包:
mvn clean package -DskipTests执行完成后在target/目录下会生成.war文件。将这个 war 放到 Tomcat 的webapps/目录,启动后会自动解压。访问路径是http://localhost:8080/项目名/。如果本地 MySQL 的密码或端口不是默认值,先修改jdbc.properties里的连接配置,再执行打包,避免 war 包内含错误配置。
5.2 三个最容易忽略的配置项
以下三个问题在交付现场非常常见,每一个都能让项目“看起来能跑,却跑不稳”。
| 问题现象 | 根本原因 | 解决方式 |
|---|---|---|
| 启动报时区错误 | JDBC URL 缺少时区参数 | 追加serverTimezone=Asia/Shanghai |
| 页面中文乱码 | 数据库或连接字符集不一致 | 统一使用utf8mb4并在连接上加characterEncoding=utf8 |
| 项目启动超时 | JVM 内存或 Tomcat 启动时间不足 | 调整catalina.sh的JAVA_OPTS中的-Xmx参数 |
如果使用 Druid 连接池,还可以在监控页面观察活跃连接数。集中选题开始时,连接数会迅速上升,如果长期处于满负荷,需要检查连接池的最大连接数和 MySQL 的max_connections是否匹配。
5.3 用一个 SQL 验证防超选逻辑
打包交付前,不需要点完所有页面,跑一条 SQL 就能发现最严重的数据问题:
SELECT t.topic_code, COUNT(s.id) AS approved_count FROM t_topic t LEFT JOIN t_selection s ON t.id = s.topic_id AND s.status = 'approved' GROUP BY t.id HAVING approved_count > t.max_people;正常情况下查询结果为空。如果查出记录,说明应用层防超选逻辑失效或事务配置错误,优先检查@Transactional是否生效,以及t_selection表上的唯一索引是否真的存在。源码包交付时,将初始化 SQL 脚本单独放在doc/目录,不要把数据库脚本和代码资源混在一起,方便接收方快速重建环境。
本文还有配套的精品资源,点击获取