
做了几年的Java全栈开发经常被刚入行的朋友问“有没有一个完整的前后端分离项目可以参考”市面上教程不少但要么是纯后端接口演示要么是阉割版的前端页面很难找到一个从头到尾能落地、能部署、能写进简历的完整案例。这套“SpringBootVueMyBatisMySQL”的图书电子商务网站系统正好补齐了这个空缺。它不是一个只跑通本地就完事的小demo而是覆盖了商品展示、搜索分页、购物车、下单流程、后台管理这些电商核心链路的完整闭环。这篇文章我会从架构设计讲到数据库表结构再一步步带你把环境搭起来、把代码跑通、把项目部署到服务器上途中遇到的坑我也会原原本本分享出来希望能帮正在做毕业设计或者准备实训项目的朋友省下一大笔摸索的时间。1. 项目架构前后端分离到底分在哪、怎么分1.1 前后端分离的本质是职责边界清晰很多新手对“前后端分离”的理解停留在“用了Vue就是前后端分离”这个理解不够准确。真正的分离不是技术栈的区分而是职责边界和数据流动方式的重构。在传统开发模式里JSP或者Thymeleaf模板把Java代码和HTML揉在一起后端不仅要处理业务逻辑还要操心页面上哪块显示什么、按钮点击后跳转到哪里这就导致前端改个样式、调个交互后端也要跟着重新编译打包。而在这套图书电商系统里前端只做三件事渲染页面、收集用户操作、调用后端接口。Vue负责通过Axios向后端发送HTTP请求拿到JSON数据再渲染到页面上页面跳转由Vue Router控制不涉及任何后端页面的参与。后端则专注于业务逻辑和数据持久化SpringBoot的Controller接收请求、Service层处理业务规则、Mapper层通过MyBatis操作MySQL数据库。两边只用一套事先约定好的API接口文档来沟通前端调试时可以用Mock数据模拟后端返回后端调试时可以用Postman绕过前端直接测接口。这样做带来的直接好处我实际体验下来有三个。第一并行开发效率大幅提高前端不用等后端把页面模板写好才能开工后端也不用等前端把UI切好才能写接口两边只要先把接口契约定下来就能同时推进。第二后端可以很方便地做成纯API服务以后如果要出小程序端、移动端App前端页面完全不用动后端代码重新做个客户端对接同一套接口就行。第三前后端可以分别部署到不同的服务器前端是静态文件扔到Nginx后端是Java进程独立运行互不干扰对服务器资源的利用也更灵活。1.2 系统的整体数据流是怎么走的这套系统的数据流是一条清晰的一维链路。用户打开浏览器访问前端页面Vue Router根据URL匹配到对应的组件组件在挂载时触发Vuex或者组件内部的数据请求Axios拦截器在请求发出前带上token之类的认证信息请求到达后端Nginx或Tomcat后进入SpringBoot的DispatcherServletController层校验参数并调用Service层Service层封装业务逻辑通过Mapper接口进入MyBatisMyBatis根据XML里的SQL映射生成JDBC语句最终由MySQL执行并返回结果集。结果集再沿着这条链路原路返回到前端Vue拿到数据后更新响应式状态页面重新渲染。这个流程听起来简单但每一步都有需要特别注意的细节。比如跨域问题——前端开发服务器运行在8080端口后端运行在8081端口两个端口不同就构成了跨域前端直接请求会被浏览器的同源策略拦截。解决办法一般有两种一种是后端配置CORS跨域过滤器允许指定来源的请求访问另一种是前端通过Nginx反向代理将/api前缀的请求转发给后端让浏览器看起来请求的是同一个域。这套项目里两个方案我都实测过开发环境用后端CORS配置比较省事生产环境用Nginx代理更安全因为可以顺便隐藏后端真实端口。1.3 为什么选SpringBootVueMyBatisMySQL这套组合技术选型没有绝对的“最好”只有“最适合当前场景”。这套组合能成为国内Java全栈开发的主流搭配有它很现实的原因。SpringBoot的前景在于它把Spring框架繁琐的XML配置大幅简化了内嵌Tomcat让Web服务可以像普通Java程序一样启动spring-boot-starter这种依赖聚合机制让你加功能时基本不用操心依赖版本兼容问题。对一个图书电商项目来说SpringBoot自带的事务管理、自动配置、监控端点已经覆盖了大部分需求不需要引入更重的Spring Cloud体系。Vue作为前端框架优势是学习曲线相对平缓模板语法直观响应式数据绑定让DOM更新不需要手动操作加上Element Plus这类成熟的UI组件库一个开发经验一般的人也能做出像样的后台管理界面。Vue3的Composition API对代码组织方式的改进在项目复杂度上升时能明显感觉到维护性更好。MyBatis在持久层框架里最大的特点是SQL由开发者自己掌控不像Hibernate那样自动生成SQL遇到复杂多表查询或者SQL优化时比较被动。图书电商里像“按分类筛选关键词模糊搜索价格区间过滤分页”这种动态SQLMyBatis的where和if标签写起来非常顺手可控性很强。MySQL则是生态成熟度的问题社区活跃、文档丰富、部署简单图书电商的读写压力和表关联复杂度MySQL应付起来绰绰有余。唯一需要提前注意的是字符集一定要统一成utf8mb4不然后端插入用户昵称里的表情符号时会有编码报错。2. 核心功能模块与数据模型设计2.1 五大核心模块的职责划分这套图书电商系统的功能模块大致分为五个用户模块、图书商品模块、购物车模块、订单模块、后台管理模块。每个模块前后端各有对应的代码组织方式。用户模块负责注册、登录、个人信息维护。密码存储这一块值得多说一句绝对不能明文存库至少要加盐后做MD5或者SHA-256散列再配合JWT做无状态登录凭证。登录成功后后端签发一个token前端存在localStorage里每次请求通过Axios拦截器加到请求头后端用一个拦截器校验token有效性这套方案不需要在后端维护Session对前后端分离的架构非常友好。图书商品模块是系统的门面包括图书列表展示、按分类筛选、关键词搜索、图书详情查看。列表页必须做好分页否则数据量一上来页面就会卡顿我这里用的是PageHelper插件一行配置就能实现物理分页。购物车模块的核心是“用户的临时选择集合”它关联用户ID和图书ID还要记录加入时的数量。购物车操作有加入、修改数量、勾选结算、删除几个动作每个动作对应一套API前端购物车组件的状态要跟后端数据保持同步不能只改页面上的数字不问后端。订单模块是整条电商链路里逻辑最复杂的部分。下单时不仅要创建订单记录还要扣减图书库存这两个操作必须放在同一个数据库事务里任何一步失败都要整体回滚否则就会出现“订单显示成功但库存没减”或者反过来“库存扣了但订单没生成”的严重数据不一致问题。订单状态一般有待付款、已付款、已发货、已完成、已取消这样几个流转状态。后台管理模块是给网站运营者用的包含图书信息管理、分类管理、用户列表、订单处理等操作本质上是一套基于同一后端API的另一个前端页面这一点也是前后端分离架构的优势一套后端同时支撑前台门户和后台管理系统前端代码却可以分成两个工程。2.2 数据库表结构设计的要点解析数据库表结构是整个项目的根基设计得好后续写代码会很顺畅设计得烂后面全是在补坑。我整理了这套系统的核心表结构DDL脚本可以按下面的说明来建。用户表user主键id用自增整型就好username要加唯一索引password存散列后的字符串nickname和avatar用于展示phone和email做选填create_time记录注册时间。这里记住一个原则字段类型宁长勿短手机号用varchar不要用int否则注册时无法存下完整的手机号。图书表bookbook_name、author、publisher、isbn、price、original_price、stock、sales、cover_image、description、status。status字段用来上下架值为1上架0下架这样下架的图书不用删记录数据不会丢失。price要定义为DECIMAL(10,2)不要用float或者double浮点数的精度问题在金额计算上会翻车。分类表categorycategory_name、sort_order、status。图书和分类的关系在这套系统里做简单的单级分类就好每本图书有一个category_id外键。如果你想做成多级分类树形结构可以加parent_id字段自关联但列表查询的递归处理工作量会大不少新手阶段一般不推荐。购物车表cart_itemuser_id、book_id、quantity、checked。user_id和book_id做联合唯一索引防止同一个用户把同一本书重复加入购物车如果已经有了就直接更新数量。订单表ordersorder_no、user_id、total_amount、status、create_time、pay_time、consignee、phone、address。order_no必须是全局唯一的业务单号常见做法是“时间戳随机数”或者“日期自增序列”不要用数据库自增ID当订单号会暴露业务量而且订单号需要提前生成在后续支付回调时使用。订单项表order_itemorder_id、book_id、book_name、book_image、price、quantity、subtotal。注意这里要把图书的名称、图片、价格冗余进来不直接关联book表因为图书信息以后可能会改但订单快照必须保持下单那一刻的样子。比如一本书后来价格变了用户的订单记录里应该还是他购买时的价格。2.3 权限设计与登录认证方案前后端分离项目里的登录认证我一直推荐JWT方案理由有三个天然无状态、支持跨域、容易扩展。JWT的流程是这样的用户输入账号密码后端校验通过后用密钥生成一个token返回token里面可以带用户ID、用户名、过期时间这些信息前端拿到token存起来每次请求在Authorization头里带上后端写一个HandlerInterceptor拦截器统一校验。SpringBoot里加一个拦截路径配置排除登录、注册、图书列表这些公开接口剩下的接口都校验token。JWT有几个坑必须提醒一下。第一token里的信息是base64编码的任何人都可以解码看到内容所以千万别把密码之类的敏感信息放进去。第二HS256对称加密的密钥要放在后端配置文件里不要提交到Git仓库。第三token的过期时间要设置合理我建议后端token有效期设2小时再提供一个/auth/refresh接口用refresh_token换新的access_token前端每次在401响应时自动刷新。如果你觉得JWT的黑话太多可以用更简单的方式登录成功后后端把用户信息放进Session同时返回一个随机的sessionId前端存下来在请求头里带着后端用拦截器查Session是否存在。这种方式实现更简单但跨域和集群部署时要处理Session共享问题实际项目里还要引入Redis。对学习项目来说JWT是性价比更高的选择面试时说起它也更占优势。3. 从零到跑通的完整搭建流程3.1 环境准备与版本选择开始写代码之前先把环境装好这是很多人忽略但坑最多的一步。我在实际搭建中验证过一套组合以下是推荐版本。JDK 1.8或者JDK 11不建议一上来就用JDK 17因为SpringBoot 2.x的某些老版本在JDK 17下会有模块化访问问题遇到报错排查起来很头疼。Maven 3.8MySQL 8.x注意8.x的驱动类名和5.x不一样8.x是com.mysql.cj.jdbc.Driver且URL里要带serverTimezoneAsia/Shanghai。Node.js 16Vue3和Vite要求较高的Node版本装太老的版本依赖都拉不下来。IDEA开发工具装好Lombok插件不然实体类的注解会不生效。Navicat或者DataGrip任选一个作为数据库客户端。安装MySQL这里多说一句很多新手装完后SpringBoot启动时连不上数据库报Public Key Retrieval is not allowed或者SSL连接错误。前者需要在JDBC的URL后面加allowPublicKeyRetrievaltrueuseSSLfalse。但useSSLfalse在生产环境并不推荐只是开发调试时为了省事。MySQL 8.X默认开启SSL如果你不想处理证书问题本地开发直接禁用就好。还有那个经典的ERROR 2002 (HY000): Cant connect to local MySQL server through socket /tmp/mysql.sock这是Mac用户很容易遇到的——用brew安装的MySQL服务还没启动启动服务后问题就消失了。Windows用户则大概率是服务没启动在服务管理器里把MySQL服务跑起来就行。3.2 后端工程初始化与关键配置创建一个SpringBoot工程我建议直接去Spring Initializr网站生成比在IDEA里一步步选组件更不容易出错。依赖选择这几组Spring Web、MyBatis Framework、MySQL Driver、Lombok如果要用PageHelper就额外在pom.xml里手动加依赖。application.yml配置文件是后端的核心我用了如下配置server: port: 8081 spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/bookstore?useUnicodetruecharacterEncodingutf8mb4serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 你的数据库密码 jackson: date-format: yyyy-MM-dd HH:mm:ss time-zone: Asia/Shanghai mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: com.example.bookstore.entity configuration: map-underscore-to-camel-case: true log-impl: org.apache.ibatis.logging.stdout.StdOutImpl pagehelper: helper-dialect: mysql reasonable: true support-methods-arguments: truemap-underscore-to-camel-case这个配置很多人不重视但它特别关键。数据库字段是create_time这种下划线命名Java属性是createTime这种驼峰命名没有这个配置查询结果映射到实体类就会全部是null。我见过不止一个新手在这个坑里反复挣扎查数据库的时候明明有数据接口返回却是null检查了半天原来是少加了这一行。MyBatis的XML映射文件放在src/main/resources/mapper目录下结构跟Mapper接口包路径对应。这里再补充一个实用的查错技巧把log-impl配置成StdOutImpl之后MyBatis执行的SQL语句会打印到控制台带参数的SQL会显示完整的占位符替换结果。写动态SQL的时候这条日志能救你无数次。3.3 前端Vue工程搭建与代码组织前端用npm create vitelatest快速初始化一个Vue3工程。Vite构建比Webpack快非常多热更新响应几乎是即时的。接着安装必要依赖npm install vue-router4 npm install axios npm install pinia npm install element-plus npm install element-plus/icons-vue安装Element Plus之后建议用按需导入而不是全量引入。全量引入会把整个组件库打进包里首屏加载很慢。按需导入可以用unplugin-auto-import和unplugin-vue-components两个Vite插件自动把用到的组件和API导入打包体积能小不少。前端代码结构可以参考下面的方式组织src/ api/ # 接口请求函数统一封装 assets/ # 静态资源 components/ # 通用组件 router/ # 路由配置 stores/ # Pinia状态管理 views/ # 页面组件 Home.vue # 首页 BookList.vue # 图书列表 BookDetail.vue # 图书详情 Cart.vue # 购物车 OrderConfirm.vue # 订单确认 Login.vue # 登录 Register.vue # 注册 admin/ # 后台管理页面 utils/request.js # Axios实例封装utils/request.js这一段封装需要重点说一下。创建一个axios实例配置baseURL指向/api请求拦截器里从localStorage取出token放到请求头响应拦截器里统一处理后端返回的结果。当后端返回401时自动跳转登录页当业务码表示失败时弹出错误提示。这样每个页面在调接口时只需要关心成功后的数据不用重复写一堆错误处理逻辑。3.4 关键接口的实现示例图书列表的分页搜索是整套系统里最有代表性的接口。后端Controller这样写RestController RequestMapping(/api/book) public class BookController { Autowired private BookService bookService; GetMapping(/list) public Result list(RequestParam(defaultValue 1) int pageNum, RequestParam(defaultValue 12) int pageSize, RequestParam(required false) Long categoryId, RequestParam(required false) String keyword) { PageHelper.startPage(pageNum, pageSize); ListBook books bookService.queryBookList(categoryId, keyword); PageInfoBook info new PageInfo(books); return Result.success(info); } }Service实现里用MyBatis的动态SQLselect idqueryBookList resultTypecom.example.bookstore.entity.Book SELECT * FROM book where if testcategoryId ! null AND category_id #{categoryId} /if if testkeyword ! null and keyword ! AND (book_name LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %) OR isbn LIKE CONCAT(%, #{keyword}, %)) /if AND status 1 /where ORDER BY sales DESC, id DESC /selectwhere标签会自动处理掉第一个AND防止SQL语法错误这个特性在多条件组合查询时太好用了。PageHelper的startPage只需要写在查询之前一行它会在执行下一条SQL时自动拼上limit分页语句PageInfo取出总数、当前页、总页数这些分页元数据返回给前端。购物车加入和下单这两个接口涉及数据一致性问题我给出下单的核心代码思路Transactional public Order createOrder(Long userId, ListCartItemDTO items, AddressDTO address) { String orderNo generateOrderNo(); Order order new Order(); order.setOrderNo(orderNo); order.setUserId(userId); order.setStatus(0); // 待付款 order.setCreateTime(new Date()); // 逐个订单项累加总金额同时扣减库存 BigDecimal total BigDecimal.ZERO; for (CartItemDTO item : items) { Book book bookMapper.selectById(item.getBookId()); if (book.getStock() item.getQuantity()) { throw new BusinessException(《 book.getBookName() 》库存不足); } int rows bookMapper.deductStock(item.getBookId(), item.getQuantity()); if (rows 0) { throw new BusinessException(《 book.getBookName() 》库存扣减失败); } OrderItem orderItem buildOrderItem(order, book, item); orderItemMapper.insert(orderItem); total total.add(orderItem.getSubtotal()); } order.setTotalAmount(total); orderMapper.insert(order); cartMapper.clearCheckedItems(userId); return order; }Transactional注解确保库存扣减和订单写入在同一个数据库事务里中间任何一步抛了异常之前的写操作都会回滚。deductStock的SQL是用UPDATE book SET stock stock - #{qty} WHERE id #{id} AND stock #{qty}这一步既扣库存又做并发保护在高并发下避免超卖。3.5 前后端联调与常见报错前后端都跑起来之后联调阶段最常见的报错就是跨域。后端可以加一个全局CORS配置类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); } }这个配置允许所有来源的跨域请求开发环境用是没问题。生产环境建议通过Nginx反向代理把前后端合并到同一个域名下具体做法是Nginx配置中把/开头的请求指向前端静态文件把/api开头的请求proxy_pass到后端8081端口这样就连后端CORS都不需要了因为浏览器看来请求的是同一个源。联调时还要注意前端传参的类型匹配。比如后端接口的categoryId是Long前端如果传了一个空字符串SpringMVC参数绑定就会报Failed to convert value of type java.lang.String to required type java.lang.Long。解决方法是后端用RequestParam(required false)并把空字符串转成null或者前端在传参前先判断空值不发这个参数。我更推荐前端处理因为参数校验逻辑放在调用端更直观。4. MyBatis使用进阶与常见问题排查4.1 TypeHandler到底在什么时候用MyBatis里的TypeHandler是一个很多人觉得难懂但实际很实用的组件它的作用是处理Java类型和数据库类型之间的转换。我在这套系统里遇到一个典型场景图书状态字段status在数据库里是TINYINT0表示下架1表示上架但Java代码里如果全用Integer表示业务代码里到处是if (status 1)这种硬编码判断阅读性和可维护性都一般。更好的做法是定义枚举类BookStatus数据库查出来的数字自动映射成枚举写库时枚举自动映射回数字。自定义TypeHandler的步骤是继承BaseTypeHandlerT实现setNonNullParameter、getNullableResult这几个方法。注册方式有两种一种是在MyBatis全局配置里注册另一种是在Mapper XML的resultMap里针对某个字段指定typeHandler属性。如果你用的是PageHelper加MyBatis的组合记得TypeHandler的注册信息和Mapper扫描配置要保持一致不然会出现“实体类映射不上”的问题。大多数图书电商项目用到的场景并不深但面试时TypeHandler是MyBatis的高频考点。建议你把这个项目里BookStatus的处理改造成TypeHandler版本既能锻炼理解能力面试被问到“MyBatis里TypeHandler的工作流程”时你也能很自然地用自己的项目代码做例子讲清楚。它的执行流程可以简单理解为查询时先经过getNullableResult把JDBC ResultSet里的TINYINT转成枚举插入更新时先经过setNonNullParameter把枚举转回JDBC支持的整型。4.2 MyBatis缓存机制与使用建议MyBatis有一级缓存和二级缓存两个层级。一级缓存是SqlSession级别的同一个会话里两次相同的查询会直接命中缓存不查数据库。SpringBoot集成MyBatis时默认的SqlSession生命周期跟一次数据库操作绑定所以一级缓存的实际作用很有限。但有个坑要提醒如果在同一个事务里先查询再更新一级缓存可能让后面的查询拿到旧数据不过MyBatis会在执行增删改时自动清空一级缓存所以这个坑通常不会出现。二级缓存是Mapper级别的跨越SqlSession共享查询结果作用范围是同一条命名空间下的查询。实现方式是在Mapper接口或者XML里加cache/标签。听上去很香但二级缓存有一个著名的问题当两张表通过联表查询时对其中一张表做更新另一个Mapper的缓存并不会被清空就会读到脏数据。图书电商系统里订单表和其他表频繁关联查询缓存失效问题几乎无法严格控制。我个人的建议是这套图书系统的MyBatis二级缓存保持默认关闭就好不需要开启。系统的读多写少确实存在但引入缓存前先问自己一个问题这个查询真的慢吗图书列表走MySQL索引单表几十万条数据性能瓶颈往往不在数据库查询而在后续的业务逻辑和网络IO。如果将来数据量真的大了优先考虑在Service层用Redis做缓存可控性比MyBatis二级缓存好太多。4.3 MyBatis编写SQL时的高频坑动态SQL里的test条件写错是最高频的问题。if testkeyword ! null and keyword ! 这种写法里多字段用and连接不能用因为XML解析的时候会被当成特殊字符轻则解析报错重则在条件判断里出现神秘行为。推荐用and兼顾了XML合法性和SQL语义。还有个更隐蔽的问题if里面字符串比较不能用要用equals。比如写teststatus 1如果你用Char类型比较可能碰巧能用但换成String类型就失效了。我的习惯是所有的枚举状态都统一用Integer存避免字符串比较的歧义。LIKE模糊查询也有性能隐患。LIKE %keyword%这种写法因为前导通配符的存在即使在book_name上建了索引也用不上索引扫描全表扫描在数据量大时很慢。图书系统的数据量短期内不算大可以接受但如果将来数据量上来了有几种替代方案用全文索引或者把搜索关键词做分词处理后在应用层做匹配更极端的方案是引入Elasticsearch。每种方案都有学习和打包成本项目阶段完全不必一步到位。4.4 mapper XML文件不生效的处理以前经常有同学来问“接口方法写了XML也写了怎么运行时一直提示Invalid bound statement (not found)”这个问题的原因八九不离十是XML文件没有被打包进classpath。SpringBoot的Maven默认资源只包含src/main/resources下的文件如果你的Mapper XML放在src/main/java目录下面不额外配置资源插件的话编译时XML会被直接丢弃。解决方案有两种一是XML统一放在resources/mapper目录二是在pom.xml里加上resources resource directorysrc/main/java/directory includes include**/*.xml/include /includes /resource /resources我强烈建议方案一保持约定优于配置的思路项目结构清晰了后面排查问题也简单。5. 部署上线从前端构建到服务器运行5.1 前端打包与Nginx配置开发完成之后前端还是跑在Vite Dev Server里的需要打包成静态文件才能部署。执行npm run build会在项目根目录生成dist目录里面就是最终的HTML、CSS、JS文件。在构建之前记得修改utils/request.js里的baseURL确保请求的路径在线上环境下能正确转发到后端。将dist目录上传到服务器比如放到/usr/share/nginx/html/bookstore然后配置Nginx站点。一个标准的配置长这样server { listen 80; server_name your-domain.com; root /usr/share/nginx/html/bookstore; index index.html; # 前端路由history模式必须的配置 location / { try_files $uri $uri/ /index.html; } # 后端接口反向代理 location /api/ { proxy_pass http://127.0.0.1:8081; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }try_files $uri $uri/ /index.html这行特别重要。Vue Router的history模式前端路由地址比如/book/12在服务器上是没有真实文件存在的如果Nginx不把这样的请求重写到index.html刷新页面就会404。这是一个非常经典的坑我记得自己第一次部署Vue项目所有页面刷新全部白屏检查了几轮网络请求才发现是Nginx路由重写的问题。顺带说一句如果你用的是hash模式路由地址会带#不会有这个问题但URL不美观也影响SEO能用history模式尽量用history。5.2 后端打包与两种运行方式后端打包有两种方式一种是打成直接用java -jar运行的jar包另一种是打成war包放进独立的Tomcat里。SpringBoot内置了Tomcat我个人强烈推荐前者少装一个软件不说启动速度、环境一致性和运维简洁度都好得多。用Maven打包前先在pom.xml确认打包方式是packagingjar/packaging排除掉测试代码后执行mvn clean package -DskipTests打包之后的jar文件在target目录下名字一般是bookstore-0.0.1-SNAPSHOT.jar。上传到服务器运行java -jar bookstore-0.0.1-SNAPSHOT.jar --spring.profiles.activeprod生产环境的数据库连接、端口等配置在application-prod.yml里维护不要跟开发配置混在一起这是个提升运维体验的好习惯。如果你想在后台运行用nohup java -jar xxx.jar app.log 21 的方式日志输出到app.log文件排查问题时直接翻这个文件。如果你的服务器内存吃紧可以加上-Xms256m -Xmx256m限制Java堆内存。有人会问“那tomcat部署前后端分离项目还成不成立”答案是依然成立只是Tomcat只负责承载后端SpringBoot应用前端仍然用Nginx或任意静态服务器。你完全可以把jar包扔到Tomcat的webapps目录下改为war包部署但这样做恰恰把一个简单的应用复杂化了属于有得选但没必要。SpringBoot团队本来就在推行把Tomcat嵌进来独立运行。5.3 jar包反编译拿来主义的学习方式有些朋友拿到别人的SpringBoot源码包想学习里面的写法但不想跑一遍整个工程这时有个取巧的办法——把jar包反编译成工程结构。SpringBoot的fat jar其实就是一个压缩包按下面几步可以拆开# 1. 解压jar包 jar -xvf bookstore.jar # 2. 在解压出来的目录里找到BOOT-INF/classes里面是编译好的class文件 cd BOOT-INF/classes # 3. 反编译class文件为java源码 # 可以用JD-GUI工具拖进去直接看源码 # 也可以用命令行工具 CFRjava -jar cfr.jar com/example/BookController.class BookController.java反编译出来的代码虽然能还原逻辑但注释、变量名、类型推断可能有些变形更建议把它当“理解思路”的参考而不是当作你能直接编译运行的源码。把别人的项目反编译了不会让你真正吃透架构还是老老实实从自己搭建的小项目开始积累经验更踏实。要学会“抄”架构思路而不是逐行抄代码前后端如何通信、事务怎么管理、异常怎么统一处理这些才是真正值钱的东西。5.4 部署后的验证清单程序部署起来之后不要急着宣告成功我一般会按下面的顺序验证一遍。前端首页能否访问图片资源能否加载。注册一个新账号确认密码是加密存库的。登录后刷新页面确认登录状态是否还在token是否持久化。搜索一本书、点击分类筛选确认动态SQL和分页正常。把一本书加入购物车调整数量刷新页面确认数据不丢。提交一个订单确认库存扣减、订单生成然后在数据库里核对数据确认order_item里的价格快照是你下单那一刻的价格。检查Nginx错误日志和后端日志看有没有异常堆栈。如果某个环节出错先别慌着看代码先分清楚是前端的问题还是后端的问题。打开浏览器开发者工具看Network面板里请求的状态码和响应内容。如果是500去后端日志里翻堆栈信息如果是4xx看是不是参数校验或者token过期如果是200但页面上没数据九成是前后端字段名对不上。这套排查流程能让你花最少的时间定位到问题所在。6. 项目扩展方向与面试常问点6.1 这套系统还能往哪些方向升级一套图书电商系统做到能跑、能部署、有完整的购物流程已经算是一个不错的实战项目了。但如果想让它更出彩在简历上有更强的竞争力可以考虑几个低成本高回报的扩展方向。第一个方向是接入Redis。目前这套系统里登录token校验每次都要查数据库用户表购物车数据如果存数据库虽然逻辑简单但每次请求都要读写数据库。把token和购物车数据塞到Redis里一方面响应速度会提升另一方面面试时能自然引出缓存、过期策略、分布式Session这些话题。第二个方向是引入消息队列。下单后发送邮件或者短信通知用户这个操作如果同步执行会拖慢下单接口的响应。用RabbitMQ或者RocketMQ下单逻辑和通知逻辑解耦异步处理这也是面试高并发场景时的高频考点。第三个方向是文件服务独立化。图书封面目前是存路径图片文件放在服务器本地将来数量多了把图片迁移到MinIO这种对象存储服务后端通过预签名URL让前端直传这是目前企业里很常见的做法。热搜词里就有“minio加入到springboot”说明很多人在研究这条路线你可以提前走一遍。第四个方向是接口安全加固。加上接口签名校验、防重复提交、限流拦截器这些细节会提升项目的专业度面试官看到这些点容易眼前一亮。6.2 面试官最爱问的相关问题做完整套项目面试时你会遇到的几个高频问题我必须提前说道说道方便你提前准备。“SpringBoot自动配置的原理是什么”这个问题是通过你这套项目引入的。你只要回答到SpringBootApplication注解组合了EnableAutoConfiguration通过META-INF/spring.factories里配置的自动配置类配合ConditionalOnClass、ConditionalOnMissingBean等条件注解按需生效基本就能过关。做项目时你肯定配置过数据源可以顺着这个例子说。“MyBatis一级缓存和二级缓存有什么区别”上面已经讲过一级缓存SqlSession级别二级缓存Mapper级别。面试时还会追问“为什么二级缓存容易脏读”你拿图书表联合查询返回不规范数据这个例子来说很具体。“SpringBoot项目怎么防止重复提交订单”这个问题在电商项目里很常见。用Redis的setnx锁或者数据库的唯一约束控制同一个用户对同一个订单号的重复提交。配合下单接口的幂等设计来回答能体现你考虑问题足够全面。“Vue里父子组件数据传递的方式”前端基本必问。props父传子、$emit子传父、v-model语法糖、provide/inject跨层级传递、Pinia做状态共享这套项目里购物车数量修改就用到了这些知识回答时把项目里的真实场景说出来加分。不管面试问什么原则都是先一句话说结论再展开细节最后结合项目实例收尾。理论背得再熟没有落地的代码实践回答起来总有点虚这套项目的价值就是给你一个实实在在的抓手。6.3 关于若依框架要不要用的建议搜索前后端分离项目的过程中你一定会频繁看到若依框架这个名字。它确实是很优秀的开源脚手架代码生成器让你几分钟就能生成一套增删改查页面日志、权限、多租户都是现成的。但我不建议一个新手用它来作为第一个完整项目“一键生成”的东西多了对底层原理的感知就会退化。你先亲手把SpringBootVue这套图书系统从零搭出来踩过坑了再看若依的代码会发现很多东西自己都能看懂甚至能改进那才是真正吃掉它的时候。如果时间实在紧张需要交东西直接用若依改造一个偏管理后台的方向也不是不行只是你得清楚哪些代码是你自己写的免得面试被追问时露馅。7. 我的实操总结与避坑清单整套做下来的周期我估算是这样如果每天有3个小时左右的时间环境熟悉的前提下后端15天左右、前端10天左右、联调加部署5天一个月能完成。中间如果你还要打散框架、自己设计表结构时间再加一周。这个时间表比我当时自学时快得多因为踩坑的记录我已经帮你整理出来了。7.1 最容易翻车的细节集合最后我把这个项目里体验最深的一些坑和避坑方法集中列一下每一条都是真实踩过的建议你把这一段存下来做项目时对照着检查。数据库字符集没有统一为utf8mb4插入中文、表情符号时产生编码错误。建库时必须显式指定CHARACTER SET utf8mb4以及COLLATE utf8mb4_unicode_ci。JDBC的URL忘记带serverTimezoneAsia/Shanghai查数据时会报时区异常或者时间偏移8小时。后端返回的时间格式默认是时间戳串前端一渲染就是一串数字。需要配置spring.jackson.date-format并确认实体类字段上没被JsonFormat覆盖。前端把localStorage里的token存得乱七八糟或者退出登录时忘了清理导致后续请求带了失效token全部401。封装request.js统一管理token的读写删是正解。删除购物车商品时只删了当前页的数据没跟后端联动刷新后又出现了。下单时重复点击提交按钮生成了两条重复订单。前端提交时把按钮禁用后端再加一层防重复订单校验双保险。库存扣减时用UPDATE book SET stock stock - 1而不是先查再更新后者在并发场景下可能把库存扣成负数。扣库存要用条件更新SET stock stock - #{num} WHERE id #{id} AND stock #{num}。修改代码后发现页面没生效十次里有八次是浏览器缓存强刷一下再看后端代码改了没重启SpringBoot也很常有devtools热部署插件值得装一个。生产环境排查问题时千万别直接在生产库里乱改数据先备份再操作养成这个习惯能保平安。7.2 我说点实在的心得翻来覆去做了几个类似的项目我最深的体会是像这种前后端分离的图书电商系统代码量并不算大真正的难点在于“打通”。打通前后端的数据结构、打通登录认证的会话机制、打通本地开发和服务器部署的差异。每打通一层你对整个Web开发的理解就上一个台阶。做这个项目的过程中MySQL的安装配置、SSL连接错误处理、MyBatis的TypeHandler使用、Vue的安装与DevTools调试、SpringBoot打包jar和反编译研究——这些热搜词里的每一个问题你都可能在实际中遇到一遍遇到的时候别怕每解决一个是问题留下的都是实打实的经验。最后再分享一个小技巧不要只做一遍做完一遍之后从零开始手写一个精简版。第一次你是在跟着教程走第二次你才是在真正内化它。到第二个精简版里我建议你把图书模块的搜索改成Radis缓存、把登录改成JWT拦截器、把部署改成Docker容器化——哪怕只加上其中两样你的简历项目就比大部分同期的人多了一截竞争力。这套系统的源码和部署步骤已经比较完整剩下的创造力就是你自己的事了。