前后端分离这套东西,在圈子里聊了好几年了,但从我自己带项目的体感来说,真正能跑通全流程的案例还是不多。很多人卡在“后端接口写好了,前端不知道该怎么调”或者“前端页面写完了,一对接就跨域报错”这种尴尬的节点上。今天分享的这个瑜伽馆管理系统,就是一个非常典型的SpringBoot+Vue+MyBatis+MySQL组合案例,业务上覆盖了会员、课程、预约、排课这些真实场馆管理场景,技术上把前后端分离从开发到部署的全链路都串起来了。哪怕你不是做瑜伽馆这个行业的,把它当成一个标准的Java全栈实战项目来参考,也完全值得花时间过一遍。
先给第一次接触这类项目的朋友说清楚这玩意儿到底能干什么。简单讲,它就是一个给瑜伽馆老板或者店长用的管理系统,功能上对标我们常见的会员管理系统:办卡、约课、上课打卡、课程排期、员工信息管理,这些日常运营动作都能在系统里完成。前台是Vue写的单页应用,负责页面展示和用户交互;后台是SpringBoot提供的接口服务,负责业务逻辑和数据存储;MySQL就是最终落数据的地方。整个项目不仅给了完整源码,连数据库脚本、接口文档、部署教程都带了,基本上你拿到手之后,按着步骤搭完环境,就能在本地把所有功能跑起来。
这个项目最适合三类人看:第一是正在做Java全栈课程设计或者毕业设计的同学,业务场景完整、技术栈主流,直接拿来改改就能用;第二是自学SpringBoot和Vue,但是一直缺一个完整串联项目的人,这个项目能帮你把零散的知识点拼成完整的地图;第三是想了解前后端分离项目从开发到部署完整流程的初级开发,你可以在里面看到接口怎么约定、跨域怎么处理、前端后端怎么联调,以及最后怎么打包上线。我下面会把整个项目从设计思路到实际部署一条线讲完,重点放在那些文档里没写、但实操中一定会遇到的坑上。
1. 项目整体设计与思路拆解
1.1 为什么选择前后端分离架构
在开始敲代码之前,得先想明白一个问题:为什么非要做前后端分离,而不是像十年前那样用JSP或者Thymeleaf把页面直接写在服务端里?
答案其实很实在。前几年我做过一个中小型健身房的管理系统,当时用的是Freemarker模板渲染,业务规模小的时候一切正常,但一旦页面上要加交互效果、要实时刷新数据、要对接微信端或者小程序,模板渲染就变得非常痛苦——前端要等着后端一起联调,改个样式还得重新部署服务。后来把项目重构为前后端分离,整个团队就舒服多了。
具体到这个瑜伽馆系统,前后端分离带来的好处是实打实的:前端只关心页面渲染和数据交互,后端只关心业务逻辑和接口输出,两边通过HTTP接口和JSON数据通信,各干各的活。而且Vue开发时自带热更新,改完代码浏览器立刻刷新,开发效率比模板引擎高太多。
还有一点很实际,瑜伽馆这种场景未来大概率要扩展小程序端或者手机H5端,如果一开始就是前后端分离,那小程序端可以直接复用后端的接口,前端页面重写一套就行,不用动后端代码。这种扩展性是模板渲染架构很难做到的。
1.2 瑜伽馆业务的三个核心模块
这个系统的业务建模是围绕瑜伽馆的典型经营场景来设计的。我拆开来看,核心就三大块。
第一个是会员管理。瑜伽馆的会员不是简单地存一个姓名和手机号就完事了,它有一套自己的计费逻辑:有按月的普通会员,有按次数的次卡会员,还有私教课包。每一种不同计费方式,在数据库表结构和代码逻辑上的处理方式都不一样。月卡会员主要看有效期,过期就要续费;次卡会员要记剩余次数,每上一次课扣一次;私教课包则可能要跟具体的教练绑定。这块是整个系统的地基,会员数据不对,后面所有业务都会跟着错。
第二个是课程与排课管理。瑜伽馆的课程种类很多——哈他瑜伽、流瑜伽、阴瑜伽、空中瑜伽、理疗私教等等。每一种课程有自己适合的教练、固定的时长、最多容纳的人数。排课就是把这些课程安排到一周的每一天、每一个时段,比如周二晚上7点有流瑜伽课,周五上午10点有空中瑜伽课。排课出来之后,会员才能去预约。这个模块对于馆方来说是运营的核心,怎么排、排什么课、谁来上,直接影响门店的收入和会员的满意度。
第三个是预约与打卡管理。会员约了一节周二的流瑜伽课,系统需要记录这个预约,到上课当天,会员到店后需要验证预约信息并打卡确认。这里还有一个容易被人忽略的细节:如果会员约了课但没来,也就是“爽约”,是扣次卡次数还是不做任何惩罚?不同场馆规则不一样。这个系统在业务设计上留有处理的余地,这也是我比较认可的一点。
1.3 数据库设计的几个关键决策
数据库设计这块,直接决定了后面写代码是省力还是费劲。这个系统的表设计有几个地方想得很清楚。
用户和角色的设计采用的是经典的RBAC模型,用户表、角色表、权限表、用户角色关联表、角色权限关联表,五张表一套带走。后面做登录和权限控制的时候非常清晰,不用在代码里写死逻辑。有些项目图省事,在用户表里加个role字段类型为字符串,比如"admin,teacher,member",这种设计能跑但非常不优雅,一旦要扩展权限点就会非常痛苦。
会员信息和用户信息是分开设计的。用户表里存的是账号、密码、手机号这类登录凭证信息,会员表里存的是会员等级、卡类型、剩余次数、有效期这些业务属性。为什么要分开?因为一个用户不一定永远是会员,会员可能过期,过期了可以续费,但用户账号还在。如果混在一张表里,会员过期就不知道该怎么处理用户数据了。分开设计的好处是,用户的生命周期和会员的生命周期可以独立管理。
课程、排课、预约这三张表的关系是这个系统里最微妙的地方。课程表定义的是“有什么课”,比如“哈他瑜伽”这个名字、时长、适合的人群;排课表定义的是“什么时候在哪儿上什么课”,比如“周二晚上7点在1号教室由李教练上哈他瑜伽”;预约表定义的是“谁约了哪一堂排课”。这三者的层级关系理清了,后面写查询和业务逻辑就顺了。特别是预约表,一定要同时存排课ID和会员ID,并且做唯一约束,防止同一个会员重复预约同一节课。
2. 技术选型深挖:为什么是SpringBoot+Vue+MyBatis
2.1 SpringBoot到底解决了什么问题
很多新手刚接触SpringBoot的时候,会有一个困惑:这东西和之前的SSM架构(Spring+SpringMVC+MyBatis)到底差在哪里?
我用一个最直观的对比来回答。SSM时代,你得先配置web.xml、配置Spring容器、配置SpringMVC的DispatcherServlet、配置数据源、配置事务管理器,光这些配置文件加起来就有几百行。而且每个项目都要重复来一遍,换个环境还经常因为某些细节配错了跑不起来。SpringBoot做的核心事情就是“约定大于配置”,它把那些繁琐的配置统统内置了,你只需要引入对应的starter依赖,项目就能自动完成配置。
比如你想在项目里用MySQL,引入一个mybatis-spring-boot-starter和mysql-connector-java,再把数据库连接信息写到application.yml里,就完事了。不需要手动创建DataSource的Bean,不需要写SqlSessionFactory的配置类,SpringBoot会自动帮你搞定。这就是为什么现在企业里新项目基本都选SpringBoot,开发效率差太多了。
这个瑜伽馆系统在SpringBoot的运用上也没整那些花活儿,就是老老实实按分层架构来:Controller层接收参数和返回结果,Service层写业务逻辑,Mapper层操作数据库。这种情况下,SpringBoot的自动配置、依赖注入、事务管理等能力正好能发挥最大的作用,不复杂也不绕。
2.2 MyBatis和MyBatis-Plus的选择思路
先统一一个认知:MyBatis本身是一个持久层框架,它的核心优势是让你自己写SQL,把SQL语句和Java方法的映射关系通过XML文件或者注解来管理。熟悉SQL的人用MyBatis会非常顺手,因为数据的查询逻辑完全在自己掌控之中,不像JPA那样需要理解它那一套方法命名规则和自动生成的SQL逻辑。
这个项目用的是原生MyBatis,我觉得是有意为之的。对于学习型项目来说,用原生MyBatis反而能让你把SQL写明白。因为你在XML里写的每一条SQL都是显式的,比如查询会员列表、判断某个时段是否已经有排课、统计某节课的预约人数,这些业务逻辑对应的SQL你都能看得一清二楚,不容易出现“用MyBatis-Plus写对了方法但不知道底层执行了什么SQL”的情况。
当然,如果你是在实际企业项目里做开发,我一般建议用MyBatis-Plus,因为单表CRUD真的不需要手写SQL,它内置的Wrapper机制帮你省掉很多模板代码。但对于这个瑜伽馆项目,牵扯到多表联查的场景不少,原生MyBatis在XML里写关联查询更加清晰可控。等你在项目里把原生SQL练熟了,再切到MyBatis-Plus会非常有感觉——你就知道它到底帮你省了哪些事。
2.3 Vue这层为什么选2而不是3
现在Vue 3已经是主流了,Composition API、TypeScript、Vite这些都很好用。但这个项目用的是Vue 2,我猜测有几个现实层面的考虑。
第一是生态成熟度。Vue 2的Element UI、Vue Router、Vuex这些配套库都发展了很多年,文档齐全、案例丰富、遇到的问题网上基本都有答案。对于学习者来说,这意味着你踩到坑的时候有大把的解决方案可以查。Vue 3虽然也好,但生态里的一些库版本更迭过程中偶尔会有兼容问题,新手容易被卡住。
第二是入门门槛。Vue 2的Options API对新手更友好——data写数据、methods写方法、computed写计算属性、watch写监听,结构非常清晰,一看就懂。Vue 3的Composition API功能更强大,但上手理解的曲线要陡一些。如果你是第一次接触Vue,从Vue 2入手会更顺畅。
第三,也是最重要的:Vue 2项目的开发思想和Vue 3是打通的。你在Vue 2里学到的组件化、路由、状态管理、生命周期、异步请求这些核心概念,迁移到Vue 3完全没有障碍。所以不要太纠结于版本,先跟着项目把Vue的整个工作流跑通,后面再花个两三天去了解Vue 3的差异点,知识迁移非常自然。
2.4 MySQL表结构设计的关键点拆解
这个系统的数据库脚本我建议拿到手之后先别急着execute,而是花点时间把每一张表过一遍。表结构设计是一个项目最底层的逻辑,你把这个搞清楚了,再去看代码就是降维打击。
我挑一张典型的表来说。预约表(appointment/booking表)的设计,核心字段至少包括:主键ID、排课ID、会员ID、预约状态、创建时间。这里有一个我特别想强调的点:预约状态为什么要单独设一个字段?因为业务上“已预约”“已到店”“已爽约”“已取消”这几个状态需要区分。如果你不设计状态字段,想要判断一个会员到底有没有来上课,就得去关联打卡表查,非常麻烦。加一个状态字段,用不同的值映射不同的业务状态,查询的时候一个WHERE status = ?就搞定了。
再比如会员表的设计。会员表里通常会有一个“卡类型”字段和一个“剩余次数”字段。卡类型可以用数字标识,比如1代表月卡、2代表次卡、3代表私教课包,然后用代码里的枚举去解析它。但是这里要注意一个细节:如果只是存一个数字,时间久了后面接手的开发者可能就忘了1到底是什么意思,所以要么在上面的常量类里写清楚注释,要么在表字段的comment里写得明明白白。这个项目的数据库脚本里注释写得比较详细,这一点我点个赞——写数据字典是一个很好的习惯,可惜很多项目都不重视。
还有一个表设计上的小细节:时间字段统一用datetime类型,不要用varchar存时间字符串。为什么?因为datetime类型在MySQL里可以直接用日期函数进行比较和计算,比如要查“今天有哪些课”,一个WHERE date(start_time) = CURDATE()就搞定了。你要是用varchar存,这种查询就没法走索引,数据量上来之后性能会非常差。
3. 核心功能实操细节与代码导读
3.1 前后端接口怎么约定和对接
前后端分离项目能不能开发顺畅,很大程度上取决于接口约定是否清晰。我在做这个项目的时候,坚持一个原则:接口返回的数据结构必须统一。
后端约定所有接口返回的JSON格式都是这样的:
{ "code": 200, "message": "success", "data": { } }code是业务状态码,200代表成功,其他值代表不同的失败原因,比如401表示未登录、403表示无权限、500表示服务器内部错误。message是给前端展示的提示信息,data是真正的业务数据。
这个约定看起来简单,但实际在项目中带来的好处特别大。前端可以封装一个统一的请求工具类,在拦截器里统一处理状态码,不需要每个接口都写一遍成功和失败的判断逻辑。
举一个前端封装的例子,用axios的话大概是这样:
import axios from 'axios' const service = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动附带token service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) // 响应拦截器:统一处理code service.interceptors.response.use(response => { const res = response.data if (res.code !== 200) { if (res.code === 401) { // 未登录,跳转到登录页 router.push('/login') } return Promise.reject(new Error(res.message)) } return res.data }) export default service这段代码非常重要,它解决了两个问题:一是所有请求自动携带token,不用每个接口单独写;二是后端返回的code不是200时,可以在统一的地方处理错误提示和登录失效跳转,不用每个页面都重复写逻辑。
再看后端Controller层的写法,拿登录接口举例:前端把用户名和密码通过POST请求发给/api/user/login,后端接收后调用Service层去数据库里查用户,校验密码,然后生成一个token返回给前端。token的生成方式很多,可以用JWT,也可以用UUID配合Redis存session。这个项目用的是JWT方案,好处是无状态——后端不需要存session,前端每次请求带上token,后端解析token就能知道当前是哪个用户。
3.2 会员预约与排课冲突校验的实现逻辑
预约功能是这个系统里业务逻辑最密集的一个模块,牵扯到的细节非常多,我给你拆解开来说。
先想一个业务场景:一个普通会员想预约周三晚上7点的流瑜伽课,这节排课对应的课程最多容纳12人。系统在接收到预约请求的时候,至少要判断三件事:这个会员是否登录并且有预约权限;这门课是否还有剩余名额;这个会员是否已经预约过同一个时段的课程了。三个条件都满足,才能预约成功。
先说剩余名额的判断。最简单可靠的办法是:在排课表(course_schedule表)里设计一个max_count字段和一个current_count字段,每次有人预约成功,就在一个事务里执行UPDATE course_schedule SET current_count = current_count + 1 WHERE id = ? AND current_count < max_count。这个SQL写法有一个好处——它把“判断是否有名额”和“更新名额”合并成了一条原子操作,在高并发场景下不会出现超卖问题。如果这次UPDATE影响的行数是0,就说明已经没有名额了,直接返回“本次课程名额已满”就可以。
再说重复预约的判断。这个需要用会员ID和排课ID去预约表里查,如果查出已经有记录且状态不是“已取消”,就说明已经约过了,不能再约。同时,预约表的这两个字段要建一个唯一索引,双保险,防止并发场景下的重复插入。
还有一个容易漏掉的细节:如果同一名会员预约了两节同一时段、但不同教室的课,这种算不算冲突?严格来说应该算,但这需要额外的业务判断逻辑——查一下该会员已预约的排课记录里,有没有时间段跟新课重叠的排课。这个查询写起来稍微复杂一点,但逻辑上是完备的。有些初级项目不做这个判断,就会导致会员同时约了两节课的极端情况。
整个预约流程为了保证数据一致性,建议在Service方法上加上@Transactional事务注解。一旦最后一步执行失败,前面加了的当前count要回滚,已插入的预约记录也要回滚,不能出现“预约记录写了但还没扣名额”这种中间状态。
3.3 权限控制与登录状态的实现思路
前后端分离项目做权限控制,跟传统的Session方案有一个关键区别:传统的Session方案天然具备“服务端状态”,登录之后服务端记住了你是谁;但前后端分离强调无状态,服务端不保留会话信息,怎么识别“你是你”?
常见的方案是用token。用户登录成功后,后端根据用户ID、角色等信息生成一个token返回给前端。前端把token存在localStorage或者sessionStorage里,每次发起HTTP请求都在请求头里带上这个token。后端通过拦截器或者过滤器解析请求头里的token,解析成功就放行,解析失败就返回401。
这个系统的做法是:定义了一个拦截器,在SpringBoot里就是实现HandlerInterceptor接口,在preHandle方法里读取请求头中的token,调用工具类解析,把解析出来的用户信息放到ThreadLocal或者HttpServletRequest的属性里,方便后面的Controller方法取用。对于登录接口本身和注册接口,这一类不需要认证的接口,要用配置方式放行,不要拦截。
从角色权限的角度来看,后端在拦截器里只做了“是否登录”的判断,那管理员和普通会员的权限怎么区分?这个要结合RBAC模型去做。管理员可以访问“课程管理”“会员列表”“排课管理”这些管理类接口,普通会员只能访问“我的预约”“我要约课”这类型接口。实现方式主要有两种:一种是在接口方法上加自定义注解,比如@RequirePermission("admin"),由AOP统一校验;另一种是在拦截器里根据请求URL路径去判断。小项目用简单的路径判断就够了,比如/admin/**开头的接口要求管理员权限,/member/**开头的接口要求登录用户权限即可。灵活度高,代码也不复杂。
3.4 Vue端核心页面与接口调用的串联
前端这块,我挑几个关键的页面来说说它的实现逻辑。
登录页是整个前端系统最先被访问的入口。用户在登录页输入账号密码,点击登录按钮后,前端调用登录接口,成功后把token和用户信息保存下来,同时根据角色跳转到不同的首页——管理员跳转到管理后台,会员跳转到课程列表页。这里有一个常见的坑:刷新页面后,Vuex里的用户状态会丢失,因为Vuex是内存存储。解决方案是在App.vue的created生命周期里,读取localStorage中保存的用户信息,重新填充到Vuex里。否则一刷新页面,页面就以为用户没登录了,这是很多新手最容易踩的坑。
课程列表页的逻辑也很有代表性。页面加载时请求后端接口拿课程列表数据,展示课程名称、课程时间、教练、剩余名额等信息。Vue 2里这个过程很清晰:created钩子里调用methods里的fetchCourseList方法,该方法通过axios请求接口,拿到数据后赋值到data里的courseList字段,模板里用v-for把列表渲染出来。如果需要“只展示今天可预约的课程”这种筛选,一种做法是后端接口接收日期参数做过滤,另一种是前端在拿到全量数据后用computed做前端过滤。推荐前者,因为数据量和并发上来之后,前端过滤的效率远不如后端SQL过滤。同理,在分页需求上,建议后端做真分页,而不是一次性返回所有数据让前端自己分。
管理端的页面上,课程管理、会员管理、排课管理这些表格类的页面,本质上都是“表格+表单弹窗+删除确认”的组合。拿课程管理举例,管理员打开课程管理页面,看到课程列表,可以点击新增课程打开一个表单弹窗,填完提交之后刷新列表;可以点击编辑回填当前数据修改后提交;可以点击删除,弹出一个确认框,确认后删除并刷新列表。这类CRUD操作在Vue里写起来很有套路,Element UI的el-table加el-dialog组合就能搞定,重点在于每次操作之后要重新请求列表数据,保证页面显示跟数据库实时同步。还有一些页面需要做条件查询,比如按课程名称模糊搜索、按教练筛选,这些搜索条件是通过请求参数传给后端的,后端在SQL里用<if>动态拼接查询条件来实现。
4. 从源码到部署:本地环境搭建与全流程实操
4.1 环境准备:JDK、Node、MySQL的版本选择和安装
实战项目最大的拦路虎不是代码本身,而是环境。我先说版本问题,这是最容易踩坑的地方。
后端如果用的是SpringBoot 2.x,那JDK选择8或者11都可以。但如果你下载的是SpringBoot 3.x,那就必须用JDK 17以上,因为SpringBoot 3是基于Jakarta EE的,底层改了。这个项目用的JDK版本在文档里应该有说明,一般配套SpringBoot 2.x的就是JDK 8——这也是目前国内大多数中小型公司的主流配置。
JDK安装没什么技术含量,但要记住设置JAVA_HOME环境变量,并且把%JAVA_HOME%\bin加入Path。装完在命令行里敲java -version验证一下,就说明环境变量的配置没有生效。
前端需要安装Node.js。这里有一个非常重要的点:不要无脑装最新版Node,Vue 2项目用Node 14或者Node 16是最稳的,因为高版本的Node可能导致老项目的依赖安装报错。装完Node之后,npm会自带,可以用npm -v验证。考虑到国内网络环境,建议把npm的镜像源切换成淘宝镜像,命令是npm config set registry https://registry.npmmirror.com,否则下载依赖的时候会等到怀疑人生。
MySQL的版本建议用5.7或者8.0,这两个版本是目前国内使用最广泛的。安装过程中要注意:MySQL 8.0默认的认证插件是caching_sha2_password,而某些老版本的数据库连接驱动不支持这个认证方式,会出现连接报错的情况。如果你用的是MySQL 8.0,建议在创建数据库用户的时候使用mysql_native_password认证,或者确保你引入的mysql-connector-java版本在8.0以上。
4.2 数据库初始化与配置
拿到项目源码后,数据库这块一般会有两种交付形式:一种是SQL脚本文件,你需要手动导入;另一种是项目启动时会自动执行初始化脚本,前提是你在配置里开启了spring.sql.init相关选项。这个项目给的是SQL脚本,我们手动导入。
首先打开MySQL命令行,或者用Navicat、MySQL Workbench这些图形化工具,创建一个新的数据库:
CREATE DATABASE IF NOT EXISTS yoga_studio DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;然后切换到该数据库,导入项目里提供的yoga_studio.sql文件:
mysql -u root -p yoga_studio < yoga_studio.sql导入完成之后,用SHOW TABLES;看看表是否都建出来了。这一步能提前排查SQL脚本和当前MySQL版本是否兼容,如果某条建表语句报错,大概率是数据类型或索引语法不兼容。
接着修改后端的数据库连接配置,位置在src/main/resources/application.yml:
server: port: 8080 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/yoga_studio?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai username: root password: 你自己的数据库密码在url参数里,useUnicode=true&characterEncoding=utf8是为了解决中文乱码问题,serverTimezone=Asia/Shanghai是为了避免MySQL连接时的时区报错。这两个参数是从老项目里继承过来最容易出问题的地方,别省略。
4.3 后端启动:从IDE到命令行
如果你用的是IntelliJ IDEA,直接用IDEA打开后端项目,等待Maven下载完依赖后,运行主启动类(通常是一个带有@SpringBootApplication注解的类)里的main方法。看到控制台输出Started Application in ...并且没有任何异常,就说明后端启动成功了。
如果你更习惯用命令行操作,可以用下面这种方式:
cd backend mvn clean package -DskipTests java -jar target/yoga-system-0.0.1-SNAPSHOT.jar第一次执行mvn clean package的时候,Maven会从中央仓库下载大量依赖包,速度取决于你的网络状况和镜像源配置。如果你的Maven下载很慢,建议在settings.xml里配置阿里云镜像,这一步能帮你节省大把时间。
后端启动成功之后,可以用浏览器直接访问一些不需要登录的接口验证一下,比如http://localhost:8080/api/course/list,如果返回了课程的JSON数据,说明后端服务、数据库连接、接口路由都没问题。
4.4 前端启动:依赖安装与本地开发服务
前端项目的启动流程相对固定。进入前端目录,先安装依赖:
cd frontend npm install这里我提前打一个预防针:npm install非常容易报错。最经典的错误是node-sass安装失败,因为node-sass是C++模块,需要本地编译环境,Node版本不匹配的时候必挂。如果你是Vue 2项目且用到了node-sass,建议检查一下它要求的Node版本;实在不行,可以把node-sass换成dart-sass,也就是sass包,API基本兼容,安装过程要友好得多。
依赖安装完成之后,启动开发服务器:
npm run serve默认情况下Vue CLI项目的开发服务器跑在http://localhost:8081。打开浏览器访问这个地址,应该能看到前端页面。这里通常会出现一个所有前后端分离新手都会遇到的问题:跨域请求被浏览器拦截。因为你的前端在8081端口,后端在8080端口,两个端口不同,浏览器同源策略就会拦截请求响应。
解决办法在开发阶段最常用的有两个。第一个是在前端配置代理转发,以Vue CLI为例,在vue.config.js里加一段配置:
module.exports = { devServer: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }这段配置的意思是:当页面发起/api开头的请求时,开发服务器自动把它转发到http://localhost:8080。这样浏览器端看到的请求还是同源的,就不会被拦截了。注意这里pathRewrite的问题——如果你的前端请求路径是/api/user/login,而后端Controller的路由是/user/login,那你需要做路径重写;如果后端接口本来就以/api开头,就取消pathRewrite。
第二种做法是在后端加CORS全局配置,加一个配置类实现WebMvcConfigurer接口,重写addCorsMappings方法,允许所有来源访问。这种方案在开发阶段方便,但在生产环境要小心,因为跨域配置太宽泛会带来安全隐患。
4.5 生产环境打包:前端构建与后端部署
开发完成之后,最终是要部署到服务器上的。前后端分离项目在部署阶段有一个经典的设计取舍:到底是把前端打包出来的静态文件扔到后端的resources/static目录里,然后用同一个端口访问,还是让前端和后端分开部署、用Nginx做反向代理?
很多课程项目为了省事,直接把前端npm run build生成的dist目录复制到后端的静态资源目录里,这样整个项目成了一个单体应用,不用处理跨域。但这种做法的坏处也很明显:前端和后端无法独立发布,每次改个前端页面都要重新打后端包。我个人的建议是使用Nginx方案,这样才能真正发挥前后端分离架构的优势。
前端构建产物就一行命令:
npm run build执行完会在frontend/dist目录下生成一堆静态文件,包括index.html、js目录、css目录等。把这些文件上传到服务器的某个目录,比如/opt/yoga/frontend。
后端打包依然用Maven:
mvn clean package -DskipTests生成的JAR包上传到服务器的/opt/yoga目录下。用nohup java -jar yoga-system.jar > log.txt 2>&1 &命令后台启动。
然后在服务器上安装Nginx,配置一个server块,核心配置是这两层:
server { listen 80; server_name your_domain_or_ip; location / { root /opt/yoga/frontend; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }这个配置有几个讲究的地方。第一,try_files $uri $uri/ /index.html是Vue Router的history模式必须的配置,否则你直接访问某个子路由(比如/admin)的时候,Nginx找不到对应的静态文件,会返回404。第二,location /api/的代理转发把前端请求的/api前缀透传给了后端接口,也就是说后端接口路径本身是要带/api的。如果你的后端接口不带/api前缀,那这里就需要加rewrite命令做路径剥离。
配置好Nginx之后,nginx -t检查语法,然后nginx -s reload重载配置,整个系统就算正式上线了。
5. 常见问题排查与避坑实录
5.1 SpringBoot版本太高导致的各种诡异问题
先聊一个我在SpringBoot项目里遇到频率极高的问题:版本太高反而引发一系列兼容性异常。
身边不少同学跟着网上的教程做项目,习惯性地去Spring Initializr下载最新版本的SpringBoot,然后一旦项目里用的是老版本的MyBatis-Starter或者其他第三方依赖,就会出现各种莫名其妙的报错。比如ClassNotFoundException、NoSuchMethodException、Failed to configure a DataSource等等。
这种问题的排查思路很清晰:先查看你项目pom.xml里SpringBoot的父版本号,然后去查它对应依赖的兼容版本。SpringBoot 2.7.x就老老实实配MyBatis-Starter 2.x,别去用还处于预览版的MyBatis-Starter 3.x。成熟稳定、使用人数最多,这本身就是选型时的重要指标。如果你的项目报错实在找不到原因,不妨先检查一下是不是版本兼容的问题——把SpringBoot的版本降一档,问题往往就消失了。
5.2 MyBatis配置文件里的常见坑
MyBatis踩坑主要集中在两个地方。第一个是XML文件里的SQL占位符和业务参数对不上。比如你写了一个WHERE id = #{id},但是Mapper接口的方法参数名不叫id,程序运行时会直接报Parameter 'id' not found的错误。解决办法是给方法参数加@Param("id")注解,或者在XML里使用#{param1}这样的位置引用方式。我个人推荐前者,因为参数名写清楚之后,代码的可读性会好很多。
第二个常见坑是动态SQL标签写错了位置或者用错了标签。举一个很经典的例子,你写了一个带条件查询的SQL:
<select id="selectList" resultType="com.example.entity.Course"> SELECT * FROM course <where> <if test="name != null and name != ''"> AND name LIKE CONCAT('%', #{name}, '%') </if> </where> </select><where>标签会智能地去掉第一个多余的AND,但如果你不用<where>,而是自己写WHERE 1=1再加<if>判断,那建议你直接用<where>,因为它内置了“自动去掉第一个AND/OR”的逻辑,简洁得多,也不会出现SQL拼接出错的问题。
还有一个几乎人人都会踩的坑:MySQL查询结果映射成Java对象时,驼峰命名和下划线命名对不上。比如数据库字段是create_time,Java属性是createTime,默认情况下MyBatis是映射不上的,查询出来的对象这个字段就是null。解决方法是开启驼峰映射配置:
mybatis: configuration: map-underscore-to-camel-case: true这一个配置能帮你解决掉90%的字段映射问题,强烈建议写上。
5.3 Vue前端接口联调阶段的高频报错
前后端联调阶段,前端控制台最常见的报错就是403和404。
403通常是权限问题。最常见的原因是你请求的接口需要登录权限,但前端没有在后端请求中带上token,或者token已经过期了。排查思路是打开浏览器开发者工具,切到Network选项卡,点击那个报错的请求,看看请求头里有没有Authorization字段。如果没有,问题就出在前端的请求拦截器没有生效;如果有但后端还是返回403,那可能是token解析失败——比如密钥不一致、token过期。
404则要仔细区分。如果是请求路径404,先看后端控制台有没有对应的请求日志,如果连日志都没有,说明前端的请求路径和后端@RequestMapping注解里的路径对不上,重点检查接口前缀是否一致。比如后端是/api/user/login,前端请求的是/user/login,就差了那一个/api。这类问题在开发阶段最典型的触发点就是配了代理转发和没配代理转发时,前端请求的路径写得不一致。
还有一个很隐蔽的问题:Vue打包之后,页面加载正常但访问子路由刷新后404。这个就是我在部署部分提到过的try_files配置问题。如果你没用Nginx,直接把dist目录挂在某个静态服务器下,这个坑一定躲不掉。
5.4 MySQL连接和中文乱码问题排查
MySQL相关的报错里,最高频的两个是连接超时和中文乱码。
连接超时一般是URL里没有加serverTimezone参数导致的——报错信息通常包含The server time zone value 'Öйú±ê׼ʱ¼ä' is unrecognized。这种乱码一样的提示,其实就是服务器的时区值不被JDBC识别。解决办法就是在数据库连接URL后面加上serverTimezone=Asia/Shanghai,同时注意你的数据库驱动版本,MySQL 8.0以上需要用com.mysql.cj.jdbc.Driver,老版本驱动类名是com.mysql.jdbc.Driver,用错了也会报错。
中文乱码问题,核心在三个方面。第一,数据库本身要设置成utf8mb4字符集;第二,数据库连接的URL要加useUnicode=true&characterEncoding=utf8;第三,前端页面的<meta charset="UTF-8">和接口返回时的Content-Type要带charset=UTF-8。这三个环节任何一个断了,就会出现中文乱码。但一般开发阶段遇到的乱码,90%都是连接URL没配置编码导致的,优先检查这一项。
还有一个经常被忽略的细节:在使用MyBatis时,如果你往MySQL里批量插入数据,一定要在连接URL后面加上allowMultiQueries=true参数,否则批量插入的SQL执行时会直接报语法错误,而且报错信息比较隐蔽,不容易联想到这个参数上。
6. 项目二次开发与能力延伸建议
跑完整套流程之后,如果想把这次实战的价值最大化,我建议在现有基础上做一些二次开发的小练习,不需要多复杂,但每一项都能帮你把知识钉得更牢。
第一个建议:给系统加上“课程评分”功能。会员上完课后可以对这节课和教练打分,课程表或者教练信息那里展示平均分。这个功能会牵扯到新增评分表、编写评分接口、在课程详情页展示评分数据,前后端都要动,是一个非常好的全栈练习。而且它有一个很有意思的点——平均分是聚合数据,可以训练你用SQL写AVG函数的熟练度。
第二个建议:把前端的列表页加上真正的分页功能。现在很多课程项目列表页是把所有数据都查出来一次性渲染到表格里,但真实项目中数据量一大,这种写法就会把页面卡死。用MyBatis的PageHelper插件或者手写LIMIT加分页参数,把分页做完,对理解“前后端如何配合处理分页”特别有帮助。
第三个建议:完善会员的生日提醒功能。给会员表加一个birthday字段,然后在后端写一个定时任务,每天早上检查当天过生日的会员,给运营人员推送一条消息。这个功能会用到SpringBoot的@Scheduled注解,还能练习一下Quartz定时任务的基础用法。你不用真的接入短信或者微信推送,只要在控制台里打一条日志,流程跑通就算达成目标了。
另外说一下这个项目可以怎么魔改成其他行业。瑜伽馆管理系统的核心表结构是“会员-课程-预约-排课”,这个骨架换到健身房、舞蹈工作室、美容美发店都完全成立。你只需要改掉课程名称、服务项目这些业务字段,整个系统就能适配一个新的行业。这也是我为什么一直建议大家抽时间读一遍完整项目源码的原因——你掌握的是一套可复用的业务模型,而不是死记硬背某个行业的功能清单。
7. 写在最后的实操总结
项目完整跑通一遍之后,我强烈建议你做一个动作:不要急着开新项目,而是把源码从头到尾读一遍。别只写代码,一边读一边在注释里写自己的理解——这个Controller为什么要写这个校验逻辑,这个Service方法为什么事务要这么设计,这个Mapper的SQL为什么要这么写而不那么写。你自己动手写过一遍之后再读,感受会完全不一样。
我在实际带新人的过程中见过太多次这样的现象:项目能跑通,但问他“这个预约名额是怎么避免超卖的”或者“token过期之后前端是怎么处理跳转的”,就说不清楚了。能用代码和不会用代码,中间隔着的就是对这些细节的深度理解。这个项目最大的价值,不是帮你交一份课程作业或者完成一次实训,而是让你真正理解一个前后端分离系统从设计到上线的完整闭环。
最后再分享一个小技巧:如果你在读源码的过程中发现自己对某一块的理解特别吃力,比如MyBatis的动态SQL或者Vue的响应式原理,不用急着死磕。先把它记下来,然后去找三到五个同类的实战项目,在练习过程中反复对照,等到量积累够了,那些当初怎么都想不通的概念,会在某一天突然贯通。学习编程本质上就是一个“大量输入、反复实践、持续复盘”的过程,这个瑜伽馆管理系统就是你的第一块跳板。