多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

SpringBoot2+Vue3宠物商城系统:前后端分离实战全解析

SpringBoot2+Vue3宠物商城系统:前后端分离实战全解析 前后端分离项目写了几年技术栈审美会变得异常挑剔。2020年以后我接到的商城类项目需求十条里有七八条是同一套配方SpringBoot2 写后端、Vue3 写前端、MyBatis-Plus 管数据访问、MySQL8 做存储。这个 Java Web 宠物商城系统项目就是很典型的例子——它不搞花活但把一套商城系统的骨架搭得相当完整商品、分类、购物车、订单、会员、后台管理全都覆盖还带文档非常适合刚走完 JavaWeb 基础、想接触前后端分离实战的同学拿来练手也适合有经验的开发者把它当成脚手架二次改造。这个项目能解决什么问题一句话概括它用最小成本演示了一条完整的电商业务链路从用户注册登录、浏览下单到管理员处理订单、管理商品数据每一层都给出了可以跑的代码。相比零散的教程 Demo它最大的价值在于完整——前后端通过 RESTful 接口通信数据库表结构设计合理权限用 JWT 处理分页、文件上传、条件查询这些高频需求都有落地实现。无论你是学生做毕业设计还是初级工程师想研究一套正规项目的组织方式这套代码都值得拆开来仔细看一遍。接下来说实际拆解过程中的重点和那些藏在代码细节里的坑。我会按技术选型复盘、核心业务链路、关键代码实现、部署注意点、扩展方向这几个方面展开读的时候建议你打开项目源码对着看体验会更好。1. 技术选型复盘这套组合为什么长盛不衰先说结论这套技术组合能成为中小型 Web 项目的标准答案是因为每一个参与者在自己的位置上都是性价比最稳的选择。它们单拎出来都不是最新的但组合在一起学习成本、开发效率、上手门槛恰好达到一个完美的平衡点。1.1 项目需要什么样的技术骨架宠物商城不是纯粹的电商它比标准 B2C 多了几个特色场景宠物活体要按品种、年龄、性别做多维筛选宠物用品有不同规格口味、大小、适用体型订单除了普通商品还会涉及预约、寄养这类服务型条目。但核心业务骨架仍然是经典的用户-商品-订单三角模型。在这种规模的项目里如果用传统的 SpringMVC JSP 做同步页面前后端代码会揉在一起后期改一个页面要连带重启后端开发和体验都差。如果上微服务那套注册中心、网关、分布式事务对这样的项目完全是过度设计光搭建环境就能劝退一半人。于是 SpringBoot2 负责快速构建独立 API 服务、MyBatis-Plus 免去大量重复的持久层代码、Vue3 负责页面交互和组件化开发、MySQL8 提供稳定可靠的数据存储——每一层都有明确的职责分工没有多余的环节。1.2 SpringBoot2 与 MyBatis-Plus 的分工逻辑SpringBoot2 在这里的核心作用不是框架而是自动装配的生工程。它把 Web 容器内嵌 Tomcat、JSON 序列化、参数校验、数据库连接池全都通过 starter 一键整合。开发者只需要关注自己的业务代码启动一个 main 方法就完成了过去配置一大堆 XML 的工作。MyBatis-Plus 则是 MyBatis 的增强工具它提供了 BaseMapper 接口单表 CRUD 不用写任何 SQL。但注意这个不用写 SQL是有边界的遇到多表联查、复杂统计、动态条件拼装MyBatis-Plus 的 QueryWrapper 虽然能写但可读性会迅速下降。所以在宠物商城项目里合理的做法是简单操作依赖 MP 的通用方法订单统计、报表这类复杂查询则直接在 Mapper 里写自定义 SQL两者互补而不是二选一。1.3 Vue3 和 MySQL8 在这个项目里的定位Vue3 相对于 Vue2 的最大变化不是渲染性能而是组合式 APIComposition API带来的逻辑复用能力。商城项目有大量页面级功能——购物车的状态管理、表单校验、路由守卫、接口封装……如果用 Vue2 的选项式写法逻辑会分散在 data、methods、computed 里逻辑稍微复杂就难维护。Vue3 允许把同一个业务场景的状态和执行函数组织在一起这在商城这种多模块项目里体验差别非常大。MySQL8 则是很多人容易低估的一环。很多教材还在教 MySQL5.7 的写法但 8.0 已经是当前生产环境的事实标准了。它默认字符集是 utf8mb4真正做到完整支持 emoji 和生僻字支持窗口函数一些订单排名的 SQL 写起来简洁得多另外缓存策略改变后一些 5.x 时代的老调优经验已经失效如果你还在用 5.7 的思路调 8.0 的性能可能会事倍功半。对比项传统方案本项目方案优势前端渲染JSP / 服务端模板Vue3 前端路由前后端分离团队并行开发后端结构SpringMVC XML 配置SpringBoot2 自动装配减少配置快速启动数据持久层手写大量 MyBatis XMLMyBatis-Plus 通用 Mapper单表零 SQL效率翻倍数据库MySQL5.7MySQL8.0utf8mb4、窗口函数、更强性能这套组合选完下一步就是梳理业务功能看看一个端到端的商城系统到底有哪些东西要写。2. 核心业务功能域拆解宠物商城必须覆盖哪些完整链路如果你已经跑过项目会发现这套代码由两个子工程组成一个后端服务存放/api开头的接口一个前端工程负责页面渲染。整个系统分为面向普通用户的商城前台和面向运营人员的管理后台两大块。2.1 前台用户端的操作链路与接口设计用户端不是简单地浏览商品 下单而是分阶段组织接口。注册登录阶段前端提交用户名密码后端校验通过后用 JWT 生成 Token 返回后续所有请求都在请求头里携带 Token由一个拦截器统一解析用户身份。这里的要点是——密码在数据库里绝不能明文存储至少要经过 BCrypt 加盐哈希再落库项目代码里如果用了简单 MD5建议你自己改成 BCrypt这是行业底线问题。商品浏览阶段首页要展示宠物轮播图、热销榜单、分类入口。这里高频的基础接口包括分类列表接口、分页查询商品接口、商品详情接口、模糊搜索接口。宠物商城比较特殊的一点是商品规格——一只猫有多种规格品种、性别、年龄如果像普通衣服一样简单用 SKU 字段存查询条件会非常难写。好的做法是单独建一张规格表通过商品 ID 关联筛选时用 JOIN 查出来再分组展示。下单支付阶段从购物车结算、生成订单、模拟支付到查询订单这是一条完整事务链。购物车是典型的临时数据可以用一张表存 userId、商品Id、数量、勾选状态结算时将勾选的购物车条目读取出来生成订单主表记录和订单明细子表记录然后清空对应购物车条目。那个清空操作要和生成订单在同一个事务里否则会出现订单生成了、购物车没清或者反向的脏数据。2.2 后台管理端的功能划分与权限控制管理后台是运营人员的操作入口功能和前台完全不同。它至少要有管理员登录与鉴权、商品管理上架/下架/编辑/库存增减、分类管理、订单管理查看订单详情、修改状态、发货、用户管理禁用/启用账户、数据统计看板销量排行、销售趋势。这里的权限控制不同于前台简单的 JWT 身份验证还要做角色区分——比如普通管理员只能处理订单商品管理员只能维护商品超级管理员才有所有权限。项目里通常的做法是给管理员表加一个 role 字段然后在后端拦截器里检查接口权限而不是单纯靠前端隐藏按钮做限制。记住一个原则前端的任何控制都只是体验优化真正的权限边界必须在后端校验。2.3 订单状态机的设计是这套代码的精华之一电商项目最容易写乱的就是订单状态。很多人用一两个字符串字段来回改改着改着状态就乱了。这个项目的订单状态设计可以当模板来学核心是让状态流动遵循一条单向流转链待支付 → 已支付待发货 → 已发货 → 已完成 ↘ 已取消 待支付 → 已取消超时关闭每个状态变更都对应一次真实的业务操作用户点击取消、用户支付成功、管理员发货、管理员确认完成。代码里状态枚举和状态变更的合法性校验是必须的——不能允许从已完成直接跳到待支付。我在自己的商城项目里见过不止一次这种 bug根源就是状态变更散落在多个 Service 方法里没有集中校验。建议学这套代码的时候把状态变更统一抽到一个 OrderStatusService 里每个公开方法只允许一次合法流转排查问题会容易十倍。3. 关键功能代码级实现笔记从 Service 层到 Mapper 层的细节这一部分我挑几个实际运行中最容易出问题的功能写每一个都配合背后的原理讲清楚为什么这么写。你可以对照项目源码里的具体类看效果更好。3.1 购物车结算的事务边界问题购物车结算逻辑看着简单无非是查购物车、算总价、插订单表、删购物车记录。但越是简单的逻辑越容易踩事务的坑。Transactional(rollbackFor Exception.class) public Long settleCart(ListLong cartItemIds, Long userId) { // 1. 校验购物车条目归属当前用户 ListCartItem cartItems cartItemMapper.selectBatchIds(cartItemIds); if (cartItems.stream().anyMatch(item - !item.getUserId().equals(userId))) { throw new BusinessException(500, 购物车数据异常); } // 2. 计算总金额注意用 BigDecimal不要用 double BigDecimal totalAmount cartItems.stream() .map(item - item.getPrice().multiply(BigDecimal.valueOf(item.getCount()))) .reduce(BigDecimal.ZERO, BigDecimal::add); // 3. 创建订单主记录 Order order new Order(); order.setUserId(userId); order.setOrderNo(generateOrderNo()); order.setTotalAmount(totalAmount); order.setStatus(OrderStatusEnum.UNPAID.getCode()); orderMapper.insert(order); // 4. 创建订单明细 for (CartItem item : cartItems) { OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(item.getProductId()); orderItem.setProductName(item.getProductName()); orderItem.setPrice(item.getPrice()); orderItem.setCount(item.getCount()); orderItemMapper.insert(orderItem); } // 5. 删除已结算的购物车条目 cartItemMapper.deleteBatchIds(cartItemIds); return order.getId(); }这段代码最关键的只有两点。第一Transactional注解默认只回滚RuntimeException如果业务里抛的是自定义检查异常一定要加rollbackFor Exception.class否则方法执行到一半出错前一半的数据照样提交这是最常见的看上去有事务实际上没有的假象。第二金额计算必须用BigDecimaldouble在涉及商品单价和数量的乘法时会产生精度丢失商城里这种事一旦出错就是资损问题不能妥协。3.2 商品列表与搜索MyBatis-Plus 条件构造器的正确姿势前台商品列表通常有多个筛选条件关键字、分类、价格区间、上下架状态、排序方式。如果用传统 MyBatis 手写 SQL这些条件要逐个判断是否为空再拼where片段代码又长又容易漏条件。MyBatis-Plus 的LambdaQueryWrapper就是为这个场景设计的。public IPageProduct searchProducts(ProductQuery query) { LambdaQueryWrapperProduct wrapper Wrappers.lambdaQuery(); // 关键字模糊搜索商品名 wrapper.like(StringUtils.hasText(query.getKeyword()), Product::getName, query.getKeyword()); // 分类筛选 wrapper.eq(query.getCategoryId() ! null, Product::getCategoryId, query.getCategoryId()); // 价格区间 wrapper.ge(query.getMinPrice() ! null, Product::getPrice, query.getMinPrice()); wrapper.le(query.getMaxPrice() ! null, Product::getPrice, query.getMaxPrice()); // 只查上架商品 wrapper.eq(Product::getStatus, ProductStatusEnum.ON_SALE.getCode()); // 排序策略 if (price_asc.equals(query.getSort())) { wrapper.orderByAsc(Product::getPrice); } else if (sales_desc.equals(query.getSort())) { wrapper.orderByDesc(Product::getSalesCount); } else { wrapper.orderByDesc(Product::getCreateTime); } PageProduct page new Page(query.getPageNum(), query.getPageSize()); return productMapper.selectPage(page, wrapper); }这里有个新手非常容易犯的错误like条件的第一个布尔参数是是否拼接该条件很多人会漏掉这个参数或者写反逻辑导致条件变成空值也要拼接查出来的结果永远是空。记住规则第一个参数传true条件就参与 SQL传falseMyBatis-Plus 会自动忽略这一条。配合StringUtils.hasText判断空字符串可以避免用户输入空格引起的误匹配。3.3 宠物图片上传与静态资源映射商城系统离不开图片上传。宠物商品的图片尤其多一只宠物往往要传五六张展示图。这套代码里的实现是典型的本地存储方案前端用 FormData 上传文件后端接收MultipartFile后保存到服务器磁盘固定目录然后把保存后的文件访问 URL 返回给前端。这里我强烈建议你注意一个容易被忽略的问题——文件不能存到项目运行目录classes里否则项目重启或重新部署时图片会全部消失。正确做法是在服务器上单独规划一个目录比如/data/image/pet/然后用 WebMvc 配置类做静态资源映射Configuration public class WebConfig implements WebMvcConfigurer { Override public void addResourceHandlers(ResourceHandlerRegistry registry) { registry.addResourceHandler(/upload/**) .addResourceLocations(file:/data/image/pet/); } }这样图片放在磁盘项目重启不会丢也不占用 classpath 空间。另外要做图片压缩——手机端上传的照片动辄几 MB直接存原图会把服务器带宽和数据库体积都撑爆。如果项目里没有压缩逻辑建议加一个简单方案用 Java 自带的ImageIO把宽度超过 1200 的图片等比缩放后再落盘对用户体验和服务器压力都有明显改善。3.4 订单编号生成与分页插件的关联坑订单号生成是一个看起来不起眼、但实际非常讲究的问题。通常不能用数据库自增 ID 直接当订单号因为会暴露系统销量也容易被人猜测遍历。项目里常见的做法是时间戳 随机数 用户 ID 后几位拼成唯一编号。如果并发量高还要考虑做号段缓存或分布式 ID但在这个体量的项目里用时间戳 三位随机数就够用了只要注意用数据库唯一索引兜底防止撞号。分页则是 MyBatis-Plus 插件的功劳。但有个问题容易在联调时暴露如果前端传的页码从 0 开始而后端Page默认从 1 开始第一页的数据会错位甚至空白。这个项目里前后端如果哪个地方对不上优先检查的就是这里——统一约定页码从 1 开始能少踩很多坑。还有一点是 MyBatis-Plus 分页插件的高版本已经不再支持旧版的limit ? offset ?拼接方式一定要在配置类里注册MybatisPlusInterceptor并添加PaginationInnerInterceptor否则分页查询会查出全量数据。4. 部署与上线的易错环节从本机跑通到服务器稳定运行拿到源码之后第一步当然是本地把前后端跑起来。但很多文章只会说导入运行不告诉你哪些地方会因为环境差异而翻车。下面这几条是我自己在部署这类项目时几乎每次都会遇到的坑直接给你排好。4.1 MySQL8 驱动与连接配置细节如果你用的是 MySQL8连接数据库的驱动类不再是com.mysql.jdbc.Driver而是com.mysql.cj.jdbc.Driver。SpringBoot 的spring.datasource.driver-class-name如果没改启动时就会报ClassNotFoundException。连接 URL 也要注意两个参数serverTimezone必须指定常用的中国时区写法是Asia/ShanghaiuseSSL建议设为false避免控制台刷一堆 SSL 连接警告。spring: datasource: url: jdbc:mysql://localhost:3306/pet_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的密码 driver-class-name: com.mysql.cj.jdbc.Driver还有一个 MySQL8 特有的坑allowPublicKeyRetrieval在某些连接工具和驱动版本下不设置会报Public Key Retrieval is not allowed错误。这不是大问题加进 URL 后面就好。如果你在导入数据库脚本时发现中文乱码检查一下 Schema 的默认字符集是不是utf8mb4MySQL8 建库脚本里高频出现这个问题。4.2 Vue3 开发环境跨域与生产环境路由前端开发环境调用后端接口时会遇到经典的跨域问题页面跑在localhost:5173接口在localhost:8080浏览器默认会拦截跨域请求。正确的处理方式不是在后端加CrossOrigin全局放开这样生产环境也会放开有安全风险而是在 Vite 配置里做代理// vite.config.js server: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样前端请求/api/pet/list时开发服务器会把请求转发到后端而且请求头里的 Host 也会被改写成后端地址浏览器感知不到跨域的存在。后端只需要在拦截器里做身份校验不必操心跨域逻辑。另一个容易踩的坑是 Vue3 路由的 history 模式上线后刷新 404。原因是history模式依赖浏览器的 History API页面刷新时浏览器会真的去请求/pet/detail/1这个路径而后端没有对应的路由就会返回 404。解决办法是在 Nginx 配置里加一条 rewrite 规则把所有路径都指到index.htmllocation / { try_files $uri $uri/ /index.html; }如果你是后端工程师第一次配前端上线时十有八九会漏这一条记得在部署清单里把它写进去。4.3 Linux 服务器部署的常规流程与资源规划项目部署到 Linux 服务器时通常流程是这样本地mvn clean package打成 jar 包用 FTP 工具传到服务器前端npm run build生成dist目录传到 Nginx 的html目录。后端用nohup java -jar pet-mall.jar logs/pet-mall.log 21 启动注意一定要加nohup ... 否则关掉终端进程就结束了。资源规划方面一台 2 核 4G 的云服务器跑这套商城完全够用。但要注意 JVM 参数不要直接用默认配置建议启动时加上-Xms512m -Xmx1024m防止堆内存占用过高导致服务器 OOM。如果访问量大可以再加一个 Nginx 做静态资源缓存——商品图片这种不常变动的资源设置expires 30d能显著降低后端负载。5. 这套代码的扩展方向从毕业设计到能上线的商用项目当你把项目跑通、理解了核心链路之后下一步就是如何把它改造成真正可商用的系统。下面这几个扩展方向是按优先级排的每个方向我都能给出具体的改造思路。5.1 接入第三方支付与订单对账目前项目里的支付多半是模拟操作只把订单状态改成已支付。要跑真实业务就要接入第三方支付平台的接口。核心改动有两点一是下单接口生成收款参数后需要生成支付二维码二是需要接收支付结果回调通知接口在回调里校验签名并修改订单状态。这里最大的坑是回调接口必须是幂等的——同一笔支付结果可能被通知多次如果每次通知都执行一次状态修改可能出现重复发货。套路是在回调处理里先查订单当前状态如果已经是已支付就直接返回成功不再重复修改。5.2 宠物档案与寄养/预约服务宠物商城和普通电商的差异点最终都会落在宠物服务上。比如宠物购买后需要免疫档案、驱虫提醒寄养服务要预约时间、选择寄养房间、上传日常照片。这些在项目里没有现成实现但表结构完全可以扩展增加宠物档案表关联用户和宠物商品、预约订单表关联订单和寄养套餐、服务记录表每次寄养生成一条。前端加两个页面即可后端逻辑和现有的订单模块非常相似复用率很高。5.3 拼团、优惠券与营销模块营销玩法是商城持续运营的灵魂也是当前项目最值得扩展的地方。优惠券模块至少要覆盖发券注册送、满减送、领券用户手动领取、核销下单时校验可用券、过期清理定时任务扫过期券。拼团则需要额外一张拼团活动表和拼团参与表下单时记录成团状态成团后再把订单状态流转到待发货。这一块涉及的并发逻辑要小心可以有意识地学习一下高并发下的库存扣减和幂等设计。5.4 数据统计可视化后台管理端的看板目前如果是简单的列表统计可以考虑引入更丰富的图表展示。用 Vue3 生态的图标库绘制销量折线图、分类占比环形图界面会更专业。数据来源上可以写一个定时任务每天凌晨把前一天的订单汇总写入统计表这样看板查询不需要实时聚合大表性能也稳。这套代码的完整度在同体量项目里算是名列前茅的从数据库脚本到后台接口再到前端页面全链路都是闭环的。我个人的建议是不要只把它当成要做的东西直接交作业而是逐层吃透——先按前两节的内容梳理业务模块再对着第三节的位置点开代码看具体实现最后自己动手把购物车或订单模块重写一遍。踩过几次坑之后你就会发现以后做任何电商项目脑子里都有一张清晰的业务-技术映射图。最后再分享一个小技巧改动数据库表结构之前一定要先备份一份原始数据脚本我在扩展优惠券模块时改错过一张订单表多亏有备份才没有把演示数据弄丢。
返回列表