资讯详情

资讯详情

基于SpringBoot+Vue3的果蔬生鲜电商系统:前后端分离与JWT鉴权实战解析

先把我做这个项目的真实感受放在最前面没有任何一个技术项目能像果蔬生鲜电商这样把SpringBoot和Vue3的实战价值体现得如此充分。前后端分离、JWT鉴权、商品与订单流转、后台管理……这些看上去很“教科书”的名词落在一个卖菜平台上突然就变得非常具体。你不再是为了学框架而写代码而是真的要去解决“水果怎么上架”“购物车怎么算钱”“订单怎么扣库存”这些实际问题。这个项目适合谁一句话适合正在准备毕业设计、想找一份前后端分离实战项目经验、或者打算从躺平阶段重新捡起JavaVue技术栈的人。果蔬生鲜电商的切入角度比普通商城更有辨识度业务上多出了“新鲜度”“重量计价”“冷链配送”这些真实痛点能写进文档和简历里的东西非常多。我接下来说的每一部分都会围绕“如何从一个空目录把整个系统落地”来展开附带我在编码、联调、部署过程中踩过的坑和找到的最优解。1. 项目整体分析与架构设计思路1.1 为什么选果蔬生鲜电商这个方向我在帮人选题时最常被问一句话“商城系统不是烂大街了吗”没错普通的图书商城、数码商城确实没有新鲜感但果蔬生鲜是一个很微妙的赛道。它的商品天然具备分类属性叶菜、根茎、水果、肉禽蛋价格体系里有“份”和“斤”两种计量方式库存会因为生鲜损耗频繁变动订单还牵扯到配送时段、冷链要求、退换货策略。这些业务特征落到技术上意味着数据库表要设计得比普通商城更细致接口要处理更复杂的条件查询前端页面也要照顾“快速浏览、快速加购”的移动端习惯。从技术训练的角度看果蔬生鲜电商几乎是“前后端分离标准范式”的浓缩体。它涵盖了用户注册登录、商品展示、购物车、订单创建、库存扣减、后台管理、文件上传、数据统计这些模块单独拆开都是面试里必问的东西组合在一起就是一个完整闭环。更关键的是你在做这个项目的过程中能自然理解“为什么需要前后端分离”前端管理交互状态后端管理数据安全两者只通过API通信互不干扰。1.2 前后端分离架构的角色划分前后端分离这四个字听起来简单真正落地时有几条明确的边界要守住。SpringBoot后端的职责是提供RESTful接口、校验参数、执行事务、操作数据库、生成JWT令牌、处理文件存储。Vue3前端的职责是渲染页面、管理路由、保存用户登录状态、调用后端接口、处理交互反馈。两者之间只通过JSON格式的数据交互不共享模板引擎不直接读取对方文件。我选的方案是后端用SpringBoot 2.7.9 MyBatis-Plus MySQL 8.0 Redis前端用Vue3 Vite Pinia Vue Router Element Plus。这里必须多说一句版本选择的问题。SpringBoot 3.x虽然已经发布很久但如果目标是稳妥落地一个电商系统2.7.x反而是更理性的选择。原因很实际3.x把javax迁移到了jakarta命名空间很多老牌工具包的兼容还在过渡期2.7.x则处于“功能够用、生态成熟、网上资料最多”的黄金阶段。我身边不止一个人卡在SpringBoot 3.0的版本兼容上浪费了两三天而2.7.x几乎不会遇到这种坑。JDK我选的8不是因为不会17而是因为部署环境兼容性最好。1.3 业务模块与核心流程拆解整个系统划分成前台商城和后台管理两大部分。前台用户操作的是浏览商品、查看分类、搜索、加入购物车、确认下单、查看订单、管理收货地址。后台管理员操作的是商品上架下架、库存调整、订单发货、用户管理、销售统计。业务核心链路是所有模块里最需要理清楚的部分用户登录后浏览商品将商品加入购物车购物车勾选后生成订单下单时校验并锁定库存支付成功这里可以接真实支付也可以做成模拟支付后库存正式扣减然后订单进入待发货状态。这条链路里最容易出问题的环节就是库存高并发场景下如果两条订单同时请求同一个商品数据库层面的库存判断和扣减必须保证原子性否则就会出现“超卖”。我在这个项目里用的办法是数据库行锁配合事务先把商品行的库存读出来如果剩余大于0才更新更新语句本身再加剩余数条件双保险。2. 后端核心模块从工程搭建到业务落地2.1 工程结构设计与数据库建模工程结构上我建议按“模块分包”而不是“按层分包”。简单说就是先按业务域划分包商品相关的Controller、Service、Mapper放在product包里订单相关的放在order包里用户相关的放在user包里。这种结构的好处是当代码量增长到几百个文件时你不会迷失在controller层、service层各自的庞大列表里而是能按业务入口快速定位。我见过太多按层分包的毕设项目最后改一个商品功能要在三层目录之间来回跳那体验真的太痛苦了。数据库表上果蔬生鲜系统我会设计这些核心表用户表字段包含用户名、密码BCrypt加密存储、昵称、头像、手机号、状态商品分类表支持两级分类商品表包含商品名称、主图、轮播图、详情描述、价格、原价、库存、销量、上下架状态、单位斤/份、新鲜度标签购物车表包含用户ID、商品ID、数量、选中状态订单表包含订单编号、用户ID、总金额、支付状态、配送地址、配送时段、订单状态订单明细表包含所属订单ID、商品ID、商品快照信息名称、图片、单价、数量、小计。这里有个值得注意的设计细节订单明细中必须保存“下单那一刻的商品名称和价格快照”而不是只存商品ID。原因很现实后续商品改名或调价不能影响历史订单的展示。我在开发文档中把这点单独标注为“数据冗余是故意的”避免被误判为不规范设计。2.2 统一返回结构与异常处理前后端对接最怕出现一种情况不同的接口返回的JSON结构完全不一样前端每个请求都要单独判断。解决方式是后端定义一个统一返回值。我将其命名为Result核心字段包括code、message、data。code为200表示成功500表示系统异常401表示未登录或令牌失效404表示资源不存在。所有Controller的方法都返回这个结构前端axios拦截器只需要统一处理一次即可。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.setCode(200); result.setMessage(操作成功); result.setData(data); return result; } public static T ResultT error(Integer code, String message) { ResultT result new Result(); result.setCode(code); result.setMessage(message); return result; } }异常处理上我写了一个全局异常处理器拦截所有Controller层抛出的异常。业务异常比如库存不足、商品已下架统一返回提示给前端系统异常则记录日志并返回“系统繁忙请稍后重试”这样安全的提示避免把数据库SQL错误直接暴露出去。这个习惯是在真实项目里养成的因为接口的错误信息一旦包含敏感SQL片段被有心人看到就是安全隐患。2.3 登录鉴权与权限控制登录鉴权我用的JWT方案。用户在登录成功后后端生成一个有效期2小时的令牌返回给前端。前端把令牌存储在本地并在后续每次请求的请求头中携带。后端有一个拦截器在访问需要登录的接口时先校验令牌无效则直接返回401状态码。JWT本身由三部分组成Header、Payload、Signature。Header里声明加密算法Payload里放用户ID、用户名、过期时间Signature用密钥对前面两部分进行签名。这样做的好处是服务端无状态不需要把会话信息存储在内存或数据库中扩展性好。Component public class JwtInterceptor implements HandlerInterceptor { Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { String token request.getHeader(Authorization); if (StringUtils.hasText(token) token.startsWith(Bearer )) { token token.substring(7); try { Long userId JwtUtil.parseToken(token); request.setAttribute(userId, userId); return true; } catch (Exception e) { // 令牌过期或非法 } } response.setStatus(401); response.setContentType(application/json;charsetUTF-8); response.getWriter().write({\code\:401,\message\:\未登录或登录已过期\}); return false; } }权限区分上管理员和普通用户我用角色字段区分。管理员接口加了一个自定义注解拦截器里判断当前用户的角色是admin才放行。这个方案比较轻量不需要引入Spring Security全家桶对于这个体量的系统已经足够。2.4 核心业务接口实现要点商品模块的最核心接口是分页查询果蔬生鲜的场景下支持按关键字搜索、按分类筛选、按价格排序、按销量排序。使用MyBatis-Plus的LambdaQueryWrapper可以很优雅地完成动态查询拼接但需要注意一个细节当排序字段来自前端时绝不能直接拼接列名而是要用白名单映射防止SQL注入。订单模块是最考验事务能力的地方。我在下单接口上加了Transactional注解方法内依次执行校验购物车商品、校验库存、扣减库存、生成订单主表、生成订单明细、清空对应购物车项。中间任何一步失败整个事务回滚保证数据一致。扣减库存的SQL语句这样写UPDATE product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}这个UPDATE语句自带条件判断MySQL会在执行时锁住这一行记录多个并发请求同时进来时后进来的只能等待前一个事务提交。这才是真正意义上的防超卖仅仅在Java代码里先select再update是不够配合的因为那两条SQL之间有空隙。3. 前端核心页面与交互实现3.1 Vue3工程搭建与关键依赖前端工程我用Vite创建相比Webpack开发服务器的启动速度和热更新时间明显提升对Vue3的默认支持也是Vite原生就有的。创建方式一句话带过npm create vitelatest选择vue模板然后安装vue-router、pinia、axios、element-plus。Element Plus按需导入可以减小打包体积但为了省事我直接全量引入毕竟本地开发和毕设演示的体量完全感知不到差异。Vue3项目中我推荐全程使用组合式API也就是setup语法糖。同样是实现一个计数器的逻辑选项式API要把数据和方法分散在data、methods两个区域而组合式API允许你按功能把相关代码聚在一起可读性高很多。更重要的是现在大部分企业新项目都在用组合式API面试时也会被问到区别提前适应不吃亏。3.2 路由与全局状态管理路由我分成两类普通页面路由和需要登录才能访问的路由。判断是否登录的方法是在路由守卫里检查轻量级本地存储里有没有token。如果没有token且访问的是受保护页面直接重定向到登录页。这种前端路由守卫解决的是体验问题真正的安全校验后端拦截器兜底执行两层缺一不可。全局状态管理用Pinia。很多人问为什么不用VuexVue3的官方推荐已经变了Pinia更轻量、TS支持更自然、没有mutations的繁琐概念。我在项目里主要用Pinia管理两个东西用户信息用户ID、昵称、头像、角色和购物车数量角标。购物车数量角标是一个非常经典的全局状态场景因为首页、商品详情页、购物车页面都会改变或显示它如果不用全局状态组件之间传值会传到你崩溃。export const useUserStore defineStore(user, () { const token ref(localStorage.getItem(token) || ) const userInfo ref({}) function setToken(value) { token.value value localStorage.setItem(token, value) } function setUserInfo(value) { userInfo.value value } function logout() { token.value userInfo.value {} localStorage.removeItem(token) } return { token, userInfo, setToken, setUserInfo, logout } })3.3 页面组件拆解首页、商品详情、购物车首页的布局核心是顶部搜索栏、左侧分类导航、中间商品瀑布信息流。分类数据在页面加载时调用后端接口获取点击左侧分类时右侧商品列表根据分类ID重新请求。商品卡片需要展示图片、名称、价格、单位、销量以及生鲜特有的“新鲜”“精选”标签。这里有一个实用技巧图片地址由后端返回相对路径前端在axios拦截器或公共方法里统一拼接完整的图片地址这样部署时只需要改一个地方就能切换整个环境的图片域名。商品详情页主要展示大图、价格、选择数量、加入购物车、立即购买。购物车页面则是全选、单选、修改数量、删除、计算合计金额。计算合计金额我是在前端实时算的每次勾选或修改数量总价立即变化。下单时再把购物车中选中项传给后端后端重新计算金额并返回实际支付金额防止前端篡改价格。购物车数量修改这里有一个常见交互坑用户连续点击加减按钮时如果每次都请求后端接口会产生大量并发请求。我的做法是做一个300毫秒的防抖用户停止操作后才发送请求体验正常且后端压力小很多。另一个坑是修改数量接口应该传商品ID和新数量后端根据商品ID找到对应购物车记录再更新而不是把整条购物车记录传回去因为前端数据可能已经过期。3.4 后台管理页面与图表统计后台管理页面我单独做了一套布局左侧菜单包含商品管理、订单管理、用户管理、分类管理、数据统计。商品管理页面有一个商品表格支持分页、搜索、上下架切换、编辑弹窗、新增商品弹窗。新增和编辑商品时需要上传图片我用的是Element Plus的Upload组件配合后端文件上传接口上传成功后后端返回图片URL前端把URL回填给表单。订单管理页面的核心是状态流转。订单状态有待付款、待发货、已发货、已完成、已取消。后台的操作为发货按钮将状态从待发货改为已发货查看明细弹出抽屉展示订单中的商品快照列表。数据统计页面我用了ECharts展示近7天销售额曲线、分类销售占比饼图。这个模块不需要后端提供复杂报表只需两个统计SQL一个是按日期分组求和一个是按分类分组求和。3.5 接口封装与交互细节axios封装是整个前端工程里价值最高的基础设施。我在封装文件中统一做了四件事设置基础URL、请求拦截器里添加token、响应拦截器里统一处理code、401时强制跳转登录页。前端所有请求都通过这个封装实例发出业务代码里只需要关心成功后的data数据。const service axios.create({ baseURL: /api, timeout: 10000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( response { const res response.data if (res.code 200) { return res } if (res.code 401) { localStorage.removeItem(token) window.location.href /login } return Promise.reject(new Error(res.message)) }, error { if (error.response error.response.status 401) { window.location.href /login } return Promise.reject(error) } )这个封装解决了我在联调阶段遇到的最烦人问题接口一多每个页面都要重复写loading、错误提示、登录失效跳转代码越来越冗余。做完这次封装之后前端页面里的请求代码极短一眼就能看出业务逻辑。4. 开发文档与部署从本地联调到上线4.1 完整开发文档都应包含哪些内容很多人把开发文档理解成“代码注释汇总”这其实是误区。真正有价值的开发文档首先要写清楚项目是怎么跑起来的环境要求、数据库初始化脚本、后端启动步骤、前端启动步骤。其次要写清楚项目的架构设计包括技术选型理由、模块划分、核心业务流程时序说明。最后要写的是接口文档。接口文档我强烈建议用在线方式生成后端集成Knife4j后启动项目自动生成Swagger文档页面所有接口的路径、参数、返回示例一目了然。比起手动维护Word文档这种自动文档永远不会过期。当然为了让开发文档更有“项目答辩味”我额外补充了数据库设计文档里面每张表都要写清楚字段的作用、类型、索引设计原因这部分内容是文档加分的关键。4.2 本地联调的三个关键配置本地联调最大的坎就是跨域。前端跑在5173端口后端跑在8080端口浏览器会拦截跨域请求。解决方案有两个一是后端配置跨域过滤器允许来自前端地址的请求二是前端在vite.config.js中配置代理。我更推荐第二种因为生产环境部署时前端和API走同一个域名不需要跨域代理方案只在开发环境生效架构上更干净。// vite.config.js server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }第二个关键配置是后端的前缀统一。所有Controller的请求路径都以/api开头前端代理中把/api转发到后端这样部署时可以灵活调整后端地址而不影响前端代码。第三个关键配置是数据库连接的时区参数。连接MySQL时URL里必须加上serverTimezoneAsia/Shanghai否则日期时间字段会相差8小时这个问题出现时特别隐蔽。4.3 打包部署jar加Nginx组合部署方案我选的是后端打成jar包由Java直接运行前端build构建后放入Nginx的静态目录。相比把前端也塞进SpringBoot的resources目录Nginx方案的好处是静态资源响应更快且未来如果前端要升级可以独立替换不用重启后端。前端打包执行npm run build产物在dist目录。把dist目录里的文件传到服务器的Nginx html目录。同时需要配置Nginx把/api路径的请求代理到本机的8080端口server { listen 80; server_name your-domain.com; root /usr/share/nginx/html; index index.html; location / { 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-Forwarded-For $proxy_add_x_forwarded_for; } }这里面的location /配置非常重要它可以解决Vue Router历史模式下刷新页面404的问题。原因在于前端路由是前端自己管理的后端服务器没有对应的物理文件try_files把不存在的路径全部重写到index.html让前端路由接管。4.4 图片存储方案的选型果蔬生鲜系统里商品图片数量不少图片存储我优先推荐本地存储加静态资源映射。后端配置一个上传目录把上传的图片保存到磁盘同时映射为可访问的URL。这个方案在没有对象存储服务的情况下最简单实用零成本、部署简单。upload: path: /data/upload/ spring: web: resources: static-locations: file:${upload.path}如果后续部署到云服务器并配置了CDN可以把代码里的上传实现替换成MinIO或阿里云OSS。项目文档中我把这个替换点明确标注出来作为一个可扩展的架构设计很方便说明。5. 常见问题与避坑实录5.1 SpringBoot版本过高导致的兼容问题我亲眼见过一个同学把项目搭在SpringBoot 3.2上然后折腾了一整天才发现是MyBatis-Plus依赖版本不兼容启动直接报错。这类问题的根源是3.x从javax迁移到jakarta命名空间老版本第三方库找不到对应的javax包。如果你不是刻意要学新特性我建议直接用2.7.9。如果已经用了3.x排查思路是升级所有第三方依赖到适配3.x的版本注意mybatis-plus-spring-boot3-starter这样的专用依赖名。5.2 Vue3的日期校验规则问题Element Plus表单校验中日期选择器使用type: date时默认的校验规则并不会自动生效需要在rules里指定validator或者用type: date带正确格式。常见的报错是输入框选了日期但提示“请输入日期”原因是绑定的值还是字符串而不是Date对象。解决方式是在选择器上设置format和value-format且校验规则里写成type: date。这个小问题在热搜里被我多次看到说明确实困扰了很多人。5.3 上传组件的on-success监听不到Element Plus的Upload组件在Vue3里有一个经典误区事件回调参数顺序。on-success回调的参数不是简单的response第一个参数是response第二个是uploadFile第三个是uploadFiles。如果你只写一个箭头函数还想直接拿到后端返回的URL就漏掉了回调的完整签名。另一个坑是组件挂载后如果upload的action属性为空上传不会触发所以action必须在模板中提前写成/api/file/upload。5.4 图片上传后商品列表不显示这个问题的排查思路我整理成了经验先看后端接口返回的URL是否完整再看浏览器直接访问该URL是否200最后检查前端拼接图片地址的逻辑。绝大多数情况不是代码写错而是图片URL没有走统一前缀拼接方法。我在项目里给所有图片地址暴露了一个全局过滤器任何模板里出现的imgSrc都会被自动补全域名前缀这个方案在一开始就解决了后续几十个页面的重复劳动。5.5 修改Tabs标签页样式的深度选择器问题Element Plus在Vue3中组件内部样式使用scoped时直接在样式表里写.el-tabs__item会不生效因为scoped会为选择器自动添加组件的data属性而Element Plus组件内部的DOM不会被添加。解决方案是用深度选择器:deep()比如:deep(.el-tabs__item) { color: #16a085; }。这是Vue3 CSS作用域机制和组件库样式冲突的典型场景多遇到几次就形成肌肉记忆了。问题现象可能原因解决方向后端启动报ClassNotFoundExceptionSpringBoot版本与依赖版本不匹配统一版本或替换为适配版本前端刷新页面404路由模式与服务器未配合Nginx配置try_files登录成功但接口401前端未携带token或token过期检查请求拦截器与JWT有效期后端返回图片URL访问404静态资源映射未配置配置resources.static-locations日期选择器校验失败value-format与校验类型不匹配设置value-format并调整rule收尾做完这个项目后我对全栈开发的重新理解这里我没有用固定的第九章总结因为做这个项目最值得留下的恰恰是一些散装心得。第一前后端分离项目最耗时间的往往不是写单点功能而是“联调”。你在后端把返回结构设计好、在前端把axios封装好是在为自己省下后面几十个小时的对接口时间。第二电商系统的真正难点不在增删改查而在数据一致性和边界情况比如库存超卖、订单金额被篡改、图片URL在全环境可用。把这些问题提前想明白比多写十个接口更有价值。第三千万别小看开发文档的撰写过程。我写完这个果蔬电商系统的完整文档后最大的感受是“原来我做的项目比我自己以为的复杂得多”那些深度拆解的接口说明、数据库设计说明、部署文档在答辩和求职时都会变成你最有说服力的作品证据。如果你正准备动手复刻这个果蔬电商系统我的建议是不要急着抄代码先用一天把数据库表和核心接口设计好再花半天把前端工程框架跑起来最后按模块逐个实现。中间遇到问题回到这篇文章的避坑清单里查一查你会发现大部分坑我都提前替你们踩过了。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →