1. 项目到底做什么:选题定位与需求拆解
1.1 为什么“防诈知识在线学习”值得做成一个毕设
每年毕业季,计算机专业的同学都在为选题发愁。常见的题库系统、图书管理系统、商城系统已经烂大街,答辩老师看一眼题目就不想追问。我做的是一个“防诈知识在线学习系统”,直接把技术栈和真实的业务场景绑在一起,SpringBoot 负责后端接口,Vue 搭前端页面,整体是一个前后端分离的互动学习平台。
这个选题最大的优势在于:它有明确的社会价值,而且功能模型非常清晰。电信诈骗、网络刷单、冒充公检法、虚假投资理财……这些骗术层出不穷,传统的反诈宣传靠民警进社区发传单、挂横幅,辐射范围有限。如果把防诈知识做成在线课程、图文案例、闯关答题,让用户在手机上就能系统学习,学习进度可追踪,学习效果可考核,这就是一个实实在在的教育平台,不是那种为了做毕设而硬凑出来的空壳系统。
我最初和某高校的导师聊这个想法的时候,他直接指出:系统功能可以简单,但业务链路必须完整。也就是说,不只要能看视频、做题目,还得有用户体系、内容管理、学习记录、积分激励、数据统计这些配套能力。这套链路跑通了,系统才是“活”的,而这也正好是毕业设计答辩时最能展示的部分。
1.2 需求场景分析:三类用户和一条完整业务闭环
在实际开发之前,我花了很长时间梳理需求。防诈学习平台的使用场景并不复杂,核心用户就三类:普通学习用户、内容管理员、系统管理员。
普通用户在平台上的完整路径是:注册登录 → 浏览课程列表 → 学习图文或视频内容 → 完成课后答题 → 获得积分和进度 → 查看个人学习档案。这个路径看起来简单,但它把“学”和“测”串成了一条线,每一步都对应一个功能模块,环环相扣。
管理员做的是:登录后台 → 发布课程和题库 → 管理用户状态 → 查看学习统计数据 → 处理举报和反馈。系统管理员则偏运维向:角色管理、菜单权限、系统日志、数据备份。这些需求拆分清楚之后,后面的表结构设计和接口设计就有了直接依据。
我把核心业务流程画成了一张功能拓扑(这里不贴图,用文字描述):用户端模块包括首页、课程中心、学习中心、答题闯关、积分商城、个人中心;管理端模块包括仪表盘、内容管理、题库管理、用户管理、系统设置。前后端通过 RESTful API 进行通信,认证方式采用 JWT,权限控制通过拦截器加注解实现。
2. 技术选型解析:SpringBoot + Vue 这套组合为什么合适
2.1 后端框架:SpringBoot 到底解决了什么问题
后端选型时,我对比过 SpringBoot、SSH(Struts+Spring+Hibernate)、Servlet 原生开发,甚至 Node.js。最终选了 SpringBoot,理由非常实际:它是当前企业级 Java 开发的事实标准,毕业设计用这个技术栈,面试聊起来也有底气。
SpringBoot 的核心价值在于“约定优于配置”。传统的 SpringMVC 项目要写一堆 XML 配置,数据源、事务、视图解析器、包扫描,每一项都要手动声明。SpringBoot 通过自动配置机制把这些全部接管了,我可以把精力放在业务代码上而不是配置文件里。比如数据源配置,application.yml 里写几行就能连上 MySQL:
spring: datasource: url: jdbc:mysql://localhost:3306/anti_fraud?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: your_password driver-class-name: com.mysql.cj.jdbc.Driver redis: host: localhost port: 6379这一段配置在答辩的时候很好解释:数据库连接池由 HikariCP 自动管理,Redis 缓存解决热点数据的读取压力,这些都不需要手写实现,SpringBoot 自动装配完成了。
为了减少冗余代码,持久层我选用了 MyBatis-Plus。它继承了 MyBatis 的灵活性,同时提供了通用的 CRUD 接口,单表查询基本不用写 SQL。我在项目里自定义了很多查询方法,比如按分类查课程、按用户查学习记录、统计每日答题人数,这些直接用注解 SQL 就能搞定,比 XML 映射文件清爽得多。
2.2 前端框架:为什么选 Vue 而不是 JSP 或者 React
这个项目的用户端需要流畅的内容展示和交互体验,管理后台需要大量的表单和表格操作,JSP 这种服务端渲染方案显然不合适。我在 Vue 2 和 Vue 3 之间犹豫了一下,最终选择了 Vue 3 + Element Plus 的组合。
原因有几条。第一,Vue 3 的组合式 API 让代码组织更清晰,同一个功能的响应式数据、计算属性、方法可以写在一起;第二,Element Plus 是 Vue 3 生态里最成熟的组件库,表格、表单、分页、弹窗、树形控件开箱即用,管理系统开发效率极高;第三,Vue 3 + TypeScript 是当前前端的主流方向,写在简历上是加分项。
不过我要提醒一点:如果对 Vue 3 的响应式原理不熟,调试起来可能比 Vue 2 更费劲。比如reactive和ref的使用场景容易搞混,数组的响应式更新方式也和以前不一样。我在项目里单独封装了一个useUserStore.js的 Pinia 模块,用来管理登录状态和用户信息,这样在各页面之间共享数据非常方便。
2.3 开发环境与工具链
整个项目我用到的环境如下:
| 组件 | 版本/方案 | 说明 |
|---|---|---|
| JDK | 1.8 | 稳定,兼容 Spring Boot 2.7.x |
| Spring Boot | 2.7.18 | 最后一个支持 JDK 8 的稳定版本 |
| MySQL | 8.0 | 存储业务数据 |
| Redis | 5.0+ | 缓存、验证码、积分防重复 |
| Maven | 3.8+ | 后端依赖管理 |
| Node.js | 16+ | 前端构建 |
| 前端框架 | Vue 3 + Vite | 开发体验比 Webpack 好很多 |
| UI库 | Element Plus + ECharts | 后台组件 + 数据可视化 |
3. 数据库设计实战:核心表结构如何支撑业务闭环
3.1 核心表清单与关系梳理
数据库设计是整个系统的地基,这块没做好,后面写代码全是坑。我设计了 10 张核心表,这里挑最重要的几张展开讲。
用户相关:sys_user(用户主表)、sys_role(角色表)、sys_user_role(用户角色关联表)、sys_user_progress(学习进度表)。课程相关:course_category(课程分类表)、course_info(课程表)、course_chapter(章节表)、learn_record(学习记录表)。答题相关:question_info(题目表)、question_option(选项表)、exam_paper(试卷表)、exam_record(答题记录表)。其他:fraud_case(防诈案例库)、integral_log(积分流水表)、sys_log(操作日志表)。
表之间的关系并不复杂:用户与课程是多对多,通过学习记录表关联;课程与章节是一对多;题目与选项是一对多;用户与答题记录是一对多。难点在于业务逻辑的约束,比如“未完成前一章节学习不允许解锁下一章节”“同一用户对同一题目的重复作答如何判分”。
3.2 用户表与角色设计
用户表的字段设计建议包含这几个关键点:用户名、密码(加密存储)、昵称、手机号、头像、状态(启用/禁用)、积分余额、用户等级、注册时间。密码加密我用的 BCrypt,这是 Spring Security 生态里成熟的加密算法,同一密码每次加密结果都不同,安全性比 MD5 高一个量级。
角色设计这里我踩了一个坑。刚开始只设计了USER和ADMIN两种角色,后来发现管理员之间也需要区分。比如内容管理员只能管理课程和题库,不能看系统日志;系统管理员拥有所有权限。所以我引入了sys_role表,用权限标识符区分操作范围,权限控制放在后端接口上用注解拦截。
3.3 课程表与学习记录表的设计细节
课程表course_info的字段包括:课程标题、封面图、分类ID、难度等级(初级/中级/高级)、课程介绍、总学习人数、总章节数、发布状态(草稿/已发布)、创建时间。这里有个容易被忽略的设计点:课程封面图不要直接存图片二进制,存 URL 路径,文件上传后返回存储路径写入数据库,这样可以减轻数据库压力,也方便静态资源分离。
学习记录表learn_record是整个系统里最重要的一张表,它记录了用户和课程之间的互动行为。关键字段有:用户ID、课程ID、章节ID、学习状态(学习中/已完成)、当前学习进度(百分比)、最后学习时间、完成时间。为了防止同一用户对同一章节产生多条重复记录,我给user_id + chapter_id加了唯一索引,数据库层面就堵住了重复数据的可能。
3.4 题目表和答题记录表的设计思路
题目表question_info的字段相对固定:题干内容、题型(单选/多选/判断)、正确答案(存选项ID或布尔值)、答案解析、题目标签、难度等级、所属分类、状态。这里提醒一下,多选和判断的正确答案存储方式一定要统一,比如单选题存一个选项ID,多选题存逗号拼接的多个选项ID,判断题存1或0。我最初没统一,导致后面判分逻辑写得很难看,重构了一次才理顺。
答题记录表exam_record需要有:用户ID、试卷ID、题目ID、用户答案、是否正确、得分、答题时间。同时维护一张exam_paper_record(考试总记录),记录用户某一次答题的总分、总题数、正确率、用时。这样做的好处是,个人中心的正确率统计、错题回顾都直接从记录表查询,不需要临时聚合计算。
3.5 积分流水表的设计
积分是激励用户持续学习的核心手段。integral_log表的职责非常明确:记录每一笔积分的来源和去向。字段包括:用户ID、积分变动值(正数为增加,负数为扣减)、变动类型(学习课程/首次答题/每日登录/兑换奖品/管理员调整)、关联业务ID(比如课程的ID)、变动说明、创建时间。
为了防止积分并发问题,我在积分变更的地方用 Redis 做了幂等控制。同一个用户、同一类型、同一业务ID 只允许触发一次积分增加,Redis 里存一个业务ID的标记键,过期时间设置成 24 小时,这样用户反复刷接口也不会被重复加积分。后面我会详细讲这个逻辑的代码实现。
4. 核心功能模块拆解与实现要点
4.1 在线学习模块:图文与视频内容的播放和进度记录
在线学习模块是系统的内容出口,用户学习的主要载体就是课程。我设计了两种内容形式:图文课程和视频课程。图文课程用富文本编辑器发布,数据库存 HTML 内容;视频课程则通过外部视频文件链接加载。
进度的核心逻辑是这样的:当用户点击进入某一章节,前端向后端发起POST /api/learn/start请求,传参为课程ID和章节ID。后端先检查用户是否有学习资格(比如前置章节是否学完),然后查询learn_record表是否有记录。如果有,则更新最后学习时间;如果没有,则插入一条新记录并初始化学习状态。
@PostMapping("/start") @ApiOperation("开始学习章节") public Result startLearning(@RequestBody LearnStartDTO dto) { // 1. 校验课程是否存在且已发布 CourseInfo course = courseService.getById(dto.getCourseId()); if (course == null || !"published".equals(course.getStatus())) { return Result.error("课程不存在或未发布"); } // 2. 校验章节前置解锁逻辑 boolean locked = learnService.checkChapterLock(dto.getUserId(), dto.getCourseId(), dto.getChapterId()); if (locked) { return Result.error("请先完成前置章节的学习"); } // 3. 写入或更新学习记录 LearnRecord record = learnService.recordProgress(dto); return Result.success(record); }这里我只展示后端的大致骨架。实际开发时,还要注意一个细节:学习进度的更新不能太频繁。如果用户每看一秒钟就调一次接口更新进度,数据库会被刷爆。我的做法是前端每 15 秒上报一次学习时长,后端定期汇总更新进度百分比。这也算是高并发场景的一个小优化思路,答辩的时候可以展开讲。
4.2 互动答题模块:随机抽题、判分与闯关解锁
答题模块是防诈学习平台的“灵魂”,它把被动的知识接收变成了主动的测试反馈。我设计了两种答题模式:章节随堂测和闯关挑战。
章节随堂测的逻辑比较简单:用户学完一个章节后,系统从该章节关联的题库中随机抽取 5 道题,用户答完提交,系统判分并显示答案解析。这里有几个关键的技术点:
第一,随机抽题要保证不重复。我用了 SQL 的ORDER BY RAND()配合LIMIT,但这种方式在数据量大时性能很差,会全表扫描。后来改成了先获取题目ID列表,再用Collections.shuffle在内存里打乱,性能明显提升。针对最多几百道题的单章节题库,内存打乱完全够用。
public List<QuestionInfo> randomQuestions(Integer categoryId, Integer count) { // 1. 查询符合条件的题目ID(不加载所有字段,减少内存占用) List<Integer> ids = questionMapper.selectIdsByCategory(categoryId); // 2. 内存中打乱 Collections.shuffle(ids); // 3. 取前count个 List<Integer> picked = ids.subList(0, Math.min(count, ids.size())); // 4. 按ID批量查询完整题目 return questionMapper.selectBatchIds(picked); }第二,判分逻辑要处理好多选题和判断题。单选题直接比对用户答案和正确答案的字符串;多选题需要把用户答案按逗号拆分,排序后拼接再比对;判断题把答案转成true/false后比较。这里最容易出错的是多选题的“漏选”和“多选”判定规则——如果设计成必须完全一致才得分,需要明确告诉用户规则,否则体验很差。
闯关挑战模式是整体的亮点。我设计成五关制,每一关的题目难度递增。用户必须达到前一关的及格线(60分)才能解锁下一关。全部通关后发放一张“反诈达人”电子证书,同时获得大量积分。这个设计在答辩演示时效果特别好,因为它是可玩性最强的功能,观众一眼就能明白平台的激励机制。
4.3 防诈案例库与实时资讯模块
除了课程和答题,我还设计了一个防诈案例库,用真实的诈骗类型分类(刷单返利、虚假购物、冒充客服、杀猪盘、冒充公检法等),每个案例包含案情描述、诈骗手法拆解、防范提示三个部分。这个模块的价值在于,它把抽象的知识点落到了具体的场景中,更容易让用户产生代入感。
案例库的搜索和筛选用到了LIKE查询和分类过滤。同时引入了 Redis 缓存热点搜索词,把搜索频率高的关键词存成一个 ZSet,在首页展示“热门防诈搜索词”,这也算一个小的玩法创新。后台管理员可以发布案例、上下架内容,所有案例在发布前需要经过“审核”状态,防止不当内容流出。
4.4 管理后台:内容维护与数据可视化
管理后台是最容易看出工程能力的地方,也是答辩老师大概率追问的部分。我的后台页面包含四个主要区域:
仪表盘区展示核心数据指标:总用户数、课程总数、今日活跃用户、累计答题次数、平均正确率,并用 ECharts 画出近 7 天的用户增长趋势和答题量柱状图。
内容管理区提供课程和题目的维护界面,支持批量导入题目的功能(Excel 模板导入),这个功能实际开发时非常有用。我封装了一个ExcelUtil,用 EasyPOI 解析上传的文件,校验每行数据的格式,把有效数据批量插入题库。批量导入的校验逻辑一定要写全:题干为空、选项缺失、正确答案超出选项范围这些都要拦截。
用户管理区支持用户列表检索、状态启停、积分调整。操作日志区记录管理员的所有关键操作,包括登录时间、IP地址、操作内容,这些日志写进sys_log表,并为管理员的账号安全提供审计依据。
4.5 积分、等级与激励体系
为了让用户愿意持续学习,我搭建了一个轻量级的激励体系。积分来源包括:每日登录(+5分)、完成一个章节学习(+10分)、首次答对一道题(+2分)、闯关成功(+50分)、连续学习 7 天(+100分)。
积分消耗的场景是积分商城兑换虚拟礼品,比如兑换“反诈卫兵”专属头像框、兑换学习证书样式等。这些内容虽然简单,但让系统有了“消耗”的出口,积分才不会无限膨胀。
学习等级根据总积分自动计算。用户在个人中心能看到自己的等级进度条,从“防诈新手”到“反诈专家”共五个等级,每升一级解锁新的课程推荐内容。这种轻游戏化的设计,是从主流的“签到+积分+等级”模式里借鉴来的,符合现代在线教育产品的基本逻辑。
5. 实操过程全记录:从零搭建到功能上线
5.1 后端项目初始化与依赖配置
我使用的是 IDEA 的 Spring Initializr 创建项目,也可以直接去官方网站生成压缩包。创建时我选择了 Java 8、Maven、Spring Web、MySQL Driver、MyBatis-Plus 相关依赖。
pom.xml里的关键依赖大致如下:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt</artifactId> <version>0.9.1</version> </dependency> <dependency> <groupId>com.github.xiaoymin</groupId> <artifactId>knife4j-openapi2-spring-boot-starter</artifactId> <version>4.4.0</version> </dependency>JWT 库我选了 JJWT,Knife4j 用于生成接口文档。强烈建议在毕设里集成 Knife4j,因为它能自动生成可调试的 API 文档,答辩演示的时候把接口文档投屏出来,效果远好于嘴上讲“这个接口是用来查数据的”。
全局配置文件里,除了数据源、Redis 连接,还需要设置 MyBatis-Plus 的逻辑删除和驼峰映射:
mybatis-plus: configuration: map-underscore-to-camel-case: true global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0逻辑删除是个好习惯。用户误删、内容下架这类操作,直接用UPDATE更新deleted字段而不是DELETE物理删除,既能保留历史数据,又方便后续召回。前提是所有表的查询都要默认带上deleted = 0的条件,MyBatis-Plus 的逻辑删除插件会帮我把这个条件自动拼接上。
5.2 JWT 认证与权限拦截的实现细节
前后端分离项目,登录状态管理是一个重点。我用 JWT 无状态认证,登录成功时后端签发一个 token,前端存储在 localStorage,每次请求在请求头中带上Authorization: Bearer <token>。
后端实现了一个拦截器AuthInterceptor,在请求进入 Controller 之前判断是否需要登录:
@Component public class AuthInterceptor implements HandlerInterceptor { @Autowired private StringRedisTemplate redisTemplate; @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行登录接口 if (request.getRequestURI().contains("/auth/login")) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } // 解析 token,拿到用户信息 Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); Long userId = Long.parseLong(claims.get("userId").toString()); // 校验 token 是否在 Redis 白名单中(用于踢人下线、主动注销) String redisKey = "login:token:" + userId; String cacheToken = redisTemplate.opsForValue().get(redisKey); if (!token.equals(cacheToken)) { throw new BusinessException(401, "账号已在其他地方登录"); } request.setAttribute("userId", userId); return true; } }这里有一个非常关键的设计点:我把 JWT 和 Redis 结合使用。JWT 本身是无状态的,但如果用户主动注销或管理员封禁账号,token 在过期前依然有效。通过在 Redis 中维护一份白名单,登录时写入,注销时删除,管理员封禁用户时移除对应的 key,就实现了有状态的安全性,又不破坏 JWT 的无状态扩展性。这个方案在答辩时被老师专门问过,我的回答能直接体现出对认证机制的深度理解。
权限控制方面,我自定义了一个@RequireRole("ADMIN")注解,配合拦截器校验当前用户的角色。管理员接口统一加上注解,防止普通用户越权调用。
5.3 前端工程搭建与 Axios 封装
前端我用 Vite 创建 Vue 3 工程,npm create vite@latest anti-fraud-web -- --template vue,然后安装 vue-router、pinia、axios、element-plus。
Axios 封装是前端最重要的基础设施,我在src/utils/request.js里统一配置了请求拦截器和响应拦截器。请求拦截负责给每个请求带上 token;响应拦截统一处理错误码,比如 401 跳转登录页、500 弹出错误提示,不用每个页面都写一遍错误处理逻辑:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 15000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = 'Bearer ' + token } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { ElMessage.error(res.message || '请求失败') return Promise.reject(new Error(res.message)) } return res.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error(error.message || '网络异常') return Promise.reject(error) } ) export default request路由守卫beforeEach里做登录态检查和页面标题设置。这里有一个小经验:页面标题最好在路由 meta 里配置,切换路由时统一更新,不然浏览器标签页永远显示一个固定的标题,观感很业余。
5.4 典型页面实操:课程列表、学习页、答题页
课程列表页是用户进入平台的第一个页面。我用了 Element Plus 的 Card 组件展示课程卡片,通过后端接口分页查询课程。搜索和分类筛选并发请求时稍微做了一次防抖处理,避免用户快速切换分类时连续请求服务器。
学习页是系统的核心页面。图文课程左侧显示章节目录、右侧渲染富文本内容,顶部显示学习进度。视频课程则嵌入 video 播放器,监听timeupdate事件,每 15 秒向后端上报一次学习进度。这里有个需要注意的交互细节:用户切走页面再回来时,应该保留上次的滚动位置和视频播放进度,否则用户体验会断档。我的做法是把进度存进 Vuex/Pinia,在页面销毁前再统一上报。
答题页的交互相对复杂。我需要一次展示一道题,支持单选/多选/判断三种题型,用户确认提交后立即显示答案和解析,然后进入下一题。全部答完后展示本次答题的结果汇总:得分、用时、正确率、每道题的错题解析。整个流程用了ref管理当前索引和用户答案,用reactive管理答题结果集合。
5.5 项目打包与部署
开发完成后,打包部署的步骤也值得记录。后端在pom.xml配置了 Spring Boot Maven 插件,执行mvn clean package -DskipTests生成可执行 Jar 包,然后用java -jar xxx.jar启动。
前端执行npm run build生成静态文件,我用 Nginx 托管静态资源并配置反向代理,把/api开头的请求转发到后端 8080 端口:
server { listen 80; server_name localhost; root /opt/anti-fraud/dist; index index.html; location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这里try_files的作用是解决前端路由在刷新时出现的 404 问题。Vue Router 的 history 模式在浏览器直接访问/course/1时,Nginx 找不到物理文件会返回 404,加了try_files ... /index.html后,所有请求都会回退到首页,再由前端路由接管。如果不想配置 Nginx,也可以直接用 hash 模式,但我个人更推荐 history 模式 + Nginx 配置,这才是企业级的标准方案。
6. 常见问题与排查技巧全记录
6.1 开发中踩过的 7 个坑
这套系统从零开始写了大约一个月,踩了不少坑,我把印象最深的几个记录下来,希望后来者少走弯路。
第一个坑是跨域问题。前端开发服务器在 5173 端口,后端在 8080 端口,浏览器直接请求后端接口会触发跨域。很多教程让你在后端加@CrossOrigin或者配置 CORS 过滤器,但这样生产环境也要放开跨域,不安全。我最后采用的是 Vite 的代理配置,开发环境用/api前缀转发请求,生产环境由 Nginx 代理,前端代码里完全没有写死后端地址。这是前后端分离项目最规范的方案。
第二个坑是 MyBatis-Plus 的分页插件。旧版本的PaginationInnerInterceptor可以直接用,但新版本需要额外配置MybatisPlusInterceptor,而且如果数据库方言没有正确配置,分页查询会对所有查询都加LIMIT,导致部分统计结果异常。建议直接用最新版插件并配置好 MySQL 方言。
第三个坑是RESTful API 的参数校验流于形式。刚开始我只用了@RequestBody接收 JSON 数据,前端传什么后端都接收,结果接口崩溃率很高。后来在 DTO 上加了@NotBlank、@NotNull等校验注解,Controller 参数前加@Validated,再配合全局异常处理器统一返回校验失败信息,接口才真正稳定下来。
第四个坑比较隐蔽,是富文本编辑器的 XSS 攻击。管理员发布课程内容用的是富文本编辑器,用户可以输入任意 HTML,如果不过滤就保存,<script>标签会在其他用户浏览课程时执行,这就是存储型 XSS。我在后端保存内容前用 Jsoup 清理了危险标签和白名单外的属性,这个细节在安全审查时会被重点关注。
第五个坑是文件上传大小限制。Spring Boot 默认单文件上传上限是 1MB,课程封面图稍微大一点就会报MaxUploadSizeExceededException。我在配置文件里调整了限制:
spring: servlet: multipart: max-file-size: 10MB max-request-size: 20MB同时前端上传前用 JS 校验文件类型和大小,双重拦截,避免无效请求打到后端。
第六个坑是时间时区问题。MySQL 的serverTimezone参数没有正确配置时,Java 端存入的时间会差 8 个小时。我在数据库连接串里加了serverTimezone=Asia/Shanghai,后端统一用LocalDateTime类型,前端展示时通过dayjs格式化,全链路时间就一致了。
第七个坑是接口文档调试的低效。一开始我连 Postman 都懒得配置,全靠前端联调时看浏览器网络请求。后来集成了 Knife4j,所有接口在线调试,token 还能通过全局参数自动携带,联调效率直接翻倍。毕设阶段一定要尽早集成接口文档工具,后面的调试时间能省一大半。
6.2 并发与性能问题的排查思路
答辩老师很容易问一个问题:“如果有 1000 个人同时答题,你的系统扛得住吗?”这个问题不能只说“扛得住”,要把方案讲清楚。
我的处理思路分三层。第一层是缓存,课程列表、题目详情这类读多写少的数据,首次查询后放入 Redis,设置 10 分钟过期,后续请求直接走缓存,减轻数据库压力。第二层是数据库索引,查询频率最高的learn_record表核心查询条件是user_id + course_id + chapter_id,已在三列上建了联合索引,单用户查询效率足够快。第三层是接口幂等和防刷,答题提交接口用 Redis 的SETNX做并发控制,同一个用户同一秒钟只能提交一次。
至于真正的压测,我没有像企业那样用 JMeter 做完整的性能测试,但至少用接口测试脚本模拟过 100 个并发请求,系统没有出现明显的卡顿或数据错乱。这个层面做毕设已经算是加分项了。
6.3 安全防护的强化记录
安全防护在我的项目里占了很大一部分比重,因为反诈学习平台本身就涉及内容安全。主要做了四件事:
密码安全使用 BCrypt 哈希存储,不允许明文入库;登录接口加上人机验证(图形验证码),并限制同一个 IP 的失败次数;所有隐私字段(手机号)在日志中脱敏展示;管理员操作全程留痕,写入操作日志。
这里想特意强调一下图形验证码的做法。我用 Redis 存储验证码内容,30 秒过期,每次登录失败连续 5 次后,必须输入验证码。这个策略借鉴了主流互联网产品的防爆破方案:让正常用户的登录几乎无感知,同时拦住自动化攻击脚本。
7. 最后想说的话:做毕设的一些个人经验
整套系统做下来,我的最大体会是:毕业设计并不是越复杂越好,而是把一条完整的业务链路跑通,并且每个环节都能自圆其说。
如果你也准备做类似的在线学习平台,我建议按这样的顺序推进:先把数据库表和接口设计定下来,然后做后端接口(用户认证、课程、答题这三块优先),再做前端核心页面,最后补管理后台和统计图表。不要一开始就陷入积分体系、排行榜这些锦上添花的功能,先把“学—测—评”这条主线打通,系统就已经能用了。
还有一个小技巧:给答辩准备演示数据时,一定要把数据做得真实。我提前导入了 10 门课程、100 多道题目,并且用测试账号真实学习了好几个章节,让答题记录、积分流水、统计图表都有真实的增长趋势。答辩现场打开个人中心,进度条、积分、等级都肉眼可见,比空口说“系统支持这些功能”有说服力得多。
最后,这个项目如果你感兴趣,还可以继续扩展:接入智能对话机器人做反诈咨询服务,或者引入推荐算法根据用户答题情况推送易错题型相关的学习内容。方向很多,先把基础平台做扎实,后面每一步都是加分项。