ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

SpringBoot+Vue前后端分离雪具商城实战:从部署到联调全流程

SpringBoot+Vue前后端分离雪具商城实战:从部署到联调全流程 前阵子帮学生改了一套前后端分离的雪具销售系统技术栈就是SpringBootVueMyBatisMySQL。改完代码又顺手把部署流程从头跑了一遍结果发现很多拿着源码就能写CRUD的同学卡在部署和联调上本地启动连不上MySQL、前后端接口对不齐、Nginx配完刷新就404。这套系统如果只看代码不看全链路价值少了一大半。本文就按我实际走通的路子把设计思路、实现要点、数据层细节和完整部署过程一次讲清楚适合正在做毕业设计、想找前后端分离实战项目练手、或者准备SpringBoot方向面试的读者参考。1. 雪具商城系统的核心业务设计不是简单地堆增删改查1.1 功能模块拆分用户端与管理端到底该做哪些事很多同学拿到这类系统题目就开始写表先把商品表、用户表建出来然后对着页面一个个堆接口。这么做的结果往往是功能看着都有但聊起来一盘散沙。我一般先按角色拆业务域。这套雪具销售系统我按两端四个模块来划分用户端主要有商品浏览、商品搜索、详情查看、购物车、下单、订单查询和个人中心管理端主要有分类管理、商品管理、轮播图管理、订单处理、用户管理和销售统计。商品浏览不是简单查一张表就完事还要承接分类筛选、按价格或销量排序、分页这几件事。购物车看起来是临时数据但它牵扯到库存校验、价格变化、登录状态是前后端交互最密集的地方。订单模块更是这样它不是一个表就能说明白的订单主表、订单明细、商品快照、库存扣减、状态流转每个环节都会暴露出不同的技术点。管理端并没有多高深但它是检验后端设计是否合理的重要考场。商品管理要处理图片上传、字段校验、上下架状态订单处理要操作状态流转待付款、已付款、已发货、已完成、已取消销售统计则需要聚合查询如果一开始没预留好字段后面写统计SQL会很痛苦。1.2 数据库表设计从商品SKU到订单明细数据库是这个系统的地基。我设计表的时候有个习惯先把订单相关的表定下来因为订单表能反过来约束商品表、用户表的设计。核心表拆开大概是这几张用户表user、分类表category、商品表product、购物车表cart、订单主表orders、订单明细表order_item。另外可以加一张轮播图表用来支撑管理端首页配置。商品表是重点。雪具商品和普通服饰不一样同样的商品会有不同长度、不同固定器尺码、左右脚区分所以商品表要保留规格信息字段我用spec字段存JSON串例如雪板长度、硬度、适用水平而价格和库存这种参与计算的核心字段单独拎出来放price和stock。这样设计的好处是规格变化不会改动表结构SKU级别的复杂拆分会留给后续系统演进前期不会过度设计。订单明细为什么要做商品快照因为订单一旦生成商品名称、图片、单价这些信息不应该再去读商品表。如果商品改价了或者下架了历史订单里显示的仍然应该是下单那一刻的信息。这个细节很多课程项目不做但只要你做过一版真实上线的交易系统就会明白它有多重要。别问我怎么知道的。建表时还可以顺手定几个约定所有表主键用bigint自增时间字段统一datetime状态字段用tinyint并写清楚注释。用注释写清楚状态含义后面写代码会省很多事。1.3 为什么这套技术组合是稳妥的长线选择SpringBoot Vue MyBatis MySQL 这套组合看起来没有新鲜感但它确实是目前中小型系统里最稳妥、资料最多、面试最好聊的组合。先说SpringBoot它把Spring的配置复杂度降下来了内嵌Tomcat让项目能直接打jar包跑起来这对前后端分离项目的部署特别重要。Vue负责页面渲染和交互数据通过axios异步请求拿到DOM操作基本不用手碰。MyBatis的价值在于SQL可控尤其像销售统计这种需要写复杂SQL的场景在XML里调优比在ORM里绕来绕去痛快多了。MySQL则完全够用一套电商demo跑几百并发绰绰有余。前后端分离、SpringBoot、Vue、MyBatis、MySQL这组关键词也是目前招聘市场上出现频率最高的一套。用这套东西做完一个完整项目简历上有得写面试官有得聊后续想扩展也容易找到参考。2. 后端SpringBoot落地分层架构与鉴权方案怎么定2.1 工程目录与统一返回结构后端工程如果不规划目录写到最后一定是大泥球。我按标准四层结构走controller接收参数并做简单校验service写业务逻辑mapper负责SQL交互entity做数据映射。另外单独放一个common包管理统一返回结构、全局异常和通用工具类。统一返回结构我建议用一个ResultT泛型类包含 code、message、data 三个字段。code为0表示成功非0表示业务失败。千万别把HTTP状态码和业务状态码混着用我见过有人返回200但是业务失败也有人直接返回500给前端当成网络错误后期排查非常痛苦。代码大致长这样Data public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT result new Result(); result.code 0; result.message success; result.data data; return result; } public static T ResultT error(String message) { ResultT result new Result(); result.code 500; result.message message; return result; } }这样前端拿到响应后只需要判断code 0即可走正常逻辑异常统一在RestControllerAdvice里拦截然后转成相同的结构抛出去。全局异常处理这块一定要加否则数据库报错或参数校验失败时前端拿到的是一堆Tomcat默认的错误JSON根本无法友好展示。2.2 JWT登录鉴权拦截器、白名单与用户上下文这个系统我用的JWT做无状态鉴权。客户端登录成功后后端签发一个token前端后续每次请求放在请求头Authorization里后端拦截器验签通过后放行。但这里面有几个容易出问题的地方。第一是拦截器注册顺序和拦截路径。我习惯把登录、注册、商品列表、商品详情这些接口放白名单其余需要登录后才能访问。管理端接口额外校验角色余额检查或者越权校验可以在service层判断。拦截器里拿到token之后把用户id存进ThreadLocal里这样后续业务代码随时可以取到当前登录用户不用每个Controller都从参数里再传一遍。这里有个大坑一定要在请求结束或异常时调用ThreadLocal.remove()否则Tomcat线程池复用会串用户数据。我自己第一次写的时候就踩过这个线上用户A能看到用户B的购物车排查半天。给一个简化版的拦截器核心逻辑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) JwtUtil.verify(token)) { Integer userId JwtUtil.getUserId(token); UserContext.set(userId); return true; } throw new BusinessException(未登录或登录已过期); } Override public void afterCompletion(...) { UserContext.clear(); } }密码存储一定用BCrypt加密不要自己写MD5加盐更不要明文存。登录注册这种接口容易被扫描攻击我在全局过滤器里顺手做了XSS和SQL关键字的简易过滤也能挡掉一部分最基础的注入尝试。别指望过滤器能挡住所有攻击但聊胜于无项目展示时这也是一个说得出口的细节。2.3 MyBatis与MyBatis-Plus怎么配合用才顺手很多教程会把MyBatis和MyBatis-Plus对立起来讲其实它们是互补关系。简单单表CRUD用MyBatis-Plus的BaseMapper能省下大量样板代码比如用户表、轮播图表基本不需要写XML但是复杂查询、多表关联、统计报表还是自己写SQL更清晰。我的习惯是实体类和Mapper接口用MyBatis-Plus的注解或代码生成器搞定复杂查询写在Mapper XML里。比如后台的销售统计按日分组求成交金额这种SQL用MyBatis-Plus的Wrapper反而不太好写XML里两句SQL就出来了。还有一个容易被忽略的配置数据库表字段是下划线风格create_timeJava属性是驼峰风格createTime必须在application.yml里开启驼峰映射mybatis: configuration: map-underscore-to-camel-case: true不开启的话你会发现在实体类里给每个字段写一遍TableField和TableName注解也不是不行就是特别累。再就是XML文件一定要放在能被扫描到的位置IDEA里默认不会把src/main/java下的XML打包进target很多人部署到服务器才发现Invalid bound statement这个问题我在后面部署章节会再提一次。2.4 订单流程与库存扣减的并发细节订单模块是最容易杀人的地方。流程本身不复杂校验商品、计算金额、扣库存、生成订单、返回订单号。但扣库存和生成订单之间如果并发没控制好超卖是必然的。我采用的方案是乐观扣减。在product表上直接执行一行SQLUPDATE product SET stock stock - #{num} WHERE id #{id} AND stock #{num}返回受影响行数为1说明扣减成功为0说明库存不足这样能规避大部分并发问题。代码逻辑上先扣库存再插入订单记录两条操作放到同一个事务里其中任何一条失败都整体回滚。这里需要强调的是不要在Java代码里先select stock再到内存里判断因为两个请求同时读到旧库存判断就会失真。订单号我用的规则是时间戳 用户id后四位 随机数。别只用自增id当订单号暴露给用户让别人通过订单号就能推断出你们的业务量不合适。3. 前端Vue落地接口封装、路由守卫与页面组件3.1 从脚手架搭建到目录规划前端这块我用的Vue 3。创建项目直接用官方脚手架npm create vuelatest选择TS还是JS看个人习惯如果刚开始用Vue不久我建议先用JS把业务跑通不要一边学组合式API一边和类型系统搏斗。项目结构上我习惯按功能分包views放页面组件components放通用组件router放路由配置store放状态管理api放接口请求方法utils放工具函数。目录规划真正解决的是团队协作问题。前后端分离项目里前端页面往往是按路由拆的一个人负责商品模块一个人负责订单模块如果各自在页面里到处写axios后面合并代码会恨不得重写一遍。3.2 axios二次封装请求拦截与401处理axios不能每个页面裸着用一定要做二次封装。核心就两个点请求拦截器里加上token响应拦截器里统一处理业务码和登录过期。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 ! 0) { ElMessage.error(res.message) return Promise.reject(new Error(res.message)) } return res.data }, (error) { if (error.response?.status 401) { localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里有个小细节响应拦截器里判断res.code ! 0时可以直接用Element Plus的ElMessage弹提示。这样业务代码里就不用每个接口都写一遍错误处理了。我一般还会在这里统一做404和500的提示前端能少写很多重复代码。baseURL的取值要配合环境变量做区分。本地开发时走Vite的代理指向http://localhost:8080生产构建时走Nginx的同源路径这样打包出来的文件无论部署到哪里都不用改代码。3.3 路由守卫与前端权限控制前端权限如果做得很重比如按钮级别权限、角色动态路由那是另一个复杂度。这个系统我控制在两级未登录用户访问需要登录的页面直接跳回登录页非管理员访问管理端页面直接提示无权访问。Vue Router的beforeEach守卫里判断router.beforeEach((to, from, next) { const token localStorage.getItem(token) const role localStorage.getItem(role) if (to.meta.requiresAuth !token) { next(/login) } else if (to.meta.requiresAdmin role ! ADMIN) { next(/403) } else { next() } })这里要注意的是前后端分离项目里前端路由守卫只是体验优化真正的权限校验永远在后端接口上。前端跳过了只是看不到页面但直接调接口还是能拿到数据所以后端接口的管理员权限校验一定不能省。我在后端用了两个拦截器一个校验登录态一个校验管理员角色两个同时生效。3.4 商品列表、购物车与订单页的组件化实现商品列表和购物车是用户端使用频率最高的页面。商品卡片我抽成了ProductCard组件接收一个商品对象展示图片、名称、价格和库存状态。列表页负责请求分页数据然后循环渲染组件。购物车我用Pinia管理并做了localStorage持久化。好处是用户刷新页面购物车不丢也能在未登录状态下把本地选中的商品合并到登录后的购物车。合并逻辑虽然不复杂但很锻炼处理边界情况的能力比如同一个商品用户可能选了两件不同尺码合并时不能直接覆盖要按规格维度累加。订单页提交时前端需要同时把商品列表、收货地址、备注信息发给后端。这里我建议前端只组装参数不做金额计算因为金额计算和优惠逻辑都应该在后端完成。前端加上价格判断很容易被请求篡改金额以服务端为准。4. 数据层这几个坑实战中一定会碰到4.1 MyBatis缓存的正确打开方式MyBatis的一级缓存是SqlSession级别的默认开启。在Spring中每个请求会新建SqlSession请求结束就关闭所以一级缓存的影响不大。二级缓存是namespace级别的也就是一个Mapper一个缓存开启方式是在XML里加cache/但它的坑在于数据一致性。比如你查询商品列表时缓存了一份数据管理端后台修改了商品价格如果不主动清缓存前端拿到的还是旧数据。这个小项目里我没有开二级缓存因为查询压力没那么大不开少很多麻烦。如果后续并发真的上来了比起在MyBatis层面开二级缓存我更建议在Redis里做商品缓存并设置过期时间可控性会好得多。4.2 分页、模糊搜索与字段安全问题商品列表页必然要分页。我用了MyBatis-Plus的分页插件在配置类里加一个MybatisPlusInterceptor注册PaginationInnerInterceptor就行。模糊搜索的写法要注意SQL注入。规范化做法是XML里的like不直接拼字符串而是用CONCAT拼接select idpageByKeyword resultTypecom.example.entity.Product SELECT * FROM product WHERE name LIKE CONCAT(%, #{keyword}, %) if testcategoryId ! null AND category_id #{categoryId} /if ORDER BY create_time DESC /select这里坚持用#{}而不是${}。#{}是预编译参数占位符${}是字符串拼接后者一旦把前端传的值拼进SQL等于把注入漏洞敞开了。很多同学的第一个SQL注入漏洞就是这么来的。4.3 一上来就连不上MySQL先查这几个地方本地启动这个项目时报错最多的就是数据库连接问题尤其MySQL 8会遇到一段乱码时间区报错原因是默认时区不对我见过的典型报错长这样java.sql.SQLException: The server time zone value Öйú±ê׼ʱ¼ä is unrecognized解决办法是在JDBC连接串里显式指定时区url: jdbc:mysql://localhost:3306/ski_shop?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8还有一类常见问题是Linux服务器上用命令行连本机MySQL时报socket错误ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这种多半是MySQL服务没起来或者socket文件路径不对。直接用service mysqld status或systemctl status mysql先看服务状态比什么都快。还有个坑是密码认证插件MySQL 8默认用caching_sha2_password有些老客户端连不上可以在建用户时指定mysql_native_password但要权衡安全性不能为了兼容老客户端牺牲太多。5. 前后端联调与部署上线的完整过程5.1 联调期最容易卡壳的三个问题第一是跨域。前端的开发服务器和後端端口不一样Vite开发环境里可以直接配proxy把/api代理到http://localhost:8080这样浏览器里看到的是同源请求基本不会触发CORS。如果后端单独配置了CorsFilter或者接口没走代理那就得在后端允许跨域。两种方案能选代理就别开跨域少一个变量少一个问题。第二是历史路由刷新404。Vue Router用history模式时开发环境正常部署到Nginx后一刷新页面就404。原因很好理解前端路由是浏览器端的服务器上并没有/product/detail这个真实文件Nginx找不到就报404了。解决办法是在Nginx配置里加try_files。第三是字段命名不统一。后端如果返回user_name前端用到的是userName就会出现页面显示undefined但接口明明有数据的情况。我的建议是后端统一返回小驼峰命名SpringBoot默认的JSON映射是能处理JavaBean转驼峰的配合前面的驼峰映射配置基本不会出问题。前端拿到数据后如果能用强类型定义一下购物车商品和订单对象能减少很多低级错误。5.2 Nginx托管前端并反向代理API生产环境我把前端dist目录交给Nginx后端SpringBoot进程独立跑在8080端口由Nginx把/api开头的请求转发到后端口。Nginx配置可以这样写server { listen 80; server_name your-server-ip; root /usr/share/nginx/html/ski-shop/dist; index 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; } location / { try_files $uri $uri/ /index.html; } }这里proxy_pass用的没有尾斜杠的写法转发时会保留/api前缀后端所以Controller统一加了/api前缀两边就能对上。这一段是整个部署过程最容易出乱子的地方我见过有人把proxy_pass写到http://localhost:8080/然后找不到接口的。5.3 后端打包jar包部署与war包部署到Tomcat默认SpringBoot项目打出来是jar包内置了Tomcat直接执行mvn clean package -DskipTests java -jar target/ski-shop.jar --spring.profiles.activeprod如果你一定要部署到外部Tomcat比如公司环境里Tomcat版本固定那就把项目改成war包步骤有三个pom.xml里packagingwar/packaging依赖里把spring-boot-starter-tomcat的scope设为provided启动类继承SpringBootServletInitializer并重写configure。SpringBootApplication public class SkiShopApplication extends SpringBootServletInitializer { Override protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) { return builder.sources(SkiShopApplication.class); } }打出来的war放到webapps/ROOT启动Tomcat后访问http://ip:8080/就能看到接口。要注意war包方式下内置的静态资源处理会被外部Tomcat接管所以前端还是放在Nginx下更干净这也是前后端分离的一个好处。5.4 服务器端部署后的失败排查清单部署完跑不起来是常态我这里列一份实战排查顺序进程是否在跑ps -ef | grep java别上来就改代码端口是否监听ss -lntp | grep 8080确认端口没被占用后端日志有没有报错用nohup java -jar xxx.jar app.log 21 启动后tail -f app.log数据库连接、SQL错误都会在这里暴露MySQL是否允许远程访问检查用户是否授权了对应IPGRANT ALL PRIVILEGES ON ski_shop.* TO user% IDENTIFIED BY password;后FLUSH PRIVILEGES;前端404先确认Nginx root路径是否正确再确认有没有try_files。另外分享一个小技巧如果线上出问题但手头没有源码只有一份jar包可以直接用IDEA打开jar它会自动反编译class文件配合FernFlower能比较清楚地看到代码逻辑排查问题、学习别人的写法都很有用。6. 拿到这套源码之后我建议你这样用起来6.1 三步在本地跑起来第一导入数据库脚本。创建一个ski_shop数据库把项目里的SQL文件导进去。第二改数据库连接配置。把application里的用户名密码改成自己本地的时区参数保留。第三分别启动后端和前端。后端点IDEA里的main方法运行前端npm install后npm run dev启动。顺序上先跑后端因为前端启动之后马上会发请求后端没起的话页面会一直空白加上报错提示。有两点特别提示一是确保本机JDK版本和项目要求一致至少Java 8以上我这边用的Java 8就能跑二是如果改了项目端口前端Vite的proxy配置也要同步改否则接口会全部走错。6.2 面试讲解时怎么突出项目亮点这套系统的面试价值不在“我做了个商城”而在你能否把关键细节讲清楚。被问到的时候我建议主动聊这几个点订单扣库存用乐观锁的思路防止超卖JWT登录的无状态设计和拦截器白名单为什么用MyBatis-Plus还要写XML部署过程中怎么排查404和数据库连接问题。面试官最喜欢听的反而是你踩过的坑和你是怎么解决它的光背概念没有说服力。还有一点简历上不要只写“负责商品模块开发”而是写“负责商品搜索与分页接口通过SQL优化将列表页响应时间从X秒降到Y毫秒”这种有感知的描述。数字不一定非要特别夸张但要真实可查。6.3 后续可以扩展的方向如果这个项目你打算继续玩下去有几个方向性价比挺高用Redis缓存商品详情和验证码减轻数据库压力用MinIO搭一套独立的图片上传服务把商品图和轮播图从本地目录迁移出去用ElasticSearch做商品搜索解决MySQL like查询在大数据量下的性能问题参考若依这类成熟的代码生成框架把后端CRUD部分改为自动生成提升后续开发效率。这些扩展方向能让项目在简历和面试里立体很多但核心还是先把当前这套SpringBootVueMyBatisMySQL前后端分离链路走完整前面这些坑踩通了后面加什么都只是时间问题。我个人在跑完这套项目后的体感是前后端分离真正的难点从来不在写接口而在部署、环境、字段对齐和并发边界。这套雪具销售系统把这些点覆盖得还算全面适合自己完整过一遍。
返回列表