☰
Spring Boot+Vue球鞋购物系统实战:电商项目开发完整指南
2026/10/17 11:53:26 网站建设 项目流程

做球鞋购物系统这个项目,其实一开始的动机很朴素——关注了几年球鞋发售和二级市场,想自己动手把“看鞋、收藏、加购、下单”这套完整流程跑通。用 Spring Boot 搭后端、Vue 做管理端和用户端页面,再把数据库表结构和接口文档整理好,整个过程下来,踩了不少坑,也把电商系统的核心链路摸了一遍。如果你也准备拿类似题目练手,或者正在做课程设计和简历项目,这篇文章可以帮你少走很多弯路。

全文会围绕这套“Spring Boot + Vue 球鞋购物系统”展开,从技术选型思路、数据库设计、后端核心接口实现、前端页面对接,到最后的部署经验和常见问题排查,尽量把我实际做过的东西讲透。尤其是一些只在实操中才会遇到、文档里不写的细节,我会重点标出来。

1. 项目定位与技术选型:为什么是 Spring Boot + Vue

1.1 这个系统到底要做什么

先把这个项目的边界说清楚。这不是一个纯粹的静态展示站,而是一个带完整交易链路的前后端分离电商系统,核心用户有两种角色:

  • C 端用户(普通买家):注册登录、浏览球鞋列表、按品牌和价格筛选、查看详情、加入购物车、提交订单、模拟支付、查看个人订单。
  • B 端管理员(后台运营):商品分类管理、球鞋上架与库存维护、订单状态更新、用户列表管理、基础数据看板。

从功能上看,它覆盖了电商系统的典型闭环:用户 → 商品 → 购物车 → 订单 → 支付模拟 → 售后状态变更。和一般 Demo 最大的区别是,它有一套真实的数据库表关系在支撑,而不是前端写死数据。

技术栈是我权衡后确定的。后端用 Spring Boot,理由很直接:生态成熟、上手快、岗位需求量大,而且做课程设计和面试项目都拿得出手。前端用 Vue,配合 Element UI 做后台管理界面,Vue Router 管路由,Axios 做请求。数据库选 MySQL,本地开发和部署都省心。整体就是一套“Java 后端 + JS 前端 + MySQL 存储”的经典组合。

1.2 前后端分离的价值点

说实话,如果用 JSP 模板引擎,这个项目能做得更简单,后端直接渲染页面,不用跨域不用考虑 Token。但我不建议这么干,原因有二:

第一,前后端分离是当前 Web 开发的主流协作方式。前端和后端各起一个服务,通过 HTTP 接口通信,这种模式练一遍,比以后在岗位上现学要划算得多。第二,分离架构天然逼着你思考接口设计和数据规范问题。前端只要接口返回结构,后端只要保证接口稳定即可,这种“契约感”是单体 JSP 项目练不出来的。

另外一个很实际的好处是,前端页面可以用 Vue CLI 直接起本地开发服务器,后端 Spring Boot 也独立运行,两边同时开,改动前端页面不用重启后端。开发体验比模板引擎顺畅。

2. 数据库设计:球鞋系统的表结构怎么规划

2.1 核心表全景

这套系统的数据库是我手工建的第二版,第一版把商品规格一股脑塞在商品表里,结果做购物车时发现尺码和库存根本没法拆,推倒重来了。踩过这次坑之后,我总结出一套比较通用的表结构,分享出来直接参考即可。

核心表共 8 张:用户表、商品分类表、球鞋商品表、商品图片表、购物车表、订单表、订单明细表、用户收藏表。每张表都落了外键逻辑,但物理外键在项目里看情况加,我建议保留逻辑关联就够了,避免后期删数据麻烦。

以球鞋商品表为例,字段设计要贴合球鞋这个品类的特点:

