☰
SpringBoot+Vue前后端分离图书电商实战:从架构到部署完整方案
2026/10/5 2:52:51 网站建设 项目流程

在博客后台收到不少读者的留言,问得最多的一个问题就是:前后端分离的项目到底应该怎么从零搭出一个能完整运行的系统?很多人跟着教程跑通过SpringBoot的HelloWorld,也用Vue做过几个单页练习,但当要把SpringBoot+Vue+MyBatis+MySQL组合起来,做成一个包含用户、商品、购物车、订单这些闭环功能的网站时,就开始不知道怎么下手了。这个图书电子商务网站系统就是为这个问题准备的——以图书电商为业务载体,采用前后端分离架构,把图书展示、分类检索、登录注册、购物车、订单流程、后台管理等模块全部跑通,同时附带完整源码和部署文档,无论是做毕业设计、课程设计,还是想系统理解前后端分离项目实战的同学,都可以拿它当一份可复现的参考工程。

我需要提前说明的是,这个项目不套现成的快速开发平台,也不用过于复杂的微服务架构,就是用最主流、面试最常问的那套技术栈,老老实实地把每一层代码写清楚。正因为技术选型足够经典,你从这个项目里学到的东西,几乎可以直接迁移到其他管理系统、电商系统甚至企业级后台中。

1. 项目全景:一个前后端分离的图书电商到底包含哪些模块

1.1 为什么选“图书”作为电商系统的业务载体

有人可能觉得,做电商系统为什么不选服装、数码这类更贴近日常的商品?我的经验是,图书这个品类在业务建模时有一个天然优势:商品属性相对固定,没有复杂的SKU维度。一套服装会有颜色、尺码多个规格组合,光SKU表就要单独设计;而图书只需要书名、作者、出版社、价格、库存这几个核心字段,非常适合用来先把电商主流程跑通。

但图书电商的业务链路并不简单——它依然覆盖了电商系统的核心闭环:注册登录、商品浏览、关键词检索、分类筛选、购物车、生成订单、订单管理、后台维护。用最少的建模成本,把电商最重要的交易主链路完整走一遍,这就是选择图书项目的原因。

1.2 整体架构与一次请求的完整走向

前后端分离的核心就是:前端负责页面渲染和交互,后端只提供数据接口,两者通过HTTP+JSON通信,互不关心对方的实现细节。

在这个架构下,一次用户下单的请求大概会走这样一条链路:

  1. 用户在Vue页面上点击“提交订单”,前端把订单数据组装成JSON对象;
  2. Axios库把请求发送到后端的Controller接口地址;
  3. SpringBoot的Controller接收参数后,调用Service层处理业务逻辑;
  4. Service层通过MyBatis的Mapper接口操作MySQL数据库;
  5. 数据结果逐层返回,最终由前端渲染成“下单成功”页面。

这样说起来很抽象,我建议你在阅读源码时,随便找一个功能点从后端Controller入口追踪到Mapper层的SQL,再从前端对应页面的点击事件追踪到API调用处,把请求链完全对齐一次。做过这个动作之后,前后端分离的模式就不再是概念,而是你脑子里一条清晰的路。

1.3 功能模块与页面端对照

整个系统按用户角色分为前台用户端和后台管理端。功能对应关系如下表,方便你对照源码找位置。

功能模块前端页面后端核心接口涉及数据表
用户注册登录Login.vue / Register.vue/api/user/login、/api/user/registeruser
图书展示与分类BookList.vue / BookDetail.vue/api/book/list、/api/book/detailcategory、book
关键词检索BookList.vue内搜索框/api/book/searchbook
购物车管理Cart.vue/api/cart/add、/api/cart/list、/api/cart/updatecart_item
确认下单Checkout.vue/api/order/createorder、order_item
我的订单OrderList.vue / OrderDetail.vue/api/order/list、/api/order/detailorder、order_item
后台图书管理AdminBook.vue/api/admin/book/save、/api/admin/book/deletebook、category
后台订单处理AdminOrder.vue/api/admin/order/updateStatusorder
后台数据概览Dashboard.vue/api/admin/statsorder、book

