ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot二手图书交易系统实战:从表设计到部署避坑指南

SpringBoot二手图书交易系统实战:从表设计到部署避坑指南 简介一套基于Spring Boot的二手图书交易系统完整源码面向Java初学者、毕业设计与课程设计者可作为从零搭建前后端分离交易平台的学习范本。项目采用JDK1.8和Spring Boot框架代码分为client_code、manage_code、server_code三部分分别承担用户端展示、后台管理与服务端业务处理数据库选用MySQL 5.7/8配备建表SQL和表结构说明也提供了Navicat操作指引可在Eclipse或IDEA中直接导入运行。压缩包共415个文件、11.2MB以124个Java源码文件、88个Vue页面组件、42个JavaScript脚本、20个CSS样式、20个XML配置及SQL脚本为主前后端资源按模块存放便于定位和阅读。项目已经过程序测试可正常运行文档中还包含部署说明覆盖环境配置、启动步骤与常见运行问题对理解Spring Boot接口开发、Vue组件化页面、数据库设计和项目上线流程都有直接帮助。目前已有99人学习下载适合课程设计、毕业设计及Spring Boot项目实战训练。1. 为什么二手图书交易系统是SpringBoot练手最好的归宿做过几个电商类管理系统后你会发现二手图书交易是最适合拿来验证SpringBoot掌握程度的业务它比增删改查多一点状态流转比全品类电商少一堆支付和库存复杂度。商品有上下架、订单有取消和完成、买卖双方有角色区分这套模型正好覆盖了SpringBootMySQL开发中最常踩到的关联查询、事务回滚和接口幂等问题。如果你正在找一份能跑通前后端、能写进简历、又能让你真正看懂每个表字段为什么这么设计的源码基于SpringBoot的二手图书交易系统是个性价比极高的切入口。这篇笔记不讲虚的直接拆表结构、接口设计、前端联调和部署上线把每一步的取舍和坑都摆出来。2. 选型与表设计先弄清楚这套系统的地基怎么打2.1 为什么是SpringBoot而不是SSH或PHP在动手看代码之前先明确这套系统最常见的技术栈组合因为源码包的解读和二次开发完全依赖这个底座。二手图书交易系统的典型实现是SpringBoot 2.x MyBatis-Plus MySQL 5.7/8.0 Vue 2 Element UI这套组合在Java从业者里接受度最高原因有三第一SpringBoot的自动配置让数据源、事务、Web容器这些原本要写一大堆XML的东西变成几个注解第二MyBatis-Plus的IService接口直接把单表CRUD省掉你可以把精力放在订单状态机和图书检索这种业务逻辑上第三前后端分离是目前二手书系统的主流形态前端跑在8080后端跑在9090或8081中间用Axios走JSON这套模式在面试里也是必问点。有一点需要提醒如果源码包里出现的是SpringBoot 2.2.x或2.3.x版本不要急着升级到2.7或3.x因为javax.servlet包名在SpringBoot 3里被换成了jakarta.servlet前端axios拦截器的写法、JSON序列化组件的兼容性都会连带出问题。对于二手图书系统这种中小型项目SpringBoot 2.3.x MyBatis-Plus 3.4.x是最稳的组合升级带来的收益不足以抵消踩坑成本。2.2 核心表拆解图书、订单、购物车、用户表之间到底是什么关系二手图书系统的表设计通常不是一张大表搞定而是按业务边界拆成六张左右t_user用户、t_book图书、t_order订单、t_order_item订单明细、t_cart购物车、t_category图书分类。用MySQL的workbench或Navicat导入sql文件后第一件事不是看数据而是打开模型视图看外键关系你会发现订单表基本不做物理外键只用user_id和book_id做逻辑关联这和网上商城系统的做法一致物理外键在高并发下会拖慢插入速度而且订单状态变更时容易触发不必要的级联操作。图书表的字段里book_status是一个容易被忽略的坑点。这个字段在二手交易系统里通常用0/1表示在售和已售而不是用ON_SALE/SOLD_OUT这种字符串枚举。原因很简单查询条件传参数时0和1的整数判断比字符串匹配更简洁MyBatis-Plus的QueryWrapper里直接.eq(book_status, 1)就能过滤在售图书。但代码里如果只写0和1运维人员看图表的语义会懵所以源码里一般会在实体类中加一个枚举注释或常量定义public class Book { /** 在售 */ public static final Integer STATUS_ON_SALE 1; /** 已售 */ public static final Integer STATUS_SOLD 0; }订单表则需要特别注意order_status字段的取值跨度。二手图书交易系统的订单状态比普通商城多了“卖家确认”环节所以典型状态机是0待付款、1待发货买家已付款等待卖家确认、2已发货卖家已确认、3已完成、4已取消。这个状态机对新手来说是个很好的理解素材为什么取消订单只能发生在0和1两个状态因为付款后取消需要涉及退款流程二手系统一般不在代码里做真实退款对接而是约定线下协商所以代码里会限制只有status0时用户能直接取消status1时只能申请取消等待卖家处理。2.3 购物车设计什么时候该建表什么时候该用Redis很多二手图书源码包里其实没有购物车表因为二手书是低频次、单件购买为主购物车更多是“收藏夹”语义。这里有个值得你注意的设计决策如果源码里用t_cart表存购物车数据那么它的主键通常是复合主键user_id book_id而不是自增id目的是用数据库唯一约束防止同一本书被重复加入购物车。相关热搜词里出现了“springboot整合flink”这种大数据方向的组合但二手图书系统完全不需要走到那一步单机MySQL加一张购物车表就能支撑几百并发。我见过一个翻车案例某源码把购物车设计成自增id但没加唯一索引结果前端连点两次“加入购物车”后端在service层虽然做了判断却因为两个请求同时到达数据库同时插入了两条相同book_id的记录。这就是前端按钮防重复提交和后端唯一约束要配合的典型场景。如果你拿到的源码是这种结构优先加一个unique indexALTER TABLE t_cart ADD UNIQUE KEY uk_user_book (user_id, book_id);购物车表加了这个约束后MyBatis-Plus的插入接口会直接抛DuplicateKeyException你在service层捕获后改为更新数量即可比单纯依赖代码判断要靠谱得多。2.4 图书检索怎么设计LIKE查询还是全文索引二手图书系统的核心功能是搜书源码里的实现方式决定了查询效率能不能扛住上万条数据。大部分毕业设计级别的源码用的是MySQL的LIKE %keyword%配合book_name、author、publisher三个字段的OR条件。这个写法在数据量小于5万条时问题不大但一旦某个二手图书平台积累了十几万条数据%keyword%前缀没有索引可走全表扫描的延迟会从几十毫秒飙到几百毫秒。进阶一点的做法是用MyBatis-Plus的LambdaQueryWrapper做多条件拼接并且用(book_name LIKE ? OR author LIKE ? OR description LIKE ?)的括号分组避免OR操作符把索引判断搅乱。如果做真正商用应该走Elasticsearch或MySQL全文索引ngram分词但那是后话源码层面能保证的是SQL可读性和扩展性Select(script SELECT * FROM t_book WHERE book_status 1 if testkeyword ! null and keyword ! \\ AND (book_name LIKE CONCAT(%, #{keyword}, %) OR author LIKE CONCAT(%, #{keyword}, %) OR description LIKE CONCAT(%, #{keyword}, %)) /if ORDER BY create_time DESC /script) ListBook searchBooks(Param(keyword) String keyword);这里有个参数细节值得说用CONCAT(%, #{keyword}, %)而不是直接写%${keyword}%前者是预编译占位符能防SQL注入后者是字符串拼接进SQLMySQL执行前就已经把用户输入当作SQL片段解析了。很多在校项目源码会偷懒用${}你在二次开发时第一件事就是全局搜${改成#{}或CONCAT写法。3. 后端核心接口设计从商品上架到订单流转的完整闭环3.1 商品上架与图片上传文件存哪里是最容易含糊的设计二手图书系统的商品发布接口通常是POST /api/book接收的表单里除了图书基本信息还有图片文件。这里的设计决策直接影响部署图片是存本地磁盘还是存OSS。源码包常见做法是存本地上传路径通过配置项upload.path指定然后在WebMvcConfig里加一个资源映射器把/images/**映射到磁盘目录。这样做的好处是内网部署零成本坏处是前后端分离部署时图片请求会跨域需要额外配置。具体实现上SpringBoot接收MultipartFile并保存到本地的代码模板如下PostMapping(/api/book) public Result addBook(RequestParam(file) MultipartFile file, RequestParam(bookName) String bookName, RequestParam(price) BigDecimal price, HttpServletRequest request) { // 拼接存储路径upload.path配置项 日期分目录 String realPath uploadPath File.separator LocalDate.now().toString(); File dir new File(realPath); if (!dir.exists()) { dir.mkdirs(); } // 用UUID重命名防止文件名带中文或特殊字符导致路径报错 String originalFilename file.getOriginalFilename(); String ext originalFilename.substring(originalFilename.lastIndexOf(.)); String filename UUID.randomUUID().toString().replace(-, ) ext; file.transferTo(new File(dir, filename)); // 保存相对路径到数据库前端通过 /images/日期/文件名 访问 book.setCover(/images/ LocalDate.now().toString() / filename); // ...省略bookService.save逻辑 }这里两个参数要重点说明。第一File.separator不要手写/或\\因为Windows和Linux的路径分隔符不同代码里写死会导致换环境部署时报目录不存在。第二文件名用UUID重命名是必须的否则两个用户同时上传封面.jpg后保存的会覆盖先保存的。相关的热词里出现了“vue打包放进springboot中”如果你最终要把前端打包后的dist目录放进SpringBoot的static目录那图片保存路径就不要和jar包放在一起否则每次重新部署jar包时上传的图片会被清掉正确做法是配置外部路径比如/data/book-images用file:${upload.path}做资源映射。3.2 订单创建与库存扣减事务边界和幂等性怎么控制订单创建是二手书系统里最容易写出bug的地方。核心逻辑是买家创建订单时同时把图书状态改为已售这样别人就不能再下单。这个操作必须在一个事务里完成否则会出现订单没建成功但书被锁定的情况或者反过来订单建好了书还在架上的情况。源码里的典型写法是在Service层加Transactional(rollbackFor Exception.class)然后先更新book状态再插入order记录。我一般会额外的做法是把订单号的生成也放进事务里。常见方案有两个一个是数据库自增id直接当作订单号简单但漏单号另一个是用时间戳随机数拼一个业务订单号形如BO yyyyMMddHHmmss 4位随机数。较专业的方案是用MyBatis-Plus的雪花ID很长但是全局唯一。不过对于二手交易系统这种规模时间戳随机数就够了关键是要给订单号加唯一索引这样即使前端重复提交两次请求第二次插入会因唯一键冲突而失败而不是生成两笔相同的订单。幂等性的坑主要体现在前端。热词里有一条“前后端对于按钮重复提交校验方法”搜得很高频说明这是从业者的普遍痛点。在前端处理方案是点击下单按钮后立即把按钮置灰或加loading状态但如果用户用开发者工具直接调接口前端拦截就无效了所以后端必须配合做一层校验。简单粗暴的方案是在OrderService里先查一下是否已存在相同user_id、相同book_id且状态为待付款的订单存在就直接返回该订单id而不是新建Transactional(rollbackFor Exception.class) public Order createOrder(Order order) { // 幂等校验同一用户对同一本书未支付订单只能有一单 LambdaQueryWrapperOrder wrapper new LambdaQueryWrapper(); wrapper.eq(Order::getUserId, order.getUserId()) .eq(Order::getBookId, order.getBookId()) .eq(Order::getStatus, 0); Order exist orderMapper.selectOne(wrapper); if (exist ! null) { return exist; } // 预占图书把book_status改为0已售 int rows bookMapper.updateStatus(order.getBookId(), Book.STATUS_SOLD); if (rows 0) { throw new ServiceException(图书已售出); } orderMapper.insert(order); return order; }注意预占图书这个动作这里的updateStatus方法里的SQL要带条件WHERE book_id ? AND book_status 1返回值rows是受影响行数。如果两个买家同时下单数据库行锁会保证只有一个update语句能拿到1行另一个拿到0行直接抛“图书已售出”这是避免超卖的关键。如果没有这个乐观锁式的更新条件两个请求同时走到insert就会产生两笔订单卖同一本书。3.3 订单状态流转接口谁有权限改状态每个状态能转向哪里在二手图书系统里订单状态不是用户想改就能改的。拿到的源码一般会在OrderController里暴露状态变更接口但真实的权限判断应该在Service层做而不是Controller层。比如买家可以取消待付款订单卖家可以标记已发货买家确认收货才能变成已完成。如果源码里只用RequestParam(status)接收一个目标状态然后直接update那基本上就等于裸奔任何登录用户都能把别人的订单改成已完成。较规范的写法是拆成多个语义明确的接口/** * 买家取消订单仅限status0待付款时执行 */ PostMapping(/api/order/{orderId}/cancel) public Result cancel(PathVariable Integer orderId, HttpSession session) { Integer userId (Integer) session.getAttribute(userId); int rows orderMapper.cancelOrder(orderId, userId, 0); if (rows 0) { // 可能原因订单不存在/不是本人订单/状态不是待付款 throw new ServiceException(订单无法取消请确认订单状态); } // 同时把图书重新上架 bookMapper.updateStatusByOrderId(orderId, Book.STATUS_ON_SALE); return Result.success(); }这里的关键是把条件和变更放在一条UPDATE语句里UPDATE t_order SET status 4 WHERE id ? AND user_id ? AND status 0而不是先查出订单再判断再UPDATE。原因和前面防超卖一样避免并发下两个请求同时读到status0然后都执行了取消操作。至于“卖家确认发货”接口则应该校验当前登录用户是不是这本书的卖家判断逻辑是Book表里有一个seller_id或user_id字段要用它的值和session中的userId比对而不是用order表里的字段判断。状态机相关的参数配置建议在源码的枚举或常量类里维护一个Map记录每个状态允许流转到的目标状态。别写在SQL注释里也别散落在Service的if判断里否则改需求时你会漏掉某一个入口。4. 前端联调与SpringBoot整合Vue项目怎么和后端真正常连通4.1 前后端分离的请求链路Axios封装与跨域配置拿到源码包后前端部分通常是一个Vue 2项目目录结构是src/views、src/api、src/router这种标准布局。你要做的第一件事不是看页面长什么样而是在src/api/request.js里找Axios的封装基础配置baseURL指向哪里、请求拦截器做了什么、响应拦截器如何统一处理code和message。如果源码里baseURL写的是http://localhost:8080而后端跑在9090端口那所有请求都会404或跨域。最稳妥的开发环境方案是在vue.config.js里配置devServer的proxy代理// vue.config.js module.exports { devServer: { port: 8080, proxy: { /api: { target: http://localhost:9090, changeOrigin: true, pathRewrite: { ^/api: /api } } } } };这样写的好处是前端代码里所有的请求路径都以/api开头开发时浏览器不直接访问后端避免跨域部署时把dist文件放到Nginx或直接放进SpringBoot的static目录由Nginx反向代理转发/api到Java后端前端代码不需要改任何baseURL。很多人在这个环节容易翻车的是开发时正常部署到服务器后图片加载不出来或接口401多是因为Nginx没有把图片路径也加入代理规则或后端的CorsFilter只允许了localhost:8080这个来源。4.2 登录态怎么维持Session还是JWT二手图书交易系统源码里登录态的实现方式五花八门。最常见的毕业设计写法是HttpSession存userId后端接口通过session.getAttribute(userId)取用户身份前端登录后把用户名存localStorage。这种方式在小规模项目里没问题前后端保持一致部署也能跑。但如果你拿到了使用JWT的版本就需要额外关注token的存放位置通常是登录后后端返回token前端存在localStorage或sessionStorage然后在Axios请求拦截器里加Authorization: Bearer ${token}头浏览器刷新页面后通过token解析出用户信息。实战中我推荐后端用JWT但同时也把userId放在session里的混合方案——因为二手书系统的管理员后台和用户端是两个不同角色JWT只承担身份凭证权限判断还是在后端拦截器里根据userId查角色。源码里如果你看到SpringMVC的拦截器配置了addPathPatterns(/api/**)和excludePathPatterns(/api/login, /api/register, /api/book/list, /api/book/detail)就说明静态资源或无需登录的查询接口已经做了白名单不要随便删掉这几个排除项否则前端一刷新页面就弹登录框。前端axios拦截器里有一个和前文重复提交相关的经典坑响应拦截器里对后端返回的code做判断如果res.data.code 401就跳转登录页。但有些源码把401既用作未登录又用作业务错误码导致下拉框加载失败也强制跳登录这是我没少见的迷惑操作。正确的做法是后端统一定义code200成功、401未登录、500业务异常前端只对401做跳转处理。4.3 图书列表的分页与筛选条件前端参数怎么拼图书列表页一般有三种筛选维度分类、价格区间、书名关键词。前端请求参数通常是pageNum、pageSize、categoryId、minPrice、maxPrice、keyword。后端用MyBatis-Plus的Page分页插件接收这里有个参数命名陷阱——MyBatis-Plus的分页对象默认用的是current和size而不是pageNum和pageSize很多源码在Controller层做了转换。如果你拿到的源码里没有转换前端传pageNum后端收不到就会导致点击页码没反应一直在第一页。分页参数的转换逻辑很简单但新手容易忽略的还有排序字段的合法性校验。如果前端能把orderByColumn传进来后端直接拼到SQL里就存在注入风险比如传一个create_time; DROP TABLE。正规做法是用白名单映射// 允许排序的字段映射表防止SQL注入 private static final MapString, String ORDER_FIELD_MAP new HashMap(); static { ORDER_FIELD_MAP.put(createTime, create_time); ORDER_FIELD_MAP.put(price, price); ORDER_FIELD_MAP.put(sales, sales); }这样前端的orderByColumn只允许传createTime、price、sales三个值其它一律按默认排序处理。二手图书系统虽然攻击面不大但这属于随手可做的基础安全加固。5. 部署上线与常见问题排查从源码压缩包到能跑通的完整链路5.1 数据库导入为什么sql文件导入MySQL老报错源码包里通常有一个.sql文件导入前最重要的一步是确认MySQL版本。如果sql文件是MySQL 8.0导出的里面可能带utf8mb4_0900_ai_ci排序规则而本地MySQL是5.7就导入失败报错信息类似Unknown collation: utf8mb4_0900_ai_ci解决思路是全局替换排序规则为utf8mb4_general_ci。反过来如果sql是5.7导出的在8.0里导入则大概率没问题但还是建议先看sql文件开头的CREATE TABLE语句里有没有ENGINEInnoDB DEFAULT CHARSETutf8mb4没有的话需要手动加上。另一个高频报错是用Navicat导入时选错字符集。出现中文乱码的原因通常是sql文件本身是UTF-8编码而数据库连接的字符集却是latin1或gbk。排除办法是在导入前先执行一条SET NAMES utf8mb4;或者在Navicat的连接属性里把编码设为utf8mb4。我遇到过一个更隐蔽的问题sql文件里的emoji表情比如图书备注字段带在utf8mb3字符集下插入报错需要把表级别和字段级别的charset都改成utf8mb4。如果导入后部分字段依然是乱码还能用ALTER TABLE t_book CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;兜底修复。相关热词里有“mysql 5.7.44 安装过程详细”“mysql安装配置教程”这类高频检索词说明连接错字符集和版本不匹配是这批源码最常见的卡点。5.2 启动项目端口冲突和配置漂移的处理SpringBoot后端默认端口如果源码里没有在application.yml中显式配置那就是8080。前端Vue也默认8080两边必然有一个要改。较稳妥的方案是后端用9090前端devServer用8080然后在application.yml加一句server.port: 9090。这里有个细节如果同时起了多个SpringBoot项目8080端口被占用会直接报Port 8080 was already in use错误用netstat -ano | findstr 8080Windows或lsof -i:8080Linux/macOS找到占用端口对应的PID结束进程即可。排查完端口再看配置application.yml里的datasource url、redis地址、upload.path这些配置项我通常会单独检查一遍因为换机器跑源码时最常见的问题就是把别人的数据库连接串原封不动复制过来连的还是别人内网地址。启动成功后第一步验证不是直接开前端页面而是先访问http://localhost:9090/api/book/list?pageNum1pageSize10看能不能拿到JSON。这一步能确认后端、数据库、端口三层都没问题。注意接口路径是否带/api前缀由后端ContextPath决定如果application.yml里设置了server.servlet.context-path: /api则实际访问路径是http://localhost:9090/api/api/book/list很多新手的困惑就来自这里多了一层路径没看清。5.3 常见异常前后端数据不一致和事务回滚的五个经典坑以下是这套源码里绕不开的五个踩坑记录按实际出现频率排序。坑一图片上传后前端访问404。现象是上传成功后图片能存到本地目录但浏览器访问http://localhost:9090/images/2025-06-01/xxx.jpg报404。原因一般是SpringBoot的静态资源映射没有覆盖外部磁盘路径因为SpringBoot默认只映射classpath:/static/下的资源。解决方法是实现WebMvcConfigurer.addResourceHandlers把/images/**映射到file:${upload.path}。较专业的做法是配置一个ResourceHandlerRegistry而不是在拦截器里放行图片路径。坑二事务方法自调用导致回滚失效。现象是service里一个方法调了同类里的另一个标注了Transactional的方法但异常发生后数据没有回滚。原因是Spring的声明式事务基于AOP代理同类自调用绕过了代理对象。解决方法是把事务方法拆分到另一个Service类中或者在调用处也加Transactional。坑三前端传日期字符串后端报400。比如用户填了个性化出版日期2024-05-20后端实体类用LocalDate接收但没有配置DateTimeFormat或全局Jackson的日期格式前端就收到HTTP 400。解决方法是统一在application.yml里配置Jackson序列化格式spring.jackson.date-format: yyyy-MM-dd HH:mm:ss或者在后端接收参数的实体字段上标注JsonFormat(pattern yyyy-MM-dd)而不要依赖前端toString后拼字符串。坑四MyBatis-Plus的updateById把null字段也更新成NULL。这是一个常见误用前端传送了部分字段后端直接用updateById就会把没传的字段置为NULL。原因是MyBatis-Plus的字段策略默认是NOT_NULL但如果你在实体类字段上手动加了TableField(updateStrategy FieldStrategy.ALWAYS)就会导致null覆盖原值。解决方式是从代码里去查实体类上有没有这类注解有就删掉或者所有更新操作都用UpdateWrapper.set显式指定字段不用实体传参。坑五前后端联调时请求体是JSON但后端要求表单。现象是前端用axios.post(url, data)默认发送application/json而后端接口用RequestParam接收参数结果所有参数为null。解决方法是后端改用RequestBody接收JSON对象前端则要确保Content-Type为application/json。这是最容易翻车的基础问题遇到参数全Null先往这个方向排查。5.4 前端打包放进SpringBoot静态资源合并部署的特殊问题热词里有“vue打包放进springboot中”这也是很多人在部署二手图书系统时想做的事后端一个jar包搞定全部不需要单独开Nginx。做法不复杂前端npm run build生成dist目录把dist里的文件复制到后端src/main/resources/static/下再重新打包。但有几个必须处理的边界第一路由问题。Vue Router如果用的是history模式直接访问http://localhost:9090/admin会404因为SpringBoot不知道该把请求转发到index.html。解决方法是后端加一个WebMvcConfigurer把非/api开头的路径转发到/index.html但实际上更简单的是把Vue Router改成hash模式url里会带#不会触发后端路径匹配。第二API路径合并问题。前端打包后http请求路径如果本来就是/api开头后端需要处理context-path。常见做法是在后端配置server.servlet.context-path: /api同时前端axios的baseURL写/api这样jar包内静态页面上的接口请求为/api/api/...需要再检查一遍。比较推荐的做法是前端baseURL留空只用/api前缀后端不设置context-path前端资源映射单独把静态文件暴露出来请求都打到/api后端的Controller路径上。第三图片存储路径问题。前面已经提过上传的图片不能放在static目录里一定要用外部路径。合并打包后jar包本身是不允许运行时往内部classpath写文件的即使写进去也会在下次部署时丢失。6. 从能跑通到能交付数据初始化脚本、测试用例与交接文档的补充技巧接手任何一个二手图书系统源码别只满足于页面能打开、能登录、能下单。要把它变成能交付、可二次开发的项目还需要在三个点上做加固第一验证初始化数据的完整性检查是否有测试账号和测试图书数据我一般在数据库脚本末尾加三条用户记录一个管理员、一个卖家、一个买家和十条覆盖不同价格段的图书记录方便演示时快速走通全流程第二补充后端的关键接口测试用例重点覆盖订单状态机的非法流转和商品超卖场景第三把配置项整理成一份部署清单包括MySQL连接串、上传路径、前端访问端口、nginx转发规则避免假期回来自己都忘了当初是怎么跑起来的。验证订单状态机的一个实用技巧是直接写一个Junit测试类走一遍“下单-付款-发货-完成”和“下单-取消-图书重新上架”这两个链路。用H2内存数据库跑测试会比连本地MySQL更快但要注意H2的模式名和MySQL不同t_order这种表名需要加上schema前缀。如果暂时不想写测试也可以用Postman或Apifox建一个二手图书系统的接口集合把下单接口的参数里bookId改成一个不存在的大数字看看后端有没有正确返回异常而不是崩掉。前后端对于重复提交校验的动态效果我习惯的验证方式是后端接口在update库存前加Thread.sleep(100)模拟网络延迟同时打开两个浏览器标签页用同一个账号同一本书下单看是否只有一单成功另一单被幂等逻辑挡回。这种并发验证在开发环境就能完成不需要压测工具。如果你拿到的源码没有幂等逻辑这一步会直接暴露问题这也是本系统最能体现工程质量的一道关卡。第一次全面排查时记得看一下项目里是否包含.gitignore文件它决定了target目录和node_modules是否会被提交进版本库。没有的话自己补一份别让构建产物污染源码仓库。最后把说明文档里的数据库账号密码改成你自己的如果源码里把密码明文写在application.yml中记得提醒自己上线时改用环境变量注入因为SpringBoot本身支持${MYSQL_PASSWORD}占位符这个习惯会帮你避免很多不安全的部署。希望这些排查顺序和踩坑记录能帮你省下几个晚上的调试时间这套老牌技术栈虽然不够新潮但把它跑通、改好、部署出去你对SpringBoot整合MySQL全链路的理解会扎实很多。本文还有配套的精品资源点击获取
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进