简介:这份文档面向计算机专业毕业设计阶段的学生,围绕《数据库系统原理》课程平台的设计与实现展开,适合需要参考SSM+Vue技术栈、完整论文结构与课程管理类选题的读者。内容从课题背景、研究现状与技术选型切入,依次覆盖可行性分析、角色与功能需求、业务流程、ER图、数据字典和数据流图,并延伸到登录注册、课程浏览、在线答疑、资源下载等模块的详细设计与系统测试,能够为开题、系统实现和论文撰写提供较完整的对照样本。资源包共1个doc文件,约2.9MB,以论文正文形式组织,目录层级清晰,便于按章节查阅和提取设计思路。已有70人学习,适合希望了解SSM框架结合Vue.js与MySQL完成B/S课程平台的毕业生参考。
1. 一份看起来最“稳”的选题,为什么反而最容易在答辩现场被问住
在计算机毕业设计选题里,《数据库系统原理》课程平台属于典型的“保底题”——不用算法、不碰硬件、界面也不复杂。我第一次帮人看这类毕业论文时也这么想,直到翻到目录:十几张表、三类角色、教学资源和资源类型还是两套体系,老师只问了三个问题就把人问住了。这套基于 SSM 框架、Vue 和 MySQL 的课程平台方案,讲的不是怎么把增删改查拼起来,而是需求怎么切、表怎么连、权限怎么拦。它适合正在写毕业设计、又不想交一份“能跑但说不清”的系统的同学,也适合需要一套可交付在线教学平台的从业者当参考底稿。
2. 需求分析与角色建模:三类用户、七个模块与四条可行性论证
课程平台跟图书管理、商品管理这类题目最大的差别在于“人是分层的”。同一个登录入口,管理员看到的是全站数据,教师看到的是自己发布的教学资源和自己出的试卷,学生只能看到已发布的资源和属于自己那份答题记录。需求分析如果只写“管理员增删改查、用户浏览查看”,写代码时权限判断就会散落在每个 Controller 里,改一处漏三处。通常的做法是:在写下第一张表之前,先把角色、模块、数据归属这三张清单列清楚,后面所有设计都在这个骨架上长。
2.1 角色边界:管理员、教师、学生各管一段
系统里存在三类角色:管理员(对应 users 表)、教师(jiaoshi 表)、学生(yonghu 表)。三个角色共用一套登录入口,但登录成功后进入的后台不同、能调的接口也不同。
管理员负责“秩序”:管理学生与教师信息、维护教学资源与资源类型、发布公告、管理论坛帖子状态、维护字典表。教师负责“内容”:发布教学资源(标题、类型、封面图、视频)、布置作业并批阅、维护试卷与试题、查看本班答题记录。学生负责“消费与反馈”:浏览公告、按类型检索教学资源、下载资源、论坛发帖回帖、提交作业、参加考试、查看错题和答题详情、维护个人信息。
把这张边界表列出来之后,很多事情就顺了:教师不能删别人发的资源,学生不能看别人的答题记录,管理员可以删帖但不能替学生答题。这些规则后面会直接落到拦截器和 Service 层的判断上。
| 模块 | 管理员 | 教师 | 学生 |
|---|---|---|---|
| 学生信息 | 增删改查、重置密码 | 只读本人所带班级 | 仅本人 |
| 教师信息 | 增删改查 | 仅本人 | 无 |
| 教学资源 | 全站查看、下架删除 | 发布、修改自己发布的 | 浏览、下载 |
| 资源类型 | 维护字典项 | 只能选择 | 只能选择 |
| 公告信息 | 发布、删除 | 浏览 | 浏览 |
| 论坛 | 删帖、置顶 | 发帖、回帖 | 发帖、回帖 |
| 试卷与答题 | 查看全部成绩 | 组卷、阅卷 | 作答、看错题 |
| 作业 | 查看全部提交 | 布置、批阅 | 提交 |
2.2 功能模块拆分:七个模块与支撑表
按系统结构图的划分,前台业务落在七个模块上:学生信息管理、教师信息管理、教学资源管理、资源类型管理、公告信息管理、论坛信息管理,以及围绕试卷、试题、答题详情、考试记录、错题、作业组成的考核模块。前六个模块都是“主表 + 字典”的常见结构,考核模块是唯一带链路的一串:组卷 → 作答 → 落答题详情 → 生成错题 → 汇总考试记录。
这里有个容易被忽略的点:资源类型和字典表其实是同一套东西。教学资源的类型下拉框、论坛的帖子类型、公告的分类,都从字典表按 dic_code 取,而不是每建一个下拉框就新建一张表。数据库设计阶段把这一条定死,后面加一个“资源难度”只需要插三行字典数据,不用改表结构、不用改前端代码。
考核模块则要单独对待。试卷和试题是一对多,试题和答题详情是一对多,学生对试卷的每一次作答生成一条考试记录。这套关系在需求阶段就要说清楚,否则实现时很容易把“试题”和“答题详情”合成一张表,等到要统计错题率时才发现没有地方存学生的答案。
2.3 可行性分析与性能指标:别写成四段正确的废话
可行性分析是毕业设计里最容易写成套话的一节。技术可行性不该只写“学过 Java”,而要落到具体依赖上:SSM 三层结构是课程内讲过的东西,Vue 的组件化和双向绑定能省掉大量 DOM 操作,MySQL 在本地跑单机足够支撑一个班级规模的数据量。经济可行性也不是“网上有免费资源”,而是开发工具、数据库、运行环境全部免费,只有一台能跑 Tomcat 的电脑的成本。
操作可行性要给出具体的交互约束:所有列表页支持关键词检索和分页,表单必填项前端即时提示,删除操作二次确认。这些约束是可以在验收时逐条演示的,比“界面简洁、操作简单”这种描述有用得多。
性能指标我给学生的建议是量化到可测试的程度:列表默认每页 10 至 20 条,首屏加载控制在 2 秒内,单表数据量按万级设计,并发按一个教学班 50 人同时在线估算。这些数字不需要多漂亮,但它们决定了后面要不要加分页插件、要不要给查询字段建索引。
2.4 注册与登录流程:边界条件比流程本身重要
注册流程的图很好画:填写信息 → 判断账号是否已存在 → 校验信息合法性 → 入库 → 提示成功。真正要写进设计里的是分支处理:账号重复给什么提示,手机号格式不对在前后端哪一层拦,密码怎么存。
// 注册校验:先查重,再校验字段,最后落库 public String register(Student stu) { // 1. 账号查重,未删除记录里不允许重复 int exist = studentMapper.countByAccount(stu.getYonghuZhanghao()); if (exist > 0) { return "账号已存在"; } // 2. 必填与格式校验,避免空串进库变成脏数据 if (!StringUtils.hasText(stu.getYonghuName()) || stu.getYonghuPhone() == null || stu.getYonghuPhone().length() != 11) { return "姓名或手机号不合法"; } // 3. 密码加盐后入库,数据库里不出现明文 stu.setYonghuPassword(DigestUtils.md5DigestAsHex( (stu.getYonghuPassword() + stu.getYonghuZhanghao()).getBytes())); stu.setCreateTime(new Date()); studentMapper.insert(stu); return "注册成功"; }这段代码里三个顺序不能换:先查重再校验再入库。如果先入库再查重,数据库里就会留下脏数据;如果密码不加盐,两个用相同密码的学生在库里是同一个哈希值,答辩时被问到会很难解释。学生表的字段按同一套命名习惯扩展为 yonghu_zhanghao、yonghu_name、yonghu_phone、yonghu_password,跟教师表、教学资源表的拼音前缀保持一致,这也是这类系统常见的做法。
登录流程同理,除了“验证通过进主页”这个主分支,还要处理密码错误三次锁定、未登录访问受限页面跳回登录页、登录后按角色跳不同后台三个分支。登录态用 Session 存,拦截器统一判断,这部分在第 4 章落代码。
3. 总体架构与技术选型:Vue 前后端分离 + SSM 三层 + MySQL 的职责划分
技术选型这一章最忌讳写成名词解释。Vue 是什么、Spring 有几个模块,这些写在论文第二章没问题,但方案里真正要交代的是:为什么这套组合能扛住一个课程平台,以及每一层各自负责什么、不负责什么。
3.1 为什么是 B/S 结构加前后端分离
B/S 结构的核心优势是把系统集中部署在服务器上,客户端只需要一个浏览器。课程平台的使用场景正好对上:学生在机房、宿舍、手机浏览器上都要能打开,不可能要求每台机器装客户端;教师临时改一份教学资源,也不该重新分发安装包。
在这个基础上做前后端分离,收益主要在开发效率上。前端 Vue 工程独立跑一个开发服务器,改完样式刷新即可看到效果,不用每次重启 Tomcat;后端只负责返回 JSON,接口用 Postman 单独测,两边的调试互不阻塞。打包时前端产物丢进后端的静态目录,部署仍然是一个 war 包,对答辩演示没有任何额外负担。
提示:前后端分离之后,跨域是绕不开的第一道坎,开发阶段用代理解决,生产阶段同源部署,具体配置放在第 5 章的排查部分。
3.2 SSM 三层的职责与包结构
Spring 管对象生命周期和事务,SpringMVC 管请求分发,MyBatis 管 SQL 映射。三层各管一段,最怕的是职责越界——Controller 里写业务判断、Service 里拼 SQL、Mapper 里做权限校验,最后谁都不敢改。
# 后端工程目录:按职责分层,业务规则只允许写在 service 层 src/main/java/com/example/course/ ├── controller/ # 接收参数、做基础校验、返回统一结果体 ├── service/ # 业务规则:谁能改谁的数据、答题怎么判分 │ └── impl/ ├── mapper/ # 只有 SQL 映射,不写 if-else 业务判断 ├── entity/ # 与数据库表一一对应 ├── interceptor/ # 登录态与角色校验 └── util/ # 统一返回体、MD5、文件命名工具分层定好之后有个直接好处:所有“这个学生能不能删这条资源”的判断都集中在 Service,拦截器只负责“有没有登录、角色对不对”,Mapper 只负责“按条件取数据”。论文里画系统结构图时,这三层也正好对应三个方框,讲起来清楚。
3.3 Vue 前端的请求封装与路由守卫
前端最容易翻车的地方不是页面写得好不好看,而是登录态在各处重复判断。统一封装一个请求实例,把 token 或 Session 带上,把 401 的处理收在一处,后面每加一个页面都省事。
// request.js —— 统一注入登录态,统一处理登录失效 import axios from 'axios' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000, withCredentials: true // 与后端 Session 配合,漏了它会一直提示未登录 }) request.interceptors.response.use( res => res.data, err => { // 后端拦截器返回 401 时统一回登录页,不在每个页面写一遍 if (err.response && err.response.status === 401) { router.push('/login') } return Promise.reject(err) } ) export default request路由守卫负责另一件事:进入教师后台前先判断当前角色,学生手动敲地址也进不去。这一步只是体验层的拦截,真正的权限判断必须在后端拦截器再做一次,前端拦截器挡不住直接调接口的人。
3.4 统一返回体与全局异常处理
接口返回格式不统一是后期联调最耗时的问题。有的接口返回对象,有的返回字符串,有的失败时返回 500,前端每个页面都要写一套判断。约定一个返回体,所有 Controller 只返回它,异常统一在全局处理器里转成它。
// Result.java —— 前端只认 code == 0 这一种成功 public class Result<T> { private int code; // 0 成功,非 0 失败 private String msg; // 失败原因,直接给前端提示用 private T data; // 业务数据 public static <T> Result<T> ok(T data) { Result<T> r = new Result<>(); r.code = 0; r.msg = "success"; r.data = data; return r; } public static <T> Result<T> fail(String msg) { Result<T> r = new Result<>(); r.code = 1; r.msg = msg; return r; } }配合一个带 @RestControllerAdvice 的全局异常处理器,把空指针、参数类型不匹配、自定义的业务异常分别映射成友好提示。答辩演示时故意输错一个参数,页面弹出“参数格式不正确”而不是一串堆栈,这个细节很加分。
3.5 MySQL 连接与 MyBatis 映射配置
数据库连接串里有三个参数最容易被复制粘贴时漏掉,漏了之后表现各不相同,排查起来很费时间。
# jdbc.properties —— 三个参数分别决定中文、时区、连接方式 jdbc.driver=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://127.0.0.1:3306/course_platform?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false jdbc.username=root jdbc.password=123456characterEncoding 决定中文能不能正确写入,serverTimezone 决定 create_time 会不会差 8 小时,useSSL 在本地环境关掉可以避免连接被拒绝。MyBatis 这边把 mapper 扫描路径和实体别名包配好,写 SQL 时就能直接用类名当 resultType,少写一长串全限定名。
4. 数据库设计与核心模块实现:十几张表、登录拦截、资源上传与试卷链路
数据库设计是整个方案里最容易被低估的部分。表少不等于简单,课程平台的表不多但关系密:教师发布资源,资源属于某个类型,类型来自字典;学生作答生成答题详情,答错的题进错题表,一次作答汇总成一条考试记录。这些关系在图上画错一根线,代码里就要多写一堆补丁。
4.1 ER 建模:先定实体,再定关系
按需求梳理出来的实体有:学生、教师、管理员用户、教学资源、资源类型(字典项)、公告、论坛帖子、试卷、试题、考试记录、答题详情、错题、作业、回答。每个实体列出属性,再确定关系:
- 教师与教学资源是一对多,一个教师可以发布多条资源;
- 资源类型与教学资源是一对多,类型来自字典表;
- 试卷与试题是一对多,一份试卷包含多道题;
- 学生与考试记录是一对多,一次作答一条记录;
- 试题与答题详情是一对多,一道题在每个学生的答卷里都有一条详情;
- 论坛帖子自身是一对多,用 super_ids 指向父帖实现回复层级。
画 ER 图时用矩形表示实体、菱形表示关系、椭圆表示属性,Visio 和常见的在线绘图工具都能画。真正决定质量的不是图好不好看,而是每个菱形两边有没有写清楚 1:N 还是 N:M。凡是出现 N:M 的地方,中间一定要落一张关联表,否则后面查数据只能靠逗号分隔的字符串,那是实打实的返工。
4.2 核心表结构:四张有代表性的建表语句
教师表、教学资源表、论坛表、字典表基本覆盖了这个系统的四种典型结构:普通主表、带外键类型的主表、自关联表、纯配置表。
-- 教师表:软删除字段单独留一列,物理删除留给管理员 CREATE TABLE jiaoshi ( id INT PRIMARY KEY AUTO_INCREMENT, jiaoshi_name VARCHAR(50) NOT NULL COMMENT '教师姓名', jiaoshi_photo VARCHAR(255) DEFAULT NULL COMMENT '头像路径', jiaoshi_phone VARCHAR(20) DEFAULT NULL COMMENT '联系方式', jiaoshi_email VARCHAR(100) DEFAULT NULL COMMENT '邮箱', jiaoshi_delete INT DEFAULT 0 COMMENT '0 正常,1 已删除', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_name (jiaoshi_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教师信息表'; -- 教学资源表:类型字段存字典项的 code_index,不直接存中文 CREATE TABLE jiaoxueziyuan ( id INT PRIMARY KEY AUTO_INCREMENT, jiaoxueziyuan_name VARCHAR(100) 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 '视频路径', jiaoshi_id INT DEFAULT NULL COMMENT '发布教师', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_types (jiaoxueziyuan_types), KEY idx_teacher (jiaoshi_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='教学资源表';类型字段存数字而不存中文,是为了改名字的时候不用批量更新业务表。这条在字典表那一节还会展开。
-- 论坛表:super_ids 指向父帖,实现“主帖 + 回复”的两层结构 CREATE TABLE forum ( id INT PRIMARY KEY AUTO_INCREMENT, forum_name VARCHAR(200) NOT NULL COMMENT '帖子标题', yonghu_id INT DEFAULT NULL COMMENT '学生发帖人', jiaoshi_id INT DEFAULT NULL COMMENT '教师发帖人', users_id INT DEFAULT NULL COMMENT '管理员发帖人', forum_content TEXT COMMENT '发布内容', super_ids INT DEFAULT 0 COMMENT '父帖 id,0 表示主帖', forum_types INT DEFAULT NULL COMMENT '帖子类型,对应字典', forum_state_types INT DEFAULT 1 COMMENT '1 正常,2 已关闭', insert_time DATETIME DEFAULT CURRENT_TIMESTAMP, update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, KEY idx_super (super_ids) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='论坛帖子表'; -- 字典表:一张表装下全站所有下拉框 CREATE TABLE dictionary ( id INT PRIMARY KEY AUTO_INCREMENT, dic_code VARCHAR(50) NOT NULL COMMENT '字典编码,如 ziyuan_type', dic_name VARCHAR(50) NOT NULL COMMENT '字典名称', code_index INT NOT NULL COMMENT '编码值,业务表存这个', index_name VARCHAR(50) NOT NULL COMMENT '编码显示名', super_id INT DEFAULT 0 COMMENT '父字段 id', beizhu VARCHAR(255) DEFAULT NULL COMMENT '备注', create_time DATETIME DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_code_index (dic_code, code_index) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='字典表';四张表里有两个设计决策值得写进论文的“设计取舍”一节:一是所有业务表都保留 create_time,出问题时可以先按时间排序看最后写入的数据;二是所有“类型”字段都不建外键约束,而是在应用层校验,因为毕设环境里表和字典项经常在演示前临时调整,物理外键会让调整变得很麻烦。这个取舍不是最优解,但是在项目周期和演示稳定性之间比较平衡的做法。
4.3 字典表与软删除:两个小设计省掉大量改代码
字典表的用法很简单:前端页面初始化时按 dic_code 拉一次列表,渲染成下拉框;提交时传 code_index,列表回显时用 code_index 换 index_name。它的价值在后期体现——老师临时说“资源类型再加一个‘实验指导’”,只需要插一行字典数据,业务表、Mapper、前端页面都不用动。
软删除也是同样的思路。教师表用 jiaoshi_delete 标记,删除操作实际上是 update 语句。这样做的好处是历史数据不会突然消失,比如某位教师离职后,他发布的资源仍然能显示发布人。代价是每一个查询都必须带上 delete = 0,漏一个就会出现“删掉的老师又出现在列表里”的玄学现象。我的习惯是在 Mapper 里把常用查询写成固定片段,再在 Service 层统一调用,不给漏条件的机会。
注意:软删除字段一旦引入,就把它当成所有查询的默认条件,别指望后面再补。
4.4 登录拦截器:把权限判断从业务代码里抽出来
登录态用 Session 存,拦截器在进入 Controller 之前判断。角色校验用自定义注解标在方法上,比在每个方法里写 if 清爽得多。
// LoginInterceptor.java —— 先判登录,再判角色 public class LoginInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求,否则跨域时所有接口都会 401 if ("OPTIONS".equalsIgnoreCase(request.getMethod())) { return true; } HttpSession session = request.getSession(); Object user = session.getAttribute("loginUser"); if (user == null) { response.setStatus(401); // 前端拦截器据此跳登录页 return false; } // 方法上标了 @RequireRole 的,再校验一次角色 if (handler instanceof HandlerMethod) { RequireRole role = ((HandlerMethod) handler) .getMethodAnnotation(RequireRole.class); if (role != null && !role.value().equals(session.getAttribute("role"))) { response.setStatus(403); return false; } } return true; } }这段代码的关键不在拦截逻辑,而在两个容易漏的分支:OPTIONS 预检请求必须放行,否则前端跨域调接口时全部报 401,而这个现象看起来像是登录态没保存;返回 401 和 403 要区分开,401 是没登录,403 是登录了但角色不对,前端对这两种情况的处理不一样。
4.5 教学资源上传:文件名、路径与大小三件事
资源上传看着简单,实际上三个细节决定它能不能在答辩现场跑通:文件名不能直接用原始名(中文和重名都会出问题)、存储路径不能写死在代码里、文件大小要有限制。
// 教学资源上传:UUID 重命名,按日期分目录,避免单目录文件过多 public String upload(MultipartFile file, HttpServletRequest request) throws IOException { if (file == null || file.isEmpty()) { throw new RuntimeException("上传文件为空"); } // 1. 取扩展名,只允许图片和视频 String original = file.getOriginalFilename(); String ext = original.substring(original.lastIndexOf(".")).toLowerCase(); if (!Arrays.asList(".jpg", ".png", ".mp4").contains(ext)) { throw new RuntimeException("不支持的文件类型"); } // 2. 按日期分目录,UUID 重命名,避免同名覆盖 String datePath = new SimpleDateFormat("yyyyMMdd").format(new Date()); String fileName = UUID.randomUUID().toString().replace("-", "") + ext; String dir = request.getServletContext().getRealPath("/upload/" + datePath); File target = new File(dir, fileName); if (!target.getParentFile().exists()) { target.getParentFile().mkdirs(); } file.transferTo(target); // 3. 入库只存相对路径,换服务器时不用改数据 return "/upload/" + datePath + "/" + fileName; }返回值存进数据库的是相对路径,不是绝对路径。这一点很多人吃过亏:本地开发时路径是 D 盘某个目录,换一台机器演示就全变成裂图。相对路径加 Tomcat 的静态资源映射,换机器只要把文件拷过去就行。
4.6 试卷链路:一次提交要落三张表
考核模块是整个系统里唯一有事务要求的链路。学生提交答卷时,要同时写答题详情、更新错题表、生成考试记录,三步必须在一个事务里。
// 提交答卷:判分、写详情、记错题、汇总成绩,四步一个事务 @Transactional public void submitPaper(Integer studentId, Integer paperId, List<Answer> answers) { int rightCount = 0; for (Answer a : answers) { // 1. 取出试题标准答案比对 Question q = questionMapper.selectById(a.getQuestionId()); boolean right = q.getAnswer().equals(a.getContent()); if (right) rightCount++; // 2. 每条作答都落一条答题详情,不管对错 a.setIsRight(right ? 1 : 0); a.setStudentId(studentId); answerMapper.insert(a); // 3. 答错的题写进错题表,供学生回看 if (!right) { WrongBook w = new WrongBook(); w.setStudentId(studentId); w.setQuestionId(q.getId()); w.setCreateTime(new Date()); wrongBookMapper.insert(w); } } // 4. 汇总一条考试记录,分数按百分制折算 ExamRecord record = new ExamRecord(); record.setStudentId(studentId); record.setPaperId(paperId); record.setScore(rightCount * 100 / answers.size()); examRecordMapper.insert(record); }为什么不把错题直接算出来、而要单独建表?因为错题本要支持“反复查看、标记已掌握”这类操作,如果每次都从答题详情里反查,SQL 会越来越长。多一张表换来查询的简单,这个交换在项目规模不大时很划算。事务注解也不能省,否则中途异常会出现“详情写了、成绩没写”的半截数据,演示时最难解释。
5. 常见问题与排查:五个最容易翻车的点
这套技术栈本身不算复杂,但组合起来有几个地方几乎每个项目都会踩一遍。下面五条按“现象 → 原因 → 解决”写,遇到时可以直接对着查。
5.1 中文变问号:连接串和请求编码各管一半
现象:公告标题在页面上显示正常,存进数据库再查出来变成一串问号;或者反过来,数据库里正常,前端显示乱码。
原因:两个环节都可能出问题。一是数据库连接串缺少 characterEncoding=utf8,JDBC 用平台默认编码传数据;二是建表时用了 latin1 字符集,或者 SpringMVC 的 CharacterEncodingFilter 没有配置。
解决:连接串加 useUnicode=true&characterEncoding=utf8,建表统一用 utf8mb4(避免 emoji 和特殊符号写不进去),web.xml 里配置 CharacterEncodingFilter 并设 forceEncoding=true。三处都改完还有问题,就去看数据库本身的默认字符集,用 show variables like 'character%' 确认。
5.2 登录成功但下一个请求就提示未登录
现象:登录接口返回成功,前端跳转到首页,紧接着列表接口返回 401,刷新一下又好了。
原因:跨域请求默认不携带 Cookie,后端的 Session 拿不到 JSESSIONID,每次都当成新会话。开发阶段前端跑在 8080,后端跑在 8081,正好命中这个问题。
解决:前端 axios 配置 withCredentials: true,后端 CORS 配置里 allowCredentials(true) 且 allowOrigin 不能写成星号,必须写具体来源。同时拦截器要放行 OPTIONS 预检请求,否则预检本身就被拦掉,浏览器直接不发送真正的请求。
5.3 视频能传上去却播不了
现象:上传进度条走完,数据库里也有记录,但页面上视频区域一片空白,或者控制台报 404。
原因:文件存到了 Tomcat 临时目录,重启后目录被清空;或者存的是绝对路径,换机器后路径不存在;再或者上传大小超过了 Tomcat 的默认限制(大约 2MB),只是前端没提示。
解决:存储路径通过 getServletContext().getRealPath 统一取,入库只存相对路径;Tomcat 配置里调大 maxSwallowSize 和 maxPostSize;如果文件放在项目外部目录,在 server.xml 里加 Context 做虚拟路径映射,把 /upload 指向真实目录。
5.4 删掉的老师又出现在列表里
现象:管理员删除一位教师,提示成功,刷新列表这位老师还在;或者学生列表里出现了重复记录。
原因:软删除只改了 delete 字段,查询语句忘了带条件。这种情况在拼接动态 SQL 时最常见——某个查询条件不传时就跳过了 where 片段,连带把 delete = 0 也跳过了。
解决:把所有涉及软删除表的查询统一写在 Mapper 的 片段里,用 引用,而不是每个语句手写一遍。另外在数据库层面加一个视图过滤已删除数据也可以,但在毕设规模下,靠代码规范更现实。
5.5 模糊查询能用但有注入风险
现象:搜索框输入正常关键词能查到数据,输入带引号的字符串时接口报 SQL 语法错误,或者返回了不该返回的数据。
原因:MyBatis 的 ${} 是字符串拼接,#{} 是参数占位。模糊查询写成 like '%${keyword}%' 时,拼接进去的内容会被当作 SQL 执行。
解决:统一用 like concat('%', #{keyword}, '%'),既安全又不影响索引使用。凡是需要动态表名、动态排序字段的地方,改成在代码里用白名单映射,不要直接拼前端传来的字符串。
6. 验收与答辩:把十几张表和一条链路讲成闭环
验收不是“能点开就算完”。我一般按四组证据准备,每一组都能当场演示:
| 验收项 | 判据 | 演示方式 |
|---|---|---|
| 权限隔离 | 学生账号直接访问管理接口返回 401/403 | 三个账号各登一次,手动改地址栏路径 |
| 数据完整性 | 删除教师后其名下资源仍可查,发布人显示为已注销 | 删一条再看列表 |
| 文件链路 | 上传的视频换一台电脑能播放 | 换机器打开同一页面 |
| 答卷链路 | 提交 10 题,答题详情 10 条、错题条数等于错题数、考试记录 1 条 | 提交后逐表核对 |
| 分页检索 | 造 100 条数据,翻到最后一页不报错 | 用脚本批量插入测试数据 |
答辩讲的时候,不要按“管理员模块、教师模块、学生模块”平铺直叙,那样十几张表讲完,评委也记不住。按一条链路讲:学生登录 → 看公告 → 按类型找资源 → 下载 → 参加考试 → 回看错题 → 教师阅卷。一条线走完,学生表、教师表、资源表、字典表、试卷、试题、答题详情、错题、考试记录自然全串上了,剩下的表和这条主线的关系一句话带过就行。
评委最常问的三个问题,提前把答案写进稿子:为什么用 SSM 不用更新的框架(课程要求 + 手写配置能讲清楚请求怎么走)、字典表到底解决什么问题(改配置不改代码)、软删除会不会有坑(有,所以查询统一走片段)。这三个问题答得利索,基本就稳了。
还有一个技巧:把“我遇到过什么问题、怎么定位的”准备两个具体例子,比背功能列表有用得多。比如跨域丢 Session 那次,我一开始以为是拦截器写错了,查了两个小时才发现是 withCredentials 没开。从那以后我每次搭前后端分离的项目,都先把登录态跑通再写业务代码,这个顺序再没变过。希望帮到你。
本文还有配套的精品资源,点击获取