字段名类型说明
idbigint主键
category_idbigint分类ID,关联分类表
namevarchar(100)球鞋名称
brandvarchar(50)品牌
seriesvarchar(50)系列,如 Air Jordan 1
colorwayvarchar(50)配色方案
release_datedate发售日期
retail_pricedecimal(10,2)发售价
sale_pricedecimal(10,2)平台售价
stockint总库存数量
salesint累计销量
statustinyint上下架状态:0下架 1上架
descriptiontext详细描述

为什么把品牌、系列、配色单独拆字段,而不是统一放在描述里?因为前台要做侧边栏品牌筛选、列表页要展示系列标签,如果这些信息是混在长文本里的大杂烩,查询只能靠 like,效率和数据规范性都很差。单独拆出来之后,分页查询和筛选用一个 where 就能解决,这是我在重构中体会最深的点。

2.2 订单和购物车表的关系处理

购物车表的逻辑比较简单:用户 ID + 商品 ID + 数量 + 选中状态。但订单表就需要多考虑一层,一个订单里往往包含多双球鞋,因此必须有订单主表和订单明细表。

订单主表的核心字段包括:订单编号、用户 ID、总金额、订单状态、收货人信息、创建时间、支付时间。订单明细表则记录每一件商品的快照——商品名、商品图片、尺码、单价、数量、小计。

这里有一个很重要的设计习惯:明细表里一定要保存商品名称和价格快照,而不是只存商品 ID。因为用户下单之后,商品可能改价、可能下架,但如果明细表读的是最新商品数据,历史订单展示就会错乱。保存快照,既保证订单数据不可变,也方便后续做对账和客服排查。

之前第一版设计时不明白这点,第二版专门加上了,回头想这应该是电商订单设计的基本原则,早理解早省事。

2.3 订单状态流转:状态字段 + 时间戳

订单状态我用了数字枚举,方便前端用 switch 映射成对应的状态标签:

  • 0:待付款
  • 1:待发货
  • 2:待收货
  • 3:已完成
  • 4:已取消

为什么不用字符串状态如 pending、paid、shipped?因为我发现数字状态配合枚举类在 Java 里做流转更干净,数据库排序和索引也更友好。不过坏处是,看库的时候不够直观,所以文档里必须附状态对照表。

每个状态变更都记录对应的时间字段:付款时间、发货时间、完成时间。这些字段最初没设计,后来为了做“个人中心订单列表显示时间线”功能,又补了一次表结构。经验就是:订单表的时间字段别怕多,能留几个就留几个,后面做统计和展示都用得上。

3. 后端核心实现:Spring Boot 从登录到下单的完整链路

3.1 项目初始化与依赖清单

后端项目直接用 Spring Initializr 生成的,Java 版本用了 JDK 8,Spring Boot 版本选的 2.3.x。这里多说一句,如果你的开发机 JDK 版本比较高,也可以选 Spring Boot 2.7.x,但尽量别直接上 Boot 3,因为有些旧教程的代码和依赖在 Jakarta 命名空间上会有兼容问题,对做项目的人来说影响进度。

核心依赖列表:

  • spring-boot-starter-web:Web 基础
  • mybatis-plus-boot-starter:持久层框架
  • mysql-connector-java:MySQL 驱动
  • lombok:减少实体类样板代码
  • jjwt(或者 java-jwt):生成和校验 JWT Token
  • hibernate-validator:参数校验
  • spring-boot-starter-test:测试依赖

MyBatis-Plus 这个选择非常重要,它内置了通用的增删改查方法,像分页查询、条件构造器这些特性能省非常多代码。如果完全写原生 MyBatis,这个项目代码量至少多一倍,尤其是一堆单表查询没必要自己写 SQL。

3.2 统一返回结构和全局异常处理

后端接口最怕各写各的,有的人返回 Map,有的人直接返回实体,前端对接时每天都在问“这个接口到底返回什么结构”。我在项目里定了统一返回类 Result,所有接口都返回这个格式:

{ "code": 200, "message": "success", "data": {} }

在 Java 里就是简单的泛型类:

public class Result<T> { private Integer code; private String message; private T data; public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("success"); result.setData(data); return result; } public static <T> Result<T> error(Integer code, String message) { Result<T> result = new Result<>(); result.setCode(code); result.setMessage(message); return result; } }

