news 2026/10/16 4:39:37

Spring Boot + Vue 3 全栈项目实战:从接口设计到前后端联调

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Spring Boot + Vue 3 全栈项目实战:从接口设计到前后端联调

在后台管理系统、小程序管理端、企业官网这类项目里,Spring Boot + Vue 的组合几乎成了事实标准。后端 Spring Boot 提供稳健的 API 服务,前端 Vue 负责交互体验,两者配合能快速完成从数据库到页面的全链路打通。这篇文章不是我第一次写全栈项目的总结,而是把最近一个完整项目的实践过程做了系统梳理——从技术选型、项目结构、后端接口设计、前端页面实现,到联调部署和踩坑记录,尽量覆盖一个真实开发周期里最容易出问题的环节。无论你是刚接触全栈开发的学生,还是工作中需要独立搞定前后端的开发者,这篇文章的目标都是让你跟着思路走一遍,就能搭出属于自己的一套可扩展、可维护的全栈项目。

我先把结论放在前面:Spring Boot + Vue 这套组合,学习曲线不算平缓,但一旦你把项目结构、接口规范、鉴权流程和数据流这几个关键点想清楚,开发效率会非常高。难点不在于某一个框架,而在于前后端如何衔接、如何约定、如何排错。正文里我会用一个模拟项目 X 作为贯穿案例,它是一个典型的课程管理 + 用户学习记录系统,既有后端的增删改查和权限控制,也有前端的列表、表单、路由守卫和状态管理,非常适合用来演示全栈开发的完整链路。

1. 在动手写代码之前,先想清楚这套全栈方案到底在解决什么问题

很多人一拿到需求就直接建项目开写,结果往往是后端接口写得很随意,前端页面堆了一大堆重复代码,联调时各种字段对不上。在我实际做过的项目里,这个阶段往往决定整个项目的最终质量,值得多说几句。

1.1 为什么是 Spring Boot 而不是其他后端框架

Spring Boot 的核心价值不是"快",而是约定优于配置。它内置了 Tomcat、自动配置了数据源、简化了依赖管理,让你从繁琐的 XML 配置里彻底解放出来。尤其是它的 Starter 机制,想加一个 Web 层就引入 spring-boot-starter-web,想操作数据库就引入 spring-boot-starter-data-jpa 或 MyBatis 依赖,几乎不用写额外的配置代码就能跑起来。

从我个人的经验看,Spring Boot 在中小型项目里的优势最明显:团队不用花大量时间在框架整合上,可以把精力集中在业务逻辑本身。它的生态也非常成熟,无论是做权限控制(Spring Security)、接口文档(springdoc-openapi),还是做缓存(Redis)、消息队列(RabbitMQ),都能找到很完善的开源解决方案。对于后面要讲的模拟项目 X 这种带用户体系、权限区分的系统,Spring Boot 自带的依赖注入和面向切面编程能力,可以让代码结构保持清晰。

1.2 为什么前端选择 Vue 3 + Vite + Pinia

Vue 在前端框架里的特点一直是"渐进式":你可以在一个页面里引入它当局部工具用,也可以把它当完整的 SPA 框架组织整个前端工程。这套灵活性让团队里不同水平的开发者都能上手,不至于像某些重型框架那样一上来就必须按特定模式写。

我特别想强调 Vue 3 组合式 API 的价值。用选项式 API 写组件时,逻辑是按 data、methods、computed 这些选项去分块的,当组件变复杂后,同一个功能的代码往往被拆散在各个选项里,改一个功能要上下跳好几处。组合式 API 则让你能按功能将逻辑组织在一个函数块里,就像写普通函数一样自然。Pinia 作为 Vue 3 官方推荐的状态管理库,比 Vuex 更简洁,没有那么多样板代码,非常适合本项目里存储用户登录状态和课程学习记录。

Vite 带来的变化同样明显。开发调试时它能做到秒级启动,得益于它基于 ES Module 的原生加载方式而不是像 Webpack 那样先把所有东西打包一遍。这种即时反馈对开发体验的提升是实打实的——我在项目里改动一个组件,浏览器几乎是同步刷新。

1.3 案例项目的整体目标和功能边界

为了让整篇指南有一条清晰的主线,我用模拟项目 X 作为贯穿案例。这是一个课程管理与学习记录系统,目标用户是在线教育机构的学习者和管理员。它需要支持以下核心功能:

  • 用户注册与登录,登录后区分普通用户和管理员角色
  • 管理员可以维护课程信息,包括新增、编辑、上下架
  • 普通用户可以浏览课程列表、查看课程详情、记录自己的学习进度
  • 所有接口都需要身份验证,管理员操作还要额外的权限校验

