首页 / 资讯中心 / 文章详情

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0图书商城全栈实战

SpringBoot2+Vue3+MyBatis-Plus+MySQL8.0图书商城全栈实战 ★ FEATURED ARTICLE
先别急着下结论。图书商城这类Java Web项目网上流传的版本很多还绑在SSH、JSP那一套上这几年真正值得参考的其实是SpringBoot2Vue3MyBatis-PlusMySQL8.0这条全栈链路。我最近把一套图书电子商务网站系统从零到一完整搭了一遍前后台模块、订单流程、部署上线都走通了今天把整套落地思路和踩过的坑整理出来。文章会覆盖技术选型、数据库建模、后端接口组织、Vue3前端工程、联调排错和最终打包部署适合正在做相关毕设、想从JSP切换到前后端分离或者打算接一个小型商城项目的朋友参考。你不需要有高级框架经验但最好先懂一点Spring和Vue的基础。1. 技术栈取舍这套SpringBoot2Vue3MyBatis-PlusMySQL8.0组合是怎么定下来的1.1 后端选型为什么不是SpringBoot3第一次做选型时我确实犹豫过2026年了还在用SpringBoot2会不会太老。实际对比下来对这个场景SpringBoot2.7.x反而是更务实的选择。SpringBoot3强制要求JDK17起步而大量做毕设、做课设的同学本机装的还是JDK8学校的课程也基本按JDK8讲解。强行上SpringBoot3光是环境适配就能劝退一批人。再一个SpringBoot3把javax包整体迁移到了jakarta网上大量老教程、老代码直接粘贴会编译报错那些报错对新手来说很难定位。MyBatis-Plus对SpringBoot3的适配虽然现在已经很成熟但早期版本确实存在兼容问题折腾成本不低。所以我最终选了SpringBoot2.7.18搭配JDK8。图书商城这种业务场景核心就是CRUD、分页、事务、文件上传、鉴权没有任何需要SpringBoot3新特性才能解决的问题。用2.7.18起步依赖也干净parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent配套依赖主要就这几个spring-boot-starter-web、spring-boot-starter-validation、mybatis-plus-boot-starter3.5.x、mysql-connector-j8.x、jjwt、hutool。这套组合的优点是网上资料密度极高任何异常只要能描述清楚基本都能搜到现成答案对新手调试特别友好。1.2 前端选型Vue3真正提升效率的是组织方式Vue3从一开始就奔着解决Vue2大型项目逻辑复用困难去的。现在搜vue3商城vue3后台管理系统热度和教程数量都说明它已经是事实标准没必要再用Vue2做什么新项目。我选择Vue3不仅仅因为Composition API是新的写法而是这套组织方式会让业务逻辑可复用。比如图书商城的购物车逻辑加购、数量增减、勾选结算、总价计算放在Vue2里只能散落在各个组件的data和methods里页面一多就开始复制粘贴。在Vue3里我可以抽出一个useCart()组合式函数把购物车状态和操作全部收进去商品列表页、商品详情页、购物车页面共用同一套逻辑改一处全生效这才是真正的效率提升。配合Vite开发时的热更新非常跟手改完代码浏览器基本秒刷新。我在这篇文章后面会专门写Vite创建工程的具体步骤这里先不展开。对了组件库我选的是Element Plus后台管理页面里的表格、分页、弹窗、表单全是现成的做图书管理这种密集CRUD页面能省大量时间。1.3 ORM与数据库MyBatis-Plus解决的是重复劳动MyBatis-Plus的定位很清晰单表CRUD不用写SQL复杂查询仍然用XML。图书商城里用户表、图书表、购物车表、订单明细表绝大多数操作都是标准单表操作。继承BaseMapperT之后selectById、selectPage、insert、deleteById这些方法直接就有根本不用写mapper.xml。只有多表联查或者统计报表时才需要自己写SQL。MySQL8.0是数据库侧的默认选项但有几个和5.7明显不同的点必须提前处理。默认字符集是utf8mb4建库时要显式指定驱动类名从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver连接串要带serverTimezoneAsia/Shanghai和useSSLfalse。不处理这些启动时最常见的两个报错是时区错误和Public Key Retrieval is not allowed后面排错章节我会说具体原因。2. 功能边界与数据库建模先画清楚8张表再动手写代码2.1 功能清单拆解动手建表之前一定先把功能边界画清楚。图书商城这种项目功能多了做不完功能少了不像电商系统我最后圈定的是这个范围用户端注册、登录、退出登录图书列表支持关键词搜索、分类筛选、分页图书详情展示封面、作者、出版社、ISBN、库存、描述购物车加购、数量调整、勾选结算收货地址增删改下单时选择下单从购物车生成订单扣减库存订单列表和订单详情状态流转为待付款、待发货、待收货、已完成、已取消管理端图书管理增删改查、上架下架、库存调整、封面上传分类管理订单管理查看明细、修改订单状态用户管理列表、禁用/启用这套功能规模刚好覆盖了CRUD、多表关联、事务、文件上传、状态机流转既有电商系统的核心模型又不至于做成一个无比庞大的平台。做毕设或者练手这个量级是最合适的。2.2 核心表结构8张表解决主要业务我没有把表设计得很复杂核心就是8张表user、category、book、cart、address、orders、order_item。这里把几张关键表的结构列出来book图书表字段类型说明idbigint主键自增category_idbigint分类idtitlevarchar(100)书名authorvarchar(50)作者publishervarchar(100)出版社isbnvarchar(20)ISBN号可加唯一索引pricedecimal(10,2)价格stockint库存covervarchar(255)封面图片路径descriptiontext图书简介statustinyint1上架 0下架is_deletedtinyint逻辑删除orders订单主表字段类型说明idbigint主键order_novarchar(32)订单号唯一user_idbigint下单用户total_amountdecimal(10,2)订单总金额statustinyint0待付款 1待发货 2待收货 3已完成 4已取消receiver_namevarchar(50)收货人快照receiver_phonevarchar(20)收货电话快照receiver_addressvarchar(255)收货地址快照create_timedatetime下单时间pay_timedatetime支付时间order_item订单明细表字段类型说明idbigint主键order_idbigint订单主表idbook_idbigint图书idbook_titlevarchar(100)图书标题快照book_covervarchar(255)图书封面快照book_pricedecimal(10,2)下单时的单价quantityint购买数量订单为什么要拆主表和明细表因为一个订单可以包含多本图书主表存订单整体信息明细表逐条记录每件商品。如果不拆就只能把图书信息用逗号拼接塞进一个字段后续做销量统计、订单退款、对账的时候会非常痛苦这属于典型的数据库设计第一课。2.3 字段设计里容易忽略的细节状态字段直接用int存枚举值不要存中文。待发货已取消这些中文写到数据库里后续改文案要全表更新程序里判断也要用字符串比较非常别扭。正确做法是int存数值代码里用常量类或者枚举统一维护映射关系。金额必须用decimal(10,2)不要用double或float。Java侧用BigDecimal接收前端传金额时也按字符串处理否则浮点误差会在计算总价时暴露比如99.9加0.1得到100.00000000000001。图片存储存相对路径不要存base64。封面图上传后保存为/upload/book/20240601.jpg这样的路径数据库只存这个路径字符串。base64写进数据库会让表体积迅速膨胀而且查询的时候还要解码将来想换OSS存储只需要改上传组件的实现数据库完全不用动。用户和图书这类基础数据建议加is_deleted字段做逻辑删除配MyBatis-Plus的TableLogic注解。电商系统里误删数据是不可接受的逻辑删除能让数据真正可恢复。3. 后端落地统一响应、JWT鉴权与订单事务的组织方式3.1 工程分包与统一响应封装我习惯把SpringBoot工程分成这几个包com.example.bookstore ├── common // Result、异常码、全局异常处理 ├── config // MyBatis-Plus分页插件、拦截器注册 ├── controller ├── service │ └── impl ├── mapper ├── entity └── vo // 前端展示对象、登录返回token对象所有Controller统一返回ResultT对象包含code、message、data三个字段。前端拿到响应后只看codecode为0表示成功其他都是业务异常。配合RestControllerAdvice做全局异常处理库存不足抛一个BizException(库存不足)前端拿到的就是code40001和明确的中文提示。没有这层封装Spring MVC默认的异常返回格式五花八门前端根本没法统一处理。3.2 用户与鉴权模块JWT是这么接进来的登录注册是前后端分离项目里最容易出问题的模块。我的方案不是完整Spring Security而是HandlerInterceptor加JWT。图书商城这类中小型项目Spring Security配置重、过滤器链复杂反而容易把新手绕晕拦截器方案代码量小、逻辑直白完全够用。核心步骤是这样的注册时密码用BCryptPasswordEncoder加密存储数据库里绝不出现明文密码。登录成功后用用户id和角色生成JWT设置7天过期时间。写一个AuthInterceptor校验请求头里的token解析出userId放入ThreadLocal。注册拦截器时用excludePathPatterns把登录、注册、图书列表公开接口放行其他接口全部拦截。用ThreadLocal存用户信息有一个必须注意的细节请求结束之后一定要在afterCompletion里执行remove否则Tomcat线程池复用线程时下一个请求可能读到上一个用户的信息这是典型的上下文串数据问题。3.3 图书分页查询MyBatis-Plus的Page和条件构造器图书列表是标准的分页条件查询关键词模糊匹配、按分类筛选、上架状态、按时间排序。用LambdaQueryWrapper可以写得很干净LambdaQueryWrapperBook wrapper new LambdaQueryWrapper(); wrapper.eq(Book::getStatus, 1) .like(StringUtils.hasText(keyword), Book::getTitle, keyword) .eq(categoryId ! null, Book::getCategoryId, categoryId) .orderByDesc(Book::getCreateTime); PageBook page new Page(current, size); bookMapper.selectPage(page, wrapper);这里有一个关键点MyBatis-Plus默认没有装配分页拦截器。不注册MybatisPlusInterceptor的话selectPage返回的记录可能正常但total始终是0。很多新手在这一步卡很久后面排错章节我详细说配置。3.4 购物车、订单与事务边界下单流程怎么防超卖购物车表的核心设计就是user_id book_id加唯一索引同一用户重复加购同一本书时走数量累加逻辑而不是再插一条记录。下单流程的六步操作读取购物车里勾选的条目计算总金额并逐项校验库存生成orders主记录状态置为待付款批量生成order_item明细扣减图书库存删除购物车中已下单的条目这六步必须在一个事务里要么全部成功要么全部回滚。方法上直接加Transactional(rollbackFor Exception.class)。库存扣减这里要特别说一下。不要先select库存再update并发场景下两个请求同时读到库存为1都判断可以下单就会超卖。正确写法是一条SQL完成校验和扣减UPDATE book SET stock stock - #{num} WHERE id #{bookId} AND stock #{num}影响行数为0说明库存不足直接抛异常回滚事务。这相当于乐观锁的思路代码简单对这个量级的商城完全够用。4. 前端Vue3工程Vite、Pinia、路由守卫和接口层怎么搭4.1 用Vite创建Vue3项目别再用vue-cli现在网上搜vue3安装vue3教程大量新内容都已经切到Vite了。2026年开发vue3项目新建工程一律用Vitevue-cli基本可以退场了。我的实际创建命令npm create vuelatest bookstore-web # 按需选择 TypeScript、Vue Router、Pinia、ESLint、Prettier cd bookstore-web npm install这个初始化工具会生成一个规范的ViteVue3工程默认带上ESLint对保持代码风格很友好。写完页面逻辑主要写在script setup里比Vue2的options API直观很多。Element Plus的接入有两种方式。全量引入最简单适合项目小、组件用得杂的场景按需引入用unplugin-vue-components自动导入打包体积更小。我的建议是直接按需引入配置一次后面就不用管了。还有一个经常被忽略的点Element Plus默认语言是英文分页组件和日期组件会出现英文提示需要在App.vue外层包一个ConfigProvider并传入zh-cn语言包。4.2 Pinia管理用户状态和token持久化Vue3项目的状态管理直接选Pinia比Vuex概念少得多用起来几乎零心智负担。用户登录信息是一个典型需要全局管理的状态// stores/user.ts import { defineStore } from pinia export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(token) || , userInfo: JSON.parse(localStorage.getItem(userInfo) || null) }), actions: { setLogin(data) { this.token data.token this.userInfo data.userInfo localStorage.setItem(token, data.token) localStorage.setItem(userInfo, JSON.stringify(data.userInfo)) }, logout() { this.token this.userInfo null localStorage.removeItem(token) localStorage.removeItem(userInfo) } } })token为什么必须持久化到localStorage因为刷新浏览器后Pinia的store会重建内存里的数据全部消失。只有把token和用户信息放进localStorage或sessionStorage刷新后才能恢复登录态。这个细节不做每次刷新页面就退回登录页体验非常糟糕。4.3 Axios封装与拦截器接口层我习惯统一封装成一个request工具。核心是两层拦截器请求拦截器负责把store里的token塞进请求头service.interceptors.request.use(config { const userStore useUserStore() if (userStore.token) { config.headers.Authorization Bearer ${userStore.token} } return config })响应拦截器负责统一处理返回结果code为0时直接返回datacode为401时清空登录态并跳转登录页其他code用ElMessage弹出后端返回的错误信息。这样业务代码里不用每次都判断响应码写起来非常清爽const res await getBookList(params) res.list // 直接就是数据4.4 路由守卫与后台管理布局管理后台和用户端要分开我的做法是后台路由统一挂在/admin下路由meta里标记requiresAuthrouter.beforeEach((to) { const userStore useUserStore() if (to.meta.requiresAuth !userStore.token) { return { path: /login, query: { redirect: to.fullPath } } } return true })后台管理布局用Element Plus的经典组合左侧el-menu菜单、顶部tabs标签页、中间el-main内容区。很多人做vue3后台管理系统时纠结tabs标签页怎么动态增删核心思路是维护一个当前打开页面的响应式数组router.push时往数组里加新tab关闭tab时router.replace到数组里剩余的那个页面。这里建议直接用el-tabs默认结构不要硬改DOM很多样式问题的根源都是折腾底层结构。做图书管理表单时还有一个常见需求就是动态增删一行数据。比如一本书有多条描述或者多个标签Element Plus的form里用v-for遍历一个数组添加按钮push新对象删除按钮splice当前项数据模型和界面自然联动。5. 联调排错跑通图书商城里最值得记录的4个坑5.1 跨域用开发服务器转发配置解决前端跑在5173端口后端跑在8080端口直接发请求一定跨域。我推荐开发阶段用Vite的server配置做转发而不是到处写CrossOrigin// vite.config.ts server: { port: 5173, proxy: { /api: { target: http://localhost:8080, changeOrigin: true } } }这样做的好处是前端请求仍然保持同源浏览器不会拦截而且上线后把Nginx转发配置一配套代码完全不用改。要注意的是项目里不要把两种方式混用否则会出现明明加了CrossOrigin为什么还是报错这种特别难排查的诡异问题。5.2 MySQL8.0的连接细节时区、驱动、认证MySQL8.0在安装环节就有一个高频问题。不管是本机安装还是docker安装mysql8.0建库时都要显式指定字符集CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;连接串是这套技术栈里最容易出错的点我最终用的是spring: datasource: url: jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: xxx driver-class-name: com.mysql.cj.jdbc.DriverallowPublicKeyRetrievaltrue这个是MySQL8.0专门多出来的。因为MySQL8默认认证插件是caching_sha2_password客户端首次连接需要处理RSA公钥不加这个参数就会报Public Key Retrieval is not allowed。第一次遇到这个报错完全不知道在说什么加上这个参数后立刻就好。还有一个容易踩的坑是时区。docker安装mysql8.0的容器默认时区是UTC如果Java连接串里serverTimezone还写UTC查出来的时间会整体差8小时。稳妥做法是容器启动时挂载本地时区连接串一律写Asia/Shanghai。5.3 MyBatis-Plus分页不生效拦截器注册问题分页总条数恒为0这个问题根因几乎都是缺少分页拦截器。必须在配置类里注册Configuration public class MybatisPlusConfig { Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }还有一个隐蔽的坑如果项目里同时引了PageHelper两个分页组件会打架分页结果完全错乱。排查的时候先看启动日志里的拦截器链这套体系里只保留MyBatis-Plus的分页插件即可。另外自己在mapper.xml里写了limit的SQL不要再套selectPage否则会出现分页二重奏数据重复或丢失。5.4 Long类型主键传到前端精度丢失MySQL的bigint主键对应Java LongJackson默认序列化成JSON数字。但前端JS的Number类型能安全表示的整数范围只有2的53次方一旦id超过这个值前端拿到的id就已经变了。比如后端返回的id是10000000123456789前端实际收到的可能是10000000123456788导致编辑图书时永远编辑错的那一本。解决方法是在配置里把数字序列化成字符串spring: jackson: generator: write_numbers_as_strings: true或者给主键字段单独加JsonSerialize(using ToStringSerializer.class)。这个坑在前期开发不一定会暴露等到图书数量多了、id变大之后才出现排查起来很费时间提前处理掉最划算。6. 打包部署与配套文档把项目收成一个可交付的完整成果6.1 前端构建与Nginx配置前端做好之后先检查vite.config.ts里的base路径我的写法是用默认的/构建输出到dist目录build: { outDir: dist, assetsDir: assets }执行npm run build生成dist静态文件。这里的常见失误是接口路径写死打包后所有请求404。所以接口路径务必统一以/api开头交给Nginx转发到后端服务。关键的Nginx配置片段如下重点是SPA刷新404的问题server { listen 80; server_name localhost; location / { root /opt/bookstore-web/dist; index index.html; try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:8080/api/; } }try_files这一行是SPA前端路由的核心。用户直接刷新/admin路径时Nginx在dist目录里找不到对应文件就把请求交给index.html由Vue Router接管完成渲染。不写这一行刷新后台页面就是404这是SPA部署最常见的问题。6.2 后端jar包启动与外部配置后端打包命令很简单mvn clean package -DskipTests生成bookstore-0.0.1.jar后启动命令java -jar bookstore-0.0.1.jar --spring.profiles.activeprod数据库连接、上传目录这些配置要放到application-prod.yml里不要写死在代码或默认配置中。上传目录用类似upload.path/opt/bookstore/upload的外部配置指定然后通过WebMvcConfigurer把本地上传目录映射到/upload/**静态资源路径这样封面上传的图片才能正常访问。6.3 配套文档怎么写才算合格一个可交付的Java Web项目文档的价值比很多人想象的大。我的文档清单是固定的四件套README.md项目简介、技术栈、启动步骤、默认账号密码bookstore.sql完整数据库脚本包含表结构和种子数据至少要有几本书和一个管理员账号接口文档项目里集成knife4j自动生成导出一份markdown放到doc目录部署说明写明JDK版本、MySQL8.0版本、Nginx版本要求前端构建命令后端启动命令文档合格的标准只有一个一个完全没接触过这个项目的人照着文档能不能一步步把它跑起来。比如数据库脚本里有外键依赖就要把建表顺序写清楚不然还原脚本时报外键错误文档再详细也无用。整理完这套系统我最大的感触是图书商城这种经典Java Web项目价值不在技术多新而在整条链路能不能完整闭环。SpringBoot2Vue3MyBatis-PlusMySQL8.0的组合每个模块都有海量现成资料但把它们咬合在一起仍然需要反复调试。如果让我重新做一遍我会先花一天时间把表结构和接口约定彻底定死再让前端后端并行开发联调时间至少能省三分之一。最后分享一个建议仓库里一定要保留一份从一开始就能跑通的数据库备份很多朋友做着做着就把数据改乱了回头想重置又没有干净版本这种细节会直接决定项目能不能顺利交付。希望这份记录能帮你在同样的路上少走几个来回。
阅读完成 · 觉得有帮助?
咨询建站