配套的还有一个全局异常处理器,用@RestControllerAdvice注解捕获业务异常和参数校验异常,统一转成 Result 返回。这样一来,controller 里的代码就能写得很薄,不需要每个方法都 try-catch,业务代码异常直接 throws 上去统一处理即可。

这个设计看起来简单,但对前端开发非常友好。所有接口的返回结构都是同一个模板,拦截器里统一判断 code 是否为 200,不用为每个接口单独写错误解析逻辑。强烈建议新手也这么做。

3.3 JWT 登录认证:从原理到落地

登录认证这块,我选的是 JWT 方案,而不是传统的 Session。原因有两个:一是前后端分离架构下,Session 要处理跨域携带 Cookie 的问题,比较啰嗦;二是微服务流行的背景下,无状态 Token 更贴近实际生产场景。

JWT 的原理一句话讲就是:服务器端在登录成功后生成一个包含用户信息的加密串,返回给前端;前端后续每次请求在请求头里带着这个串;后端用一个拦截器解析这个串,能得到当前用户 ID 和角色。这个串本身经过签名,理论上来讲服务器不必保存会话状态。

核心代码分成三块。

第一块是登录接口,用户提交用户名和密码后,用 BCrypt 校验密码是否匹配,匹配则生成 Token:

// 登录成功生成 Token String token = Jwts.builder() .setSubject(String.valueOf(user.getId())) .claim("username", user.getUsername()) .claim("role", user.getRole()) .setExpiration(new Date(System.currentTimeMillis() + 7 * 24 * 60 * 60 * 1000)) .signWith(SignatureAlgorithm.HS256, secretKey) .compact();

第二块是拦截器。这里我写了一个 JwtInterceptor 实现 HandlerInterceptor,在 preHandle 方法里从请求头取出 Token,校验合法性,并把用户信息塞进 request attribute,方便 controller 获取。

第三块是配置类注册拦截器,并设置放行路径:登录、注册、商品列表、商品详情等不需要登录就能访问的接口要排除掉,购物车、订单相关接口则全部拦截。

这里有一个我实际遇到的坑:JWT 的密钥千万不要直接在代码里写死,因为一旦泄露,任何人都能伪造合法 Token。我后来把它移到 application.yml 配置文件里,用@Value注入,这样既方便修改,也不会出现在代码仓库的显眼位置。

3.4 商品列表与分页查询:MyBatis-Plus 的加分项

商品列表页需要同时支持关键词搜索、分类筛选、品牌筛选、价格区间筛选,还要分页返回。用原生的 MyBatis 写的话,每个条件都要拼 SQL,很痛苦。MyBatis-Plus 的 LambdaQueryWrapper 一句代码就能搞定:

public Page<Shoe> getShoePage(int pageNum, int pageSize, String keyword, Long categoryId, String brand) { Page<Shoe> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Shoe> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.hasText(brand), Shoe::getBrand, brand) .eq(categoryId != null, Shoe::getCategoryId, categoryId) .like(StringUtils.hasText(keyword), Shoe::getName, keyword) .orderByDesc(Shoe::getSales); return shoeMapper.selectPage(page, wrapper); }

筛选条件为空时,eq和like方法第一个 boolean 参数会决定是否拼接这个条件,省掉了一堆 if 判断。分页对象直接返回给前端时,还需要把 total 字段带上,前端就能根据总条数计算总页数了。

3.5 购物车加购与提交订单:事务和库存校验

购物车加购的逻辑相对简单:先查用户购物车是否已有该商品,有数量则累加,没有则插入记录。但提交订单的逻辑就要小心了,这里涉及多张表的联动操作,必须加上事务注解@Transactional。

提交订单的核心流程如下:

  1. 根据购物车中勾选的记录列表,查出商品当前价格和库存。
  2. 校验库存是否够用,不够就抛异常终止。
  3. 生成订单主表和明细表,状态置为待付款。
  4. 扣减商品库存,同时销量累加。
  5. 清空对应的购物车记录。