模块不多,但每个都是电商系统的核心组成部分。做完这套,再去接触更复杂的电商项目,你会发现本质上都是这些模块的扩展和拆分。

2. 技术选型取舍:为什么偏偏是SpringBoot+Vue+MyBatis+MySQL这套组合

2.1 后端选型:SpringBoot负责简化,MyBatis负责SQL可控

现在Java后端做Web项目,SpringBoot几乎已经是默认选择。它最核心的价值在于自动配置和约定大于配置,原本SpringMVC时代需要手动配置的一大堆Bean,在SpringBoot里通过Starter依赖自动装配完成,开发者只需要关注业务代码。但如果只有SpringBoot还不够,ORM层的选择才是真正影响开发体验的。

MyBatis和Spring Data JPA的对比,是这个项目里最值得说的一点。我见过很多初学者在这两个选择之间纠结,直接说结论:

对比维度MyBatisSpring Data JPA
SQL控制力手写SQL,完全可控框架自动生成,复杂查询难优化
学习曲线需要理解XML映射和动态SQL概念抽象,入门容易精通难
复杂多表查询适合写定制SQL多表关系一多就容易踩坑
数据库移植性SQL与具体数据库绑定更灵活但性能优化受限
面试考点密度缓存、动态SQL、TypeHandler等高频相对较少

图书电商这个项目里有不少多表关联查询和动态条件拼接的场景,例如图书列表按分类、价格区间、关键词组合筛选。用MyBatis写动态SQL非常顺手,直接用<where>、<if>标签拼接即可,SQL的执行计划也能自己掌控,出了问题可以直接把日志里的SQL复制到数据库客户端执行验证。如果你的目标是求职,MyBatis本身也是目前国内企业使用率很高的持久层框架,相关面试题在简历里写上之后被问到的概率很大,做完这个项目你至少能对Mapper动态代理、缓存机制、TypeHandler这些概念有实际体感。

2.2 为什么不用现成的若依这类快速开发平台

很多读者会问:现在像若依这类前后端分离脚手架已经做得很成熟,权限、代码生成器都有,为什么不直接套一个?

我的看法是:工具项目和使用工具是两回事。若依这类框架确实能帮你快速生成CRUD页面,但如果你拿它做毕业设计,答辩时老师问你“用户登录后的权限是怎么实现的”,而你答不上来,反而会扣分。从零搭建这个项目,你被迫去理解每一个细节:登录为什么用JWT而不是Session、跨域请求怎么处理、购物车数据存前端还是后端、下单时如何保证库存不被超卖。

另外,直接套用成熟脚手架还有一个问题——冗余功能太多。图书电商本身业务体量不大,引入完整的部门管理、菜单权限、代码生成体系,会让代码量翻倍,也让你很难说清楚“每个类到底解决了什么问题”。自己搭一套轻量架构,每个文件都有明确职责,反而更容易讲清楚、更容易维护。

2.3 版本组合的推荐与“版本太高”的坑

技术选型定了之后,版本组合是另一个让人头疼的事。这里直接给出一套经过大量验证、能够顺利运行到底的组合,也是我整理源码时采用的版本:

  • JDK 1.8 或 11
  • Spring Boot 2.7.x
  • MyBatis 3.5.x + mybatis-spring-boot-starter 2.3.x
  • MySQL 5.7 或 8.0.x
  • Node.js 16.x(Vue CLI项目对Node版本兼容性最好)
  • Vue 2.7 + Vue Router 3.x + Vuex 3.x
  • Element UI 2.15.x

为什么特别强调版本?因为“SpringBoot版本太高”是所有新手的第一个坑——如果你用SpringBoot 3.x,JDK必须升级到17,javax.*命名空间变成jakarta.*,mybatis-spring-boot-starter的兼容性也需要重新匹配,很多旧教程的代码直接就无法运行了。Node高版本同样会带来node-sass编译失败之类的连锁问题。对于以学习为主要目的的项目,用成熟稳定的版本组合,远比追新版本更有价值。