这套功能虽然不算复杂,但覆盖了全栈开发里最重要的几块拼图:用户鉴权、CRUD 接口、前后端交互、状态管理、权限控制。把它拆解清楚,再做类似的系统就有了明确的参考。

2. 后端工程结构:先搭骨架还是先写业务,顺序有讲究

后端工程结构决定了整个后端团队协作的顺畅程度。我见过太多项目把 Controller、Service、Mapper 全部写在几个大类里,几百行的文件比比皆是,后续维护苦不堪言。Spring Boot 本身对分层没有强制规定,但成熟的工程结构能帮你和同事节省大量沟通成本。

2.1 按功能模块分包,而不是按技术分层分包

这里要区分两个概念:按技术分层是指所有 Controller 放一个包、所有 Service 放一个包、所有 Mapper 放一个包;按功能模块分包是指把用户相关的 Controller、Service、Mapper 放在同一个包下,课程相关的放在另一个包下。

我强烈建议按功能模块分包。为什么?想象一个用户模块和课程模块,按技术分层时,你改一个用户功能可能需要同时在 controller、service、mapper 三个包之间跳跃;而按功能模块分包后,所有相关的文件就在同一个目录树里,查找和修改的路径缩短很多。当项目变大,按模块分包还能天然地形成模块边界,后续拆分成微服务时可以直接把某个包提取出去。

具体到模拟项目 X,我建议的包结构大致是这样的:

com.example.projectx ├── common // 通用类:统一返回结果、异常处理、工具类 ├── config // 配置类:安全配置、跨域配置、拦截器注册 ├── security // 鉴权相关:JWT 工具类、过滤器、自定义认证逻辑 ├── module │ ├── user // 用户模块:实体、DTO、Controller、Service、Mapper │ ├── course // 课程模块 │ └── study // 学习记录模块

common 和 config 是横向的公共基础设施,module 下每个子包都是一个完整的业务模块。如果你用了 MyBatis,通常会在 module 里直接放 Mapper 接口和对应的 XML 文件,而不是单独抽一个 mapper 包,这同样是按模块聚合的思路。

2.2 统一后端返回结果和异常处理,联调时能省一半时间

前后端联调里最让人烦躁的就是错误信息不统一。有时候接口返回的是一个字符串,有时候是一个 Map,前端根本没法做统一的错误提示。我在项目里的做法是定义一个通用的返回结果类,所有接口都用它包一层。

