ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

SpringBoot+Vue宠物商城系统设计与实现全解析

2026/10/6 9:54:09 拓冰建站 浏览量
SpringBoot+Vue宠物商城系统设计与实现全解析 做毕业设计或者给简历攒项目的时候很多人第一眼看到基于SpringBootVue的宠物商城网站管理系统这种题目会觉得就是一套普通的商城CRUD随便写写就能交差。但实际上我帮人改过不少类似的代码从商品库存、购物车逻辑、订单状态流转到前端路由守卫、接口鉴权、打包部署每一环都能把新手绊一跤。这篇内容我拿一套我实际整理过、能跑通的JavaMySQLMyBatis完整源码来讲把项目怎么设计、表怎么建、接口怎么写、Vue怎么对接、部署的时候踩了哪些坑全部摊开说清楚。适合正在做毕设、准备面试项目或者想快速搭一套前后端分离web系统的人参考照着这个思路走能少走很多弯路。1. 项目整体设计与技术选型思路1.1 为什么锁定SpringBootVue这套组合先说选型。宠物商城本质是标准的电商系统核心域就三个用户、商品、订单。做这种系统技术选型的首要标准不是炫技而是稳和生态好。SpringBoot作为后端框架最大的优势是自动配置和起步依赖不用像SSH时代那样写一堆XML配置Maven拉依赖、写个启动类就能把Web环境跑起来这对快速开发、毕业设计、面试演示都非常友好。而Vue作为前端前后端分离以后后端只需要出一套RESTful API前端负责页面渲染和交互两边职责清楚后期加功能也方便。有人可能会问用JSP加Thymeleaf不也能做吗能做但维护性差一大截。服务端渲染模式下前端改个样式还得动后端工程重新编译部署换成前后端分离以后前端改页面完全不影响后端Vue打包出来的静态文件还能直接塞进SpringBoot的static目录部署方式很灵活。这也是目前市面上多数中小型管理系统的主流形态用这套组合做出来的项目写在简历上说服力也更强。1.2 整体架构与数据流怎么设计这套系统的架构我建议按经典的前后端分离来组织。浏览器请求先到达部署静态资源的位置凡是/api开头的请求打到后端SpringBoot控制器。控制器层只做参数接收和结果封装业务逻辑放到Service层数据访问由MyBatis的Mapper层完成。整个请求链路就是Vue页面 - axios - Controller - Service - Mapper - MySQL返回结果再原路回来统一封装成包含code、message、data的JSON结构。工程结构上后端我习惯分成config、controller、service、mapper、entity、common几个包。config放拦截器、跨域配置、Knife4j接口文档配置controller负责对外接口service放业务代码先写接口再写实现类mapper是MyBatis的接口和XML映射文件entity对应数据库表common放统一的返回结果类、全局异常处理器和工具类。前端则是api、router、views、components、utils几个目录。api目录集中管理所有接口请求router配置路由和权限守卫views放页面components放复用组件utils放axios封装和工具方法。这个结构不复杂但足够支撑一个商城系统正常迭代而且面试官问起来你也答得上每个目录存在的理由。1.3 环境准备与开发工具清单再补一句环境准备。JDK我用的是1.8因为SpringBoot 2.x在JDK8下最稳定网上资料也最多别一上来就装JDK17配SpringBoot 3.x虽然能用但很多老教程里的配置写法都对不上排查起来非常浪费时间。数据库我建议用MySQL 5.7或者8.0都行注意8.0的驱动类名和时区配置跟5.7不一样这个后面章节专门讲。IDE方面后端用IDEA前端用VS Code或者IDEA都行Maven用3.6以上版本Node环境14以上Vue CLI版本4或者5。先把这套环境统一好能省掉后面八成莫名其妙的报错。2. 数据库设计与核心表结构解析2.1 三大核心域拆解用户、商品、订单做数据库设计我一向建议先把业务画成几张实体关系图再动手建表不然建到后面字段越加越乱。宠物商城里用户表要存登录账号、密码加密后的密文、昵称、手机号、头像、角色标识角色区分前台买家和后台管理员。宠物商品表和普通商品不一样的地方在于要额外关注品种、年龄、疫苗状态这类字段但它同时依然要有分类ID、价格、库存、主图、详情描述、上下架状态。分类表单独拆一张方便前台按猫狗、水族、小宠等维度筛选也方便后台维护导航菜单。订单这块最容易出问题。表至少要拆成两张订单主表和订单明细表。主表存订单号、用户ID、总金额、收货地址快照、状态、创建时间明细表存商品ID、商品名称快照、单价、数量、小计。为什么要存商品名称快照因为商品改了名或者下架历史订单里的显示内容不能被牵连这是电商系统里很基本的经验。很多新手直接去关联商品表商品一删订单详情页就报错或者显示空这就是没做快照的后果。2.2 关键建表语句与索引设计我用一段精简的建表SQL说明核心设计实际项目里再加一些审计字段就好create table pet_category ( id bigint primary key auto_increment, name varchar(64) not null, parent_id bigint default 0, sort int default 0 ); create table pet_product ( id bigint primary key auto_increment, category_id bigint not null, name varchar(128) not null, breed varchar(64), age varchar(32), price decimal(10,2) not null, stock int not null default 0, main_image varchar(255), detail text, status tinyint default 1 comment 1上架 0下架, create_time datetime default current_timestamp, index idx_category (category_id), index idx_status (status) ); create table pet_order ( id bigint primary key auto_increment, order_no varchar(32) not null unique, user_id bigint not null, total_amount decimal(10,2) not null, receiver_name varchar(64), receiver_phone varchar(20), receiver_address varchar(255), status tinyint not null comment 0待付款 1待发货 2待收货 3已完成 4已取消, create_time datetime default current_timestamp, index idx_user (user_id), index idx_status (status) ); create table pet_order_item ( id bigint primary key auto_increment, order_id bigint not null, product_id bigint not null, product_name varchar(128) not null, product_image varchar(255), price decimal(10,2) not null, quantity int not null, subtotal decimal(10,2) not null, index idx_order (order_id) ); create table pet_cart ( id bigint primary key auto_increment, user_id bigint not null, product_id bigint not null, quantity int not null default 1, checked tinyint default 1, unique key uk_user_product (user_id, product_id) );索引设计这里有个原则查询频繁的字段建索引写多读少的表别乱加。订单表和明细表都以订单ID作为索引是为了订单详情页能快速查出明细列表用户表在手机号上建唯一索引是为了登录时快速定位。购物车表用(user_id, product_id)做联合唯一键这样同一个用户往购物车加同一件商品时可以直接用insert ... on duplicate key update把数量累加不用先查一遍再决定新增还是更新少一次数据库往返。2.3 MyBatis映射与查询优化的几个关键点MyBatis这块很多人一上来就写注解SQL小项目可以但商城系统查询条件多商品列表要按分类筛选、按价格排序、按模糊名称搜索动态SQL的需求非常典型建议还是用XML文件方式。我就用了一个比较经典的写法来实现商品列表查询select idselectProductList resultTypecom.example.entity.PetProduct select * from pet_product where if testcategoryId ! null and category_id #{categoryId} /if if testkeyword ! null and keyword ! and name like concat(%, #{keyword}, %) /if if teststatus ! null and status #{status} /if /where order by ${sortField} ${sortOrder} limit #{offset}, #{pageSize} /select这里解释两个细节。第一模糊搜索用concat(%, #{keyword}, %)而不是直接在Java代码里拼好%再传进来是为了避免SQL注入虽然MyBatis的#{}本身会预编译但把百分号拼接在参数里容易出乱码和空格问题统一在SQL里拼接更干净。第二排序字段用了${sortField}而不是#{sortField}因为ORDER BY后面的列名不能被预编译占位符替换这是MyBatis的语法限制既然用了${}就一定要在Java层做白名单校验比如只允许传入price或create_time防止注入风险。关联查询方面订单详情需要查出订单明细列表我习惯在实体里加一个List 字段然后在XML里用collection标签一次性查出而不是在Service层循环查数据库。但要注意大结果集下嵌套查询容易触发N1问题所以在列表页不要带明细只有进详情页才查明细这是典型的按需加载思路。3. 后端核心功能实现与踩坑记录3.1 统一返回结构与全局异常处理前后端分离的项目最忌讳的就是每个接口返回格式都不一样前端解析的时候得写一堆if。所以我第一步就定义一个Result类public class ResultT { private Integer code; private String message; private T data; public static T ResultT success(T data) { ResultT r new Result(); r.code 200; r.message success; r.data data; return r; } public static T ResultT error(Integer code, String message) { ResultT r new Result(); r.code code; r.message message; return r; } }配合一个全局异常处理器用RestControllerAdvice拦截业务异常和系统异常统一转成上面这个结构。这样做的好处有几个一是前端axios拦截器里只需要判断code是不是200不是就统一弹错误提示二是Service层可以大胆抛业务异常比如库存不足、商品已下架不用每个接口都写try-catch三是日志里能看到真实的异常堆栈返给用户的却是友好的提示信息。这个习惯我建议所有SpringBoot项目都保持省事而且规范。3.2 商品分页查询与分页参数计算商品列表页、后台管理列表都需要分页。分页有两种做法引入PageHelper插件或者自己算offset。PageHelper用起来很简单但有个众所周知的坑它依赖ThreadLocal如果你写了多数据源或者某些特殊查询分页可能会串到下一个查询上。我在这套项目里选择自己算分页逻辑其实就三行int pageNum Math.max(param.getPageNum(), 1); int pageSize Math.min(param.getPageSize(), 50); int offset (pageNum - 1) * pageSize;pageSize限制最大50是为了防止有人传个10000把数据库压垮。然后把offset和pageSize传给Mapper同时用select count(*)查出总数返回给前端total字段前端就能算出总页数。这里有个性能点count查询不要带order by也不需要查明细字段MyBatis里单独写一个count语句别复用列表SQL。数据量过万之后这个优化非常明显。3.3 购物车合并与订单提交的事务控制购物车最简单的实现就是建一张cart表加购、改数量、勾选、删除都是对这张表的操作。真正容易出问题的是从购物车生成订单的接口。这个接口涉及多步操作查询购物车选中项 - 校验商品状态和库存 - 计算总金额 - 插入订单主表 - 插入订单明细 - 扣减库存 - 清空购物车。任何一步失败前面已经写入的数据都得回滚否则就会出现订单生成了但库存没扣、或者扣了库存但订单没生成的对不上账问题。所以订单创建方法必须加上Transactional注解并且要理解它的几个关键配置。默认情况下事务只在抛出RuntimeException时回滚如果你在代码里catch掉了异常事务就不会回滚这是新手最容易踩的坑。正确做法是Service层不要随意catch让异常抛出去交给全局处理器如果确实需要捕获捕获后要手动TransactionAspectSupport.currentTransactionStatus().setRollbackOnly()。另外库存扣减要在插入订单之前完成用带条件的update语句来保证不超卖int rows productMapper.reduceStock(productId, quantity); if (rows 0) { throw new BusinessException(库存不足); }这种写法有个专有名字叫乐观锁思想update时会带上stock quantity这个隐含条件数据库行锁保证同一件商品不会被两个并发请求同时扣超返回影响行数为0就说明库存不够直接回滚整个订单事务。实测下来这个方案比先select再update要可靠得多。3.4 登录鉴权与拦截器的实现方案登录模块我选择用JWT原因很简单前后端分离后Session方案要处理跨域携带Cookie的问题比较麻烦而JWT是无状态的后端只需要在登录成功时签发一个token前端存起来之后每次请求放在请求头里带过来后端拦截器解出来就知道是谁了。JWT本身包含三部分header、payload、signaturepayload里我放userId和role设置过期时间2小时注意不要在payload里放敏感信息因为它只是Base64编码不是加密。拦截器配置是这套系统里比较绕的一环。我定义了一个LoginInterceptor实现HandlerInterceptor接口在preHandle里从Authorization请求头取token解析失败就返回401成功就把userId放进request的attribute里后续Controller通过RequestAttribute取出。然后在WebMvcConfigurer里注册拦截器同时用excludePathPatterns放行登录、注册、商品列表、商品详情这些不需要鉴权的公开接口。后台管理员的权限校验则是在注解或拦截器里判断role字段。这样一套下来接口的访问控制就清晰了。有个小细节很多教程不讲拦截器放行了登录接口但vue打包后的静态资源也在同一个应用里如果你把静态资源路径也拦截了页面就会白屏。所以拦截器注册时一定要把/static/**、/favicon.ico这些路径排除掉或者干脆把拦截范围限定在/api/**下。4. Vue前端实现与前后端联调4.1 前端工程初始化与路由设计前端我用Vue CLI初始化工程安装element-ui做后台管理界面axios做请求。初始化完成后第一步不是写页面而是设计路由。游客能访问首页、商品列表、商品详情、登录注册登录用户可以进购物车、订单列表、个人中心管理员进后台管理页面。这个权限模型用前端路由守卫实现router.beforeEach((to, from, next) { const token localStorage.getItem(token); if (to.meta.requiresAuth !token) { next(/login); } else if (to.path.startsWith(/admin) localStorage.getItem(role) ! 1) { next(/); } else { next(); } });路由懒加载也建议加上通过component: () import(/views/ProductList.vue)这种方式按需加载页面组件不然首次进入页面会把所有JS都下载下来体验很差。vue动态路由在这个项目里也有体现后台菜单根据角色动态生成虽然这套系统角色只有两种但接口保留在公共api里未来扩展多角色时前端路由也能灵活适配。4.2 axios封装与请求拦截器axios不封装直接用后期改baseURL、加token、统一处理错误会让你痛不欲生。我在utils/request.js里创建一个axios实例设置baseURL为/api超时时间15秒然后在请求拦截器里从localStorage取出token加到请求头的Authorization字段。响应拦截器里统一处理返回结构code为200就直接返回data给业务页面code为401就清掉本地token并跳转登录页其他code就弹Message提示错误信息。这里有一个联调时必须注意的点就是开发环境跨域问题。前端跑在localhost:8080这种端口后端跑在8081直接请求必然跨域。两个解决方案任选一是在后端写跨域配置CorsFilter允许指定来源二是在前端vue.config.js里配置devServer的proxy把/api开头的请求转发到后端地址。我个人推荐第二种因为生产环境Vue打包后会放在SpringBoot同一端口下根本不存在跨域开发环境用代理模拟同源代码里就不需要写死后端地址了。配置大概是devServer: { proxy: { /api: { target: http://localhost:8081, changeOrigin: true } } }4.3 核心页面组件怎么拆页面组件拆得好不好直接决定你写第二遍、第三遍页面的效率。商品列表页我拆成筛选区、商品卡片网格、分页条三个部分商品卡片抽成ProductCard组件首页推荐、分类列表页都能复用。购物车页面拆成商品清单和结算栏两个区域结算栏显示选中商品的总金额这个金额我是在前端实时算的没有每次都请求后端只有点击提交订单时才用后端重新计算一次前后端金额以最后一次后端计算为准避免用户篡改前端数据造成损失。订单列表页按状态Tab分栏待付款、待发货、待收货、已完成每个Tab对应后端一个状态码前端映射成中文标签。管理后台这块用element-ui的表格组件能省很多事。商品管理页面就是el-table加el-dialog表单校验用rules图片上传用el-upload上传完成后后端返回图片访问路径存到商品主图字段。我遇到过不少人在图片这卡住其实思路很简单前端把文件传到后端的upload接口接口把文件保存到本地指定目录或OSS返回一个URL前端再把URL和商品其他信息一起提交。一定要让上传接口单独存在不要和商品保存接口混在一起否则表单还没提交图片就先传上去了数据容易对不上。4.4 vue打包放进SpringBoot的具体操作项目做完要部署最常见的方式就是把Vue打包后的静态文件放进SpringBoot工程里让后端用一个端口同时提供页面和接口。具体操作很简单先在前端工程根目录执行npm run build生成dist目录然后把dist目录下的文件全部复制到后端src/main/resources/static目录下重新打包后端jar。启动后直接访问http://localhost:8080/index.html就能看到页面/api开头的请求由后端Controller处理不会打到前端路由上因为我们的路由模式是history刷新页面时会去请求后端后端没有对应的Controller就会404。解决history路由刷新404的办法也顺手说一下写一个转发Controller把所有非/api路径转发到index.html让前端路由接管。这是SPA部署的经典配置代码不多但不写的话用户在商品详情页按F5就会白屏这是很多人部署完才发现的坑。5. 常见问题与排查技巧实录5.1 版本不匹配与驱动配置问题我整理这套源码和帮人排查问题时遇到过的问题九成都是环境版本造成的。SpringBoot版本太高是最常见的比如SpringBoot 2.7以上配合某些旧版MyBatis starter启动时直接报循环依赖或者BeanDefinitionStoreException还有JDK版本高过SpringBoot支持范围启动直接崩。我的建议是锁定这套组合JDK 1.8 SpringBoot 2.5或2.6 mybatis-spring-boot-starter 2.x MySQL 5.7这个组合在大量项目里验证过最稳。MySQL连接这块也常踩坑。MySQL 8.0的驱动类是com.mysql.cj.jdbc.Driver而5.7是com.mysql.jdbc.Driver8.0在连接串里还必须带上serverTimezoneAsia/Shanghai否则报时区错误。这两个坑报错信息都不直观很多人卡半天。数据库连接串里建议再加上useSSLfalse和characterEncodingutf8前者避免本地环境SSL证书问题后者保证中文不乱码。至于mysql ssl连接这种报错十有八九就是没加useSSLfalse导致的。5.2 Maven构建时MyBatis XML丢失问题再说一个隐蔽的坑。很多人把Mapper的XML文件放在src/main/java的mapper目录下和Java接口放一起然后用Maven打包时发现XML文件根本没进classpath启动报Invalid bound statement (not found)。原因是Maven默认只打包resources目录下的资源不处理java目录下的XML。解决方案有三种一是把XML文件移到src/main/resources/mapper目录下在application.yml里配置mybatis.mapper-locationsclasspath:mapper/*.xml二是在pom.xml里加build-resources配置让Maven额外打包java目录下的xml三是直接在pom里配置mybatis-generator插件自动生成。我推荐第一种路径清晰不会出现资源文件被漏打包的问题。这个错误和mybatis中xml配置初始化原理有关从MyBatis源码角度看它内部是通过Resources.getResourceAsStream去classpath找指定路径的XML找不到自然初始化不了映射语句理解了这层机制排查方向就会很明确。5.3 跨域联调与接口对接问题联调阶段最常见的报错是浏览器提示CORS policy。如果后端没配跨域前端代理没生效都会出这个错。排查思路我总结成一句话先看Network面板里请求到底发出去了没有发出去看响应头里有没有Access-Control-Allow-Origin没有就是后端的问题根本没发出去多半是前端代码或者代理配置的问题。另外注意如果前端配置了proxy代理浏览器Network里看到的请求地址应该还是/api开头如果看到的是http://localhost:8081这种完整地址说明代理没生效要重启devServer并检查vue.config.js是否被正确加载。接口对接还有一个很常见的问题是参数格式。后端接口用RequestBody接收JSON对象时前端axios必须设置Content-Type为application/json并且传入的是对象用RequestParam接收表单参数时前端要传字符串或者用URLSearchParams。这个不匹配经常导致后端报Required request body is missing但其实代码逻辑都对。5.4 常见问题速查表现象大概率原因处理方式启动报ClassNotFoundException: com.mysql.jdbc.DriverMySQL驱动类名不匹配换成com.mysql.cj.jdbc.Driver8.0启动报serverTimezone异常连接串缺时区参数加上serverTimezoneAsia/Shanghai查询报表不存在数据库没执行SQL脚本用建表SQL初始化数据库接口返回404但Controller存在拦截器放行路径没配好或包扫描不对检查SpringBootApplication扫描范围中文乱码连接串没指定编码加characterEncodingutf8Invalid bound statementXML没打进去或路径不对检查mapper-locations配置刷新页面404history路由没配置转发写转发Controller到index.htmlCORS跨域报错前端代理没生效或后端没配检查proxy配置和后端跨域配置最后分享一个我个人的习惯每次启动SpringBoot之前先看端口有没有被占用Windows下用netstat -ano查一下8080和8081端口别等到启动报Port already in use才去处理。项目跑起来之后先用Postman或Apifox把后端接口全部测通再打开前端页面联调这样排查问题的时候你至少能确定是谁的锅而不是前后端互相猜忌。把这套流程走顺了这个宠物商城项目从设计到上线大概也就是一个周末加两个晚上的工作量而且每个模块你都能讲清楚为什么这么做面试的时候底气完全不一样。