SpringBoot+Vue猫咪商城管理系统:毕业设计全栈项目实战

发布时间:2026/10/6 19:28:05
SpringBoot+Vue猫咪商城管理系统:毕业设计全栈项目实战 如果你正在准备毕业设计或者想让简历上多一个能打的完整项目那“商城系统”这四个字大概率绕不开。而这次我整理的这套基于SpringBootVue的猫咪商城管理系统算是把这个常见选题做得稍微有点辨识度的一套工程化代码。整套项目不只有源码还配套了完整的设计文档、答辩PPT和数据库脚本。从后端接口设计、前端页面开发到Vue打包放进SpringBoot一体化部署再到订单并发扣减这种容易被追问的细节我都在这篇博文里把关键思路和实际踩过的坑写清楚。适合对象很明确正在做Web方向毕业设计的在校生以及打算用一套全栈项目充实简历的初、中级开发。1. 选题逻辑与技术选型这是一套工程化的猫咪商城1.1 为什么毕业设计或简历项目要选商城商城系统的经典程度是别的选题比不了的。从用户注册登录、商品展示、购物车到订单创建、支付模拟、后台管理几乎覆盖了一个Java后端开发者日常会碰到的所有技术面增删改查、状态机、事务、并发、文件上传、权限控制。更关键的是商城的业务链路足够长长链路才有东西可写。文档里可以画流程图答辩时可以讲状态流转简历上可以写“独立完成从需求分析到部署上线的完整流程”。相比之下“图书管理系统”这类选题业务太薄一个普通CRUD就没了问到并发和事务时很难往深处聊。我不太建议选“通用商城”这种名字太容易和网上下载的模板撞车。这套项目叫“猫咪商城”主题本身就是一个记忆点。商品分类围绕宠物主食、零食、猫砂、玩具做设计页面文案、分类命名都贴合真实养猫场景答辩时评委扫一眼截图就能记住这个项目而不是听完就忘的“某某商城”。1.2 技术栈版本与选型理由版本选择这种事看着简单其实最容易踩坑。尤其SpringBoot 3.x发布之后很多人直接跟着网上新教程用3.x结果本地JDK还是8编译直接失败。下面是我在这套项目里锁定的版本组合技术组件版本选型理由Spring Boot2.7.18兼容JDK 8生态资料最全稳定MyBatis-Plus3.5.3单表CRUD零SQL分页插件好用MySQL8.08.0窗口函数和JSON能力好但核心用的是基础功能Redis6.x存放验证码、实现分布式登录态、库存预扣JWTjava-jwt 4.4无状态登录拦截器校验避免Session共享问题Vue2.7.16最后一个2.x大版本支持组合式APIElement UI高度适配Element UI2.15.14管理端表单、表格、弹窗开箱即用Axios0.27.2请求拦截器处理token响应拦截器统一错误处理ECharts5.4.3后台统计页面画销售趋势、分类占比图选SpringBoot 2.7而不是3.x是最重要的一次决策。3.x强制JDK17而很多毕设环境还是JDK8即便你自己装了JDK17指导老师电脑上跑不起来也很麻烦。2.7.18是SpringBoot 2.x最后的维护版本安全更新到2023年底对于教学和演示场景完全够用。Vue这块我选了Vue2.7而不是Vue3。原因很现实大部分现有商城模板、Element UI组件、答疑帖子都基于Vue2遇到问题找资料快。Vue2.7虽然还是Options API风格但已经支持script setup写起来并没有落后太多。1.3 数据库核心表设计解决“表怎么建”的问题数据库设计决定了这个项目能聊多深。我给这套系统规划了十来张核心表挑几张重点说说设计思路。user用户表除了常规的username、password、phone加了一个role字段区分普通用户和管理员权限控制先走角色不做细粒度权限。category分类表一级分类结构比如“猫咪主粮”“零食罐头”“猫砂用品”“玩具周边”用parent_id支持后续无限级扩展。goods商品表SPU层存商品标题、主图、详情富文本、销量、上下架状态。价格和库存不放这里因为同一个商品可能有多个规格。goods_sku规格表SKU层一个商品对应多个SKU例如“鸡肉味2kg”“鱼肉味2kg”“鸡肉味5kg”每个SKU单独记录price、stock、spec。cart购物车表字段包括user_id、goods_id、sku_id、quantity。主键用自增id同时给user_id建索引。orders订单表核心字段包括order_sn订单号、user_id、total_amount、freight、pay_type、status、address_snapshot。order_item订单明细表一个订单对应多个商品快照快照里保存了当时的价格、商品名、规格避免后续商品改名影响历史订单。address收货地址表默认地址用is_default字段标识每次只有一个默认地址。有几个细节值得说明。订单表里的address_snapshot我存的是下单时地址的JSON字符串而不是外键关联地址表。因为用户改地址不应该影响历史订单的配送信息快照是订单系统里很常用的做法。order_sn我生成规则是“yyyyMMddHHmmss 用户ID 四位随机数”保证唯一性的同时方便肉眼排查问题。商品表和SKU表分离这件事很多人容易忽略。如果只用一个商品表把“价格”“库存”塞进去遇到多规格商品就会非常痛苦改一个规格的价格要更新整个商品购物车也没法精确记录用户选的是哪个规格。拆出SKU表之后商品详情页的规格选择、购物车快照、订单明细都有据可循。2. SpringBoot后端用户认证、商品查询与订单状态机2.1 工程分层与统一返回结构后端工程我按照标准的分层结构组织controller、service、mapper、entity、dto、config、common。controller层只做参数接收和结果包装不写业务逻辑service层处理具体业务mapper层用MyBatis-Plus的BaseMapper单表操作基本不用写SQL。一个容易被忽略但很影响代码质量的点是统一返回结构。我定义了一个ResultT类所有接口返回格式固定为{code, message, data}。前端axios响应拦截器只需要判断code是否等于200不用每个接口单独写一套错误处理。public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ... } public static T ResultT error(String message) { ... } }配合全局异常处理器RestControllerAdvice业务异常、参数校验异常、兜底异常都能统一转成Result格式。这样做的好处是前端联调时减少80%的“这个接口返回格式怎么不一样”类问题后端的错误堆栈也不会直接暴露给用户。2.2 用户登录注册与JWT刷新方案登录认证这块我抛弃了传统的Session方案改用JWT。核心原因有两个前后端分离架构下Session跨域处理麻烦商城后台需要区分用户角色JWT可以把用户ID和角色写进token里后端解析一次就能拿到全部身份信息。密码存储用BCrypt加密代码里只需要一行String encodedPassword BCrypt.hashpw(rawPassword, BCrypt.gensalt());注册时把加密后的密码写入数据库登录时用BCrypt.checkpw(rawPassword, encodedPassword)校验。BCrypt是自带盐的哈希算法同一密码两次加密结果不同安全性比MD5加固定盐高一个数量级。面试或者答辩时如果被问到“密码明文存储会有哪些风险”这是一个可以直接答上来的加分项。JWT这块我用的java-jwt库生成token时把userId和role放进payloadString token JWT.create() .withClaim(userId, user.getId()) .withClaim(role, user.getRole()) .withExpiresAt(new Date(System.currentTimeMillis() 2 * 60 * 60 * 1000)) .sign(Algorithm.HMAC256(secretKey));后端写一个拦截器统一校验请求头里的token校验通过后把userId放到ThreadLocal里供后续Service使用。这里有两个实战细节一是放行登录、注册、商品列表、商品详情这些公开接口其余接口全部需要token二是过期时间我设了2小时前端在axios响应拦截器里发现401就直接跳登录页并清除本地登录态。2.3 购物车合并思路与商品列表接口购物车有经典的两种实现路线纯前端本地存储或者后端接口同步。我在这套系统里做了一个折中方案未登录时购物车数据存到浏览器LocalStorage登录后点击购物车时前端先把本地购物车数据提交到后端由后端合并到数据库购物车表并清空本地数据。合并逻辑按SKU维度进行前端传来的本地购物车数据逐条检查数据库里是否已有相同user_id加sku_id的记录有就累加quantity没有就插入新记录。这个方案兼顾了未登录用户的购物体验和登录后的数据持久化毕设答辩时还能作为“本地缓存与服务端数据同步”的案例来讲。商品列表接口我做了三个条件分类ID、关键字模糊搜索、价格排序。直接用MyBatis-Plus的LambdaQueryWrapper拼条件LambdaQueryWrapperGoods wrapper new LambdaQueryWrapper(); if (categoryId ! null) { wrapper.eq(Goods::getCategoryId, categoryId); } if (keyword ! null !keyword.isEmpty()) { wrapper.like(Goods::getTitle, keyword); } wrapper.orderByDesc(Goods::getSales);分页用MyBatis-Plus内置的分页插件前端传current和size后端返回total和当前页数据。这样列表页可以直接交给Element UI的分页组件渲染。2.4 订单创建的事务边界与超时关单订单创建是整套系统里业务逻辑最密集的一段涉及的步骤包括校验收货地址、组装订单明细、计算总价、扣减SKU库存、生成订单号和明细记录、清空对应购物车。整个过程必须在一个事务里任何一步失败都不能留下半截订单和已经被扣掉的库存。我在订单Service方法上加Transactional注解同时把操作顺序设计为先扣库存再插入订单。扣库存使用了一条带条件的更新语句int rows skuMapper.deductStock(skuId, quantity); if (rows 0) { throw new BizException(库存不足); }对应的SQL核心逻辑是UPDATE goods_sku SET stock stock - #{quantity}, version version 1 WHERE id #{skuId} AND stock #{quantity}stock #{quantity}是防止超卖的第一道防线。SpringBoot中的Transactional默认遇到RuntimeException会回滚所以只要扣库存失败抛了异常前面插入的数据会一起回滚不会出现订单失败但库存少了的情况。订单状态我用整型字段表示比字符串更省空间也更方便比较0待付款、1已付款、2已发货、3已完成、4已取消、5已关闭。用户下单后状态是0点击“模拟支付”后变成1这个模拟支付按钮是我刻意为之的——毕业设计场景对接真实微信/支付宝支付需要商户号门槛高且审核麻烦用模拟支付把状态流转跑通核心流程不受影响真要上线时只需要替换支付回调这一个接口。超时未支付订单的关闭我写了一个Spring定时任务每5分钟扫描一次创建超过30分钟且状态为0的订单批量改为已关闭同时把扣掉的库存加回去。这个“释放库存”的逻辑很关键不然超时订单会一直占着库存。3. Vue前端路由守卫、组件插槽与商城页面落地方案3.1 Vue2.7与Element UI的生态选择前端这层的选型我前面提过Vue2.7加Element UI。需要多说一句的是Element UI虽然官方已经不再更新但它的表单组件、表格组件、分页组件、弹窗组件在管理端场景里依然非常好用尤其是数据表格配合自定义列模板写后台商品管理页面比手撸原生HTML高效十倍。整套前端分成两个大界面商城用户端和管理员后台。用户端包含首页、商品列表、商品详情、购物车、结算页、我的订单、登录注册管理员后台包含商品管理、分类管理、订单管理、用户管理、数据统计。两部分我放在同一个Vue工程里用路由前缀区分/mall开头的是用户端页面/admin开头的是管理端页面。3.2 页面路由设计静态路由加动态权限路由路由这块用了Vue Router默认使用history模式。项目里的路由分为两部分静态路由是所有登录用户都能访问的比如首页、商品列表、登录注册动态路由是管理员专属的比如后台的商品管理、订单管理、数据统计页面。// 静态路由 const routes [ { path: /login, component: Login }, { path: /, component: Home }, { path: /goods, component: GoodsList }, { path: /goods/:id, component: GoodsDetail } ] // 动态路由登录后根据用户角色追加 if (role admin) { router.addRoute({ path: /admin, component: AdminLayout, children: [...] }) }登录成功后把用户信息存进Vuex页面路由守卫beforeEach里判断当前路由是否需要登录需要登录但本地没有token时统一跳转登录页并带上redirect参数。商品详情页的路由传参我用了/goods/:id这种路径参数方式而不是?idxxx的query方式。路径参数更语义化刷新页面不会丢失订单页和结算页之间跳转则用query方式方便携带多个参数。两种方式的使用场景我已经在实际项目里分得很清楚单一ID用params多个查询条件用query。3.3 商品卡片组件复用与slot插槽商城用户端有一个高频复用组件商品卡片。首页的猜你喜欢、商品列表页、搜索结果的每一件商品都用同一套卡片来渲染。我把它抽成组件后只暴露三个props商品对象、是否显示标签、是否显示销量。组件内部用Element UI的卡片组件商品主图用懒加载指令价格高亮显示。为了满足不同页面的个性化展示需求我在卡片底部预留了一个插槽slot。首页会在插槽里显示一个“新品”角标列表页会在插槽里显示促销倒计时组件本身不关心插槽内容是什么只需要替外部预留好位置。template el-card img :srcgoods.mainImage / div classprice{{ goods.price }}/div div classtitle{{ goods.title }}/div slot nametag/slot /el-card /template这种组件拆分带来的收益在开发后期非常明显需要调整商品展示样式时只改一个地方全站所有商品卡片同步生效。答辩时如果被问到“组件化开发怎么理解”这是一个可以直接指向的代码实例。3.4 购物车与结算页的前端状态管理购物车页面是前端状态最复杂的页面多选、全选、数量加减、删除、合计金额这些状态全部联动。我用Vuex管理购物车状态state里存一个购物车商品数组mutations负责加减数量、切换选中getters负责计算选中的商品数量和总金额。结算页会做三步处理一是从购物车读取选中的商品二是请求后端地址列表让用户选择或新增收货地址三是点击“提交订单”后调后端接口创建订单。整个流程中后端只信任自己数据库里的数据前端购物车里的金额只做展示不参与最终价格计算后端会根据SKU表里的真实价格重新计算一次总价。4. 前后端联调阶段跨域、上传与鉴权三件坎4.1 开发环境代理与后端CORS配置如何取舍前后端分离项目联调时第一个遇到的就是跨域。前端跑在http://localhost:8081后端跑在http://localhost:8080浏览器直接发请求必然被CORS拦截。我给了两套方案开发环境用Vue CLI的devServer.proxy配置代理前端请求/api前缀自动转发到http://localhost:8080浏览器看到的是同源请求不存在跨域。生产环境前端被打包进后端本身就是同端口访问没有任何跨域问题。后端我也加了一个CorsFilter配置作为兜底但优先级最低。开发时优先走前端代理这样不用每次改动都去后端加跨域配置。有的同学习惯直接在后端CrossOrigin注解解决跨域也能跑通但生产环境如果域名做了变化这些硬编码的跨域配置反而会成为隐患。4.2 图片上传的本地落盘与静态映射商品图片上传我采用了最简单也最适合毕设的本地存储方案。后端接收MultipartFile按日期生成目录文件名用UUID避免重复保存到项目根目录的uploads目录数据库存相对路径如/uploads/2024/06/xxx.jpg。关键在后端静态映射。SpringBoot默认只映射classpath:/static/目录我要让/uploads/**也能被直接访问于是写了一个配置类Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/uploads/**) .addResourceHandler(file: uploadDirPath); } }如果省略这段配置图片上传后前端通过http://localhost:8080/uploads/xxx.jpg访问会直接404这是本地图片方案最常见的坑。另外要注意SpringBoot的multipart.max-file-size默认只有1MB我在配置文件里调到了10MB不然上传商品主图会报“文件大小超出限制”。4.3 401响应拦截与登录态统一处理前端axios封装里请求拦截器统一从localStorage取token放进请求头的Authorization字段。响应拦截器统一处理后端返回的code这里有一个很实用的细节当后端返回401表示token过期时前端需要先清掉本地存储的登录信息再跳转登录页同时用一个全局变量防止多个接口同时报401导致重复弹跳。service.interceptors.response.use( (response) { const res response.data if (res.code ! 200) { Message.error(res.message || 请求出错) return Promise.reject(new Error(res.message)) } return res }, (error) { if (error.response.status 401) { localStorage.clear() router.push(/login) } return Promise.reject(error) } )这里要注意401跳转时不能简单window.location.href /login否则会丢失当前路由地址没法在登录后跳回原页面。我用router.push({path:/login, query:{redirect: router.currentRoute.fullPath}})保存来源地址。4.4 联调排错的经验套路联调阶段遇到问题我习惯按固定顺序排查打开浏览器Network面板看这个请求到底发出去了没有接口路径和后端Controller里的RequestMapping值是否完全一致。看请求状态码404一般是路径不对500一般是后端代码报错401是token缺失或过期405一般是GET/POST方法对不上。看后端控制台日志SpringBoot默认会打印异常堆栈先找到第一行java.lang.Exception再看具体在哪个Service方法抛出的。如果是参数为null的问题大概率是前端字段名和后端实体字段名对不上比如前端传userName后端实体类字段是username。这套排查套路帮我在实际开发中省了很多时间。遇到问题先不要急着改代码先把“到底是哪一层的问题”确认清楚再动手。5. Vue打包进SpringBoot的集成部署与404排查5.1 为什么建议做成单端口一体化部署Vue工程开发完有两种部署路线一是单独部署Nginx反向代理后端接口二是把Vue构建产物放进SpringBoot的static目录打成一个大Jar包直接运行。我在这套项目里选了第二种。原因有两方面从使用场景看毕设答辩要在一台电脑上快速启动演示装Nginx、改配置、代理后端对临时环境来说是徒增负担从部署方式看一个java -jar命令解决问题演示前只需要确认Java环境存在风险点最少。两种方案的对比我在文档里也写了一段对比项单端口一体化部署Nginx分离部署启动复杂度一条java -jar命令需要装Nginx并维护两套进程跨域问题不存在需要Nginx反向代理配置前端更新重新打包替换Static目录直接替换静态文件目录适合场景教学演示、内网部署公网正式环境5.2 前端构建到后端静态目录的操作流程实际操作流程分四步第一步前端工程修改接口地址。开发时请求的是http://localhost:8080/api打包后前端页面和后端接口同源我统一改成相对路径/api这样无论部署到8080还是80端口都能自适应。第二步执行构建命令npm run build构建完成后在dist目录生成index.html、css、js、static等文件。第三步把dist目录里的全部内容复制到SpringBoot的src/main/resources/static目录下。这里有个小注意点复制前先清空static目录里的旧文件否则旧资源残留可能导致缓存错乱。第四步后端执行打包mvn clean package -DskipTests拿到target目录下的jar包后java -jar直接运行浏览器访问http://localhost:8080/就能看到首页。5.3 刷新页面404的完整排查链路这个坑我印象太深了。首次部署完成后从首页点进商品详情页一切正常但只要在商品详情页按F5刷新页面就变成一个白屏加404错误。当时的第一反应是前端路由问题但本地开发环境刷新明明没这个问题。完整的排查链路是这样的先打开浏览器Network面板刷新时发现请求的URL是http://localhost:8080/goods/1响应状态404响应体是SpringBoot默认的Whitelabel Error Page。这说明浏览器根本没有拿到前端的index.html而是把/goods/1当成后端接口请求了。再想一下本地开发为什么正常Vue CLI的devServer对history模式做了兜底所有没有匹配到静态资源的路径都会自动返回index.html所以本地刷新没问题。但SpringBoot默认的静态资源映射没有这个逻辑找不到/goods/1这个静态资源就直接404。根因找到了解决方案是在后端加一个视图控制所有非/api开头且不是静态资源的路径都转发到index.html由前端路由接管。我写了一个配置类Configuration public class SpaForwardConfig implements WebMvcConfigurer { Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController(/{path:[^\\.]*}) .setViewName(forward:/index.html); } }这段配置的意思是路径中不包含点号的请求全部转发到index.html因为静态资源一般都会带.js、.css、.png后缀带后缀的请求走正常静态映射不带后缀的交给前端路由。这个转发配置是SpringBoot集成Vue history路由的必备方案。如果看完能理解“为什么本地启动没事、部署到后端就404”的原因以后再遇到同类问题基本上可以一眼定位。5.4 启动脚本与常见运行期问题我习惯在项目根目录放一个启动说明文档里面写清楚三个常见问题的解法端口被占用java -jar启动时提示Port 8080 was already in use除非换端口否则先找到占用进程。Windows下用netstat -ano | findstr 8080查看PID再去任务管理器结束进程。数据库连接失败SpringBoot启动报Communications link failure大概率是MySQL没启动或者数据库名、账号密码和application.yml里不一致。图片路径丢失jar包部署后上传图片保存到的是jar包同级的目录如果后续在别的机器上重新部署需要把uploads目录一起拷贝否则历史图片全部失效。6. 并发扣减、订单幂等与答辩材料整理6.1 库存扣减从超卖事故到乐观锁方案商城项目最容易被深挖的就是并发场景。我当时在做压测时故意模拟了50个用户同时下单同一件库存只有10件的商品结果数据库里出现了库存负数的订单。复现过程是这样的两个请求同时读到库存10都判断库存充足一起执行update stock stock - 1最后库存变成8但生成了两条订单。虽然最终数据不精确但足以证明“先查库存再扣减”的逻辑在并发下会超卖。我采用的方案是把判断库存和扣减库存合并成一条SQLUPDATE goods_sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity}数据库的行锁和条件判断保证了并发安全同一时刻只有一个事务能更新这个SKU行stock #{quantity}条件又保证了库存不足时不执行扣减。如果更新影响行数为0说明库存不足订单事务直接回滚。在此基础上我又加了一列version做乐观锁更新时带上version #{oldVersion}防止ABA问题。对于毕设场景这个方案已经完全够用。真要在秒杀场景支撑更大并发再去考虑Redis预扣库存、异步队列等更重的方案我也在文档里写了这部分扩展思路。6.2 防重复提交前端置灰与后端幂等除了超卖另一个容易被刁难的问题是重复下单。用户快速点击“提交订单”两次系统如果生成了两条一模一样的订单就是典型的非幂等。我的处理分两层前端提交按钮在点击后立即置灰并显示loading直到后端返回结果才恢复。这一层挡掉大多数误触。后端生成订单号时用order_sn做唯一索引插入订单前先查询是否存在相同order_sn插入时依靠数据库唯一索引兜底。真正执行创建订单的接口里判断当前用户是否在60秒内有相同金额的进行中订单有就直接返回已存在订单。底层思路很好理解防止用户产生重复数据不能只靠前端后端也要有拦截能力。数据库唯一约束是最后一道保底防线三层叠加才能保证“无论请求怎么进来订单都只有一条”。6.3 文档、PPT、源码三件套的整理技巧很多人把精力全放在写代码上最后文档和PPT草草了事结果答辩被问得手忙脚乱。这套项目我配套整理了一份设计文档文档结构调整为需求分析、系统架构设计、数据库设计、接口设计、核心业务流程、测试报告、总结与展望。其中“数据库设计”和“接口设计”这两章是评委最爱翻的部分。数据库设计里除了表结构字段说明我还画了实体关系说明重点标注外键关系和索引设计思路。接口设计表列了URL、请求方式、请求参数、返回结果整理成接口文档后前端联调和评委阅读体验都好很多。PPT控制在14页左右整体逻辑是选题背景、需求分析、技术选型、系统架构图、功能模块图、数据库设计、核心流程、页面截图、亮点说明、总结。页面截图要挑最有代表性的首页、商品详情、购物车结算、后台商品管理、数据统计这五张每张图配两三句说明讲清楚页面的功能和实现方式。源码交付这块我做了一个非常重要的整理动作清点完整工程目录确认Vue前端和SpringBoot后端代码齐全数据库脚本db_cat_mall.sql单独放在根目录README.md写清楚环境要求、启动步骤、默认账号密码。很多人的源码交上去跑不起来问题往往不是代码不对而是缺少数据库脚本或者默认端口冲突。我在README里写了默认管理员账号admin/admin123测试用户user/user123演示时直接登录省去现场注册的时间。写在最后整套猫咪商城从最初搭建骨架到文档PPT整理完毕最耗时的地方其实不在某个具体页面的实现而在于把每个功能背后的“为什么”想清楚。比如订单状态为什么这样流转、库存扣除为什么用条件更新、Vue为什么必须加路由兜底配置这些问题如果只是照抄代码是看不出来的但一旦在答辩或简历中被问起能不能讲清楚才是项目有没有真正变成自己的关键分界线。如果要给正在做同类项目的朋友一个建议我会说优先把代码跑通但一定不要停在跑通这一步。数据库表之间的关系、订单事务的边界、前端路由守卫的执行时机这些“看不见的部分”反而决定了项目的含金量。源码、文档、PPT只是交付物你脑子里的那套完整技术推理才是这套项目带给你最核心的东西。