☰
基于SpringBoot+Vue的宠物用品商城系统开发与部署实践
2026/10/7 11:45:48 网站建设 项目流程

做了差不多一整年的宠物用品商城项目,从数据库设计到前后端联调,再到最后部署上线,踩了不少坑,也沉淀了不少东西。这套基于SpringBoot+Vue的在线宠物用品交易网站管理系统,用了MyBatis做持久层、MySQL做数据存储,属于目前中小型电商项目里相当经典的一套技术组合。我把整个从零搭建的过程、核心模块的实现逻辑、还有实际开发中容易被忽略的细节,在这里完整梳理一遍。如果你正准备做类似的电商管理系统、毕业设计,或者公司内部需要一个可落地的全套源码参考,这篇内容应该能帮你省下不少摸索时间。

1. 为什么做在线宠物用品交易系统:项目背景与选型逻辑

做这个项目之前,我其实先花了几天时间调研了市面上几个开源商城系统。大部分要么功能太重、二开成本高,要么技术栈停留在老版本,用起来处处掣肘。考虑到宠物用品这个品类有其特殊性——商品规格多(猫粮狗粮有不同口味和克重)、库存敏感、订单关联宠物档案信息,最终还是决定自己从零搭建一套轻量但完整的交易系统。

1.1 宠物用品交易的核心业务痛点

宠物用品交易和普通服饰、数码产品不一样的地方在于,用户决策链条更依赖商品信息质量。比如一袋猫粮,用户饥要看配方、要看适用年龄段、要看净含量和保质期,甚至要看是不是正品。这意味着商品详情字段必须做得足够富。另外,宠物主通常是周期性采购,同一款猫砂一个月可能要买两三次,所以会员系统、收藏夹、复购提醒这些功能非常重要。

还有一个容易被忽略的点是售后流程。宠物用品很多属于"开袋不退"类目,订单状态里必须区分"未发货可退""已发货仅退款""签收后售后申请"等不同状态,系统里需要一套清晰的状态机来约束订单流转,否则后期和快递、仓库对账会一片混乱。我在这版系统里把订单状态设计成了枚举驱动,从代码层面杜绝了"非法跳转状态"出现的可能。

1.2 技术栈选型的底层逻辑

选SpringBoot,是因为它对Spring生态做了极简封装,内置Tomcat、自动配置,一个注解就能启动一个Web服务,特别适合我们这种需要快速迭代、但不是几十人团队规模的项目。Vue负责前端页面,组件化开发让商城的商品列表、购物车、订单管理都能以独立组件方式维护,避免页面之间互相污染。

MyBatis这块,很多新手会纠结到底用MyBatis还是MyBatis-Plus。我的实际感受是:如果你的项目有一定数量的自定义复杂查询、报表统计和动态SQL需求,原生MyBatis灵活度更高;如果你只想快速CRUD,MyBatis-Plus确实更省事。我的做法是两者结合:基础单表操作用MyBatis-Plus注解,复杂查询写XML映射,既保留灵活度又没有丢失效率。

MySQL作为存储层,没有悬念。电商交易系统的事务性要求高,MySQL的InnoDB引擎行级锁和ACID事务能力足够成熟。同时配合索引设计,商品搜索、订单列表的查询性能在数据量几百万以内完全扛得住。这套组合的逻辑很简单:SpringBoot负责业务调度与接口暴露,Vue负责交互与渲染,MyBatis负责SQL灵活性,MySQL负责数据可靠落地。

1.3 项目整体架构与模块划分

整个系统划分为用户端和管理端两个大的操作面,但共享同一套后端服务。用户端面向普通消费者,提供注册登录、宠物档案管理、商品浏览、购物车、下单支付、订单跟踪、售后申请这些能力。管理端则面向运营和客服人员,提供商品上下架、类目管理、库存调整、订单处理、用户管理、数据概览等能力。

从工程结构上,我采用前后端分离的方式:前端是独立的Vue工程,后端是标准的SpringBoot多模块目录。两者通过RESTful API通信,鉴权走JWT,数据格式统一为JSON。这种分离方式的优势很直接——可以各自独立部署、独立升级,前端团队和后端开发的职责边界清晰,就算后面要把管理端单独拆出去做成一个后台项目也很轻松。后面我会把具体目录结构展示出来。

