
做了差不多一整年的宠物用品商城项目从数据库设计到前后端联调再到最后部署上线踩了不少坑也沉淀了不少东西。这套基于SpringBootVue的在线宠物用品交易网站管理系统用了MyBatis做持久层、MySQL做数据存储属于目前中小型电商项目里相当经典的一套技术组合。我把整个从零搭建的过程、核心模块的实现逻辑、还有实际开发中容易被忽略的细节在这里完整梳理一遍。如果你正准备做类似的电商管理系统、毕业设计或者公司内部需要一个可落地的全套源码参考这篇内容应该能帮你省下不少摸索时间。1. 为什么做在线宠物用品交易系统项目背景与选型逻辑做这个项目之前我其实先花了几天时间调研了市面上几个开源商城系统。大部分要么功能太重、二开成本高要么技术栈停留在老版本用起来处处掣肘。考虑到宠物用品这个品类有其特殊性——商品规格多猫粮狗粮有不同口味和克重、库存敏感、订单关联宠物档案信息最终还是决定自己从零搭建一套轻量但完整的交易系统。1.1 宠物用品交易的核心业务痛点宠物用品交易和普通服饰、数码产品不一样的地方在于用户决策链条更依赖商品信息质量。比如一袋猫粮用户饥要看配方、要看适用年龄段、要看净含量和保质期甚至要看是不是正品。这意味着商品详情字段必须做得足够富。另外宠物主通常是周期性采购同一款猫砂一个月可能要买两三次所以会员系统、收藏夹、复购提醒这些功能非常重要。还有一个容易被忽略的点是售后流程。宠物用品很多属于开袋不退类目订单状态里必须区分未发货可退已发货仅退款签收后售后申请等不同状态系统里需要一套清晰的状态机来约束订单流转否则后期和快递、仓库对账会一片混乱。我在这版系统里把订单状态设计成了枚举驱动从代码层面杜绝了非法跳转状态出现的可能。1.2 技术栈选型的底层逻辑选SpringBoot是因为它对Spring生态做了极简封装内置Tomcat、自动配置一个注解就能启动一个Web服务特别适合我们这种需要快速迭代、但不是几十人团队规模的项目。Vue负责前端页面组件化开发让商城的商品列表、购物车、订单管理都能以独立组件方式维护避免页面之间互相污染。MyBatis这块很多新手会纠结到底用MyBatis还是MyBatis-Plus。我的实际感受是如果你的项目有一定数量的自定义复杂查询、报表统计和动态SQL需求原生MyBatis灵活度更高如果你只想快速CRUDMyBatis-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 idselectProductPage resultTypemap 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 testcategoryId ! null AND p.category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (p.title LIKE CONCAT(%, #{keyword}, %) OR p.subtitle LIKE CONCAT(%, #{keyword}, %)) /if if testminPrice ! null AND p.min_price gt; #{minPrice} /if if testmaxPrice ! null AND p.min_price lt; #{maxPrice} /if /where ORDER BY choose when testsortRule sales_descp.sales_count DESC/when when testsortRule price_ascp.min_price ASC/when otherwisep.create_time DESC/otherwise /choose LIMIT #{offset}, #{pageSize} /select这种写法最直接的好处是一个方法覆盖所有筛选组合而不是为每个条件组合单独写一个方法。我还会在开发环境配置MyBatis打印SQL日志通过mybatis.configuration.log-implorg.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-formatyyyy-MM-dd HH:mm:ss spring.jackson.time-zoneGMT8配置文件一加所有接口的时间和格式就齐整了。前后端联调时一定要先约定好全局的响应结构、分页结构、时间格式和错误码语义不要各自定义一套然后靠接口文档二次翻译那样在高强度迭代时非常容易埋雷。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里必须带serverTimezoneAsia/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 evictionLRU flushInterval60000 size1024 readOnlytrue/但这里必须提醒一个严重的坑如果表涉及多表关联查询而其中一个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这四样东西单拆开都有大量教程但真正把它们无缝衔接成一个可交易、可管理、可上线的项目中间的取舍和坑只有动手做一遍才能真正理解。希望上面的内容能让你在这个项目的落地过程中少走一些弯路。