简介这套基于SSM与MySQL实现的毕业选题管理系统是面向Java Web学习者、毕业设计开发者及需要快速搭建选题管理场景的学生的完整源码包。系统采用Spring、SpringMVC、MyBatis分层架构覆盖用户登录注册、选题发布、学生选题、教师审核、选题状态查询等核心业务并包含数据库SQL脚本与可部署的WAR包能够帮助读者直观理解企业级Java项目的目录结构、Maven依赖管理及前后端交互流程。压缩包共1150个文件以240个JSP页面、160个JavaScript脚本、115个XML配置、108个Class文件及Java源文件为主同时包含CSS样式、图片、字体等静态资源整体约60.44MB结构清晰便于按模块查阅。目前已有65人学习下载。通过运行和分析该项目可深入学习SSM框架整合、MyBatis映射、MySQL数据操作以及Tomcat部署等关键技能对完善个人项目经验与提升开发能力具有较高参考价值。1. 毕业选题管理系统卡在 Excel 传阅时SSM 和 MySQL 是首选方案每年五月份都会收到几条类似的求助毕业选题还停留在辅导员收 Excel、老师统计、学生看公告栏的阶段临近截止日期时容易出现选题撞车、名额分配不均、审核意见无法留痕。用 SSMSpring SpringMVC MyBatis搭配 MySQL 实现的毕业选题管理系统正是这类场景下最常见的落地方案它把题目发布、学生选题、教师审核、管理员导出合成一条可跟踪的业务线。这篇记录按一套常规的选题管理诉求来拆先设计能跑通全流程的 MySQL 表结构再搭出 SSM 工程骨架接着实现选题与审核的状态流转最后把源码打包后常踩的部署坑和上线核对技巧一并列出。适合正在做毕业设计、想用 SSM 补一次完整 CRUD 加状态机的同学也适合需要把类似选题流程搬到内部系统的开发。2. 先立数据模型毕业选题管理系统的 MySQL 表结构与建表脚本2.1 从业务角色推导出的五张核心表毕业选题管理并不需要把权限设计做成一棵 RBAC 树。常规系统只需要三种登录身份学生、教师和管理员。学生要能浏览题目并提交申请教师负责发布题目与审核申请管理员做基础数据维护和最终名单导出。围绕这三个角色核心表通常是五张sys_user统一存账号与角色student_profile存学生学号和班级teacher_info存教师工号和每学期可带人数topic存题目发布信息selection_record存一条条选题申请和审核结果。这五张表的划分有一个明显用意登录认证、人员档案、业务数据三层分离。这样后续要把系统改成“课程设计选题”或“竞赛报名”只需要替换topic表和selection_record表账号体系不动。表与表之间不建物理外键原因是后期清数据、做导入导出时物理外键会很碍事另外在选题高峰期外键检查会扩大锁范围影响 MySQL 的写入并发。表之间的关联交给应用层校验数据库只保留必要的唯一索引来兜底这是 SSM 项目里更省心的做法。2.2 能直接导入 Navicat 的建表脚本脚本在 MySQL 5.7 和 8.0 上都能直接跑不依赖 8.0 的新语法。字符集统一用utf8mb4避免姓名、备注里的生僻字或 emoji 在写入时变成乱码。导入顺序建议是sys_user→student_profile→teacher_info→topic→selection_record因为后面的表要引用前面的 id。用 Navicat 导入时选“运行 SQL 文件”别直接复制整个文件到查询窗口否则注释和分隔符解析容易出现偏差。CREATE DATABASE IF NOT EXISTS topic_selection DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE topic_selection; CREATE TABLE sys_user ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT 用户ID, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT 演示环境建议先存md5上线前必须替换, user_type TINYINT NOT NULL DEFAULT 0 COMMENT 0学生 1教师 2管理员, real_name VARCHAR(50) NOT NULL, major_id BIGINT NULL COMMENT 学生专业ID这里冗余不建外键, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_username (username) ) ENGINEInnoDB COMMENT系统用户表; CREATE TABLE teacher_info ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id BIGINT NOT NULL COMMENT 对应sys_user.id, department VARCHAR(100) COMMENT 所属系部, capacity INT NOT NULL DEFAULT 10 COMMENT 本学期可带人数, remaining INT NOT NULL DEFAULT 10 COMMENT 剩余可带人数, UNIQUE KEY uk_teacher_user (user_id) ) ENGINEInnoDB COMMENT教师信息表; CREATE TABLE topic ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, teacher_user_id BIGINT NOT NULL COMMENT 发布教师, title VARCHAR(200) NOT NULL COMMENT 题目名称, description TEXT COMMENT 题目要求和参考方向, pub_year CHAR(4) NOT NULL COMMENT 选题年度如2026, status TINYINT NOT NULL DEFAULT 0 COMMENT 0草稿 1发布 2满员, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_year_teacher (pub_year, teacher_user_id), KEY idx_status (status) ) ENGINEInnoDB COMMENT题目表; CREATE TABLE selection_record ( id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, student_user_id BIGINT NOT NULL COMMENT 学生用户ID, topic_id BIGINT NOT NULL, topic_year CHAR(4) NOT NULL COMMENT 冗余年度便于按年查询, status TINYINT NOT NULL DEFAULT 1 COMMENT 1待审核 2通过 3驳回 4退选, apply_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, approve_time DATETIME NULL, reject_reason VARCHAR(255) NULL COMMENT 驳回原因, UNIQUE KEY uk_student_topic (student_user_id, topic_id), UNIQUE KEY uk_student_year (student_user_id, topic_year) ) ENGINEInnoDB COMMENT选题申请表;sys_user是登录入口password字段在演示环境里可以用 MD5正式环境建议改成加盐哈希。teacher_info里的capacity和remaining互为镜像不是冗余设计而是为并发扣减预留条件下一节展开。topic的pub_year用 CHAR(4) 刻意压长度比直接存 DATE 更省空间按年查询时能直接命中索引。selection_record里冗余topic_year是为了在不 jointopic表的情况下就能保证“一个学生在同一学年只能有一条选题记录”。参数说明UNIQUE KEY uk_student_topic防止同一学生对同一题目提交多条申请即使前端不做校验也能在数据库层挡住KEY idx_year_teacher是组合索引服务端做“本年度某教师名下的题目列表”时能同时用上年度和教师两个过滤条件。组合索引的顺序要把等值查询的字段放前面这里pub_year是等值条件teacher_user_id是范围条件顺序不能反。2.3 唯一索引和剩余配额字段的取舍第一次接触这个系统的人容易纠结既然selection_record可以通过 COUNT 统计某老师当前带了多少学生为什么还要在teacher_info里放remaining这里的关键不是省一次 COUNT而是状态口径。学生提交申请时记录状态是“待审核”此时题目还不算真正带出去但老师看到的剩余名额必须把“待审核 已通过”一起扣否则拦截不了超量申请。每次选题都去 COUNT 一遍selection_record会带来锁竞争和索引扫描开销不如在teacher_info维护一个剩余配额字段申请成功的瞬间就把它减一。剩余配额和唯一索引承担两种不同职责。唯一索引用于应用层漏判时兜底剩余配额用于热点路径上的快速判断。两者缺一不可也解释了“明明 remaining 大于 0INSERT 却失败”的疑问先执行 UPDATE 减配额减成功后记录插入失败事务回滚会把配额捞回来。这个流程放在第 4 章的 service 层代码里展开。外键和级联删除在这个系统里不建议使用。题目下已经有学生申请时题目只允许下架不允许物理删除学生毕业后历史选记录要保留用于复查。让这些规则收敛在 service 层比靠数据库级联更可控也给后续对接消息通知、成绩录入留了扩展位。2.4 初始化账号与测试数据建表完成后需要造几笔最小数据才能进入开发调试。通用做法是插入一个管理员、两个教师、三个学生并为每个教师初始化容量。真实场景中这些数据来自教务系统导出开发阶段手工 INSERT 即可。INSERT INTO sys_user (username, password, user_type, real_name, major_id) VALUES (admin, MD5(123456), 2, 系统管理员, NULL), (t1001, MD5(123456), 1, 张老师, NULL), (t1002, MD5(123456), 1, 李老师, NULL), (s20001, MD5(123456), 0, 王小明, 1), (s20002, MD5(123456), 0, 刘小红, 1), (s20003, MD5(123456), 0, 赵小刚, 2); INSERT INTO teacher_info (user_id, department, capacity, remaining) VALUES (2, 计算机系, 10, 10), (3, 软件工程系, 8, 8);这里的MD5(123456)只是在开发环境维持统一口径。用 MySQL 内置的 MD5 函数会让相同明文得到相同散列值上线前需要换成加盐算法否则一旦源码包泄露密码可以被彩虹表直接查出来。测试数据量不需要大五张表能串出“教师发题 — 学生选题 — 教师审核”的完整链路就够了性能问题留给索引优化章节处理。3. 落地 SSM 工程骨架配置顺序和关键参数3.1 依赖先把 mybatis-spring 接通SSM 工程和 Spring Boot 的最大区别在于一切依赖都显式声明启动过程也完全由 web.xml 驱动。这里不用 Spring Boot 的原因很实际毕业设计管理系统这类源码包往往需要同学在答辩现场直接改配置跑起来SSM 结构直观出错点固定更容易在几分钟内定位。如果引入 Boot 的自动配置一旦自动配置和手工配置冲突排查成本反而更高。依赖清单里最容易被忽略的是mybatis-spring它负责把 MyBatis 的SqlSessionFactory挂到 Spring 容器里。只用 MyBatis 不加mybatis-springService 层拿不到SqlSession启动时会出现NoSuchBeanDefinitionException。版本选择建议以你本地 Maven 仓库已有的为准Spring 5.2.x、MyBatis 3.5.x、mybatis-spring 2.0.x 是常见组合MySQL Connector/J 用 8.0.x 可以同时连接 5.7 和 8.0 的数据库反向则不成立。dependencies dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-jdbc/artifactId version${spring.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version${mybatis-spring.version}/version /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version${mysql.connector.version}/version /dependency /dependencies版本变量集中放在properties里以后升级依赖只改一处。spring-jdbc提供事务管理和DataSource支持spring-webmvc负责处理 HTTP 请求两者缺了任何一个都会让工程在启动阶段报ClassNotFoundException。JDBC 驱动的 scope 保持默认 compile打包进 war 时才能把驱动带上。3.2 web.xml、Spring 与 SpringMVC 的分层配置SSM 的 web 启动逻辑分两层ContextLoaderListener负责初始化根容器加载业务 Service、Dao 和数据源DispatcherServlet负责初始化 Web 层容器加载 Controller 和视图解析器。两个容器各管各的 BeanController 能看到 ServiceService 看不到 Controller这种分层能有效避免循环依赖。关键配置如下web-app xmlnshttp://xmlns.jcp.org/xml/ns/javaee version3.1 context-param param-namecontextConfigLocation/param-name param-valueclasspath:applicationContext.xml/param-value /context-param listener listener-classorg.springframework.web.context.ContextLoaderListener/listener-class /listener filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param init-param param-nameforceEncoding/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping servlet servlet-namedispatcher/servlet-name servlet-classorg.springframework.web.servlet.DispatcherServlet/servlet-class init-param param-namecontextConfigLocation/param-name param-valueclasspath:spring-mvc.xml/param-value /init-param load-on-startup1/load-on-startup /servlet servlet-mapping servlet-namedispatcher/servlet-name url-pattern//url-pattern /servlet-mapping /web-appCharacterEncodingFilter必须放在所有过滤器的最前面并且forceEncoding必须为 true。只设置 encoding 而不强制后端代码里手动request.setCharacterEncoding时可能被覆盖。DispatcherServlet映射/而不是/*前者让 Tomcat 的 DefaultServlet 能处理 js、css、图片等静态资源后者会把静态资源请求也拦截进 SpringMVC导致页面样式加载不出来。load-on-startup为 1让 Web 容器启动时就初始化 SpringMVC尽早暴露配置错误。applicationContext.xml放context:component-scan扫描com.example.service和com.example.daospring-mvc.xml只扫描com.example.controller两边扫描包不能重叠。常见做法是 Controller 包放在父包com.example下如果根容器扫描了整个com.exampleController 就被注册了两遍事务代理可能失效。这里建议直接拆开扫描粒度省去后顾之忧。3.3 MyBatis 驼峰映射与 Mapper 绑定MyBatis 配置里最值得注意的开关是mapUnderscoreToCamelCase。表字段是teacher_user_id实体属性是teacherUserId开启这个开关后查询结果能自动映射省去大量手写 resultMap。剩余字段类型不一致或需要关联对象时再用 resultMap 精准指定。configuration settings setting namemapUnderscoreToCamelCase valuetrue/ setting namelogImpl valueSTDOUT_LOGGING/ /settings typeAliases package namecom.example.entity/ /typeAliases /configurationMapper 接口与 XML 文件的绑定通过两处保证接口全限定名等于 XML namespace方法名等于 XML 中 statement 的 id。XML 文件放在src/main/resources/mapper目录下并在 Spring 配置里声明mapperLocations。一个典型的列表查询如下select idselectPublishList resultTypeTopic SELECT id, teacher_user_id, title, description, pub_year, status, create_time FROM topic WHERE pub_year #{pubYear} AND status IN (1, 2) AND teacher_user_id #{teacherUserId} ORDER BY create_time DESC /select#{pubYear}是预编译参数占位符MyBatis 会把它替换成?由 JDBC 驱动做参数绑定能有效防 SQL 注入。对比${pubYear}是字符串拼接任何字符都会直接拼进 SQL绝不应当出现在面向用户的查询条件里。ORDER BY create_time DESC让新发布的题目排前面这是选题系统的默认排序习惯学生打开页面第一眼看到的是最新题目。3.4 数据源参数和本地连通性验证数据源选型常见是 DBCP2 或 Druid。毕业选题系统并发量不高DBCP2 零依赖更容易上手如果需要在运维页面看活跃连接数Druid 的监控页面更方便。核心参数只有五个初始连接数、最大活跃连接数、最大等待时间、连接验证语句、空闲回收周期。推荐配置如下参数推荐值说明initialSize5启动时建立的连接数过小导致首次请求慢maxTotal20高峰期最大连接超过后线程等待maxWaitMillis6000等待连接的超时时间单位毫秒validationQuerySELECT 1验证连接是否有效防止拿到断开的连接testWhileIdletrue空闲连接定期验证默认 true建议保留数据库连接串是本地跑通最容易出错的一处。MySQL 5.7 和 8.0 对时区、SSL 的处理不同建议统一写成这样jdbc.driverClassNamecom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/topic_selection?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue jdbc.usernameroot jdbc.passwordyour_passworduseSSLfalse在本地开发环境能避免证书校验警告serverTimezoneAsia/Shanghai解决 JDBC 驱动与 MySQL 服务器时区不一致导致的日期偏移allowPublicKeyRetrievaltrue是 MySQL 8.0 使用 caching_sha2_password 登录时需要的选项不加会报 Access denied。先用命令行mysql -uroot -p确认账号密码能登录再用 Navicat 或 MySQL Workbench 连一次排除端口和防火墙问题最后才启动 SpringMVC。连接串错了报的错很直接Cannot create PoolableConnectionFactory看到这个异常优先检查密码、端口、库名三项。4. 选题和审核实现状态机、并发扣减与容错4.1 选题状态机与状态流转控制选题管理系统的业务本质是状态机。状态定义如下学生提交申请后记录变成“待审核”教师审核通过后变成“通过”驳回到“驳回”学生主动取消或超时未处理则变成“退选”。状态迁移不允许跳变已通过的选题不能直接变成“待审核”这类规则写在 service 层比写 SQL 更容易维护。public static final int STATUS_WAITING 1; public static final int STATUS_APPROVED 2; public static final int STATUS_REJECTED 3; public static final int STATUS_CANCELED 4; public void approve(Integer recordId) { int updated selectionRecordMapper.updateStatus( recordId, STATUS_WAITING, STATUS_APPROVED ); if (updated 0) { throw new BizException(该申请已被处理请刷新页面); } }updateStatus的 SQL 要带上当前状态条件UPDATE selection_record SET status #{newStatus}, approve_time NOW() WHERE id #{id} AND status #{expectStatus}。用返回行数判断是否更新成功比先 SELECT 再 UPDATE 更可靠因为它把“确认旧状态”和“写入新状态”合并成一条原子语句。没有这一步两个教师同时处理同一申请时后提交的人会把先提交的结果覆盖掉。4.2 原子扣减剩余配额学生点击选题后系统要做两件事扣掉教师的剩余配额插入选题申请记录。这两步必须在一个事务里。扣减不能先查询remaining再在 Java 里减一后写回因为高并发下两个请求可能同时读到相同的remaining导致超卖。正确做法是把判断和更新合并成一条 UPDATETransactional(rollbackFor Exception.class) public boolean applyTopic(Long studentUserId, Long topicId) { Topic topic topicMapper.selectById(topicId); if (topic null || topic.getStatus() ! TOPIC_PUBLISHED) { throw new BizException(题目不可选); } int rows teacherInfoMapper.decreaseRemaining(topic.getTeacherUserId()); if (rows 0) { throw new BizException(该教师可带人数已满); } SelectionRecord record new SelectionRecord(); record.setStudentUserId(studentUserId); record.setTopicId(topicId); record.setTopicYear(topic.getPubYear()); record.setStatus(STATUS_WAITING); selectionRecordMapper.insert(record); return true; }对应 Mapper XML 里的扣减语句UPDATE teacher_info SET remaining remaining - 1 WHERE user_id #{teacherUserId} AND remaining 0这行 UPDATE 是并发安全的关键。MySQL 对单条 UPDATE 加行锁后到的请求会等前一个事务提交或回滚remaining 0条件保证不会扣成负数。如果更新行数为 0说明配额已经耗尽直接抛出业务异常事务回滚不会插入脏数据。Transactional确保扣配额和插记录要么都成功要么都回滚任何一步失败都不会留下“配额扣了但没申请记录”的中间状态。4.3 驳回回补的幂等写法教师驳回申请后剩余配额要加回来。这个步骤同样要防重复执行同一申请被驳回两次配额不能加两次。常见做法是在更新状态时带上前置状态条件更新行数为 1 时才回补配额Transactional(rollbackFor Exception.class) public void reject(Integer recordId, String reason) { int updated selectionRecordMapper.updateStatusWithReason( recordId, STATUS_WAITING, STATUS_REJECTED, reason ); if (updated 0) { throw new BizException(申请状态已变化请刷新后重试); } Long teacherUserId selectionRecordMapper.selectTeacherByRecordId(recordId); teacherInfoMapper.increaseRemaining(teacherUserId); }这里的selectTeacherByRecordId通过关联查询拿到教师 ID不能用前端传来的参数防止有人篡改请求把配额加回别人账上。increaseRemaining的 SQL 是UPDATE teacher_info SET remaining remaining 1 WHERE user_id #{teacherUserId}天然幂等联调阶段即使手动重跑也不会把数值加错。4.4 截止时间与自动结题选题阶段通常有固定截止日期。实现方式有两种一种是 Spring 的Scheduled定时任务另一种是 MySQL 的事件调度器。推荐前者因为业务逻辑还能复用 service 层的 Java 代码日志也集中在应用里。事件调度器可以作为应用宕机时的兜底但不要在事件里写复杂业务只做状态推进。Scheduled(cron 0 0 2 * * ?) Transactional(rollbackFor Exception.class) public void autoCancelExpiredRecords() { ListLong expiredIds selectionRecordMapper.selectWaitingBefore(deadline); for (Long id : expiredIds) { selectionRecordMapper.updateStatus(id, STATUS_WAITING, STATUS_CANCELED); Long teacherUserId selectionRecordMapper.selectTeacherByRecordId(id); teacherInfoMapper.increaseRemaining(teacherUserId); } }cron表达式0 0 2 * * ?表示每天凌晨两点执行避开学生使用高峰期。selectWaitingBefore查询所有状态为“待审核”且申请时间早于截止时间的记录然后把状态置为“退选”并归还配额。这个任务必须在配置类上启用EnableScheduling否则注解不生效。定时任务里的事务粒度按单条记录控制不要包住整个循环否则一条数据失败会导致全部回滚影响范围过大。4.5 列表查询的索引与参数化首页题目列表最常见的是按年度、按状态、按教师筛选。组合索引idx_year_teacher(pub_year, teacher_user_id)能同时覆盖年度和教师两个条件。如果还要加状态过滤就在组合索引后面扩展状态列顺序是等值列在前范围列在后但修改索引前要对比执行计划不要凭感觉加。字段上用了函数或表达式后索引会失效比如WHERE YEAR(create_time) 2026无法命中create_time上的索引推荐写法是WHERE create_time 2026-01-01 AND create_time 2027-01-01。模糊查询要留意%位置。搜索题目时LIKE java%能走索引LIKE %java%无法走索引数据量到几万条后前后模糊扫描会拖慢页面。若确实需要包含匹配可以引入全文索引或者保持现状并限制必须同时带年度过滤条件。排序字段也尽量落在索引列上ORDER BY create_time DESC在create_time有索引时使用 filesort 的概率会明显降低。SELECT id, title, teacher_user_id, pub_year, status FROM topic WHERE pub_year #{pubYear} AND teacher_user_id #{teacherUserId} ORDER BY create_time DESC LIMIT #{offset}, #{pageSize}LIMIT分页在深页码时性能下降OFFSET 100000会让 MySQL 扫描前面十万行再丢弃。对毕业选题这种数据量很低的应用深分页问题可以暂不处理但要在代码里固定pageSize上限防止有人把参数改成百万级把数据库拖垮。5. 源码打包后的启动检查、排错对照与一致性核对5.1 拿到打包源码以后先检查三处配置源码打包的 scrum 包通常包含 SQL 脚本、Maven 工程、README但不同人打包习惯不同启动前不要默认里面所有配置都对。先看applicationContext.xml或jdbc.properties里的数据库连接串确认库名是topic_selection而不是test或别人的库名再看spring-mvc.xml里的注解扫描路径是否和你的包名一致包名不一致时 Controller 不会被加载访问 URL 直接 404最后检查pom.xml里是否配置了maven-war-plugin没有这个插件时打包出的 war 会缺少 web.xml 声明的结构。打包命令用 Maven 的标准流程mvn clean package -DskipTests产物在target目录下文件名带SNAPSHOT。部署到 Tomcat 8.5 或 9.0 时把 war 包丢进webapps目录Tomcat 会自动解压。应用上下文默认是 war 包名如果包名带版本号访问路径会变成http://localhost:8080/topic_selection-1.0/不想带版本号就把 war 重命名为ROOT.war直接通过根路径访问。5.2 启动时的高频报错对照报错现象常见原因处理方式Access denied for user rootlocalhost密码错误或账号无远程权限先用命令行登录确认密码再检查jdbc.properties是否被覆盖Unknown database topic_selection只部署了应用没执行 SQL 脚本用 Navicat 导入第 2 章的建表脚本Invalid bound statement (not found)Mapper 接口和 XML 没绑定检查 XML namespace 和接口全限定名再确认mapperLocations路径ClassNotFoundException: org.springframework.web.context.ContextLoaderListener依赖没打包进 war检查maven-war-plugin配置或直接mvn clean package重新打Failed to configure a DataSource多数据源配置或驱动类名错误确认驱动类是com.mysql.cj.jdbc.Driver不是老旧的com.mysql.jdbc.DriverInvalid bound statement是最容易误导人的问题。IDE 里直接运行 main 方法能加载到 XML但打 war 后 XML 没进classes目录就会出现这个报错。验证方法把 war 解压查看WEB-INF/classes/mapper下有没有 XML 文件。没有就调整 pom 里的 resource 配置把src/main/resources/mapper明确声明为资源目录。5.3 上线后的备份与配额一致性核对选题系统上线后最该盯的不是页面交互而是teacher_info.remaining和selection_record中的有效申请数是否一致。配额一旦和记录数偏离后续教师端显示的名额就会出错。可以用一条 SQL 做全量核对SELECT t.id, t.remaining, t.capacity - COALESCE(cnt.active_count, 0) AS calc_remaining FROM teacher_info t LEFT JOIN ( SELECT teacher_user_id, COUNT(*) AS active_count FROM topic tp JOIN selection_record sr ON sr.topic_id tp.id WHERE sr.status IN (1, 2) GROUP BY teacher_user_id ) cnt ON cnt.teacher_user_id t.user_id WHERE t.remaining ! t.capacity - COALESCE(cnt.active_count, 0);返回空结果说明数据一致返回记录说明存在配额回补遗漏需要以计算值为准修正remaining。备份用 mysqldump 即可放在每天凌晨的 Windows 计划任务里执行配一个自动备份脚本保留最近七天文件echo off set YYYYMMDD%date:~0,4%%date:~5,2%%date:~8,2% mysqldump -uroot -p123456 topic_selection D:\backup\topic_selection_%YYYYMMDD%.sql forfiles /p D:\backup /m *.sql /d -7 /c cmd /c del path恢复时用mysql -uroot -p123456 topic_selection 备份文件.sql恢复前确认当前库没有正在进行的选题事务否则会把别人的操作覆盖。备份脚本里的密码在正式环境不要明文写在 bat 里改成 MySQL 的--defaults-extra-file指向权限为 600 的配置文件。启动前先用上面的一致性 SQL 跑一次确认数据干净再开放给学生访问。本文还有配套的精品资源点击获取