Spring Boot+Vue前后端分离商城系统实战:甘肃特产线上平台解析
2026/9/10 2:30:17 网站建设 项目流程

这个项目做完前后大概花了三周时间,名字看着很直白——Vue基于Java的甘肃特产商城销售系统,本质上就是一个标准的 Spring Boot + Vue 前后端分离商城项目。做这个系统的初衷,是给甘肃本地的农特产品做一个线上销售展示平台,像兰州百合、苦水玫瑰、临泽小枣、岷县当归这类有地域标签的商品,通过商城的形式统一管理、在线交易,同时给商家留一个独立的运营后台。标题里的“商家_d3wdv0e7”,可以理解成这套系统的商家端实例标识,它在项目里对应着 seller 这个角色,也对应一套独立的部署环境配置。

如果你正在找毕业设计、课程设计,或者想拿一个完整的全栈项目练手,这篇文章应该能让你少走不少弯路。我不会只贴代码,而是把整个系统的设计思路、数据库关系、关键接口、前端交互,以及我在实际开发中踩过的坑都拆开讲一遍。内容偏实战,适合已经掌握了 Java 和 Vue 基础语法、但还缺一个完整项目串联经验的读者。

1. 项目概述与方案选型过程

1.1 这套系统的核心业务到底长什么样

甘肃特产商城,业务上最核心的不是“卖货”这两个字,而是需要同时处理三类角色的诉求:普通用户要能浏览商品、加购物车、下单支付、查看物流;商家要能维护自己的商品、处理订单、管理库存;平台管理员要能审核商家、管理分类、查看全站数据。这意味着系统从一开始就不能只做用户端,必须把后台管理和商家端一起设计进去。

用户端主要是商品展示和交易流程,商品列表需要支持分类筛选和关键词搜索,商品详情页要有轮播图、规格选择、库存展示。订单流程是整个商城的灵魂,从购物车到结算页,再到提交订单、支付、发货、收货,每一步的状态都要能被追踪。商家端侧重商品上下架、库存修改、订单发货,平台管理端则负责系统全局配置。

1.2 为什么选 Vue + Java 这个组合

这个选择其实不复杂。市面上能搭商城的方案很多,有电商 SaaS 平台直接买,有用 PHP 或 Node 快速实现的,也有 Spring Cloud 微服务全家桶。但作为个人项目或者毕设,Spring Boot + Vue是性价比最高的组合。Java 生态在高校和企业里普及率最高,网上关于权限、支付、部署的资料也最多,遇到问题随便一搜都有答案;Vue 上手曲线平缓,组件化开发方式很适合商城这种多模块页面,Element Plus 这类组件库能快速做出合格的界面。

前后端分离是这套架构的必然选择。前端独立打包成静态资源,后端只提供 JSON 接口,开发时两边可以并行推进,部署时也能各自扩容。比起传统的 JSP 或 Thymeleaf 模板渲染,前后端分离的调试体验和后期可维护性都要好不少,这也是现在企业里最主流的开发模式。

1.3 技术栈清单和版本选型参考

后端我用的 Spring Boot 2.7.x,这个版本相对稳定,网上资料也最多。ORM 框架选了 MyBatis-Plus,因为它内置了分页插件和简单的 CRUD 方法,能省去大量重复的 SQL 编写。数据库用 MySQL 8.x,缓存用 Redis,主要用于验证码存储、Token 管理和热点商品缓存。登录鉴权采用 JWT,无状态、好扩展。

前端用 Vue 3 + Vite,状态管理用的 Pinia,路由用的 Vue Router 4,UI 组件库用的是 Element Plus。HTTP 请求统一封装 Axios,配合拦截器处理 Token 注入和 401 跳转。

这里想提醒一点:很多人在版本上纠结,其实没必要追新,稳定能用才是第一位。比如 Spring Boot 3 配合 JDK 17 虽然性能更好,但很多老资料不兼容,出了问题查起来很麻烦。我的选择是 JDK 8 + Spring Boot 2.7,稳妥。