@Data public class ApiResult<T> { private int code; private String message; private T data; public static <T> ApiResult<T> success(T data) { ApiResult<T> result = new ApiResult<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> ApiResult<T> error(int code, String message) { ApiResult<T> result = new ApiResult<>(); result.setCode(code); result.setMessage(message); return result; } }

这样前端只需要根据 code 字段判断请求是否成功,message 用于提示,data 才是真正需要的数据。配合全局异常处理器,业务异常能自动转换成统一的错误结构,不会让一些底层异常堆栈直接暴露到前端。

全局异常处理也是必须考虑的,我用了@RestControllerAdvice来统一接收异常。比如参数校验失败的异常、业务异常、未知异常,分别处理成合适的返回码和提示信息。这一点很多人容易忽略,等到接口报错时前端看到的是 500 加一串堆栈,体验很差。

2.3 使用 JWT 做登录态管理,无状态鉴权更适合前后端分离

模拟项目 X 需要区分用户和管理员角色,所以我选了 JWT(JSON Web Token)做鉴权。JWT 的核心原理是:用户在登录成功后,服务端生成一个包含了用户信息和过期时间的签名字符串返回给前端;前端在后续请求的请求头里带上这个 Token;服务端通过校验签名和有效期来识别用户身份。

这里值得说明的是 JWT 本身是无状态的,服务端不需要存储 Session,这让服务可以水平扩展,非常适合微服务和前后端分离架构。但在实际项目中我也踩过一些坑,比如 Token 如何续期的问题。我的做法是设置一个较短的过期时间(比如三十分钟),同时前端维护一个刷新 Token 的接口,在 Token 快过期时自动换取新的 Token。这样既保证了安全性,又不会频繁要求用户重新登录。

在 Spring Boot 里用 JWT,一般通过自定义过滤器在请求进入 Controller 之前完成 Token 的解析和用户信息的注入。Spring Security 配置里,我允许了登录和注册接口匿名访问,其余接口需要经过 JWT 过滤器,同时针对/admin开头的接口额外校验管理员角色。

3. 模拟项目 X 后端核心模块实现:从数据库表设计到接口组装

前面讲了工程结构和通用基础设施,现在开始具体写业务代码。模拟项目 X 涉及用户、课程和学习记录三个核心模块,我会重点讲解课程模块和学习记录模块的设计思路,用户模块主要涉及密码加密和角色处理。

3.1 数据库表设计:宁可多花半天建模,也不要上线后三天两头改表

数据表是业务的地基,设计不好后面改动成本极高。模拟项目 X 里我设计了五张关键表:

表名用途关键字段
user用户表id, username, password, role, create_time
course课程表id, title, cover, description, teacher, status
course_chapter课程章节表id, course_id, title, sort, content
study_record学习记录表id, user_id, course_id, chapter_id, progress
course_category课程分类表id, name, sort

关于 user 表的 password 字段,我很想强调一点:永远不要明文存储密码。这里我用了 BCryptPasswordEncoder 对密码做不可逆的哈希处理,而不是简单的 MD5。尽管 MD5 在学术意义上也是哈希,但它已经被彩虹表攻击得很透,BCrypt 加入了盐值计算,同样的密码在不同用户表里也会生成不同的哈希值,安全性高了很多。

course 表和 course_chapter 表是一对多的关系,每一门课程下面挂着多个章节。study_record 表则记录了某个用户在学习某个课程时的进度,比如学到了第几章节、当前进度百分比。这样设计的好处是可以很自然地支持后续的"继续上次学习"功能。

3.2 课程接口的完整实现:Controller 只做调度,Service 承载业务

以课程的新增、修改、列表查询三个核心接口作为例子,说明控制层和服务层各自的职责划分。

先看 Service 层的实现思路。Service 接口定义了业务方法,ServiceImpl 实现类里写真正的逻辑。课程新增时,需要做几件事:检查课程标题是否重复、处理封面图片的存储路径、设置默认状态为下架。课程列表查询时,支持按关键词搜索、按分类筛选、按状态过滤,还要支持分页。

@Service public class CourseServiceImpl implements CourseService { @Autowired private CourseMapper courseMapper; @Autowired private CourseCategoryMapper categoryMapper; @Override public PageResult<CourseVO> pageQuery(CourseQueryDTO query) { PageHelper.startPage(query.getPageNum(), query.getPageSize()); // 条件查询交给 Mapper 来处理动态 SQL List<CourseVO> list = courseMapper.selectCoursePage(query); PageInfo<CourseVO> pageInfo = new PageInfo<>(list); return PageResult.build(pageInfo.getTotal(), pageInfo.getList()); } }

Controller 层只负责接收参数和返回结果,不再写业务逻辑。拿新增课程接口来说:

@PostMapping("/course") public ApiResult<Long> createCourse(@RequestBody @Validated CourseCreateDTO dto) { Long courseId = courseService.createCourse(dto); return ApiResult.success(courseId); }

参数校验我放在了 DTO 里,通过@NotNull、@NotBlank这类注解完成简单的非空校验。复杂一点的业务校验则下放到 Service 层,比如管理员角色校验、状态机流转的合法性判断。这样拆分之后,Controller 很薄,Service 才是逻辑的载体。

3.3 学习记录模块:事务边界和进度计算的细节

学习记录是模拟项目 X 里最容易被忽视但实际很考验细节的模块。用户每次看完一节,前端会调用接口上报学习进度。这个接口的特别之处在于:它不是一个简单的新增或修改,而是需要"有则更新、无则插入"的逻辑。

@Transactional @Override public void reportStudyProgress(StudyProgressDTO dto) { StudyRecord record = studyRecordMapper.findByUserAndCourseAndChapter( dto.getUserId(), dto.getCourseId(), dto.getChapterId()); if (record == null) { StudyRecord newRecord = new StudyRecord(); newRecord.setUserId(dto.getUserId()); newRecord.setCourseId(dto.getCourseId()); newRecord.setChapterId(dto.getChapterId()); newRecord.setProgress(dto.getProgress()); studyRecordMapper.insert(newRecord); } else { record.setProgress(dto.getProgress()); studyRecordMapper.updateById(record); } // 更新课程总进度:如果本次上报的章节是最后章节且进度为100%,则整个课程完成 updateCourseTotalProgress(dto.getUserId(), dto.getCourseId()); }

为什么这里一定要加@Transactional?因为"查找—插入或更新—更新总进度"这三个操作是一个整体,任何一个环节失败,前面的操作都不应该生效。比如用户在极短的时间内重复点击"下一章",并发请求可能导致记录被插入两次。事务配合数据库的唯一索引,可以尽量避免这种脏数据问题。

关于进度计算的细节,我在项目里是把每个章节的权重按章节数平均分配。课程的总进度等于用户学完的章节数占总章节数的百分比。前端上报的是"看完了哪一章",后端来计算总进度,而不是让前端直接传一个百分比数字,避免前端数据被篡改。

3.4 MyBatis 动态 SQL 和多表联查的实际用法

在课程列表的分页查询里,需要根据不同的条件拼接不同的 SQL,MyBatis 的<where>和<if>标签在此格外好用。

<select id="selectCoursePage" resultType="com.example.projectx.module.course.vo.CourseVO"> SELECT c.id, c.title, c.cover, c.teacher, c.status, cc.name AS categoryName FROM course c LEFT JOIN course_category cc ON c.category_id = cc.id <where> <if test="keyword != null and keyword != ''"> AND c.title LIKE CONCAT('%', #{keyword}, '%') </if> <if test="categoryId != null"> AND c.category_id = #{categoryId} </if> <if test="status != null"> AND c.status = #{status} </if> </where> ORDER BY c.create_time DESC </select>

这里要特别留意 SQL 注入的问题。很多新手会把前端传来的关键字直接用字符串拼接进 SQL,这是非常危险的做法。MyBatis 的#{}预处理参数会生成?占位符,由 JDBC 的 PreparedStatement 来绑定参数,从根本上避免了注入风险。永远不要在生产代码里用${}去拼接用户输入。

4. 前端工程实现:路由、状态管理、API 封装一个都不能少

后端接口写完,前端就可以同步开工了。我用 Vite 初始化了一个 Vue 3 项目,目录结构做了清晰的分层:页面组件放在 views 下,公共组件放在 components 下,网络请求封装在 api 目录,状态管理放在 stores 目录。

4.1 用 Pinia 管理登录状态,而不是每个页面各自请求用户信息

一个非常常见的错误设计是:每个需要用户信息的页面都去调一次"获取当前用户信息"的接口,多浪费请求不说,页面间状态还不统一。我用 Pinia 定义了一个 user store,登录成功后把用户信息和 Token 都保存到 store 里,后续页面直接从 store 中读取,需要时才向后端刷新信息。

import { defineStore } from 'pinia' export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: null }), actions: { async login(loginForm) { const res = await api.post('/login', loginForm) this.token = res.data.token this.userInfo = res.data.userInfo localStorage.setItem('token', this.token) }, logout() { this.token = '' this.userInfo = null localStorage.removeItem('token') } } })

为什么要把 token 同时存到 localStorage?原因很简单:页面刷新后 Pinia 的状态会全部丢失,Token 存在 localStorage 里可以保证刷新后还能从本地恢复登录态。这种"持久化一部分状态,内存里留一部分状态"的组合策略在全栈项目里很常用。

4.2 axios 拦

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/16 4:39:16

JavaWeb学习路线:从Servlet到Spring Boot的全链路解析

1. 为什么我在2024年才开始写JavaWeb学习日记打开博客后台准备敲第一篇文章的时候&#xff0c;其实挺感慨的。做开发这几年&#xff0c;断断续续用过不少框架&#xff0c;也写过一些接口&#xff0c;但一直没系统梳理过JavaWeb这条线。直到最近重新翻自己早年记的零散笔记&…

作者头像 李华
网站建设 2026/10/16 4:38:46

端侧大模型推理性能调优实战:从0.4到12.8 token/s的排查指南

最近被一个“80 倍”的性能对比搞得相当狼狈。前提是端侧推理——模型不联网&#xff0c;完全在手机本地跑 LLM 的那种部署方式。别人演示视频里每秒稳定输出二十多个 token&#xff0c;我手里这台设备一个字一个字往外蹦&#xff0c;体感差距大到让人怀疑是不是拿错了机器。以…

作者头像 李华
网站建设 2026/10/16 4:38:11

C++高精度计时与延时:从steady_clock到低漂移定时器实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 4:38:01

OpenClaw AI代理爆火背后:权限失控与隐私危机深度解析

1. 从"排队装机"到"全网裸奔"&#xff1a;OpenClaw到底火成什么样了1.1 一周之内&#xff0c;它怎么把整个技术圈的时间线刷屏了这几天我被问得最多的一句话几乎一模一样&#xff1a;OpenClaw你装了吗&#xff1f;打开朋友圈&#xff0c;有人晒自己抢到了安…

作者头像 李华
网站建设 2026/10/16 4:37:27

双串磷酸铁锂主动均衡方案:MP2642GR-Z与PIC18F56K42实战解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/16 4:36:53

CPU调度与多核负载均衡:从概念到实践全解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华