前端方面,Vue 2.7 + Element UI的选型是因为这套组合的文档密度最大、坑最少。等你跑通整个项目,理解了组件、路由、状态管理的核心逻辑之后,再迁移到Vue 3 + Element Plus,你会发现概念高度重合,只是API写法变了。

3. 后端核心战场:表结构、鉴权方案与下单事务的细节

3.1 六张核心表的建模思路

数据库设计是整个项目的基座。这个系统一共六张核心表,设计逻辑一句话可以概括:围绕“用户买书”这条主线,把商品、购物车、订单串起来。

  • category分类表:字段为id、name、sort。三个字段足够,不用做得太复杂。
  • book图书表:id、category_id、title、author、publisher、cover、price、stock、sales、description、status。注意把price设为DECIMAL(10,2)而不是FLOAT,避免浮点精度问题;status字段用于上下架控制。
  • user用户表:id、username、password、email、role、create_time。role字段区分USER和ADMIN,密码用BCrypt加密后存储,不要明文。
  • cart_item购物车表:id、user_id、book_id、quantity。需要建一个(user_id, book_id)的联合唯一索引,保证同一个用户的同一本书只有一条记录。
  • order订单表:id、order_no、user_id、total_amount、status、receiver_name、receiver_phone、receiver_address,以及create_time、pay_time。order_no是业务单号,建议用时间戳加随机数生成。
  • order_item订单项表:id、order_id、book_id、book_title、price、quantity、amount。这里把下单时的商品快照信息(书名、价格)冗余存储,是为了防止商品后续改价影响历史订单展示。

这里有个值得思考的设计点:为什么订单项要冗余商品名称和价格,而不是通过book_id关联实时查询?原因很简单——商品信息是可变的。如果用户下单后管理员改了书名或者价格,历史订单再通过关联表查询时,看到的就是当前商品信息,而不是用户实际购买时的信息。电商系统里,订单属于交易快照,必须记录购买时刻的完整上下文。

3.2 登录鉴权:为什么用JWT而不是Session

前后端分离场景下,登录状态管理是一个绕不开的问题。传统的Session方案依赖于服务端和浏览器的会话Cookie,跨域请求时Cookie处理很麻烦,而且如果后续后端集群部署,Session同步又是一个问题。JWT(JSON Web Token)方案把登录状态直接编码进一个加密的Token字符串中,服务端不保存任何会话数据,天然适合前后端分离和水平扩展。

这个项目的JWT实现逻辑分成三段:

  1. 用户登录成功后,后端生成一个包含用户id、用户名、角色、过期时间的Token返回给前端;
  2. 前端拿到Token后存储在localStorage中,每次请求在请求头Authorization字段携带;
  3. 后端写一个拦截器,在请求进入Controller之前校验Token的合法性和有效期,解析出当前用户信息放入请求上下文。

具体实现上,用io.jsonwebtoken:jjwt库,核心代码大致是这样的逻辑:

public String generateToken(User user) { Date now = new Date(); Date expire = new Date(now.getTime() + 24 * 60 * 60 * 1000); return Jwts.builder() .setSubject(user.getUsername()) .claim("userId", user.getId()) .claim("role", user.getRole()) .setIssuedAt(now) .setExpiration(expire) .signWith(SignatureAlgorithm.HS256, secretKey) .compact(); }

拦截器校验的要点是:先放行登录、注册、图书列表这些公开接口,再拦截需要认证的接口。如果你不熟悉拦截器的注册方式,项目源码里的WebMvcConfigurer配置类已经写好了,建议自己动手把拦截的路径规则改一改,观察不同路径下Token缺失时的行为变化。

3.3 MyBatis层的工作方式与关键配置

MyBatis的启动流程可以从一个高频面试问题说起:MyBatis在启动时到底做了什么?简单说,SqlSessionFactoryBuilder会调用XMLConfigBuilder解析mybatis-config.xml配置文件,把数据源、事务管理器、Mapper映射文件全部加载,构建出Configuration对象。当我们调用Mapper接口方法时,其实是MyBatis通过JDK动态代理生成了一个MapperProxy代理对象,在代理逻辑中根据方法名找到对应的SQL语句并执行。