2. 数据库设计与核心业务模型拆解

2.1 从零设计一张覆盖商城全流程的表结构

数据库设计是这套系统里最考验功力的部分,它决定了后面能做什么功能、不能做什么功能。我的核心表一共九张:用户表、商家表、商品分类表、商品表、库存表(SKU)、购物车表、订单表、订单明细表、收货地址表。

用户表和商家表分开设计,是因为用户和商家虽然都是账号体系,但属性和权限边界完全不同。用户表主要存联系方式、积分、会员等级;商家表则要有店铺名称、店铺简介、审核状态、营业执照信息。平台管理员不单独建表,直接通过角色字段标识。

商品表和库存表是最容易被人忽略、但也是最重要的关系。比如同一款“兰州百合”,有 300g 装和 500g 装两个规格,价格和库存都不一样。如果直接在商品表里存价格和库存,那每一次增加规格都要改表结构,后期维护成本极高。所以我把商品基本信息放在 product 表,把规格、价格、库存、SKU编码放在 sku 表,通过 product_id 关联。

2.2 购物车和订单的数据流转逻辑

购物车表相对简单,关联字段有 user_id、sku_id、数量、勾选状态。真正复杂的是订单表,因为它需要冗余一份商品快照信息。为什么必须冗余?因为商品的价格和名称随时可能被商家修改,订单一旦生成,用户看到的下单价格必须锁定。如果订单明细表直接关联商品表的实时数据,商家改价之后用户的历史订单就全乱套了。

订单表的核心字段包括订单号、用户ID、商家ID、订单状态、商品总金额、运费、实付金额、收货地址快照、支付时间、发货时间、完成时间。订单号我采用“时间戳 + 用户ID后四位 + 随机数”的生成策略,比如 20250108153012 后拼接四位随机数,这样既保证唯一性,又能从订单号直接看出下单时间。

订单状态我用一个枚举类管理,包含 PENDING_PAYMENT 待支付、PENDING_SHIPMENT 待发货、SHIPPED 已发货、COMPLETED 已完成、CANCELLED 已取消、REFUNDING 退款中。状态流转是单向的,每个状态能执行什么操作都有严格限制,比如待支付状态下用户能取消,商家不能发货;已发货状态下用户可以申请退款,不能直接取消。这个状态机设计的合理与否,直接决定了订单模块的代码复杂度。

2.3 一套值得参考的表字段设计示例

以商品表为例,我用表格把核心字段列出来,方便你对照设计自己的表:

字段名类型说明
idbigint主键,自增
category_idbigint商品分类ID
seller_idbigint所属商家ID
product_namevarchar(128)商品名称
main_imagevarchar(255)商品主图地址
detailtext商品详情富文本
statustinyint上下架状态:0下架,1上架
origin_placevarchar(64)产地,如“甘肃兰州”
shelf_lifevarchar(32)保质期说明
create_timedatetime创建时间
update_timedatetime更新时间

这里加上 origin_place 和 shelf_life 是考虑到甘肃特产有很多生鲜和农副产品,产地和保质期是用户下单时非常关注的信息,也方便以后做产地溯源这类扩展功能。设计表之初就为业务特性预留字段,比我一开始只按通用商城模板建表,后期再改动要省心得多。

3. 后端服务搭建与核心接口实现

3.1 工程目录结构和分层思想

后端我按经典的四层结构来组织:controller 接收请求、service 写业务逻辑、mapper 做数据库操作、entity 放实体类。另外加了 config 包放配置类,common 包放统一返回结果和异常处理,utils 包放 JWT、订单号生成等工具类。一个简单但清晰的包结构如下:

com.gansu.mall ├── controller // 接口层 ├── service // 业务层 │ └── impl ├── mapper // 数据访问层 ├── entity // 数据库实体 ├── dto // 请求和响应对象 ├── config // 配置类 ├── common // 公共响应、异常处理 └── utils // 工具类

