基于SSM+Vue的课程平台数据库设计与权限分页实现
简介《数据库系统原理》课程平台的设计与实现毕业论文文档面向计算机专业毕业设计学生与指导教师围绕SSMSpring、SpringMVC、MyBatis结合Vue.js与MySQL构建在线教学平台覆盖课程管理、用户角色、论坛讨论、成绩管理、在线答疑与资源下载等场景。压缩包共1个doc文件约2.9MB为完整论文稿含摘要、绪论、开发环境、系统分析、数据库设计、详细设计、系统截图与测试及总结致谢附ER图、数据字典和数据流图等设计材料。已有70人学习下载可用于梳理从需求分析、可行性论证到功能模块划分与数据库表结构设计的完整写作脉络也能对照SSMVue技术选型、测试验证与论文目录组织适合需要参考课程平台类毕业设计选题、结构和实现说明的读者。1. 从答辩现场的一问说起这套课程平台要交付出什么答辩时被问得最多的一个问题是网盘加班级群就能发课件为什么还要专门做一个《数据库系统原理》课程平台这个问题恰好点在要害上——一份合格的计算机毕业设计价值不在功能堆了多少而在把「角色—数据—流程」这条线走通。教师发资源、出试卷、批作业学生看课件、下载、答题、翻错题本管理员管人管公告管论坛这几类动作最后都要落到 MySQL 的表结构和 SSM 的分层调用上。这套方案用 Spring SpringMVC MyBatis 做后端、Vue 做前端、MySQL 存数据B/S 结构浏览器打开就能用覆盖学生信息、教师信息、教学资源、资源类型、公告信息、论坛、试卷、试题、作业、答题详情、错题和字典表十来张表。适合正在做计算机毕业设计或课程设计、需要一套能讲清楚也能跑起来的完整结构的人也适合要把教学辅助平台先落地一版雏形的从业者。2. 需求边界与总体架构三类角色、三层结构和技术栈的对应关系2.1 管理员、教师、学生三类角色的用例边界怎么划课程平台最容易做崩的地方是角色权限图省事一套接口所有人共用靠前端藏菜单来控制。前端藏起来的按钮后端照样能收到请求测试的时候没人查答辩老师随手用 Postman 打一下接口就露馅。所以角色划分要在需求阶段就定死并且和数据库、接口前缀一一对应。角色核心动作明确不该有的权限接口前缀管理员用户、教师、学生、资源类型、公告、论坛的全量增删改查不参与答题、不提交作业、不改自己的角色/admin/**教师发布与下架教学资源、维护试卷和试题、发布作业、批阅不能改其他教师发布的资源不能动字典表/teacher/**学生浏览与下载资源、答题交卷、查看错题、发帖不能发布资源不能查看别人的错题与成绩/student/**这张表看着简单落地时至少决定了三件事第一后端拦截器可以按 URL 前缀做粗粒度角色拦截不用在每个方法里写 if第二数据库里要有独立的教师表和学生表而不是所有人在 users 表里靠一个 role 字段区分——教学资源表要外键到教师答题记录要外键到学生混在一张表里查询会立刻变得又慢又难懂第三前端路由可以按角色拆成三份菜单配置登录后按返回的角色下发页面不用写一堆 v-if。提示角色划分的粒度决定后面数据库设计的复杂度。三类角色就够用的情况下不要硬拆第四类「游客」表未登录状态用 session 为空判断即可多一张表就多一层联调成本。2.2 B/S 结构下的请求链路一次分页查询要经过哪几层B/S 结构的好处是客户端零安装浏览器刷新就能拿到最新数据代价是所有状态都压到服务端。用户在资源列表页点「下一页」这一下完整链路是这样的Vue 组件把 page、size 和查询条件拼成 query 参数发给浏览器同源的/api/teacher/resource/page请求进到 Tomcat 后先经过登录拦截器校验 session再由 SpringMVC 的 DispatcherServlet 按RequestMapping分发到 ControllerController 只做参数校验和结果包装真正的分页计算交给 ServiceService 把 page 换算成 offset 后调 MapperMyBatis 拼出带 LIMIT 的 SQL 打到 MySQL结果集按实体类映射回来逐层包装成统一 JSON 返回给前端。任何一层偷懒后面都要返工。项目的目录结构建议按下面这个方式切和后端三层一一对应答辩时讲到哪一层就能立刻在工程里点开对应目录course-platform ├── src/main/java/com/demo │ ├── controller # 按角色分子包admin / teacher / student │ ├── service # 接口 impl业务规则只写在这里 │ ├── mapper # MyBatis 接口与 XML 一一对应 │ ├── entity # 和数据库表同名的实体类 │ ├── common # Result 统一返回、PageInfo、工具类 │ └── interceptor # 登录拦截、角色拦截 ├── src/main/resources │ ├── mapper # SQL 的 XML 文件 │ ├── spring-*.xml # 容器、MVC、MyBatis 三份配置 │ └── jdbc.properties # 数据库连接不进版本库 ├── src/main/webapp │ ├── static # Vue 打包产物、上传目录挂载点 │ └── WEB-INF/web.xml └── sql # 建表脚本 初始化字典数据目录分层的意义不只是好看。Controller 里一旦出现 SQL 或者业务判断测试阶段想单独验证某条规则就得整条链路起一遍反过来Service 里如果塞了 request 和 response接口就没法做单元测试。守住这条线系统测试那一章才有东西可写。2.3 技术栈各管什么SSM 的职责划分和依赖清单Spring 管对象生命周期和事务SpringMVC 管请求分发和参数绑定MyBatis 管 SQL 与 Java 对象的映射Vue 管视图和数据绑定MySQL 管存储。五个东西边界清楚各干各的这是 SSM Vue 这套组合到现在还在毕业设计里高频出现的原因——它足够老资料多出问题能查到也足够分层让论文的每一章都有对应内容可写。Maven 依赖按下面这组配版本号换成你本地仓库里已经下好的同大版本即可不要为了追新把 Spring 5 和 Spring 6 的包混在一起那是自找麻烦。!-- SSM 项目的打包方式必须写 war要丢进外部 Tomcat 跑 -- packagingwar/packaging properties spring.version5.3.30/spring.version mybatis.version3.5.13/mybatis.version /properties dependencies !-- Spring 核心 事务 -- dependency groupIdorg.springframework/groupId artifactIdspring-context/artifactId version${spring.version}/version /dependency dependency groupIdorg.springframework/groupId artifactIdspring-tx/artifactId version${spring.version}/version /dependency !-- SpringMVC -- dependency groupIdorg.springframework/groupId artifactIdspring-webmvc/artifactId version${spring.version}/version /dependency !-- MyBatis 与 Spring 的粘合包 -- dependency groupIdorg.mybatis/groupId artifactIdmybatis/artifactId version${mybatis.version}/version /dependency dependency groupIdorg.mybatis/groupId artifactIdmybatis-spring/artifactId version2.1.1/version /dependency !-- 数据库驱动与连接池 -- dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency dependency groupIdcom.alibaba/groupId artifactIddruid/artifactId version1.2.20/version /dependency !-- 文件上传要用的两个包 -- dependency groupIdcommons-fileupload/groupId artifactIdcommons-fileupload/artifactId version1.5/version /dependency /dependencies依赖里有两个容易漏的点commons-fileupload不引入MultipartFile 会一直是 null页面上传按钮点了没反应也不报错mysql-connector-java用 8.x 版本时驱动类名要写com.mysql.cj.jdbc.DriverURL 还得带上时区和 SSL 参数写 5.x 的com.mysql.jdbc.Driver会直接抛异常。2.4 环境配置与前后端联调约定前后端分离最耗时间的不是写代码是联调。约定好两件事能省掉大量来回一是后端统一返回体结构前端只认三个字段二是跨域和接口前缀在开发阶段就配好。# jdbc.properties数据库连接这个文件不要提交到公开仓库 jdbc.drivercom.mysql.cj.jdbc.Driver jdbc.urljdbc:mysql://localhost:3306/course_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse jdbc.usernameroot jdbc.password你的本地密码!-- spring-mvc.xml 片段静态资源放行 跨域 上传解析器 -- mvc:resources mapping/upload/** location/upload// mvc:annotation-driven mvc:message-converters !-- 统一 UTF-8避免中文乱码在联调阶段反复出现 -- bean classorg.springframework.http.converter.StringHttpMessageConverter constructor-arg valueUTF-8/ /bean /mvc:message-converters /mvc:annotation-driven bean idmultipartResolver classorg.springframework.web.multipart.commons.CommonsMultipartResolver !-- 单个文件上限 100MB教学视频够用超过就要走分片 -- property namemaxUploadSize value104857600/ property namedefaultEncoding valueUTF-8/ /bean统一返回体我一般写成{code, msg, data}三段code用 200 表示成功、401 表示未登录、500 表示业务异常。这样前端拦截器只要判断一次code ! 200后面所有页面都不用各写各的错误处理。开发阶段前端跑在另一个端口后端要么配CrossOrigin要么在 webpack/vite 里配 proxy 把/api转发到 Tomcat 的上下文路径两种方式选一种别一边配代理一边又开着跨域注解浏览器会报重复的响应头。3. 数据库设计《数据库系统原理》课程平台的表结构与关系落地3.1 从实体图到表十四类实体怎么归并论文里画了一堆实体属性图真正落到建表时第一件事是归并。学生、教师、管理员是三类人各自字段差异大——学生有学号、班级教师有工号、所属院系管理员的字段反而最少——硬塞进一张 users 表会导致大量空列也不好加唯一约束。所以拆成三张主表再加一张users表专门存登录凭证和角色标识登录时先查 users 拿到角色再按角色去对应表取详细信息。这是「一人多表」的常见做法牺牲一次关联查询换来字段干净和约束清晰。教学资源和资源类型是一对多资源类型做成字典表的一种管理员维护教师发布资源时下拉选择。公告信息是纯管理端内容带发布状态和置顶标记。论坛是自关联结构靠super_ids字段支持楼中楼回复帖子类型和帖子状态走字典。试卷、试题、作业、答题详情、错题这一组是题库链路试卷包含多道试题学生交卷后答案写进答题详情判错的题单独进错题表方便学生反复刷。字典表统一管理所有下拉项避免每加一个状态就改一次表结构。注意把所有状态值都写成硬编码的数字0 未发布、1 已发布、2 已下架是毕设里最常见的偷懒方式答辩时一句「再加一个状态要改几处代码」就能问住。用字典表改数据不改代码这一点在论文的数据库设计章节写出来是加分项。3.2 四张核心表的结构落地学生表、教学资源表、论坛表和字典表是这套系统里被引用最多的四张其余表基本是它们的组合衍化。表名关键字段说明xueshengid、xuesheng_name、xuesheng_xuehao、xuesheng_phone、xuesheng_email、banji、xuesheng_delete、create_time学生信息学号加唯一索引jiaoxueziyuanid、jiaoxueziyuan_name、jiaoxueziyuan_types、jiaoxueziyuan_photo、jiaoxueziyuan_vid、jiaoxueziyuan_file、jiaoshi_id、create_time资源标题、类型、封面、视频、附件、发布教师forumid、forum_name、yonghu_id、jiaoshi_id、users_id、forum_content、super_ids、forum_types、forum_state_types、insert_time发帖人三选一super_ids存父帖 iddictionaryid、dic_code、dic_name、code_index、index_name、super_id、beizhu一张表管所有下拉项dic_code区分业务域教学资源表里jiaoxueziyuan_vid和jiaoxueziyuan_file分开存是踩过坑之后的习惯视频要在页面上直接播放用video标签附件是课件压缩包要点下载两者在展示层行为完全不同混成一个字段前端会写得很拧巴。论坛表的三个人员外键看着冗余实际是为了让三种角色都能发帖查询时按哪个字段非空来判断发帖人身份比再加一张中间表简单得多。3.3 字典表与通用字段一套能复用的字段套路字典表的设计值得单独说因为它是整套系统里复用率最高的表。dic_code表示这个字段属于哪个业务域比如forum_types表示帖子类型、jiaoxueziyuan_types表示资源类型code_index是存进业务表的值index_name是展示给用户看的名字。查询时业务表存数字展示时关联字典表翻译成人话前端拿到的直接就是可读文本。通用字段有三组每张业务表都建议带上。create_time和update_time用datetime插入和更新时由 MyBatis 或数据库默认值填充不要用timestamp的自动更新——时区一改历史数据全乱。xxx_delete是假删标记整型0 正常 1 已删所有查询默认带AND xxx_delete 0这样管理员误删的资源还能从后台恢复也避免了物理删除后外键悬空。super_id是树形结构预留字段字典表用它做二级分类论坛用它做楼层关系一套结构两种用途写论文时可以说清楚为什么这么设计。3.4 建表 SQL、索引与假删的取舍建表语句要写进论文附录所以字段注释必须齐否则答辩老师翻到这一页会直接问「这个字段是干什么的」。-- 学生表学号唯一假删字段统一用 _delete 后缀 CREATE TABLE xuesheng ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, xuesheng_name VARCHAR(50) NOT NULL COMMENT 学生姓名, xuesheng_xuehao VARCHAR(30) NOT NULL COMMENT 学号唯一, xuesheng_phone VARCHAR(20) DEFAULT NULL COMMENT 联系电话, xuesheng_email VARCHAR(80) DEFAULT NULL COMMENT 邮箱, banji VARCHAR(50) DEFAULT NULL COMMENT 班级, xuesheng_delete INT NOT NULL DEFAULT 0 COMMENT 0正常 1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, UNIQUE KEY uk_xuehao (xuesheng_xuehao) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT学生信息表; -- 教学资源表类型和发布教师都建索引列表页两个条件都会用到 CREATE TABLE jiaoxueziyuan ( id INT AUTO_INCREMENT PRIMARY KEY COMMENT 主键, jiaoxueziyuan_name VARCHAR(120) NOT NULL COMMENT 资源标题, jiaoxueziyuan_types INT NOT NULL COMMENT 资源类型对应字典 code_index, jiaoxueziyuan_photo VARCHAR(255) DEFAULT NULL COMMENT 封面图路径, jiaoxueziyuan_vid VARCHAR(255) DEFAULT NULL COMMENT 视频路径, jiaoxueziyuan_file VARCHAR(255) DEFAULT NULL COMMENT 附件路径, jiaoxueziyuan_content TEXT COMMENT 资源简介, jiaoshi_id INT DEFAULT NULL COMMENT 发布教师 id, jiaoxueziyuan_delete INT NOT NULL DEFAULT 0 COMMENT 0正常 1已删除, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT 创建时间, KEY idx_types (jiaoxueziyuan_types), KEY idx_jiaoshi (jiaoshi_id), KEY idx_create_time (create_time) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT教学资源表;索引不是越多越好。学生表只有一个唯一索引因为学号是登录和查询的主入口教学资源表的三个索引分别对应「按类型筛选」「按教师筛选」「按时间倒序分页」三种最常出现的查询。像xuesheng_email这种基本不单独查的字段就不建索引建了只会拖慢插入。假删字段不建议建索引它的区分度太低加了也用不上真正该做的是所有查询条件里统一带上它这件事靠 Mapper 里写死别指望每个开发者都记得。4. 关键模块实现从教学资源管理到在线答题的代码链路4.1 后端三层一个资源列表接口的完整落地Controller 只做三件事接参数、兜底校验、包装返回。分页参数最容易出事前端传 0 或者负数LIMIT -10, 10直接报错页面白屏。所以页码兜底写在 Controller 里offset 换算写在 Service 里这是分工不是重复。RestController RequestMapping(/teacher/resource) public class ResourceController { Autowired private ResourceService resourceService; /** * 资源分页列表 * param page 页码从 1 开始非法值兜底为 1 * param size 每页条数上限 50防止前端传超大值拖垮数据库 * param name 标题模糊查询条件可为空 * param types 资源类型对应字典表 code_index可为空 */ GetMapping(/page) public ResultPageInfoResource page(RequestParam(defaultValue 1) Integer page, RequestParam(defaultValue 10) Integer size, String name, Integer types) { page (page null || page 1) ? 1 : page; size (size null || size 1 || size 50) ? 10 : size; return Result.ok(resourceService.page(page, size, name, types)); } }Service 层负责把页码换算成数据库认识的下标同时把总条数和当前页数据一起返回前端分页组件需要这两个值。Service public class ResourceServiceImpl implements ResourceService { Autowired private ResourceMapper resourceMapper; Override public PageInfoResource page(int page, int size, String name, Integer types) { // LIMIT 的下标从 0 开始第 1 页对应 offset 0 int offset (page - 1) * size; ListResource list resourceMapper.selectPage(name, types, offset, size); int total resourceMapper.countPage(name, types); return new PageInfo(list, total, page, size); } }对应的 XML 里条件判断用if拼count和select分开写而不是用select count(*) over()因为 MySQL 5.7 不支持窗口函数很多学校的实验环境还停在这个版本上。select idselectPage resultTypecom.demo.entity.Resource SELECT id, jiaoxueziyuan_name AS name, jiaoxueziyuan_types AS types, jiaoxueziyuan_photo AS photo, jiaoxueziyuan_vid AS vid, jiaoxueziyuan_file AS file, jiaoshi_id AS teacherId, create_time AS createTime FROM jiaoxueziyuan WHERE jiaoxueziyuan_delete 0 if testname ! null and name ! AND jiaoxueziyuan_name LIKE CONCAT(%, #{name}, %) /if if testtypes ! null AND jiaoxueziyuan_types #{types} /if ORDER BY create_time DESC LIMIT #{offset}, #{size} /select select idcountPage resultTypeint SELECT COUNT(*) FROM jiaoxueziyuan WHERE jiaoxueziyuan_delete 0 if testname ! null and name ! AND jiaoxueziyuan_name LIKE CONCAT(%, #{name}, %) /if if testtypes ! null AND jiaoxueziyuan_types #{types} /if /selectLIKE CONCAT(%, #{name}, %)这种写法一定要用#{}不用${}前者走预编译后者是字符串拼接一个单引号就能把 SQL 结构改掉。左侧带%的模糊查询用不上索引数据量上万以后会明显变慢毕设规模没问题但如果真要往上加数据可以改成前缀匹配或者引入全文索引这一点写进论文的「系统性能分析」里比空喊「性能良好」有说服力。4.2 前端列表页Vue 组件加 axios 封装前端最省事的做法是把 axios 封一层把 token 携带、错误提示、返回体拆包都收敛到一个文件里页面里只关心业务数据。// request.js统一请求实例所有页面都从这里走 import axios from axios const request axios.create({ baseURL: /api, // 开发环境由代理转发到 Tomcat生产环境与后端同源 timeout: 8000 }) request.interceptors.request.use(config { // 登录后把后端下发的 token 存进 localStorage每次请求带上 const token localStorage.getItem(token) if (token) config.headers[Authorization] token return config }) request.interceptors.response.use(res { // 后端统一返回 {code, msg, data}这里直接拆出 data页面不用层层 .data.data if (res.data.code 200) return res.data.data if (res.data.code 401) { localStorage.removeItem(token) window.location.href /login return Promise.reject(未登录) } return Promise.reject(res.data.msg) }, err Promise.reject(err)) export default request组件里就只剩数据和翻页两件事created里拉一次搜索时把页码重置回 1 再拉一次。export default { data() { return { list: [], total: 0, page: 1, size: 10, query: { name: , types: null }, typeOptions: [] // 资源类型下拉来自字典表接口 } }, created() { this.loadTypes() this.loadList() }, methods: { loadList() { this.$request.get(/teacher/resource/page, { params: { page: this.page, size: this.size, ...this.query } }).then(data { this.list data.list this.total data.total }) }, loadTypes() { // 字典表统一接口dic_code 决定拿哪一组下拉项 this.$request.get(/common/dict, { params: { code: jiaoxueziyuan_types } }) .then(list { this.typeOptions list }) }, handleSearch() { this.page 1 // 搜索必须回到第一页否则会查到空结果 this.loadList() } } }handleSearch里那行重置页码是血泪经验。不重置的时候用户翻到第 5 页再搜一个只有 3 条结果的词页面直接空白用户以为系统坏了其实是查的第 5 页没数据。4.3 登录态与拦截器session 和 token 的取舍毕设里两种做法都能用选一种说清楚理由就行。session 的好处是后端零改造request.getSession().setAttribute()存完就能取缺点是分布式部署要共享 session前端跨域还得带 cookie。token 的好处是无状态、前后端分离更顺缺点是要自己处理过期和续期。这套系统单机部署我一般直接用 session拦截器写起来短答辩时也解释得清楚。public class LoginInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 从请求头拿 token没有就退回 session两种方式兼容 String token request.getHeader(Authorization); Object user null; if (token ! null !token.isEmpty()) { user TokenUtil.parse(token); // 自定义工具解析失败返回 null } if (user null) { HttpSession session request.getSession(false); user session null ? null : session.getAttribute(loginUser); } if (user null) { response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\msg\:\未登录或登录已过期\}); return false; // 返回 false 后不再进 Controller } return true; } }拦截器在spring-mvc.xml里注册/**全拦再放行登录、注册、字典查询、静态资源这几类不需要身份的路径。注册顺序有讲究登录拦截器要在跨域配置之后否则预检请求 OPTIONS 会被当成未登录直接拦掉前端会看到一个莫名其妙的 401排查半天以为是 token 问题。4.4 资源上传下载与在线答题的边界处理文件上传有两个必须做对的点文件名不能直接落盘类型要白名单校验。原始文件名里带中文、空格、甚至../直接拼路径就是安全漏洞。PostMapping(/upload) public ResultString upload(RequestParam(file) MultipartFile file, HttpServletRequest request) throws IOException { String original file.getOriginalFilename(); if (original null || !original.contains(.)) return Result.fail(文件名非法); // 取扩展名做白名单教学场景够用不要用黑名单 String ext original.substring(original.lastIndexOf(.) 1).toLowerCase(); ListString allow Arrays.asList(pdf, ppt, pptx, doc, docx, zip, mp4); if (!allow.contains(ext)) return Result.fail(不支持的文件类型 ext); // 用 UUID 重命名避免同名覆盖和路径穿越 String fileName UUID.randomUUID().toString().replace(-, ) . ext; String dir request.getServletContext().getRealPath(/upload/resource); File target new File(dir); if (!target.exists()) target.mkdirs(); file.transferTo(new File(target, fileName)); return Result.ok(/upload/resource/ fileName); }下载接口的关键在响应头Content-Disposition里的中文文件名必须用URLEncoder.encode(name, UTF-8)编码否则浏览器拿到的是乱码文件名用户下载下来一堆问号。在线答题这条链路稍微长一些学生进试卷页拉到试题列表答完提交时前端把所有答案打包成一个数组发过来后端在一个事务里先写答题详情再把判错的题写进错题表。事务边界一定要卡在 Service 的方法上两件事要么都成功要么都回滚不能出现答题记录写了、错题没进库的情况。5. 避坑与常见问题排查5.1 环境与配置类现象一项目在 IDE 里跑得好好的打成 war 丢进 Tomcat 就 404所有接口都访问不到。原因基本是上下文路径对不上。IDE 里默认上下文是/部署到 Tomcat 后变成/war包名前端请求的/api/xxx自然打不中。还有一种可能是 SpringMVC 的DispatcherServlet映射写成了/api/*但RequestMapping上又带了一层/api路径重复拼接。解决办法是统一约定war 包名和前端 baseURL 的前缀保持一致DispatcherServlet映射用/拦截全部接口路径只在一处写前缀。部署后先在浏览器直接敲一个最简单的 GET 接口地址确认能返回 JSON再去调前端这个顺序能省掉一半排查时间。现象二中文全变问号或者乱码前端页面、接口返回、数据库三处都可能出问题。原因是编码链路有四段数据库字符集、连接串、响应编码、页面编码。任何一段没设成 UTF-8中文就会在那一环断掉。最常见的是数据库建表时用了默认的 latin1或者连接串里漏了characterEncodingutf8。解决时按链路依次确认数据库和表都用utf8mb4连接串带characterEncodingutf8web.xml里配CharacterEncodingFilter并设置forceEncodingtrue返回 JSON 的StringHttpMessageConverter指定 UTF-8。四段都对上了问题自然消失不要靠new String(bytes, UTF-8)硬转那是在掩盖问题。现象三上传功能点了没反应后台断点也进不去MultipartFile 始终是 null。原因是multipartResolver的 bean id 写错或者根本没配。Spring 靠固定的 bean 名字multipartResolver来查找解析器写成multipartResolverBean就不认。另一种情况是表单的enctype没设成multipart/form-data文件根本不会随请求发出去。解决方法是检查spring-mvc.xml里 bean 的 id 必须精确为multipartResolver同时前端上传组件的enctype和 axios 的FormData用法都要对。用 axios 时把文件 append 进FormData再发手动设Content-Type反而会让浏览器丢掉 boundary这一点上不要自作聪明。5.2 数据与业务逻辑类现象四列表页数据重复出现同一条或者分页翻到第二页出现第一页的内容。原因是排序字段不唯一。ORDER BY create_time DESC时如果多条记录的create_time完全相同批量插入时非常常见MySQL 不保证它们的相对顺序两次查询的顺序可能不一样分页就会串。解决方法是排序字段一定要带上主键兜底ORDER BY create_time DESC, id DESC。另外确认count查询和select查询的WHERE条件完全一致少一个条件就会出现「总数 30 条翻到第 3 页没数据」的情况。现象五管理员删除了资源页面上看不到了但统计报表里总数没变或者再次新增同名资源时提示重复。原因是假删只改了xxx_delete 1没有在所有查询里加过滤条件。有的 Mapper 写了有的忘了数据就出现「一半可见一半不可见」的分裂状态。解决方法是把AND xxx_delete 0作为所有业务查询的默认条件写进 XML不依赖开发者记忆唯一索引的建立也要考虑假删要么用联合唯一索引(name, delete)要么在应用层做重名校验。物理删除虽然省事但删掉的数据恢复不回来一旦误操作答辩前一天晚上就得重录演示数据。提示题库相关的表关系最长改一处字段容易漏掉下游。每次动试卷、试题、答题详情这三张表都把「出卷 → 答题 → 判分 → 进错题本 → 查看错题」这条链路从头点一遍比看代码快。6. 验收与答辩怎么自测、怎么讲、怎么答6.1 验收清单一张能自测也能给答辩组看的表系统测试那一章不要写成「点击按钮功能正常」这种口水话。列一张能复现的用例表每行写清楚操作路径和预期结果自己按表跑一遍就是验收答辩时把表放上去老师一眼能看出你测过什么。编号功能点操作预期结果T01学生注册填学号、姓名、密码学号用已存在的值提示学号已存在不入库T02登录拦截未登录直接访问/teacher/resource/page返回 401跳转登录页T03资源类型筛选选择某一类型后查询只返回该类型且未删除的记录T04分页边界总 23 条每页 10 条翻到第 3 页返回 3 条页码正确T05文件上传上传 200MB 文件提示超出大小限制不落盘T06文件类型上传.exe文件提示不支持的类型T07交卷判分答对 3 题、答错 2 题后交卷得分正确2 条进错题表T08假删验证删除一条资源后重新查询列表不显示统计数量同步减少表里 T04、T05、T07 这三条是重点它们分别对应分页边界、上传边界和事务边界也是最容易在答辩现场被要求「现场演示一下」的地方。演示前把数据准备好造 23 条资源正好卡在第三页准备一个超限文件放在桌面预先把一套答错两题的试卷留着现场点两下就能出效果。6.2 答辩陈述顺序与三个必被问到的问题陈述时间通常只有五到八分钟顺序按「问题—方案—证据」走不要按论文目录念。开口先说清楚这个平台解决什么把课程资料的发布、下载、作业与答题从群聊里搬到有权限、有记录的系统里。接着讲技术方案为什么这么选重点讲 SSM 三层各自承担什么、Vue 解决了什么问题别背框架介绍。最后落到证据数据库十几张表的关系图、关键模块的代码片段、测试用例表的执行结果。三个问题几乎必问。第一个是「为什么用 SSM 不用 Spring Boot」答法不要贬低任何一方讲清楚场景这套系统需要外部 Tomcat 部署、配置文件方式更直观地展示分层SSM 的 XML 配置在论文里能画出清晰的结构图。第二个是「数据量大了怎么办」答索引设计、分页优化、模糊查询的局限承认左侧%用不上索引给出可以改成前缀匹配或全文索引的方向。第三个是「权限怎么保证」答拦截器 URL 前缀 后端二次校验说明前端隐藏菜单只是体验优化不是安全措施。每次带学生改这类系统我最后都会强制走一遍「删库重来」把数据库清空只跑一遍建表脚本和初始化字典的 SQL从注册开始把三类角色的主流程点一遍。能走通说明脚本齐、依赖全、没有哪张表是手工改出来的走不通问题当场就暴露总比答辩当天当机强。这套东西看着简单真正能一次跑通的不到一半剩下的都是靠这半小时补回来的。希望帮到你。完整源码、论文文档和建表脚本按标题检索即可拿到压缩包里已经按章节分好了目录建议先看第 3 章的建表 SQL 和第 4 章的模块代码再回头把你自己的业务字段替换进去比从头写一遍省力得多。本文还有配套的精品资源点击获取