这个项目里,MyBatis配置文件有几个必须注意的细节:

<configuration> <settings> <setting name="mapUnderscoreToCamelCase" value="true"/> <setting name="cacheEnabled" value="false"/> </settings> <typeAliases> <package name="com.bookstore.entity"/> </typeAliases> </configuration>

mapUnderscoreToCamelCase开启后,数据库的create_time字段可以自动映射到实体的createTime属性,省去大量手写映射的麻烦。这也是新手最容易忽略的一步——我曾经见过有人因为没开这个开关,花了一个小时排查为什么查询出来的对象一堆字段为null。

关于缓存配置,MyBatis默认开启一级缓存(SqlSession级别),二级缓存默认关闭。在这个电商系统中,我建议显式关闭二级缓存,因为图书库存、订单状态这类数据强调实时一致性,引入二级缓存后会导致其他线程更新了数据库,本线程读取的却是旧缓存数据。面试时经常被问的“一级缓存和二级缓存区别”,这个项目就是最好的反例素材——知道什么场景不该用缓存,比知道怎么配置缓存更能体现水平。

3.4 下单流程的事务边界与库存超卖问题

订单创建是整个系统最核心、最容易出错的业务场景。一次下单涉及的操作包括:

  1. 校验商品是否存在且在售;
  2. 校验库存是否充足;
  3. 扣减库存;
  4. 创建订单主记录;
  5. 创建订单项明细;
  6. 清空用户购物车中已下单的商品。

这六步必须要么全部成功,要么全部失败,因此整个方法需要加@Transactional事务注解。但事务注解有几个典型的失效场景,这个项目里可以逐一验证:

  • 异常被捕获后不抛出:如果Service内部用try-catch把异常吞掉,事务拦截器感知不到异常,事务不会回滚;
  • 同类内部方法调用:同类中A方法调用B方法,@Transactional注解不生效,因为事务是通过代理对象拦截的,内部this调用不走代理;
  • 非public方法:事务注解只对public方法生效。

库存扣减的SQL写法也是重点。最朴素的写法是“先查询库存,再判断是否大于0,最后更新”,但这种写法在并发场景下必然出问题。正确的做法是把判断和扣减合并成一条原子SQL:

UPDATE book SET stock = stock - #{quantity} WHERE id = #{bookId} AND stock >= #{quantity}

通过stock >= #{quantity}条件,数据库在更新时自动判断库存是否足够,如果不满足则影响行数为0。在Service层根据返回的影响行数判断是否扣减成功,如果为0则抛出“库存不足”异常,回滚整个事务。这种乐观锁思路不依赖数据库锁,性能和正确性兼顾。

4. 前端工程组织:路由守卫、Axios封装与购物车状态设计

4.1 前端目录结构与工程初始化

后端接口设计得再好,前端页面组织混乱一样会让项目不可维护。这个项目的前端目录结构在设计时就保持了清晰的职责边界:

src/ ├── api/ // 按后端接口模块拆分的请求定义 │ ├── user.js │ ├── book.js │ ├── cart.js │ └── order.js ├── components/ // 通用组件 ├── router/ // 路由配置与动态路由 ├── store/ // Vuex状态管理 ├── utils/ // request.js等工具封装 ├── views/ // 页面组件 │ ├── Home.vue │ ├── BookList.vue │ ├── BookDetail.vue │ ├── Cart.vue │ ├── Login.vue │ ├── Register.vue │ └── admin/ └── App.vue

为什么要单独建api目录而不是在每个页面里直接写axios.get?原因有三:一是接口地址集中管理,后端修改路径时只需改一处;二是每个模块的请求参数和返回类型一目了然;三是便于统一处理拦截逻辑。很多初学者习惯在组件里随手写请求,页面一多,接口地址散落各处,维护起来非常痛苦。

