第一次看到这个项目标题的人,注意力多半会落在后半段那串技术名词上:SpringBoot2、Vue3、MyBatis-Plus、MySQL8.0,再加上一个【含文档】,看起来就像培训课里非常标准的实战项目。但真把它当成一套拿到手就跑、跑起来就能改的完整电商系统源码来拆,你会发现里面的东西远比“模板”两个字要多。这是一个前后端分离的家电销售展示平台,覆盖商品展示、分类检索、购物车、订单提交、库存扣减、后台管理一整条闭环业务,技术栈选型非常贴近当前 Java Web 开发岗位的真实分工,不是那种只有几个 CRUD 页面的教学 Demo。
只要你学过 Java 基础、知道 Spring 和 Vue 大概是什么,这套源码就值得完整过一遍。它能让你在一个真实业务场景里理解:前端路由怎么切、接口怎么约定、事务怎么控制、数据库表为什么长这样,以及在踩坑之后怎么定位问题。这篇文章不会复述项目文档,而是站在拿到源码的人的角度,把系统的选型逻辑、架构拆解、核心实现和实战中的坑从头到尾讲一遍。适合打算做课程设计、毕业设计,或者想让简历里多一个完整项目的初级开发者。
1. 项目定位与整体技术选型解析
1.1 为什么是 SpringBoot2 + Vue3,而不是别的组合
先看后端。SpringBoot2 的 2.7.x 是目前生态资料最全、兼容性最好的版本,市面上大量存量系统和教学项目都停在这一代。SpringBoot3 虽然性能更好,但底层迁移到 Jakarta EE 9 之后,很多老教程直接失效,JDK 17 的准入门槛也更高。对一个“以学习为主、兼顾落地”的商城系统来说,选 SpringBoot2 是性价比最高的决定:玩得转、资料多、找工作对口。
前端选 Vue3 而不是 Vue2,原因也很直白。Vue3 的组合式 API 已经是当前实际开发的主流写法,配套的 Element Plus 组件库对应的是 Vue3,Vite 构建速度远快于 Webpack。电商后台这种表格、表单、弹窗密集的场景,用组件库搭界面效率很高。如果翻到的是纯 Vue2 + Element UI 的老代码,那才应该警惕,那类项目往往是几年前的教学遗留物。
MyBatis-Plus 和 MySQL8.0 的组合是我个人很推崇的一套:单表 CRUD 几乎不用写 XML,BaseMapper 直接提供增删改查;分页插件一装,连 PageHelper 都省了;逻辑删除、自动填充这些电商系统高频能力也是内置的。数据库选 MySQL8.0 而不是 5.7,是因为 8.0 默认字符集就是 utf8mb4,对商品名称、详情描述这类中文字段非常友好,而且支持窗口函数和公共表表达式,以后要做统计报表会方便很多。
我见过不少初学者纠结“为什么不用 Spring Cloud 微服务”。答案很简单:这是一个单体项目,一个后端服务 + 一个前端页面足够支撑家电销售场景。微服务带来的注册中心、网关、分布式事务成本在这个业务体量下是负收益。把单体吃透,再去看微服务会轻松得多。
| 技术模块 | 本项目选型 | 核心原因 |
|---|---|---|
| 后端框架 | SpringBoot 2.7.x | 生态成熟、资料丰富、兼容 JDK8/11 |
| ORM 框架 | MyBatis-Plus 3.5.x | 内置分页、逻辑删除、自动填充,开发效率高 |
| 前端框架 | Vue3 + Element Plus | 组合式 API + 组件库,适合快速搭建管理端 |
| 数据库 | MySQL 8.0 | 默认 utf8mb4,支持窗口函数、CTE |
| 鉴权方式 | JWT | 无状态登录,前后端分离场景的标准方案 |
1.2 前后端分离带来的开发范式转变
很多人第一次接触这种项目,会拿它和学校里的 JSP 课程设计做对比。JSP 那套是后端把数据查出来,再通过模板引擎把 HTML 拼好丢给浏览器,代码耦合度高,前端想改个样式都要碰 Java 代码。而这个项目是真正的前后端分离:后端只负责输出 JSON,前端只负责渲染页面,两边通过约定好的接口路径和参数格式通信。
这种分离带来三个直接影响。第一,职责边界非常清楚,前端不会碰业务逻辑,后端不用管页面长什么样;第二,前后端可以并行开发,后端把接口文档先定义好,前端就能用 Mock 数据先做页面;第三,部署也更灵活,前端打包成静态文件扔到任意 Web 服务器,后端单独跑一个服务。
但对学习者来说,前后端分离也意味着心智负担变大了。你不仅要会写接口,还要懂 Vue 的组件通信、路由跳转、Axios 请求封装、跨域处理这一整套前端知识。这一点我在后面“跑通项目”的部分会专门讲,很多人项目跑不起来的第一个坎就是跨域。
2. 系统架构与功能模块全景拆解
2.1 前端工程:页面、路由、状态管理怎么组织
前端工程一般长这样:src/views放页面组件,src/router放路由配置,src/store放全局状态,src/api放所有接口调用函数。页面按业务分成前台和后台两块,前台是用户看得到的部分,后台是管理员操作的部分。
前台页面包括首页、商品列表页、商品详情页、购物车页、结算页、登录注册页。首页会展示轮播图和热门商品,商品列表页负责分类筛选和关键字搜索,详情页展示商品图片、价格、库存和详情描述,购物车页和结算页走完整个交易流程。后台页面包括商品管理、分类管理、订单管理、轮播图管理,管理员登录后能上架商品、调整库存、修改订单状态。
路由配置里藏着权限控制逻辑。普通用户和管理员请求的页面不一样,前端一般会在路由守卫里判断当前用户的角色,没有权限的直接跳回登录页。Axios 实例会在请求拦截器里统一把本地存储的 token 加到请求头,后端凭这个 token 识别用户身份。状态管理(Pinia)主要存用户信息、购物车商品数量这类需要在多个页面共享的数据,避免每个页面都重复请求接口拿数据。
2.2 后端工程:分层架构与统一处理机制
后端代码结构按照经典的controller / service / mapper / entity四层划分。entity是数据库表的映射对象,mapper负责数据库操作,service写业务逻辑,controller只负责接收请求、调用 service、返回结果。网上很多人说“Controller 要薄、Service 要厚”,这套代码就是这个实践,业务规则全部沉淀在 Service 层,Controller 里很少看到直接操作数据库的代码。
统一响应结构是这个项目比较规范的地方。所有接口返回的数据都封装成Result<T>格式,包含code、message、data三个字段。前端的 Axios 拦截器会判断code是否为 200,不是的话直接弹错误提示。这样做的意义在于,前后端不用每个接口都约定不同的返回格式,维护成本大幅下降。
异常处理也是统一掉的。项目里通常会有一个@RestControllerAdvice标注的全局异常处理器,把业务异常、参数校验异常、系统异常分别映射成不同的返回结果。用户看到的提示是“库存不足”而不是一串堆栈信息,这在真实项目中是基本要求。
后端还配置了 JWT 拦截器。登录接口除外,其他接口都要先经过拦截器校验 token,解析出用户 ID 后放到请求上下文里,后续业务代码直接从上下文拿当前用户。这种写法的好处是,你不需要在 Controller 里写一堆获取用户信息的重复代码。
2.3 数据库设计:几张核心表撑起电商闭环
先看最核心的商品表。字段一般包括商品 ID、分类 ID、商品名称、主图、轮播图、详情描述、单价、库存、销量、上下架状态、创建时间、更新时间、逻辑删除标记。注意价格的类型要用decimal,不要用float或double,电商系统里金额精度问题是不能妥协的。
分类表设计成支持层级结构,字段包括分类 ID、父级 ID、分类名称、排序值。家电品类下面可以挂“冰箱”“洗衣机”“电视”等子分类,前端可以根据parent_id递归渲染分类树。用户表、购物车表、订单表、订单明细表、收货地址表、轮播图表组成了整个电商交易主链路。
关于外键,这个项目大概率不会用物理外键,而是用逻辑外键。所谓逻辑外键,就是表里存着关联表的 ID,但不在数据库层面真正建立 FOREIGN KEY 约束。原因很简单:物理外键在插入、更新、删除时会做一致性校验,高并发下容易成为瓶颈;而且电商系统经常要分库分表,物理外键在这种架构下根本没法用。逻辑外键牺牲一点数据库层面的完整性,换来的是开发灵活性和扩展性。
订单表是这套系统里设计最讲究的表。它要有一个订单编号字段,由程序生成,格式一般是时间戳 + 用户 ID + 随机序列,保证不重复。订单金额和实付金额为什么要分两个字段?因为后续可能出现优惠券、满减活动,基础金额和实际支付金额必须分开记录。订单明细表存商品快照,为什么叫快照?因为商品价格会变动,下单时把商品名称、单价、数量、图片都复制到明细表里,订单生成之后即使商品后来改了价格,订单里的数据也不会受影响。
2.4 一条请求从登录到下单的完整链路
我习惯把这个过程当故事讲,因为它把所有模块串起来了。
第一步,用户在前端登录页输入用户名和密码,前端调用登录接口,后端校验账号密码正确后,用 JWT 工具类生成一个带过期时间的 token 返回给前端。前端把 token 存到localStorage里,同时把用户信息存到 Pinia。
第二步,用户看到商品后点加购,前端发起请求,Axios 拦截器自动把 token 放进Authorization请求头。后端拦截器拦截到这个请求,解析 token 得到用户 ID,放进请求上下文,然后放行。
第三步,Controller 接收到加购请求,调用 Service 层,Service 拿到当前用户 ID 和商品 ID,先查这个用户购物车里是否已经有该商品,有就加数量,没有就插一条新记录。
第四步,用户去结算页提交订单,后端把购物车里checked = 1的商品取出来,校验库存,生成订单主记录,批量插入订单明细,扣减库存,清空购物车,整个流程在一个事务里完成,任何一步失败都会整体回滚。
这条链路把前端路由、状态管理、后端分层、数据库设计、JWT 鉴权全部串了起来。你在读源码的时候,如果能把这条链路在脑子里跑一遍,理解速度会快很多。
3. 核心业务实现与逻辑细节
3.1 商品中心:展示、检索与推荐
商品展示是电商系统的门面,也是这套源码里前端交互最丰富的模块。首页一般会去查轮播图和推荐商品,推荐逻辑通常是用销量和创建时间排序,把热卖和新品放在显眼位置。数据量小的时候直接ORDER BY sales DESC, create_time DESC LIMIT 10就够了,不用做复杂的推荐算法。
商品列表页的核心是分页和筛选。前端传分类 ID、搜索关键字、排序方式、页码、每页条数,后端组装查询条件。用 MyBatis-Plus 的LambdaQueryWrapper写的话很直观:分类 ID 相等就eq,商品名模糊搜索就like,排序字段动态指定就orderByDesc。分页插件只需要在配置类里注册一下,然后new Page<>(pageNum, pageSize)传进去,返回结果里直接带总记录数和当前页数据,前后端都不用写复杂的 SQL 拼接。
商品详情页有一个很容易被忽略的细节:浏览数 +1。很多初写者会直接在查询详情的方法里加一个 update 语句,这种做法在小流量下没问题,但要注意它和查询操作在同一个事务里,频繁刷新会导致数据库写压力增大,这个可以等入门后再考虑用 Redis 做计数。
3.2 购物车:加购、勾选与数量管理
购物车表的设计核心是user_id + product_id联合维度。加购接口的逻辑是:先按这两个字段查购物车记录,查到了就把数量加一,查不到就插入一条新记录,数量设为 1。这个“先查再改”的逻辑,在并发场景下可能有重复插入的风险,但购物车加购的并发量远小于下单,所以危害可控。
购物车表里的checked勾选状态字段,是电商购物车跟普通“收藏夹”的本质区别。真实购物车要支持全选、取消全选、单独勾选、批量结算,所以必须在数据库里存这个状态,而不是前端自己算。前端页面改变勾选状态后,会调一个更新接口把最新状态同步到后端。
结算时,后端只会取checked = 1的购物车记录来计算订单金额。这里要注意一个边界情况:用户把商品加进购物车之后,商品可能被管理员下架或者删除。所以结算时后端要重新校验商品状态,如果商品已下架,要主动提示用户。这套源码里一般会通过商品表的上下架字段配合逻辑删除来处理,前端也能通过列表接口返回的实时库存做提醒。
3.3 订单闭环:事务、库存扣减与状态流转
订单提交是这套系统里最值得反复看的核心方法。它的流程是:校验收货地址和购物车数据,生成订单主记录,批量插入订单明细,扣减库存,删除已结算的购物车项,返回订单号。六个动作必须是一个原子操作,所以方法上会加@Transactional注解,任何一步抛异常,前面的数据库操作全部回滚。
库存扣减是有技术含量的地方。初级写法是先查库存,判断库存够不够,再执行UPDATE product SET stock = stock - ? WHERE id = ?。这个写法在并发场景下会出问题:两个请求同时查到库存是 1,同时判断够,同时执行更新,最后库存变成 -1,这就是典型的超卖问题。正确的做法是写一个条件更新:UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?。如果影响行数为 0,说明库存不足,直接抛业务异常触发回滚。这条 SQL 靠数据库的行锁天然规避了并发超卖,效率比先查后改高得多,也是这套源码里最值得记住的写法。
订单状态一般用数字表示:待支付、已支付、已发货、已完成、已取消。教学项目里的支付通常是模拟的,前端点“立即支付”,后端直接把订单状态改成已支付。但在真实项目中,支付回调、超时关单、取消订单恢复库存都是必须考虑的。很多初学者做完这个项目会在简历上写“订单模块”,但被问起超时关单怎么做就答不上来,所以我建议你在跑通源码后,自己把“下单后 15 分钟未支付自动取消并恢复库存”这个逻辑补上去,用定时任务或者延迟消息实现都可以,这会让项目深度提升一个档次。
3.4 管理后台:商品管理、文件上传与统计权限
管理后台是这个项目让普通人看起来“像个系统”的地方。商品管理要支持新增、编辑、删除、上下架、库存调整,分类管理要做成树形结构,订单管理要能修改订单状态,轮播图管理要支持上传图片。
文件上传是个高频功能,流程不复杂但坑不少。前端用 Element Plus 的el-upload组件选图,后端接收MultipartFile,用 UUID 重新生成文件名防止重名,图片保存到本地磁盘的一个固定目录,然后把访问路径返回给前端。关键点在后端要配置静态资源映射:把/upload/**这个 URL 路径映射到本地磁盘的真实存储路径,否则前端拿到的图片地址是 404。
权限控制上,管理端接口通常只允许管理员角色调用。一般的做法是写一个自定义注解,比如@RequireAdmin,然后在拦截器里判断当前用户的角色字段,不是管理员就返回“无权限”。你可以留意下后端是通过什么手段区分普通用户和管理员的,一般用户表里会有一个role字段,这是最直接的方案。
4. 跑通整个项目:从环境准备到前后端联调
4.1 环境版本清单与匹配关系
跑这套系统,环境版本是第一个容易出问题的地方。
| 软件 | 建议版本 | 重要说明 |
|---|---|---|
| JDK | 8 或 11 | SpringBoot2 兼容这两个版本,不需要 17 |
| Maven | 3.6+ | 用于后端依赖管理和启动 |
| Node.js | 16 或 18 | Vue3 + Vite 要求较新版本 |
| MySQL | 8.0.x | 驱动类和 5.7 不同,注意账号密码 |
| 前端包管理器 | npm 或 pnpm | 建议配置国内镜像源加速 |
我见过最可惜的失败案例,是 JDK 装了 17 之后后端项目一直启动失败,报一堆模块错误,结果换了 JDK8 立刻就好了。很多时候问题不在代码,在环境。所以先把本机版本和上表对齐,再开始跑项目,能省很多事。
4.2 数据库初始化与后端配置
项目文档里通常会有一个 doc 目录,里面放着 SQL 脚本和说明文档。拿到源码第一步,先打开数据库管理工具,创建一个字符集是utf8mb4的数据库,然后把 SQL 文件直接导入。导入成功后确认一下表和数据有没有进去,尤其是用户表里要有一个管理员账号,不然后台登录不了。
接着改后端配置文件,重点是 Spring 数据源的连接信息。我把关键配置贴出来,你对照着自己的实际情况改:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai&useSSL=false&allowPublicKeyRetrieval=true username: root password: your_password三个最容易出错的地方:驱动类一定是com.mysql.cj.jdbc.Driver,不是老版本的com.mysql.jdbc.Driver;连接串里必须带上serverTimezone,否则时区报错;MySQL8 的密码加密方式可能导致Public Key Retrieval is not allowed错误,所以 URL 里要加allowPublicKeyRetrieval=true。
后端启动命令就两个,任何选一个都行:
mvn spring-boot:run或者先打包再跑:
mvn clean package java -jar target/mall-admin.jar启动成功的标志是控制台出现 Tomcat 端口号,一般是 8080。如果有集成了接口调试页面,可以直接访问对应的 doc 地址,在页面上查看所有接口定义。
4.3 前端启动与代理配置
前端部分先装依赖,在有package.json的目录下执行:
npm install如果网络太慢,先设置镜像源再安装:
npm config set registry https://registry.npmmirror.com npm install启动前端开发服务器:
npm run dev默认端口是 5173,启动后浏览器打开这个地址。关键问题是前后端联调:前端跑在 5173,后端跑在 8080,直接请求会产生跨域。项目里一般会在vite.config.js中配置代理,把前端的/api请求转发到后端:
server: { port: 5173, proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } }有了这个配置,前端所有请求都写/api/xxx,浏览器看到的是同源请求,代理在背后转发给后端。你就不需要给每个 Axios 请求写完整的后端地址了。
4.4 联调验证:从登录到下单的完整实测
启动完成后的验证流程,我建议按这条路径走一遍:先打开系统首页,确认轮播图和推荐商品正常加载;注册一个新账号或直接用测试账号登录,进入商品列表页,切换分类、搜索关键字,确认分页正常;点进详情页,确认图片和库存显示正常;加入购物车,改数量、勾选、去结算、提交订单,确认订单生成;最后用管理员账号登录后台,确认能看到刚才的订单,并能修改订单状态。
这一套流程能走通,说明环境配置、数据库、前后端联调都没有问题。任何一个环节报错,都不是坏事,正好可以拿来练排查能力。下面这部分就是专门讲排查的。
5. 常见问题与排查技巧实录
5.1 依赖冲突与版本错位
后端启动报NoClassDefFoundError或者BeanCreationException,十有八九是依赖版本互相打架。比如 SpringBoot 依赖里自带一个 MyBatis 版本,你又手动引入一个不同版本的 MyBatis-Plus,两个包的类路径冲突,启动的时候就报各种奇怪的类找不到。
排查方法很固定:先用 Maven 命令看依赖树,确认到底引入了哪些版本:
mvn dependency:tree看到重复依赖就加exclusion排除,或者统一版本。另外注意 SpringBoot 2.7.x 对应的 MyBatis-Plus 版本要在 3.5.x 左右,太老的 MyBatis-Plus 和新版 SpringBoot 配合可能出问题。
5.2 MySQL 8.0 连接失败的典型场景
数据库连不上的报错千奇百怪,最常见的是这三个。第一个是Access denied for user,典型的账号密码错误或者用户没有这个数据库的权限,去数据库里授权一下就行。第二个是CLIENT_PLUGIN_AUTH is required,这是驱动版本太老导致的,换成 8.0 对应的驱动就能解决。第三个是Public Key Retrieval is not allowed,这个刚才已经说了,连接串里加allowPublicKeyRetrieval=true。
还有一个非常容易被忽视的问题:MySQL 服务根本没启动。很多人改了配置发现还是连不上,折腾半天才发现本地 MySQL 服务没有开。这种低级错误最浪费时间,排查的时候先从最基础的开始。
5.3 前后端联调时的跨域问题
跨域问题有两个层面的解法。后端可以配全局 CORS 过滤器,允许指定来源访问。前端可以走代理,把请求转发到后端同一台服务器,从浏览器角度看就是同源请求。
实际项目中主流做法是前端代理,因为部署到生产环境时,前端静态资源往往就放在后端服务前面,代理是天然存在的。如果前端代码里看到每个请求都写死了后端地址,那大概率没有走代理,这种前后端硬耦合的写法不建议借鉴,改成相对路径/api/xxx才是规范的做法。
5.4 业务逻辑上的隐蔽坑
有些问题不报错,但是行为不符合预期,排查起来更头疼。
比如商品删除之后,购物车里还有这个商品。因为购物车表里存了商品 ID,商品表做了逻辑删除,如果查询的时候没过滤deleted = 0,购物车数据会带着已删除商品一起出现。遇到这种问题,先确认查询里是不是漏了逻辑删除过滤条件。
再比如库存扣减后变成负数。如果代码用了“先查后改”的写法,并发下单时一定出现这个现象。解决办法参考我前面讲的条件更新 SQL,这是面试里的高频考点,也是实际项目里的底线要求。
还有图片上传后刷新就看不到的情况。文件确实存到了本地目录,但访问路径映射没配置,或者前端部署后访问的是另一个端口,导致图片地址解析不到。检查后端addResourceHandlers的配置,确认/upload/**映射到了正确的磁盘路径。
5.5 问题速查表
最后把高频问题整理成一个速查表,方便遇到报错时快速定位。
| 现象 | 排查方向 | 解决方案 |
|---|---|---|
| 后端启动报类找不到 | 依赖冲突 | mvn dependency:tree检查版本,排除重复依赖 |
| 数据库连接被拒绝 | 服务未启动 / 账号密码错误 / 权限不足 | 先确认 MySQL 服务状态,再核对账号密码 |
| 时区报错 | 连接串缺失参数 | 加serverTimezone=Asia/Shanghai |
| 前端请求 404 | 代理没配 / 接口路径写错 | 检查 vite proxy 配置和接口路径前缀 |
| 请求 CORS 报错 | 跨域未处理 | 配后端 CORS 或前端代理 |
| 库存变负数 | 并发下先查后改 | 改条件更新stock >= ? |
| 图片上传后无法访问 | 静态资源映射缺失 | 配置 resource handler 映射到存储目录 |
| 管理员登录失败 | 初始数据没有管理员 | 检查用户表的 role 字段和数据脚本 |
最后说一点拿到这类源码之后我的个人习惯:先把 SQL 跑起来,把项目跑通,再去读代码。读代码也从一条请求链路读起,跟着用户的操作走一遍,比从头硬读“包结构”要有用得多。如果你准备拿它做毕业设计或求职项目,特别推荐在跑通的基础上往下扩展三步:给订单模块补超时自动取消和库存恢复,把热门商品缓存到 Redis,把前端 JS 重构为 TypeScript。这三件事做完,这个项目的完成度在面试官眼里会完全不一样。