
做这个网上服装商城项目的时候我其实是被技术选型吸引过来的——SpringBoot2 加 Vue3 加 MyBatis-Plus 加 MySQL8.0这套组合放在今天依然是 Java Web 领域里相当能打的配置。前后端分离、通用 CRUD 封装、现代化前端框架几乎覆盖了企业级开发的核心套路。这篇文章不打算讲大道理就围绕这个商城系统源码把从环境搭建到核心业务实现的完整链路梳理一遍顺便把踩过的坑和排查思路也一并交代清楚。这个项目适合三类人正在做毕业设计的学生、刚入行想练手的后端开发、以及公司内部需要快速搭一套电商 Demo 做原型验证的工程师。它的价值在于你能从一套真实可运行的代码里同时看到后端服务化设计、前端组件化开发、数据库建模和订单状态机这几块知识点是怎么串起来的。1. 项目整体架构与设计思路1.1 为什么选 SpringBoot2 Vue3 这套组合先说后端。SpringBoot2 虽然不是最新的大版本但在企业生产环境里它的生态成熟度是最高的。为什么不用 SpringBoot3因为很多老牌中间件和第三方组件对 Jakarta EE 9 的适配还在过渡期项目如果追求稳定落地SpringBoot2 反而更省心。而且网上能找到的踩坑资料、代码示例、面试题覆盖面也最广遇到问题基本搜得到答案。MyBatis-Plus 的定位更直接它就是冲着消灭重复 CRUD 代码去的。传统 MyBatis 写一个简单的单表查询得配 XML、写 SQL、建 Mapper 接口而 MyBatis-Plus 提供了一整套内置的通用方法比如 selectById、selectPage、insert、updateById直接继承 BaseMapper 就能用。商城系统里像商品分类、轮播图、用户地址这类标准的增删改查根本不需要手写 SQL把精力省下来去处理订单、库存、购物车这些真正的业务难点。MySQL8.0 这边核心是窗口函数和公用表表达式CTE带来了很大的便利。比如统计销量排行、计算累计销售额一条窗口函数就能解决放在 MySQL5.7 里得写子查询甚至临时表麻烦得多。另外 MySQL8.0 的默认字符集是 utf8mb4表情符号和生僻字都能正常存储对商城这种需要展示商品描述、用户评论的场景来说非常关键。前端选择 Vue3 基本是当下的共识了。Composition API 让逻辑复用变得比 Options API 优雅得多写自定义 Hook 的时候体会特别明显。配合 Element Plus后台管理界面能快速搭建商城前台则可以利用 Vue3 的响应式机制做购物车实时计算。Vite 的开发体验也比 Webpack 舒服改代码热更新速度肉眼可见。1.2 这套系统的整体业务模型先把商城的核心角色拆出来看无非是用户端和管理端两块。用户端包含注册登录、商品浏览、商品详情、加入购物车、提交订单、支付模拟、查看订单、个人中心这些模块。管理端则要覆盖商品管理、分类管理、轮播图管理、订单处理、用户管理、数据统计。这里面订单模块是重中之重它牵扯到多个表的状态流转用户在购物车勾选商品生成订单要同时校验库存、计算总价、生成订单明细、扣减库存、清空对应购物车项最后还要创建支付记录。任何一个环节出了问题订单数据就会不一致所以这套代码里订单部分用了事务控制这是大家看源码时要重点关注的。和普通博客项目不同商城系统对数据一致性的要求很高。比如两个人同时买同一件只剩一件库存的商品如果不对库存操作做控制必然发生超卖。源码里用的是数据库行级锁的方式在扣减库存时使用 UPDATE t_product SET stock stock - 1 WHERE id ? AND stock 0 这样的条件更新通过受影响行数判断是否扣减成功这样既避免了超卖问题实现成本又低。2. 后端核心实现与数据库设计2.1 SpringBoot2 工程骨架搭建打开源码先看目录结构我建议新手按照 controller、service、mapper、entity、config、common 这六个包去理解。controller 只做参数接收和结果返回service 层处理业务逻辑mapper 层直接与数据库交互entity 是数据库表对应的实体类config 放配置类common 放统一返回结果、异常处理、工具类。这套分层的思想是Controller 尽可能薄Service 承担主要业务逻辑。比如用户下单Controller 里不要写业务判断只负责把参数校验后传给 Service真正的库存检查、价格计算都在 Service 里完成。这样做的优点很多最直接的好处是方便做单元测试你可以绕过 Controller 直接测试 Service 方法不用启动 Web 容器。创建 SpringBoot2 工程时有个细节要注意需要锁定 Spring Boot 2.7.x 这个范围内的版本因为 2.7 是 SpringBoot2 的最后一个维护分支安全更新和 bug 修复覆盖时间最长。如果选 2.5 或 2.6可能会有已知漏洞没有补丁。另外依赖关系也容易踩坑比如 mysql-connector-java 的 groupId 在 MySQL 官方驱动从 Oracle 移交到社区后变成了 com.mysql而 mysql-connector-j 是新的名字如果引入错了启动阶段不会报错但第一次查询数据库时就会抛驱动类找不到的异常。源码里连接池用的是 HikariCP这是 SpringBoot2 默认的数据源连接池之所以选它是因为在性能和稳定性测试里它的表现都优于老牌的 Druid。要注意的配置项是 maximum-pool-size默认是 10但在商城这种读多写少的场景下可以适当调大到 20避免高峰时期线程都在等连接。而 minimum-idle 控制在 5 就行太小了容易频繁创建新连接太大又浪费资源。2.2 MyBatis-Plus 通用 CRUD 的实战用法MyBatis-Plus 最核心的价值在于 BaseMapper 提供了 17 个内置方法你不用写一条 SQL 就能完成绝大多数单表操作。来看实际场景public interface ProductMapper extends BaseMapperProduct { // 没有任何代码就已经拥有了 insert/deleteById/selectById/updateById/selectPage 等方法 }当你需要一个带条件的分页查询时调用方式是这样的LambdaQueryWrapperProduct wrapper new LambdaQueryWrapper(); wrapper.like(StringUtils.isNotBlank(name), Product::getName, name) .eq(Product::getCategoryId, categoryId) .orderByDesc(Product::getCreateTime); PageProduct page new Page(current, size); productMapper.selectPage(page, wrapper);LambdaQueryWrapper 的好处是类型安全字段名用的是方法引用而不是字符串编译期就能发现错误。比如你写错了 .eq(Product::getName, name) 在编译时就会被拦截Blog 项目里常见的把字段名写错字导致运行期才报 SQL 异常的情况也就不会发生了。还有一个实用技巧是逻辑删除。商城里的商品如果管理员误操作删除了我们希望数据能保留以便恢复而不是物理删除。MyBatis-Plus 在实体类字段上加上 TableLogic 注解配置好全局的 deleted 字段之后 deleteById 会自动变成 update set deleted 1select 时也会自动加上 deleted 0 的条件。TableLogic private Integer deleted;这个机制的实现原理其实也不复杂MyBatis-Plus 在运行期对 SQL 做了动态拼接。理解了这个机制你就知道逻辑删除只对通过 MyBatis-Plus 内置方法执行的 SQL 生效如果你自己写了 XML 里的自定义 SQL就必须手动加上 deleted 0 的过滤条件这也是一个很容易被忽略的坑。2.3 MySQL8.0 数据库设计要点商城表结构我建议重点关注这几张sys_user 用户表、product 商品表、product_category 分类表、product_sku 库存表、cart_item 购物车表、orders 订单主表、order_item 订单明细表。订单主表和订单明细表为什么要拆开原因在于一个订单对应多个商品如果把商品信息直接塞进订单表字段会爆炸且难以查询。拆开之后orders 表存订单级信息订单号、总金额、状态、用户ID、收货地址order_item 表存商品级信息商品ID、商品名称、快照价格、数量。这里有个经验订单明细里必须冗余一份商品名称和价格的快照因为商品之后可能改名、调价但你历史订单里的金额不能跟着变否则对账就乱了。订单状态字段我建议用 tinyint 而不是 varchar。电商订单状态基本是固定的用数字 0、1、2、3 分别表示待付款、待发货、待收货、已完成再通过一个枚举类做映射既节省存储空间在 Java 代码里也能用枚举做类型安全的判断。MySQL8.0 里有一点要特别注意时区问题。连接串里如果不加 serverTimezone 参数在默认环境UTC下获取到的时间会比北京时区少 8 个小时。源码里连接串的正确写法是jdbc:mysql://localhost:3306/mall_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrueallowPublicKeyRetrievaltrue 是为了解决 MySQL8.0 使用 caching_sha2_password 认证插件时出现的 Public Key Retrieval is not allowed 报错这个问题在我刚接触 MySQL8.0 的时候浪费了不少时间值得记住。3. 前端核心实现与业务联动3.1 Vue3 工程搭建与目录结构规划前端用的是 Vue3 Vite Element Plus Pinia。搭建过程很简单但目录规划很有讲究。源码里的前端目录大致是这样的src/api 存放所有接口请求方法统一封装 axios 实例src/router 存放路由配置区分前台用户页面和后台管理页面src/store 存放 Pinia 状态管理模块比如用户信息、购物车状态src/views 存放页面组件按功能模块分子目录src/components 存放公共组件比如商品卡片、分页组件、轮播图组件axios 封装这一层非常关键。统一拦截器的作用有两个一是在请求发出前自动把后端返回的 token 塞进请求头二是在收到响应后做统一错误处理状态码 401 就跳转到登录页业务码非 0 就弹出错误提示。否则你每个接口都要手动处理这些逻辑代码里到处都是重复的 if 和 alert。// 这个代码在 store/user.js 里用于管理用户状态 import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: {} }), actions: { setToken(token) { this.token token localStorage.setItem(token, token) }, clearLogin() { this.token this.userInfo {} localStorage.removeItem(token) } } })Pinia 和 Vuex 相比API 简洁太多没有 mutations 的强制区分直接在 actions 里写同步更新逻辑TypeScript 的类型推导也更好。对商城这种业务不算特复杂但状态多跨页面的场景Pinia 确实是省心选择。3.2 商城前台关键页面的实现逻辑商品列表页是用户进来的第一站也是前后端交互比较复杂的页面。它需要同时处理分类筛选、关键字搜索、价格区间、分页这四个维度。前端把搜索条件整理成一个 query 对象传给后端后端用 MyBatis-Plus 条件构造器动态拼装 SQL然后以分页结果返回。商品列表里最难处理的是图片懒加载。商城的商品图片往往几百张如果一次性全部加载首屏会非常慢。Vue3 里可以通过 v-lazy 指令或者引入 vue-lazyload 插件来解决核心思路是只加载视口内的图片滚动到哪个位置再加载哪个。实测下来首屏渲染速度能提升 40% 以上是很值得做的一个优化。购物车模块是前端状态管理的重头戏。用户可能一次性添加多件商品切换页面后还要保持数据不丢失。源码里的处理方式是加入购物车时向后端发请求写入 cart_item 表前端同时用 Pinia 维护一份购物车状态这样首页的角标和购物车的列表能瞬时响应不需要每次渲染都去请求后端。购物车数量在商品详情页修改购物车页面要同步更新并实时重算总价这里应对交叉状态更新的简单方法是统一从 Pinia 的 getter 里读取最终价格数据流是单向的页面代码就不容易写出 bug。3.3 管理后台与 Vue3 常用组件实践后台管理界面用的是 Element Plus。如果你是第一次用这个组件库最需要熟悉的三个东西是表格、表单和弹窗的组合用法。商品管理页的典型结构是查询条件表单、el-table 展示商品列表、el-dialog 内嵌新增/编辑表单。表单校验是电商后台很重要的体验点比如商品价格必须是大于 0 的数字库存不能为负数上传图片必须有文件。Element Plus 的表单校验规则是在 rules 里声明的const rules { price: [ { required: true, message: 请输入价格, trigger: blur }, { pattern: /^(\d)(\.\d{1,2})?$/, message: 价格格式不正确, trigger: blur } ], stock: [ { required: true, message: 请输入库存, trigger: blur }, { type: number, min: 0, message: 库存不能为负数, trigger: blur } ] }商品图片上传组件是我强烈建议单独封装的一个通用组件因为分类图片、轮播图、商品主图都要用到。核心是调后端上传接口拿到 URL再回填到表单的图片字段中。封装一次后面所有页面都能复用这其实就是 Vue3 组件化开发的核心思路——把一件重复性高的事情抽象成独立组件。后台还有一个容易被忽视的管理模块是轮播图管理。轮播图虽然不是核心业务但它是提升商城首页观感的直接元素而且它的字段里有 sort 排序值修改 sort 后前端要按降序重新拉取列表。用 MyBatis-Plus 的 orderByDesc 方法处理这个需求很简单。4. 几个核心业务场景的难点拆解4.1 登录认证与权限控制方案商城的登录认证用的是 JWTJSON Web Token。它的原理说起来不复杂用户登录成功后后端生成一个包含用户信息的 token 返回给前端前端把 token 存在本地之后每次请求在请求头里带上它后端再校验 token 的合法性。String token Jwts.builder() .setSubject(userId.toString()) .setExpiration(new Date(System.currentTimeMillis() 3600000)) .signWith(secretKey, SignatureAlgorithm.HS256) .compact();用 JWT 而不是传统的 Session 方案核心原因是商城系统可能部署在多台服务器上如果用 Session 就面临 Session 同步问题。JWT 是无状态的每个请求自己携带凭证后端不需要保存会话信息天然适合分布式环境。但 JWT 也有需要注意的地方token 过期后怎么处理源码里用了简单的方式token 有效期设为 1 小时当接口返回 401 错误时前端 axios 拦截器捕获后直接跳转登录页。这个方案简单粗暴但可用如果想要更顺滑的体验可以加上 refresh_token 机制这里就不展开讲了。权限控制上商城比较简单只区分管理员和普通用户两个角色。后端在 JWT 拦截器里取出用户角色如果是管理端接口就校验角色是否为 1。要注意拦截器的白名单配置比如注册、登录、商品列表这些接口不能拦截否则用户还没登录就访问不了任何页面那就出大问题了。4.2 购物车与订单事务联动来具体梳理用户从点击购买到生成订单的完整链路这里是最容易出错的一段代码逻辑。用户请求提交订单后端 OrderService 大概要按顺序执行以下步骤根据用户ID查询购物车中被勾选的项目遍历购物车项检查每件商品的库存是否充足计算订单总金额创建订单主表记录状态为待付款批量插入订单明细表记录扣减每件商品的库存删除已被下单的购物车项返回订单号和总金额上面任何一步失败前面已经执行的操作都要全部回滚。所以源码里对这个方法加了 Transactional 注解让 Spring 在抛出运行时异常时自动回滚整个事务。Transactional(rollbackFor Exception.class) public ResultVO createOrder(Long userId) { ListCartItem cartItems cartItemMapper.selectUserCheckedItems(userId); if (cartItems.isEmpty()) { return ResultVO.error(没有选中商品); } // ... 校验库存、计算金额、插入订单... }关于事务的细节这里必须强调Transactional 只有在调用方和被调用方不在同一个类时才会生效通过代理实现如果是在同类里的自调用事务是失效的。源码里把订单创建方法写在 OrderService 里从 Controller 调用这个没问题。但如果你把事务方法写在 Controller 里再私有调用那就有隐患了。另外 rollbackFor Exception.class 这个参数也非常重要。Spring 默认只在抛出 RuntimeException 时回滚事务而 checked exception比如 IOException不会触发回滚。如果代码里捕获了异常包装成自定义业务异常抛出但忘记指定 rollbackFor就会出现数据库更新了一半但没有回滚的脏数据。这是实际生产环境里容易埋雷的地方。4.3 支付模拟与订单状态更新电商系统的真实支付要对接支付宝、微信支付涉及签名、回调、对账等一堆逻辑。但教学项目为了演示完整流程源码里做的通常是模拟支付——用户点击立即支付按钮前端弹窗模拟支付成功后端直接把订单状态更新为待发货。让我重点讲一下支付回调的思路。真实的支付流程中后端需要提供一个回调接口支付平台在用户支付成功后主动调用这个接口通知结果。源码里模拟支付虽然不一定开了回调接口但你在写技术方案说明时最好把这个流程画明白用户提交订单后后端生成预付单并记录到支付流水表前端跳转到收银台页面调用模拟支付接口模拟支付成功 notify 后端接口修改订单状态和支付流水状态前端轮询订单状态成功后跳转到订单详情页订单状态的流转用一个常量类或枚举类管理会清晰很多。我在开源项目里常用的做法是定义 StatusEnum把每个状态的描述和可跳转的后续状态都写清楚这样在代码里流转时不容易把状态值写错。5. 部署运行与常见问题排查实录5.1 本地环境搭建与运行步骤如果你拿到源码后想先跑起来我的建议是严格按下面这个顺序操作第一步装 JDK 8 或 11配置 JAVA_HOME 环境变量。一般来说 Java 版本和 SpringBoot2 兼容性最好的 JDK 8 也能跑JDK 11 也完全没问题建议用 JDK 11官方支持时间更长。第二步安装 MySQL8.0。Windows 上 Community Server 安装包是完全免费的Linux 上用包管理器就能装。注意安装过程中设置的 root 密码要记住后面改配置要用。第三步创建数据库。用 Navicat 或者命令行执行源码里提供的 mall_db.sql 脚本它会自动建库建表并插入一些演示数据。第四步修改后端的 application.yml 配置文件重点是数据库连接串、账号密码。spring: datasource: url: jdbc:mysql://localhost:3306/mall_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/Shanghai username: root password: 123456第五步启动后端。用 IDEA 打开 pom.xml 导入依赖然后运行 MallApplication 的主类。看到日志输出 Started MallApplication 就说明启动成功。第六步启动前端。命令行进入 frontend 目录先执行 npm install 安装依赖然后 npm run dev 启动 Vite 开发服务器。浏览器打开终端提示的地址一般为 localhost:5173。第七步用源码里的测试账号登录。管理员账号一般是 admin/admin123用户账号在 SQL 脚本里会预置几条比如 user/123456。5.2 我实际踩过的坑与排查方法代码跑起来之后坑往往出现在你意想不到的地方。我把自己踩得最多的几个问题整理成了一张排查表希望能帮大家少走弯路问题现象可能原因排查思路与解决方案启动报 Communications link failure数据库没有启动或密码错误检查 MySQL 服务是否运行检查 application.yml 密码是否正确连接 MySQL 时报 Public Key Retrieval is not allowedMySQL8.0 认证方式问题在连接串加 allowPublicKeyRetrievaltrue前端页面打开空白控制台报跨域错误后端未启动或后端地址不正确确认后端启动成功检查 vite.config.js 中 proxy 配置的 target 地址是否与后端端口一致前端请求返回 401token 过期或未传递重新登录检查 axios 请求拦截器是否把 token 放进了 header运行 npm install 报孤立包错误Node 版本太旧或依赖冲突删除 node_modules 和 package-lock.json 后重新 install上传图片失败后端上传目录权限不足检查后端配置的上传本地目录是否存在并给与写权限下单时报库存不足但库存明明有事务回滚导致库存扣减失败检查订单创建方法是否加了 Transactional以及是否有异常被吞掉时间显示相差 8 个小时数据库连接串缺少时区配置连接串中加 serverTimezoneAsia/Shanghai后台管理页面的新增表单提交后没有反应表单校验未通过打开浏览器控制台看是否有校验错误提示重点检查必填字段是否填写完整Element Plus 组件样式错乱全局样式覆盖或组件引用方式错误确认是否按文档引入完整样式 app.use(ElementPlus)避免局部样式覆盖全局分页数据返回正常但前端列表不刷新Pinia 状态未更新或组件未响应检查列表页是否使用 reactive 或 ref 保存数据赋值时不要直接替换整个对象登录后刷新页面用户信息丢失用户信息只存在 Pinia 内存中登录成功后把用户信息同时写入 localStorage每次刷新后先读取再填充 store后端接口返回 500 且日志出现 NullPointerException实体对象或参数为 NUll检查参数非空校验推荐使用 NotBlank 注解或前端做必填判断MySQL8.0 连接时报 Unknown database数据库没有创建或名称不对执行 SQL 脚本或者用 CREATE DATABASE 手动创建同名数据库这里要特别说一下跨域问题。前后端分离项目如果没有配好代理大概率会撞上跨域。源码里的做法是在 Vite 里配置开发者代理请求 /api 前缀的接口自动转发到后端 localhost:8080import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } } })changeOrigin 设置为 true 非常关键它会把请求头中的 Host 字段改成目标地址的域名后端接收请求时不会因为是跨域请求而拒绝。另一种方案是在后端加全局跨域配置类实现 WebMvcConfigurer 接口并重写 addCorsMappings 方法。两种方式都可行开发环境用 Vite 代理更灵活生产环境则更适合在后端配 CORS。5.3 关于文档的那点事源码包里附带了一份项目文档一般是需求分析、数据库设计、接口说明、部署手册这几部分。我是真心建议大家花时间把这份文档从头到尾看一遍因为看文档的过程中你能注意到很多代码里不会显示的细节比如为什么要加这个字段某个状态为什么要这样流转。写文档这件事本身对程序员来说是很好的复盘方式。这个商城项目做完后我建议你也尝试自己补充一份文档把接口设计的取舍写进去。当你以后面试或者接手更复杂的系统时这种全局视角的价值会特别大。6. 这套源码还可以怎么扩展项目跑通了基础功能也理顺了接下来其实是真正有意思的部分——在这个基础上做扩展。我认为可以在下面几个方向上动手缓存优化是最值得做的第一步。商城商品的访问热度差异很大热门商品被频繁查询但内容不常变化。在商品详情查询接口上加一层 Redis 缓存命中缓存就直接返回避免每次都查数据库。商品上架或修改时再主动删除对应缓存保证数据一致性。这个改动不复杂但对高并发场景的支撑能力提升非常明显。搜索功能的强化也很有价值。目前商品搜索大概率是用 MySQL 的 LIKE 模糊查询数据量小的时候没问题但到几万条商品时性能就会开始拉垮。可以考虑引入 Elasticsearch利用它的倒排索引实现商品关键字的快速检索甚至可以做到搜索关键词自动补全和搜索结果高亮。支付模块如果要接真实的支付宝或微信支付需要走的流程会多不少申请商户号、配置密钥、对接网关、处理回调签名验证、对账逻辑。不过核心思路和模拟支付是大同小异的主要是要对签名算法和回调幂等性特别注意。秒杀场景也是电商面试里一定会被问到的热点。商城源码的普通下单流程在高并发秒杀时撑不住因为数据库行锁的争用太严重。可以参考的思路是先把秒杀请求放入 Redis 队列做削峰再用 Lua 脚本原子性地扣减 Redis 中的库存最后异步把成功下单的数据同步到 MySQL。这个架构改造说起来不复杂但每一步都有细节可以深挖。另外多商户支持、优惠券系统、商品评论评分、推荐系统这些都是可以作为独立模块扩展的方向。最后聊点实操中的体会做这类电商项目我最想提醒的一件事是不要把主要精力耗在技术炫技上先把核心业务逻辑做对、做稳。商品、购物车、订单、支付这条链路能不出 bug 地跑通比什么微服务、分布式事务这些概念都重要。源码里用到的 MyBatis-Plus 通用 CRUD 和事务控制虽然看起来朴实无华但恰恰是解决实际问题最高效的武器。另外遇到问题的时候我强烈建议你先看日志再看网络请求最后才去猜代码逻辑。后端启动日志里如果有异常堆栈信息通常已经指明了问题方向。前端控制台的报错和 Network 面板的响应状态码能帮你快速定位是接口没通、参数传错还是代码运行时报错。学会用好这些基础排查手段你解决 bug 的效率能翻倍。这个商城项目能折腾的细节还有很多前端可以做移动端适配、骨架屏、路由懒加载后端可以加接口限流、操作日志、系统监控。不妨先从一个小功能开始比如给商品模块加个浏览记录然后慢慢把整条链路吃透。代码要自己敲一遍才记得住坑要自己踩一遍才知道怎么避这才是这套源码最大的学习价值所在。