为什么强调分层?因为商城业务逻辑比较复杂,拿下单来说,它涉及到校验库存、计算金额、生成订单、更新库存、清空购物车、生成支付单等多个步骤。如果这些逻辑全写在 controller 里,一个接口几百行代码,后期根本没法维护。分层之后,controller 只负责参数校验和结果返回,service 里串联具体业务,代码可读性和复用性都大幅提升。

3.2 JWT 登录鉴权与拦截器配置

登录这块,我采用手机号 + 密码的方式注册登录,密码通过 BCrypt 加密存储,登录成功之后生成 JWT 返回前端。JWT 的有效期设置是两小时,前端在 axios 拦截器里取出 Token,放到请求头的 Authorization 字段。

后端需要写一个拦截器,统一校验所有需要登录的接口。拦截器里对白名单放行,比如用户的注册登录接口、商城的商品查询接口都不需要登录。校验时从请求头解析出 Token,解析成功把用户ID放入 ThreadLocal;解析失败直接返回 401 状态码。核心代码如下:

public class JwtInterceptor implements HandlerInterceptor { @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { if (!(handler instanceof HandlerMethod)) { return true; } String token = request.getHeader("Authorization"); if (token == null || !token.startsWith("Bearer ")) { throw new BusinessException(401, "未登录或登录已过期"); } Claims claims = JwtUtil.parseToken(token.replace("Bearer ", "")); UserContext.set(claims.get("userId").toString()); return true; } }

UserContext 本质是一个 ThreadLocal 包装类,在请求结束后要及时 clear,避免线程复用导致数据串号。这个坑我印象很深,刚开始没清理 ThreadLocal,高并发下偶尔会出现一个用户查到另一个用户购物车的情况,排查了很久才发现是这个原因。

3.3 商品模块和库存扣减的并发处理

商品模块的查询接口要支持分页、分类筛选、价格排序、关键词搜索。数据量不大的时候,直接用 MyBatis-Plus 的分页插件加条件构造器就够用了。但如果商品数量增多,我建议在商品表的 name 字段上加全文索引,或者引入 Elasticsearch,这也是后续可以扩展的方向。

库存扣减是商城系统里最典型的并发问题。用户下单时,不能只做“查询库存 → 判断充足 → 扣减”这三步,因为在高并发情况下会超卖。我采用的方案是 SQL 层面的原子操作,一次性完成条件判断和扣减:

UPDATE sku SET stock = stock - #{num} WHERE id = #{skuId} AND stock >= #{num}

这条 SQL 的执行效果是:只有当当前库存大于等于购买数量时,才执行扣减,并且数据库行锁保证了同一时刻只有一个事务能改这条记录。受影响的行数是 1 说明扣减成功,是 0 说明库存不足,直接给用户返回“库存不足”的提示。这个方法比在 Java 代码里加锁性能好得多,比乐观锁版本号方案也更简洁。

3.4 订单生成接口的完整流程

订单接口是整个后端最核心、也最容易写乱的一个接口。正常的流程是:接收下单请求,取出购物车中选中的 SKU 列表,逐个校验商品是否处于上架状态、库存是否充足,然后计算总金额,生成主订单和子订单,批量扣减库存,清空购物车,返回支付所需参数。

这里我遇到一个实际的问题:一个订单里可能包含不同商家的商品,如果 A 商家的商品已经发货了,B 商家的还在备货,订单状态怎么处理?我的做法是同一用户同一批商品生成一个主订单,再按商家维度拆分成多个子订单。每个子订单有自己独立的订单状态和物流信息,主订单的状态根据子订单的状态汇总计算。用户在“我的订单”页面看到的是主订单,点进去能查看每个商家的发货进度。这个拆单逻辑虽然增加了一些编码量,但更符合真实电商的业务形态。

4. 前端页面设计与核心交互实现

4.1 Vue 工程创建与路由权限控制

前端我用 Vite 创建 Vue 3 项目,安装了 vue-router、pinia、axios、element-plus 这几个核心依赖。创建项目以后第一件事是配置路由,这里我用的是动态路由的思路:所有页面组件按模块放在 views 目录下,在路由配置里区分“不需要登录的页面”和“需要登录的页面”。

