基于SSM和MySQL的毕业选题管理系统:从表设计到并发控制
2026/9/16 21:16:21 网站建设 项目流程

简介:这是一份基于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_nostudent_namemajorclass_name;教师表保留teacher_noteacher_namedepartment;题目表保留唯一编号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,三态状态值pendingapprovedrejected正好够用,不要无意义地写成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.xmlspring-mvc.xmlweb.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&amp;characterEncoding=utf8&amp;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.xmlDispatcherServlet 映射、Spring 监听器
applicationContext.xml数据源、事务、MyBatis SqlSessionFactory
spring-mvc.xmlController 扫描、视图解析、静态资源、JSON 支持

3.4 启动后先看日志,再点页面

配置完成后,先不要急着一键部署到生产。在本地启动 Tomcat,观察catalina.out日志里是否出现Initializing Spring root WebApplicationContextRegistered 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.shJAVA_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/目录,不要把数据库脚本和代码资源混在一起,方便接收方快速重建环境。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询