ARTICLE DETAIL

资讯详情

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

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0母婴商城系统开发实战

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0母婴商城系统开发实战 说来有点不好意思这个母婴商城项目最开始并不是我的主动选择而是一个朋友让我帮忙救火的活。但真正做下来之后我发现这套SpringBoot2 Vue3 MyBatis-Plus MySQL8.0的组合在中小型电商系统里确实非常有代表性。它技术栈足够主流既不是老掉牙的SSH框架也没盲目上微服务那一套业务复杂度又刚好能把电商的典型难点都覆盖到——商品SKU、购物车、订单、库存、权限一个不少。所以我干脆把整个项目重新梳理了一遍补全文档整理成一套可以复用的源码。这篇博文就把我在开发过程中的核心决策、踩过的坑、以及那些文档里不会写的细节一次性说清楚。1. 技术选型思考——这套组合到底解决了什么实际问题很多人在拿到这类商城项目时第一反应是随便找个框架套上去就行。但真正动手之后才发现技术选型决定了后面三个月的开发体验。我先说说为什么最终定下这套组合而不是其他方案。1.1 为什么是SpringBoot2而不是SpringCloud或其他刚接手时我也纠结过要不要直接上SpringCloud Alibaba那套微服务想了三天否决了。原因很简单商城系统的核心痛点从来不是服务拆分不明显而是业务逻辑怎么组织更清晰。母婴商城的用户量在初期通常也就是百万级以下单体服务加缓存完全可以扛住。SpringBoot2在这时候的优势非常明显自动配置极大减少了XML配置量内嵌Tomcat让部署变成一个jar包的事而且它的生态成熟度在2024年的今天依然可以用恐怖来形容——你踩过的坑别人基本都踩过了。不过有一个细节很多人会忽略SpringBoot选2.7.x还是3.x我在这上面反复横跳过。SpringBoot3基于JDK17和Jakarta EE问题是很多老项目的公共依赖还没完全跟进尤其是有些公司内部的运维脚本还在跑JDK8。最终选了SpringBoot2.7.x上可兼容JDK8和11下可方便部署在云服务器上搭配MyBatis-Plus的3.5.x版本完全没有兼容性焦虑。1.2 Vue3 Vite Pinia为什么前端必须换血母婴商城的后端逻辑再完善用户最终接触的还是界面。一开始我想用Vue2 Element UI快速出活毕竟这方面的现成方案太多了。但考虑这个商城以后要做小程序端的复用Vue3的组合式API在逻辑复用上比Vue2的Options API强太多而且Vite的冷启动速度比Webpack快得不是一星半点。Vue3这边我用了Vite来初始化项目配合vue-router和Pinia。说到Pinia它是Vuex的替代品API简洁了很多。比如购物车状态管理用Vuex写要定义state、getters、mutations、actions四个文件而Pinia只需要定义一个Store函数TypeScript类型推导也非常顺滑。这个选择在后期维护时省了特别多心智负担。1.3 MySQL8.0在订单与库存场景下的可靠性MySQL8.0其实发布好几年了但还有不少团队因为稳定性理由停留在5.7。我的实际体验是8.0的InnoDB引擎在并发控制和崩溃恢复上明显比5.7精细特别是默认的utf8mb4字符集、窗口函数、原子DDL这些对于一个电商系统来说非常关键。订单表在用户下单高峰期会产生大量写入MySQL8.0的redo log优化能减少不少磁盘I/O。另外8.0的降序索引对查询最新订单这类场景极其友好。2. 数据库设计——母婴商品不是简单的分类规格数据库是整个商城的底盘。这块如果没设计好后面写业务代码时各种别扭。我在这里投入的时间大概占整个项目的15%但回报是后面CRUD写得非常顺。2.1 母婴商品SKU的特殊之处普通服装电商的SKU无非是颜色尺码但母婴商品要复杂得多。以纸尿裤为例它有品牌、系列、尺码NB/S/M/L/XL、片数包装每个组合就是一个SKU奶粉更是要区分段数1段、2段、3段、产地国产/进口、规格400g/800g。所以我的数据库设计采用了经典的SPU-SKU模型product_spu表存商品公共信息如标题、主图、品牌ID、分类ID、描述。product_sku表存每个具体规格的价格、库存、SKU编码、规格属性JSON。product_attr表用键值对的方式存规格名和规格值动静态属性都兼容。规格属性和查询条件的对应关系我用了类似规格名:规格值拼接存储的冗余策略这样在搜索页做筛选时只需要like匹配冗余字段而不需要操作复杂的JSON解析查询速度上去了代码也简单。2.2 用MyBatis-Plus写通用CRUD的边界MyBatis-Plus的BaseMapper把单表CRUD封装得非常完善不过它的能力边界我要说清楚。简单场景下selectById、selectPage、updateById这几个方法就够了几乎不用写SQL。但一旦涉及订单表联查商品表、用户表或者需要根据多个动态条件统计销售额我就主动写Select注解的SQL或者用Wrapper构造器。分享一下我写复杂查询的经验先用QueryWrapper把静态条件拼好动态条件用condition布尔参数控制。例如后台商品列表的筛选LambdaQueryWrapperProductSpu wrapper new LambdaQueryWrapper(); wrapper.eq(StringUtils.hasText(brandId), ProductSpu::getBrandId, brandId) .like(StringUtils.hasText(keyword), ProductSpu::getTitle, keyword) .eq(status ! null, ProductSpu::getStatus, status) .orderByDesc(ProductSpu::getCreateTime);这种方法的好处是代码可读性强也避免了字符串拼接SQL的注入风险。注意LambdaQueryWrapper不是万能的如果查询条件特别复杂比如存在子查询、Union、复杂的Case When还是老老实实写XML映射文件。2.3 订单表结构设计的几个冷门细节订单模块有几个很容易被忽略的设计点订单号不能用自增ID直接展示给用户一是怕暴露销量二是自增ID在分库分表场景会冲突。我用的是时间戳 用户ID后四位 随机数的组合生成之后加唯一索引避免并发场景下的重复。金额字段一律用DECIMAL(10,2)绝不用FLOAT或DOUBLE。浮点数在金额比较时经常出幺蛾子比如0.10.2 ! 0.3这种订单金额算错了可就是事故了。状态流转需要用一个order_status字段0待支付、1已支付/待发货、2已发货、3已完成、4已取消、5退款中并且用一张order_log表记录所有状态变更。后面如果运营需要排查用户说付了款但商家没收到单这张日志表能让你少吵很多架。3. 后端代码架构——包结构设计比写代码更重要一套清晰的后端代码架构能让三个后端开发同时在一个工程里开发而不互相踩脚。总结我这次项目的包结构设计思路。3.1 Maven多模块还是单模块起初我建的是多模块工程mall-common、mall-system、mall-product、mall-order。但后来发现这个购物商城项目的体量并不需要拆得那么细多个模块之间的依赖管理反而拖慢开发速度。于是转为单模块但严格分包。最终结构非常清晰com.mall.shop ├── common // 通用工具、全局异常、统一返回结果 ├── config // Spring配置如跨域、拦截器、MyBatisPlus分页插件 ├── controller // 接口层 ├── service // 业务层 │ └── impl ├── mapper // MyBatis-Plus的Mapper层 ├── entity // 数据库实体 ├── dto // 入参出参对象 ├── vo // 视图对象 ├── utils // JWT、日期工具等 └── security // 认证与授权相关一个原则Controller只做参数接收和结果封装所有业务逻辑都放到Service层。这样如果需要加缓存、加消息队列不会动到接口定义。3.2 统一返回结果和全局异常处理前后端分离的项目必须有统一的响应结构。我定义的格式是{ code: 200, message: success, data: {} }所有Controller的方法都返回这个R对象前端根据code判断业务是否成功而不是依赖HTTP Status Code。这个设计初期看起来多写了几行代码但联调时特别省事因为前端只需要封装一个request函数拦截器里统一处理code即可。全局异常这块我用RestControllerAdvice配合ExceptionHandler把业务异常比如库存不足、未登录、参数校验异常、系统异常分门别类处理保证抛出到前端的错误信息是友好、明确的避免出现一长串堆栈信息。3.3 JWT登录鉴权的真实落地方式母婴商城有用户端和管理员端权限粒度其实不一样。用户端登录后可以访问自己的订单、购物车管理员端要能管理商品、订单甚至查看用户列表。我的做法不是简单地在拦截器里判断有没有token而是用Spring拦截器配合JWT做双端鉴权用户端使用/api/user/**前缀管理员使用/api/admin/**前缀。登录成功后签发tokentoken里包含用户ID、角色代号、过期时间。拦截器解析token将用户信息放入ThreadLocal方便Service层随时获取当前登录用户。实际开发中踩过一个很大的坑JWT天然无状态但如果用户被禁用了已经签发的token在过期前依然有效。我的解决方案是在Redis里维护一份用户token黑名单或者用户状态缓存每次请求时检查一下虽然多了一次Redis查询但换来了可控性。4. 前端Vue3商城开发——从搭环境到核心功能实现前端这部分我用的技术栈是Vite Vue3 Pinia Element Plus Axios。整个开发过程踩了不少环境配置的坑我挑几个最有代表性的写一下。4.1 Vite创建Vue3项目与配置用Vite创建项目很简单一行命令的事npm create vitelatest mall-web -- --template vue但初学或者刚从Vue2转来的同学经常卡在环境变量上。Vite的环境变量必须以VITE_开头才会暴露给客户端我在.env.development里配置了VITE_API_BASE_URL/api部署到测试环境后才发现问题——没有以VITE_开头的变量在打包时是拿不到的。另一个要注意的点是Vite的代理配置。开发时前端启动在5173端口后端接口在8080直接请求必然跨域。我在vite.config.js里配置了proxy代理server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true, rewrite: (path) path.replace(/^\/api/, ) } } }这样开发环境下的请求路径写/api/user/login实际上是转成了http://localhost:8080/user/login完美绕开跨域。4.2 购物车状态管理Pinia的优雅实现Vue3里用Pinia管理购物车状态非常顺手。我声明了一个cartStore核心状态是items数组每个item包含skuId、商品名、单价、数量、选中状态。一个关键操作是加入购物车时数量累加。如果用纯前端状态刷新页面数据就丢了所以我做了两个层面的设计未登录时购物车数据存在localStorage登录后同步到后端接口。这里用computed做总价和总数量就非常合适const totalPrice computed(() cart.items.filter(item item.checked) .reduce((sum, item) sum item.price * item.count, 0) );每次操作购物车时只要修改items的数据totalPrice会自动更新不需要手动在每次操作后调用计算函数。4.3 商品搜索筛选让用户找得到才是真功能母婴商城的商品类目非常多用户搜索时绝不能只靠一个模糊匹配。我做了两级筛选分类维度大类奶粉、纸尿裤、洗护、玩具 - 子类 - 品牌。排序维度综合排序、销量优先、价格从低到高、价格从高到低。前端搜索页在用户点击筛选条件时用Vue Router的query参数来保存当前筛选状态。这样用户从商品详情页返回列表页时浏览器返回操作可以保留筛选条件但这需要给列表页组件设置keepAlive否则页面刷新会丢失状态这是我调试时发现的体验短板加一个KeepAlive就解决了。5. 环境部署与踩坑实录——环境配置比写代码更费头发这部分值得单独拿出来写。很多新手系统源码拿到手之后卡住的往往不是代码本身而是环境跑不起来。我在MySQL8.0、Tomcat部署、Nginx反代这三个环节踩了特别多坑整理出来就是白花花的经验。5.1 Docker安装MySQL8.0与Navicat连接避坑我需要在本机和服务器上各部署一套MySQL8.0。本机最容易的方式是用Dockerdocker run --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD123456 \ -e TZAsia/Shanghai \ -v /mydata/mysql/data:/var/lib/mysql \ -d mysql:8.0看起来很简单吧但有几个隐藏的坑第一时区问题。MySQL8.0默认时区是UTC连接后执行SELECT NOW()得到的时间比北京时间晚8小时。我的解决办法是在Docker启动时加-e TZAsia/Shanghai并在连接串里强制指定参数。第二Navicat连接报2059 - Authentication plugin caching_sha2_password。这是因为MySQL8.0默认认证插件改成了caching_sha2_password而老版本Navicat不支持。解决办法是修改用户的认证插件ALTER USER root% IDENTIFIED WITH mysql_native_password BY 123456; FLUSH PRIVILEGES;或者升级Navicat到16.x版本以上新版本已经支持这个插件了。5.2 SpringBoot连接MySQL8.0的驱动配置网上很多老教程用的是com.mysql.jdbc.Driver这在MySQL8.0下已经不行了驱动类名改成了com.mysql.cj.jdbc.Driver。数据库连接串也必须带上SSL和时区参数否则会报警告甚至连接失败spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/mall?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue username: root password: 123456allowPublicKeyRetrievaltrue这个参数很多人不知道如果不加使用caching_sha2_password认证时可能报Public Key Retrieval is not allowed。如果你是用root账号并且改为mysql_native_password这个问题不会出现但用默认认证方式就必须加。5.3 前后端联调跨域不解决项目跑不起来后端接口写的没问题前端页面也写的没问题但一联调就崩溃90%的可能是CORS跨域没处理。我在SpringBoot后端直接写了跨域配置类实现WebMvcConfigurer接口Configuration public class CorsConfig implements WebMvcConfigurer { Override public void addCorsMappings(CorsRegistry registry) { registry.addMapping(/**) .allowedOriginPatterns(*) .allowedMethods(GET, POST, PUT, DELETE, OPTIONS) .allowedHeaders(*) .allowCredentials(true) .maxAge(3600); } }这里有一个细节要特别注意allowCredentials(true)时allowedOriginPatterns不能写成*必须用具体的域名或allowedOriginPatterns(*)否则浏览器会拦截。我一度因为这个配置浪费了半天时间以为是代码写错了结果是这个隐蔽的问题。5.4 部署到服务器Nginx托管前端 jar包跑后端生产环境的经典部署方式是前端打包后让Nginx托管后端SpringBoot以jar包方式运行。前端打包命令是npm run build产物在dist目录。Nginx的关键配置server { listen 80; server_name yourdomain.com; # 前端静态资源 location / { root /opt/mall/dist; index index.html; try_files $uri $uri/ /index.html; } # 后端API反向代理 location /api/ { proxy_pass http://127.0.0.1:8080/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }try_files那行非常关键否则Vue Router使用history模式时刷新子路由页面比如/product/123会出现404。有了try_filesNginx会尝试找对应的静态文件找不到就回退到index.html再由前端路由接管。后端jar包启动我建议用nohup java -jar mall.jar --spring.profiles.activeprod log.out 21 。如果服务器内存比较紧张可以参考我用的JVM参数java -Xms256m -Xmx512m -jar mall.jar对于一个小型商城系统512M堆内存完全够用没必要上来就给2G毕竟云服务器每一点内存都是要钱的。6. 把ThreadLocal穿透到全局的隐形工具——这个细节让代码质量上了个台阶可能很多读者注意到了一个被我反复提到的小东西ThreadLocal。这是我在整个项目里最满意的一个设计值得好好说说。6.1 为什么Controller到Service的传递不该塞参数如果不做任何处理Service层要拿当前登录用户最常见的做法是Controller把userId当参数传进去。但这个做法会导致业务方法的签名非常臃肿而且如果一个Service方法内部又调用了别的Service方法层层传递非常难看。我的做法是在JWT拦截器里解析token并塞入ThreadLocal然后封装一个SecurityUtils.getCurrentUser()静态方法。这样无论在哪一层只要想获取当前登录用户直接调用这个方法即可public class SecurityUtils { private static final ThreadLocalLoginUser HOLDER new ThreadLocal(); public static void setUser(LoginUser user) { HOLDER.set(user); } public static LoginUser getCurrentUser() { return HOLDER.get(); } public static void clear() { HOLDER.remove(); } }特别注意一定要在接口处理完之后做清理否则Tomcat的线程池会复用线程导致下一个请求读到上一个用户的登录信息这是严重的越权隐患。我在拦截器的afterCompletion方法里调用SecurityUtils.clear()。6.2 MyBatis-Plus的自动填充也依赖这个机制我用MyBatis-Plus的MetaObjectHandler实现createTime、updateTime、createBy字段的自动填充。其中createBy就是通过SecurityUtils获取当前登录用户的用户名写入的。这样所有业务表都带了谁创建的审计信息排查问题时非常有帮助。6.3 压测和生产环境下的坑ThreadLocal用在高并发场景下没坑有。我第一次压测时就发现某些请求在并发高时偶尔会把用户A的数据返回给用户B。查了很久才发现是异常请求在抛出业务异常时afterCompletion没被正确执行导致ThreadLocal没清理。后来我改成在try-finally里做清理并且加了一层自检如果当前线程的ThreadLocal里还有残留用户且与当前token不一致强制清空并重新写入。7. 几种常见坑的排查链路——我把定位过程完整拆给你看这个章节是对整个开发过程中最典型的三个问题进行一次复盘把从现象到根因的完整链路写出来。建议读者在自己遇到类似问题时按这个思路排查。7.1 问题一MySQL连接时报Unknown database但库明明存在现象重启SpringBoot项目后日志报Unknown database mall。排查链路先用SHOW DATABASES;确认数据库列表发现mall库确实在。再用USE mall;手动切换报错Access denied for user rootlocalhost。这才醒悟问题出在用户权限上——Docker里MySQL8.0的root账号虽然可以从任意主机连接但mall库只授权给了指定用户。用GRANT ALL PRIVILEGES ON mall.* TO root%;修复。根因不是数据库不存在而是当前用户对数据库没有操作权限。这种问题最迷惑人因为现象非常像库不存在但仔细排查发现是权限边界的问题。7.2 问题二前端请求接口报402、403或504现象登录接口能通但商品列表接口返回403有时候又是504。排查链路先在浏览器Network面板看具体报错403说明是权限问题504说明是超时。登录能通说明CORS本身基本没配置错。再把请求头和后端拦截器的逻辑对照发现商品列表接口除了登录token还要求在请求头带上X-User-Type参数而前端Axios拦截器里没有默认传这个头。补上之后403解决。504则是由于数据库索引没建好全表扫描导致接口超过Nginx默认的代理超时时间。根因同一个问题往往不是同一个原因403看权限504看性能/超时配置。排错时一定要从响应状态码出发不要凭感觉把所有问题都归结到跨域。7.3 问题三SpringBoot包扫描不到Mapper接口现象项目能正常启动但一调用任何操作数据库的接口就报Invalid bound statement。排查链路看错误信息是MyBatis找不到Mapper的Statement。检查Mapper接口上是否加了Mapper注解以确认接口被Spring容器扫描。检查启动类上有没有MapperScan(com.mall.shop.mapper)如果两个都不加接口永远扫不到。确认pom.xml里MyBatis-Plus和MySQL驱动的依赖是否都引入了。根因我这次是多个模块分包时把Mapper接口放在了com.mall.shop.admin.mapper而启动类的MapperScan只扫了com.mall.shop.mapper导致扫描路径不匹配。这个问题的坑在于项目启动时不报错只有实际调用接口时才暴露第一印象很像SQL写错了。8. 项目打包和文档写作——让源码真正能落地一个只堆放代码、没有文档的源码项目对使用者来说其实是个灾难。既然标题里写了含文档我就把文档的整理思路也说一下。8.1 文档应该包含哪几部分我的项目文档主要分四块环境准备列出JDK8/11、MySQL8.0、Node14、Maven3.6的安装方式。快速启动分成前端和后端两部分从clone代码到浏览器能访问全程不超过20分钟。项目结构说明把每个包、每个核心文件的功能说清楚尤其是数据库初始化SQL脚本放在哪个目录。部署上线包含Nginx配置、jar包启动命令、常见问题FAQ。8.2 数据库初始化SQL的一个大坑很多源码项目会在SQL脚本里带着测试数据这是好事。但我遇到过一个情况脚本里只包含建表语句没有任何初始数据导致后台登录时找不到管理员账号。所以我的脚本做了双层设计mall.sql包含表结构和基础字典数据mall_data.sql里则封装了一整套演示商品、测试订单、模拟用户并且文档里专门说明了如何清理测试数据但保留表结构的方法DELETE FROM order_item; DELETE FROM order_master; DELETE FROM user_address; DELETE FROM cart; DELETE FROM product_sku; DELETE FROM product_spu;以及如何重置自增IDALTER TABLE product_spu AUTO_INCREMENT 1;8.3 从源码到二次开发还需要注意什么如果你拿到这套源码准备做二次开发我有两个额外建议第一替换默认密钥。项目里的JWT密钥、加密salt目前是写死在配置里的上线前一定要换成你自己的随机字符串否则有心人可以伪造token。第二理解业务再改代码。母婴商城业务中比较有行业特色的部分是保质期管理和批次追溯。如果只是做演示项目这部分可以不管但如果要真实运营建议在原有表结构上增加batch_number和expire_date字段这方面MyBatis-Plus的updateById天然支持新增字段扩展成本很低。第三日志配置不要省略。在logback-spring.xml里我把日志按天滚动保留了最近30天的日志并区分了info和error级别的输出文件。排查线上问题时没有日志等于盲人摸象。9. 我在这个项目里学到的最重要一课项目做完之后复盘最大的感受是技术栈只是工具业务模型的合理性才是商城的灵魂。举个很简单的例子母婴商品里纸尿裤NB码和奶粉1段都有比较严格的适用年龄区间很多家长下单前会犹豫尺码/段数选错了怎么办。如果不允许退换口碑会坏如果随意退换商家又容易亏。我在订单里加了售后类型的枚举字段退货、换货、仅退款并且预留了reject_reason和audit_status字段方便后续做售后的半自动审核。另一个感受是前后端分离的项目接口文档要走在开发前面。我之前吃过亏后端已经把接口写好了前端等了三天没有页面联调时才发现前端要的数据结构跟后端返回的完全不一致。后来我在项目初始化时用Apifox把核心接口的入参出参先定好前后端并行开发时各做各的配合效率提升了不止一个档次。最后再分享一个很多人忽略的小技巧在Controller层的返回值上不要直接用实体类。我所有返回前端的对象都定义成VO哪怕VO和实体类字段一模一样也要单独建类。这样做的好处是当数据库字段需要调整时比如新增了一个internal_remark内部备注字段不用担心这个字段意外暴露给前端。这个习惯帮我挡住了好几次潜在的信息泄露风险。如果你正在做或者准备做一个电商类的Java Web项目这套基于SpringBoot2 Vue3 MyBatis-Plus MySQL8.0的母婴商城源码可以作为一个非常完整的参考。把它跑起来、读懂每一层的作用然后再动刀改业务这条路比从零开始自己搭要高效得多。
返回列表