用户端页面包括首页、商品分类页、商品详情页、购物车页、结算页、个人中心页。商家端单独用一套布局,挂在 /seller 路径下,包含商家商品管理、订单管理、数据概览等页面。路由守卫在每次跳转前检查用户信息和 Token,没有登录一律跳转到登录页。

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next('/login') } else { next() } })

这里的坑在于,前端判断有没有登录只能看本地有没有 Token,但 Token 是否过期需要后端接口验证。如果只在前端判断,会出现 Token 已经过期但页面还能打开、接口全部报 401 的情况。所以真正的登录状态校验,是在 axios 响应拦截器里捕获 401,然后统一跳转登录页并清除本地缓存。

4.2 axios 封装与用户状态共享

axios 封装是前端的一个基础工程,所有请求都要走统一的实例。我在 request.js 里做了三件事:设置 baseURL,在请求拦截器里自动携带 Token,在响应拦截器里统一处理业务错误码和 HTTP 状态码。后端接口返回格式统一为 { code: 200, data: {}, msg: "success" },前端在拦截器里判断 code,非 200 的 code 直接弹出错误提示。

用户登录状态用 Pinia 管理,登录成功之后把用户信息、Token 存到 Pinia 和 localStorage。刷新页面时从 localStorage 恢复用户状态,并调用一次“获取当前用户信息”的接口,确保数据是最新的。购物车数量也可以在用户信息里冗余一个 totalCartCount 字段,登录后显示在导航栏的购物车图标上。

4.3 商品列表、购物车、结算页的实现细节

商品列表页是用户进店之后看到的第一屏,性能直接影响转化率。列表页一次加载 12 条商品,使用 el-card 展示商品图、名称、价格、销量,支持分类切换和关键词搜索。价格显示需要注意,商品口径的价格应该读取当前 SKU 的最低售价,但列表页如果要拿最低价,SQL 查询会比较费劲。我这里的做法是在 product 表冗余一个 min_price 字段,SKU 表价格变动时同步更新,列表页直接读取冗余字段,避免子查询。

购物车页交互上有几个容易被忽略的细节:选中状态要单独存,不能和购物车记录混在一起;数量修改要防抖处理,不然用户连续点加减会发出大量请求;金额总计要前端实时算,但最终下单金额以后端计算为准。

结算页是用户路径的最后一站。页面上要展示收货地址(支持新增和切换)、商品清单、金额明细(商品总额、运费、优惠金额、实付金额)、支付方式。提交订单按钮要做防重复提交处理,我用了提交中禁用按钮加后端幂等校验的双保险方案。前端禁用按钮只是体验上的优化,真正防止重复下单要靠后端——生成订单前先查询是否存在相同用户和相同业务流水号的订单,有就直接返回,没有才创建。

4.4 商家端页面的快速搭建思路

商家端页面相比用户端要简单得多,核心是商品管理和订单管理。商品管理页包含商品列表、新增商品、编辑商品三个部分,新增和编辑共用一个表单组件。表单里的规格录入是动态的,商家可以点“添加规格”按钮实时增加一行 SKU 输入框,提交时组装成数组传给后端。这个交互用 Element Plus 的 el-form 和动态表单组件就可以实现,代码量不大但实用性很强。

订单管理页面更直接,商家看到的是分配给自己的、状态为待发货的订单,点击发货按钮后填写物流公司和物流单号,订单状态变成已发货。我特意在商家订单列表页加了数据统计卡片,展示“今日新增订单、待发货数、待处理退款数”,让商家一进后台就能掌握当天经营概况。

5. 部署上线全过程与常见问题排查实录

5.1 前端打包与 Nginx 部署配置

项目开发完以后要正式部署,这一步很多第一次做前后端分离项目的人会卡住。前端打包用 npm run build,输出一个 dist 静态目录。后端用 Maven 执行 package 命令,生成一个可执行的 jar 包。

