ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

SpringBoot+Vue+Mybatis开源商城项目全栈实战与部署解析

SpringBoot+Vue+Mybatis开源商城项目全栈实战与部署解析 前阵子群里有人问想找一套能真正跑起来的全栈项目练手技术栈最好就是SpringBoot Vue Mybatis别整太新太偏的东西。我翻了翻自己收藏夹直接丢了这个开源商城过去。代码已经在GitHub上开源前端是Vue后端是SpringBoot持久层用Mybatis商品、购物车、订单、支付、后台管理等一套完整流程全都有。他照着部署文档跑通之后拿这套项目去面试居然真的把Offer聊到手了。这不是段子是最近真实发生的事。今天就把这个开源商城的技术选型、核心模块实现、本地跑通步骤以及我在实操中踩过的坑完整拆一遍给正在学这三件套、想找个完整项目练手的人做个参考。1. 项目整体设计与技术选型解析1.1 为什么是SpringBoot Vue Mybatis这套组合很多人一上来就问都2024年了为什么不用微服务为什么不用Mybatis-Plus为什么不用Vue3我先说实话这个项目选型没什么花哨的核心就两个字稳。SpringBoot是目前Java后端最主流的框架。它把Spring那套繁琐的XML配置全部干掉了改成自动装配和约定优于配置你只需要加依赖、写业务代码即可。对初学者来说SpringBoot把“能跑起来”这件事的门槛压得非常低这对建立信心很重要。Vue在国内前端圈的地位也不用多说组件化、响应式、生态丰富社区资料多到看不完。Vue2虽然已经是旧时代的东西但很多企业存量项目还在用学会Vue2再去迁Vue3其实成本不高所以这个项目里用Vue2 Element-UI做后台管理端我认为是完全合理的。Mybatis就更不用吹了。它跟SpringBoot配合非常丝滑SQL让你自己写灵活性拉满尤其是复杂的多表关联和报表统计你要是用JPA写那些动态SQL能把人憋死。Mybatis的XML里可以用动态SQL任意拼接该优化的时候还能直接手写LIMIT、FOR UPDATE这些拿捏数据库非常准。国内企业大面积使用Mybatis也侧面说明这个选型贴近真实生产环境。还有一个关键点这项目是单体架构不是微服务。我在实际操作中体会很深对于中小型电商系统来说单体足够用了。微服务化会引入注册中心、配置中心、网关、分布式事务等一系列复杂度对个人学习和中小团队来说完全是负担。先把一个单体做扎实再去看分布式那套理论顺序才符合认知规律。1.2 功能模块和数据库设计思路这套商城系统从功能上分大致有这几个模块用户认证、商品中心、购物车、订单中心、支付中心、优惠券以及后台管理。每个模块在数据库里的表都有清晰的前缀约定比如用户相关的叫ums_member、商品相关的叫pms_product、订单相关的叫oms_order。这个命名习惯参考了知名开源商城项目的管理风格好处是一眼能看出表属于哪个业务域避免几十张表堆在一起找不到方向。我来列一下核心表的设计逻辑表名模块核心字段设计意图pms_product商品id、brand_id、category_id、title、price、pic商品SPU表属性继承自分类pms_sku商品id、product_id、price、stock、spec商品SKU表具体规格库存价格oms_cart_item购物车id、member_id、product_id、sku_id、quantity冗余商品信息减少查询oms_order订单id、member_id、order_sn、total_amount、status订单主表状态用数字维护oms_order_item订单id、order_id、product_id、sku_id、product_name下单快照防止商品后续变动影响历史订单ums_member用户id、username、password、phone密码字段用BCrypt加密存储我自己拆到这套表结构的时候有个感受特别深设计表的人非常懂电商业务里的历史数据问题。比如订单明细里会把商品名称、单价、图片全部复制一份存下来这就是快照设计。为什么必须要快照因为商品价格和名称经常变你下单时买的是99块几个月后商品涨到129你订单记录里如果直接关联商品表商家后台看到的金额就变成129了这肯定出问题。所以在下单那一刻把商品关键信息固化下来是电商系统下最基础也最容易被新手忽略的设计。权限设计方面后台管理端用的是RBAC模型。角色和菜单关联用户在角色里通过中间表来维护多对多关系登录成功后根据角色查询菜单树返回给前端动态渲染。权限这块要用Mybatis做多表关联查询的场景也不少刚好能练到联表查询和动态SQL的能力。2. 核心链路实现从商品浏览到支付回调2.1 商品SPU/SKU与库存扣减电商系统里SPU和SKU这两个概念必须搞懂。SPU是商品抽象概念比如“iPhone 15”SKU是具体可购买单元比如“iPhone 15 黑色 128G”。商城的商品表只存通用属性规格、价格、库存都放到SKU表里。用户在前端选完规格后前端就能定位到具体SKU然后把这个SKU的ID传给后端。SKU表用Mybatis操作时不需要多复杂但库存扣减绝对是个容易翻车的点。我见过的初级写法是先把库存查出来判断是否大于要买的数再执行update。这种写法在单线程测试没问题但并发一上来就会超卖因为两步操作之间有间隙两个请求同时读到库存100同时减到99最终结果可能就把第100个商品卖出去了。正确的做法是用一条原子更新SQL把判断条件和扣减动作融在一起UPDATE pms_sku SET stock stock - #{quantity} WHERE id #{skuId} AND stock #{quantity}这样数据库行锁会保证同一时刻只有一个事务能更新成功返回受影响行数为0就说明库存不够了。我在这个开源项目里看到的就是这种写法很标准。还有一点购车表里存了sku_id和quantity但页面展示时需要商品的价格和图片。初级设计容易直接查SKU表拿价格但价格是时时变化的购物车页面显示的价格和下单时计算的最终价格要分开。购物车里只是一个预估展示真正锁定价格在下单接口里再重新计算一遍这样才不会出现用户看到100元、最后支付却是120元的情况。这个逻辑我踩过坑后来学乖了。2.2 订单状态机与超时关单订单模块是整个系统里逻辑最复杂的模块它不能只是CRUD。打开oms_order表你会看到status字段这个字段不是随便填的数字它对应一套完整的状态机待支付、已支付、已发货、已完成、已取消。为什么要用状态机而不是随便改状态因为订单每个操作都必须在合法状态下进行。比如待支付状态下才能取消订单已支付状态下才能发货已完成状态下不能再执行退款这些规则如果写散在代码各处很容易漏判。我在这个项目里看到他们把订单状态流转逻辑收敛到了Service层每个状态变更前都校验当前状态非法流转直接抛业务异常。虽然还没做到用状态模式那一套设计模式但对于学习来说这种朴素的判断已经能保证正确性了。超时关单是电商系统里躲不开的需求。用户在支付页面待了太久没付钱订单不能一直挂着不处理不然库存都被占着卖不出去。这个项目用的是定时任务扫描方案每60秒扫一次订单表找出创建时间超过30分钟且状态是待支付的订单把它们批量改成已取消。批量取消时还要做两件事一是把SKU库存加回去二是把用户下单时使用的优惠券释放。这里提醒一下定时关单一定要加状态条件更新比如这样UPDATE oms_order SET status 6 WHERE id #{orderId} AND status 0如果用户恰好正在支付而定时任务把订单取消了这条更新语句会因为status不匹配而更新不到数据。更新行数为0时再判断是否需要回滚库存和券避免重复执行。我在写类似功能时用这招避免过不少并发问题。2.3 支付流程与回调幂等处理商城系统到了支付环节就绕不开对接微信支付或者支付宝。流程其实不复杂用户在订单页发起支付后端调用支付平台的统一下单接口拿到支付二维码返回给前端前端展示二维码用户扫码支付完成后支付平台会往你配置的回调地址发一条POST通知。回调处理是这里的重中之重。有几个细节比业务本身更重要。第一是验签。支付平台的回调信息里有签名必须用官方SDK里的验证方法验一遍防止伪造回调。我在开源项目里的实现中看到他们把验签放在回调接口最前面验签失败直接返回给支付平台一个失败标识让它稍后重试这个顺序绝对正确。第二是幂等。支付回调可能因为网络抖动被支付平台重发好几次同一个订单的回调如果处理两遍订单状态就会被更新两次可能导致重复发货。解决思路很简单回调里先根据订单号查订单状态如果是已支付就直接返回成功标识不再重复处理。有些项目还会用数据库唯一索引来锁订单号双保险。第三是回调入参处理。回调数据是表单格式还是JSON格式不同支付平台不一样用SpringBoot接收时要注意Content-Type否则字段绑定不上。支付回调里的金额通常以“分”为单位跟前端展示的“元”不同换算关系容易出错。我见过有人在金额换算上丢了一分钱对不上账排查半天发现是类型精度问题。所以订单金额最好用BigDecimal别用double。3. 后端实操SpringBoot整合Mybatis的关键细节3.1 依赖配置和SpringBoot全局配置跑通这个商城的第一步是确保你的开发环境已经有JDK 1.8以上、Maven 3.6以上、MySQL 5.7或者8.0以及一个本地Redis。看代码仓库里的pom.xml核心依赖就几个spring-boot-starter-web、mybatis-spring-boot-starter、pagehelper-spring-boot-starter、mysql-connector-java、spring-boot-starter-data-redis以及JWT相关工具包。拿到源码后第一步不是急着读代码而是先看application.yml。我总结了这个项目里需要重点配置的几个点配置项示例值说明spring.datasource.urljdbc:mysql://localhost:3306/mall?useSSLfalseserverTimezoneAsia/Shanghai必须加serverTimezone否则MySQL 8会报时区错误spring.datasource.username / passwordroot / 你自己的密码记得改成你自己的别直接跑默认的mybatis.mapper-locationsclasspath:mapper/*.xml指定Mapper XML文件位置mybatis.configuration.map-underscore-to-camel-casetrue开启下划线自动转驼峰pagehelper.helper-dialectmysql指定分页插件方言有些同学刚拿到项目时容易忽略map-underscore-to-camel-case这个配置。如果不开启数据库表里的create_time字段就无法自动映射到实体类的createTime属性上查出来全是null看起来像数据丢了其实是映射没配对。开启之后Mybatis会在结果映射时自动把下划线风格的列名转换成驼峰风格的属性名省事很多。3.2 分页插件PageHelper的正确用法商城系统的商品列表、订单列表只要列表就分页。Mybatis里最常用的分页方案就是PageHelper这个项目也用了它。用法看起来很简单查询前调用一次PageHelper.startPage()然后写正常的Mapper查询PageHelper会自动在SQL后面拼接LIMIT。PageHelper.startPage(pageNum, pageSize); ListProduct products productMapper.selectList(categoryId); PageInfoProduct pageInfo new PageInfo(products);PageInfo里封装了总条数、总页数、当前页数据等前端拿过来直接渲染分页组件。使用时有几个坑我必须讲清楚不然分页会莫名其妙失效。第一个坑startPage之后只能跟一条查询语句遇到多条Mapper查询时后面的全都会加上LIMIT数据就乱了。第二个坑不要在循环里调用startPage这会导致每循环一次就改变页码数据结果完全不可控。第三个坑最好在Service层调用startPage而不是在Controller层调用避免查询参数还没处理好就创建了分页上下文。我实操中遇到过的最经典问题是自定义的统计类SQL包含子查询PageHelper自己生成的COUNT查询在复杂SQL下计数不对。解决办法是手写一个count查询语句配置到对应Mapper里PageHelper会自动优先使用你写的count语句。这个办法能救活一大批复杂分页报表。3.3 Mybatis缓存机制哪些地方真的适合开二级缓存热词里很多人搜Mybatis缓存我借这个项目多说一点。Mybatis有一级缓存和二级缓存。一级缓存是SqlSession级别的默认开启。同一个SqlSession里执行两次相同的查询第二次直接命中缓存不再查数据库。但要注意Spring和Mybatis整合后每次查询用的SqlSession是会被关闭和重建的所以一级缓存的效果非常有限尤其在开启了Spring事务后才能在一个事务里共享同一个SqlSession从而用到一级缓存。别指望它能在高并发下分担数据库压力。二级缓存是namespace级别的也就是每个Mapper一个缓存区域。在XML里加一行cache/就能开启。开启后同一个Mapper的查询结果会被缓存但有一个很大的坑缓存的数据在执行增删改时会被自动清空可是如果项目里有多个Mapper操作同一张表或者用到了多表联查缓存数据可能产生脏读。缓存理解起来很直观一级缓存像你手里的草稿纸用完了就扔二级缓存像你的笔记本换个Session还在Redis则是大家共用的共享文件柜多个应用节点都能访问。对于商城系统这种场景我的建议是不要轻易开Mybatis二级缓存而是把Redis缓存用在更高价值的场景上比如首页轮播、商品分类树、热门商品列表这些读多写少的数据。这个开源项目里商品详情接口就用了Redis缓存第一次查库后写入Redis后面直接查Redis缓存穿透、缓存雪崩的代码也处理了这点非常值得学。3.4 全局XSS过滤、上传PDF安全检查与统一异常处理热词里有一条“SpringBoot项目全局过滤器处理上传pdf文件时xss攻击”这个问题确实存在而且不少新手项目根本没管。用户提交的富文本内容里如果带script标签存到数据库再渲染到页面上就可能执行恶意脚本盗取Cookie。商城系统的商品评价、后台公告这种带文本输入的场景很容易中招。这里用全局过滤器处理。继承OncePerRequestFilter重写doFilterInternal方法核心逻辑是把请求参数包括JSON body里的HTML标签清洗掉。处理JSON body有一个坑如果直接调用request.getInputStream()去读bodyController层再读就什么都没有了因为输入流已被消费。解决办法是用一个包装类缓存请求body过滤器里解析、清洗再把清洗后的内容传给后面。如果你想用SpringBoot做PDF上传拦截只检查扩展名和Content-Type是远远不够的因为PDF文件可以内嵌脚本比如通过JavaScript触发的漏洞或者利用JBIG2编码的恶意内容。稳妥的做法是用PDFBox库把PDF内容提取出来检查是否包含Javascript对象或者可执行代码片段同时限制上传文件大小、设置存储路径可读写权限。这些都是我自己处理安全问题时常用的方案。除了XSS商城项目还需要一个全局异常处理。用RestControllerAdvice加ExceptionHandler就可以兜底把业务异常、参数校验异常、系统异常分别转成统一结构的JSON返回。这样Controller层就干净了不用每个方法都try-catch。我在这个项目里看到他们的Result对象里包含了code、message、data三个字段前端配合axios拦截器统一处理错误提示体验很好。事务管理上Transactional要注意自调用失效问题。如果类内部一个方法调用另一个带Transactional的方法事务注解是无效的因为Spring事务是基于AOP代理的内部调用不会经过代理。解决办法是拆到不同的Bean里调用或者通过ApplicationContext获取代理对象。如果在商城项目的订单服务里看到类似问题可以往这个方向排查。3.5 写Mapper XML时容易踩的细节这个项目的Mapper XML文件里有几个非常值得学习的点。一个是对#{}和${}的选择。#{}是预编译参数占位符最终变成?Mybatis会帮你加单引号并做转义能防SQL注入${}是字符串拼接直接把内容拼到SQL里有注入风险但某些场景必须用比如动态排序字段。写动态排序时如果兜不住用户传一个order by 1 desc进来整个查询就可能被拖慢甚至报错。正确做法是用白名单校验排序字段。还有Like查询的写法。我见过很多人直接写LIKE %${keyword}%这是又注入又不优雅。正确写法是SELECT * FROM product WHERE name LIKE CONCAT(%, #{keyword}, %)这样既能避免注入也能正常走预编译。另外动态SQL里用if判断时注意参数为数字时0的判断。if teststatus ! null and status ! 在status为0时是成立的但如果写成了status true之类的判断0会被当成false导致条件失效这种Bug极难排查。4. 前端实操Vue项目搭建与商城页面核心要点4.1 Vue开发环境配置与项目初始化很多新手卡在前端环境搭建这一步不是代码问题是工具链问题。这里我按常见的实操顺序梳理一遍。先装Node.js不要装最新版建议用LTS版本因为新版的npm和工具链偶尔会有兼容问题。装完在终端里执行node -v和npm -v能输出版本号就说明成功。接着设置npm镜像国内直接用官方源下载依赖慢到崩溃换成国内镜像会舒服很多。你可以用cnpm也可以在用户目录下建一个.npmrc文件把registry换成国内镜像地址效果一样。我个人的习惯是保留npm只改registry配置因为cnpm有时候对package-lock.json的处理有差异。依赖装完后用Vue CLI创建项目。执行vue create mall-admin选择默认预设或者手动勾选Router、Vuex等一两分钟脚手架就生成完了。然后npm install再npm run serve浏览器打开localhost:8080看到Vue的默认页面就说明环境没问题。如果要用VSCode写Vue代码我建议装的插件有VolarVue3语法高亮和类型提示Vue2项目用Vetur、ESLint代码规范检查、Prettier格式化。装好这些代码风格自动统一省心不少。4.2 前端路由参数传递与Axios封装商城项目里最典型的场景就是商品列表点击进入详情页需要把商品ID从列表页传到详情页。Vue Router有两种传参方式一定要分清。一种是params传参配合路由配置里定义动态路径参数// 路由配置 { path: /product/:id, component: ProductDetail } // 跳转方式 this.$router.push({ name: ProductDetail, params: { id: 123 } }) // 接收方式 this.$route.params.id另一种是query传参链接上会带问号参数this.$router.push({ path: /product, query: { id: 123 } }) // 接收 this.$route.query.id区别在哪里params传参如果配合name跳转刷新页面后参数容易丢失而query参数因为挂在URL上刷新后还能拿到。所以详情页的商品ID我强烈建议用path加query的方式传刷新不丢。接口请求这块商城前端项目一般会对axios做封装。核心工作有三件设置baseURL、请求拦截器里注入Token、响应拦截器里统一处理业务错误码和HTTP状态码。比如后端返回code等于500时拦截器直接弹出Message错误提示前端业务代码就不需要每个接口都重复写错误处理逻辑。这个封装在各后端项目里大同小异核心在于拦截器里不要做太重的同步逻辑否则容易影响请求响应时间。4.3 商品视频m3u8播放与调试工具链现在商城系统很多商品详情页会放视频m3u8格式很常见。Vue里播放m3u8我惯用的方案是video.js加videojs-http-streaming插件或者用hls.js。如果你不想引入太重的东西用hls.js简单处理if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(https://example.com/demo.m3u8); hls.attachMedia(videoElement); }hls.js的优势是能在不支持原生HLS的浏览器上通过MSE实现播放兼容性比原生video标签好很多。而且调试m3u8时F12面板里能看到hls.js的日志和分片请求定位源码是七牛云还是腾讯云很快就能分清。调试Vue项目Vue DevTools是必装工具。浏览器扩展装好后在开发模式下可以直接查看组件树、props、data还能在Vuex面板里看到状态变更记录。调试路由跳转时切换到Routing面板就能看到当前路由名称、路径和参数结合VueRouter的导航守卫断点定位问题效率翻倍。4.4 VSCode Vue 怎么打包成手机App热词里有条“vscode vue 怎么制作手机软件”我顺带说一句。如果商城做完H5之后想打包成App最简单的方式是用Capacitor或者uni-app。Capacitor可以把你的Vue项目打包成Android/iOS应用它在WebView里加载打包后的静态资源同时通过桥接层调用相机、推送、文件系统等原生能力。用起来就是在项目根目录初始化Capacitor把webDir指向前端构建输出目录然后编译Android工程。打包后要重点测的是移动端路由刷新问题。因为App里的WebView加载的是本地资源直接使用history模式的路由刷新可能会404。解决思路是改hash模式或者在原生层处理路由回退。商城这种项目本身不复杂用hash模式是最省心的方案。5. 本地跑起来完整部署流程与常见问题排查5.1 后端启动流程启动后端前先确认基础设施都准备好了。MySQL要建好数据库并执行项目里的init.sql脚本Redis要启动服务别漏了哨兵密码配置。然后打开application.yml把数据库用户名密码改成你自己的Redis连接信息也检查一下。确认没问题后在项目根目录执行mvn spring-boot:run或者用IDE直接运行主类。启动时如果报端口被占用最常见的是8080端口被之前某个服务占用。用netstat -ano | findstr 8080找到PID然后在任务管理器里结束进程就行。还有一点SpringBoot项目启动时的Banner就是控制台那一大段字符很多看不懂的地方其实是你自己配的banner文件别紧张。我用idea跑这个后端时踩过一个坑编译时Mapper XML文件没有复制到target目录启动后Mybatis一直报“Invalid bound statement (not found)”。原因是pom.xml里没有把src/main/resources下的xml文件打包进去。解决办法是在pom里加一个resources配置把mapper目录和xml包含进来。这个问题查了半小时最后看到target目录里没有xml文件才反应过来。5.2 前端启动流程与跨域处理前端项目拿到后先npm install装依赖再npm run serve启动。如果端口配置冲突在vue.config.js里改port就行。启动后如果页面能打开但接口请求全部失败基本就是跨域问题。前后端分离开发时前端跑的端口是8080或其它后端是8080或9090两个端口不一样浏览器就会因为同源策略拦截跨域请求。简单的做法是在前端配置devServer代理把/api开头的请求都转发到后端地址devServer: { proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }代理的好处是浏览器看到的前端地址就是请求地址没有跨域后端也不需要额外写CORS配置。生产环境的话用Nginx把/api反向代理到后端服务思路一模一样。5.3 高频问题与排查技巧速查表把我在实操中遇到过的问题整理成一张表方便你对照排查。现象原因解决方式分页查询结果越界或总页数为0PageHelper.startPage之后跟了多条SQL把startPage移到目标查询前或使用PageHelper.clearPage()实体类日期字段查出来为null没开启驼峰映射application.yml里加map-underscore-to-camel-casetrue数据库时间比本地晚了8小时连接串缺少时区参数url加serverTimezoneAsia/Shanghai上传图片后访问404静态资源路径没映射配置WebMvcConfigurer映射磁盘目录到虚拟路径npm install报node-sass错误Node版本和高版本node-sass不兼容升级到sass或docker或者切换Node版本前后端联调跨域开发环境无代理或后端CORS未配置配置devServer.proxy或全局CORS过滤器Mybatis报Invalid bound statementXML没有被打包或者namespace写错检查pom.xml打包配置核对mapper接口与XML namespace还有一个热词搜得比较多“怎么将SpringBoot jar反编译成项目”。这个在商业项目里偶尔也会用到比如只有生产jar没有源码要排查问题。用JD-GUI打开jar包能看到class文件的源码用CFR或者Procyon可以把class反编译成Java文件class文件里的注解和泛型大多能还原。要注意的是反编译只能用于学习或者合法授权的场景不要拿来搞别人的闭源代码。把lib目录下依赖的jar也反编译的话可以分析出每个类之间的关系不过代码可读性会比较差通常只用来应急定位问题。5.4 这个商城项目后续可以怎么扩展你要真把这套商城跑通了后面可以做的事情非常多。文件存储可以接入MinIO。现在商品图片存在本机磁盘搬到分布式环境就废了。MinIO是开源的跟SpringBoot整合也不难用SDK把文件传到MinIO里再配置Nginx做访问代理前端图片地址就变成http://你的域名/桶名/文件名了。这个改造过程能学到对象存储的设计思路。搜索可以引入Elasticsearch。商品表和SKU表MySQL查询性能很有限等数据量到几十万条关键词模糊搜索会越来越慢。把商品数据同步到ES用分词能力做全文检索响应时间可以从秒级降到毫秒。同步的话可以在商品维护接口里加逻辑也可以监听数据库binlog后者更贴近生产实践。秒杀可以加Redis Lua脚本。Redis的原子性操作天生适合做库存扣减配合Lua脚本可以把判断库存、扣减库存、记录用户等操作合并成原子操作。但这套方案做好也不简单需要处理超卖、限流、接口防刷刚好用于练习高并发场景。我个人认为从这套商城项目学到的最核心的东西是分层思维。后端Controller层只做参数接收Service层做业务逻辑Mapper层只做数据访问前端页面组件和API请求分离公共逻辑抽成工具类。只要这些分层做清晰了代码再多、功能再复杂也不至于混乱。最后分享一个小经验跑通这个项目后我建议你把它当成自己的代码重构一遍。第一步不改功能只把每个Controller、Service、Mapper的职责重新梳理一遍去除重复代码第二步尝试加一个自定义功能比如商品收藏第三步把订单模块改造成Redis缓存方案。这三步做完你对SpringBoot、Vue、Mybatis这套技术栈的理解会比单纯看教程强十倍。
返回列表