
简介这是一套基于Spring Boot与MySQL的家具销售电商平台项目源码及配套文档面向毕业设计、课程设计及自学电商开发的读者可完整还原从用户端购物到后台管理的系统闭环。压缩包约为24.89MB内含完整工程源码、设计文档、README项目说明与开发环境配置说明便于快速还原项目运行环境。源码按Controller、Service、Model、Repository分层组织覆盖注册登录、商品浏览、购物车、订单处理等核心模块并配有后台管理功能能完整看出请求处理、业务逻辑与MySQL数据库交互的实现链路。文档部分整理了需求分析、功能模块划分、数据库表结构及接口设计等要点既适合跟读源码也便于后续二次扩展。目前已有537人学习下载对提升Spring Boot实战能力、掌握电商平台常见实现方式很有参考价值。1. 家具电商平台到能做出来还差什么Spring Boot MySQL 先把骨架搭对打开一个标题挂着“基于 Spring BootMySQL 的家具销售电商平台系统设计与实现源码文档”的压缩包外行先看页面截图内行先翻数据库脚本和订单事务。这套系统的本质并不复杂前台做商品展示、搜索、购物车和下单后台做类目、商品和订单管理文档则把需求分析、ER 图、用例图和测试用例补到能过答辩的程度。它真正值钱的地方不在功能多少而在表结构是否合理、下单时库存会不会出错、鉴权能不能说清楚。下面我按最常见的毕业设计和小型商用方案把数据库设计、核心接口、文档匹配和避坑点一次讲透让你拿到这类源码后能独立复现、改得动还能答得上追问。2. 数据库与模块设计用 8 张表撑起商品、购物车和订单我拿到这类电商源码的第一件事不是急着启动而是把建表脚本和模块划分对一遍。表结构决定了库存扣减能不能做对、订单后续能不能扩展支付和退款这部分错了后面全是返工。以下这套设计不是唯一答案但它能覆盖毕业设计评审最常问的“高内聚、低耦合”也够支撑一个真实可用的商城后端。2.1 模块切分前台商城、后台管理、订单中心三块先按业务边界把系统拆成三个模块文档里的模块图和代码里的包结构都按这个来。前台用户模块注册登录、商品分类浏览、搜关键词、购物车增删改、提交订单。后台管理模块类目管理、商品上下架、库存调整、订单状态流转、用户列表。公共支撑模块统一返回结构、全局异常处理、图片上传、配置文件管理。这样切的原因是职责单一。前台只管“用户怎么买”后台只管“运营怎么改”公共模块处理横切逻辑。代码里对应的包结构一般是controller/service/mapper/entitycontroller 只做参数接收和返回业务判断全部下沉到 servicemapper 只碰 SQL。答辩被问到“为什么这样分层”时这句话就能顶上。2.2 建表 SQL 的最小集合用户、类目、商品、购物车、订单、订单项一张用户表、一张类目表、一张商品表、一张购物车表、一张订单主表、一张订单明细表这是电商平台的六张核心表。如果要做得更完整再加收货地址表和后台操作日志表。下面这套 DDL 以 MySQL 8.0 为准字符集统一用 utf8mb4引擎统一用 InnoDB。-- 用户表管理员和普通用户用 role 区分 CREATE TABLE t_user ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT 登录名, password VARCHAR(128) NOT NULL COMMENT BCrypt 密文, nickname VARCHAR(50) DEFAULT , role TINYINT NOT NULL DEFAULT 2 COMMENT 1 管理员 2 普通用户, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户表; -- 商品类目表 CREATE TABLE t_category ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, name VARCHAR(50) NOT NULL, sort INT NOT NULL DEFAULT 0 COMMENT 排序值越小越靠前, PRIMARY KEY (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品类目; -- 商品表 CREATE TABLE t_product ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, category_id INT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, subtitle VARCHAR(255) DEFAULT , main_image VARCHAR(255) DEFAULT COMMENT 主图 URL存相对路径, price DECIMAL(12,2) NOT NULL COMMENT 售价精确到分, stock INT UNSIGNED NOT NULL DEFAULT 0, status TINYINT NOT NULL DEFAULT 1 COMMENT 1 上架 0 下架, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), KEY idx_category (category_id), KEY idx_status (status) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品表; -- 购物车表 CREATE TABLE t_cart_item ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, user_id INT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, quantity INT UNSIGNED NOT NULL DEFAULT 1, checked TINYINT NOT NULL DEFAULT 1 COMMENT 是否选中结算时用, PRIMARY KEY (id), UNIQUE KEY uk_user_product (user_id,product_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT购物车; -- 订单主表 CREATE TABLE t_order ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_no VARCHAR(40) NOT NULL COMMENT 业务订单号, user_id INT UNSIGNED NOT NULL, total_amount DECIMAL(12,2) NOT NULL, status TINYINT NOT NULL DEFAULT 10 COMMENT 10 待付款 20 已付款 30 已发货 40 已完成 50 已取消, address_snapshot VARCHAR(255) DEFAULT COMMENT 收货地址快照, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_order_no (order_no), KEY idx_user (user_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单主表; -- 订单明细表 CREATE TABLE t_order_item ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, order_id BIGINT UNSIGNED NOT NULL, product_id INT UNSIGNED NOT NULL, product_name VARCHAR(100) NOT NULL COMMENT 冗余商品名称防止商品被删后订单不可读, product_image VARCHAR(255) DEFAULT , price DECIMAL(12,2) NOT NULL COMMENT 下单时的成交价快照, quantity INT UNSIGNED NOT NULL, PRIMARY KEY (id), KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT订单明细;这套表设计里有几个容易被忽略但很关键的细节。第一购物车表用user_id product_id建了唯一索引。往购物车加商品时不需要先 select 再判断是否存在直接执行INSERT ... ON DUPLICATE KEY UPDATE quantity quantity 1一次 SQL 解决问题省一次查询也避免并发重复插入。第二订单明细表冗余了product_name和price这是刻意为之因为商品可能下架、改名或调价订单历史必须保留下单那一刻的快照。第三订单主表的address_snapshot存的是一次下单时的完整收货地址字符串而不是关联地址表 ID原因同样是快照思维。2.3 字段选型与索引取舍金额用 DECIMAL、库存用 INT UNSIGNED字段类型是这类源码里最容易埋雷的地方我逐一说明选型理由。金额必须用DECIMAL(12,2)不能碰 FLOAT 和 DOUBLE。二进制浮点在运算时会出现 0.10.20.30000000000000004 这种误差订单金额一旦有偏差对账时极其痛苦。Java 实体类里对应的类型也要用BigDecimal并且金额运算必须走BigDecimal的方法。库存用INT UNSIGNED加了 UNSIGNED 后数据库层就不会出现负数库存但不要指望它兜底后面仍要在 SQL 里加stock 数量的条件。订单号用VARCHAR(40)存业务号不要直接暴露自增主键给前端通用做法是“时间戳 用户ID后四位 随机数”拼一段 20 位左右的字符串。索引方面我习惯遵循一个原则等值查询建普通索引高频组合查询建联合索引索引列不要参与函数运算。商品表按category_id和status分别建索引因为前台列表页最常见的两种查询是“按类目看”和“只看上架商品”。订单表按user_id建索引支撑“我的订单”列表。订单明细表按order_id建索引支撑订单详情页。至于如“按价格区间查家具”这种低频需求数据量没到几十万时MySQL 全表扫也能接受不必为了它堆太多索引。3. 核心接口落地登录鉴权、商品检索与下单的 Spring Boot 最小实现表结构定下来后就该把代码骨架搭起来。网上搜 spring boot mybatis 的教程一大把但很多包有一个通病依赖版本随意、拦截器没生效、事务注解失效。这一章我按最常规的毕业设计路径走用 Spring Boot 2.7 MyBatis兼顾文档好写和运行稳定。3.1 项目结构与依赖pom.xml 里到底该放哪些东西先看包结构拿到源码后对照这个结构检查完整度。com.furniture.mall ├── MallApplication.java ├── config │ └── WebConfig.java // 拦截器注册、跨域配置 ├── controller │ ├── UserController.java │ ├── ProductController.java │ ├── CartController.java │ └── OrderController.java ├── service │ ├── UserService.java │ ├── CartService.java │ └── OrderService.java ├── mapper │ ├── UserMapper.java │ ├── ProductMapper.java │ ├── CartItemMapper.java │ └── OrderMapper.java ├── entity ├── common │ ├── Result.java // 统一返回体 │ └── GlobalExceptionHandler.javapom.xml 里的依赖我一般只保留五类web、validation、mybatis、mysql 驱动、lombok。够用且不容易出兼容问题。parent groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-parent/artifactId version2.7.18/version /parent dependencies dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-web/artifactId /dependency dependency groupIdorg.mybatis.spring.boot/groupId artifactIdmybatis-spring-boot-starter/artifactId version2.3.2/version /dependency dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId scoperuntime/scope /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-validation/artifactId /dependency dependency groupIdorg.projectlombok/groupId artifactIdlombok/artifactId optionaltrue/optional /dependency /dependencies这里有两个版本细节要特别说明。Spring Boot 2.7 用的是javax.servlet命名空间如果换成 Spring Boot 3.x所有javax.*要改成jakarta.*拦截器注册的写法也有细微差别MyBatis 的 starter 版本要和 Spring Boot 版本匹配2.3.2 对应 Spring Boot 2.x 是经过大量实践验证的稳定组合。MySQL 驱动在 8.x 后包名改成了com.mysql.cj.jdbc.Driver所以 url 里带不带driver-class-name都要写对这个问题后面避坑章再展开。3.2 登录鉴权Session 还是 JWT毕业设计怎么选很多源码包一上来就上 JWT但拦截器配置和 token 刷新逻辑写得一塌糊涂。我的建议是毕业设计优先用 Session 拦截器理由有三点不引入额外依赖、代码量少、答辩时能把“会话保持”讲清楚。如果你的简历想写 JWT那就在 Session 基础上换掉令牌生成部分业务逻辑基本不变。先看登录接口。密码在数据库里存的是 BCrypt 密文所以登录服务里要做的是matches校验而不是把数据库里的密码捞出来比明文。RestController RequestMapping(/api/user) public class UserController { Resource private UserService userService; PostMapping(/login) public Result login(RequestBody LoginDTO dto) { User user userService.login(dto.getUsername(), dto.getPassword()); if (user null) { return Result.fail(用户名或密码错误); } // 只存最小必要信息不要把整个用户对象塞进 Session request.getSession().setAttribute(userId, user.getId()); request.getSession().setAttribute(role, user.getRole()); return Result.success(user); } }Service 层的登录逻辑要注意两点第一先按用户名查出用户查不到直接返回失败避免做无意义的密码比对第二密码比对用passwordEncoder.matches(rawPassword, user.getPassword())这样即使数据库泄露也不会直接暴露明文。拦截器的注册放在 WebConfig 里排除登录、注册和商品浏览这些不需要鉴权的路径。很多源码包在这里翻车最常见的是拦截器配了addPathPatterns(/**)但没有把静态资源和前端页面放行导致登录页样式全丢或者登录接口被拦截。Configuration public class WebConfig implements WebMvcConfigurer { Resource private LoginInterceptor loginInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(loginInterceptor) .addPathPatterns(/api/**) .excludePathPatterns( /api/user/login, /api/user/register, /api/product/**, /api/category/** ); } }拦截器里从 Session 取 userId取不到就返回 401 JSON而不是直接response.sendRedirect因为接口和页面要分开处理。3.3 商品检索与购物车Service 层怎么写才能不给后续留坑商品列表页用分页查询后端返回页码、总条数和当前页数据。我习惯用 MyBatis 的 PageHelper但注意 PageHelper 对线程有隐式依赖使用时要确保它在 mapper 方法调用前设置而且只对紧随其后的一条查询生效。Service public class ProductService { Resource private ProductMapper productMapper; public PageResultProduct page(int pageNum, int pageSize, Integer categoryId) { PageHelper.startPage(pageNum, pageSize); ListProduct list productMapper.selectByCondition(categoryId, 1); PageInfoProduct pageInfo new PageInfo(list); return PageResult.of(pageInfo.getTotal(), pageInfo.getList()); } }商品表查询只返回status 1的商品这是硬过滤条件防止后台下架的商品还能被前台访问。购物车加购接口则利用唯一索引做“存在即累加”。public void addToCart(Long userId, Long productId, Integer quantity) { CartItem cartItem new CartItem(); cartItem.setUserId(userId); cartItem.setProductId(productId); cartItem.setQuantity(quantity); cartItemMapper.insertOrUpdate(cartItem); }对应的 Mapper 用一条INSERT ... ON DUPLICATE KEY UPDATE完成MySQL 会自动检测uk_user_product唯一索引冲突。这是整个购物车模块性能最好的写法没有先查后改的竞态窗口。删除、修改数量也都是围绕这个表做逻辑简单但边界要清晰改数量时 quantity 不能小于 1删除时只能删当前用户的数据所以 SQL 的 WHERE 条件必须带user_id。3.4 下单事务订单主表 订单项 扣库存的边界下单是整个系统最复杂的链路也是答辩时最能拿分的点。一个规范的下单事务应该包含四件事校验商品和数量、锁定商品行、扣减库存、写订单主表和明细表。顺序上我先锁行再扣库存最后写订单这样能最大限度缩短库存锁的持有时间。Transactional(rollbackFor Exception.class) public Order createOrder(Long userId, ListCartItem items) { // 1. 收集商品 ID 列表 ListLong productIds items.stream().map(CartItem::getProductId).collect(Collectors.toList()); // 2. 按 ID 批量查商品并加行锁防止并发修改 ListProduct products productMapper.selectByIdsForUpdate(productIds); MapLong, Product productMap products.stream() .collect(Collectors.toMap(Product::getId, p - p)); // 3. 校验库存并计算总价 BigDecimal total BigDecimal.ZERO; for (CartItem item : items) { Product product productMap.get(item.getProductId()); if (product null || product.getStock() item.getQuantity()) { throw new RuntimeException(商品库存不足 item.getProductId()); } total total.add(product.getPrice().multiply(BigDecimal.valueOf(item.getQuantity()))); } // 4. 扣库存SQL 里加 stock quantity 条件防超卖 for (CartItem item : items) { Product product productMap.get(item.getProductId()); int updated productMapper.deductStock(item.getProductId(), item.getQuantity()); if (updated 0) { throw new RuntimeException(扣库存失败请重试); } } // 5. 生成订单号并插入订单和订单明细 Order order new Order(); order.setOrderNo(generateOrderNo(userId)); order.setUserId(userId); order.setTotalAmount(total); order.setStatus(10); orderMapper.insert(order); for (CartItem item : items) { Product product productMap.get(item.getProductId()); OrderItem orderItem new OrderItem(); orderItem.setOrderId(order.getId()); orderItem.setProductId(product.getId()); orderItem.setProductName(product.getName()); orderItem.setProductImage(product.getMainImage()); orderItem.setPrice(product.getPrice()); orderItem.setQuantity(item.getQuantity()); orderItemMapper.insert(orderItem); } return order; }第三步和第四步之间为什么不能拆开如果先查库存再在内存里判断库存足够然后单独执行update stock stock - 1两个人同时下单就会同时通过检查把 1 件商品扣成 -1。所以扣库存的 SQL 必须带上条件UPDATE t_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}返回更新行数为 0 就说明库存不够或者商品已下架事务整体回滚订单不会落库。Transactional的rollbackFor Exception.class也要写全因为 Spring 默认只在遇到 RuntimeException 时回滚如果手动抛的是受检异常事务不会回滚。四步操作任何一步失败订单、订单明细、库存扣减一起回滚这就是事务边界的意义。4. 源码之外的文档要怎么和源码对齐用例图、ER 图和验收对照表标题里带了“文档”两字说明这套源码不只是能跑还要有一份能放进毕业论文或者项目说明书的材料。很多人在这个环节翻车写出来的文档和代码对不上图里画了三张表代码里实际有六张用例图写了“用户管理”代码里根本没有后台用户管理接口。文档和源码不一致答辩时被追问一句“你这里写的和实现不一致”就很难圆回来。4.1 文档目录结构从需求分析到测试用例的 7 个章节毕业论文或者项目文档按这个目录写基本能覆盖主流要求。文档章节内容要点对应源码位置1. 绪论研究背景、意义、国内外现状无纯文字2. 需求分析功能需求、非功能需求、用例图对照已实现的模块列功能点3. 概要设计技术架构、模块划分、数据库 ER 图对应包结构、数据库表4. 详细设计表结构、核心接口时序图、关键类说明对应实体类、核心 service 方法5. 系统实现页面截图、接口调用说明、核心代码片段对应 controller、页面6. 系统测试测试环境、核心功能测试用例表对应手工执行的验证记录7. 总结不足与展望客观写别乱吹拿到源码包先做映射每个文档章节能对应到代码里具体位置才算过关。如果包里的文档缺了某一章补写时以源码为准不要照网上的模板硬套否则查重和答辩都会很难受。4.2 用 ER 图和用例图把表结构反推成论文插图ER 图不要凭空画直接从建表脚本反推。每张表是一个实体表之间的外键关系就是实体关系。以这套六表设计为例t_user 和 t_order 是一对多t_order 和 t_order_item 是一对多t_product 和 t_order_item 是一对多t_category 和 t_product 是一对多t_user 和 t_cart_item 是一对多。用 draw.io 或者 Visio 画出来就行实体属性列主要字段不要全字段罗列。用例图也一样按角色画。普通用户有注册、登录、浏览商品、搜索、加购、结算、查看订单这些用例管理员有商品管理、类目管理、订单管理、用户管理这些用例。每个用例都要能对应到实际的 controller 接口写文档时我习惯做一个映射表把用例和接口路径一一列出。这个表在答辩时非常有用老师问“你这个功能在哪”直接报接口路径。4.3 验收对照每个页面/接口对应论文里的哪一节为了避免文档和代码“两张皮”我在整理项目时一定会做一张对照表放在文档的附录或者测试章节里。前端页面/功能后端接口论文对应章节注册页POST /api/user/register4.1 用户模块详细设计登录页POST /api/user/login4.1 用户模块详细设计商品列表页GET /api/product/list4.2 商品模块详细设计商品详情页GET /api/product/{id}4.2 商品模块详细设计购物车页GET/POST /api/cart4.3 购物车模块详细设计提交订单POST /api/order/create4.4 订单模块详细设计我的订单GET /api/order/my4.4 订单模块详细设计后台商品管理POST /api/admin/product4.5 后台管理模块详细设计这张对照表写完之后论文每个章节写什么就非常清晰。写系统实现章节时每个页面截图的旁边配一段接口说明写测试章节时按这张表逐项填测试结果即可。文档这块还有一个实用技巧多用表格和截图替代大段文字既能撑起篇幅又能降低查重率图表中的数据要和源码实际跑出来的结果一致不要编造测试数据。5. 避坑手册Spring Boot MySQL 电商项目最容易翻车的 5 个位置这一章是我做这类项目时踩过最痛的几个坑每条按“现象 → 原因 → 解决”的顺序讲基本涵盖从环境搭建到部署上线的常见问题。如果你拿到源码后第一步跑不起来先按这个清单排错。5.1 MySQL 8 时区报错和驱动类名一堆乱码的异常信息现象Spring Boot 启动时或首次查询时报The server time zone value йʱ is unrecognized或者干脆报ClassNotFoundException: com.mysql.jdbc.Driver。原因MySQL 8.x 的驱动类名已经从com.mysql.jdbc.Driver改成了com.mysql.cj.jdbc.Driver低版本依赖或配置还在用老类名同时 MySQL 8 对连接串里的时区要求更严格不显式指定serverTimezone就会用服务器本地时区解析一旦系统时区不是标准 UTC 就报错。解决把数据源配置固定成下面这套这是经过反复验证的稳定写法。spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://localhost:3306/furniture_mall?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalseallowPublicKeyRetrievaltrue username: root password: 123456注意allowPublicKeyRetrievaltrueMySQL 8 默认的密码认证方式在部分 JDBC 驱动下会要求先获取公钥不加这个参数启动时可能报Public Key Retrieval is not allowed。如果密码里有特殊字符url 里还要做 URL 编码否则连不上数据库还看不出明显原因。5.2 本地能跑打包部署就挂图片全 404现象IDE 里点运行一切正常mvn package后用java -jar启动接口能通但图片和静态资源 404或者启动直接报端口被占用。原因开发时用了src/main/resources/static/upload这种相对路径IDE 能识别但打成的 jar 是压缩包Java 代码并不能像操作系统目录那样往里写文件另外图片如果存了绝对路径部署到另一台机器路径就失效。端口占用则多半是旧进程没杀掉。解决文件上传目录改用绝对路径并写进配置文件不放在 classpath 里。app: upload-dir: /var/www/furniture-mall/upload后端代码里拼接访问 URL 时前缀和物理路径解耦存数据库只存相对路径访问时再拼完整前缀。打包部署前先用lsof -i:8080查端口确保旧进程已停止。静态资源映射在 WebConfig 里单独加一个ResourceHandler把磁盘路径映射到/files/**这样图片在本地和服务器上都能正常显示。5.3 金额用 Double订单总额差一分钱现象订单明细加起来 399.00 99.00 498.00订单总额却算出 498.00000000000006数据库里显示成 498.00页面展示正常但程序里 if 判断等值会失败。原因FLOAT 和 DOUBLE 是二进制浮点十进制小数转二进制后多数是无限循环运算时必然出现精度误差。这是浮点数的内在限制不是代码写错。解决数据库金额字段改成DECIMAL(12,2)Java 实体类用BigDecimal加购和下单计算用BigDecimal的add/multiply方法不要用和*。从数据库查出来的值赋给BigDecimal时用resultSet.getBigDecimal()不要先getString再手工转。public BigDecimal calcTotal(ListOrderItem items) { BigDecimal total BigDecimal.ZERO; for (OrderItem item : items) { BigDecimal subtotal item.getPrice().multiply(BigDecimal.valueOf(item.getQuantity())); total total.add(subtotal); } return total.setScale(2, RoundingMode.HALF_UP); }setScale(2, RoundingMode.HALF_UP)是最后一个保险所有涉及金额运算的结果统一保留两位小数并四舍五入。5.4 并发下单把库存扣成负数现象用脚本模拟 50 个用户同时买同一种只剩 20 件库存的商品最终订单创建了 50 条库存变成负数。原因代码写成了“先 select 查库存Java 里判断库存是否充足再 update 减库存”。这个流程在单线程下没问题并发时多个线程同时通过了库存判断再各自执行更新最终库存被扣穿。解决扣库存 SQL 必须把条件写进 update 语句见 3.4 节那段UPDATE t_product SET stock stock - #{quantity} WHERE id #{productId} AND stock #{quantity}。执行后判断影响行数为 0 就抛异常回滚。这一步不依赖任何“锁表”操作纯粹靠条件更新保证原子性是电商库存扣减的通用解法。5.5 事务注解失效库存扣了订单没生成现象下单时库存成功扣减但订单插入抛了异常事务回滚后库存却没有恢复数据不一致。原因有三种常见现场。第一种Transactional加在了同类内部的某个方法上被另一个方法自调用Spring 事务代理不生效第二种方法内部用 try/catch 吞掉了异常事务框架感知不到异常自然不会回滚第三种表引擎是 MyISAM压根不支持事务Spring 的事务注解形同虚设。解决事务方法必须是 public 且通过外部调用进入把 try/catch 去掉或者 catch 到异常后再throw new RuntimeException(e)同时确认所有表都用 InnoDB。确认是否生效的土办法在事务方法里故意抛一个异常看数据是否回滚这个方法比读一百篇原理文章都直观。6. 进阶验证模拟并发下单、修复超卖并打包部署做完上面的功能系统只是“能跑”离“能证明没大问题”还差一步。我一般会做三类验证并发下单测试、商品浏览缓存对比、生产打包部署。这三件事做完答辩和实际演示基本都能稳住。6.1 用 Python 脚本做 50 并发下单验证不需要引入 JMeter一个简单的 Python 脚本就能验证库存不会再扣成负数。提前把商品库存设为 10然后用 50 个线程同时下单。import requests from concurrent.futures import ThreadPoolExecutor url http://localhost:8080/api/order/create headers {Cookie: JSESSIONID你的会话ID, Content-Type: application/json} def buy(i): payload {productId: 1, quantity: 1} resp requests.post(url, jsonpayload, headersheaders, timeout5) return resp.json().get(code) with ThreadPoolExecutor(max_workers50) as executor: codes list(executor.map(buy, range(50))) success codes.count(0) fail len(codes) - success print(f成功: {success}, 失败拦截: {fail}) print(剩余库存应等于:, 10 - success)正确的结果是只有 10 个请求成功40 个请求被库存不足拦下数据库库存归零。如果成功数超过 10说明扣库存 SQL 没加条件回 5.4 节检查。这组数据截图放进论文测试章节比任何文字描述都有说服力。6.2 商品详情加一层本地缓存MySQL 扛住首页列表不难但商品详情在促销场景会被高频访问。常见的优化方案是加 Redis但毕业设计不一定需要引入额外中间件用 Caffeine 本地缓存也能把单机查询压力降下来。这一步不是必须但如果访谈讲到性能优化这是能拿得出手的点。缓存的粒度是按商品 ID 缓存失效策略用写入后过期商品修改后调用删除缓存方法避免缓存和数据库不一致。6.3 打包部署和最终验证清单部署用传统的 jar 方式最稳。执行mvn clean package -DskipTests后在target目录得到可执行 jar用nohup java -jar furniture-mall.jar --server.port8080后台启动。启动日志里看到Started MallApplication后按这个清单做最终回归注册一个新用户、登录、浏览商品列表、加入购物车、提交订单、查看订单列表、后台下一件商品。每一步和文档里的功能描述一一对应全部通过就说明这套源码和文档是真的能交付的版本。这类项目做到最后拼的不是某个炫技功能而是链路完整、数据一致、文档对得上。我每次验收前都会把自己当成第一次用的用户从头点一遍流程这个习惯帮我拦下了不少低级失误。希望这篇笔记能让你少踩几个坑把这个方向做扎实。本文还有配套的精品资源点击获取