多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

基于SpringBoot2与Vue3的课程答疑系统前后端分离实战

基于SpringBoot2与Vue3的课程答疑系统前后端分离实战 做课程答疑系统这件事听起来不算新鲜但真要把它做扎实技术点一点都不少。我最近完成的一套 Java Web 课程答疑系统源码选型是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0前后端分离带完整文档。这套东西不是简单跑通 demo 就完事而是把登录鉴权、课程管理、提问回答、问答检索、后台审核这些真实业务都落了地。如果你正在做 Java Web 毕业设计或者想快速构建一个前后端分离的管理类系统那这篇博客值得你从头看到尾——里面涉及的技术选型逻辑、数据库设计思路、前后端联调细节都是实际踩坑换来的。1. 课程答疑系统为什么这样搭需求拆解与技术选型1.1 课程答疑的核心需求到底是什么很多初学者一听到“答疑系统”第一反应就是“做一个问答页面”。但真去拆需求你会发现事情远不止这么简单。一个能实际用的课程答疑系统至少要覆盖四类角色和五条核心业务线。角色可以分成学生、教师、管理员有的系统还会加一个“助教”角色。学生要能浏览课程、发问题、看别人的回答、采纳最佳答案老师要能创建课程、回答问题、管理自己课程下的内容管理员要负责用户审核、问题审核、数据统计。五条核心业务线则是认证与授权、课程管理、问题发布与检索、回答与采纳、后台运营管理。别看系统不大这几个业务线互相咬合得很紧。比如问题发布必须关联课程和用户回答采纳要更新问题状态后台审核又要跨表查询。如果一开始不把需求拆清楚后面写代码很容易出现“改一个功能动三个表”的情况。我这次做的时候先把用例图画出来再把每个用例对应的表结构列出来最后才动代码这样整个项目推进非常顺。1.2 技术栈选型SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0这套系统最终选型是 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0。我知道有人会问Spring Boot 3 都出这么久了为什么还用 2原因很简单稳定性和生态成熟度。SpringBoot2 经过大量生产环境验证文档、视频、答疑资料一搜一大把出任何问题都能快速找到解决方案。而很多学校机房、企业内网环境还在用 JDK8SpringBoot3 强制要求 JDK17直接把一大半环境卡死了。所以对于课程答疑系统这类偏教学、偏毕业设计、偏中小型项目的场景SpringBoot2 反而是最稳妥的答案。Vue3 的选择则基于两个判断。一是组合式 API 让组件逻辑复用变得非常清爽一个问题列表组件、一个回答框组件逻辑抽出来之后代码量少了很多二是 Vue3 生态已经非常成熟Element Plus、Pinia、Vue Router 4 这些配套库都稳定了不像刚发布时那样到处踩坑。至于 MyBatis-Plus它最大的价值是把单表 CRUD 彻底简化分页插件也做得很好我项目里所有基础查询几乎不需要手写 SQL速度提升非常明显。MySQL8.0 则是数据库层面的基本盘窗口函数、CTE、JSON 字段这些功能对答疑场景的高级查询很实用。1.3 为什么不用更“新”的版本顺带提一个很多人在选型时容易犯的毛病一味追求最新。我之前见过有人为了用 SpringBoot3把整台开发机的 JDK 从 8 升到 17然后发现公司内部封装的公共组件全部编译不过最后又退回 2。做项目最重要的是“在限定条件下选出最优解”而不是“选理论上的最前沿”。课程答疑系统的核心诉求是快速交付、稳定运行、方便二次开发。SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 这四个组合恰好都是各自领域里的“中坚力量”不激进、资料多、组件全、坑都被人踩过了。说实话这套组合就算放到实际生产环境里做一个内部知识库系统也完全扛得住。选型这件事千万别为了炫技给自己挖坑。2. 数据库与后端设计从表结构到接口实现2.1 数据库表设计用户、课程、问题、回答四张主表答疑系统的核心表不算多但设计上需要照顾到几个关键点。我先列一下主体结构再逐个讲为什么这样设计。user 表id、username、password、real_name、role、avatar、status、create_timecourse 表id、course_name、teacher_id、description、create_timequestion 表id、user_id、course_id、title、content、status、view_count、answer_count、create_timeanswer 表id、question_id、user_id、content、is_accepted、create_time用户表里有个细节password 字段我存的是 BCrypt 加密后的密文而不是明文。很多新手为了调试方便把密码直接存明文这放在学习项目里勉强能接受但只要你打算把项目展示到简历上这条就是硬伤。课程表里的 teacher_id 关联 user 表通过 role 字段区分老师和学生比单独建一张“教师表”要简洁得多。问题表的 status 字段特别关键我用 0、1、2 三个值表示待审核、已发布、已关闭。为什么要加审核状态因为答疑系统一旦开放注册就会有大量垃圾内容后台不审核根本控制不住。回答表里的 is_accepted 是采纳标记一道题只能有一个最佳回答这个逻辑不光前端要限制后端在更新 is_accepted 时也必须加事务和唯一性校验。还有一个容易忽略的设计点冗余字段。比如 question 表里有 answer_count这个值本可以通过 count 查询拿到但为了列表页不用每次连表统计我直接把它冗余进去。数据量大的时候这种小设计能显著减少查询压力。当然冗余也有代价每次新增、删除回答都要记得更新这个字段我在项目里是通过事务保证一致性的。2.2 MyBatis-Plus 的便捷与坑点MyBatis-Plus 我用下来最大的感受是单表 CRUD 真的一行 SQL 都不用写。继承 BaseMapper 接口基础的增删改查、条件查询、分页查询直接调用方法就可以了。比如按课程 ID 查问题列表用 LambdaQueryWrapper 写出来非常优雅LambdaQueryWrapperQuestion wrapper new LambdaQueryWrapper(); wrapper.eq(Question::getCourseId, courseId) .eq(Question::getStatus, 1) .orderByDesc(Question::getCreateTime); PageQuestion page questionMapper.selectPage(new Page(current, size), wrapper);分页插件配置也很简单在配置类里注入 MybatisPlusInterceptor添加 PaginationInnerInterceptor。我这里要提醒一下新版 MyBatis-Plus 的分页拦截器构造要指定数据库类型否则分页 SQL 可能生成不对。我之前在 3.5.x 版本里就遇到过这个问题后来补上 DbType.MYSQL 就正常了。但 MyBatis-Plus 也不是没有坑。最大的坑是“过度依赖它遇到复杂查询就懵了”。我这次系统里有几个报表类统计比如“每个课程下问题数量分布”“每天新增问题趋势”这种多表关联聚合用 MP 的 QueryWrapper 硬写会很别扭。我的做法是保留 XML 文件手写几个自定义 SQL用 Select 注解或者 XML 映射都可以。手写 SQL 的时候要注意MP 的逻辑删除、自动填充在自定义 SQL 里不会自动生效需要自己在 SQL 里判断 deleted 字段和手动填充 create_time。2.3 后端接口设计要点鉴权、分页、事务接口设计上我采用了 RESTful 风格统一以 /api 开头版本号直接放在路径里比如 /api/v1/question/page。为什么加版本号因为项目后期扩展接口时很可能有老接口不能改、只能新增新接口的情况版本号可以让你无痛兼容。鉴权我用的是 Spring Security JWT。登录成功后签发一个 token前端把 token 存在 localStorage每次请求放到 Authorization 头里。后端通过 OncePerRequestFilter 过滤器统一解析 token把用户信息放进 ThreadLocal 或者 SecurityContext。这里有个经验JWT 的过期时间不要设太长我一般设成 24 小时前端在 401 响应时自动跳转登录页重新获取 token。分页参数我统一封装成 PageParam 和 PageResult前端传 current、size、keyword、courseId 这些参数后端返回 total、records 等数据。事务这块主要用在“新增回答 更新问题 answer_count 处理采纳状态”这个组合操作上直接用 Transactional 注解就能保证原子性。凡是涉及多表更新的操作我强烈建议都加上事务不然并发高的时候数据很容易不一致。3. 前端 Vue3 实战环境搭建与核心模块实现3.1 Vue3 项目初始化和环境配置Vue3 前端我用的是 Vite 构建工具创建项目命令很简单npm create vitelatest question-web -- --template vue cd question-web npm install npm install vue-router4 pinia element-plus axios sass npm run dev这里有个很容易踩的坑如果你创建完项目之后访问页面是空白的大概率是 Vite 5 Node 版本不匹配。Vite 5 要求 Node 18 以上很多人的机器还是 Node 16运行报错信息也不够直观花很长时间才排查出来。我现在的建议是用 Node 20 LTS 版本配 Vite 5 或者 Vite 6 都非常稳。环境配置上我做了几件事vite.config.js 设置 别名指向 src 目录方便引用组件和工具模块配了 server.proxy 把 /api 请求代理到后端 8080 端口解决开发环境跨域问题。要注意代理配置不能写死 target 为 localhost我用环境变量区分本地和测试环境不然换一台机器部署又得改代码。还有一个细节Element Plus 组件库。为了减小打包体积我用 unplugin-auto-import 和 unplugin-vue-components 两个插件实现按需自动导入Element Plus 的组件和 API 用的时候直接写不用手动 import构建后体积小很多。很多初学者为了省事直接全量引入页面多了之后性能会明显变差。3.2 登录、答疑、后台管理页面的组件拆分前端页面上我做了登录注册页、首页导航、课程列表页、问题列表页、问题详情页带回答列表和提交回答框、个人中心、后台管理页这几块。组件拆分原则很简单一个页面一个 View公共部分抽成 Components状态统一放 Pinia。登录页的校验我用的是 Element Plus 表单校验用户名和密码必填长度 4-20 位。这里我想特别提一下 Vue3 里表单校验的一个常见问题on-success 事件监听不到。很多人给 upload 组件设置了 on-success但是上传成功后事件一直不触发。这个问题的根源通常是组件版本不匹配或者写法上用了老 Vue2 的 options API 写法。Vue3 Element Plus 里 on-success 是直接写在组件属性上的回调而且要注意 bind 的 this 上下文最好用箭头函数否则函数内部访问不到 Vue 实例的 data 和 method。答疑核心页面是问题详情页。我拆成了 QuestionInfo 组件、AnswerList 组件、AnswerEditor 组件三个部分。AnswerEditor 用的是 markdown 编辑器我选了 wangeditor 的 Vue3 版本因为它的中文文档比较友好支持图片上传和公式编辑对教学场景很合适。回答提交之后通过 pinia 里的 state 触发 AnswerList 刷新这样就不用手动刷新整个页面体验好很多。后台管理页面我用的是典型的侧边栏 顶栏 内容区布局。侧边栏菜单通过路由表动态生成管理员才有权限看到“用户管理”“问题审核”“课程管理”这几个菜单入口。这里有个小技巧路由 meta 里放 permission 字段前端路由守卫统一判断比在页面里一个一个写 v-if 要方便得多。3.3 前后端联调跨域、Axios 封装、路由守卫前后端联调是整个项目最琐碎但最关键的环节。Axios 我封装了一个 request.js统一设置 baseURL请求拦截器里自动带上 token响应拦截器统一处理业务错误码和 401 状态。错误消息用 ElMessage 弹出后端返回的错误码对应不同的提示文案。跨域问题除了刚才说的 dev 环境的 vite proxy生产环境还要处理一下。我这里生产环境是把前端打包后的 dist 目录直接放到 SpringBoot 的 static 目录下由同一个后端提供服务这样根本不存在跨域。虽然这种做法看起来有点“土”但对于小型项目和毕业设计来说非常可靠部署成本最低物理上消灭了跨域问题。路由守卫我很建议做成两种beforeEach 里做登录状态校验白名单页面登录页、注册页直接放行其他页面如果没 token 就重定向到登录页同时用 afterEach 设置页面标题 title这样浏览器标签页上能看到当前所在页面体验比全部叫“课程答疑系统”要好得多。我在联调阶段最深的体会是前端的各种“怪问题”很大程度上都是几个固定原因——token 没带上、参数名对不上、跨域被拦、路由路径不匹配。只要围绕这四类去排查问题通常能很快定位不要一开始就怀疑代码逻辑复杂。4. MySQL8.0 环境准备安装、配置与常见坑4.1 Windows 和 Linux 下 MySQL8.0 安装要点MySQL8.0 的安装教程网上非常多我只挑重点并且是容易出问题的部分说。Windows 下推荐用解压版zip 包因为安装版装完经常出现服务无法启动的问题。解压之后需要手动配置 my.ini 文件[mysqld] basedirD:/mysql-8.0.36-winx64 datadirD:/mysql-8.0.36-winx64/data port3306 character-set-serverutf8mb4 default-authentication-pluginmysql_native_password然后以管理员身份打开 cmd执行mysqld --initialize-insecure初始化数据目录再执行mysqld --install MySQL8安装服务最后net start mysql启动服务就可以了。初始化时加上--initialize-insecure是为了让 root 用户初始密码为空方便第一次登录后自己修改。如果你用--initialize会自动生成一个随机密码找密码的过程在 Windows 上很容易搞崩心态。Linux 下用 CentOS 或者 Ubuntu 的包管理器安装就比较省心。Ubuntu 直接apt install mysql-server-8.0CentOS 需要先下载 MySQL 官方 yum 仓库再安装。装完之后一定要跑一下mysql_secure_installation把匿名用户、默认数据库、远程 root 这些风险项目都处理掉。我个人比较推荐的其实是 Docker 安装因为隔离性最好不同项目可以用不同 MySQL 版本而不互相干扰。一条命令就能搞定docker run -d --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v /opt/mysql8/data:/var/lib/mysql \ -v /opt/mysql8/conf:/etc/mysql/conf.d \ mysql:8.0数据目录和配置目录挂载出来后续升级容器、备份数据都非常方便。我项目里本地开发用的就是 Docker 跑的 MySQL8.0需要切换到别的项目时直接停掉容器就行一点不污染系统环境。4.2 连接 MySQL8.0 的驱动和 URL 配置不管是用 Navicat 还是用 JDBC 连接 MySQL8.0最容易遇到的一个问题就是报错Public Key Retrieval is not allowed。这个错误的原因是 MySQL8.0 默认认证插件 caching_sha2_password 在第一次连接时需要从服务端获取 RSA 公钥。解决办法有两个第一个是在 JDBC URL 上加上参数jdbc:mysql://localhost:3306/question_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue第二个办法是创建用户时指定 mysql_native_password 插件CREATE USER question% IDENTIFIED WITH mysql_native_password BY password123;我推荐两个一起用兼容性最好。因为如果你用的是 SpringBoot2 自带的 mysql-connector-java 8.0.x对 caching_sha2_password 的支持已经没问题了但连接池里跑一些老版本 JDBC 还是会出错。另外时区问题也是个高频坑。MySQL8.0 的驱动要求 serverTimezone 必须设置否则会报The server time zone value 乱码 is unrecognized。我统一在容器环境变量里设置了 TZAsia/ShanghaiJDBC URL 里也带上了 serverTimezoneAsia/Shanghai双保险。4.3 中文乱码、连接数、大小写敏感等典型配置中文乱码问题核心就一句话客户端、连接、数据库三层字符集要统一成 utf8mb4。我在初始化数据库时固定这样建库CREATE DATABASE question_db DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;同时 my.ini 里设置 character-set-serverutf8mb4。只要建库时用了 utf8mb4表默认都会继承基本不会出现乱码。如果插入之后发现还是乱码检查一下连接参数 characterEncoding 有没有设置为 utf8。连接数配置也要提一下默认 max_connections 是 151对于答疑系统这种中小型项目完全够用但如果你把 MySQL 和 SpringBoot 都跑在一台 2G 内存的云服务器上连接池最大连接数不要设太高SpringBoot 的 HikariCP 我一般设置 maximum-pool-size10否则连接数一多内存和 CPU 都扛不住。最后一个容易踩的坑是表名大小写敏感。MySQL8.0 在 Linux 下默认表名区分大小写Windows 下默认不区分。同一个项目在 Windows 开发、Linux 部署就会出现“本地没问题、线上报 table not exist”的奇葩问题。解决办法是在 my.ini 的 [mysqld] 段加一行lower_case_table_names1让表名统一不区分大小写。注意这个参数必须在数据库初始化之前设置才生效已经创建完数据后再改是没有用的。5. 常见问题与排查技巧实录5.1 Vue3 项目里的几个高频谜之问题开发 Vue3 项目过程中我记了一堆排查记录下面这几个都是真实遇到并且高频发生的。第一个是登录后页面不跳转。这种情况我先查路由实例是否是通过 createRouter 创建的Vue3 里必须用router.push(/home)跳转而不是像 Vue2 那样直接修改this.$route。另外还要确认 js 文件里 import router 的路径没有问题一个容易犯的错是直接在 main.js 里把 router 写成了 new RouterVue3 里这是过时 API还是没有跳转效果。第二个是上一个用户提到的 on-success 监听不到。Element Plus 的 el-upload 组件on-success 回调如果不触发先检查组件库版本是不是 2.x然后确认书写格式。正确写法应该是el-upload :on-successhandleUploadSuccess/el-upload不能在事件名里加符号因为它不是 DOM 事件是组件透传出来的属性。如果你是 options API 写法还要注意 handleUploadSuccess 里如果用到 this最好改成箭头函数否则 this 指向不对。第三个是 Vite 项目局域网打开白屏。开发环境下手机想访问电脑上的页面默认 Vite 只绑定 localhost所以需要在 vite.config.js 里配置server: { host: true }这样 Vite 会监听 0.0.0.0局域网其他设备就能通过你的局域网 IP 访问了。但如果设置了 host: true 之后还是白屏检查一下是不是代理 target 写成了 127.0.0.1改成局域网 IP 或者后端实际地址就行。这四个问题我在社区里看到的频率非常高尤其是刚入手 Vue3 的开发者几乎每个人都会撞上其中一两个。5.2 SpringBoot2 与 MyBatis-Plus 联调时的排查思路SpringBoot2 起不起来多半先看依赖冲突和端口占用。这里有个非常典型的错误SpringBoot2 MyBatis-Plus 时如果同时引入了spring-boot-starter-web和spring-boot-starter-webflux会启动失败因为两者争抢 Tomcat 端口。这个问题通常出现在接手别人的代码时解决方法是删掉 webflux 依赖。还有一个 MyBatis-Plus 的坑是逻辑删除字段加了 TableLogic 之后手写 SQL 查不到数据。因为这个查询条件 MP 不会自动拼上去需要你在 SQL 里自己写deleted 0。排查思路很简单先去掉 TableLogic 试一下如果好了八成就是逻辑删除没有在自定义 SQL 里处理。至于“页面请求卡顿 数据库连接超时”这种组合第一反应不是查代码而是查连接池。我用 HikariCP 默认配置时连接空闲超时时间是 10 分钟数据库的 wait_timeout 是 8 小时如果代码里用了长连接跨过空闲超时后连接就失效了。解决办法是把 HikariCP 的 max-lifetime 设置成小于数据库 wait_timeout 的值比如 60 秒连接一旦过期就重建而不是复用。5.3 从答疑系统延伸出去若依、商城、Three.js 等衍生场景这套答疑系统的技术底座稍加改动就可以演变成非常多的项目。比如现在很多人问“若依 vue3 ts 报错”怎么处理本质上就是我前面说的 Vue3 TS 环境配置问题。若依框架的路由是通过动态路由生成的Vue3 TypeScript 下报错很大概率是类型声明没配好或者是 router.beforeEach 里的类型断言不对。排查思路是保证 tsconfig.json 里 moduleResolution 设置为 bundler并且把types数组加上vite/client。还有问 vue3 商城、vue3 后台管理系统精简框架怎么做的往答疑系统里加几个模块就是了。商城无非就是多了商品表、订单表、购物车表后台管理则把答疑系统里的用户管理、角色管理和权限管理扩一扩就能变成一个通用中后台脚手架。我之前用这套模板做一个打印模板组件就是把 el-table 的列表数据转成特定 HTML 格式再调用 window.print()再把答疑系统里的头像上传逻辑改成图片签名和商品图上传改动量非常小。至于更复杂的场景比如 Vue3 Three.js TypeScript 做机房可视化这种需求和答疑系统之间的重叠部分就是项目骨架登录、路由、状态管理、Axios 封装、后端接口规范全部可以直接复用只需要把中间的 CRUD 页面换成 Three.js 场景渲染。换句话说答疑系统这类“管理后台 内容交互”的项目天生适合作为其他系统改造的起点。6. 这套系统的可扩展方向与我的个人体会前面把搭建细节都讲完了最后聊两句实在话。很多人拿到一个项目源码后第一反应是“跑起来”但我觉得更重要的第二步是“换一个需求再改一遍”。比如把课程答疑系统改成内部知识库、把问答改成用户反馈工单、把课程列表改成租房信息列表你会发现所有底层模块都是可以迁移的这正是我选择 SpringBoot2 Vue3 MyBatis-Plus MySQL8.0 这套组合的原因——它不是一个只能应付一次的作业而是一个能反复使用的脚手架。回到 MySQL8.0 的部署话题上我再特别强调一次环境问题占了答疑系统包括其他所有项目实际开发时间的三成以上。Docker 跑 MySQL8.0、Windows 解压版 MySQL8.0、Linux 包管理器安装 MySQL8.0三种方式我都实测过生产环境用 Docker 最省心本地开发看习惯。如果只是写代码做功能哪个顺手用哪个但如果你需要移植给同学、老师、同事复现那么把数据库装进 Docker 并且把所有配置写进 docker-compose 是降低“我明明跑起来了为什么你跑不起来”这类问题的最好办法。最后再分享一个我在项目交付时的小技巧除了给源码一定要给一份环境清单和启动顺序说明。很多项目“代码没问题但跑不起来”卡在数据库版本不一致、Node 版本不一致、JDK 没配环境变量。我会在 README 里直接写清楚 JDK 用 8 还是 11、Node 用 20、MySQL8.0 的 root 密码和库名、前端 npm run dev 之前要改哪个常量。这些看似琐碎的细节往往决定了别人拿到你的代码后是第一眼跑通还是第一眼放弃。答疑系统本身不算复杂但把交付体验做好才是一个有经验的开发者跟刚写完代码的新手最大的区别。
返回列表