2. 数据库设计:从商品到订单的完整链路

电商项目的成败,数据库设计至少占一半。我见过很多项目在起跑阶段表结构就埋了雷,比如订单和商品直接耦合、库存用浮点数存储、没有逻辑删除字段,后期改起来痛不欲生。这个商城系统的数据模型,我是按照"商品域-交易域-用户域-营销域"这样的思路拆分的。

2.1 核心表结构设计思路

商品域方面,设计了类目表、商品表、SKU表和商品图片表。这里SKU是关键,宠物粮同一款商品会有不同口味和克重,每个SKU对应一个独立的库存数和价格。商品表存的是公共属性,比如标题、品牌、详细描述;SKU表存的是具体规格组合值、价格、库存和SKU编码。图片表单独拆出来,是为了支持一个商品多张图并且可以排序,避免把JSON塞在商品表里导致查询和更新麻烦。

交易域方面,包括购物车表、订单表、订单明细表和售后申请表。订单表和订单明细表是一对多的关系,订单表存收货人信息、总金额、订单状态、支付方式等;订单明细表记录每一行商品对应的SKU快照——这里必须用快照思维,商品价格和标题经常变动,如果订单明细里不存当时的快照,后续对账或者售后时就会产生纠纷。

用户域比较简单:用户表、收货地址表、宠物档案表。宠物档案表是这个项目比较有特色的地方,宠物主可以给自家猫狗建立档案,记录品种、年龄、体重、绝育状态,系统基于这些信息可以做一些推荐逻辑,比如根据体重推荐猫粮规格。这个功能虽然占用的表不多,但在产品体验上非常加分。

2.2 库存与订单的并发控制

宠物用品促销期经常会出现多人同时抢一款热门猫粮的情况,这时候最怕超卖。我的处理方案是:扣减库存的SQL语句强制带上库存大于零的条件,然后用受影响行数来判断是否扣减成功。

UPDATE sku_stock SET stock = stock - #{quantity}, update_time = NOW() WHERE sku_id = #{skuId} AND stock >= #{quantity}

这里有个非常关键的操作细节:在SpringBoot的Service层,这个过程必须加事务,并先锁定订单记录。我的实际做法是创建订单时,先插入订单主记录拿到订单号,再逐条锁SKU扣库存,全部成功后才提交事务;任何一条SKU扣减失败,直接抛出异常触发回滚,这样能让下单操作具备原子性。如果并发量再往上走,可以引入Redis预减库存,但当前数据库直扣方案在中小型商城场景下已经足够稳定。

2.3 MyBatis的SQL映射和项目中的动态SQL实践

MyBatis的XML映射我大量用于复杂查询。最典型的是商品列表的条件筛选——用户可能按类目、品牌、价格区间、销量排序全部叠加筛选。如果逐个拼接字符串写死SQL,代码维护成本很高。我用动态SQL标签做了个通用的商品查询映射:

<select id="selectProductPage" resultType="map"> SELECT p.id, p.title, p.main_image, p.min_price, p.sales_count, s.stock_total FROM product p LEFT JOIN ( SELECT product_id, SUM(stock) AS stock_total FROM sku GROUP BY product_id ) s ON p.id = s.product_id <where> <if test="categoryId != null"> AND p.category_id = #{categoryId} </if> <if test="keyword != null and keyword != ''"> AND (p.title LIKE CONCAT('%', #{keyword}, '%') OR p.subtitle LIKE CONCAT('%', #{keyword}, '%')) </if> <if test="minPrice != null"> AND p.min_price &gt;= #{minPrice} </if> <if test="maxPrice != null"> AND p.min_price &lt;= #{maxPrice} </if> </where> ORDER BY <choose> <when test="sortRule == 'sales_desc'">p.sales_count DESC</when> <when test="sortRule == 'price_asc'">p.min_price ASC</when> <otherwise>p.create_time DESC</otherwise> </choose> LIMIT #{offset}, #{pageSize} </select>

