接手这个项目之前,我对“图书电商网站”的认知还停留在Spring MVC + JSP的时代。真正把SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套全栈组合落地跑通,才发现里面的坑和细节远比想象中多。如果你正准备做一个Java Web方向的实战项目,或者想把手里的老旧管理系统升级成前后端分离架构,这篇文章就是为你准备的——我会从系统设计、核心技术选型、数据库建模到前后端实现,把整个图书商城从零搭建的关键环节和踩坑实录都拆开讲清楚。
这套项目源码的结构很典型:前端是Vue3 + Vite + Element Plus,后端是SpringBoot2 + MyBatis-Plus,数据库用MySQL8.0,包含完整的用户登录注册、图书展示、购物车管理、订单处理、后台商品管理等模块,还附带详细的开发文档。对于刚学完SpringBoot和Vue基础、想找一个完整项目练手的人来说,它的技术栈足够主流,代码规模又不会大到让人劝退。接下来的内容,我会结合自己实际开发中的经验,把每个环节的设计思路和实现细节一项项过一遍。
1. 项目梳理与技术选型分析
1.1 为什么是SpringBoot2 + Vue3 + MyBatis-Plus + MySQL8.0这套组合
先聊聊技术选型。这套组合在2024到2026年之间依然是Java Web项目里最常见的一套配置,尤其是中小型系统和毕业设计、个人实战项目,几乎快成了标准答案。很多人问为什么不用SpringBoot3,原因很现实:SpringBoot3强制要求JDK17,而很多学校的教学环境、企业现有服务器还在用JDK8,SpringBoot2.x + JDK8的搭配依然拥有巨大的存量市场。更重要的是,市面上大量的教程、开源代码、面试题都围绕SpringBoot2展开,用这套技术栈踩坑时能搜到的资料最多,学习成本最低。
再来看前端。Vue3从2023年开始已经全面普及,官方默认推荐使用组合式API(Composition API)和<script setup>语法糖,配合Vite构建工具,开发体验比Vue2的Options API流畅不少。项目里用到的Pinia也是Vue3官方推荐的状态管理库,比Vuex更轻量,TypeScript支持也更友好。Element Plus作为Vue3对应的UI组件库,后台管理界面里的表格、表单、弹窗、菜单基本都能直接覆盖,不用自己去写复杂的样式逻辑。
至于MyBatis-Plus,它的核心价值在于把单表CRUD的样板代码几乎清零。以前用MyBatis写一个简单的用户查询,要写Mapper接口、XML文件、ResultMap映射,现在MyBatis-Plus直接用BaseMapper接口提供现成的selectById、selectList、insert等方法,配合LambdaQueryWrapper做条件查询,代码量至少减少一半。项目里图书列表的搜索、分页、条件筛选,基本都是用它来完成的。
MySQL8.0则是数据库层面的默认选项。8.0相比5.7有更好的性能和安全性,默认字符集是utf8mb4,对中文和Emoji的支持更完善,窗口函数、CTE这些新特性也让复杂SQL的写法更简洁。唯一需要注意的是驱动和连接配置有些变化,这个后面我会专门讲。
1.2 系统功能模块全景图
整个图书商城是一个典型的前后端分离电商系统,按角色可以划分成两个端:前台用户端和后台管理端。
前台用户端面向普通用户,核心流程是:注册登录 → 浏览图书 → 搜索筛选 → 加入购物车 → 结算下单 → 查看个人订单。这里面还包含图书详情展示、分类浏览、关键字搜索这些辅助功能。前台页面的设计重点是体验流畅,Vue3的响应式特性在图书列表的实时筛选、购物车数量联动这些场景下表现很直观。
后台管理端面向管理员,功能包括图书信息管理(新增、编辑、上下架、删除)、图书分类管理、订单处理(查看订单详情、修改订单状态)、用户管理、数据统计等。后台界面的重点是信息密度和操作效率,Element Plus的el-table加el-form组合可以快速搭建出规范的管理界面。
数据库层面,核心表就那几张:用户表(user)、图书表(book)、分类表(category)、购物车表(cart)、订单表(orders)、订单详情表(order_item)。这几张表之间的关系很清晰,用户和订单是一对多,订单和订单详情是一对多,图书和分类是多对一。后文我给出具体的建表SQL和字段说明。
1.3 项目目录结构与代码组织策略
一个容易让人看得头疼的项目,往往是目录结构混乱导致的。这套项目的目录组织按业界常见做法来划分,后端采用标准的分层架构:
com.example.bookstore ├── controller // 接口层,接收前端请求,返回统一响应 ├── service // 业务层,处理核心业务逻辑 │ └── impl // 业务实现类 ├── mapper // 数据访问层,继承BaseMapper接口 ├── entity // 实体类,对应数据库表结构 ├── common // 公共类:统一返回结果、异常处理、常量 ├── config // 配置类:跨域、拦截器、MyBatis-Plus配置 └── utils // 工具类:JWT工具、MD5加密等这种分层的好处是单向依赖,controller调service,service调mapper,每一层各司其职。前端结构同样按功能模块划分:
src ├── api // 存放所有请求接口的封装 ├── assets // 静态资源 ├── components // 通用组件 ├── router // 路由配置 ├── store // Pinia状态管理 ├── views // 页面组件 │ ├── admin // 后台管理页面 │ └── shop // 前台商城页面 └── utils // 工具函数(请求封装、Token处理等)这样的目录组织一眼就能看懂,后续想扩展功能模块时,直接按照对应目录往里面加代码就行,不需要在整个项目里到处找文件。
2. 数据库设计核心细节与MySQL8.0部署
2.1 MySQL8.0安装与Docker部署方案
数据库环境是很多人卡壳的第一步。MySQL8.0的安装方式主要有两种:本地直接安装和Docker容器化部署。本地安装适合日常学习和开发调试,最省心的做法是下载MySQL Installer,选择Server only,一路下一步。需要特别注意的是,8.0默认的认证插件是caching_sha2_password,如果你的客户端工具比较老(比如Navicat 15以前版本),可能会报Authentication plugin 'caching_sha2_password' cannot be loaded的错误。解决方法是修改用户的认证插件为mysql_native_password,命令如下:
ALTER USER 'root'@'localhost' IDENTIFIED WITH mysql_native_password BY '你的密码'; FLUSH PRIVILEGES;Docker部署则是另一种更干净的方式,特别适合想快速体验或者需要在多台机器间迁移环境的场景。安装Docker之后,一条命令就能拉取并启动MySQL8.0:
docker run -d \ --name mysql8 \ -p 3306:3306 \ -e MYSQL_ROOT_PASSWORD=123456 \ -e TZ=Asia/Shanghai \ -v /data/mysql:/var/lib/mysql \ mysql:8.0这条命令里几个参数值得解释一下:-p 3306:3306把容器内的3306端口映射到宿主机的3306,这样外部工具和SpringBoot才能连上;-e MYSQL_ROOT_PASSWORD指定root密码;-e TZ=Asia/Shanghai设置时区,避免后面出现数据库时间与本地时间相差8小时的问题;-v /data/mysql:/var/lib/mysql把容器内的数据目录挂载到宿主机,这样容器删除后数据也不会丢。
项目里实际使用时,我个人推荐用Docker方式,因为MySQL8.0在本机卸载再重装比较麻烦,用Docker随时可以docker ps查看状态、docker logs mysql8查看日志,排查问题效率高很多。
2.2 核心数据表结构与字段设计
数据库表设计是整个系统的地基,表结构建得合理,后面的业务逻辑就写得顺畅。这里给出几张关键表的建表SQL和设计思路。
用户表(user)
CREATE TABLE `user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用户ID', `username` varchar(50) NOT NULL COMMENT '用户名', `password` varchar(100) NOT NULL COMMENT '密码(加密后)', `nickname` varchar(50) DEFAULT NULL COMMENT '昵称', `avatar` varchar(255) DEFAULT NULL COMMENT '头像地址', `email` varchar(100) DEFAULT NULL COMMENT '邮箱', `phone` varchar(20) DEFAULT NULL COMMENT '手机号', `role` tinyint(4) NOT NULL DEFAULT '0' COMMENT '角色:0-用户 1-管理员', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-正常 0-禁用', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '注册时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), UNIQUE KEY `uk_username` (`username`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='用户表';字段里面有几个值得关注的点:role字段直接区分用户和管理员,避免了单独建角色表、权限表的复杂度,适合图书商城这种轻量级系统;password字段存储的是加密后的密文,不是明文,这个安全习惯一定要养成;status字段用于软禁用,删除用户这种操作尽量别用物理删除,否则订单表里关联的用户信息会变成脏数据。
图书表(book)
CREATE TABLE `book` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '图书ID', `category_id` bigint(20) NOT NULL COMMENT '分类ID', `title` varchar(200) NOT NULL COMMENT '书名', `author` varchar(100) DEFAULT NULL COMMENT '作者', `publisher` varchar(200) DEFAULT NULL COMMENT '出版社', `isbn` varchar(20) DEFAULT NULL COMMENT 'ISBN号', `price` decimal(10,2) NOT NULL COMMENT '价格', `stock` int(11) NOT NULL DEFAULT '0' COMMENT '库存', `sales` int(11) NOT NULL DEFAULT '0' COMMENT '销量', `cover` varchar(500) DEFAULT NULL COMMENT '封面图片地址', `description` text COMMENT '图书简介', `status` tinyint(4) NOT NULL DEFAULT '1' COMMENT '状态:1-上架 0-下架', `create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间', `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间', PRIMARY KEY (`id`), KEY `idx_category_id` (`category_id`), KEY `idx_title` (`title`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COMMENT='图书表';图书表是前台展示的核心数据源。price字段用decimal(10,2)而不是float或double,是因为浮点数在计算金额时会产生精度误差,这在任何涉及钱的系统里都是大忌。sales字段用来做销量排序和“热销榜”功能,前台可以直接按这个字段倒序排列。status字段控制上下架,下架的商品在前台检索时会被过滤掉。
购物车表和订单相关表
购物车表设计时有个关键决策:购物车数据存数据库还是存前端本地?我们选择存数据库,原因很简单——用户换设备登录时购物车数据不会丢,这是电商网站的基本要求。表结构包含user_id、book_id、quantity三个核心字段,再加一个联合唯一索引(user_id, book_id),避免同一用户把同一本书加入购物车多次。
订单表(orders)和订单详情表(order_item)是典型的主从表结构。订单表记录一次购买行为的总金额、下单时间、订单状态(待付款、已付款、已发货、已完成、已取消)、收货人信息;订单详情表记录这次订单里包含的每一本书、购买数量、下单时的价格。为什么价格要冗余到订单详情里?因为图书价格可能调整,如果不冗余存储,订单历史里看到的价格会跟着图书表一起变,这在电商系统里是不能接受的。
2.3 MyBatis-Plus与MySQL8.0的连接配置注意事项
有了数据库表结构,接下来就是让后端代码连上数据库。SpringBoot的application.yml里数据库连接配置这样写:
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bookstore?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true username: root password: 123456这里有三个关键点。第一,驱动类名必须是com.mysql.cj.jdbc.Driver,这是MySQL8.0的驱动类,老式的com.mysql.jdbc.Driver在8.0里已经废弃,写了会报警告甚至启动报错。第二,URL里一定要加serverTimezone=Asia/Shanghai,否则服务器和本地时区不一致会导致datetime字段读取时差8小时。第三,allowPublicKeyRetrieval=true这个参数很多人会漏掉,如果不加,用caching_sha2_password认证插件连接时可能会报Public Key Retrieval is not allowed错误。
MyBatis-Plus的配置在application.yml里主要关注两点:
mybatis-plus: configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl global-config: db-config: logic-delete-field: deleted logic-delete-value: 1 logic-not-delete-value: 0map-underscore-to-camel-case: true让数据库的snake_case字段名(如create_time)自动映射到Java实体的camelCase属性(createTime),这能省去大量@TableField注解。log-impl配置为StdOutImpl后,控制台会打印执行的SQL语句和参数,这在调试阶段排错非常有用,上线前记得关掉。
3. 后端关键模块实现:从登录认证到订单处理
3.1 SpringBoot2项目搭建与统一响应封装
后端工程创建的基础步骤是:在Spring Initializr里选择SpringBoot 2.7.x版本,依赖选好Spring Web、MySQL Driver、Lombok,生成工程后再手动引入MyBatis-Plus和JWT相关依赖。这一步我给个提醒:SpringBoot2.7.x对应的MyBatis-Plus版本用mybatis-plus-boot-starter的3.5.x版本,直接Maven引入即可,不需要额外配置分页插件以外的内容。
为了让前后端接口交互保持统一规范,所有接口的返回值都用统一响应体包装。定义如下:
@Data public class Result<T> { private Integer code; // 状态码:200成功,其他失败 private String message; // 提示信息 private T data; // 返回数据 public static <T> Result<T> success(T data) { Result<T> result = new Result<>(); result.setCode(200); result.setMessage("操作成功"); result.setData(data); return result; } public static <T> Result<T> error(String message) { Result<T> result = new Result<>(); result.setCode(500); result.setMessage(message); return result; } }统一响应体的好处是前端axios拦截器里只用判断code字段就能知道接口是否成功,不用每个接口各写一套成功失败判断逻辑。前端请求封装里配合响应拦截器,能实现全局错误提示和Token失效跳转登录页。
3.2 JWT登录认证机制与请求拦截
图书商城的认证方案采用JWT(JSON Web Token),它的核心思路是:用户登录成功后,服务端生成一个带签名和过期时间的Token返回给前端,前端在后续请求的请求头里带上这个Token,服务端通过拦截器验证Token的合法性,从而识别用户身份。
登录接口的核心逻辑是:
@Service public class UserServiceImpl implements UserService { @Autowired private UserMapper userMapper; @Override public Result<UserVO> login(String username, String password) { // 1. 根据用户名查询用户 LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>(); wrapper.eq(User::getUsername, username); User user = userMapper.selectOne(wrapper); // 2. 用户不存在或密码错误 if (user == null || !MD5Utils.encrypt(password).equals(user.getPassword())) { return Result.error("用户名或密码错误"); } // 3. 用户被禁用 if (user.getStatus() == 0) { return Result.error("账号已被禁用"); } // 4. 生成Token并返回用户信息 String token = JwtUtils.generateToken(user.getId(), user.getUsername(), user.getRole()); UserVO userVO = new UserVO(); BeanUtils.copyProperties(user, userVO); userVO.setToken(token); return Result.success(userVO); } }这里用LambdaQueryWrapper做条件查询的方式很常见,MyBatis-Plus提供的这种链式写法既安全又能避免手写SQL。登录逻辑里需要先校验用户是否存在、密码是否正确、状态是否正常,每一步失败都给出明确的错误提示——这是业务系统的基本素养。
密码加密这里用的是MD5加盐处理。严格来说MD5的安全性已经不足以应对现代安全需求,但在学习项目中仍然是教学常用的方式。如果你在做商业项目,建议换成BCrypt,Spring Security自带BCryptPasswordEncoder,安全性高很多。
Token拦截器的实现方式,是继承HandlerInterceptor,在preHandle方法里从请求头拿到Authorization字段,解析Token,失败则直接返回401错误码。然后在WebMvcConfigurer里注册拦截器,并配置放行路径——登录接口、注册接口、图书查询接口这些不需要认证的请求要放行,购物车、订单等需要登录的接口则必须拦截。这种前后端分离的认证模式是主流方案,理解它对你面试时讲项目帮助很大。
3.3 购物车与订单模块的并发与事务处理
购物车模块的核心操作是加入购物车。注意一个并发细节:用户重复点击“加入购物车”按钮时,如果库里还没有这条记录,先去查一次,没有再插入;如果两条请求同时查到“不存在”,就可能插入两条相同用户和相同图书的记录。解决方式就是前面设计表时提到的联合唯一索引(user_id, book_id),数据库层面兜底防止重复数据。代码里可以用saveOrUpdate方法,结合唯一索引的冲突处理,来保证最终结果正确。
订单模块是整个系统里最需要小心的地方,因为它涉及库存扣减和金额计算,操作不当会出现超卖问题。创建订单的核心流程是:
- 前端提交订单,携带购物车中选中的图书id列表和收货信息
- 后端遍历图书,逐一校验库存是否充足
- 扣减库存,更新销量
- 计算订单总金额
- 生成订单主表和订单详情表数据
- 删除购物车中对应的记录
这个过程必须加事务,用@Transactional注解保证要么全部成功,要么全部回滚。扣减库存的SQL要写成原子操作:
UPDATE book SET stock = stock - #{quantity} WHERE id = #{bookId} AND stock >= #{quantity}这里的关键在于WHERE条件里带上了stock >= #{quantity},如果库存不足,这条SQL影响行数为0,代码里检查这个结果就可以判定库存不足并抛出异常,从而避免超卖。这种写法比先查库存再扣减要安全得多,因为查询和扣减之间的事务隔离问题会导致并发下超卖。
3.4 MyBatis-Plus分页查询与条件筛选的实际应用
后台管理系统的图书列表页,最常见的需求是分页加多条件筛选。MyBatis-Plus的分页功能需要先配置分页插件:
@Configuration public class MybatisPlusConfig { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL)); return interceptor; } }分页查询的写法:
public Page<Book> getBookPage(int pageNum, int pageSize, String keyword, Long categoryId, Integer status) { Page<Book> page = new Page<>(pageNum, pageSize); LambdaQueryWrapper<Book> wrapper = new LambdaQueryWrapper<>(); // 关键字模糊查询书名和作者 if (StringUtils.hasText(keyword)) { wrapper.and(w -> w.like(Book::getTitle, keyword).or().like(Book::getAuthor, keyword)); } // 分类筛选 if (categoryId != null) { wrapper.eq(Book::getCategoryId, categoryId); } // 状态筛选 if (status != null) { wrapper.eq(Book::getStatus, status); } // 按创建时间倒序 wrapper.orderByDesc(Book::getCreateTime); return bookMapper.selectPage(page, wrapper); }这个查询里值得学的是LambdaQueryWrapper组合条件的写法。StringUtils.hasText()判断是否为空字符串,避免前端传空字符串过来干扰查询;wrapper.and()把书名like或作者like包成一组括号,避免和后面的分类、状态条件产生逻辑混乱。
4. 前端Vue3核心实现:从环境搭建到页面开发
4.1 Vue3项目搭建与工程化配置
前端工程用的是Vite构建工具,创建项目的命令是:
npm create vite@latest bookstore-web -- --template vue这个命令会生成一个Vue3 + Vite的基础工程。接下来安装项目依赖:
npm install npm install vue-router@4 pinia axios element-plus @element-plus/icons-vue安装依赖阶段比较容易踩坑的是版本冲突。建议直接安装最新稳定版本,Vue3项目如果出现运行时报错,多半是版本不兼容导致的。Element Plus的完整引入方式是在main.js里:
import { createApp } from 'vue' import ElementPlus from 'element-plus' import 'element-plus/dist/index.css' import zhCn from 'element-plus/es/locale/lang/zh-cn' import App from './App.vue' const app = createApp(App) app.use(ElementPlus, { locale: zhCn }) app.mount('#app')locale: zhCn这个配置很关键,特别是日期选择器、分页组件的文字显示,不配置中文包的话会显示英文。项目里还用到了路由懒加载,在router/index.js里把页面组件改为动态导入:
{ path: '/', name: 'BookList', component: () => import('../views/shop/BookList.vue') }这样配置的好处是首屏只加载首页需要的代码,不是把所有页面的代码一次性打包,打开速度会明显快很多。
4.2 组合式API与Pinia状态管理实战
Vue3的<script setup>语法让代码组织清晰了不少。以前Vue2里数据、计算属性、方法散落在不同区块,现在可以按业务维度聚在一起。拿购物车状态来举例,用Pinia管理的核心代码:
// store/cart.js import { defineStore } from 'pinia' export const useCartStore = defineStore('cart', { state: () => ({ cartItems: [], cartCount: 0 }), actions: { async fetchCartItems() { const res = await api.getCartItems() this.cartItems = res.data this.cartCount = res.data.reduce((sum, item) => sum + item.quantity, 0) }, async addToCart(bookId, quantity) { await api.addToCart({ bookId, quantity }) await this.fetchCartItems() }, async removeFromCart(cartId) { await api.removeFromCart(cartId) await this.fetchCartItems() } } })为什么购物车要用全局状态管理而不是在组件里本地维护?因为购物车的数据在头部导航栏、购物车页面、结算页面等多个组件里都要用到,如果每个组件自己拉接口、自己维护数据,就会出现数据不同步的问题。Pinia把数据集中在store里,任何组件改动数据后,其他组件通过响应式更新自动感知,逻辑清晰且不易出错。
cartCount这个计算属性在导航栏的商品图标上显示数量,用户点击加入购物车后调用addToCart,然后自动刷新cartItems和cartCount,图标和页面数据实时联动,这个交互体验是前端框架响应式能力的一个直观体现。
4.3 axios请求封装与前端鉴权逻辑
前端和后端交互统一走axios封装,核心文件在utils/request.js:
import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) // 请求拦截器:自动携带Token request.interceptors.request.use(config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = token } return config }) // 响应拦截器:统一处理返回值和错误 request.interceptors.response.use( response => { const res = response.data if (res.code === 200) { return res } // Token过期,跳转登录页 if (res.code === 401) { localStorage.removeItem('token') localStorage.removeItem('userInfo') router.push('/login') } ElMessage.error(res.message) return Promise.reject(new Error(res.message)) }, error => { ElMessage.error('网络请求异常,请稍后重试') return Promise.reject(error) } ) export default request拦截器是前端工程里提升效率的核心设计。有了这层封装,业务代码里调用接口时不需要关心Token怎么带上、报错怎么提示,只需专注业务逻辑。比如登录接口调用时只写request.post('/user/login', data),拦截器会自动处理其他所有事情。
开发环境下Vite还需要配置代理,把前端的/api开头的请求转发到后端服务地址。在vite.config.js里配置:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:8080', changeOrigin: true } } } })这里需要说明的是changeOrigin: true的作用:它会把请求头中的Host字段改为目标服务器地址,防止后端在域名校验时拒绝请求。
4.4 图书商城核心页面与交互设计
前台商城的核心页面是图书列表页。这个页面的关键交互是左侧分类菜单加右侧图书卡片列表,点击分类可以刷新图书列表,顶部搜索框可以按关键字搜索。图书卡片显示封面、书名、作者、价格,点击可以跳转详情页。价格显示有一个细节:价格字段来自后端返回的price(小数),前端展示时需要格式化为两位小数,可以直接用toFixed(2),或者封装成formatPrice函数统一处理。
图书详情页包含图书简介、作者信息、库存状态和“加入购物车”按钮。库存不足时按钮要禁用并显示“已售罄”,这个判断在页面渲染时做一次即可。加入购物车成功或者失败,都用Element Plus的ElMessage提示,给用户即时反馈,比弹窗更轻量。
后台管理的书籍列表页用el-table展示数据,配合分页组件el-pagination,每行操作列放“编辑”“删除”按钮。编辑弹窗用el-dialog加el-form实现,表单提交前做简单的必填校验。后台管理的代码风格要跟前台区分开,功能优先、界面简洁,把重点放在操作效率上。
5. 典型问题排查与实操避坑速查
5.1 MySQL8.0连接与驱动问题汇总
做这个项目时,数据库相关的问题出现频率最高。下面整理一张对照表,方便你遇到问题时直接查:
| 问题现象 | 根本原因 | 解决方案 |
|---|---|---|
启动报错java.sql.SQLException: Unknown system variable 'query_cache_size' | 驱动版本过低,与MySQL8.0不兼容 | 升级mysql-connector-java到8.x版本 |
| 连接超时或握手失败 | 未使用com.mysql.cj.jdbc.Driver驱动类 | 修改driver-class-name配置 |
报Connection is read-only | 事务上下文与连接配置冲突 | 检查@Transactional注解所在层,确保数据源配置正确 |
| 中文乱码 | 数据库、连接URL、表字符集不一致 | 统一使用utf8mb4字符集 |
报Public Key Retrieval is not allowed | 认证插件为caching_sha2_password,未开启公钥检索 | URL加allowPublicKeyRetrieval=true |
5.2 MyBatis-Plus使用过程中的常见问题
问题一:分页查询返回总数为0或数据不完整
这个问题的原因往往是分页插件没有注册。MyBatis-Plus的分页功能必须依赖PaginationInnerInterceptor,缺少这个配置时selectPage会退化成普通的list查询。解决方式就是前面提到的在配置类中注册MybatisPlusInterceptor。
问题二:saveBatch批量插入性能差
saveBatch内部默认不是真正的批量插入,而是一条一条地insert,性能不理想。如果数据量大,可以配置批量SQL执行器,或者在SQL注入器里自定义批量插入方法。对于图书商城这种低并发场景,saveBatch够用,不需要过度优化。
问题三:字段自动填充失效
创建时间、更新时间这类字段,更优雅的做法是用MyBatis-Plus的字段自动填充功能,在实体类的字段上加@TableField(fill = FieldFill.INSERT)注解,然后实现MetaObjectHandler接口统一填充。有些同学把逻辑写在业务代码里,每个插入操作都要手动set时间,反而是给自己添麻烦。
5.3 Vue3开发过程中的高频报错与处理
问题一:组件渲染时xxx is not defined
<script setup>语法中,模板里用的变量、函数必须从setup作用域中暴露。如果直接使用组件外部导入的工具函数而忘了引入,就会出现这个报错。解决办法是检查模板中使用的每一个变量是否都在<script setup>中有定义。
问题二:el-form表单验证不生效
Element Plus的el-form验证依赖rules对象和prop属性的对应关系。rules里的字段名必须和el-form-item的prop属性一致,少了其中一个验证就不会触发。另外验证规则里的required为true时,还需要设置message提示信息,否则控制台会有警告。
问题三:页面刷新后路由跳转404
这个问题通常出现在生产环境部署后。Vue3是单页应用,路由由前端控制,后端服务器没有配置try_files时,访问/book/123这种路径会触发404。解决方式是在Nginx配置中加入:
location / { try_files $uri $uri/ /index.html; }开发环境下不用管,生产部署一定要配置这条,否则刷新页面就白屏。
5.4 部署上线阶段的实践心得
整个项目开发完成后的部署,我习惯采用前后端分离部署方案:后端打包成jar包运行,前端打包成静态文件交给Nginx托管。
后端打包命令:
mvn clean package -DskipTests生成的jar包在target目录下,运行命令:
java -jar bookstore.jar --spring.profiles.active=prod我建议把生产环境和开发环境的配置分开,用application-prod.yml存放生产环境数据库地址、日志级别等参数,这样切换环境时不用反复改配置。
前端打包命令:
npm run build生成的文件在dist目录,把这个目录配置为Nginx的根目录,同时把/api开头的请求反向代理到后端服务的8080端口:
server { listen 80; server_name yourdomain.com; root /usr/share/nginx/html; index index.html; location /api/ { proxy_pass http://localhost:8080; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; } location / { try_files $uri $uri/ /index.html; } }这个部署方案的好处是前后端完全解耦,后端服务升级时前端页面不受影响,反之亦然。我实际部署时还遇到一个坑:jar包的启动端口和Nginx代理端口冲突,解决方式是把SpringBoot服务端口改成8081,Nginx代理那边指向8081,或者在配置里通过环境变量动态控制。
最后再分享一个小技巧:项目里的数据库脚本和接口文档一定要随手维护。你永远不知道三个月后回来改需求时,是否还能记得当初为什么给order_item表加了一个remark字段。这套项目自带的文档虽然不算特别详尽,但至少把表结构说明、接口地址和部署步骤记录下来了,这本身就是很值的参考。如果你准备二次开发,先把数据库脚本跑通、前后端启动起来,再对照着文档把登录认证流程走一遍,整个项目的脉络基本就能理清了。