ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

在线互动学习网站全栈开发:SpringBoot+Vue3+MyBatis-Plus实战拆解

在线互动学习网站全栈开发:SpringBoot+Vue3+MyBatis-Plus实战拆解 1. 为什么选了这套技术栈来做在线学习网站先说结论这套在线互动学习网站系统本质上是一个典型的前后端分离 权限角色分离 互动业务闭环的 Java Web 项目。它适合两类人一是刚学完 Spring Boot 和 Vue 基础、想找一个完整项目练手的同学二是学校课程设计或毕业设计阶段需要快速交付一套可演示系统的开发者。我为什么说它是典型因为它的技术选型几乎踩中了当前 Java Web 教学和中小型项目的主流组合SpringBoot2 做后端底座Vue3 做前端交互MyBatis-Plus 做数据持久层增强MySQL8.0 做数据存储。这套组合的好处是各层技术都有庞大的社区基础和现成资料遇到问题基本都能搜到答案对于学习者来说卡壳成本很低。但对于一个在线互动学习网站来说技术栈只是地基。真正有意思的是它的业务设计——它不像普通的 CRUD 管理系统那样只做增删改查而是要把课程内容展示—用户学习记录—提问互动—测试评估这条链路跑通。这背后牵扯到数据表的关系设计、权限控制粒度、前端页面状态管理、文件上传与访问策略等一系列问题。我的一个总体感受是这个项目的难度曲线比较平缓没有特别炫技的算法或高并发设计但它的完整度高。从数据库脚本到后端接口从管理后台到用户前端从登录鉴权到互动功能都有落地的实现。也就是说你可以通过阅读这份源码把一套真实项目的完整开发流程串起来。在开始拆解之前我要先说明一个认识学习网站的核心竞争力是内容组织方式和互动机制而不是技术名词的堆砌。所以下面我会把重点放在这套系统是如何围绕互动学习场景做技术落地上顺带解答那些热搜词里大家普遍困惑的细节比如 MyBatis-Plus 的 XML 和 Mapper 放置问题、MySQL8.0 的安装与连接配置、Vue3 的项目初始化方式等。2. 项目骨架从数据库设计到后端分层目录规范很多人在拿到一份源码时会先去看代码写了什么但我个人的习惯是先看数据库设计。因为数据库表结构基本决定了这个系统的业务边界。你可以通过表之间的关系反推出功能模块也可以在后期做二次开发时快速定位要改哪里。2.1 数据库设计的核心表关系一个在线互动学习网站最少需要这几类数据支撑用户、课程/内容、互动记录、评估数据。这套系统对应的核心表大概可以抽象为以下结构表类型典型表名承载的核心字段作用用户相关sys_userid、username、password、role区分管理员和学生课程内容course、chapter、lesson标题、简介、正文/视频地址、排序组织学习内容互动相关comment、question_answer课程id、用户id、内容、回复关系实现问答与评论学习记录study_record用户id、课程id、进度、时长跟踪学习行为评估相关exam、question、exam_record题目、选项、答案、得分小测验与自测如果你有课程表通常还会关联一个用户课程表用于区分所有人可见和报名后可见两种内容访问方式。互动学习网站和普通内容网站的差异正在这里——它需要感知学习者的身份和学习进度所以用户与内容之间不是简单的浏览关系而是有状态的关系。数据库脚本文件通常在源码的sql/目录下注意导入时选择 MySQL8.0 版本。如果你本机是 MySQL5.7可能会遇到排序规则或默认字符集的问题。MySQL8.0 默认字符集是utf8mb4建议保持这个设置因为要存储用户评论这种非结构化文本utf8mb4能完整支持 emoji 字符和多语言内容。2.2 后端分层Controller—Service—Mapper 的边界划分这套系统的后端基于 SpringBoot2 MyBatis-Plus目录结构基本遵循标准 Java Web 分层com.example.study ├── controller // 接收前端请求做参数校验和结果封装 ├── service // 业务逻辑层处理核心事务 ├── mapper // 数据访问层继承 BaseMapper 或定义 XML ├── entity // 数据库实体映射类 ├── dto // 数据传输对象用于前后端交互 ├── vo // 视图对象用于封装响应数据 ├── config // 配置类如拦截器、跨域处理 ├── common // 通用类如统一返回结果、异常处理 └── utils // 工具类如 JWT 工具、密码加密工具这种划分的最大价值是隔离变化。Controller 只做接参和返参Service 只做业务规则Mapper 只做数据读写。比如我要增加一个课程报名功能在 Controller 层加一个接口在 Service 层实现报名逻辑和重复校验在 Mapper 层操作用户课程表改动范围清晰可测不会牵一发而动全身。MyBatis-Plus 在这个架构里承担的是少写代码的任务。单表 CRUD 完全不需要手写 SQL直接继承BaseMapperT就能获得selectById、selectPage、insert等基础方法。对于课程分页列表这种高频接口配合Page分页插件两三行代码就能搞定。2.3 MyBatis-Plus 的 XML 与 Mapper 同目录配置细节热搜词里有一条非常具体的问题Spring Boot 项目使用 MyBatis-PlusXML 与 Mapper 在同一个文件夹下应该如何配置。这个坑我太熟悉了因为我在第一次配置时也被它卡过。常规做法是把 XML 文件放在resources/mapper/目录下然后在application.yml里指定mybatis-plus: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.study.entity但如果你想让 XML 文件和 Mapper 接口在同一个 Java 包下比如com.example.study.mapper必须在pom.xml里加一段资源配置让 Maven 在打包时把.xml文件也纳入产物build resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource resource directorysrc/main/resources/directory /resource /resources /build然后在application.yml里改成mybatis-plus: mapper-locations: classpath*:com/example/study/mapper/*.xml为什么一定要加classpath*:因为 Maven 默认不会把src/main/java下的 XML 文件视为资源文件即使它们在 IDE 里看起来是正常的。不加前面的配置编译后 XML 不会出现在target/classes里运行时会直接报Invalid bound statement (not found)。这个错误的典型特征是单个selectById能跑但带自定义 SQL 的方法全部报错。我的建议是如果项目里有超过三个自定义 SQL连表查询、复杂统计等就老老实实用resources/mapper/的标准方式避免和 Java 源码混在一起。如果只有一两个自定义查询放在同级目录也能接受但要记住给pom.xml加资源配置。这个细节在文档里往往不会重点写恰恰是初学者最容易踩坑的地方。3. 互动学习模块是怎么一步步做出来的说完了地基我来说说业务层。在线互动学习网站和传统内容管理系统的最大区别是它要把学和习结合起来。我的做法是先把一个最小闭环跑通用户登录 → 浏览课程 → 学习内容 → 提问和评论 → 记录学习进度。这套系统里的互动模块就是围绕这个链路设计的。3.1 用户体系JWT 鉴权与验证码校验学习网站必须区分用户角色。一般来说管理员负责上传课程和查看统计数据学生负责学习和互动。这套系统用 JWT 做无状态鉴权流程是用户提交用户名密码登录页还有图形验证码后端校验通过后生成 Token 返回前端前端把 Token 存到localStorage或Pinia中后续请求在请求头携带Authorization: Bearer token后端通过拦截器解析 Token识别用户角色JWT 的好处是服务端不需要存储 Session在前后端分离架构下特别方便。但要注意一个问题JWT 无法主动失效如果用户修改了密码或被管理员禁用了旧的 Token 依然有效直到过期。解决思路有二一是把 Token 的有效期设置短一些配合前端定时刷新二是在 Redis 里维护一个 token 黑名单。这套系统如果只是课程设计或学习用途短期 Token 就够了。图形验证码是容易被忽略的环节。它的作用不是防止暴力破解JWT 本身有失败次数限制就够而是防止脚本机器人刷注册和登录接口。如果用 Java 生成验证码图片常见方案是kaptcha或者hutool的CaptchaUtil生成后把验证码文本存到 Redis设置两三分钟的有效期。3.2 课程与章节内容组织的内容模型设计课程内容是学习网站的主干。课程表通常包含标题、封面图、简介、分类、难度等级等字段。章节表通过course_id外键关联课程再用sort字段控制排序。这里有一个我在实际开发中反复踩过的坑不要直接用order作为字段名。order是 SQL 的保留关键字在 MyBatis-Plus 自动生成 SQL 时会报语法错误。你可以在 Java 实体里写TableField(sort)来映射但更推荐在一开始就用sort或sort_order这类非保留字命名。课程内容分两种富文本和视频。在线学习网站通常两种都要支持。富文本内容我建议直接存 HTML 字符串到数据库的longtext字段这样在页面上渲染时只需要用v-html输出不需要额外的文件存储逻辑。但要注意 XSS 攻击富文本编辑器提交的内容里可能携带有害的script标签后端在接收 content 字段时要做标签过滤或者使用白名单策略只允许p、img、a、ul、ol、li等常规教学标签。这一步在系统文档里不会强调但对真实上线很重要。3.3 互动功能评论、提问与点赞的实现策略互动学习网站区别于普通网盘资源站的核心在于用户能留下内容。评论和问答模块是这个系统最出彩的部分。在数据模型上评论表和问答表结构非常相似id bigint primary key course_id bigint not null comment 课程ID user_id bigint not null comment 用户ID parent_id bigint default 0 comment 父评论ID0表示顶级评论 content text not null like_count int default 0 create_time datetimeparent_id的存在是为了支持楼中楼回复。当parent_id0时是顶级评论当parent_id指向某条评论时是回复。查询时先查所有顶级评论按时间倒序或热度排序再根据顶级评论的 id 集合查询回复列表最后在 Java 内存中组装成树形结构。这种方式在数据量不大的场景下完全够用简单直接比 SQL 里做递归查询更容易理解和维护。点赞功能看似简单但是有个坑连续点赞的防重。最基础的实现是在点赞表加唯一约束(user_id, target_id, target_type)每次点赞前先查记录如果存在则取消点赞并减少计数不存在则插入并增加计数。但这个操作在高并发下可能产生超卖问题需要给点赞数更新这条 SQL 加乐观锁Update(UPDATE comment SET like_count like_count 1 WHERE id #{id} AND like_count 0) int increaseLikeCount(Long id);对于课程设计或中小型学习网站这个方案的性能足够代码量也少。如果想追求更好的响应速度可以把点赞行为写入 Redis 并异步同步到数据库但那样的话项目复杂度会明显上升不利于初学者理解和维护。3.4 学习进度记录核心技术点的前后端配合学习进度是这套系统互动属性的重要体现。它解决的核心问题是一个用户学了一半退出下次进来怎么快速回到上次的位置。后端需要一张学习记录表id bigint primary key user_id bigint course_id bigint chapter_id bigint last_lesson_id bigint progress_percent int update_time datetime当用户点击开始学习某个章节时前端发送请求后端更新或插入当前用户的进度记录。再次进入课程详情页时后端查询该用户最近的学习记录返回last_lesson_id前端通过路由跳转到对应章节。这里的核心设计点是更新策略。如果用户每打开一个小节就写一次数据库会产生大量冗余写操作。我建议在前端做节流用户切换章节时先更新本地变量和 Pinia 状态然后通过setTimeout延迟 3 秒统一提交一次这样可以减少 70% 以上的无效请求。也可以用上次进度 最高已学章节号的方式简化只记录用户学到的最大章节不记录具体到秒的观看时间这样每次请求的数据量极小表也不需要频繁更新。前后端联调要注意一个细节进度计算要以后端返回的 course 总章节数为基准而不是前端在列表页数出来的数字。因为课程可能包含未发布的隐藏章节前端看到的章节列表未必是全量数据进度百分比如果用前端数字算后续内容发布后会导致进度回退显得很奇怪。最稳妥的方式是后端在课程详情接口里返回totalChapters和learnedChapters前端只负责展示比例。4. 前端实现细节Vue3 状态管理 网络请求封装前端部分这套系统用的是 Vue3。相比 Vue2Vue3 最大的区别是组合式 APIComposition API和基于 Proxy 的响应式系统。在学习网站上组合式 API 带来的好处是逻辑复用更自然比如把课程列表页的加载逻辑抽成一个useCourseList()函数在任何组件里都能复用。4.1 创建项目与目录规划如果是按照官方推荐方式用 Vite 创建 Vue3 项目npm create vitelatest study-web -- --template vue这里有个经验如果使用 Vue3 和 Element Plus 这类组件库建议在创建项目时直接选vue模板而不是vue-ts模板。不是说 TypeScript 不好而是对于学习用途和个人项目来说TS 会引入类型声明的维护成本。如果你用的是若依框架的 Vue3 版本那另当别论但本系统如果是纯手动搭建JS 模板上手更快。目录规划方面我推荐下面这个结构src ├── api // 接口请求封装按模块拆分 ├── assets // 静态资源 ├── components // 通用组件比如分页、富文本编辑器、文件上传 ├── router // 路由配置包含动态路由守卫 ├── stores // Pinia 状态管理 ├── views // 页面组件按 role 区分 admin 和 student └── utils // 工具类比如 request.js 封装 axiosrequest.js是前端网络请求的核心封装建议在项目一开始就写好不然后面每个页面都要重复处理 Token 和错误码。封装思路如下import axios from axios import { ElMessage } from element-plus import router from /router const request axios.create({ baseURL: /api, timeout: 10000 }) // 请求拦截器携带 Token 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 || 请求失败) if (res.code 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(new Error(res.message)) } return res }, error { ElMessage.error(error.message || 网络错误) return Promise.reject(error) } ) export default request为什么要统一封装因为学习网站里像课程列表、评论列表、试卷提交这类接口数量很多如果每个页面都手动处理错误码和 Token代码冗余不说出错的概率也会增加。统一封装之后后端返回code401时自动跳转登录页用户被顶号或 Token 过期时体验不会太突兀。4.2 状态管理用户信息和学习进度的存储策略Vue3 的状态管理库对应的是 Pinia。对于这套系统至少要有两个 store一个是用户信息 store一个是课程学习状态 store。用户信息 store 的作用是全局共享当前登录用户的基本信息避免在多个页面重复请求用户接口// stores/user.js import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: {} }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, setUserInfo(info) { this.userInfo info }, logout() { this.token this.userInfo {} localStorage.removeItem(token) } } })学习状态存储的作用是协调不同组件间的数据同步。比如用户在课程详情页上一节下一节切换时侧边栏的章节高亮、顶部的进度条、评论区的刷新标记都需要知道当前章节的 ID。把当前课程和章节信息放到 Pinia 里课程页的各个子组件可以共享这个状态不需要每次切换都重新请求后端。这里有一个实测经验不要在 Pinia 里持久化整棵课程树的数据。课程内容数据量大、变动频繁放内存里刷新页面就丢了反而造成看起来有数据、实际已过期的错觉。正确的做法是Pinia 只存当前选中的课程 ID 和章节 ID课程树的完整数据用浏览器的sessionStorage按course_${courseId}做缓存下次进入课程页时直接读缓存再在后台静默刷新一次保证用户快速看到上次的浏览位置。4.3 登录页和注册页的交互设计热搜词里有vue3 登录页面 点线动态的背景和使用 Vue3 和 Element Plus 构建用户注册和登录界面说明很多人在做学习网站时登录页的设计是绕不开的需求。学习网站的登录页要兼顾美观和实用性。我的建议是视觉上以简洁为主背景可以用 Canvas 绘制粒子连线效果简单版或者直接用纯 CSS 渐变加浮动几何图形。粒子连线效果的实现思路是Canvas 上随机生成一些点每个点按随机方向缓慢移动当两个点之间的距离小于阈值时画一条透明度随距离变化的线段。本质上这属于锦上添花的功能不要让它影响性能。requestAnimationFrame驱动动画时记得组件卸载时调用cancelAnimationFrame取消动画循环否则页面切换后 Canvas 还在后台绘制白白消耗 CPU。表单逻辑上注册页至少要有用户名、密码、确认密码、邮箱/手机号、图形验证码。Element Plus 的Form组件自带了rules校验规则密码强度校验可以加一个8-20位包含字母和数字的规则。这个问题看很多人问vue3怎么连接后端其实就是通过 axios 调用后端接口。比如注册接口const handleRegister async () { await formRef.value.validate() const res await request.post(/auth/register, { username: formData.username, password: formData.password, email: formData.email, captcha: formData.captcha }) if (res.code 200) { ElMessage.success(注册成功请登录) router.push(/login) } }这里有个容易被忽略的问题注册成功后和前端校验失败时的提示要区分开。比如后端返回该用户名已被注册前端不应该只是弹一个请求失败。好的做法是后端在业务异常时返回code500加上具体message前端响应拦截器统一弹出message而不是覆盖成固定文案。我看到很多项目在响应拦截器里写死了错误提示导致后端返回的有效信息被吞掉用户体验非常差。4.4 富文本编辑器和文件上传的常用方案课程管理员需要在后台发布课程内容学生评论区也可能需要上传图片。这里涉及两个常用组件富文本编辑器比如 wangEditor 或 Quill和文件上传组件。富文本编辑器我推荐wangeditor它对 Vue3 的支持比较成熟API 简单中文文档完善。实践中要注意的是编辑器产生的 HTML 提交到后端后回显时要用 v-html 渲染但渲染前一定要经过后端的白名单过滤。另外编辑器里插入的图片默认是 base64 格式一大段富文本提交后体积会非常大。建议配置customUpload事件把图片先传到自己的服务器或 OSS然后在内容里插入图片的 URL这样数据库只存 HTML 结构和图片链接而不是图片数据本身。文件上传这块常规方案是写好一个UploadFile接口接收multipart/form-data请求把文件存储到本地磁盘或云存储。本地存储要注意部署环境的路径一致性推荐把上传目录配置到application.yml中通过配置项动态获取file: upload-dir: /data/study-web/upload access-path: /files/**同时配置静态资源映射让上传的图片可以通过 URL 直接访问Configuration public class WebConfig implements WebMvcConfigurer { Value(${file.upload-dir}) private String uploadDir; Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/files/**) .addResourceHandler(file: uploadDir /); } }如果你希望图片访问不走应用服务器可以直接接入阿里云 OSS前端通过后端签发临时凭证上传代码会复杂一些但性能更好。对于学习网站来说本地文件存储已经够用而且更容易在课程设计答辩时讲清楚原理。5. 前后端联调中的接口约定与权限校验前后端分离项目的核心工作不只是各自写代码联调阶段的接口约定直接决定了开发效率。如果前期没有约定好请求和响应格式前后端各自为政后面联调时会出现大量无意义的扯皮。我觉得这套系统里有一个很好的示范统一返回体、统一权限注解、统一异常处理。5.1 统一返回体的设计后端接口返回给前端的数据结构应该是一致的通常是这样{ code: 200, message: success, data: { id: 1, title: Spring Boot 入门 } }在 Java 中可以用一个ResultT泛型类实现public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.setCode(200); result.setMessage(success); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }所有 Controller 方法的返回值统一用ResultT这样前端的响应拦截器只需要判断code即可不需要每个接口单独处理返回格式。5.2 登录拦截器和角色权限控制后端的权限控制有两层一是必须登录才能访问的接口二是必须管理员才能访问的接口。SpringBoot 里可以通过拦截器HandlerInterceptor实现登录校验再结合自定义注解做角色控制。拦截器代码大致结构public class LoginInterceptor implements HandlerInterceptor { Autowired private StringRedisTemplate redisTemplate; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 放行预检请求 if (OPTIONS.equals(request.getMethod())) { return true; } // 获取 Token String token request.getHeader(Authorization); if (token null || !token.startsWith(Bearer )) { response.setStatus(401); return false; } // 解析 Token 并校验 Redis 中的有效状态 String realToken token.substring(7); // 伪代码根据 token 解析用户 id查询 Redis不存在则 401 return true; } }拦截器配置类里要排除掉登录注册接口、课程列表页的公开接口和静态资源路径。不然用户没登录的时候连课程列表都看不了这对于学习网站来说反而不合理。更好的策略是课程内容列表公开访问但提交评论记录学习进度参加测验这些操作必须登录。也就是读接口放行写接口鉴权。角色区分用拦截器不好做精细控制我建议在后端加一个自定义注解RequireRole(ADMIN)配合 AOP 或拦截器判断当前登录用户的角色。这样在课程管理、用户管理等接口上打一个注解代码可读性比在拦截器里硬编码 URL 白名单要清晰得多。5.3 跨域处理与 CRUD 接口的常见返参前端开发服务器Vite 默认端口 5173和后端应用默认 8080不在同一个源上必须解决跨域问题。两种常见方式后端加CrossOrigin或全局跨域配置前端 Vite 配置 proxy 代理我推荐第二种因为生产环境上线时 Nginx 也会做反向代理前端开发时用 proxy 可以让浏览器始终请求同源地址减少调试干扰。Vite 的配置// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })前端请求http://localhost:5173/api/course/listVite 会代理到http://localhost:8080/api/course/list。这里的后端context-path如果是/api就保持一致如果不是可以在 proxy 里加rewrite去掉或替换路径。课程列表接口的返回格式建议用分页对象{ code: 200, message: success, data: { total: 50, records: [ { id: 1, title: 算法入门, cover: http://xxx/cover.jpg, studyCount: 256, chapterCount: 12 } ] } }records列表里的字段名尽量和前端表格或卡片组件需要的字段名对齐比如头像字段叫avatar就叫avatar不要叫imageUrl或img。前后端字段命名混乱是联调时长最大的隐形杀手建议在接口设计阶段就拿着一张字段清单逐一确认。6. 部署上线时踩过的坑与性能注意点源码在你本地能跑起来只算完成了 30%真正让系统可用还需要处理部署和运行环境的问题。我把自己在部署 Java Web 前后端分离项目时遇到的高频问题整理一下你可以对照检查。6.1 MySQL8.0 安装与连接配置MySQL8.0 和 5.7 有个显著区别默认密码加密方式是caching_sha2_password而很多旧版本的数据库连接驱动5.1.x 或更早不支持这种认证方式导致 JDBC 连接时报Public Key Retrieval is not allowed或Unable to load authentication plugin。解决方案有三种在 JDBC URL 参数中加allowPublicKeyRetrievaltrueuseSSLfalse将 MySQL 用户密码改为mysql_native_password方式ALTER USER rootlocalhost IDENTIFIED WITH mysql_native_password BY 你的密码; FLUSH PRIVILEGES;使用 MySQL8.0 对应的 JDBC 驱动版本com.mysql.cj.jdbc.Driver并在pom.xml里指定dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.33/version /dependency在application.yml里对应的配置为spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/study_web?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456如果使用 Docker 安装 MySQL8.0我建议把数据目录挂载到宿主机否则容器删除后数据全部丢失docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e MYSQL_DATABASEstudy_web \ -v /data/mysql:/var/lib/mysql \ mysql:8.06.2 Nginx 的反向代理与前端刷新 404 问题前后端分离项目部署时通常由 Nginx 托管前端静态文件并把/api路径下的请求反向代理到后端服务。一个典型的 Nginx 配置server { listen 80; server_name study.example.com; root /usr/share/nginx/html; index index.html; # 前端页面 location / { try_files $uri $uri/ /index.html; } # 后端接口 location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这一行非常关键。Vue3 使用 Vue Router 的 history 模式时刷新某个子路由页面比如/course/1会直接请求服务器上的这个路径如果没有对应的静态文件就会 404try_files会把它回退到index.html再由前端路由接管页面才能正常显示。我在第一次部署 Vue 项目时忘了加这一行结果所有非首页的链接分享出去都打不开被测试同学连续吐槽了好几次。后来把这行加上就彻底解决了前端路由模式如果没有特殊需求建议配套 history 模式加 try_files 一起用URL 不带有#更美观也更好分享。6.3 安全性检查日志脱敏与默认口令上线之前有几项安全相关的检查建议过一遍修改默认管理员密码。很多系统数据库脚本里内置了admin/123456部署后如果不强制修改很容易被扫库攻击。密码加密存储。明文密码在数据库里一旦泄露就是灾难。Spring Security 的BCryptPasswordEncoder是常用方案也可以使用hutool的BCrypt工具类密码加盐后存储登录时比对加密值。日志脱敏。不要在日志里打印用户的完整密码、身份证号、Token 等敏感信息。前后端联调时我见过有人在 controller 里打印整个请求体主密码字段也被完整输出这在真实环境是不可接受的。接口限流。评论和提问接口容易被脚本刷屏简单做法是在拦截器里加一个 IP 维度的频率限制比如一分钟内最多提交 5 条评论。用 Redis 的INCR加过期时间就能实现不需要引入复杂的限流框架。6.4 性能优化索引设计与缓存策略这套系统的表数据量在初期不大但还是建议在一开始就给关键查询字段加索引避免后期数据上涨后接口越来越慢。哪些字段需要加索引外键字段course_id、user_id、parent_id查询条件字段create_time按时间排序列表、统计近期活跃用户状态字段比如status筛选上架/下架课程唯一字段username、emailMySQL 里创建索引的语句ALTER TABLE comment ADD INDEX idx_course_time (course_id, create_time); ALTER TABLE study_record ADD INDEX idx_user_course (user_id, course_id);课程列表页如果访问量大可以考虑在 Redis 里缓存首页推荐课程和热门课程列表缓存 5 分钟失效。这样数据库的读压力可以大幅降低。用 SpringBoot 的Cacheable注解就能实现但要注意缓存的 key 要和分页参数关联避免不同页的数据互相串。对于学习进度这类写多读少的接口可以考虑异步化处理。前端提交学习记录时后端先把数据写到消息队列比如简单地用CompletableFuture线程池或者 Redis 中间隔一段时间批量落库。但这是进阶优化课程设计阶段不一定需要做到这个程度。7. 文档里不会细讲、但你实际开发会遇到的关键问题复盘这部分算是我自己从零手写类似项目后的复盘挑几个容易让新手产生困惑的点展开说一下。这些问题在项目文档中往往不会写得太细但如果你能提前掌握二次开发和扩展时会顺畅很多。7.1 数据初始化和测试数据的重要性很多系统源码的 SQL 脚本里只有表结构没有测试数据。这会导致你第一次启动项目后页面上空空荡荡不知道该从哪里开始测试。我个人的建议是导入表结构后先手动插入 3-5 门课程、每门课程 3-5 个章节创建一个普通测试用户登录后走一遍完整的选课 → 学习 → 评论流程检查学习进度是否按预期记录这样做的目的是验证核心链路而不是验证每个页面都能打开。如果课程数据都没有你很难判断是前端渲染问题还是后端查询问题。7.2 为什么接口一多Controller 层就会变得很臃肿当课程模块、评论模块、用户模块、测验模块都堆在 Controller 里后你会发现问题开始变得混乱有的方法负责返回页面数据有的方法负责处理表单提交有的方法还要处理文件上传。我在后期扩展时做了一次重构核心原则是一个 Controller 只负责一个业务域CourseController只管课程信息、章节列表、课程发布相关接口CommentController只管评论的增删查和点赞StudyRecordController只管学习进度ExamController只管测验的题目和记录同时把请求参数的校验放到 DTO 层用Validated加NotBlank、NotNull等注解接口方法内不再写冗长的 if 判断。这样代码的可读性和可维护性提升了一个台阶。另一个实用技巧是Controller 的方法命名要统一动词。列表统一叫list详情统一叫detail提交统一叫submit删除统一叫delete。这看起来是小事但当你接口数量超过三十个时统一命名能让你在 IDE 里快速搜索到目标方法。7.3 富文本回显时样式丢失的问题课程内容的富文本回显我遇到过两个问题一是编辑器生成的 HTML 里带了大量的class名但项目里没有引入编辑器的 CSS导致回显时排版完全乱掉二是后端过滤 XSS 时把class属性也过滤掉了导致样式进一步丢失。解决方法是在后端过滤 HTML 时保留常用属性白名单// 伪代码使用 Jsoup 清理 HTML Whitelist whitelist Whitelist.relaxed(); whitelist.addAttributes(img, src, alt, width, height); whitelist.addAttributes(a, href, target); whitelist.addAttributes(p, style); String cleanContent Jsoup.clean(html, whitelist);前端则需要在课程详情页引入编辑器对应的基础样式比如 wangEditor 的内容 CSS。如果编辑器是 Quill前端要引入quill/dist/quill.snow.css。确保回显内容和编辑时看到的效果尽量一致。7.4 用户头像和课程封面的处理建议用户上传头像和课程封面时前端组件需要先压缩图片再上传。否则一张几 MB 的手机照片直接传到服务器既占带宽又拖慢页面加载速度。可以使用compressorjs这样的前端压缩库在用户选择文件后先压缩到合适尺寸头像建议 200x200封面建议 800x450再调用上传接口。后端在接收上传文件时也要做两个校验一是文件大小限制比如最大 5MB二是文件类型校验白名单.jpg, .jpeg, .png, .gif, .webp。不要信任前端传来的文件类型必须读取文件头的 magic number 或用图片处理库二次校验防止有人绕过前端上传可执行文件。7.5 前后端时间格式的坑Java 后端默认序列化LocalDateTime为数组格式前端接受后可能显示成奇怪的对象而不是可读的字符串。解决方案是在application.yml里统一配置时间格式spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT8对于LocalDateTime类型只配置date-format不一定生效还需要单独设置spring: jackson: serialization: write-dates-as-timestamps: false或者给实体类的时间字段加注解JsonFormat(pattern yyyy-MM-dd HH:mm:ss, timezone GMT8) private LocalDateTime createTime;前端展示评论发布时间、学习记录更新时间时也要统一处理。如果后端返回的是字符串时间直接用就行如果返回的是毫秒时间戳前端可以用dayjs格式化为YYYY-MM-DD HH:mm:ss。时间格式不一致的问题我在联调中反反复复遇到过多次提前约定好能省很多事。8. 这套源码可以怎么扩展源码拿到手只是为了跑通真正有意义的是基于它做二次开发。根据这套系统的模块划分我给出几条扩展思路你可以根据自己的需求选择一条深入。8.1 从学习记录到学习分析学习记录表已经存了用户的学习进度但它只做了记录没有做分析。你可以在此基础上增加一个简单的统计接口比如查询某个用户的周学习时长、完成课程门数、平均章节完成率在前端做一个学习仪表盘页面。技术复杂度不高核心是一个聚合查询SELECT course_id, COUNT(DISTINCT chapter_id) AS learned_chapters FROM study_record WHERE user_id #{userId} GROUP BY course_id;配合课程表的章节总数就能算出一组完整的进度和完成率数据。这个功能加进去之后系统从能记录进度升级成了能展示学习成果对用户的粘性会有明显提升。8.2 从评论到实时互动的改造当前的评论表结构已经支持了用户讨论但它是异步的。如果想把互动体验做到实时可以接入 WebSocket。用户在前端页面提问时通过 WebSocket 推送给当前在线的备考老师或管理员管理员收到消息后实时回答形成类似在线答疑的体验。SpringBoot 集成 WebSocket 的成本不高核心是配置一个WebSocketHandler前端通过原生 WebSocket API 或socket.io-client连接。但要注意WebSocket 连接的鉴权方式和 HTTP 不同前端需要在连接 URL 上拼接 Token 或 userId后端在握手阶段校验身份。这部分在现有系统里没有属于扩展功能但思路很清晰。8.3 从单机到容器化部署如果想把项目做得更规范可以编写Dockerfile和docker-compose.yml把前后端和 MySQL 全部容器化。后端的 Dockerfile 大致如下FROM maven:3.8-openjdk-8 AS build COPY . /app WORKDIR /app RUN mvn clean package -DskipTests FROM openjdk:8-jre COPY --frombuild /app/target/study-web.jar /app/study-web.jar WORKDIR /app EXPOSE 8080 ENTRYPOINT [java, -jar, study-web.jar]前端构建后放入 Nginx 镜像MySQL 用官方镜像docker-compose.yml里用depends_on控制启动顺序。如果环境里有 Docker整个系统一条命令就能启动对演示和交付都很方便。8.4 从学习网站到知识付费的小功能如果你想把网站从免费学习变成课程售卖需要扩展的是订单和支付模块。但要注意接入真实支付需要企业资质和商户号个人项目很难申请。课程设计阶段可以做一个模拟支付流程用户选择课程后进入确认页点击模拟支付系统创建一条订单记录订单状态变成已支付然后自动给用户分配课程权限。真实支付接口可以留作扩展接口对接微信支付或支付宝时只需要替换订单状态更新的回调逻辑即可。9. 遇到报错时的排查思路以实际运行环境为例最后这部分我以自己实际运行这套系统时遇到的问题为例讲一下从报错到解决的完整排查链路。这个方法比直接告诉你答案更有价值因为报错可能因环境不同而千奇百怪但排查思路是通用的。9.1 场景一启动时提示数据库连接失败启动 SpringBoot 项目时报错信息通常是Cannot connect to MySQL server on localhost:3306. Connection refused排查链路先用命令行测试 MySQL 是否启动mysql -u root -p能进入就说明服务正常检查application.yml里的数据库名和用户名是不是连了一个不存在的库如果数据库存在但连接失败检查 MySQL 配置文件里的端口看 3306 是否被占用或改成了其他端口检查防火墙Windows 和 Linux 下都要确认 3306 端口对本地连接是放行的有一个比较容易忽略的原因是MySQL8.0 的 root 用户默认只允许 localhost 连接。如果你通过远程工具比如 Navicat连接需要授权CREATE USER study% IDENTIFIED BY password; GRANT ALL PRIVILEGES ON study_web.* TO study%; FLUSH PRIVILEGES;9.2 场景二前端接口请求返回 401前端页面能打开但请求课程列表时返回 401。这个报错的排查要分两段看。先看浏览器控制台。如果请求头里没有Authorization说明前端在发起请求时没有把 Token 塞进请求头问题在前端的request.js拦截器或登录后的 Token 存储逻辑。如果请求头里带了 Token 但还是 401再把 Token 复制出来放到后端的拦截器里调试。检查 Token 是否过期、解析是否正常。一个常见的隐蔽问题是生成 Token 时用的密钥和后端校验时用的密钥不一致或者application.yml里的jwt.secret配置在部署时被改动了导致同一个 Token 在本地能用、在服务器上用不了。9.3 场景三评论功能能提交但页面不显示这是典型的 数据写入成功但查询失败 或 数据写入成功但前端渲染失败 的问题。排查步骤用数据库工具直接查询评论表看数据是否真实写入如果数据存在再请求评论列表接口看返回数据里是否包含刚才的评论如果接口返回了数据检查前端渲染逻辑。常见问题是列表数据存在但字段名对不上比如后端返回nickName前端却显示nickname这类问题用浏览器 DevTools 的 Network 面板能快速定位。看到接口返回的 JSON 结构再对照前端页面的绑定字段很快就能发现是哪一层的数据映射出了问题。9.4 场景四本地能访问部署到服务器后图片加载失败本地开发时图片上传到本地目录能通过/files/**访问。部署到服务器后图片上传成功但访问时 404 或 403。原因通常是后端静态资源映射配置的是相对路径而服务器上的工作目录和本地不一致。解决方式是把上传目录改成绝对路径并在配置文件里统一维护file: upload-dir: /data/study-web/upload然后在 Nginx 里也加一条静态资源映射location /files/ { alias /data/study-web/upload/; }这样图片请求直接由 Nginx 处理不再经过 Java 应用响应速度更快。注意alias后面的路径要以/结尾否则文件路径拼接会出错。10. 最后的落地建议整套系统覆盖了从前端交互、后端接口、数据库设计、权限控制到部署上线的核心环节。如果你正在用这份源码做课程设计或毕业设计我建议你不要只停留在能运行而是花时间做三件事第一把数据库表的字段逐个过一遍搞清楚每一张表、每一个字段存在的意义。面试官或答辩老师最喜欢问的问题就是为什么这张表要有这个字段能答上来才说明你真的理解了系统。第二把核心业务链路画成文字流程比如用户登录后如何访问课程→如何记录进度→如何提交评论→如何查看自己的学习数据。按照这个链路把对应的接口代码逐行读懂把关键方法的调用关系捋清楚。第三尝试自己加一个小功能不加复杂的哪怕是给课程表加一个标签字段然后在课程列表页按标签筛选。这个过程中你会完整经历改表结构、改后端代码、改前端代码、重新部署的整个流程比反复看十遍源码都管用。我自己的体会是这类项目的难点从来不在某个单一技术上而在模块之间的联系和状态流转。只要你能把互动学习这条主线的数据流走通其他问题都是局部问题遇到一个解决一个就行。希望这篇拆解能让你少走一些弯路也欢迎你在评论区聊聊你在部署和二次开发中遇到的具体问题我看到后会给出对应的解决思路。
返回列表