这种写法最直接的好处是:一个方法覆盖所有筛选组合,而不是为每个条件组合单独写一个方法。我还会在开发环境配置MyBatis打印SQL日志,通过mybatis.configuration.log-impl=org.apache.ibatis.logging.stdout.StdOutImpl在控制台直接看到执行语句,排查问题效率会提升非常明显。上线后则切换为不打印SQL,避免日志量过大和参数泄漏。

3. 后端核心业务逻辑实现:从JWT鉴权到订单状态机

后端是整个系统的枢纽,SpringBoot项目结构清晰与否直接影响后续维护难度。我习惯把代码按功能域而不是按技术类型来分包,这样每增加一个业务模块,只需要在对应域下添加controller、service、mapper,而不是散落在十几个无意义的包中。

3.1 SpringBoot工程结构与启动流程

项目采用Maven多环境配置,通过application.yml配合application-dev.yml和application-prod.yml区分开发与生产环境。在开发环境我配置了HikariCP连接池,并打开了SQL打印和慢查询日志;生产环境则关闭控制台SQL输出、开启更严格的连接池检测。启动类非常简单,核心是通过@SpringBootApplication开启自动配置,@MapperScan("com.petmall.mapper")扫描MyBatis的Mapper接口。

整个启动流程对新人来说容易看不懂的地方在于SpringBoot自动配置到底做了什么。通俗点说,SpringBoot在启动时会加载META-INF/spring.factories里面定义的一堆配置类,比如数据源配置类、事务配置类、WebMvc配置类。我们只需要引入对应的starter依赖,再把需要的参数写到yml里,剩下的容器创建、Bean装配、请求分发都由框架接管。理解了这个逻辑,再看项目结构会有一种豁然开朗的感觉。

3.2 用户登录注册与JWT无状态鉴权

用户模块我用双token方案——访问令牌accessToken有效期短(约30分钟),刷新令牌refreshToken有效期长(约7天)。用户登录成功后,后端签发这两个令牌,客户端保存后每次请求在HTTP Header中携带accessToken。后端通过SpringMVC拦截器对所有需要登录的接口做令牌解析,校验通过则把用户ID注入ThreadLocal,方便Service层直接获取当前登录用户。

在用户密码安全上,我采用BCrypt加密存储,不用MD5。MD5是哈希算法不是加密算法,而且现在彩虹表攻击太成熟,哪怕加盐都很容易爆破。BCrypt内部自带随机盐,每次哈希结果都不一样,验证时再对输入密码做同样的BCrypt运算来比对,安全性高一个量级。实测同样一批密码,BCrypt哈希后的存储长度和形态都更稳健,对于学生项目或商业项目都是首选。

3.3 商品、购物车与订单状态机设计

购物车模块是比较常规的CRUD,但有个细节需要注意:购物车里的商品数量不能超过对应SKU的实时库存,否则用户在结算时会出现库存不足。我的做法是购物车列表接口里联查库存,前端在数量加减处做动态限制。

订单模块是整个系统的核心。我在代码里用枚举类定义了订单状态的合法流转方向:

public enum OrderStatusEnum { WAIT_PAY(0, "待支付"), WAIT_SHIP(1, "待发货"), WAIT_RECEIVE(2, "待收货"), FINISHED(3, "已完成"), CANCELED(4, "已取消"), REFUNDING(5, "售后中"); public boolean canTransferTo(OrderStatusEnum target) { switch (this) { case WAIT_PAY: return target == CANCELED || target == WAIT_SHIP; case WAIT_SHIP: return target == WAIT_RECEIVE; case WAIT_RECEIVE: return target == FINISHED || target == REFUNDING; default: return false; } } }

为什么订单要设计状态机而不是单纯用一个String字段?因为交易系统一旦允许任意状态跳转,就会产生大量数据不一致的问题,比如用户还没付款,仓库就发货了;或者售后处理中订单被标记成已完成。状态机从代码层面对流转做了硬约束,每个接口只允许状态按合法路径变化,不合法的情况直接抛异常。这个设计在未来接入更复杂的售后流程时尤其重要。

4. 前端页面开发与API对接:Vue组件化实战

前端这块我用了Vue2 + Vuex + Vue Router + Element UI的组合。虽然Vue3已经出来挺长时间,但考虑到这个项目要和新老同事协作,Element UI在管理端的中后台组件丰富度和资料成熟度还是很有优势。当然,如果你是从零开始并且团队对新语法接受度高,直接上Vue3 + Element Plus也完全没问题。