部署时我在服务器上装了 Nginx,把前端静态文件放到 /usr/share/nginx/html,然后配置反向代理,把所有以 /api 开头的请求转发到后端的 8080 端口。这样一个 Nginx 同时承担了静态文件服务和接口代理两个职责,避免了前端直接请求后端接口带来的跨域问题。

Nginx 配置片段如下:

server { listen 80; server_name yourdomain.com; location / { root /usr/share/nginx/html; 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; } }

location / 里的 try_files 配置必不可少,它的作用是让 vue-router 的 history 模式在刷新页面时不报 404。第一次部署时我没加这行配置,用户停留在商品详情页按 F5 刷新,Nginx 找不到对应的物理路径,直接返回 404,排查了半天才发现是这个原因。

5.2 常见报错与排查方法速查表

开发过程中遇到的报错,我整理成了一张速查表,很多问题具有一定的普适性:

问题现象可能原因解决办法
Element Plus 的 ElMessage 提示未定义组件库按需引入时未注册 ElMessage 相关样式在 main.js 里全局引入完整样式,或单独引入 message 样式
前端请求接口报跨域前后端域名或端口不一致后端配置跨域过滤器或使用 Nginx 反向代理
上传商品图片报文件大小超限Spring Boot 默认单文件最大 1MB在 application.yml 中调整 spring.servlet.multipart.max-file-size
刷新页面后登录状态丢失没有持久化 Token 或刷新时未重新读取使用 localStorage 持久化,路由守卫里读取后恢复
库存偶尔出现负数并发场景下扣减库存不是原子操作使用 UPDATE 条件判断扣减,保证原子性
订单创建成功但购物车没清空清空操作失败后事务未回滚下单和清空购物车放在同一个事务里,异常整体回滚

5.3 项目上线后要注意的几点安全配置

项目上线前我做了几项关键配置,确保系统不会被轻易打穿。一是数据库账号不使用默认的 root,而是单独创建只具备单库权限的账号;二是 JWT 密钥不要写在代码里,通过环境变量注入;三是后端的全局异常处理器要做好兜底,任何未捕获的异常都返回统一格式的错误响应,不要把堆栈信息直接暴露给前端;四是商家上传的图片文件要做类型校验,只允许 jpg、png、webp 等白名单格式,防止上传恶意脚本文件。

还有一个容易被忽略的点:后端接口要做好参数校验,尤其是订单金额、购买数量这类关键参数。虽然前端已经做了校验,但接口是公开的,直接拿 Postman 调接口完全可以绕过前端限制。我在下单接口里强制要求传用户ID,服务端从登录态中取用户ID,而不是信任前端传入的用户ID,这样才能防止越权操作。

5.4 个人总结:做完整套系统后的几点真实感受

这个项目做完之后,我对前后端分离开发有了更深的体会。以前学框架的时候,每个知识点都是孤立的,Spring Boot 会用了、Vue 会用了,但不知道它们之间怎么配合。真正做完一个完整的商城系统,才会理解接口设计为什么要统一返回格式、状态码为什么要约定好、跨域问题为什么会在前后端联调时集中爆发。这些都是教科书上学不到、只有踩过坑才能记住的经验。

如果再给我一次机会从头做,我会在动手编码之前,先花更多时间把数据库表关系和状态流转搞清楚。表结构设计合理,后面写业务的效率会提升一倍;表结构设计不合理,后期每次加需求都像在修补一座地基歪了的房子。另外,我强烈建议做完核心功能之后尽早部署一版到云服务器上,不要等全部功能做完再部署。因为很多问题只有在真实的部署环境中才会暴露,比如静态资源路径、上传文件存储位置、数据库连接池配置等,早部署早发现问题,早解决。

做这套系统的过程虽然辛苦,但收获非常大。它让我完整地走了一遍从需求分析、数据库设计、接口开发、前端实现到服务器部署的全流程,也让我对电商系统的业务逻辑有了体系化的认知。如果你也准备上手类似的商城项目,我建议你参考这个思路,先理清角色和业务流程,再动编码,过程中遇到的问题逐个记录下来,做完你会发现,你的收获远不止一套代码那么简单。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询