先说清楚这篇东西的定位:它不是那种“跟着敲一遍就能跑起来”的教程,而是我从实际项目里提炼出来的全栈开发经验总结。如果你正准备用 Spring Boot 和 Vue 搭一套前后端分离的系统,或者已经在这条路上踩坑,那这篇内容应该能帮你省下不少时间。我会从技术选型讲到前后端联调,再讲到部署上线,中间穿插大量实际项目中遇到的问题和对应的解决办法。内容偏实战,代码会贴,但不会通篇堆代码,重点放在“为什么这么做”上。
1. 技术选型与整体设计思路
1.1 为什么选 Spring Boot + Vue 这套组合
很多人在选型阶段就容易纠结:后端用 Spring Boot 还是 Node.js,前端用 Vue 还是 React。我的看法是,这套组合之所以在国内中小型团队里那么流行,核心原因是“稳定”和“好招人”。Spring Boot 的生态太成熟了,从数据库访问到消息队列、从缓存到定时任务,几乎你能想到的需求都能找到现成的 starter。Vue 的上手门槛比 React 低,模板语法直观,对后端出身的开发者特别友好。团队里如果都是 Java 背景,Vue 的模板风格和传统的 JSP 之类的东西有相似之处,转型成本低。
从项目维护角度讲,Spring Boot 的自动配置机制让服务端代码的“约定”感很强,新成员看代码时不需要太多上下文就能找到入口。而 Vue 的单文件组件方式把模板、脚本、样式收拢在一个文件里,职责边界清晰。这套组合的另外一个优势是前后端可以完全独立开发、独立部署,只要接口文档定得够详细,两边并行推进的效率非常高。
1.2 前后端分离架构里的职责边界
前后端分离这个词被喊了很多年,但真正落地的时候很多人还是搞混。我见过不少项目,前端页面里直接拼 RESTful URL,后端 Controller 里返回视图跳转,本质上还是耦合的。正确做法是:后端只负责提供数据接口和业务逻辑,不关心前端页面怎么渲染;前端只负责用户交互和数据展示,不直接操作数据库。
我习惯的职责切分是这样的:
- 后端负责:身份认证、权限校验、业务规则、数据持久化、事务管理
- 前端负责:路由管理、状态管理、接口调用、表单校验、交互反馈
- 两边共同遵守:接口文档里约定的请求格式、响应格式、错误码
这套切分做完之后,一个很直接的好处是:后端接口写好了可以直接用 Postman 或者 Apifox 自测,前端也可以用 mock 数据先跑起来,两边互不阻塞。项目进度条拉快了不少。
1.3 项目结构规划:从单体到模块化的渐进式拆分
很多人一上来就按“controller/service/mapper”三层结构堆代码,小项目还好,一旦业务复杂起来,包越来越多,层级越来越深,最后改一个功能要翻十几个文件。我的建议是:先按“业务模块”划分顶层包,再在模块内部做三层结构。
举个例子,一个电商系统的后端包结构可以这么设计:
com.example.mall ├── common // 通用组件:统一返回、异常处理、工具类 ├── config // 配置类:拦截器、CORS、安全配置 ├── module │ ├── user // 用户模块 │ │ ├── controller │ │ ├── service │ │ ├── mapper │ │ └── entity │ ├── order // 订单模块 │ ├── product // 商品模块 │ └── payment // 支付模块 └── security // 安全相关:JWT工具、认证过滤器这样做的优势在于:每个模块的业务代码内聚在一起,模块之间通过接口交互,降低耦合。等到将来需要拆微服务,直接把这个模块抽出去变成独立服务就行,改造工作量比从三层结构里硬拆要小得多。
1.4 环境版本选择,别踩版本坑
版本兼容问题是我在实际开发中最头疼的问题之一。Spring Boot 和 Vue 的生态更新节奏快,不同大版本之间的 API 变化有时很隐晦。我目前生产环境比较稳的一套组合是:
- Java 17 + Spring Boot 2.7.x(3.x 也用过,但有些第三方 starter 还没跟进,建议团队先用 2.7 过渡)
- Vue 3.x + Vite + Element Plus
- Node.js 16.x 或 18.x LTS
- 数据库 MySQL 8.0.x,ORM 用 MyBatis Plus
这个组合有个好处:网上踩坑资料最多。你遇到任何奇奇怪怪的问题,基本都能搜到前人留下的经验。那些追求最新版本的同学,建议先在测试环境玩通了再上生产,不然一个依赖升级引发的连锁兼容问题,排查起来很狼狈。
2. 后端核心实现与开发经验
2.1 Spring Boot 项目搭建和分层实践
项目搭建我用的是 Spring Initializr 生成基础骨架,然后按第一章节的模块结构调整包名。依赖选择上,Web、Validation、MyBatis Plus 启动依赖是必须的,JWT 相关的我直接引入 jjwt 库,省得自己去实现签名算法。
这里要重点说下分层代码怎么写才规范。以用户注册为例,Controller 层只做参数接收,Service 层做业务逻辑,Mapper 层只负责数据库操作。千万别图省事,在 Controller 里写一大串业务代码,后期维护会非常痛苦。比如用户注册接口,规范的写法是:
@RestController @RequestMapping("/api/user") public class UserController { private final UserService userService; public UserController(UserService userService) { this.userService = userService; } @PostMapping("/register") public Result<Void> register(@RequestBody @Valid RegisterRequest request) { userService.register(request); return Result.success(); } }Service 里负责校验用户名唯一性、加密密码、保存用户记录,事务的管理也在这一层,用@Transactional注解控制。这样接口层非常薄,看着舒服,出了问题也好排查——先看 Controller 参数对不对,再看 Service 逻辑,最后才查 SQL。
2.2 统一返回格式,前端少踩一半坑
接口返回格式如果不统一,前端每次拿到响应都要猜结构,联调效率低得一塌糊涂。我见过最离谱的接口,有的返回{"code":0,"data":...},有的返回{"status":"success","result":...},前端封装 Axios 拦截器时没法做统一处理,只能到处写判断逻辑。
我推荐一个简单可靠的统一返回结构:
{ "code": 200, "message": "操作成功", "data": {} }其中 code 是业务状态码,200 代表成功,其它值代表各种业务异常,比如 401 未认证、403 无权限、400 参数错误。message 是给前端提示用的文案,data 是真正的业务数据。
后端代码上,定义一个泛型类Result<T>,包含静态方法success()、success(T data)、error(String message),然后在所有 Controller 的返回值里都用它。另外再用一个全局异常处理器@RestControllerAdvice,把各种校验异常、业务异常、未知异常统一兜底,转成上面的格式返回。前端只需要在 Axios 响应拦截器里判断 code 是否为 200,其它情况统一弹提示或跳转登录页,省心很多。
2.3 参数校验:后端校验是第一道防线,也是最后一道
关于参数校验,网上有个常见争论:前端已经做了表单校验,后端还需要校验吗?答案是:必须做,而且要做全套。前端校验只是为了用户体验,只要有人绕过前端直接调接口,后端不校验的话,脏数据就直接进了数据库。这种问题一旦发生,后续排查的成本远高于当初写校验的功夫。
Spring Boot 里做参数校验很方便,利用 Bean Validation 注解,在实体类字段上加上约束注解:
public class RegisterRequest { @NotBlank(message = "用户名不能为空") @Size(min = 4, max = 20, message = "用户名长度必须在4到20之间") private String username; @NotBlank(message = "密码不能为空") @Size(min = 6, max = 32, message = "密码长度必须在6到32之间") private String password; @NotBlank(message = "邮箱不能为空") @Email(message = "邮箱格式不正确") private String email; }然后在 Controller 参数上加上@Valid注解,Spring 会在进入方法前自动完成校验。校验失败抛出的异常在全局异常处理器里统一捕获,把 message 返回给前端。这里有个细节:分组校验。比如用户 ID 在更新接口里必须传,但新增接口里不能传,就可以用@Validated的分组功能来区分,不要为了省事写两套实体类。
2.4 JWT 身份认证:从登录到请求鉴权的完整链路
JWT(JSON Web Token)是前后端分离场景下最常用的认证方案。它的原理不复杂:用户登录成功后,后端生成一个带有签名信息的 token 字符串返回给前端;前端之后每次请求都在 header 里带上这个 token;后端通过验签来确认用户身份。整个过程中后端不需要存 session,天然适合横向扩展。
我这里分享一个生产可用的实现链路:
- 登录接口校验用户名密码,成功后用 jjwt 生成 token,token 里面包含 userId、username 等非敏感信息
- token 设置过期时间,我一般给 24 小时,具体业务可以自己调
- 后端写一个拦截器(HandlerInterceptor),拦截除了登录接口和静态资源之外的所有请求
- 拦截器里读取 header 的 Authorization 字段,校验 token 签名和过期时间
- 校验通过后,从 token 里解析出 userId,放入 ThreadLocal 或 Request attribute 中,方便后续业务代码获取当前登录用户
有一个关键点想特别提醒:禁止把敏感信息放进 token,比如手机号、身份证号这些。因为 JWT 的 payload 只是 Base64 编码,不是加密的,任何人拿到 token 都能解码看到内容。token 泄露的风险本来就存在,再放敏感信息就是雪上加霜。
2.5 数据库交互优化:分页查询和防止 SQL 注入
MyBatis Plus 在单表 CRUD 上能省大量模板代码,BaseMapper 里已经内置了插入、更新、按 ID 查询这些方法。但多表关联查询、复杂 SQL 还是需要自己写 XML。这里有两个日常开发最容易忽略的点:
第一个是分页插件配置。MyBatis Plus 的分页需要显式引入分页插件,否则 Page 对象不会生效。配置方法很简单,在配置类里加一个 MybatisPlusInterceptor 的 bean。分页查询的好处不只是用户体验,更重要的是避免一次性把几万条数据加载到内存里,把数据库和服务器内存打爆。
第二个是防 SQL 注入。我见过不少人在 XML 里用${}拼接参数,这是非常危险的行为。${}是直接文本替换,如果参数来自用户输入,有心人构造一个恶意字符串就能把 SQL 改得面目全非。正确做法是尽量用#{}预编译参数,如果某处确实需要动态排序字段这样的场景,必须在代码层做白名单校验,只允许传入白名单里的列名。
3. 前端工程化与核心实现
3.1 基于 Vite 的 Vue3 项目搭建,取代 Webpack 后的真实体验
现在新建 Vue 项目我推荐直接用 Vite。Vite 基于原生 ES Module,冷启动速度比 Webpack 快一个数量级,开发体验是质的提升。创建命令很简单:
npm create vite@latest frontend -- --template vue生成的项目结构很清爽,之后我需要做几件事:装 vue-router、pinia、axios、element-plus 这些核心库,配置路径别名@指向src目录。路径别名这个配置建议第一时间做,不然后期代码里到处都是../../../../,看着心烦也容易引错路径。
Vite 的代理配置是联调阶段的神器。在vite.config.js里加个 server.proxy,把/api开头的请求转发到后端地址,就能完美解决开发环境下的跨域问题。前端代码里写的请求地址直接是/api/user/list这种相对路径,联调时后端换个端口,只需要改配置文件,前端一行代码不用动。
3.2 组件化设计:从页面到组件的拆分思路
Vue 的组件化开发,最核心的是一句话:一个组件只干一件事。页面拆得细,每个组件的逻辑就简单清晰,出了问题也容易定位。
我常用的拆分策略分三层:
- 页面级组件:对应一个路由,负责数据加载和页面布局,比如登录页、商品列表页
- 业务组件:处理特定的业务功能,比如用户信息卡片、订单状态标签
- 基础组件:纯粹展示型组件,不掺业务逻辑,比如自定义的分页组件、空状态占位
组件之间通信我用 props 和 emit 解决绝大多数场景。props 传数据、emit 抛事件,单向数据流很清晰。遇到跨多层组件传值,才考虑用 Pinia 或者 provide/inject。这里有个原则:别为了图省事随便引入全局状态,很多东西其实用 props 传递就够用了,过度设计同样会增加维护成本。
3.3 Axios 封装:统一请求拦截和响应处理
Axios 的封装质量,直接决定了联调效率。我会把 Axios 实例单独放在一个文件里,统一配置 baseURL、超时时间、请求拦截器、响应拦截器。
请求拦截器里做一件事:从 localStorage 里拿 token,如果有就加到请求头的 Authorization 字段。响应拦截器里做的事稍微多一点:
- 判断 HTTP 状态码,如果不是 2xx 就直接抛错
- 再根据业务 code 判断是否成功,如果不成功,统一弹 Element Plus 的 Message 提示
- 特殊处理 401:清除本地 token,跳转登录页
这个封装做完之后,业务代码里的接口调用会非常简洁,比如:
// API 层 export function getUserList(params) { return request.get('/user/list', { params }) } // 页面里调用 const getList = async () => { const res = await getUserList(pageParams.value) tableData.value = res.data }不需要每个接口都重复处理错误,也不需要在每个页面判断登录状态。这些横切关注点一次性处理好,开发效率能提升不少。
3.4 Pinia 状态管理:Vuex 之后的轻量选择
Pinia 是目前 Vue 官方推荐的状态管理库,相比 Vuex 最大的优势是:API 更简洁,TypeScript 支持更好,模块化默认就是按 store 拆分的,不再需要麻烦的 module 注册逻辑。
我的实际项目里 Pinia 主要管理两类数据:一类是登录用户的全局信息(用户ID、昵称、权限列表),另一类是跨页面共享的业务数据,比如购物车数据。定义 store 的写法很直接:
export const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: null }), actions: { async login(formData) { const res = await loginApi(formData) this.token = res.data.token localStorage.setItem('token', res.data.token) }, async logout() { this.token = '' this.userInfo = null localStorage.removeItem('token') } } })有个和 localStorage 同步的细节提醒:state 初始化和突变时都要同步到 localStorage,否则页面刷新后 store 数据就丢了,又要重新登录。
3.5 路由权限控制:动态路由如何加进项目
前端项目只要涉及登录,就一定绕不开路由权限控制。思路分为两种:简单场景用导航守卫判断是否登录,复杂场景用动态路由按角色加载可访问页面。
导航守卫的方案比较直接,在router.beforeEach里做拦截,判断目标路由是否需要认证。如果需要,并且本地没 token,就跳转到登录页;如果有 token 直接放行。这个方案适合后台管理系统中大部分页面都需要登录的情况。
动态路由适合权限区分明确的系统。用户登录后从接口拿到角色和权限标识,前端根据这些信息动态 addRoute。比如管理员登录能看到用户管理页,普通用户看不到。这个方案实现难度高一些,但灵活性强。我建议一开始就把权限模块设计好,哪怕初期权限很简单,也先用统一方案做,不然后面加需求改起来很痛。
4. 前后端联调:从接口文档到跨域处理
4.1 接口文档先行,别让联调变成灾难
前后端并行开发的团队里,最大的矛盾点往往不是技术难点,而是接口约定不一致。前端按自己的理解调接口,后端按自己的理解返回数据,两边各写各的,联调时互相甩锅。要解决这个问题,唯一的出路是接口文档先于代码。
我的工作流程是:先和前端一起梳理页面需要的所有接口,列出接口清单,包括 URL、请求方式、请求参数、返回数据结构、错误码含义。然后按这个清单开发,前端也按它做 mock。接口文档用 Apifox 这类工具维护,它支持从代码注解自动生成文档,也能在线调试,还能生成 mock 数据,效率比 Word 文档高太多了。
4.2 开发环境跨域问题的几种解法
前后端分离之后,开发阶段跨域几乎是必踩的坑。前端跑在 5173 端口,后端跑在 8080 端口,浏览器的同源策略直接把请求拦了。解决办法有两种思路:
第一种,后端开启 CORS。在 Spring Boot 里写一个配置类,实现 WebMvcConfigurer 的 addCorsMappings 方法,配置允许的域名、请求头和请求方法。这个方案适合简单场景,但生产环境一般不推荐全放开的配置,如果有安全需求要严格限制来源。
第二种,前端 Vite 配置代理,这也是我更推荐的开发环境方案。Vite 启动的服务收到/api开头的请求后,转发到后端服务,浏览器眼里请求是同源的,根本没触发跨域机制。配置在vite.config.js里:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })生产环境用 Nginx 做反向代理,原理一样:把/api转发到后端服务。整个链路下来,前后端代码里不需要处理任何跨域逻辑,跨域问题在网关层就解决得干干净净。
4.3 高频联调问题清单:字段不一致、时间格式、null 处理
联调阶段最容易出问题的,往往在小细节上。我总结几个高频问题类型:
字段命名不一致:后端习惯下划线命名(user_name),前端习惯驼峰(userName)。解决方案是后端在全局配置里开启 Map 下划线转驼峰,MyBatis Plus 有现成配置,或者序列化时用命名策略转换。提前约定好这事,能省大量修改工作。
时间格式不统一:后端返回的是2025-03-20T10:30:00,前端要显示2025-03-20 10:30,两边各做各的转换,容易出幺蛾子。建议后端统一返回特定格式,在application.yml里配置全局时间格式化,或者在 Jackson 的配置类里统一指定格式。
null 处理:列表接口里某条数据的某个字段是 null,前端渲染出来显示成空页面或者“undefined”。这个在开发阶段就得小心,前端的展示逻辑要兼容 null 值,后端的返回数据里能去掉的空字段也可以直接用@JsonInclude(Include.NON_NULL)全局忽略。
4.4 调试技巧:前后端联合排查的思路
联调时接口有问题,先别急着提交代码,按照固定顺序排查能节省大量时间:
- 第一,看浏览器 Network 面板,确认请求是否发出、URL 是否正确、请求头和参数是什么
- 第二,看请求响应状态码,4xx 大概率是参数或权限问题,5xx 大概率是后端代码问题
- 第三,看响应内容,把接口返回的 JSON 复制到格式化工具里,确认返回结构是否符合预期
- 第四,如果是 5xx,看后端日志,重点看异常堆栈的第一行,一般就能定位问题
这个排查顺序的逻辑是:从请求出发,确认前端的锅;然后到响应,确认后端的锅;最后到异常,定位代码问题。有了合理流程,就不会出现两边对着同一个接口猜来猜去的情况。
5. 生产环境部署与上线检查清单
5.1 前端构建与 Nginx 配置的最佳实践
前端部署的本质只有一个:把源码编译成静态文件,然后用 Web 服务器托管。Vue 项目执行npm run build,生成一个 dist 目录,里面是打包好的 HTML、JS、CSS。关键点是 Nginx 的配置。
第一个要点:单页应用的路由是前端控制的,刷新页面时浏览器会按 URL 去找资源,但是服务器上并没有这个路径对应的文件,所以 Nginx 必须配置 try_files 把所有请求回退到 index.html,让前端路由接管。
location / { root /usr/share/nginx/html; index index.html; try_files $uri $uri/ /index.html; }第二个要点:/api 开头的请求要反向代理到后端服务,不要直接去找静态文件。
location /api/ { proxy_pass http://localhost:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }第三个要点:静态资源缓存。JS、CSS、图片这些打包出来的文件,文件名带了 hash 值,内容变了文件名就变,可以放心设置长时间缓存。HTML 文件设置为不缓存,保证用户能拿到最新的入口文件。
5.2 后端打包:从 Jar 包到 Docker 容器化
Spring Boot 项目的部署方式,我经历了从“手动跑 Jar 包”到“Docker 容器化”的过程。手动跑 Jar 包很简单,mvn clean package生成 jar,然后java -jar启动。但问题也明显:环境依赖不一致、日志管理麻烦、进程挂掉没人管、升级要手动操作。
现在我的标准做法是写 Dockerfile,把应用做成镜像:
FROM openjdk:17-jdk-alpine WORKDIR /app COPY target/mall-server.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]再用 docker-compose 把后端和数据库编排在一起,一条docker-compose up -d命令搞定整套环境。数据库用独立的容器挂了数据卷,数据不会丢。这套方案的好处是:本地环境、测试环境、生产环境完全一致,不会出现“我本地跑得好好的,到你那里就报错”的问题。
5.3 反向代理与 HTTPS:上线前必须确认的安全项
部署上线前,有几个安全项一定要过一遍。首先是 HTTPS。现在的浏览器对 HTTP 网站越来越不友好,很多新特性都被限制,而且用户在地址栏看到“不安全”的标识,对产品信任感影响很大。用 Nginx 加证书的过程不复杂,证书可以通过各种云服务商免费申请。
其次是敏感信息的保护。数据库连接串、密码、密钥这些本来写在 application.yml 里的配置,建议改用环境变量注入,避免打包后的 Jar 包里藏着生产环境凭据。Docker 环境里可以用环境变量或者 Docker Secrets 来管理,不要图方便硬编码在镜像里。
最后是日志的合理配置。Spring Boot 默认日志输出到控制台,容器化环境下靠 docker logs 查看,文件一多就难查。建议配置 logback 把日志输出到文件,按天滚动,同时保留最近 N 份。配合问题排查时,这些日志就是救命稻草。
5.4 上线检查清单:避免“上线即事故”的实操经验
我把自己踩过坑之后的教训总结成一张检查清单,每次上线前逐项确认:
- 数据库执行过最新版本的初始化脚本,字段变更用迁移工具管理
- 配置文件使用环境变量,没有敏感信息硬编码
- 后端接口做过简单的并发测试,确认没有明显的性能瓶颈
- 前端打包产物上传到了 CDN 或服务器,不存在绕开缓存的问题
- Nginx 配置的 try_files 和 /api 代理都验证过
- 登录流程完整走一遍:注册、登录、token 过期、刷新登录
- 退出登录后不能通过浏览器返回键看到敏感页面
- 挂掉一个后端实例不影响整体可用性(如果有多实例部署)
每次照着这个清单过一遍,上线后的突发事故率降得特别明显。清单看着简单,但每一条背后都是真金白银的教训。
6. 常见问题排查与避坑记录
6.1 典型问题速查表,照着排查省时间
整理一个高频问题速查表,遇到问题先对着看:
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
| 登录后请求一直 401 | token 没存到请求头 | 检查 axios 拦截器是否在每次请求时加 Authorization |
| 跨域请求被拦截 | 后端 CORS 未配置或配置错误 | 开发环境优先用 vite 代理,生产用 Nginx |
| 刷新页面 404 | Nginx 没有配 try_files | 把所有路由回退到 index.html |
| 前端拿到的时间格式不对 | 前后端时间格式约定不一致 | 后端全局配置时间格式化 |
| 中文乱码 | 请求或响应编码不一致 | 统一 UTF-8,检查 Nginx 配置的 charset |
| 接口返回慢 | SQL 缺索引或 N+1 查询 | 慢日志定位 SQL,看执行计划 |
| 文件上传失败 | Nginx 的 client_max_body_size 限制 | 调大配置项 |
单独解释一个高频问题:404。很多人部署 Vue 项目上线之后,访问首页没问题,一点页面刷新就 404。这个基本就是 Nginx 配置的 try_files 没写,或者写完 Nginx 没重载。我之前第一次部署时也踩过一次,后来理解前端路由和静态文件服务器的关系之后,再也没犯过。
6.2 Security 相关坑:认证过滤器与静态资源放行
集成了权限认证的框架之后,最大的坑就是过滤器拦截了不该拦截的路径。最典型的是:登录页面、注册接口、验证码接口、静态资源(图片、CSS、JS)全部被拦截,导致前端打开登录页就报 401。
解决思路是梳理好“白名单”路径。在拦截器配置里,把所有公开访问的接口和资源路径放行,剩下的才走认证逻辑。维护这个白名单要细心,新增公共接口时顺手加进去,不然后端上线一个公开的接口却被认证拦截,莫名其妙多一个 bug。
另一个坑是:过滤器的执行顺序。如果同时有多个过滤器,顺序调整起来很麻烦,尤其是 Spring 的 OncePerRequestFilter 和各种第三方过滤器混在一起时,要确保认证过滤器在正确的位置执行,不然 token 解析的结果还没放进上下文,接口就被后面的过滤器拦了。
6.3 事务和并发问题的几个复盘
业务系统里最容易翻车的两类坑,事务不生效和并发覆盖。
事务不生效最常见的三种情况:方法不是 public 的、同类内部直接调用导致代理失效、异常被捕获后事务感知不到。我复盘过一次典型的例子,删除订单的业务里,删除订单和写操作日志放到了同一个类里的两个方法,日志那个方法异常被 try catch 吞了,整个事务回滚不了,订单删了但日志没了。改法很简单:把操作日志做成独立服务类注入调用,或者用编程式事务手动控制。
并发覆盖的典型场景是库存扣减。两个用户同时下单,都读取到库存为 1,都执行减库存操作,最后库存变 0 但卖出了两单。解决思路一般是用数据库乐观锁,在库存表加 version 字段,update 时带上条件判断 version 是否有变化,如果变化说明被别的请求修改过,重试或者报错。这个问题的本质是:先读后写这种模式必须处理并发环境下的数据一致性,不能想当然。
6.4 前端的几个隐蔽 Bug 复盘
前端也有一些看着诡异,实际是细节没处理好的问题。第一个是异步更新问题:数组里一个对象的属性改完之后,页面没更新。原因是 Vue 3 的响应式代理可以捕获数组下标修改了,但深层的嵌套属性如果初始化时不存在,且没通过响应式 API 添加,就不会触发更新。解决办法是初始化时把整个对象结构定义完整,或者用this.$set的思路在 Vue3 里就是直接重新赋值一个新的对象引用。
第二个是路由切换后页面数据残留。列表页滚动加载过的数据,跳到详情页再返回,列表还在原来的位置,虽然这是个体验优化点,但没有需求时,可能导致数据状态不一致的问题。可以监听路由变化,重新初始化页面数据来避免。
第三个是组件卸载后的异步回调。页面里发起了一个请求,用户等不及直接跳走了,请求回来之后的代码还在执行,容易报 “Cannot read properties of null” 这类错误。应对方法是在组件卸载时标记状态,或者在异步回调里做组件销毁的判断,防止对话更新已经不存在的 DOM。
7. 项目质量的最后防线:测试与代码评审
7.1 后端接口测试:不止是单元测试
提到测试,很多人第一反应是写单元测试。但实际项目里,投入产出比最高的其实是接口级测试。我习惯用 Apifox 或者写一个简单的集成测试类,直接调用 Controller 接口,验证请求响应是否符合预期。接口测试覆盖的是完整链路:参数校验、业务逻辑、数据库操作、返回格式。一个接口一条测试用例,跑一次能发现的问题,比写几十个单测还多。
关键接口我建议必须测:登录、注册、数据列表、提交订单这类核心链路。写测试用例时,正常路径要测,异常路径更要测——参数缺失、参数类型错误、未登录访问、无权限访问。这些异常场景才是用户最容易触发的。
7.2 前端代码规范:从命名到状态管理的约定
前端项目人多之后,代码风格不统一是必然的事。我建议从一开始就立好规范,强制执行。我记忆中踩过最烦的坑是命名混乱:有的组件用 PascalCase,有的用 kebab-case,有的接口函数用中文拼音缩写。维护起来像是拆盲盒。
规范至少要覆盖这些方面:
- 组件文件名统一 PascalCase,比如 UserTable.vue
- 变量和函数用 camelCase,常量用大写 + 下划线
- 页面路由 path 全部小写,多单词用连字符
- API 接口单独放 api 目录,按业务模块拆分文件
- 状态管理统一走 Pinia,禁止随手在组件里写全局变量
规范落到代码里最有效的做法是用 ESLint 和 Prettier,提交代码前自动检查、自动格式化,从规则层面保证代码质量。
7.3 代码评审应该关注什么,而不是走过场
代码评审如果只是走过场,值不了多少钱。真正有效的评审,我觉得应该关注几个重点:
- 业务逻辑的正确性:实现是否覆盖了所有分支,边界条件是否处理
- 数据安全问题:有没有 SQL 注入风险、越权访问风险
- 异常处理:该捕获的异常有没有捕获,不该吞的异常是不是被吞了
- 性能和资源释放:有没有在循环里查数据库、有没有内存泄漏隐患
- 可测试性:代码能不能方便地写测试
评审中发现问题,建议直接说“这里有个问题”,不要纠结是不是某人的锅。评的是代码,不是人。氛围融洽的团队,代码质量才提得上去。
8. 个人经验总结与后续扩展建议
做完几个全栈项目之后,我的体会是:全栈开发最重要的不是会用多少个框架,而是能把前后端的边界理清楚。这个边界一旦清晰,很多问题都变得简单了——接口文档、联调协作、权限控制、异常处理,都是在边界上做文章。
回头来看这套技术选型,Spring Boot + Vue 在日常业务系统里具备足够的灵活性和可靠性。遇到高并发场景,可以在后端引入 Redis 做缓存、引入消息队列做异步削峰;遇到复杂交互场景,前端可以引入更重的状态管理方案或组件库。底层骨架不变,扩展能力都由生态提供。
后续想在这个项目上继续深入学习,我建议按这几条线走:一是把权限模型升级为 RBAC 完整模型,支持多角色多权限的动态路由方案;二是引入 Redis 缓存热点数据,再做接口幂等性设计;三是把部署升级为自动化流水线,提交代码自动构建、自动测试、自动部署到测试环境;四是补充完善的日志监控体系,生产环境出问题能快速告警定位。
全栈这条路没有捷径,但通过一个个项目的积累,踩过的坑会慢慢变成经验。上面写到的这些细节和方法,都是我实际项目中反复验证过的,希望能帮你少走一些弯路。最后想多说一句:不管技术栈怎么变,真正拿得出手的永远是解决问题的能力。框架学得多不如用得扎实,把一个项目从开发到上线完整走一遍,比看一百篇教程都有用。