4.1 Vue工程化搭建与路由设计

通过vue create脚手架生成的工程,我把目录按照"页面-组件-服务"三分法组织:views里面放页面级组件,components里面放可复用业务组件,api目录抽离所有Axios请求。这样做的原因很简单——商城页面中有大量需要复用的模块,比如商品卡片、分页器、SKU选择器,把它们拆成独立组件后,商品列表页、搜索结果页、推荐模块都可以直接拿来用,不用重复写模板代码。

Vue Router这块我做了两个重要的设计:路由懒加载和登录守卫。路由懒加载通过const GoodDetail = () => import('@/views/product/GoodDetail.vue')实现,这样首屏只加载当前页面需要的JS,整个项目首屏体积能减少30%以上。登录守卫在router.beforeEach钩子里检查是否携带有效的用户令牌,没登录访问"我的订单"这类页面时会自动跳转到登录页,登录成功再回到原来的目标路由,体验很顺滑。

4.2 商城核心页面实现要点

商品列表页的最大挑战是筛选条件与URL的同步。用户勾选类目、价格区间、排序方式后,如果不把这些条件同步到URL query中,用户刷新页面或分享链接时筛选状态就会丢失。我的做法是把筛选条件直接绑定到this.$route.query,筛选变化时通过router.replace更新URL并重新请求列表数据。这样浏览器自带的后退键也能很好地配合筛选状态的历史回退。

商品详情页的SKU选择器是交互难点。同一个商品同时有多种口味、多种克重,用户选择其中一个维度后,另一个维度有没有货要实时刷新。我的实现方式是用一个二维矩阵标记可售组合,用户选中某一个维度值时,判断剩余维度的每个值是否存在有效组合,不可选的值置灰。这套U逻辑虽然不复杂,但是对用户体验的提升非常直接,用户知道自己选的规格是否有货,而不是提交订单时才被告知库存不足。

4.3 前后端联调的关键节点与踩坑

联调阶段最容易出问题的其实是跨域和鉴权。开发环境下前端跑在localhost:8080,后端跑在localhost:9090,浏览器会触发跨域拦截。我的解决方案是后端配置全局CORS策略,允许指定来源和携带凭证;生产环境则完全不需要跨域配置,因为Vue打包后的静态资源直接由SpringBoot同域托管,请求走同源路径。

另外一个印象深刻的坑是时间格式化不一致。后端返回LocalDateTime默认序列化成2025-06-18T10:30:00这种带T的格式,前端显示订单时间很别扭。后来我在Jackson配置里统一指定了全局时间格式:

spring.jackson.date-format=yyyy-MM-dd HH:mm:ss spring.jackson.time-zone=GMT+8

配置文件一加,所有接口的时间和格式就齐整了。前后端联调时一定要先约定好全局的响应结构、分页结构、时间格式和错误码语义,不要各自定义一套然后靠接口文档二次翻译,那样在高强度迭代时非常容易埋雷。

5. Vue打包进SpringBoot的部署方案与上线踩坑记录

系统开发完成后会面临部署问题。现在主流的部署方式是前端Nginx + 后端Java进程分离部署,或者使用Docker Compose一键编排。这个项目我经过几轮调整后,用了一种比较轻量的方案——把Vue构建后的静态资源直接放进SpringBoot项目的static目录,打成单个jar包运行,非常适合中小型项目和服务器资源有限的情况。

5.1 三种前后端部署方案对比

我整理了实际项目中考虑过的三种部署方式,这里直接给出一张对比表供参考:

部署方式优点缺点适用场景
Vue打包进SpringBoot的static目录单jar包部署,运维简单,不涉及跨域前端更新必须重新打后端jar包个人项目、毕设、小型内网系统
Nginx托管前端 + Java后端前后端独立升级,静态资源由Nginx服役性能好需要额外配置Nginx代理转发,跨域需配置正式商业项目、前后端耦合度低
Docker Compose一键编排环境一致性好,升级回滚方便需要掌握Docker基础知识多人协作、CI/CD流程完善的项目