关于库存扣减,这里多说一句。我这个项目体量下,直接update stock = stock - #{count}就够了,但必须保证是在事务里执行,且 SQL 里带上库存充足的条件。真实秒杀场景需要考虑行锁、乐观锁、Redis 预扣减,但课程设计和简历项目做到事务这一步已经属于超出平均水准了。

3.6 管理员端接口:权限控制和数据看板

管理端接口我没有单独再写一套权限框架,而是沿用 JWT 里的角色字段。定义一个 AdminInterceptor,专门校验 Token 里的 role 是否为 admin,是则放行,否则返回 403。这样用户端和管理端的接口路径天然分开了:

  • 用户端接口以/api/user/**和/api/shop/**为主
  • 管理端接口统一放在/api/admin/**下

数据看板这块,我做了三个简单的统计数据:商品总数、订单总数、用户总数,以及最近 7 天每个时间段的订单量趋势。订单趋势查询用到了 MySQL 的DATE_FORMAT函数,按天分组统计订单数量,SQL 如下:

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) AS count FROM orders WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY day

这个 SQL 返回的是一组“日期+数量”的键值对,前端拿到之后直接渲染折线图,非常省事。

4. 前端页面:Vue 组件拆解与接口对接细节

4.1 前端工程结构和路由规划

前端用的是 Vue CLI 创建的标准工程,核心目录:

  • src/api/:所有接口请求方法,按模块拆文件
  • src/router/:路由配置,区分用户端和管理端
  • src/store/:Pinia(或 Vuex)状态管理,存用户信息
  • src/views/user/:用户端页面
  • src/views/admin/:管理端页面
  • src/components/:公共组件

路由规划上,用户端页面和管理后台是通过不同布局区分的。用户端有首页、商品列表、商品详情、购物车、订单列表、个人中心;管理端有仪表盘、商品管理、订单管理、分类管理。我做了一个嵌套路由的方案:父路由UserLayout和AdminLayout,子路由挂到各自布局下面,这样侧边栏和顶部导航只需要在布局组件里写一次。

路由守卫也很关键。用户访问需要登录的页面时,先在router.beforeEach里检查 store 里是否有用户信息,没有就跳转登录页并带上 redirect 参数,登录成功后再跳回原页面。这个体验细节很多新手容易漏。

4.2 Axios 封装与 Token 处理

前端每一个页面都在调接口,如果不做统一封装,代码里会到处都是重复的请求配置。我封装了一个 request.js 文件,对外暴露封装好的 axios 实例:

  • 请求拦截器:自动从 store 中获取 Token,并放到请求头Authorization: Bearer xxx
  • 响应拦截器:统一解析返回的 Result 结构,code 不是 200 则弹出错误提示
  • 特殊处理:当响应状态码是 401 时,说明 Token 过期或者未登录,自动跳转到登录页

这样每个接口函数只需要写请求地址和参数,其它都不需要关心。例如:

export function getShoeDetail(id) { return request({ url: `/api/shop/shoe/${id}`, method: 'get' }); }

注意封装的拦截器一定要处理 401 场景。我在测试阶段就遇到过:登录后放在那里很久不动,Token 过期后继续点购物车,接口一直返回错误,但前端没反应,页面像卡死一样。后来加上 401 自动跳转登录的逻辑,这个体验才正常。

4.3 商品列表页的筛选与分页

商品列表页是用户端最复杂的页面之一,因为它同时包含筛选条件、排序方式和分页。我的做法是,在页面的 data 里维护一个 searchParams 对象,所有筛选条件都绑定到这个对象上。每次筛选条件变化时,调用接口带上当前参数,后端返回最新的分页数据,前端用 query 参数驱动。

这样做的核心好处是,刷新页面时,URL 上的 query 参数可以直接初始化 searchParams,用户刷新后筛选条件还在。这个细节我花了些时间才完善。

页面滚动位置和分页状态也是个容易忽略的点:如果每次都重新拉数据,用户在列表第 3 页点击商品进详情,返回时列表应该还停在第 3 页。解决方法是把滚动位置和页码存到 sessionStorage 里,详情页返回时读取并恢复。这个小功能很提升使用体验。

4.4 管理后台的表格与表单

管理后台我直接用了 Element UI。商品管理页面是典型的 CRUD 场景:顶部搜索栏 → 表格展示 → 分页 → 弹窗表单(新增/编辑)→ 删除确认。

表格列设计上有个经验值得分享:图片列不要直接把整张图渲染在表格里,而是用小缩略图 + hover 放大的方式。Element UI 的 table 列可以自定义渲染,我用一个带object-fit: cover的小图片展示,宽度固定 60px,表格整体会清爽很多。

表单校验这块,我用的是 Element UI 的 rules 机制。商品名称必填、库存必须大于等于 0、价格必须大于 0,这些都配置在表单控件上。但要注意数字校验的坑:输入框 v-model 绑定的是字符串,必填校验可以通过,但如果要对数字大小做校验,需要自定义 validator 用 Number 类型转换后再判断。这个坑当时花了半个下午才定位到。

4.5 购物车和订单的状态联动

购物车页面我做的比较细,包括勾选、全选、数量加减、商品小计、选中商品总金额的动态计算。这个页面的核心是理解“选中状态”和“金额计算”之间是计算属性关系:computed 属性根据 cartList 中所有checked为 true 的商品,实时求出总件数和总价。

提交订单时,只把选中的购物车记录传给后端。后端根据这些记录生成订单,并删除购物车中已下单的记录。这个逻辑我在后端接口中做了明确约定:购物车记录传给后端时带上商品 ID 和数量,后端按当前数据库中的最新价来计算订单金额,而不是用前端传来的价格,避免有人篡改商品价格下单。这个安全点非常重要,我在项目文档的接口说明里单列了一个小节提醒。

5. 项目部署与常见问题排查实录

5.1 本地联调环境搭建

这个系统同时运行前端和后端,联调环境的配置是很多人第一次跑不起来的关键。我用的方案是前端开发服务器端口 8081,后端 Spring Boot 端口 8080,前端通过代理解决跨域问题。

在 Vue CLI 的vue.config.js里配置 devServer.proxy,把/api开头的请求代理到后端地址:

devServer: { port: 8081, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }

这样前端请求/api/xx时,开发服务器自动转发到后端,浏览器里不出现跨域错误,不用后端单独配 CORS。如果后端非要配 CORS,记得注意allowedOrigins不能写成*同时放开带凭证请求,否则浏览器会直接拒绝。

5.2 常见异常和解决对照表

我整理了一份这个项目里最高频的几个问题,基本覆盖了从启动到联调再到部署的全部阶段:

现象可能原因解决办法
后端启动失败,报数据库连接错误MySQL 没启动或账号密码错误检查 application.yml 的 url、username、password,确认库已创建
前端页面白屏,控制台报 404路由 history 模式刷新丢失改为 hash 模式,或者后端配置 history 回退
登录接口返回 401Token 校验失败或过期检查拦截器放行路径,检查前端请求头是否带 Token
请求跨域报错代理没配置或后端 CORS 未开vue.config.js 配置 proxy,前端请求路径必须用相对路径
分页数据不对pageNum、pageSize 前端和后端参数名不一致统一参数名,前后端文档要对照审核
下单报库存不足库存字段被修改或是并发问题临时解决方案是直接在数据库更新库存,但代码里必须有库存校验逻辑
图片上传失败上传接口路径写死绝对磁盘路径使用相对存储路径,并把路径前缀返回给前端拼完整地址
管理端页面可被普通用户访问前端路由和后端接口都没做角色校验前端路由守卫加 admin 角色判断,后端 AdminInterceptor 拦截

5.3 数据库初始化和模拟数据加载

项目能直接跑起来,数据库脚本功不可没。我把init.sql文件和data.sql分开设计了:前者建库建表,后者插入初始化数据。初始化数据里准备了几个管理员账号,C 端用户则是通过注册接口自动生成。

模拟数据方面,商品数据我导入了常见品牌的热门系列,每个商品配了 4-5 张图片,还生成了近 50 条的历史订单数据。订单数据用来测试个人中心的订单状态、数据看板的图表,没有它们管理端页面会显得空。推荐做法是:用 SQL 的循环或者 Python 脚本批量生成模拟订单,制造不同时间段的分布,这样图表渲染出来才有说服力。

5.4 部署时容易忽略的三个细节

打包部署阶段,最常踩的坑有三个,分享出来:

第一,前端打包后 dist 目录放到哪里。最省事的方式是把 dist 目录拷到后端项目的src/main/resources/static里,让 Spring Boot 直接托管静态资源,这样一个 jar 包就能跑整个系统。但需要注意,前端路由如果是 history 模式,刷新某个子路由时后端会返回 404。这时需要做一个 ForwardController,把非接口路径都转发到 index.html。

第二,application.yml 里的配置项不要全是开发环境。我把数据库地址、JWT 密钥、文件上传路径都抽成了环境变量,部署时用--spring.profiles.active=prod指定生产配置。一步到位虽然麻烦,但比临到部署再把配置写死省心。

第三,MySQL 建表建库时注意字符集。如果建库时没有指定 utf8mb4,插入一些包含生僻字或表情符号的球鞋备注会出现乱码。建议建表语句统一写成:

CREATE DATABASE shoe_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;

5.5 我总结的开发顺序建议

这个项目很多人是一上来就写代码,结果前后端来回沟通成本很高。我建议按这个顺序推进:

第一步,先把数据库表结构设计好,这是地基。表结构不清晰,后面接口怎么写都别扭。第二步,用 Postman 或 Apifox 把后端接口全部调试通,前端可以先 mock。第三步,搭前端工程,先跑通登录和商品列表这两个大模块,因为这两个模块会把拦截器、路由守卫、Axios 封装这些基础设施全部打通。第四步,再快速推进购物车、订单、管理端这些相对模式化的模块。

如果按模块顺序做,会非常零散。基础链路通了之后,后续模块基本上是在复制前一个模块的模式,开发速度会明显加快。

6. 这套系统的后续扩展方向

项目做完之后,我有几个后续扩展的想法,这里一并说一下,供大家参考。

一是引入 Redis 做缓存。现在商品列表每次查询都直接打 MySQL,虽然数据量小感觉不出来,但如果模拟数据增加到几千条,响应速度就会明显下降。把热门商品列表和商品详情缓存到 Redis,设置过期时间,性能会好很多。

二是订单超时自动关闭。现在用户创建订单后,如果不支付,订单会一直停在待付款状态。真实电商系统都会设置订单 15 分钟或 30 分钟内未支付就自动取消。实现方式可以是在创建订单后把订单号发到延迟队列,或者在每个订单上设置过期时间字段,通过定时任务扫描处理。这个功能加分项很强。

三是接入真实的文件存储。目前商品图片上传是存到本地磁盘的,部署到服务器后图片会丢。可以考虑接入云存储对象服务,后端只存 URL 地址,扩展也比较方便。

四是消息通知。比如用户下单成功后发送站内信,或者订单状态变化时推送提醒。用 WebSocket 或者轮询都能实现,属于锦上添花。

从我自己的实操感受来说,这个项目做完最大的收获不是背会了多少框架 API,而是真正理解了“一个带交易链路的系统从零到上线到底要经过哪些步骤”。数据库设计要反复推敲,后端接口要考虑安全和事务,前端页面要关注状态联动和用户体验,最后部署上线又是一套组合拳。把这些环节都走一遍,比看十篇教程都有用。如果你正在做类似的项目,希望这篇分享能帮你理清思路,也欢迎在评论区聊聊你自己踩过哪些坑。

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

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

立即咨询