ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SpringBoot+Vue3+MyBatis-Plus在线考试系统设计与全栈实现解析

SpringBoot+Vue3+MyBatis-Plus在线考试系统设计与全栈实现解析 最近整理了一套“Java Web 语言在线考试与学习交流网页平台系统”基于SpringBoot2 Vue3 MyBatis-Plus MySQL8.0这套全栈技术栈来做的分为前后端分离架构并附带了完整的项目文档和部署说明。整套代码是从零开始搭建的不是那种只丢一堆散装代码的教学demo而是可以从数据库设计一路跑到线上部署的真实项目。这篇文章把整个系统的核心设计思路、关键实现步骤、踩过的坑和排查过程一次性讲清楚适合正在做毕业设计、想系统学习前后端分离实战的Java学习者也适合想快速搭一套带考试功能平台的技术朋友做参考。1. 项目价值与整体设计思路1.1 在线考试平台到底要解决什么问题这类“语言在线考试与学习交流平台”本质上是一套面向教育场景的业务系统。拆开看其实就两块核心业务一块是“考试”另一块是“学习交流”。先说“考试”这块。传统线下考试最大的痛点在于组卷难、监考累、改卷慢学生试卷发下去之后老师要花大量时间批改客观题、统计分数。在线考试系统要解决的核心问题就是把“出题-组卷-答题-交卷-自动判分-成绩统计”这条链路全部线上化。其中客观题单选、多选、判断可以完全交给程序自动判定每道题预设正确答案和分数系统在学生提交试卷后就可以立刻算出总分这个过程用MyBatis-Plus做数据操作非常顺手因为整个判分逻辑的核心就是遍历试题集合、比较答案、累加得分。再说“学习交流”这块。考试并不是学习的终点考完之后师生之间、同学之间需要交流互动。很多初学者容易把交流模块做成一个复杂的聊天系统其实没必要。这里实际采用的是社区论坛模式包括用户发布学习心得、提问、回复、查看热门帖子等功能。这样设计的好处是一是逻辑简单帖子、评论各两张表就可以搞定二是和考试业务可以天然打通比如考完之后系统可以根据成绩推送相关的学习资料或讨论区热门话题。技术上前端用Vue3做单页应用SPA后端用SpringBoot2提供RESTful API数据库用MySQL8.0。前后端通过JSON格式进行数据交互登录认证采用JWT方案无状态、易扩展。整套架构非常标准跑起来之后结构清晰也不容易出现那种前后端搅在一起、改一行代码崩一片的尴尬局面。1.2 为什么选SpringBoot2 Vue3这套组合很多人在选型时会在SpringBoot2还是SpringBoot3之间纠结。作为一个面向真实应用场景的项目选SpringBoot2.7.x是一个务实的选择。SpringBoot3要求JDK17起步虽然性能更强但很多老牌第三方库和插件对它的兼容还不够完善。而大多数教学环境、企业现有的技术栈还停留在JDK8或JDK11上SpringBoot2.7.x对JDK8的支持非常友好同时可以无缝升级到JDK11。另外MyBatis-Plus目前对SpringBoot2的支持最稳定文档和社区解决方案也最丰富遇到问题随便一搜就有答案。前端选择Vue3而不是Vue2最主要的原因是Vue3已经是大趋势组合式APIComposition API加上script setup语法糖让组件代码的可维护性大幅提升。很多同学可能刚接触Vue3时会被ref、reactive、computed这套响应式API搞晕但其实核心就是记住一句话ref用来声明基本类型或对象reactive用来声明复杂嵌套对象computed用来声明依赖其他响应式数据的派生状态。在实际项目里我写的表单校验、分页查询这些业务场景用起来比Vue2的选项式API顺手很多。数据库选MySQL8.0则是最稳妥的选择性能、稳定性、窗口函数支持都比5.7强不少。8.0中默认的认证插件改成了caching_sha2_password这会让一些旧版的连接工具报错后面第5章排查部分会详细说。1.3 数据库设计与表结构规划数据库设计是这个系统最值得花时间琢磨的部分。模块一是用户与角色模块、模块二是题库与试卷模块、模块三是考试记录模块、模块四是帖子与评论模块。这里把主要表结构列出来表名用途核心字段sys_user用户表id、username、password、nickname、role_id、statussys_role角色表id、role_name、role_codeexam_question试题表id、question_type、content、options、answer、score、difficulty、subjectexam_paper试卷表id、paper_name、total_score、duration、create_timeexam_paper_question试卷-试题关联表id、paper_id、question_id、question_scoreexam_record考试记录表id、user_id、paper_id、score、start_time、submit_timeexam_record_detail答题明细表id、record_id、question_id、user_answer、is_correctforum_post帖子表id、user_id、title、content、category、view_count、reply_countforum_comment评论表id、post_id、user_id、content、create_time这里重点提一下exam_paper_question这张关联表。很多初学者设计“试卷-试题”关系时会犯一个错误直接在试卷表里用一个varchar字段存试题ID列表比如1,2,3,4这种做法虽然省事但想要实现“查询某试卷下所有试题”这种功能时就非常痛苦还得在代码里拆分字符串再组装对象。采用关联表的设计通过join查询就能完美解决这才是关系型数据库该有的样子。删除策略上也有一点经验用户和角色、试题和试卷之间是存在引用关系的物理删除容易引发外键约束报错或历史数据错乱。系统里这里统一使用逻辑删除即在表结构设计时预留一个deleted字段MyBatis-Plus的TableLogic注解可以自动实现逻辑删除功能。这样所有delete操作实际上变成了update操作数据不会真正消失查历史数据、恢复误删记录都非常方便。2. 后端核心实现SpringBoot2 MyBatis-Plus MySQL8.0环境搭建2.1 SpringBoot2项目结构与启动类配置项目采用标准的Maven多模块或单模块目录结构这里实现时采用的单模块便于学习和部署核心目录结构如下src/main/java ├── com.example.exam │ ├── ExamApplication.java │ ├── config │ │ ├── MybatisPlusConfig.java │ │ ├── WebConfig.java │ │ └── JwtInterceptor.java │ ├── controller │ ├── service │ ├── mapper │ ├── entity │ ├── dto │ └── common │ ├── Result.java │ ├── ResultCode.java │ └── exception src/main/resources ├── application.yml └── mapper启动类ExamApplication.java只需要加上SpringBootApplication和MapperScan两个注解即可。我的习惯是扫描所有mapper接口所在的包这样就不需要在每个Mapper接口上单独加Mapper注解代码更干净。application.yml中需要配置的关键项包括数据源、MyBatis-Plus、JWT密钥等。这里特别提醒一下配置文件里的密码这类敏感信息不要硬编码成别人一眼就能看懂的明文本地开发怎么方便怎么来但部署到服务器时至少要养成环境变量引用的习惯比如password: ${MYSQL_PASSWORD:root}这样。2.2 MyBatis-Plus的关键配置与使用套路MyBatis-Plus真正强大之处在于单表CRUD几乎不用写SQL。继承BaseMapperT之后直接就有selectById、selectList、selectPage这些方法配合LambdaQueryWrapper或LambdaUpdateWrapper来做条件构造代码非常优雅。来看一段实际的分页查询代码GetMapping(/page) public ResultIPageExamPaperVO page(RequestParam(defaultValue 1) Integer pageNum, RequestParam(defaultValue 10) Integer pageSize, RequestParam(required false) String paperName) { PageExamPaper page new Page(pageNum, pageSize); LambdaQueryWrapperExamPaper wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.hasText(paperName), ExamPaper::getPaperName, paperName) .orderByDesc(ExamPaper::getCreateTime); IPageExamPaper paperPage examPaperMapper.selectPage(page, wrapper); // 组装VO返回略去转换逻辑 return Result.success(voPage); }注意这段代码里分页插件必须在MybatisPlusConfig中配置否则selectPage查出来的数据不会真正分页而是全量查出后在内存里假分页。这是一个非常常见的坑后面排查章节会展开说。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }MyBatis-Plus和Spring Data JPA的对比也是很多同学纠结的点。简单说JPA更适合实体关系特别复杂、喜欢用对象导航查询的团队MyBatis-Plus则更适合SQL优化能力强、业务查询负责、想要精确控制SQL的团队。SQL优化这层上MyBatis-Plus可以通过自定义XML轻松写出复杂join查询而JPA做复杂查询往往需要写JPQL或者原生SQL门槛反而更高。2.3 MySQL8.0配置细节与连接实践MySQL8.0的Maven坐标已经改为com.mysql:mysql-connector-j不要在依赖里写mysql:mysql-connector-java这样过时的坐标。SpringBoot2.7.x对应的驱动版本建议用8.0.33兼容性测试过很稳定。数据源配置如下spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/exam_platform?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码这里面serverTimezoneAsia/Shanghai和allowPublicKeyRetrievaltrue两个参数非常关键。前者解决时区相差8小时的问题后者解决MySQL8.0的caching_sha2_password认证插件连接时报Public Key Retrieval is not allowed的问题。MySQL8.0在Linux和Windows上的安装方式有一些差异这里不展开但建议统一指定utf8mb4字符集因为它完整支持emoji和生僻字。建库语句通常是CREATE DATABASE IF NOT EXISTS exam_platform DEFAULT CHARACTER SET utf8mb4 DEFAULT COLLATE utf8mb4_general_ci;3. 前端核心实现Vue3项目搭建与关键模块开发3.1 Vue3项目创建与目录结构设计前端采用Vite作为构建工具。Vite创建项目的命令很简洁npm create vitelatest exam-web -- --template vue cd exam-web npm install目录结构按照实际业务来规划避免所有组件堆在一起src ├── api // 接口请求封装 │ ├── auth.js │ ├── exam.js │ └── forum.js ├── assets ├── components // 公共组件 ├── router // 路由配置 │ └── index.js ├── stores // Pinia状态管理 │ └── user.js ├── views │ ├── login │ ├── exam │ ├── forum │ └── admin ├── utils │ └── request.js // Axios封装 └── App.vueVue3开发中最大的变化是组合式API。以登录页为例用script setup语法和Pinia的store来组织代码清晰度明显高于Vue2的data methods模式script setup import { ref } from vue import { useRouter } from vue-router import { useUserStore } from ../stores/user const username ref() const password ref() const router useRouter() const userStore useUserStore() const handleLogin async () { await userStore.login(username.value, password.value) router.push(/dashboard) } /script有同学问Vue2和Vue3到底有哪些具体区别这里挑几个最直观的Vue3的响应式底层由Object.defineProperty换成了Proxy可以监听属性新增和删除Vue3的模板支持多个根节点Vue3的Composition API让逻辑复用从mixin变成了自定义Hook。这些差异在做这个项目时都会感受到。3.2 Axios请求封装与跨域处理前后端分离项目里Axios封装是必不可少的一步。统一封装的核心目的有三个统一设置baseURL、统一携带token、统一处理错误响应。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) 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 }, 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开发环境的跨域问题通过Vite代理解决在vite.config.js中配置server: { port: 3000, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样做的好处是浏览器请求的URL始终是/api/xxx不会出现跨域报错。前后端联调时很多问题都是跨域引起的用代理的方式可以完全规避生产环境再用Nginx配置一个反向代理就行。3.3 前端几个核心页面实战要点考试页面是这个项目最复杂的前端页面难点在于倒计时和交卷逻辑。倒计时使用setInterval实现但需要在组件卸载时清除定时器否则会出现内存泄漏let timer null const countdown ref(0) const startExam () { countdown.value examDuration * 60 timer setInterval(() { countdown.value-- if (countdown.value 0) { clearInterval(timer) submitPaper() } }, 1000) } onBeforeUnmount(() { if (timer) clearInterval(timer) })这里我实际踩过一个坑如果用户在考试中途刷新了页面倒计时会重置。解决方案是点击“开始考试”时后端返回截止时间戳前端用Date.now()实时计算剩余时间即使页面刷新也能从后端拿回到期的截止时间。学习交流区的核心是帖子列表和评论渲染。帖子列表用el-table或卡片列表都行评论用递归组件。不过项目里做了简化帖子下面只做了一层评论没有嵌套多级评论因为二级评论对SQL的设计要求会高很多为了控制项目复杂度先用一级评论结构效果已经足够。4. 核心业务模块的实操过程与关键实现4.1 在线考试模块随机组卷、计时交卷与自动判分考试模块是整套系统的核心这里拆解成三个关键环节随机组卷、考试计时、自动判分。随机组卷的算法思路用户教师选择试题分类、难度、题型和数量系统从题库中随机抽取符合条件的试题。MyBatis-Plus中可以使用last(ORDER BY RAND() LIMIT ?)来快速实现随机抽取例如public ListExamQuestion selectRandomQuestions(String subject, Integer difficulty, String type, Integer limit) { LambdaQueryWrapperExamQuestion wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(subject), ExamQuestion::getSubject, subject) .eq(difficulty ! null, ExamQuestion::getDifficulty, difficulty) .eq(StringUtils.hasText(type), ExamQuestion::getType, type) .last(ORDER BY RAND() LIMIT limit); return examQuestionMapper.selectList(wrapper); }这套写法的好处是SQL很简单对于题库量在几千道这个量级完全没有性能问题。如果题库到了十万级再考虑方案调整比如先固定随机种子再取数或者用雪花ID取模来做随机抽签。大多教学场景和中小型考试ORDER BY RAND()完全够用。自动判分的核心逻辑其实不复杂关键是答题明细的保存。学生交卷时系统遍历每道题将用户的答案与标准答案比对逐题判分后汇总总分Transactional public ExamResultVO submitExam(SubmitExamDTO dto) { ExamPaper paper examPaperMapper.selectById(dto.getPaperId()); ListExamPaperQuestion pqList paperQuestionMapper.selectList( new LambdaQueryWrapperExamPaperQuestion() .eq(ExamPaperQuestion::getPaperId, paper.getId())); int totalScore 0; ListExamRecordDetail details new ArrayList(); MapLong, String userAnswerMap dto.getAnswers().stream() .collect(Collectors.toMap(AnswerItem::getQuestionId, AnswerItem::getAnswer)); for (ExamPaperQuestion pq : pqList) { ExamQuestion question examQuestionMapper.selectById(pq.getQuestionId()); ExamRecordDetail detail new ExamRecordDetail(); detail.setQuestionId(question.getId()); detail.setUserAnswer(userAnswerMap.getOrDefault(question.getId(), )); detail.setCorrectAnswer(question.getAnswer()); boolean correct question.getAnswer().equalsIgnoreCase(detail.getUserAnswer()); detail.setIsCorrect(correct ? 1 : 0); if (correct) { totalScore pq.getScore(); } details.add(detail); } // 保存考试记录和明细略 // 返回试卷题目解析、得分、用时等 return resultVO; }注意这里加了Transactional事务注解因为交卷过程涉及多张表的写入考试记录表、答题明细表甚至还要更新试卷表的考试次数。任何一步失败都要回滚不能出现“记录已保存但明细丢失”这种脏数据。事务是数据一致性的最后一道防线在这个场景不是可选项是必选项。4.2 学习交流模块帖子发布、分类检索与评论回复学习交流模块更像是一个轻量的社区。进首页后能看到帖子列表按最新和热门排序点进帖子详情能看到正文和所有评论登录用户可以发帖、评论。这里使用了view_count做浏览量每次请求详情时1达到“热门帖子”排序的数据基础。帖子列表的分页查询和试题分页写法大同小异这里不再重复。评论的插入需要注意防重复提交问题前端做了按钮loading后端通过Redis的分布式锁或数据库唯一索引来兜底。不过这个项目里为了简化只在前端做了提交中禁用按钮的处理真实商用项目中这件事必须要考虑否则连点几次发送按钮就会出现多条相同评论。帖子内容我处理成纯文本而不是富文本原因有两点一是富文本编辑器的集成和XSS过滤需要额外的工作量对于学习项目来说会增加不必要的复杂度二是考试交流平台的帖子内容以文字和代码为主纯文本完全够用。展示时通过white-space: pre-wrap保留换行格式阅读效果也不会差。4.3 用户体系与角色权限控制用户体系采用经典的RBAC基于角色的访问控制模型包含用户、角色、权限三层。简化后系统只设计了三个角色学生、教师、管理员。权限控制的核心逻辑实现在后端的拦截器或切面中。系统使用JWT做身份认证。用户登录成功后后端签发一个有效期2小时的JWT里面包含userId和roleCode。前端每次请求把JWT放在请求头里后端定义了一个JwtInterceptor拦截器对所有/api/**接口做校验但放行登录、注册接口。对于需要特定角色的接口在Controller方法上加自定义注解RequireRole(admin)拦截器里读取注解并比对角色权限。其实初学者在写权限时最容易犯的错误是只在前端隐藏了按钮后端接口完全裸奔。这种做法等于把保险柜的钥匙挂在门上别人绕过前端直接用Postman调用接口就能完成越权操作。所以后端的接口校验是必须做的前端隐藏按钮只是提升用户体验的一种手段。5. 常见问题与排查技巧实录5.1 MySQL8.0连接与部署高频问题这个项目在开发过程中遇到的数据库方面问题集中在以下几个方面现象原因解决方案启动报Public Key Retrieval is not allowedMySQL8.0默认认证插件为caching_sha2_passwordJDBC URL追加allowPublicKeyRetrievaltrue查询结果中文乱码数据库或表字符集不是utf8mb4统一设置库、表、连接三处字符集Linux下MySQL8.0无法远程访问默认bind-address为127.0.0.1root不允许远程修改bind-address0.0.0.0并授权远程连接Docker安装MySQL8.0数据丢失容器未挂载数据卷启动时加-v参数挂载宿主机目录allowPublicKeyRetrieval这个参数特别值得展开细讲。MySQL8.0改进了密码认证的加密方式在首次连接时需要通过RSA公钥传输加密的密码。有些客户端工具会默认禁止自动获取公钥导致连接直接报错。大多数初学者看到这个报错还很懵以为是密码错了。把allowPublicKeyRetrievaltrue加进JDBC URL并配合useSSLfalse这个问题就会消失。如果把MySQL跑在Docker容器里这里也分享一个实际项目中的常用命令docker run --name mysql8 \ -e MYSQL_ROOT_PASSWORDroot123 \ -p 3306:3306 \ -v /mydata/mysql/data:/var/lib/mysql \ -v /mydata/mysql/conf:/etc/mysql/conf.d \ -d mysql:8.0挂载数据卷是必须做的一步否则容器一删数据就全没了这是容器化部署MySQL最常见的坑。5.2 前端构建与Vue3开发中的典型坑前端方面遇到的第一坑是npm install装依赖时极慢或报错。解决方案是设置淘宝镜像源npm config set registry https://registry.npmmirror.com。这个改完之后装Element Plus、Axios、Pinia这些大依赖包的速度会快非常多。第二个坑是Vue3项目在Edge浏览器中偶发无法关闭右上角最小化按钮等异常界面问题。这个实际上和前端代码关系不大通常是浏览器插件、渲染进程卡死导致的。排查思路是先用无痕模式试如果无痕模式正常说明是浏览器扩展冲突如果无痕模式也复现则需要检查页面里是否存在过大的DOM渲染或内存泄漏。在项目里最直接的规避措施就是考试倒计时组件在页面卸载时会清除定时器避免定时器累积。第三个坑是Element Plus按需引入和全量引入的取舍。全量引入配置简单适合后台管理型项目打包体积大一些但省心。按需引入要配置unplugin-vue-components打包体积能缩小近一半。做这个在线考试项目时优先选择了全量引入因为学习阶段代码正确性比性能更关键等熟悉之后再去做按需引入优化即可。师徒级的理解胜过一次最佳实践等到项目上线之前再处理打包体积优化完全不迟。5.3 MyBatis-Plus使用中的高频坑MyBatis-Plus的分页失效是排名第一的高频问题。很多同学发现selectPage返回的记录总数对但数据条数不对或者limit根本没生效。排查思路很明确检查MybatisPlusInterceptor是否注册成功。这里有个容易被忽略的细节点分页插件配置的DbType要和实际数据库保持一致如果填错为OTHER分页SQL会生成失败。第二个高频问题是字段映射。数据库字段命名如果用的下划线风格如question_type实体类属性用驼峰如questionTypeMyBatis-Plus默认开启了下划线转驼峰映射通常不需要额外配置。但如果自己的命名不规范一会儿下划线一会儿大小写混用就会出现查询结果某些字段为null的情况。我的习惯是数据库字段全部小写下划线Java实体类属性全部驼峰这是最稳妥的约定。第三个坑是TableField注解和TableLogic逻辑删除同时使用时的冲突。比如某个字段既想自动填充创建时间又不想被删除时影响历史数据就需要理解MyBatis-Plus的自动填充机制实现MetaObjectHandler接口在insertFill和updateFill方法中统一设置createTime和updateTime。这样新增和更新记录时时间字段会自动填充不需要每次手工set值。5.4 系统部署与文档配套这套系统的运行环境建议统一用JDK8或JDK11后端打jar包执行java -jar exam-server.jar即可启动。前端执行npm run build产出dist目录生产环境用Nginx托管静态文件并把/api路径反向代理到后端服务。完整的部署步骤如下# 后端构建 mvn clean package -DskipTests nohup java -jar target/exam-server.jar --server.port8080 exam.log 21 # 前端构建 npm run build # Nginx配置核心部分 location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://localhost:8080/api/; }这里有一个前端的部署细节Vue3使用history路由模式时必须配置try_files $uri $uri/ /index.html否则用户刷新当前页面时会因为找不到对应的真实文件路径而出现404。这个坑几乎每个做前后端分离项目的人都会遇到。配套的项目文档是这套源码的一个亮点。文档里面包含的内容有系统需求分析与功能清单、数据库设计说明含ER图和表结构SQL脚本、接口文档每个接口的请求方式、参数、返回值、部署说明开发环境和生产环境、测试报告模板。对于毕业设计来说这套文档基本满足论文茶相关的材料需求对于团队交接来说也大幅降低了接手成本。6. 实操中的个人体会与扩展思路6.1 完整跑通一次项目比看十遍教程都有效我在实际搭建这套系统时最大的体会是纸上得来终觉浅绝知此事要躬行。看别人的项目展示时觉得不过如此真正动手从数据库建表、写Mapper接口、调通第一个登录接口再到前端渲染出第一个页面中间遇到的问题远比想象中多。但恰恰是这些问题的解决过程才把SpringBoot、MyBatis-Plus、Vue3这些知识真正内化成了自己的技能。6.2 功能扩展的想象空间这套系统后续还可以按以下方向继续演进试题导入导出Excel批量导入试题减少手动录入的成本考试防作弊切屏检测、IP白名单限制、随机抽题顺序打乱成绩统计可视化接入ECharts做班级成绩分布图、知识点掌握率雷达图消息通知学生考试完成或成绩公布后通过WebSocket实时推送消息。这几个方向无论做了哪一个都能明显提升系统的完整度和真实使用价值。6.3 最后一个小技巧分享最后分享一个在实际调试中很有用的排查思路当前后端联调出现问题时先别急着猜是哪边的锅先用Postman或Apifox直接调用后端接口。如果后端返回的数据正确那么问题基本确定在前端如果后端返回的数据已经不对那就完全不需要看前端直接顺着后端代码往下排查。这个做法可以砍掉50%以上的无效排查时间。在初学者阶段大家最容易犯的错误是前后端代码来回翻翻半天也定位不到问题越调越乱。记住这条方法论调试效率提升一倍并不夸张。回头再看这套SpringBoot2 Vue3 MyBatis-Plus MySQL8.0的在线考试系统从最初的需求梳理到最终部署上线整个流程走下来最大的收获其实不是“我会写某个接口了”而是终于理解了“一个完整业务系统是怎么从零到一组织起来的”。这种端到端的全栈视角是看再多零散教程也学不来的。如果你也在做类似的Java Web全栈项目建议动手之前先拿这套思路把表结构和接口设计理清能省下不少反复改代码的功夫。
返回列表