从零做一个Springboot+Vue电脑商城系统,我的源码梳理、部署踩坑和讲代码的心得
作为一个做过不少全栈练习项目的人,我见过太多同学卡在同一个地方:框架学了一堆,demo跑通了,但一提到“完整项目”就发怵。今天要聊的这套基于Springboot+Vue的电脑商城系统,就是非常适合用来打通全栈任督二脉的典型项目。它不像秒杀系统那样动不动就分布式、消息队列,也不像CRUD后台那样简单到没有成就感,而是恰好卡在一个“麻雀虽小五脏俱全”的甜点上:有商品、购物车、订单、支付回调、权限控制,前端还要处理路由守卫、状态管理、接口联调。这套系统的源码、部署文档和代码讲解,我会用实际做过的方式拆开细讲,包括哪些部分值得花时间看、哪些地方容易写废、部署时哪个环节最坑。如果你是准备用这个项目做毕业设计,或者想通过一个完整案例把Springboot和Vue串明白,这篇文章应该能让你少走不少弯路。
1. 电脑商城这个业务场景,为什么特别适合全栈练手
先说结论:商城类系统是极少数“业务复杂度够用,但技术深度刚好不劝退”的项目类型。我做过的几个练手项目里,博客系统偏简单,后台管理系统偏枯燥,而电脑商城这种带明确商品属性和交易流程的场景,天然能驱动你写出有逻辑的代码,而不是为了用技术而堆技术。
1.1 业务链路完整,前端后端都能练到真东西
一个电脑商城要跑通,至少需要这些业务动作:用户注册登录、商品分类浏览、商品详情查看、加入购物车、确认订单、生成支付订单、模拟支付回调、订单状态更新。这条链路下来,后端要处理的不只是增删改查,还有库存扣减、订单号生成、支付状态校验这些稍微带点味道的逻辑。前端这边也不是纯静态页,购物车状态要跨页面共享,没登录就不能下单,下单成功要跳到订单列表——这些正好逼着你用Vuex和路由守卫。
我自己体会最深的点是,商城系统的“状态”特别多,比如购物车数量、订单状态、用户登录态。每个状态放在哪里管理,是放在组件内部、Vuex里还是后端返回,这个思考过程比写代码本身值钱得多。你做完一遍之后,再去面试被问“前端如何管理状态”或者“后端如何保证订单数据一致性”,脑子里是有画面感的,而不是背概念。
1.2 角色划分清晰,方便控制项目规模
一套合格的电脑商城系统,通常分前台用户端和后台管理端。用户端负责浏览商品、下单、查看订单,管理端负责商品上下架、分类管理、订单发货。这两部分共享同一套后端接口,但前端是两个独立工程,后端也天然分成普通用户接口和管理员接口两层。这样拆的好处是,你能在一套项目里同时练到“面向用户的交互设计”和“面向数据的管理界面”,还不用担心代码糊在一起。
我做的时候是先把用户端跑通,再补管理端,这样迭代压力小很多。很多人上来就想着一次把两边做完,结果前端写了一堆重复页面,后端接口也分不清哪些是给谁用的。建议你反过来:先按角色把页面列清楚,再设计API路径前缀,比如/api/user/**和/api/admin/**分开,这样后面加权限拦截就非常顺。
1.3 选电脑品类比服装、生鲜更省心
这个可能很多人没提到,但实际做的时候区别很大。电脑商品属性相对规整,比如品牌、CPU、内存、显卡、价格、库存,不太需要像服装那样处理多规格(颜色、尺码)和SKU联动。生鲜类还要搞保质期、冷链啥的。电脑品类一张表基本能搞定,即便拆两张规格表也清晰。对练手项目来说,这能避免在业务建模上消耗过多精力,把时间留给框架和代码质量。
所以,如果你正在纠结做什么项目,听我一句:别上来搞秒杀,也别搞社交电商,先把一个规规矩矩的电脑商城做利索,比什么都强。
2. 源码结构怎么组织才不乱,我的分包思路和容易踩的坑
拿到一套源码,第一件事不是急着跑,而是先看目录结构。结构合理不合理,直接决定你后面改代码、写部署文档、给人讲代码的体验。我见过太多项目,代码全堆在controller里,一个类写两千行,这种项目就算跑起来,你也很难跟别人讲明白,更别说维护了。
2.1 后端分包:按业务模块切,而不是按技术层切
后端用Springboot,最常见的分包有两种思路:一种是按技术层分controller、service、mapper,所有业务混在一起;另一种是按业务模块分,比如user、product、cart、order、admin,每个模块内部再分controller和service。我强烈推荐后者。虽说按层分包在超大型项目里也常见,但对电脑商城这种中等体量项目,按业务模块分包读起来最直观——你找一个“订单功能”,直接进去order包看,里面就那三五个类,一眼扫完。
我推荐的结构大概是这样的:
com.example.mall ├── common // 通用类:统一返回结果、异常处理、常量 ├── config // 配置类:跨域、拦截器、MybatisPlus配置 ├── controller // 各业务模块的controller放在一个包下面 ├── entity // 实体类,对应数据库表 ├── mapper // 数据访问层接口 ├── service // 业务逻辑层接口和实现 └── util // 工具类:JWT生成解析、订单号生成这里有个细节要注意:common里的统一返回结果类,一定要在一开始就写好。我喜欢定义一个Result<T>,字段包含code、message、data,所有接口都返回这个结构。别嫌麻烦,等你写前端的时候就知道,前端axios拦截器统一处理返回结构是多么省事的事情。如果不统一,前端每个请求都要单独判断,代码会非常啰嗦。
2.2 前端分包:按页面视图切,公共组件单独拎出来
Vue前端这块,src目录下的组织方式对后期维护影响也很大。我做这个项目时是这样分的:
src ├── api // 所有接口请求函数,按模块分文件 ├── assets // 静态资源 ├── components // 公共组件,比如商品卡片、订单列表项 ├── router // 路由配置 ├── store // Vuex状态管理 ├── utils // 请求封装、token管理 └── views // 页面视图,按用户端和管理端分目录特别强调一下api目录。很多人会直接在页面里写axios.get(...),一开始项目小没事,等页面多了,你会发现接口路径满天飞,改一个后端地址要全局搜索。正确的做法是把所有接口按模块集中到api文件夹里,比如product.js里放商品相关的所有请求函数,order.js里放订单相关的。这样做还有一个好处:每个接口的入参和返回结构都在一个地方,前端排查问题时非常明确。
2.3 最容易写脏的两个地方
第一是全局异常处理。很多新手项目里try-catch满屏飞,其实Springboot可以用@RestControllerAdvice做全局异常捕获,业务代码里根本不需要那么多try-catch。自定义一个业务异常类BizException,在service里该抛就抛,全局处理器统一返回失败结果,前端拿到的永远是同一套JSON格式。这个要是做好了,代码整洁度能上一个档次。
第二是时间格式化。数据库里存的是datetime,后端返回给前端的就是不规范的ISO字符串,前端又要额外做处理。Springboot里可以直接在application.yml里配置:
spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: GMT+8这一行配置解决前后端时间格式打架的经典问题。我见过不少项目卡在这上面,前端拿到的日期是2025-08-09T12:30:45.000+00:00,然后各种找解析库,其实后端一行配置就能搞定。
3. 核心业务模块的开发逻辑:商品、购物车、订单不能马虎
商城的核心模块其实就三块:商品浏览、购物车管理、订单流转。每一块都有它的业务特点,写的时候想清楚逻辑,比直接上手敲代码重要得多。
3.1 商品列表与搜索:分页参数和条件拼接是基本功
电脑商城的商品列表基本都需要分页加条件查询。可以用MybatisPlus提供的Page对象配合LambdaQueryWrapper,条件搜索就是动态拼接eq、like这些方法。这里的关键点是:前端传过来的筛选条件,后端要做空值判断,不能前端不传价格区间的时候你后端直接SQL报错。
我记得自己第一次写分页时,傻乎乎地手写SQL的LIMIT offset, size,还要自己算offset,后来发现MybatisPlus的Page分页直接帮你处理了。你只需要这样:
Page<Product> page = new Page<>(current, size); LambdaQueryWrapper<Product> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(StringUtils.isNotBlank(brand), Product::getBrand, brand); wrapper.like(StringUtils.isNotBlank(keyword), Product::getName, keyword); wrapper.orderByDesc(Product::getSales); productMapper.selectPage(page, wrapper);注意StringUtils.isNotBlank这个判断,它决定了条件是否加入SQL。这个模式可以推广到所有列表查询接口。
还有一个小坑:价格筛选用的是between,如果前端只传了一个价格上限或下限,你得代码里判断再拼条件:
if (minPrice != null) { wrapper.ge(Product::getPrice, minPrice); } if (maxPrice != null) { wrapper.le(Product::getPrice, maxPrice); }这些细节看起来琐碎,但都是真实商城系统里一定会遇到的。
3.2 购物车:后端存还是前端存,我的选择和建议
购物车是很多初学全栈的人纠结的点。最省事的方式是纯前端用Vuex存,购物车数据放本地不请求后端。但这样做的问题很明显:用户换一台设备,购物车就没了;而且订单生成时还得把购物车数据传后端重新校验,多一层风险。如果你是做正经项目,我建议后端存一张购物车表,字段就是user_id、product_id、quantity,前端负责调用接口同步事实。
后端存购物车的好处是,下单的时候可以直接从数据库读用户购物车条目来创建订单,库存校验和金额计算都在后端完成,前端没法篡改价格。前端这边,Vuex里维护一个cartCount数字,用户的每次添加、删除购物车操作都调后端接口,成功后再更新本地状态。这样购物车数量在页头就能实时显示。
3.3 订单状态机:从待付款到已发货的流转
订单模块是整个系统里最值得多花时间研究的。我的订单表会设计一个状态字段,用数字表示:0待付款、1待发货、2待收货、3已完成、-1已取消。每次状态变更,都必须校验当前状态是不是目标状态的前置状态。比如待发货订单不能直接跳到已完成,必须先经历待收货。
用一个专门的OrderStatus枚举来管理这些状态位,别到处写魔法数字:
public enum OrderStatus { UNPAID(0, "待付款"), SHIPPED(1, "待发货"), RECEIVED(2, "待收货"), COMPLETED(3, "已完成"), CANCELED(-1, "已取消"); // getter和setter... }下单的时候,要做的操作比较多,最好在service层加事务注解@Transactional。大致流程是:校验购物车、计算总价、生成订单号、扣库存、清空购物车。其中任何一个环节失败,整个操作都要回滚,不然会出现订单建了但库存没扣的脏数据。记得当时我的项目里库存扣减是直接UPDATE product SET stock = stock - #{count} WHERE id = ? AND stock >= #{count},用SQL层面的条件来防止超卖。虽然商城系统并发不会特别高,但这个写法本身就是个好习惯。
3.4 支付模块:模拟支付回调怎么设计才像那么回事
真实商城要对接支付宝微信支付,但练手项目一般都做模拟支付。模拟支付要设计得像真实流程:前端发起支付请求后跳到一个模拟收银台页面,点击“确认支付”,后端收到请求后执行一个payOrder逻辑,把待付款订单改成待发货,同时更新支付流水记录。
我这里强烈建议加一张支付流水表,记录订单号、支付金额、支付方式、支付时间。这样你的订单列表能显示哪些订单支付过,管理端也能看流水,项目显得完整很多。模拟支付接口的回调地址写在前端路由里,可以起一个/pay/success的页面,支付成功后展示结果,然后跳转订单详情。这一套做完,后续如果要接入真实支付,只需要换掉收银台那段逻辑,其他代码都不用大改。
4. 前端工程化要点:路由、权限、请求封装一次讲明白
前端这部分,Vue项目最核心的两个点:一个是路由该怎么配才能防住未登录用户,另一个是axios怎么封装才能不写重复代码。
4.1 路由守卫和权限控制
商城系统的用户端要求登录后才能进购物车和订单页面,管理端要求登录且角色是管理员才能进后台。实现方式是Vue Router的beforeEach守卫里做检查:
router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') if (to.meta.requiresAuth && !token) { next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.meta.isAdmin && !isAdmin()) { next('/') } else { next() } })关键是要在路由的meta字段里标注权限信息。比如/admin下的子页面都设置meta: { requiresAuth: true, isAdmin: true },用户下单相关的设置requiresAuth就够。这里踩过的坑是:管理员登录后也需要有用户端的能力,所以角色判断时要用管理员标识位去放行,而不是只判断普通用户token。
4.2 axios封装:拦截器处理token和错误提示
前端请求后端的标准姿势是封装一个request.js,统一配置baseURL、超时时间,在请求拦截器里加token,响应拦截器里剥出data或者统一报错。我写出来的版本大概是这样的:
import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }) request.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(new Error(res.message)) } return res.data }, error => { Message.error(error.message || '网络异常') return Promise.reject(error) } )注意,baseURL: '/api'这个配置是配合前端工程里的devServer.proxy把请求转发到后端,这样在开发环境没有跨域问题。部署到生产时,Nginx也要做对应的反向代理。这一块别偷懒,直接写死完整的后端地址也行,但换环境就要改代码,很麻烦。
4.3 Vuex到底要存哪些东西
很多初学者会把后端给的商品列表、订单列表全部塞进Vuex,这是错误用法。Vuex适合存全局共享且低频变化的数据,比如用户信息、购物车数量、未读消息。商品列表这种每次进页面都要刷新而且数据量大的数据,建议在组件里调用API,拿到后存本地data或者ref里即可。
我这套系统的Vuex模块只设了三个:user存token和用户信息,cart存购物车条目和数量,app存侧边栏折叠这种全局UI状态。这几个模块之间要互不依赖,修改一个不影响另一个。这样代码逻辑非常清晰,排查问题也容易。
4.4 组件通信的几种常见方式
新手往往纠结组件传值。商城用的最多的就是父组件给子组件传商品对象,子组件emit增加购物车事件,跨层级的用户信息用Vuex。有一个很实用的点:商品卡片组件ProductCard接收product对象,点击“加入购物车”时不应该自己调接口,而是通过this.$emit('add-to-cart', product)把事件抛给商品列表页,由列表页统一处理。这样做的好处是组件可复用,以后你在首页、搜索页、管理端都放同一个商品卡片,不需要改卡片内部逻辑。
5. 部署文档是给谁看的?我写部署文档的思路和几个必填重点
标题里提到的“部署文档”,是这类源码资料里特别关键的产物。很多人写部署文档只写一句“把项目打包扔到服务器上就行”,这种等于没写。部署文档的第一原则是:假设读者是一个只装了JDK和Maven的人,他能按你的文档一步步把系统跑起来,过程中不需要自己猜。所以,部署文档要详细到“执行哪条命令、在哪个目录下执行、看到什么输出算成功”。
5.1 本地开发环境部署,文档要这样写
先讲本地跑通的流程。后端需要的环境是JDK8+和Maven 3.6+,数据库MySQL 5.7或8.0。文档第一步要写清楚如何创建数据库,比如给出一条初始化SQL命令:
CREATE DATABASE mall_db DEFAULT CHARACTER SET utf8mb4;然后导入项目里提供的mall.sql文件。这种细节如果文档里不写,新手真的会卡在“找不到表”上。第二步是在application.yml里改数据库的用户名密码,并确认端口没被占用。第三步是用mvn spring-boot:run启动后端,看到Tomcat started的日志就算成功。
前端本地开发是npm install安装依赖,然后npm run serve启动开发服务器。这里有个很容易踩的坑:npm install可能因为Node版本太高而报错,我项目里用的依赖版本比较旧,切换Node版本到16左右比较稳。文档里最好明确写出来:“推荐使用Node.js 16.x版本,低于14或高于18可能导致依赖安装失败”。
5.2 生产环境部署,Nginx和进程守护不能少
生产环境我建议用Linux服务器,装Nginx、JDK、Maven、MySQL。后端打包成jar:
mvn clean package -DskipTests然后放到服务器目录下,用nohup java -jar mall.jar > mall.log 2>&1 &启动。但这样启动的进程没有守护,进程挂了不会自动重启,所以文档里最好推荐用systemd配置服务,或者至少用nohup并解释日志文件怎么看。例如:
tail -f mall.log这个命令要写进去,不然启动失败用户也不知道去哪看原因。
前端构建是npm run build,生成dist目录,把dist里的文件上传到服务器某个目录,然后配置Nginx。Nginx配置算部署文档里最容易写翻车的地方,核心要点是把/api开头的请求反代到后端端口:
server { listen 80; server_name your_server_ip; location / { root /home/www/dist; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }这里try_files那行必须要有,不然刷新首页或深链接路由会404,因为Vue是单页应用,所有路径都该指向index.html。我第一次部署就是漏了这行,一刷新全白屏,排查半天。
5.3 部署文档里要有问题排查小表格
写部署文档时,我习惯加一个“常见问题”小节,把最容易碰到的问题和解决手段列成表格。比如端口占用、数据库连接失败、前端代理超时。别觉得这是多余的,读者部署不上来就会跑去问开发者,你一次性写清楚,双方都省时间。
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 后端启动报Port 8080被占用 | 端口冲突 | 修改application.yml里的server.port或杀占用进程 |
| 连不上数据库 | 用户名密码不对 | 检查账号密码,用Navicat先测连接 |
| 前端登录后立即退出 | token请求头没传 | 检查axios拦截器里Authorization设置的拼写 |
| 刷新页面404 | Nginx未配置try_files | 在location /里加上try_files $uri $uri/ /index.html; |
这种表格在部署文档里特别有用,相当于给读者一个快速定位渠道。
6. 代码讲解怎么做才不算白讲,我的讲解结构与表达技巧
最后说说“代码讲解”这件事。很多拿着源码分享的人会从头到尾逐行读代码,听的人昏昏欲睡,讲的人也累。我自己的经验是,代码讲解要分三条线:主线讲业务逻辑,辅线讲设计意图,暗线讲避坑经验。
6.1 先画业务流程图,再讲代码
不管分享给谁,第一件事不是打开IDE,而是把系统的业务流程讲清楚。比如“用户从商品列表页点进详情页,加入购物车,进购物车页勾选商品下单,跳支付页支付成功,返回订单列表”这个过程,用文字加箭头描述出来。思维正常的人只要明白业务长什么样,再去看代码,每行代码都能对应上业务环节,理解成本直接减半。
6.2 每个模块挑3个关键代码点讲透彻
一个Controller几十行,没必要全讲。我通常每个模块只挑最核心的三处:一处是路由设计,讲URL和HTTP方法怎么对应操作;一处是参数校验逻辑,讲哪些参数不能为空,为什么不能为空;还有一处是数据返回格式,讲为什么返回Result而不是裸数据。比如讲订单模块时,重点讲@Transactional和状态枚举的配合,这是真正的业务痛点。而像@RestController这种注解,一句话带过就好,不用展开。
6.3 讲代码时多用“为什么”,少用“是什么”
我发现自己刚做分享时特别爱讲“这段代码做了什么”,但听的人关心的是“为什么这样做”。同样是购物车接口,讲“这里是判断用户没有登录就返回401”远不如讲“如果这里不判断登录状态,后端的库存扣减就会因为找不到用户ID而出错,所以必须提前拦截”。这种“因为所以”的结构能帮听众建立因果链条,而不是名词堆砌。
还有一个高效做法:刻意讲一个“错误写法”。比如说库存扣减,先展示最容易犯的“先查库存再判断能不能扣”的写法,然后告诉大家在并发情况下这个写法会超卖,再展示正确的“带条件更新的SQL”写法。这个对比下来,听的人印象会极其深刻。
6.4 讲代码前准备一份小抄
哪怕是博主或者讲师,讲代码前都该准备一份提纲。我的小抄很简单:每个模块下面列三个关键词。比如商品模块就是“分页、条件拼接、图片上传”,订单模块就是“事务、状态机、防超卖”。开讲前自己看一眼,就能保证全程不跑偏,也不会漏掉关键点。
7. 从开发到讲完,这套项目还留了哪些可扩展的接口
做完整套系统再回头看,你会发现它能扩展的点其实挺多的。比如商品模块可以加一个商品评论功能,订单模块可以加优惠券,用户模块可以加积分体系。这些扩展都不难,操作路径清晰:加表、加实体、加service逻辑、加controller接口、前端加页面和API方法。对于想拿这套项目做二次开发或者写论文的人来说,这种可扩展性相当重要。
还有一个我自己比较推荐的方向:给项目补充单元测试。Springboot的测试算好写,比如测订单service的库存扣减有没有生效,@SpringBootTest配上一个临时数据库就能跑。虽然商城项目做单元测试有点“重”,但面试时能主动提这个,很加分。
部署方面剩下的优化空间也有不少:前后端分离后可以改成Docker Compose一键启动,把MySQL、Redis、后端jar、前端nginx一起编排起来。这样一来部署文档可以进一步简化,读者只需要装Docker,跑一条命令。我们现在的项目还没有引入Redis,但商品热点列表其实很适合做缓存,加一个Redis模块会让项目的技术含量更强。
代码讲解的后续也可以做成视频加断点调试的录屏,比纯讲代码直观得多。至少在我自己带人做全栈项目时,效果对比非常明显——对着代码干讲,对方容易走神;开一个调试器,在行号位置打断点,看到购物车数据一步步加进去,比任何解释都有效。
8. 最后说几个折腾这个项目时,我被坑得最惨的地方
如果非要把这套项目从开发到部署再到讲完,挑几个最痛的教训,我想说是下面几个。
第一个坑是后端跨域。开发环境用了Vite的代理没感觉,但生产环境Nginx配置里忘了写proxy_set_header Host $host;,结果后端拿到的用户IP全变成Nginx地址,登录记录和日志排查直接乱掉。后来我养成一个习惯:前端所有请求都用/api前缀,后端接口统一放在/api下,Nginx只需一条location /api/就全搞定。
第二个坑是前端打包后路由历史模式的问题。Vue默认使用createWebHistory,如果Nginx没有try_files指令,刷新路由页面就会404。当时我以为代码写错了,折腾好久才发现是nginx配置问题。后来我干脆写成createWebHashHistory,虽然URL多一个#,但对练手部署省心很多。如果要用history模式,就在部署文档里用醒目字标出Nginx配置要求。
第三个坑是数据库删表顺序。项目里表有外键关联,如果手滑删某张表先删了父表,后面会报外键约束错误。后来我准备了一个完整的mall.sql,重新执行时直接全库重建,省掉了这种低级烦恼。
第四个坑是讲代码时,只顾着讲后端或前端,忽略了联调过程。后来我发现,听的人最感兴趣的反而是“前端怎么调后端、后端怎么返回给前端、遇到接口报错怎么查”。这个联调链路才是全栈项目的灵魂,所以我通常会把每个功能拆成“前端动作”、“后端接口”、“数据库变化”三栏来展示,比如加入购物车这个操作,对应前端调用POST /api/cart/add,后端往cart表插入一条记录,前端Vuex的cartCount加一。这样的讲解方式比单讲任何一部分都有用。
整套项目做下来,从源码到部署文档,再到给人讲明白每一块逻辑,我最大的体会是:技术本身没有多高深,但把它组织成一个别人能看懂、能复现、能扩展的形态,这本身就是一门手艺。如果你正打算对着这套Springboot+Vue电脑商城系统下手,我的建议是:先把商品浏览、购物车、下单支付这条主链路跑通,再补管理端和优化项,最后写部署文档和代码讲解稿。按这个顺序来,你得到的不仅是一个系统,更是一套完整的全栈表达框架。