前端环境配置方面,第一次npm install可能会遇到下载慢的问题,建议配置一下npm镜像。项目如果锁定了依赖版本,不要轻易全局升级依赖,Vue 2项目尤其不要手滑升级到Vue 3的大版本。

4.2 Axios封装:一次配置,全局生效

项目里utils/request.js是对Axios的二次封装,这也是前后端分离项目里几乎是标配的操作。它解决三个痛点:请求头统一注入Token、响应错误统一处理、接口地址统一前缀。

核心逻辑很简单:

import axios from 'axios' const service = axios.create({ baseURL: '/api', // 统一接口前缀 timeout: 10000 }) service.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = token } return config }) service.interceptors.response.use( response => { return response.data }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } return Promise.reject(error) } )

这里的关键设计是:Token注入和401处理都收口在拦截器里,页面代码完全感知不到这些事。如果没有这层封装,每个页面请求都要手动带请求头、手动判断登录过期,那将是灾难级的代码重复。

4.3 路由守卫与动态路由:区分用户和管理员

这个系统有普通用户和管理员两种角色,前端路由需要做访问控制。实现上分为两层:

  • 静态路由:登录、注册、图书列表、图书详情这类公开页面;
  • 动态路由:购物车、订单这类需要登录的页面,以及后台管理页面。

Vue Router的路由守卫在跳转前执行,核心逻辑是判断是否登录、是否具备访问目标路由的权限:

router.beforeEach((to, from, next) => { const token = localStorage.getItem('token') const role = localStorage.getItem('role') if (to.meta.requiresAuth && !token) { next('/login') } else if (to.meta.requiresAdmin && role !== 'ADMIN') { next('/') } else { next() } })

配合路由表的meta元信息标记权限,比在组件内部做判断清晰得多。这里也顺便提一下“路由动态添加”的思路——如果后台菜单权限复杂,可以在登录后根据用户角色动态调用router.addRoutes()添加路由,但本项目角色只有两种,静态配置配合守卫已经足够,不必引入更多复杂度。

4.4 购物车状态管理:为什么需要Vuex

购物车数据有这样一个特点:用户可能在图书列表页加购,在购物车页改数量,在下单页确认结算,几个页面共享同一份数据。如果用组件内部data存购物车,页面切换后数据就丢失了。Vuex把购物车数据放在全局状态中,任何组件都能读取和修改,而且修改是响应式的。

store/modules/cart.js里维护cartList、totalCount、totalPrice,加购、删减、清空都通过mutation修改状态。这里有一个不算常见但很实用的做法:购物车数据同时存在后端和Vuex中。首次进入页面时,从后端拉取购物车列表并初始化Vuex状态;后续加购、调整数量时,先调后端接口,成功后再更新Vuex。这样页面刷新后购物车不会丢失,且多个设备之间数据一致。

注意Vuex的mutation里不能执行异步操作,所以请求后端接口这类动作要放在action中,commit mutation只负责同步更新状态。很多初学者在这块容易绕晕,建议对照源码来理解action和mutation的分工。

4.5 图书列表的实用细节:搜索防抖、分页与自定义插槽

图书列表页有三个实用细节,代码里都已经实现,你可以重点看:

  • 搜索防抖:用户输入关键词时,如果每次按键都发请求,后端压力大且页面闪烁严重。通过lodash的debounce方法或自己写一个300毫秒的定时器,等用户停止输入后再发送搜索请求;
  • 分页参数:后端接口接收pageNum和pageSize,前端使用Element UI的Pagination组件,注意页码变化时同步更新URL参数,保证刷新页面后停留在当前页;
  • 表格插槽:后台图书管理表格中,状态列、图片列、操作按钮列需要自定义展示内容,Vue通过slot插槽机制实现。Element UI表格的列内写<template slot-scope="scope">,就能拿到当前行数据做定制渲染。

插槽是Vue高频考察点,它解决的是组件的“内容分发”问题,相当于在组件内部预留了一个填内容的坑位。在图书管理表格里,操作列根据当前行的上下架状态显示不同的按钮,就是插槽的典型应用。

5. 完整部署链路:从本地启动到Nginx上线的每一步

5.1 环境准备:先把地基打好

部署阶段最怕的就是环境不一致。我见过太多代码没问题、实际部署却起不来的案例,基本都是环境问题。这个项目建议按以下版本准备环境:

软件推荐版本说明
JDK1.8或11不要用17除非升级到SpringBoot3
Maven3.6+建议配置阿里云镜像加速
MySQL5.7或8.0注意utf8mb4字符集
Node.js16.x兼容Vue CLI项目
Nginx1.20+生产环境部署前端

IDEA中启动SpringBoot时,需要确认几件事:Project SDK指向正确的JDK版本;Maven配置的镜像能正常拉取依赖;启动配置中Program arguments可以指定--server.port=8088来修改后端端口。

MySQL安装过程中有两个高频问题需要提前知道。第一个是MySQL 8.0的默认字符集,建库时要显式指定:

CREATE DATABASE bookstore DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

utf8mb4比utf8多支持四字节字符,像emoji符号、生僻字都能存储。第二个是连接时可能遇到的SSL报错。MySQL 8.0默认开启SSL,如果本地开发没配置证书,JDBC连接串里要加上useSSL=false参数,或者在MySQL服务端关闭SSL相关配置。这个问题非常典型,只要用的是MySQL 8.0,几乎每个人都会遇到一次。

5.2 后端打包启动的完整步骤

后端启动我建议走“Maven打包后再运行”的方式,而不是直接在IDE里点击运行。这样更接近生产环境的行为,也能提前暴露打包阶段的问题。

cd backend mvn clean package -Dmaven.test.skip=true java -jar target/bookstore.jar --spring.profiles.active=dev

打包后的jar包可以放到任意有JDK环境的机器上运行,这也是SpringBoot“可执行jar”的特性。启动成功后,先访问一下接口地址确认服务正常。比如项目里配置了server.port=8088,那么浏览器访问http://localhost:8088/api/book/list应该能返回JSON数据。

多环境配置是这个项目部署中的一个亮点。application-dev.yml和application-prod.yml分别维护开发和生产环境的数据源配置,通过启动参数--spring.profiles.active切换。开发环境连本地数据库,生产环境连服务器数据库,代码不需要改动,只改配置文件即可。这看起来简单,但对刚接触项目的同学来说,是理解配置分离很好的范例。

5.3 前端构建与开发态跨域处理

前端联调时第一个拦路虎就是跨域。开发环境下,Vue CLI启动的开发服务器默认运行在http://localhost:8080,后端接口在http://localhost:8088,两个端口不同,浏览器会判定为跨域请求。

开发环境的解决方案是配置Vue CLI的devServer.proxy:

// vue.config.js module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://localhost:8088', changeOrigin: true } } } }

这样前端请求/api/book/list时,开发服务器会把它自动转发到http://localhost:8088/api/book/list,从后端返回的数据再由开发服务器传给浏览器,浏览器感知不到跨域的存在。baseURL在开发环境也正好设置为/api,与代理路径保持一致。

生产环境则不再需要Vue CLI代理,而是由Nginx统一处理静态资源和反向代理。

5.4 两种生产部署方案:Nginx反向代理与Jar内置前端

方案一:Nginx反向代理(推荐)

这是标准的前后端分离部署方式。前端构建产物是纯静态文件,由Nginx托管,后端是一个独立运行的Java进程。核心Nginx配置如下:

server { listen 80; server_name your-domain.com; # 前端静态资源 location / { root /opt/bookstore/dist; try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8088; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } }

两个关键点必须说清楚。第一,try_files $uri $uri/ /index.html这一行是为了解决前端路由在history模式下刷新404的问题——浏览器直接访问/book/3时,Nginx发现没有这个物理文件,就回退到index.html,再由前端路由接管。没有这一行,刷新页面必然404。第二,接口代理必须带proxy_set_header,否则后端取不到真实的客户端IP和Host信息。

方案二:前端dist放入SpringBoot

这个方案对应很多教程里“Vue打包放进SpringBoot”的做法。执行npm run build后,把dist目录下的文件复制到SpringBoot的src/main/resources/static/目录下,重新打包jar,前端页面和后端接口就由同一个Java进程对外提供服务,只需要开放一个端口。

这个方案的优点是部署简单、只需要管理一个进程,适合演示环境或个人项目。缺点是前端更新时要重新打包整个jar,且Nginx的静态文件高性能特性也享受不到。我的建议是:毕设答辩演示环境用方案二最省心,生产环境一定要用方案一。

5.5 前端项目构建命令与目录产物

前端部署前先执行构建,构建产物会输出到dist目录:

cd frontend npm install npm run build

构建成功后,dist目录下会有index.html和static(或assets)文件夹。把这个dist目录整体拷贝到服务器上,在Nginx配置中指向它即可。如果构建过程报node-sass相关的错误,通常是Node版本过高,降级到Node 16就能解决;也可以把依赖换成sass来规避node-sass编译问题。

6. 高频踩坑记录:版本兼容、SQL执行和前端404的修复过程

6.1 MySQL 8.0安装与连接踩坑实录

部署这套系统时,MySQL相关的坑是最折腾人的。不少人按网上教程安装完MySQL 8.0之后,后端启动一直报SSL connection error,控制台日志里写着Communications link failure。

这个问题的根因是MySQL 8.0默认启用了SSL加密连接,而客户端连接时没有配置合适的SSL参数。最简单稳妥的处理方式是在JDBC连接串后面加上:

jdbc:mysql://localhost:3306/bookstore?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8mb4

serverTimezone参数同样要留意,如果不指定时区,MySQL驱动会报The server time zone value异常。在部署文档里,我详细写了MySQL 8.0的安装配置步骤,包括安装后初始化密码、修改认证插件、创建账号授权等。特别提醒一点:MySQL 8.0默认使用caching_sha2_password认证插件,某些旧版本的Java驱动不支持这种认证方式,如果你的驱动版本比较老,需要在MySQL里把账号的认证插件改回mysql_native_password,否则会报Unable to load authentication plugin错误。

6.2 MyBatis的XML映射文件容易踩的细节

MyBatis上手容易,但XML映射文件里的细节坑不少。这个项目里特意保留了几个容易被问到的写法,你可以作为排查问题的对照样本。

第一个是动态SQL的标签使用。多条件查询时,如果用<if>标签拼接条件,很容易出现where关键字后直接跟and的情况。正确做法是用<where>标签包住所有条件,它会智能判断是否添加WHERE关键字,并在开头不需要时去掉多余的AND:

<select id="searchBooks" resultType="Book"> SELECT * FROM book <where> <if test="title != null and title != ''"> AND title LIKE CONCAT('%', #{title}, '%') </if> <if test="categoryId != null"> AND category_id = #{categoryId} </if> </where> ORDER BY id DESC </select>

第二个是特殊字符转义。在XML中,<和>是XML标签的边界符号,直接写stock < 5会报错。要用&lt;和&gt;代替,或者把整个条件包进<![CDATA[ ]]>中。

第三个是#{}和${}的区别。#{}是预编译占位符,MyBatis会把它转成?,参数通过PreparedStatement设置,天然防止SQL注入;${}是直接字符串拼接,执行的是拼接后的SQL。这个项目里所有用户传入的参数都用#{},包括排序字段——为什么排序字段反而不能用#{}?因为排序字段是会出现在SQL语句中的标识符,预编译占位符只能用于值,不能用于表名、列名这类结构。如果你确实需要动态排序字段,一定要在代码里做白名单校验,只允许指定的几个字段值进入SQL。

6.3 前端部署后刷新404:history模式的标准解法

有部署经验的人对这个坑再熟悉不过了。前端本地开发时一切正常,点击页面跳转也没问题,但部署到Nginx后,停留在某个子页面时刷新浏览器,直接白屏404。

原因是Vue Router默认使用history模式,路由URL是/book/3这种看似是文件路径的形式。浏览器刷新时,请求真实发到了Nginx,而Nginx在/opt/bookstore/dist目录下找不到名为book的目录,就返回404。解决方案就是在Nginx配置中添加:

location / { root /opt/bookstore/dist; try_files $uri $uri/ /index.html; }

try_files会依次尝试:请求的路径是否存在对应文件,是否存在对应目录,如果都不存在则回退到index.html,由前端路由接管。加上这一行后,刷新404问题立刻消失。

顺带提醒一个相关的小问题:如果使用方案二把前端放进SpringBoot的static目录,刷新404同样会出现。SpringBoot默认的静态资源处理方式对history模式不友好,需要额外配置转发规则,这也是我始终推荐用Nginx方案的原因——动静分离清晰,问题也好排查。

6.4 前后端联调错位问题:字段名与日期格式

联调时最消磨耐心的不是代码逻辑错误,而是前后端约定不一致。这个项目中曾经出现过的两类典型错位,值得提前规避。

第一类是字段名风格不一致。后端返回的JSON里是createTime(驼峰风格),而前端代码里写成create_time(下划线风格),结果页面上一片空白,控制台里全是undefined。解决方案是前后端约定统一风格——后端实体使用驼峰命名,JSON序列化默认输出驼峰字段;前端拿到数据后按驼峰访问即可。如果坚持要下划线风格,也可以在后端配置Jackson的PropertyNamingStrategy,但务必前后端保持一致。

第二类是日期格式不一致。Java后端LocalDateTime序列化默认输出2025-06-01 12:30:00这样的字符串,但某些情况下会输出一串毫秒时间戳。如果前端拿到时间戳直接展示,用户看到的就是1748752200000这种天书。解决方式是在后端配置Jackson的全局日期格式:

spring: jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai

这类问题看起来小,但在联调阶段会消耗大量时间。建议拿到后端接口文档的第一时间,先检查字段类型和日期格式是否和前端预期一致,把这个动作变成习惯。

6.5 事务、并发与分布式场景的现实提醒

最后说一个容易被初学者忽略但面试很高频的问题。这个项目在“下单扣库存”时采用stock >= #{quantity}的原子SQL方案,已经能解决单机环境下的并发超卖问题。但如果以后业务量增长,需要部署多台后端实例时,单纯依赖数据库的原子SQL仍然可能出现超卖——因为多个实例间的请求并发到达数据库,极端情况下会有大于实际库存的请求同时通过判断。

生产环境中更稳妥的方案是引入Redis做分布式锁,或者在数据库层面使用SELECT FOR UPDATE行锁。但这套图书电商作为学习和毕设项目,单机版本已经完全够用,不必强行引入分布式组件。理解当前方案的适用边界和不足,本身就是一种技术能力。面试时如果被问到“这个系统如果并发量大怎么办”,能够清楚说出当前方案的限制以及后续演进方向,会让面试官觉得你有真实项目思维,而不只是抄了一个demo。

最后再分享一点个人体会

整套项目从前端页面到后端接口再到部署上线,我自己完整搭一遍用了两个周末。第一个周末建表、写后端接口、用Postman把每个接口调到能通;第二个周末写前端页面、联调、修跨域和字段问题。最深的感受是:版本兼容问题比逻辑问题更耗时间,如果你照着部署文档走还会报错,九成是环境版本和文档不一致,而不是代码问题。

再分享一个排查问题的小技巧:后端启动后,把application.yml里的日志级别改成debug,MyBatis会打印每条SQL的执行语句和参数。前端联调时,打开浏览器开发者工具,查看每个请求的URL、请求头、响应体。前后端各看一层,大多数问题都能在十分钟内定位到具体位置。

这套系统的完整源码和部署文档我已经整理好了,需要对照学习的同学可以直接拿去跑。但我不建议你只是把代码下下来跑通就结束——那只能证明环境没问题。建议挑一个模块从数据库表、后端接口、前端页面三个层面自己重新写一遍,遇到问题再回来看源码。那种“卡住然后突然看懂”的体验,才是这个项目真正留给你的东西。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询