如果你的前端静态资源整体打包进SpringBoot,有一个很重要的步骤:配置SpringBoot的资源映射和history路由回退。Vue Router使用history模式时会依赖浏览器的history API,这时前端路由路径比如/product/123,如果后端没有做回退处理,用户直接刷新该页面就会404。所以我在SpringBoot里添加了一个路由转发规则,将非API路径一律转发到index.html。

@Controller public class ViewController { @RequestMapping(value = {"/", "/product/**", "/cart/**", "/order/**", "/user/**"}) public String forward() { return "forward:/index.html"; } }

5.2 服务器部署与MySQL初始化

我部署到一台2核4G的云服务器上,系统是CentOS。运维步骤其实并不复杂:先安装JDK和MySQL,把SQL脚本导入数据库并创建专用账号;接着用mvn clean package把后端打成jar;把Vue构建好的dist目录拷到SpringBoot的src/main/resources/static下;最后java -jar启动。

MySQL在生产环境下的初始化配置有几个点容易大意:第一是字符集,数据库、数据表、连接URL都要保持一致,否则中文乱码查到头大;第二是时区设置,连接URL里必须带serverTimezone=Asia/Shanghai;第三是连接池大小,2G内存的服务器连接池上限不要配太大,我用的是maximum-pool-size: 10,实测并发足够,也不会把内存吃爆。

5.3 线上常见的几个坑和排查思路

上线后最常碰到的问题可以归结为几类。第一类是首次启动后接口返回500,多半是数据源或Mapper XML映射路径没对。排查思路很简单,看启动日志最下面有没有APPLICATION FAILED TO START字样,如果有就顺着Cause提示去排查,大概率是yml配置或注解写错。

第二类是前端页面可以打开但数据请求失败,这就要区分是404还是500。404可能是后端打包时没有把静态资源放进去,或者网关路由没配置好;500则优先看后端日志的具体报错堆栈。

第三类是数据库连接池爆掉,比如高峰时段出现connection is not available错误。遇到这种,十有八九是应用没有正确释放连接,后来我定位到是因为某几个Service方法里手动获取连接后没有在finally中释放,改成Spring事务托管后问题消失。

6. MyBatis配置优化:从控制台SQL打印到二级缓存落地

MyBatis在ORM层带来的体验好坏,很大程度取决于配置是否合理。之前在看MyBatis缓存面试题时,很多人能背出源码原理,但真到项目里不会配置,等于没掌握。这里把我在项目中的配置文件方式做一次完整分享。

6.1 开发环境的SQL打印配置

如果你用的是SpringBoot,只需要在application-dev.yml中加入以下配置:

mybatis: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

第一个配置把数据库下划线段自动映射到驼峰属性,比如create_time对应createTime,省去大量手动resultMap;第二个配置在控制台直接输出完整SQL和执行参数。实测在联调阶段,这个配置能帮你快速定位SQL语法错误、参数绑定错误、数据库字段和实体类不匹配的问题。

每一条SQL执行完,控制台会出现三行信息:Preparing、Parameters、Total。Preparing显示的是真正执行的SQL语句,Parameters显示的是PreparedStatement绑定的参数,Total显示的是查询耗时。如果你发现某条SQL的Total很高,基本可以考虑加索引或改写SQL了。

6.2 二级缓存的使用边界与风险

MyBatis的二级缓存是基于namespace级别的,也就是一个Mapper对应一个缓存区域。这个项目里,我对商品类目的信息做了二级缓存,因为类目变动频率极低、查询次数极多。开启方式是在Mapper XML上配置:

<cache eviction="LRU" flushInterval="60000" size="1024" readOnly="true"/>

但这里必须提醒一个严重的坑:如果表涉及多表关联查询,而其中一个Mapper开启了二级缓存,那么当关联表发生增删改时,缓存并不会自动失效,就会出现查询出旧数据的脏读情况。我在订单相关的Mapper上严格禁止了二级缓存,因为订单数据实时性要求极高,任何缓存带来的数据延迟都无法接受。一句话总结:二级缓存只能用在几乎不变的基础数据上,动态更新的业务数据绝对不要用。如果你在面试中被问到MyBatis二级缓存,能说出这条实际踩坑经验,会显得比背书扎实很多。

7. 这个系统后续可以怎么扩展:从推荐系统到多商户支持

