首页 / 资讯中心 / 文章详情

SpringBoot+Vue3英语知识网站全栈开发实战

SpringBoot+Vue3英语知识网站全栈开发实战 ★ FEATURED ARTICLE
接手这个英语知识应用网站项目的时候我脑子里第一反应是这不就是一套“课程列表在线刷题”的系统吗真正把需求聊细、把表结构铺开之后才发现这类项目看着简单实际上要处理的内容组织、用户学习路径、数据统计、权限边界这些问题每一个都能让人折腾好几天。这篇文章我就把整个项目的实现链路从头到尾拆开聊一遍结合SpringBoot2Vue3MyBatis-PlusMySQL8.0这套技术栈把我踩过的坑、做过取舍的地方、以及最后交付时文档该怎么沉淀一次性写清楚。内容主要以项目负责人的视角来写适合正在做类似Web应用系统、毕业设计或者想快速把全栈项目跑通的开发者参考。1. 需求画像这个英语知识网站到底要装下什么东西1.1 从一句话需求到完整功能清单很多项目一开始的需求都特别模糊我手里这个也不例外。原话大概就是“想做一个英语知识应用网站用户能看内容、能练习”具体看什么内容、练什么题型、谁去维护内容全都得自己去补。这个阶段最忌讳直接开写代码我习惯先画一张功能脑图把用户能感知到的页面和操作列出来。用户端这边核心是围绕“学习闭环”来做注册登录、课程列表、课程详情、章节内容展示、词汇表、在线练习、答题结果反馈、个人学习记录。管理员端则要能管课程、管章节、管题库、管用户最好再看一眼基础的数据统计比如某道题的正确率、某个课程的学习人数。这些功能拼起来才是标题里说的“英语知识应用网站系统”的真正内涵。如果你自己也接到类似需求我建议先想清楚一个边界问题这个网站是“内容展示为主”还是“练习交互为主”我这次主要偏向练习交互所以题库设计和答题判分逻辑占了比较大的比重课程内容只做结构化的文字和图片展示不做视频流这样前后端的工作量都被控制在合理范围内。1.2 技术选型背后的取舍逻辑技术栈是SpringBoot2Vue3MyBatis-PlusMySQL8.0这个组合不是随便拍脑袋定的。SpringBoot2的生态非常成熟周边资料多团队里其他人上手也快。SpringBoot3虽然也出了但当时考虑到一些老依赖的兼容性还是选了2.x这也是很多企业项目里的务实做法。Vue3这边的理由就更直接了组合式API写业务逻辑比选项式API更集中尤其是页面里有多个数据请求、多个状态需要联动的时候ref、reactive、computed一套写下来代码量比Vue2清爽不少。配合Vite做开发服务器热更新速度确实快启动一个中大型前端工程基本就是一两秒的事。MyBatis-Plus的存在感主要体现在单表CRUD和分页这两块。BaseMapper自带的方法基本覆盖了用户表、课程表这类简单增删改查接口层代码可以少写一大截。MySQL8.0则是为了用上窗口函数和一些新的JSON函数后面做统计报表和复杂查询时会舒服很多。整体原则就是每个部件的选择都服务于开发效率而不是盲目追新。2. 后端SpringBoot2工程拆分从Controller到SQL的完整链路2.1 包结构设计与请求流转路径后端工程我采用的是经典的分层结构包名固定几个controller、service、mapper、entity、dto、config、common。entity对应数据库表dto承接前端入参和出参service里写业务逻辑mapper只做数据访问controller只管参数校验和路由分发。一个典型的请求流转路径是这样的前端调用接口URLSpringMVC根据RequestMapping定位到Controller方法Controller接收参数后调用Service层的业务方法Service内部通过Mapper与MySQL交互最后结果封装成统一响应对象返回给前端。这个链路不复杂但每一层你都得有清晰的职责约束否则业务一旦多起来Service就会变成几百行的上帝类。我之前见过很多新手把SQL拼在Controller里图省事结果后面接口一变连最基本的权限控制都很难加。我的习惯是Controller层永远只做三件事接收参数、调用Service、返回响应。数据库操作不允许出现在Controller事务边界也永远放在Service层这样代码才好维护。2.2 MyBatis-Plus数据访问层单表CRUD与复杂查询的边界引入MyBatis-Plus之后最直观的提升就是不再需要为每个实体类手写BaseMapper里的那些基本SQL。我在项目里定义了一个公共的实体基类把主键ID、创建时间、更新时间、逻辑删除标记这些公共字段统一放进去每一个业务表对应的实体直接继承这个基类。这样建新表的时候实体类只需要写上业务字段开发速度会明显快一些。Data public class BaseEntity { TableId(type IdType.AUTO) private Long id; TableField(fill FieldFill.INSERT) private LocalDateTime createTime; TableField(fill FieldFill.INSERT_UPDATE) private LocalDateTime updateTime; TableLogic private Integer deleted; }不过MyBatis-Plus的优势集中在单表操作。一旦涉及多表关联查询、复杂统计你还是要回到自定义SQL这条路。我这里统计课程学习人数的时候就手写了一组SQL用到了MySQL8.0的COUNT、GROUP BY和左连接。所以记住一个边界简单操作用框架封装复杂的统计和报表查询该写SQL就写SQL不要强行用LambdaQueryWrapper去拼那种十几层嵌套的条件那比写原生SQL还难读。分页方面MyBatis-Plus提供了一个分页插件PaginationInnerInterceptor配置很简单但有个细节容易忽略必须手动new一个MybatisPlusInterceptor再把分页内拦截器加进去而不是直接new一个PaginationInnerInterceptor就完事。Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }2.3 登录认证与权限拦截的实现方式权限这块我故意没有上Spring Security原因是这个项目的角色只有管理员和普通用户两类而且大多数接口只要区分“登录了没有”即可没必要引入那么重的安全框架。我用的是一个轻量的方案用户登录成功后后端生成一个JWT令牌前端把Token存起来每次请求放到Header的Authorization字段里后端通过拦截器统一校验。拦截器的逻辑集中在WebMvcConfigurer配置里添加一个自定义HandlerInterceptor。校验通过后从Token里解析出用户ID并塞到ThreadLocal里后面的Service层要拿当前用户信息直接从这个ThreadLocal取不用每个方法都传一遍userId参数。代码看起来像这样public class AuthInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (!StringUtils.hasText(token)) { response.setStatus(401); return false; } // 解析token设置用户上下文省略细节 UserContext.setUserId(currentUserId); return true; } }这里有一个很容易踩的坑自定义拦截器虽然生效了但放行的路径没有配置好会导致登录接口本身也被拦截前端永远登不上。我的做法是把登录、注册、课程展示这类公开接口写成白名单其余接口都走拦截器。配置路径匹配的时候注意SpringMVC的路径匹配规则是Ant风格拦截器里不要用“/*”模糊自欺欺人一定要精确到接口前缀。3. Vue3前端不只是套壳组合式API与页面流转设计3.1 前端工程目录与路由规划前端我使用的是ViteVue3目录结构非常常见views放页面级组件components放可复用组件router放路由配置api目录按后端模块划分接口请求方法store里管理全局用户状态。这个结构最大的好处是新同事接手时不用猜代码在哪里。路由规划基本对应后端接口的模块划分。用户端有登录页、注册页、课程列表页、课程详情页、练习页、个人记录页。管理端独立一个路由前缀比如/admin开头下面挂课程管理、题库管理、用户管理等页面。这里路由配置要注意懒加载webpack时代用import()Vite下面直接配合defineAsyncComponent或者直接在Router配置里使用动态导入首屏加载速度会好很多。我在开发过程中特别调整了一个东西把“练习页”和“答题结果页”拆成了两个路由而不是用同一个页面去做状态切换。原因是答题结果页需要支持刷新后仍能查看结果如果把结果状态放在同一个组件里刷新页面状态就丢了用户会以为系统出错。拆成两个路由结果页通过路由参数携带练习记录ID刷新也没问题。3.2 axios封装与统一响应格式前后端联调过程中最容易出问题的就是响应格式不统一。我在项目里约定了一个统一响应结构code、message、data。code为200表示成功其他值表示业务错误。后端所有Controller返回的都是这个结构前端axios做一层拦截器统一处理返回逻辑。import axios from axios const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization token } return config }) service.interceptors.response.use( response { const res response.data if (res.code ! 200) { // 统一错误提示 return Promise.reject(new Error(res.message)) } return res.data }, error { // 处理401跳登录 return Promise.reject(error) } )这个封装里的关键是401处理。有次我在测试过程中发现Token过期后前端会一直卡在空白页排查了半天才发现是拦截器里没有做跳转逻辑。最终在响应错误里增加了一个判断如果状态码是401就清理本地用户信息并跳转到登录页体验才算正常。3.3 组合式API在业务页面中的落地Vue3组合式API极大改善了中后台页面的状态组织方式。举个例子课程详情页需要三个数据源课程基本信息、章节列表、当前用户的学习进度。如果用Vue2的data、methods、computed分开写这些逻辑会散落在文件的不同角落维护起来特别割裂。用setup语法之后我可以把这三个数据请求集中在一个函数里逻辑清晰很多。实际开发中我的习惯是页面级组件里的逻辑按照“数据获取用户交互展示计算”三个维度拆成若干个自定义hook。比如useCourseDetail负责拉取课程信息和学习进度usePractice负责练习题目的加载和提交换题。每个hook内部自己维护自己的状态和方法组件主文件只负责把它们串起来。这个写法在后面接下一个人接手项目时特别受欢迎因为看组件文件就能知道页面的所有行为。还有一个需要注意的点是关于响应式数据丢失的问题。有次我把接口返回的数据直接放进reactive对象里然后异步请求回来后又整体赋值结果页面怎么都不更新。原因是reactive对象里面如果塞了其他响应式对象的引用再整体替换的时候会丢失响应式链接。后来统一改用ref管理业务数据赋值用xxx.value responseData这个坑就再没遇到过了。4. MySQL8.0表结构设计给“知识内容”建模的几种思路4.1 核心表设计把课程、章节、词汇、题库串联起来表结构设计是这个项目里最需要提前思考的部分因为内容类系统和普通的管理系统不一样它天然有层级关系课程包含多个章节章节下面挂词汇条目题库里的题目又往往归属于某个章节。如果不把层级梳理清楚后面做练习记录和学习进度功能会非常痛苦。我设计的核心表主要有这几张user用户表、course课程表、chapter章节表、word词汇表、question题库表、practice_record练习记录表。course与chapter是一对多关系chapter与question是一对多关系practice_record记录用户每次练习的得分详情同时关联到具体的question方便追溯每个题目的答题情况。这些表在字段设计上都要继承公共的id、create_time、update_time、deleted字段。deleted字段用于逻辑删除这样用户误删课程时还能找回不会直接物理丢失数据。MySQL8.0的utf8mb4字符集一定要设置好之前默认latin1的表遇到过中文乱码问题后来统一改成utf8mb4_general_ci才解决。4.2 索引策略与常见查询性能问题数据量不大的时候索引问题通常不会被注意到。但题库这种表一旦往里面灌了几千道题查询体验就会明显下降。我在question表上给chapter_id建了一个普通索引因为最常见的查询场景就是“某个章节下的题目列表”。课程列表页的keyword搜索则在course表上建了一个针对title字段的普通索引。关于常见的性能问题有一个非常典型的场景是统计课程学习人数。如果在practice_record表上反复用COUNT查询每一门课请求一次前端页面会慢到让人抓狂。我的优化方法是把所有统计数据放到一张独立的统计表里后台定时任务去汇总更新课程列表页只查这张统计表。这种空间换时间的思路在内容类网站里几乎是标配。MySQL8.0有一个让我比较惊喜的特性就是支持窗口函数。比如统计某道题在所有用户中的正确率排名以前要写复杂的子查询现在直接用ROW_NUMBER() OVER (PARTITION BY question_id)就能搞定SQL简洁很多。不过窗口函数对索引的利用方式也有讲究临时文件排序会消耗内存字段类型和索引顺序还是要设计好。4.3 初始化种子数据与测试数据准备项目交付后别人拿到源码第一件想做的事情就是启动系统点开页面看看长什么样。如果你不准备任何种子数据对方看到的会是空荡荡的页面第一印象直接打折扣。所以我写了一个DataInitializer组件在应用启动的时候检测数据库中是否有数据没有就自动插入一套预设的课程、章节、单词和题目。这个初始化过程里有个要点把种子数据的ID固定住。比如课程ID从1001开始词汇表ID从2001开始这样后面的练习记录和成绩统计才能正确地关联到这些固定ID。如果让MySQL自增ID随机分配统计结果很可能会串表。Component public class DataInitializer implements CommandLineRunner { Override public void run(String... args) { if (courseMapper.selectCount(null) 0) { // 插入预设课程和章节 initCourses(); initChapters(); initWords(); initQuestions(); } } }种子数据不要只做几行意思意思我一般在开发阶段准备10门课程、每个课程5到6个章节、每个章节几十道题这样前端页面浏览起来有层次感调试分页和搜索功能也更有说服力。5. 上线部署与文档沉淀让项目从“能跑”到“能交付”5.1 环境配置与打包流程的每一步项目的运行环境是JDK8以上、Maven 3.6、Node 16、MySQL8.0。后端打包用mvn clean package -DskipTests生成一个可执行的jar包。注意SpringBoot2默认打包方式不是可执行jar需要在pom.xml里显式配置spring-boot-maven-plugin否则打出来的jar无法用java -jar直接启动。前端打包是npm run build产物会生成在dist目录。部署时我个人更倾向于用Nginx托管前端静态文件同时把SpringBoot后端接口通过反向代理转发。Nginx配置里需要特别注意的是try_files指令否则Vue路由在刷新非根路径时会报404。核心思路是让所有路由都回退到index.html由前端路由接管而不是让Nginx真的去找对应路径的文件。server { listen 80; server_name your-domain.com; root /data/www/dist; index index.html; location /api { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; } location / { try_files $uri $uri/ /index.html; } }5.2 接口文档与开发文档的整理思路这个项目标题里专门写了【含文档】可见文档在交付中的分量不轻。我的文档体系分为三块需求说明文档、接口文档、部署手册。需求说明文档把功能清单和角色权限写清楚接口文档我采用手写Markdown的方式把每个接口的入参、出参、错误码、示例请求都列出来部署手册则从环境安装到启动顺序一步步写明白。接口文档有个实用技巧写清楚测试数据。比如某个接口返回的data里包含哪些字段给一段真实的JSON示例比空洞描述字段类型有用得多。我平时会在文档里为每个模块附一条完整的请求和响应示例这样联调的时候前端开发直接照着示例就能判断问题出在前端还是后端不用反复问我。5.3 从“能跑”到“可交付”的避坑要点最后一个模块我专门说说项目交付前的几个避坑点全是实操中暴露出来的问题。第一个是跨域配置。开发阶段前后端往往分离跑前端在5173端口后端在8080端口如果不配置跨域接口请求会被浏览器拦截。我在后端的WebMvcConfigurer里配置了CorsMapping允许本地开发环境的地址访问。这个配置在上线后反而成了一个安全隐患所以记得在正式环境里收紧只允许自己的域名或干脆关闭跨域。第二个是文件存储路径。这个项目里涉及课程封面图片的上传功能如果用本地磁盘存储打包成jar后图片路径会变来变去容易出现问题。我的办法是统一使用一个外部配置项指定上传根目录部署时通过application.yml里的upload.path配置不写死在代码里。第三个是数据库的初始化脚本要跟着代码一起提交。很多人改表结构只在自己的本地库改代码里没有同步更新SQL脚本结果别人拉下来项目后数据库结构对不上。我现在的做法是每次有表结构变更都写一个带版本号的新SQL脚本合并到项目的db目录下并在文档里注明最新脚本是哪个。最后再分享一点个人体会整条链路做下来我最大的感受是英语知识应用网站这类偏内容管理的Web系统核心难点往往不在某个具体框架的API使用上而在于如何把内容层级设计好、如何让用户的学习行为被持久化并可追踪。如果表结构一开始就设计得含糊后面加功能、改统计成本会一路翻倍。如果你准备利用这套技术栈去搭自己的项目我的建议是先别急着写CRUD花一个晚上把课程、章节、题目、用户、练习记录这几张表的关系画清楚把需求文档写出来。折腾文档的时间不会白费后面所有代码都是在为文档里的功能清单做填充。等系统能跑起来之后再按我第五部分说的补上种子数据和部署手册这个项目才真正算得上可以交付给别人使用的完整产品。
阅读完成 · 觉得有帮助?
咨询建站