
简介这是一份基于SSMSpring、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 应用里用数据库事务保证名额不被超卖用状态字段记录流程进度。选择 SSMSpring 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) ) ENGINEInnoDB; 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) ) ENGINEInnoDB; 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) ) ENGINEInnoDB;这段 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.version5.3.27/spring.version mybatis.version3.5.15/mybatis.version /properties dependencies dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.0.7/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency /dependencies需要留意mybatis-spring版本不是越新越好2.0.x 与 Spring 5.3 配合比较稳定。如果原始源码包使用的是 Spring 4 或更早版本建议一并升级到上面这组否则运行时很容易出现ClassNotFoundException。如果不是必须使用特定版本不要混合 Spring 5 和 MyBatis 3.4 以下的老版本。3.2 Spring 配置文件中的三个关键 beanSSM 的配置通常会拆成applicationContext.xml、spring-mvc.xml和web.xml三份。其中applicationContext.xml承载数据源、事务管理器和 SQL 会话工厂下面是核心片段。bean iddruidDataSource classcom.alibaba.druid.pool.DruidDataSource property namedriverClassName valuecom.mysql.cj.jdbc.Driver/ property nameurl valuejdbc:mysql://localhost:3306/graduation_topic?useUnicodetrueamp;characterEncodingutf8amp;serverTimezoneAsia/Shanghai/ property nameusername valueroot/ property namepassword value你的密码/ /bean bean idsqlSessionFactory classorg.mybatis.spring.SqlSessionFactoryBean property namedataSource refdruidDataSource/ property namemapperLocations valueclasspath:mapper/*.xml/ /bean bean classorg.mybatis.spring.mapper.MapperScannerConfigurer property namebasePackage valuecom.graduation.dao/ /beandriverClassName使用的是com.mysql.cj.jdbc.Driver这是 MySQL 8 驱动类老项目里的com.mysql.jdbc.Driver兼容但会打印弃用警告。serverTimezoneAsia/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 SqlSessionFactoryspring-mvc.xmlController 扫描、视图解析、静态资源、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 idselectByIdForUpdate resultTypeTopic SELECT id, topic_code, title, max_people, deadline FROM t_topic WHERE id #{id} FOR UPDATE /select select idcountApproved resultTypelong SELECT COUNT(*) FROM t_selection WHERE topic_id #{topicId} AND status approved /selectFOR 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 缺少时区参数追加serverTimezoneAsia/Shanghai页面中文乱码数据库或连接字符集不一致统一使用utf8mb4并在连接上加characterEncodingutf8项目启动超时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/目录不要把数据库脚本和代码资源混在一起方便接收方快速重建环境。本文还有配套的精品资源点击获取