现在的版本相当于一个单商户自营型宠物商城,能做基本的商品交易闭环。但如果你想把它推向商业使用,或者做一个更完整的毕设项目,有几个方向非常值得往里加。

7.1 基于宠物档案的智能推荐

我们已经有了宠物档案表,每个人的宠物年龄、品种、体重都是结构化的数据。在这个基础上可以做一个简单的推荐引擎:根据宠物体重匹配猫粮克重规格,根据宠物年龄匹配幼年期/成年期/老年期专用粮,甚至可以对同一品种宠物的历史购买记录做协同过滤。

这个模块的技术门槛其实不高,一个定时任务分析订单数据生成推荐结果,然后写入推荐表,用户端每次请求推荐列表时直接查询,像Redis加速一下就能跑得很流畅。宠物档案数据的积累会随着使用时间越来越有价值,这是这个项目区别于普通商城的一大亮点。

7.2 引入Redis缓存热点数据

商品详情页往往是访问量最高的页面,而商品详情相对稳定。把商品详情在Redis中做缓存,key设计为product:detail:{id},可以大幅降低数据库压力,详情页接口的QPS能提升至少一个量级。需要特别注意的是缓存失效策略,商品上架下架、价格变动时要主动删除对应缓存,而不是等它自然过期。

7.3 多商户商城化改造

如果想把系统从单商户升级成平台型商城,最核心的表改动是引入商家维度。商品表增加merchant_id,订单表也关联商家,管理端升级为平台运营后台,商家需要独立的商家中心。技术层面还会涉及分账、提现、商家结算这些资金链路,复杂度会明显上升。好在当前项目的表结构和代码域划分留有足够的扩展空间,商品域、交易域、用户域的边界清晰,改造时不会牵一发而动全身。

7.4 支付对接

目前我用的是模拟支付,方便演示和测试。真正上线必须接入支付宝或微信支付,流程大同小异:后端统一下单、生成支付二维码、轮询或回调确认支付结果、回调中更新订单状态。这里最需要严谨的是回调签名验证——一定要在回调处理时先验签,再按订单号更新订单状态,并且回调处理接口做成幂等的,防止支付宝/微信重复通知导致订单状态被错误覆盖。

我在处理一个回调的细节时踩过坑:回调成功更新了订单状态,但客户端因为网络原因没收到结果,用户刷新页面后还是待支付状态,实际上钱已经扣了。后来我在前端增加了一个"主动查询订单状态"的按钮,并在支付成功页做了轮询兜底,彻底解决了这个问题。建议你对接支付时提前设计好这一层交互,不然会收到不少用户反馈。

关于源码获取与使用的一点个人建议

写到最后,聊聊这套源码的使用方式。这类系统最忌讳的是拿来就跑,完全不看表结构和代码逻辑。我建议你拿到源码后,先花半天时间把SQL脚本里的每张表过一遍,跟我在前面列的商品域、交易域、用户域对应起来,然后启动项目,把用户端从注册到下单购买的整个流程走一遍,再进管理端上下架一个商品,修改一下库存和价格。整个链路通顺了以后,再开始动手改代码。

如果你要在这个项目基础上做毕设,我建议你要么加深一个方向的技术深度,比如把缓存体系做成Redis + Caffeine多级缓存、把商品搜索换成Elasticsearch;要么增加一个完整的业务闭环,比如积分商城、秒杀活动、拼团功能。这两条路都比在原版上改改页面配色要扎实得多,答辩时也更有内容可以讲。

根据我的经验,如果遇到Vue打包后放进SpringBoot白屏的问题,绝大多数是因为前端资源路径配置不对,需要在vue.config.js里把publicPath设为相对路径'./',然后用hash模式。

最后再说一句我个人在实际部署中的体会吧:一个小型电商系统的复杂度从来不在于单个技术点有多难,而是如何把这些技术点有序地编排在一起。SpringBoot、Vue、MyBatis、MySQL这四样东西单拆开都有大量教程,但真正把它们无缝衔接成一个可交易、可管理、可上线的项目,中间的取舍和坑只有动手做一遍才能真正理解。希望上面的内容能让你在这个项目的落地过程中少走一些弯路。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询